From a2d8e0ec42eb9f921c9a355937b6fe73fca1f58e Mon Sep 17 00:00:00 2001 From: Christopher Snowhill Date: Sat, 18 Jun 2022 23:00:08 -0700 Subject: [PATCH] [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 --- Application/PlaybackController.m | 7 ++ Audio/AudioPlayer.h | 3 + Audio/AudioPlayer.m | 14 +++ Audio/Chain/OutputNode.h | 4 + Audio/Chain/OutputNode.m | 21 ++++ Base.lproj/InfoInspector.xib | 100 +++++++++++------- Cog.sdef | 8 +- .../DataModel.xcdatamodel/contents | 10 ++ Playlist/PlaylistController.h | 3 + Playlist/PlaylistController.m | 40 +++++++ Playlist/PlaylistEntry.h | 4 + Playlist/PlaylistEntry.m | 54 ++++++++++ en.lproj/Localizable.strings | 3 + es.lproj/Localizable.strings | 3 + 14 files changed, 234 insertions(+), 40 deletions(-) diff --git a/Application/PlaybackController.m b/Application/PlaybackController.m index 2efa189af..8a39c2544 100644 --- a/Application/PlaybackController.m +++ b/Application/PlaybackController.m @@ -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]]; diff --git a/Audio/AudioPlayer.h b/Audio/AudioPlayer.h index 458a45094..39bdb2770 100644 --- a/Audio/AudioPlayer.h +++ b/Audio/AudioPlayer.h @@ -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 diff --git a/Audio/AudioPlayer.m b/Audio/AudioPlayer.m index baecd4d2a..bf23ab454 100644 --- a/Audio/AudioPlayer.m +++ b/Audio/AudioPlayer.m @@ -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 diff --git a/Audio/Chain/OutputNode.h b/Audio/Chain/OutputNode.h index e414d6c44..032002c2a 100644 --- a/Audio/Chain/OutputNode.h +++ b/Audio/Chain/OutputNode.h @@ -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; diff --git a/Audio/Chain/OutputNode.m b/Audio/Chain/OutputNode.m index 9f4c280d8..ff8bbf05a 100644 --- a/Audio/Chain/OutputNode.m +++ b/Audio/Chain/OutputNode.m @@ -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; } diff --git a/Base.lproj/InfoInspector.xib b/Base.lproj/InfoInspector.xib index 6aac27384..0a19f3a1c 100644 --- a/Base.lproj/InfoInspector.xib +++ b/Base.lproj/InfoInspector.xib @@ -1,8 +1,8 @@ - + - + @@ -15,16 +15,16 @@ - + - + - + - + @@ -33,7 +33,7 @@ - + @@ -42,7 +42,7 @@ - + @@ -51,7 +51,7 @@ - + @@ -60,7 +60,7 @@ - + @@ -69,7 +69,7 @@ - + @@ -78,7 +78,7 @@ - + @@ -87,7 +87,7 @@ - + @@ -96,7 +96,7 @@ - + @@ -105,7 +105,7 @@ - + @@ -114,7 +114,7 @@ - + @@ -123,7 +123,7 @@ - + @@ -135,7 +135,7 @@ - + @@ -147,7 +147,7 @@ - + @@ -159,7 +159,7 @@ - + @@ -171,7 +171,7 @@ - + @@ -183,7 +183,7 @@ - + @@ -195,7 +195,7 @@ - + @@ -207,7 +207,7 @@ - + @@ -219,7 +219,7 @@ - + @@ -231,7 +231,7 @@ - + @@ -243,7 +243,7 @@ - + @@ -255,7 +255,7 @@ - + @@ -264,7 +264,7 @@ - + @@ -276,7 +276,7 @@ - + @@ -285,7 +285,7 @@ - + @@ -297,7 +297,7 @@ - + @@ -306,7 +306,7 @@ - + @@ -318,7 +318,7 @@ - + @@ -327,7 +327,7 @@ - + @@ -340,7 +340,7 @@ - + @@ -349,7 +349,7 @@ - + @@ -375,7 +375,7 @@ - + @@ -384,7 +384,7 @@ - + @@ -395,6 +395,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Cog.sdef b/Cog.sdef index f46b27665..9d9307352 100644 --- a/Cog.sdef +++ b/Cog.sdef @@ -168,6 +168,12 @@ + + + + + + @@ -260,4 +266,4 @@ - \ No newline at end of file + diff --git a/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents b/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents index 57e547d82..5b7b5ee15 100644 --- a/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents +++ b/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents @@ -4,6 +4,15 @@ + + + + + + + + + @@ -54,5 +63,6 @@ + \ No newline at end of file diff --git a/Playlist/PlaylistController.h b/Playlist/PlaylistController.h index 3fce1f833..1b94ea1ea 100644 --- a/Playlist/PlaylistController.h +++ b/Playlist/PlaylistController.h @@ -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; diff --git a/Playlist/PlaylistController.m b/Playlist/PlaylistController.m index 96b586963..c74fd21ca 100644 --- a/Playlist/PlaylistController.m +++ b/Playlist/PlaylistController.m @@ -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); diff --git a/Playlist/PlaylistEntry.h b/Playlist/PlaylistEntry.h index 2a60fa7dc..2363bf870 100644 --- a/Playlist/PlaylistEntry.h +++ b/Playlist/PlaylistEntry.h @@ -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 diff --git a/Playlist/PlaylistEntry.m b/Playlist/PlaylistEntry.m index 08c85838a..3066f8e35 100644 --- a/Playlist/PlaylistEntry.m +++ b/Playlist/PlaylistEntry.m @@ -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 diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 695e4a13e..bb5d55f4a 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -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"; diff --git a/es.lproj/Localizable.strings b/es.lproj/Localizable.strings index 695e4a13e..bb5d55f4a 100644 --- a/es.lproj/Localizable.strings +++ b/es.lproj/Localizable.strings @@ -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";