[Track Info] Add play count tabulation and display

Add play count data collection, including first seen times for every
file first added to the playlist. Data is indexed by album, artist, and
title, or by filename, whichever matches first. Add interfaces to
AppleScript automation definition as well.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
swiftingly
Christopher Snowhill 2022-06-18 23:00:08 -07:00
parent 404d354918
commit a2d8e0ec42
14 changed files with 234 additions and 40 deletions

View File

@ -717,6 +717,13 @@ NSDictionary *makeRGInfo(PlaylistEntry *pe) {
[[NSNotificationCenter defaultCenter] postNotificationName:CogPlaybackDidBeginNotficiation object:pe];
}
- (void)audioPlayer:(AudioPlayer *)player reportPlayCountForTrack:(id)userInfo {
if(userInfo) {
PlaylistEntry *pe = (PlaylistEntry *)userInfo;
[playlistController updatePlayCountForTrack:pe];
}
}
- (void)audioPlayer:(AudioPlayer *)player setError:(NSNumber *)status toTrack:(id)userInfo {
PlaylistEntry *pe = (PlaylistEntry *)userInfo;
[pe setError:[status boolValue]];

View File

@ -77,6 +77,7 @@ using std::atomic_bool;
- (double)volumeDown:(double)amount;
- (double)amountPlayed;
- (double)amountPlayedInterval;
- (void)setNextStream:(NSURL *)url;
- (void)setNextStream:(NSURL *)url withUserInfo:(id)userInfo withRGInfo:(NSDictionary *)rgi;
@ -116,6 +117,7 @@ using std::atomic_bool;
//- (BufferChain *)bufferChain;
- (void)launchOutputThread;
- (void)endOfInputPlayed;
- (void)reportPlayCount;
- (void)sendDelegateMethod:(SEL)selector withVoid:(void *)obj waitUntilDone:(BOOL)wait;
- (void)sendDelegateMethod:(SEL)selector withObject:(id)obj waitUntilDone:(BOOL)wait;
- (void)sendDelegateMethod:(SEL)selector withObject:(id)obj withObject:(id)obj2 waitUntilDone:(BOOL)wait;
@ -134,5 +136,6 @@ using std::atomic_bool;
- (void)audioPlayer:(AudioPlayer *)player sustainHDCD:(id)userInfo;
- (void)audioPlayer:(AudioPlayer *)player restartPlaybackAtCurrentPosition:(id)userInfo;
- (void)audioPlayer:(AudioPlayer *)player pushInfo:(NSDictionary *)info toTrack:(id)userInfo;
- (void)audioPlayer:(AudioPlayer *)player reportPlayCountForTrack:(id)userInfo;
- (void)audioPlayer:(AudioPlayer *)player setError:(NSNumber *)status toTrack:(id)userInfo;
@end

View File

@ -226,6 +226,10 @@
[self sendDelegateMethod:@selector(audioPlayer:pushInfo:toTrack:) withObject:info withObject:userInfo waitUntilDone:NO];
}
- (void)reportPlayCountForTrack:(id)userInfo {
[self sendDelegateMethod:@selector(audioPlayer:reportPlayCountForTrack:) withObject:userInfo waitUntilDone:NO];
}
- (void)setShouldContinue:(BOOL)s {
shouldContinue = s;
@ -240,6 +244,10 @@
return [output amountPlayed];
}
- (double)amountPlayedInterval {
return [output amountPlayedInterval];
}
- (void)launchOutputThread {
initialBufferFilled = YES;
if(outputLaunched == NO && startedPaused == NO) {
@ -409,6 +417,12 @@
return YES;
}
- (void)reportPlayCount {
if(bufferChain) {
[self reportPlayCountForTrack:[bufferChain userInfo]];
}
}
- (void)endOfInputPlayed {
// Once we get here:
// - the buffer chain for the next playlist entry (started in endOfInputReached) have been working for some time

View File

@ -20,10 +20,12 @@
uint32_t config;
double amountPlayed;
double amountPlayedInterval;
OutputCoreAudio *output;
BOOL paused;
BOOL started;
BOOL intervalReported;
}
- (void)beginEqualizer:(AudioUnit)eq;
@ -31,9 +33,11 @@
- (void)endEqualizer:(AudioUnit)eq;
- (double)amountPlayed;
- (double)amountPlayedInterval;
- (void)incrementAmountPlayed:(double)seconds;
- (void)resetAmountPlayed;
- (void)resetAmountPlayedInterval;
- (void)endOfInputPlayed;

View File

@ -17,9 +17,11 @@
- (void)setup {
amountPlayed = 0.0;
amountPlayedInterval = 0.0;
paused = YES;
started = NO;
intervalReported = NO;
output = [[OutputCoreAudio alloc] initWithController:self];
@ -50,14 +52,29 @@
- (void)incrementAmountPlayed:(double)seconds {
amountPlayed += seconds;
amountPlayedInterval += seconds;
if(!intervalReported && amountPlayedInterval >= 60.0) {
intervalReported = YES;
[controller reportPlayCount];
}
}
- (void)resetAmountPlayed {
amountPlayed = 0;
}
- (void)resetAmountPlayedInterval {
amountPlayedInterval = 0;
intervalReported = NO;
}
- (void)endOfInputPlayed {
if(!intervalReported) {
intervalReported = YES;
[controller reportPlayCount];
}
[controller endOfInputPlayed];
[self resetAmountPlayedInterval];
}
- (BOOL)chainQueueHasTracks {
@ -86,6 +103,10 @@
return amountPlayed;
}
- (double)amountPlayedInterval {
return amountPlayedInterval;
}
- (AudioStreamBasicDescription)format {
return format;
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -15,16 +15,16 @@
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Info Inspector" separatorStyle="none" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" visibleAtLaunch="NO" frameAutosaveName="InfoInspector" animationBehavior="default" titlebarAppearsTransparent="YES" id="1" customClass="NSPanel">
<windowStyleMask key="styleMask" titled="YES" closable="YES" resizable="YES" utility="YES" nonactivatingPanel="YES" HUD="YES"/>
<rect key="contentRect" x="700" y="80" width="300" height="560"/>
<rect key="contentRect" x="700" y="80" width="300" height="582"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<value key="minSize" type="size" width="240" height="528"/>
<value key="minSize" type="size" width="240" height="550"/>
<value key="maxSize" type="size" width="400" height="600"/>
<view key="contentView" id="2">
<rect key="frame" x="0.0" y="0.0" width="300" height="560"/>
<rect key="frame" x="0.0" y="0.0" width="300" height="582"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9">
<rect key="frame" x="60" y="504" width="45" height="14"/>
<rect key="frame" x="60" y="526" width="45" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Artist:" id="10">
<font key="font" metaFont="smallSystem"/>
@ -33,7 +33,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="11">
<rect key="frame" x="62" y="482" width="43" height="14"/>
<rect key="frame" x="62" y="504" width="43" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Album:" id="12">
<font key="font" metaFont="smallSystem"/>
@ -42,7 +42,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="13">
<rect key="frame" x="67" y="438" width="38" height="14"/>
<rect key="frame" x="67" y="460" width="38" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Track:" id="14">
<font key="font" metaFont="smallSystem"/>
@ -51,7 +51,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="15">
<rect key="frame" x="60" y="416" width="45" height="14"/>
<rect key="frame" x="60" y="438" width="45" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Length:" id="16">
<font key="font" metaFont="smallSystem"/>
@ -60,7 +60,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="17">
<rect key="frame" x="73" y="394" width="32" height="14"/>
<rect key="frame" x="73" y="416" width="32" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Year:" id="18">
<font key="font" metaFont="smallSystem"/>
@ -69,7 +69,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="19">
<rect key="frame" x="65" y="372" width="40" height="14"/>
<rect key="frame" x="65" y="394" width="40" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Genre:" id="20">
<font key="font" metaFont="smallSystem"/>
@ -78,7 +78,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="21">
<rect key="frame" x="32" y="328" width="73" height="14"/>
<rect key="frame" x="32" y="350" width="73" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Sample Rate:" id="22">
<font key="font" metaFont="smallSystem"/>
@ -87,7 +87,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="27">
<rect key="frame" x="48" y="306" width="57" height="14"/>
<rect key="frame" x="48" y="328" width="57" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Channels:" id="28">
<font key="font" metaFont="smallSystem"/>
@ -96,7 +96,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="29">
<rect key="frame" x="63" y="284" width="42" height="14"/>
<rect key="frame" x="63" y="306" width="42" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Bitrate:" id="32">
<font key="font" metaFont="smallSystem"/>
@ -105,7 +105,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="30">
<rect key="frame" x="15" y="262" width="90" height="14"/>
<rect key="frame" x="15" y="284" width="90" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Bits Per Sample:" id="31">
<font key="font" metaFont="smallSystem"/>
@ -114,7 +114,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="23">
<rect key="frame" x="73" y="460" width="32" height="14"/>
<rect key="frame" x="73" y="482" width="32" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Title:" id="24">
<font key="font" metaFont="smallSystem"/>
@ -123,7 +123,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="33" customClass="ToolTipTextField">
<rect key="frame" x="113" y="504" width="170" height="14"/>
<rect key="frame" x="113" y="526" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="34">
<font key="font" metaFont="smallSystem"/>
@ -135,7 +135,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="35" customClass="ToolTipTextField">
<rect key="frame" x="113" y="482" width="170" height="14"/>
<rect key="frame" x="113" y="504" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="36">
<font key="font" metaFont="smallSystem"/>
@ -147,7 +147,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="37" customClass="ToolTipTextField">
<rect key="frame" x="113" y="460" width="170" height="14"/>
<rect key="frame" x="113" y="482" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="38">
<font key="font" metaFont="smallSystem"/>
@ -159,7 +159,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="39" customClass="ToolTipTextField">
<rect key="frame" x="113" y="438" width="170" height="14"/>
<rect key="frame" x="113" y="460" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="40">
<font key="font" metaFont="smallSystem"/>
@ -171,7 +171,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="41" customClass="ToolTipTextField">
<rect key="frame" x="113" y="416" width="170" height="14"/>
<rect key="frame" x="113" y="438" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="42">
<font key="font" metaFont="smallSystem"/>
@ -183,7 +183,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="43" customClass="ToolTipTextField">
<rect key="frame" x="113" y="394" width="170" height="14"/>
<rect key="frame" x="113" y="416" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="44">
<font key="font" metaFont="smallSystem"/>
@ -195,7 +195,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="45" customClass="ToolTipTextField">
<rect key="frame" x="113" y="372" width="170" height="14"/>
<rect key="frame" x="113" y="394" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="46">
<font key="font" metaFont="smallSystem"/>
@ -207,7 +207,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="49" customClass="ToolTipTextField">
<rect key="frame" x="113" y="328" width="170" height="14"/>
<rect key="frame" x="113" y="350" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="50">
<font key="font" metaFont="smallSystem"/>
@ -219,7 +219,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="51" customClass="ToolTipTextField">
<rect key="frame" x="113" y="306" width="170" height="14"/>
<rect key="frame" x="113" y="328" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="52">
<font key="font" metaFont="smallSystem"/>
@ -231,7 +231,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="53" customClass="ToolTipTextField">
<rect key="frame" x="113" y="284" width="170" height="14"/>
<rect key="frame" x="113" y="306" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="54">
<font key="font" metaFont="smallSystem"/>
@ -243,7 +243,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="55" customClass="ToolTipTextField">
<rect key="frame" x="113" y="262" width="170" height="14"/>
<rect key="frame" x="113" y="284" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="56">
<font key="font" metaFont="smallSystem"/>
@ -255,7 +255,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="QPg-Mb-Urn">
<rect key="frame" x="60" y="240" width="45" height="14"/>
<rect key="frame" x="60" y="262" width="45" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Codec:" id="cbq-TT-CZX">
<font key="font" metaFont="smallSystem"/>
@ -264,7 +264,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ijS-y2-eCZ" customClass="ToolTipTextField">
<rect key="frame" x="113" y="240" width="170" height="14"/>
<rect key="frame" x="113" y="262" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="Yby-OU-cqP">
<font key="font" metaFont="smallSystem"/>
@ -276,7 +276,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="bti-s6-SIU">
<rect key="frame" x="38" y="218" width="67" height="14"/>
<rect key="frame" x="38" y="240" width="67" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Encoding:" id="8e7-lp-K5l">
<font key="font" metaFont="smallSystem"/>
@ -285,7 +285,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="L4f-rE-CN3" customClass="ToolTipTextField">
<rect key="frame" x="113" y="218" width="170" height="14"/>
<rect key="frame" x="113" y="240" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="v14-AG-B9D">
<font key="font" metaFont="smallSystem"/>
@ -297,7 +297,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gn9-9b-eV8">
<rect key="frame" x="15" y="196" width="90" height="14"/>
<rect key="frame" x="15" y="218" width="90" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Cuesheet:" id="Cu5-ia-Z5b">
<font key="font" metaFont="smallSystem"/>
@ -306,7 +306,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WOl-SC-4tu" customClass="ToolTipTextField">
<rect key="frame" x="113" y="196" width="170" height="14"/>
<rect key="frame" x="113" y="218" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="UyE-Mc-e39">
<font key="font" metaFont="smallSystem"/>
@ -318,7 +318,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="arA-Pj-ANg">
<rect key="frame" x="15" y="174" width="90" height="14"/>
<rect key="frame" x="15" y="196" width="90" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="ReplayGain:" id="qBi-M8-kEx">
<font key="font" metaFont="smallSystem"/>
@ -327,7 +327,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2xx-It-i6I" customClass="ToolTipTextField">
<rect key="frame" x="113" y="174" width="170" height="14"/>
<rect key="frame" x="113" y="196" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="rAo-AP-lVa">
<font key="font" metaFont="smallSystem"/>
@ -340,7 +340,7 @@
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="84">
<rect key="frame" x="49" y="350" width="56" height="14"/>
<rect key="frame" x="49" y="372" width="56" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Filename:" id="85">
<font key="font" metaFont="smallSystem"/>
@ -349,7 +349,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="86" customClass="ToolTipTextField">
<rect key="frame" x="113" y="350" width="170" height="14"/>
<rect key="frame" x="113" y="372" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="87">
<font key="font" metaFont="smallSystem"/>
@ -375,7 +375,7 @@
</connections>
</imageView>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vB6-9J-5qg">
<rect key="frame" x="18" y="526" width="87" height="14"/>
<rect key="frame" x="18" y="548" width="87" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Album Artist:" id="LFJ-QQ-gGr">
<font key="font" metaFont="smallSystem"/>
@ -384,7 +384,7 @@
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cj0-Tw-xpq" customClass="ToolTipTextField">
<rect key="frame" x="113" y="526" width="170" height="14"/>
<rect key="frame" x="113" y="548" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="B8w-o8-ZBw">
<font key="font" metaFont="smallSystem"/>
@ -395,6 +395,28 @@
<binding destination="-2" name="value" keyPath="valueToDisplay.albumartist" id="gTS-bf-rHT"/>
</connections>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="FWx-Sc-Ocv">
<rect key="frame" x="15" y="174" width="90" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Play Count:" id="fiv-eh-w3c">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WSs-wC-mWc" customClass="ToolTipTextField">
<rect key="frame" x="113" y="174" width="170" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="N/A" id="Ial-XI-91y">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="-2" name="toolTip" keyPath="valueToDisplay.playCountInfo" id="ydF-ec-fBX"/>
<binding destination="-2" name="value" keyPath="valueToDisplay.playCount" id="Mpe-FA-gRG"/>
</connections>
</textField>
</subviews>
</view>
<point key="canvasLocation" x="136" y="131"/>

View File

@ -168,6 +168,12 @@
<property name="bitrate" code="pBit" description="The bitrate of the entry, in kilobits per second." type="integer" access="r">
<cocoa key="bitrate" insert-at-beginning="yes"/>
</property>
<property name="playcount" code="pPlc" description="The play count of the entry." type="text" access="r">
<cocoa key="playCount" insert-at-beginning="yes"/>
</property>
<property name="playinfo" code="pPli" description="The play count info of the entry, as either first seen time, or also last played time. May be empty string." type="text" access="r">
<cocoa key="playCountInfo" access="r"/>
</property>
<property name="spam" code="pSpm" description="Formatted now playing spam for the entry." type="text" access="r">
<cocoa key="spam"/>
</property>
@ -260,4 +266,4 @@
<command name="previous" code="coreprev" description="Play previous track."/>
<command name="next" code="corenext" description="Play next track."/>
</suite>
</dictionary>
</dictionary>

View File

@ -4,6 +4,15 @@
<attribute name="artData" optional="YES" attributeType="Binary"/>
<attribute name="artHash" optional="YES" attributeType="String"/>
</entity>
<entity name="PlayCount" representedClassName="PlayCount" syncable="YES" codeGenerationType="class">
<attribute name="album" optional="YES" attributeType="String"/>
<attribute name="artist" optional="YES" attributeType="String"/>
<attribute name="count" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="filename" optional="YES" attributeType="String"/>
<attribute name="firstSeen" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lastPlayed" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="title" optional="YES" attributeType="String"/>
</entity>
<entity name="PlaylistEntry" representedClassName="PlaylistEntry" syncable="YES" codeGenerationType="class">
<attribute name="album" optional="YES" attributeType="String"/>
<attribute name="albumartist" optional="YES" attributeType="String"/>
@ -54,5 +63,6 @@
<elements>
<element name="AlbumArtwork" positionX="0" positionY="207" width="128" height="59"/>
<element name="PlaylistEntry" positionX="-36" positionY="9" width="128" height="704"/>
<element name="PlayCount" positionX="-18" positionY="171" width="128" height="134"/>
</elements>
</model>

View File

@ -134,6 +134,9 @@ typedef NS_ENUM(NSInteger, URLOrigin) {
// reload metadata of selection
- (IBAction)reloadTags:(id _Nullable)sender;
// Play statistics
- (void)updatePlayCountForTrack:(PlaylistEntry *)pe;
- (void)moveObjectsInArrangedObjectsFromIndexes:(NSIndexSet *_Nullable)indexSet
toIndex:(NSUInteger)insertIndex;

View File

@ -21,6 +21,8 @@
#import "Logging.h"
#import "Cog-Swift.h"
#define UNDO_STACK_LIMIT 0
@implementation PlaylistController
@ -242,6 +244,40 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc
}
}
- (void)updatePlayCountForTrack:(PlaylistEntry *)pe {
PlayCount *pc = pe.playCountItem;
if(pc) {
pc.count += 1;
pc.lastPlayed = [NSDate date];
} else {
pc = [NSEntityDescription insertNewObjectForEntityForName:@"PlayCount" inManagedObjectContext:self.persistentContainer.viewContext];
pc.count = 1;
pc.firstSeen = pc.lastPlayed = [NSDate date];
pc.album = pe.album;
pc.artist = pe.artist;
pc.title = pe.title;
pc.filename = pe.filename;
}
[self commitEditing];
}
- (void)firstSawTrack:(PlaylistEntry *)pe {
PlayCount *pc = pe.playCountItem;
if(!pc) {
pc = [NSEntityDescription insertNewObjectForEntityForName:@"PlayCount" inManagedObjectContext:self.persistentContainer.viewContext];
pc.count = 0;
pc.firstSeen = [NSDate date];
pc.album = pe.album;
pc.artist = pe.artist;
pc.title = pe.title;
pc.filename = pe.filename;
[self commitEditing];
}
}
- (void)updatePlaylistIndexes {
NSArray *arranged = [self arrangedObjects];
NSUInteger n = [arranged count];
@ -1587,6 +1623,10 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc
- (void)didInsertURLs:(NSArray *)urls origin:(URLOrigin)origin {
if(![urls count]) return;
for(PlaylistEntry *pe in urls) {
[self firstSawTrack:pe];
}
CGEventRef event = CGEventCreate(NULL);
CGEventFlags mods = CGEventGetFlags(event);
CFRelease(event);

View File

@ -66,6 +66,10 @@
@property(nonatomic) BOOL Unsigned;
@property(nonatomic) NSURL *_Nullable URL;
@property(nonatomic) PlayCount *_Nullable playCountItem;
@property(nonatomic, readonly) NSString *_Nonnull playCount;
@property(nonatomic, readonly) NSString *_Nonnull playCountInfo;
- (void)setMetadata:(NSDictionary *_Nonnull)metadata;
@end

View File

@ -493,4 +493,58 @@ NSURL *_Nullable urlForPath(NSString *_Nullable path) {
[self setMetadataLoaded:YES];
}
@dynamic playCountItem;
- (PlayCount *)playCountItem {
NSPredicate *albumPredicate = [NSPredicate predicateWithFormat:@"album == %@", self.album];
NSPredicate *artistPredicate = [NSPredicate predicateWithFormat:@"artist == %@", self.artist];
NSPredicate *titlePredicate = [NSPredicate predicateWithFormat:@"title == %@", self.title];
NSCompoundPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:@[albumPredicate, artistPredicate, titlePredicate]];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"PlayCount"];
request.predicate = predicate;
NSError *error = nil;
NSArray *results = [__persistentContainer.viewContext executeFetchRequest:request error:&error];
if(!results || [results count] < 1) {
NSPredicate *filenamePredicate = [NSPredicate predicateWithFormat:@"filename == %@", self.filename];
request = [NSFetchRequest fetchRequestWithEntityName:@"PlayCount"];
request.predicate = filenamePredicate;
results = [__persistentContainer.viewContext executeFetchRequest:request error:&error];
}
if(!results || [results count] < 1) return nil;
return results[0];
}
@dynamic playCount;
- (NSString *)playCount {
PlayCount *pc = self.playCountItem;
if(pc)
return [NSString stringWithFormat:@"%llu", pc.count];
else
return @"0";
}
@dynamic playCountInfo;
- (NSString *)playCountInfo {
PlayCount *pc = self.playCountItem;
if(pc) {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateStyle = NSDateFormatterMediumStyle;
dateFormatter.timeStyle = NSDateFormatterShortStyle;
if(pc.count) {
return [NSString stringWithFormat:@"%@: %@\n%@: %@", NSLocalizedStringFromTableInBundle(@"TimeFirstSeen", nil, [NSBundle bundleForClass:[self class]], @""), [dateFormatter stringFromDate:pc.firstSeen], NSLocalizedStringFromTableInBundle(@"TimeLastPlayed", nil, [NSBundle bundleForClass:[self class]], @""), [dateFormatter stringFromDate:pc.firstSeen]];
} else {
return [NSString stringWithFormat:@"%@: %@", NSLocalizedStringFromTableInBundle(@"TimeFirstSeen", nil, [NSBundle bundleForClass:[self class]], @""), [dateFormatter stringFromDate:pc.firstSeen]];
}
}
return @"";
}
@end

View File

@ -62,3 +62,6 @@
"ErrorInvalidTrackId" = "Invalid track ID sent to SQLite request.";
"ErrorSqliteProblem" = "General problem accessing track from SQLite database.";
"ErrorTrackMissing" = "Track entry is missing from SQLite database.";
"TimeLastPlayed" = "Last played";
"TimeFirstSeen" = "First seen";

View File

@ -62,3 +62,6 @@
"ErrorInvalidTrackId" = "Invalid track ID sent to SQLite request.";
"ErrorSqliteProblem" = "General problem accessing track from SQLite database.";
"ErrorTrackMissing" = "Track entry is missing from SQLite database.";
"TimeLastPlayed" = "Last played";
"TimeFirstSeen" = "First seen";