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";