cog/Application/PlaybackEventController.m

398 lines
13 KiB
Objective-C

//
// PlaybackEventController.m
// Cog
//
// Created by Vincent Spader on 3/5/09.
// Copyright 2009 __MyCompanyName__. All rights reserved.
#import "PlaybackEventController.h"
#import "AudioScrobbler.h"
#import "PlaylistEntry.h"
NSString *TrackNotification = @"com.apple.iTunes.playerInfo";
NSString *TrackArtist = @"Artist";
NSString *TrackAlbum = @"Album";
NSString *TrackTitle = @"Name";
NSString *TrackGenre = @"Genre";
NSString *TrackNumber = @"Track Number";
NSString *TrackLength = @"Total Time";
NSString *TrackPath = @"Location";
NSString *TrackState = @"Player State";
typedef NS_ENUM(NSInteger, TrackStatus) { TrackPlaying,
TrackPaused,
TrackStopped };
@implementation PlaybackEventController {
AudioScrobbler *scrobbler;
NSOperationQueue *queue;
PlaylistEntry *entry;
Boolean didGainUN API_AVAILABLE(macosx(10.14));
}
- (void)initDefaults {
NSDictionary *defaultsDictionary = @{
@"enableAudioScrobbler": @YES,
@"automaticallyLaunchLastFM": @NO,
@"notifications.enable": @YES,
@"notifications.itunes-style": @YES,
@"notifications.show-album-art": @YES
};
[[NSUserDefaults standardUserDefaults] registerDefaults:defaultsDictionary];
}
- (id)init {
self = [super init];
if(self) {
[self initDefaults];
didGainUN = NO;
if(@available(macOS 10.14, *)) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center
requestAuthorizationWithOptions:UNAuthorizationOptionAlert
completionHandler:^(BOOL granted, NSError *_Nullable error) {
self->didGainUN = granted;
if(granted) {
UNNotificationAction *skipAction = [UNNotificationAction
actionWithIdentifier:@"skip"
title:@"Skip"
options:UNNotificationActionOptionNone];
UNNotificationCategory *playCategory = [UNNotificationCategory
categoryWithIdentifier:@"play"
actions:@[skipAction]
intentIdentifiers:@[]
options:UNNotificationCategoryOptionNone];
[center setNotificationCategories:
[NSSet setWithObject:playCategory]];
}
}];
[center setDelegate:self];
}
queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:1];
scrobbler = [[AudioScrobbler alloc] init];
[[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self];
entry = nil;
}
return self;
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:
(void (^)(UNNotificationPresentationOptions options))completionHandler
API_AVAILABLE(macos(10.14)) {
UNNotificationPresentationOptions presentationOptions = UNNotificationPresentationOptionAlert;
completionHandler(presentationOptions);
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler API_AVAILABLE(macos(10.14)) {
if([[response actionIdentifier] isEqualToString:@"skip"]) {
[playbackController next:self];
}
}
- (NSDictionary *)fillNotificationDictionary:(PlaylistEntry *)pe status:(TrackStatus)status {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
if(pe == nil || pe.deLeted || pe.url == nil) return dict;
[dict setObject:[pe.url absoluteString] forKey:TrackPath];
if(pe.title) [dict setObject:pe.title forKey:TrackTitle];
if(pe.artist) [dict setObject:pe.artist forKey:TrackArtist];
if(pe.album) [dict setObject:pe.album forKey:TrackAlbum];
if(pe.genre) [dict setObject:pe.genre forKey:TrackGenre];
if(pe.track)
[dict setObject:pe.trackText forKey:TrackNumber];
if(pe.length)
[dict setObject:@((NSInteger)([pe.length doubleValue] * 1000.0))
forKey:TrackLength];
NSString *state = nil;
switch(status) {
case TrackPlaying:
state = @"Playing";
break;
case TrackPaused:
state = @"Paused";
break;
case TrackStopped:
state = @"Stopped";
break;
default:
break;
}
[dict setObject:state forKey:TrackState];
return dict;
}
- (void)performPlaybackDidBeginActions:(PlaylistEntry *)pe {
if(NO == [pe error]) {
entry = pe;
[[NSDistributedNotificationCenter defaultCenter]
postNotificationName:TrackNotification
object:nil
userInfo:[self fillNotificationDictionary:pe status:TrackPlaying]
deliverImmediately:YES];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if([defaults boolForKey:@"notifications.enable"]) {
if([defaults boolForKey:@"enableAudioScrobbler"]) {
[scrobbler start:pe];
if([AudioScrobbler isRunning]) return;
}
if(@available(macOS 10.14, *)) {
if(didGainUN) {
UNUserNotificationCenter *center =
[UNUserNotificationCenter currentNotificationCenter];
UNMutableNotificationContent *content =
[[UNMutableNotificationContent alloc] init];
content.title = @"Now Playing";
NSString *subtitle;
NSString *artist = (pe.artist && [pe.artist length]) ? pe.artist : nil;
NSString *album = (pe.album && [pe.album length]) ? pe.album : nil;
if(artist && album) {
subtitle = [NSString stringWithFormat:@"%@ - %@", artist, album];
} else if(artist) {
subtitle = artist;
} else if(album) {
subtitle = album;
} else {
subtitle = @"";
}
NSString *body = [NSString stringWithFormat:@"%@\n%@", [pe title], subtitle];
content.body = body;
content.sound = nil;
content.categoryIdentifier = @"play";
if([defaults boolForKey:@"notifications.show-album-art"] &&
[pe albumArt]) {
NSError *error = nil;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *tmpSubFolderURL = [[NSURL fileURLWithPath:NSTemporaryDirectory()]
URLByAppendingPathComponent:@"cog-artworks-cache"
isDirectory:true];
if([fileManager createDirectoryAtPath:[tmpSubFolderURL path]
withIntermediateDirectories:true
attributes:nil
error:&error]) {
NSString *tmpFileName =
[[NSProcessInfo.processInfo globallyUniqueString]
stringByAppendingString:@".jpg"];
NSURL *fileURL =
[tmpSubFolderURL URLByAppendingPathComponent:tmpFileName];
NSImage *image = [pe albumArt];
CGImageRef cgRef = [image CGImageForProposedRect:NULL
context:nil
hints:nil];
if(cgRef) {
NSBitmapImageRep *newRep =
[[NSBitmapImageRep alloc] initWithCGImage:cgRef];
NSData *jpgData = [newRep
representationUsingType:NSBitmapImageFileTypeJPEG
properties:@{ NSImageCompressionFactor: @0.5f }];
[jpgData writeToURL:fileURL atomically:YES];
UNNotificationAttachment *icon =
[UNNotificationAttachment attachmentWithIdentifier:@"art"
URL:fileURL
options:nil
error:&error];
if(error) {
// We have size limit of 10MB per image attachment.
NSLog(@"%@", error.localizedDescription);
} else {
content.attachments = @[icon];
}
}
}
}
UNNotificationRequest *request =
[UNNotificationRequest requestWithIdentifier:@"PlayTrack"
content:content
trigger:nil];
[center addNotificationRequest:request
withCompletionHandler:^(NSError *_Nullable error) {
NSLog(@"%@", error.localizedDescription);
}];
}
} else {
NSUserNotification *notif = [[NSUserNotification alloc] init];
notif.title = [pe title];
NSString *subtitle;
NSString *artist = (pe.artist && [pe.artist length]) ? pe.artist : nil;
NSString *album = (pe.album && [pe.album length]) ? pe.album : nil;
if(artist && album) {
subtitle = [NSString stringWithFormat:@"%@ - %@", artist, album];
} else if(artist) {
subtitle = artist;
} else if(album) {
subtitle = album;
} else {
subtitle = @"";
}
if([defaults boolForKey:@"notifications.itunes-style"]) {
notif.subtitle = subtitle;
[notif setValue:@YES forKey:@"_showsButtons"];
} else {
notif.informativeText = subtitle;
}
if([notif respondsToSelector:@selector(setContentImage:)]) {
if([defaults boolForKey:@"notifications.show-album-art"] &&
[pe albumArtInternal]) {
NSImage *image = [pe albumArt];
if([defaults boolForKey:@"notifications.itunes-style"]) {
[notif setValue:image forKey:@"_identityImage"];
} else {
notif.contentImage = image;
}
}
}
notif.actionButtonTitle = NSLocalizedString(@"SkipAction", @"");
[[NSUserNotificationCenter defaultUserNotificationCenter]
scheduleNotification:notif];
}
}
}
}
- (void)performPlaybackDidPauseActions {
[[NSDistributedNotificationCenter defaultCenter]
postNotificationName:TrackNotification
object:nil
userInfo:[self fillNotificationDictionary:entry status:TrackPaused]
deliverImmediately:YES];
if([[NSUserDefaults standardUserDefaults] boolForKey:@"enableAudioScrobbler"]) {
[scrobbler pause];
}
}
- (void)performPlaybackDidResumeActions {
[[NSDistributedNotificationCenter defaultCenter]
postNotificationName:TrackNotification
object:nil
userInfo:[self fillNotificationDictionary:entry status:TrackPlaying]
deliverImmediately:YES];
if([[NSUserDefaults standardUserDefaults] boolForKey:@"enableAudioScrobbler"]) {
[scrobbler resume];
}
}
- (void)performPlaybackDidStopActions {
[[NSDistributedNotificationCenter defaultCenter]
postNotificationName:TrackNotification
object:nil
userInfo:[self fillNotificationDictionary:entry status:TrackStopped]
deliverImmediately:YES];
entry = nil;
if([[NSUserDefaults standardUserDefaults] boolForKey:@"enableAudioScrobbler"]) {
[scrobbler stop];
}
}
- (void)awakeFromNib {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playbackDidBegin:)
name:CogPlaybackDidBeginNotficiation
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playbackDidPause:)
name:CogPlaybackDidPauseNotficiation
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playbackDidResume:)
name:CogPlaybackDidResumeNotficiation
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playbackDidStop:)
name:CogPlaybackDidStopNotficiation
object:nil];
}
- (void)playbackDidBegin:(NSNotification *)notification {
NSOperation *op = [NSBlockOperation blockOperationWithBlock:^{
[self performPlaybackDidBeginActions:(PlaylistEntry *)[notification object]];
}];
[queue addOperation:op];
}
- (void)playbackDidPause:(NSNotification *)notification {
NSOperation *op = [NSBlockOperation blockOperationWithBlock:^{
[self performPlaybackDidPauseActions];
}];
[queue addOperation:op];
}
- (void)playbackDidResume:(NSNotification *)notification {
NSOperation *op = [NSBlockOperation blockOperationWithBlock:^{
[self performPlaybackDidResumeActions];
}];
[queue addOperation:op];
}
- (void)playbackDidStop:(NSNotification *)notification {
NSOperation *op = [NSBlockOperation blockOperationWithBlock:^{
[self performPlaybackDidStopActions];
}];
[queue addOperation:op];
}
- (void)userNotificationCenter:(NSUserNotificationCenter *)center
didActivateNotification:(NSUserNotification *)notification {
switch(notification.activationType) {
case NSUserNotificationActivationTypeActionButtonClicked:
[playbackController next:self];
break;
case NSUserNotificationActivationTypeContentsClicked: {
NSWindow *window = [[NSUserDefaults standardUserDefaults] boolForKey:@"miniMode"] ? miniWindow : mainWindow;
[NSApp activateIgnoringOtherApps:YES];
[window makeKeyAndOrderFront:self];
}; break;
default:
break;
}
}
@end