Cog/Playlist/PlaylistController.m
Christopher Snowhill 4bed0af868 Core Data Store: Handle concurrency properly
All concurrency from other threads should pass through the viewContext's
performBlock or performBlockAndWait functions, and no other way. So now,
all access to Core Data is either happening on the main thread, or by
using these code blocks, all of which will wait for their access to
proceed.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-10-30 16:54:52 -07:00

1953 lines
61 KiB
Objective-C

//
// PlaylistController.m
// Cog
//
// Created by Vincent Spader on 3/18/05.
// Copyright 2005 Vincent Spader All rights reserved.
//
#import "PlaylistController.h"
#import "PlaybackController.h"
#import "PlaylistEntry.h"
#import "PlaylistLoader.h"
#import "RepeatTransformers.h"
#import "Shuffle.h"
#import "ShuffleTransformers.h"
#import "SpotlightWindowController.h"
#import "StatusImageTransformer.h"
#import "ToggleQueueTitleTransformer.h"
#import "NSString+CogSort.h"
#import "Logging.h"
#import "Cog-Swift.h"
#import "AppController.h"
#import "SandboxBroker.h"
#define UNDO_STACK_LIMIT 0
extern BOOL kAppControllerShuttingDown;
NSPersistentContainer *kPersistentContainer = nil;
@implementation PlaylistController
@synthesize currentEntry;
@synthesize totalTime;
@synthesize currentStatus;
static NSArray *cellIdentifiers = nil;
NSMutableDictionary<NSString *, AlbumArtwork *> *kArtworkDictionary = nil;
static void *playlistControllerContext = &playlistControllerContext;
+ (void)initialize {
cellIdentifiers = @[@"index", @"status", @"title", @"albumartist", @"artist",
@"album", @"length", @"year", @"genre", @"track", @"path",
@"filename", @"codec", @"rating", @"samplerate",
@"bitspersample"];
NSValueTransformer *repeatNoneTransformer =
[[RepeatModeTransformer alloc] initWithMode:RepeatModeNoRepeat];
[NSValueTransformer setValueTransformer:repeatNoneTransformer forName:@"RepeatNoneTransformer"];
NSValueTransformer *repeatOneTransformer =
[[RepeatModeTransformer alloc] initWithMode:RepeatModeRepeatOne];
[NSValueTransformer setValueTransformer:repeatOneTransformer forName:@"RepeatOneTransformer"];
NSValueTransformer *repeatAlbumTransformer =
[[RepeatModeTransformer alloc] initWithMode:RepeatModeRepeatAlbum];
[NSValueTransformer setValueTransformer:repeatAlbumTransformer
forName:@"RepeatAlbumTransformer"];
NSValueTransformer *repeatAllTransformer =
[[RepeatModeTransformer alloc] initWithMode:RepeatModeRepeatAll];
[NSValueTransformer setValueTransformer:repeatAllTransformer forName:@"RepeatAllTransformer"];
NSValueTransformer *repeatModeImageTransformer = [[RepeatModeImageTransformer alloc] init];
[NSValueTransformer setValueTransformer:repeatModeImageTransformer
forName:@"RepeatModeImageTransformer"];
NSValueTransformer *shuffleOffTransformer =
[[ShuffleModeTransformer alloc] initWithMode:ShuffleOff];
[NSValueTransformer setValueTransformer:shuffleOffTransformer forName:@"ShuffleOffTransformer"];
NSValueTransformer *shuffleAlbumsTransformer =
[[ShuffleModeTransformer alloc] initWithMode:ShuffleAlbums];
[NSValueTransformer setValueTransformer:shuffleAlbumsTransformer
forName:@"ShuffleAlbumsTransformer"];
NSValueTransformer *shuffleAllTransformer =
[[ShuffleModeTransformer alloc] initWithMode:ShuffleAll];
[NSValueTransformer setValueTransformer:shuffleAllTransformer forName:@"ShuffleAllTransformer"];
NSValueTransformer *shuffleImageTransformer = [[ShuffleImageTransformer alloc] init];
[NSValueTransformer setValueTransformer:shuffleImageTransformer
forName:@"ShuffleImageTransformer"];
NSValueTransformer *statusImageTransformer = [[StatusImageTransformer alloc] init];
[NSValueTransformer setValueTransformer:statusImageTransformer
forName:@"StatusImageTransformer"];
NSValueTransformer *toggleQueueTitleTransformer = [[ToggleQueueTitleTransformer alloc] init];
[NSValueTransformer setValueTransformer:toggleQueueTitleTransformer
forName:@"ToggleQueueTitleTransformer"];
}
- (void)initDefaults {
NSDictionary *defaultsDictionary = @{ @"repeat": @(RepeatModeNoRepeat), @"shuffle": @(ShuffleOff) };
[[NSUserDefaults standardUserDefaults] registerDefaults:defaultsDictionary];
}
- (id)initWithCoder:(NSCoder *)decoder {
self = [super initWithCoder:decoder];
if(!self) return nil;
shuffleList = [[NSMutableArray alloc] init];
queueList = [[NSMutableArray alloc] init];
undoManager = [[NSUndoManager alloc] init];
[undoManager setLevelsOfUndo:UNDO_STACK_LIMIT];
[self initDefaults];
_persistentContainer = [[NSPersistentContainer alloc] initWithName:@"DataModel"];
[self.persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *description, NSError *error) {
if(error != nil) {
ALog(@"Failed to load Core Data stack: %@", error);
abort();
}
}];
kPersistentContainer = self.persistentContainer;
self.persistentContainer.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
_persistentArtStorage = [[NSMutableDictionary alloc] init];
kArtworkDictionary = self.persistentArtStorage;
return self;
}
+ (NSPersistentContainer *)sharedPersistentContainer {
return kPersistentContainer;
}
- (void)awakeFromNib {
[super awakeFromNib];
statusImageTransformer = [NSValueTransformer valueTransformerForName:@"StatusImageTransformer"];
numberHertzToStringTransformer = [NSValueTransformer valueTransformerForName:@"NumberHertzToStringTransformer"];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"index" ascending:YES];
[self.tableView setSortDescriptors:@[sortDescriptor]];
[self addObserver:self
forKeyPath:@"arrangedObjects"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:playlistControllerContext];
[playbackController addObserver:self
forKeyPath:@"progressOverall"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionOld)
context:playlistControllerContext];
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.fontSize" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial) context:playlistControllerContext];
observersRegistered = YES;
[self.tableView setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
}
- (void)deinit {
if(observersRegistered) {
[self removeObserver:self forKeyPath:@"arrangedObjects" context:playlistControllerContext];
[playbackController removeObserver:self forKeyPath:@"progressOverall" context:playlistControllerContext];
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.fontSize" context:playlistControllerContext];
}
}
- (void)startObservingProgress:(NSProgress *)progress {
[progress addObserver:self forKeyPath:@"localizedDescription" options:0 context:playlistControllerContext];
[progress addObserver:self forKeyPath:@"fractionCompleted" options:0 context:playlistControllerContext];
}
- (void)stopObservingProgress:(NSProgress *)progress {
[progress removeObserver:self forKeyPath:@"localizedDescription" context:playlistControllerContext];
[progress removeObserver:self forKeyPath:@"fractionCompleted" context:playlistControllerContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if(context == playlistControllerContext) {
if([keyPath isEqualToString:@"arrangedObjects"]) {
[self updatePlaylistIndexes];
[self updateTotalTime];
[self.tableView reloadData];
} else if([keyPath isEqualToString:@"values.fontSize"]) {
[self updateRowSize];
} else if([keyPath isEqualToString:@"progressOverall"]) {
id objNew = [change objectForKey:NSKeyValueChangeNewKey];
id objOld = [change objectForKey:NSKeyValueChangeOldKey];
NSProgress *progressNew = nil, *progressOld = nil;
if(objNew && [objNew isKindOfClass:[NSProgress class]])
progressNew = (NSProgress *)objNew;
if(objOld && [objOld isKindOfClass:[NSProgress class]])
progressOld = (NSProgress *)objOld;
if(progressOld) {
[self stopObservingProgress:progressOld];
}
if(progressNew) {
[self startObservingProgress:progressNew];
}
[self updateProgressText];
} else if([keyPath isEqualToString:@"localizedDescription"] ||
[keyPath isEqualToString:@"fractionCompleted"]) {
[self updateProgressText];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)updateProgressText {
NSString *description = nil;
if(playbackController.progressOverall) {
if(playbackController.progressJob) {
description = [NSString stringWithFormat:@"%@ - %@", playbackController.progressOverall.localizedDescription, playbackController.progressJob.localizedDescription];
} else {
description = playbackController.progressOverall.localizedDescription;
}
}
if(description) {
[self setTotalTime:nil];
description = [description stringByAppendingFormat:@" - %.2f%% complete", playbackController.progressOverall.fractionCompleted * 100.0];
[self setCurrentStatus:description];
} else {
[self setCurrentStatus:nil];
[self updateTotalTime];
}
}
- (void)commitPersistentStore {
[self.persistentContainer.viewContext performBlockAndWait:^{
NSError *error = nil;
[self.persistentContainer.viewContext save:&error];
if(error) {
ALog(@"Error committing playlist storage: %@", [error localizedDescription]);
}
}];
}
- (void)updatePlayCountForTrack:(PlaylistEntry *)pe {
if(pe.countAdded) return;
pe.countAdded = YES;
__block PlayCount *pc = pe.playCountItem;
if(pc) {
[self.persistentContainer.viewContext performBlockAndWait:^{
pc.count += 1;
pc.lastPlayed = [NSDate date];
}];
} else {
[self.persistentContainer.viewContext performBlockAndWait:^{
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.filenameFragment;
}];
}
[self commitPersistentStore];
}
- (void)firstSawTrack:(PlaylistEntry *)pe {
__block PlayCount *pc = pe.playCountItem;
if(!pc) {
[self.persistentContainer.viewContext performBlockAndWait:^{
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.filenameFragment;
}];
}
}
- (void)ratingUpdatedWithEntry:(PlaylistEntry *)pe rating:(CGFloat)rating {
if(pe && !pe.deLeted) {
__block PlayCount *pc = pe.playCountItem;
if(!pc) {
[self.persistentContainer.viewContext performBlockAndWait:^{
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.filenameFragment;
}];
}
[self.persistentContainer.viewContext performBlockAndWait:^{
pc.rating = rating;
}];
[self commitPersistentStore];
}
}
- (void)resetPlayCountForTrack:(PlaylistEntry *)pe {
PlayCount *pc = pe.playCountItem;
if(pc) {
[self.persistentContainer.viewContext performBlockAndWait:^{
pc.count = 0;
}];
}
}
- (void)removeRatingForTrack:(PlaylistEntry *)pe {
PlayCount *pc = pe.playCountItem;
if(pc) {
[self.persistentContainer.viewContext performBlockAndWait:^{
pc.rating = 0;
}];
}
}
- (void)updatePlaylistIndexes {
NSArray *arranged = [self arrangedObjects];
NSUInteger n = [arranged count];
BOOL updated = NO;
for(NSUInteger i = 0; i < n; i++) {
PlaylistEntry *pe = arranged[i];
if(pe.index != i) { // Make sure we don't get into some kind of crazy observing loop...
pe.index = i;
updated = YES;
}
}
if(updated) {
[self commitPersistentStore];
}
}
- (void)updateTotalTime {
double tt = 0;
ldiv_t hoursAndMinutes;
ldiv_t daysAndHours;
ldiv_t weeksAndDays;
for(PlaylistEntry *pe in [self arrangedObjects]) {
if(!isnan([pe.length doubleValue])) tt += [pe.length doubleValue];
}
long sec = (long)(tt);
hoursAndMinutes = ldiv(sec / 60, 60);
NSString *week = NSLocalizedString(@"%1d week(s)", @"week for total");
NSString *day = NSLocalizedString(@"%1d day(s)", @"day for total");
NSString *hour = NSLocalizedString(@"%1d hour(s)", @"hour for total");
NSString *min = NSLocalizedString(@"%1d minute(s)", @"minute for total");
NSString *second = NSLocalizedString(@"%1d second(s)", @"second for total");
if(hoursAndMinutes.quot >= 24) {
daysAndHours = ldiv(hoursAndMinutes.quot, 24);
if(daysAndHours.quot >= 7) {
weeksAndDays = ldiv(daysAndHours.quot, 7);
NSString *weekCount = [NSString localizedStringWithFormat:week, weeksAndDays.quot];
NSString *dayCount = [NSString localizedStringWithFormat:day, weeksAndDays.rem];
NSString *hourCount = [NSString localizedStringWithFormat:hour, daysAndHours.rem];
NSString *minuteCount = [NSString localizedStringWithFormat:min, hoursAndMinutes.rem];
NSString *secCount = [NSString localizedStringWithFormat:second, sec % 60];
[self setTotalTime:[NSString stringWithFormat:
NSLocalizedString(@"wdhms", "weeks, days, hours, minutes and seconds"),
weekCount, dayCount, hourCount, minuteCount, secCount]];
} else {
NSString *dayCount = [NSString localizedStringWithFormat:day, daysAndHours.quot];
NSString *hourCount = [NSString localizedStringWithFormat:hour, daysAndHours.rem];
NSString *minuteCount = [NSString localizedStringWithFormat:min, hoursAndMinutes.rem];
NSString *secCount = [NSString localizedStringWithFormat:second, sec % 60];
[self setTotalTime:[NSString stringWithFormat:
NSLocalizedString(@"dhms", "days, hours, minutes and seconds"),
dayCount, hourCount, minuteCount, secCount]];
}
} else {
if(hoursAndMinutes.quot > 0) {
NSString *hourCount = [NSString localizedStringWithFormat:hour, hoursAndMinutes.quot];
NSString *minuteCount = [NSString localizedStringWithFormat:min, hoursAndMinutes.rem];
NSString *secCount = [NSString localizedStringWithFormat:second, sec % 60];
[self setTotalTime:[NSString stringWithFormat:
NSLocalizedString(@"hms", "hours, minutes and seconds"),
hourCount, minuteCount, secCount]];
} else {
if(hoursAndMinutes.rem > 0) {
NSString *minuteCount = [NSString localizedStringWithFormat:min, hoursAndMinutes.rem];
NSString *secCount = [NSString localizedStringWithFormat:second, sec % 60];
[self setTotalTime:[NSString stringWithFormat:
NSLocalizedString(@"ms", "minutes and seconds"),
minuteCount, secCount]];
} else {
NSString *secCount = [NSString localizedStringWithFormat:second, sec];
[self setTotalTime:secCount];
}
}
}
}
- (NSView *_Nullable)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *_Nullable)tableColumn row:(NSInteger)row {
NSImage *cellImage = nil;
NSString *cellText = @"";
NSString *cellIdentifier = @"";
NSTextAlignment cellTextAlignment = NSTextAlignmentLeft;
PlaylistEntry *pe = [[self arrangedObjects] objectAtIndex:row];
float fontSize = [[[NSUserDefaultsController sharedUserDefaultsController] defaults] floatForKey:@"fontSize"];
BOOL cellRating = NO;
if(pe) {
cellIdentifier = [tableColumn identifier];
NSUInteger index = [cellIdentifiers indexOfObject:cellIdentifier];
switch(index) {
case 0:
cellText = [NSString stringWithFormat:@"%lld", pe.index + 1];
cellTextAlignment = NSTextAlignmentRight;
break;
case 1:
cellImage = [statusImageTransformer transformedValue:pe.status];
break;
case 2:
if([pe title]) cellText = pe.title;
break;
case 3:
if([pe albumartist]) cellText = pe.albumartist;
break;
case 4:
if([pe artist]) cellText = pe.artist;
break;
case 5:
if([pe album]) cellText = pe.album;
break;
case 6:
cellText = pe.lengthText;
cellTextAlignment = NSTextAlignmentRight;
break;
case 7:
if([pe year]) cellText = pe.yearText;
cellTextAlignment = NSTextAlignmentRight;
break;
case 8:
if([pe genre]) cellText = pe.genre;
break;
case 9:
if([pe track]) cellText = pe.trackText;
cellTextAlignment = NSTextAlignmentRight;
break;
case 10:
if([pe path]) cellText = pe.path;
break;
case 11:
if([pe filename]) cellText = pe.filename;
break;
case 12:
if([pe codec]) cellText = pe.codec;
break;
case 13:
{
NSString *filledStar = @"";
NSString *emptyStar = @"";
NSUInteger rating = (NSUInteger)ceil(pe.rating);
if(rating < 0)
rating = 0;
else if(rating > 5)
rating = 5;
cellText = [@"" stringByPaddingToLength:rating withString:filledStar startingAtIndex:0];
cellText = [cellText stringByPaddingToLength:5 withString:emptyStar startingAtIndex:0];
cellRating = YES;
break;
}
case 14:
cellText = [numberHertzToStringTransformer transformedValue:@(pe.sampleRate)];
cellTextAlignment = NSTextAlignmentRight;
break;
case 15:
cellText = [NSString stringWithFormat:@"%u", pe.bitsPerSample];
cellTextAlignment = NSTextAlignmentRight;
break;
}
}
NSString *cellTextTruncated = cellText;
if([cellTextTruncated length] > 1024) {
cellTextTruncated = [cellTextTruncated substringToIndex:1023];
cellTextTruncated = [cellTextTruncated stringByAppendingString:@""];
}
NSView *view = [tableView makeViewWithIdentifier:cellIdentifier owner:nil];
if(view && [view isKindOfClass:[NSTableCellView class]]) {
NSTableCellView *cellView = (NSTableCellView *)view;
NSRect frameRect = cellView.frame;
frameRect.origin.y = 1;
frameRect.size.height = tableView.rowHeight;
cellView.frame = frameRect;
if(cellView.textField) {
cellView.textField.allowsDefaultTighteningForTruncation = YES;
NSFont *font = [NSFont monospacedDigitSystemFontOfSize:fontSize weight:NSFontWeightRegular];
cellView.textField.font = font;
cellView.textField.stringValue = cellTextTruncated ?: @"";
cellView.textField.alignment = cellTextAlignment;
if(cellView.textField.intrinsicContentSize.width > cellView.textField.frame.size.width - 4)
cellView.textField.toolTip = cellTextTruncated ?: @"";
else
cellView.textField.toolTip = [pe statusMessage];
NSRect cellFrameRect = cellView.textField.frame;
cellFrameRect.origin.y = 1;
cellFrameRect.size.height = frameRect.size.height;
cellView.textField.frame = cellFrameRect;
}
if(cellView.imageView) {
cellView.imageView.image = cellImage;
cellView.imageView.toolTip = [pe statusMessage];
NSRect cellFrameRect = cellView.imageView.frame;
cellFrameRect.size.height = frameRect.size.height * 14.0 / 18.0;
cellFrameRect.origin.y = (frameRect.size.height - cellFrameRect.size.height) * 0.5;
cellView.imageView.frame = cellFrameRect;
}
cellView.rowSizeStyle = NSTableViewRowSizeStyleCustom;
}
return view;
}
- (void)tableView:(NSTableView *)view didClickRow:(NSInteger)clickedRow column:(NSInteger)clickedColumn atPoint:(NSPoint)cellPoint {
NSTableColumn *column = [view tableColumns][clickedColumn];
NSString *cellIdentifier = [column identifier];
NSUInteger index = [cellIdentifiers indexOfObject:cellIdentifier];
if(index == 13) {
NSInteger rating = ((CGFloat)ceil(cellPoint.x * 5.0 / 64.0));
if(rating < 1) rating = 1;
else if(rating > 5) rating = 5;
PlaylistEntry *pe = [[self arrangedObjects] objectAtIndex:clickedRow];
[self ratingUpdatedWithEntry:pe rating:rating];
NSIndexSet *refreshRow = [NSIndexSet indexSetWithIndex:clickedRow];
NSIndexSet *refreshColumn = [NSIndexSet indexSetWithIndex:clickedColumn];
[self.tableView reloadDataForRowIndexes:refreshRow columnIndexes:refreshColumn];
}
}
- (void)updateRowSize {
[self.tableView reloadData];
if(currentEntry != nil) [self.tableView scrollRowToVisible:currentEntry.index];
}
- (void)updateNextAfterDeleted:(PlaylistEntry *)lastEntry withDeleteIndexes:(NSIndexSet *)indexes {
__block PlaylistEntry *pe = nil;
NSArray *allObjects = [self arrangedObjects];
[indexes enumerateRangesUsingBlock:^(NSRange range, BOOL *_Nonnull stop) {
if(range.location <= lastEntry.index &&
range.location + range.length > lastEntry.index) {
NSUInteger index = range.location + range.length;
if(index < [allObjects count])
pe = [allObjects objectAtIndex:index];
else
pe = nil;
} else if(pe && range.location <= [pe index] &&
range.location + range.length > [pe index]) {
NSUInteger index = range.location + range.length;
if(index < [allObjects count])
pe = [allObjects objectAtIndex:index];
else
pe = nil;
} else if(pe && range.location > [pe index]) {
*stop = YES;
}
}];
nextEntryAfterDeleted = pe;
}
- (void)tableView:(NSTableView *)tableView didClickTableColumn:(NSTableColumn *)tableColumn {
if([self shuffle] != ShuffleOff) [self resetShuffleList];
NSUInteger index = [cellIdentifiers indexOfObject:[tableColumn identifier]];
NSSortDescriptor *sortDescriptor;/* = [tableColumn sortDescriptorPrototype];*/
NSArray *sortDescriptors;/* = sortDescriptor ? @[sortDescriptor] : @[];*/
BOOL ascending;
NSImage *indicatorImage = [tableView indicatorImageInTableColumn:tableColumn];
NSImage *sortAscending = [NSImage imageNamed:@"NSAscendingSortIndicator"];
NSImage *sortDescending = [NSImage imageNamed:@"NSDescendingSortIndicator"];
if(indicatorImage == sortAscending) {
[tableView setIndicatorImage:sortDescending inTableColumn:tableColumn];
ascending = NO;
} else {
[tableView setIndicatorImage:sortAscending inTableColumn:tableColumn];
ascending = YES;
}
switch(index) {
default:
case 0:
sortDescriptors = @[];
break;
case 1:
case 2:
case 3:
case 4:
case 5:
case 8:
case 10:
case 11:
case 12:
sortDescriptor = [[NSSortDescriptor alloc] initWithKey:[tableColumn identifier] ascending:ascending selector:@selector(caseInsensitiveCompare:)];
sortDescriptors = @[sortDescriptor];
break;
case 6:
case 7:
sortDescriptor = [[NSSortDescriptor alloc] initWithKey:[tableColumn identifier] ascending:ascending selector:@selector(compareTrackNumbers:)];
sortDescriptors = @[sortDescriptor];
break;
case 9: {
// Unfortunately, this makes the column header bug out. No way around it.
sortDescriptors = @[
[[NSSortDescriptor alloc] initWithKey:@"albumartist"
ascending:ascending
selector:@selector(caseInsensitiveCompare:)],
[[NSSortDescriptor alloc] initWithKey:@"album"
ascending:ascending
selector:@selector(caseInsensitiveCompare:)],
[[NSSortDescriptor alloc] initWithKey:@"disc" // Yes, this, even though it's not actually a column
ascending:ascending
selector:@selector(compareTrackNumbers:)],
[[NSSortDescriptor alloc] initWithKey:@"track"
ascending:ascending
selector:@selector(compareTrackNumbers:)]
];
}
}
[self setSortDescriptors:sortDescriptors];
}
// This action is only needed to revert the one that follows it
- (void)moveObjectsFromIndex:(NSUInteger)fromIndex
toArrangedObjectIndexes:(NSIndexSet *)indexSet {
[[[self undoManager] prepareWithInvocationTarget:self]
moveObjectsInArrangedObjectsFromIndexes:indexSet
toIndex:fromIndex];
NSString *actionName =
[NSString stringWithFormat:@"Reordering %lu entries", (unsigned long)[indexSet count]];
[[self undoManager] setActionName:actionName];
[super moveObjectsFromIndex:fromIndex toArrangedObjectIndexes:indexSet];
[playbackController playlistDidChange:self];
}
- (void)moveObjectsInArrangedObjectsFromIndexes:(NSIndexSet *)indexSet
toIndex:(NSUInteger)insertIndex {
[[[self undoManager] prepareWithInvocationTarget:self]
moveObjectsFromIndex:insertIndex
toArrangedObjectIndexes:indexSet];
NSString *actionName =
[NSString stringWithFormat:@"Reordering %lu entries", (unsigned long)[indexSet count]];
[[self undoManager] setActionName:actionName];
[super moveObjectsInArrangedObjectsFromIndexes:indexSet toIndex:insertIndex];
[playbackController playlistDidChange:self];
}
- (id<NSPasteboardWriting>)tableView:(NSTableView *)tableView
pasteboardWriterForRow:(NSInteger)row {
NSPasteboardItem *item = (NSPasteboardItem *)[super tableView:tableView
pasteboardWriterForRow:row];
if(!item) {
item = [[NSPasteboardItem alloc] init];
}
PlaylistEntry *song = [[self arrangedObjects] objectAtIndex:row];
if(song.url != nil) {
[item setData:[song.url dataRepresentation] forType:NSPasteboardTypeFileURL];
}
return item;
}
- (BOOL)tableView:(NSTableView *)tv
acceptDrop:(id<NSDraggingInfo>)info
row:(NSInteger)row
dropOperation:(NSTableViewDropOperation)op {
// Check if DNDArrayController handles it.
if([super tableView:tv acceptDrop:info row:row dropOperation:op]) return YES;
if(row < 0) row = 0;
// Determine the type of object that was dropped
NSArray *supportedTypes =
@[CogUrlsPboardType, NSPasteboardTypeFileURL, iTunesDropType];
NSPasteboard *pboard = [info draggingPasteboard];
NSString *bestType = [pboard availableTypeFromArray:supportedTypes];
NSMutableArray *acceptedURLs = [[NSMutableArray alloc] init];
// Get files from an file drawer drop
if([bestType isEqualToString:CogUrlsPboardType]) {
NSError *error;
NSData *data = [pboard dataForType:CogUrlsPboardType];
NSArray *urls;
if(@available(macOS 11.0, *)) {
urls = [NSKeyedUnarchiver unarchivedArrayOfObjectsOfClass:[NSURL class]
fromData:data
error:&error];
} else {
NSSet *allowed = [NSSet setWithArray:@[[NSArray class], [NSURL class]]];
urls = [NSKeyedUnarchiver unarchivedObjectOfClasses:allowed
fromData:data
error:&error];
}
if(!urls) {
DLog(@"%@", error);
} else {
DLog(@"URLS: %@", urls);
}
//[playlistLoader insertURLs: urls atIndex:row sort:YES];
[acceptedURLs addObjectsFromArray:urls];
}
// Get files from a normal file drop (such as from Finder)
if([bestType isEqualToString:NSPasteboardTypeFileURL]) {
NSArray<Class> *classes = @[[NSURL class]];
NSDictionary *options = @{};
NSArray<NSURL *> *files = [pboard readObjectsForClasses:classes options:options];
//[playlistLoader insertURLs:urls atIndex:row sort:YES];
[acceptedURLs addObjectsFromArray:files];
}
// Get files from an iTunes drop
if([bestType isEqualToString:iTunesDropType]) {
NSDictionary *iTunesDict = [pboard propertyListForType:iTunesDropType];
NSDictionary *tracks = [iTunesDict valueForKey:@"Tracks"];
// Convert the iTunes URLs to URLs....MWAHAHAH!
NSMutableArray *urls = [[NSMutableArray alloc] init];
for(NSDictionary *trackInfo in [tracks allValues]) {
[urls addObject:[NSURL URLWithString:[trackInfo valueForKey:@"Location"]]];
}
//[playlistLoader insertURLs:urls atIndex:row sort:YES];
[acceptedURLs addObjectsFromArray:urls];
}
if([acceptedURLs count]) {
if(![[self content] count]) {
row = 0;
}
NSDictionary *loadEntriesData = @{ @"entries": acceptedURLs,
@"index": @(row),
@"sort": @(YES),
@"origin": @(URLOriginInternal) };
[self performSelectorInBackground:@selector(insertURLsInBackground:) withObject:loadEntriesData];
}
return YES;
}
- (NSUndoManager *)undoManager {
return undoManager;
}
- (NSIndexSet *)disarrangeIndexes:(NSIndexSet *)indexes {
if([[self arrangedObjects] count] <= [indexes lastIndex]) return indexes;
NSMutableIndexSet *disarrangedIndexes = [[NSMutableIndexSet alloc] init];
[indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *_Nonnull stop) {
[disarrangedIndexes addIndex:[[self content] indexOfObject:[[self arrangedObjects] objectAtIndex:idx]]];
}];
return disarrangedIndexes;
}
- (NSArray *)disarrangeObjects:(NSArray *)objects {
NSMutableArray *disarrangedObjects = [[NSMutableArray alloc] init];
for(PlaylistEntry *pe in [self content]) {
if([objects containsObject:pe]) [disarrangedObjects addObject:pe];
}
return disarrangedObjects;
}
- (NSIndexSet *)rearrangeIndexes:(NSIndexSet *)indexes {
if([[self content] count] <= [indexes lastIndex]) return indexes;
NSMutableIndexSet *rearrangedIndexes = [[NSMutableIndexSet alloc] init];
[indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *_Nonnull stop) {
[rearrangedIndexes addIndex:[[self arrangedObjects] indexOfObject:[[self content] objectAtIndex:idx]]];
}];
return rearrangedIndexes;
}
- (void)insertObjects:(NSArray *)objects atIndexes:(NSIndexSet *)indexes {
[self insertObjects:objects atArrangedObjectIndexes:indexes];
[self rearrangeObjects];
}
- (void)untrashObjects:(NSArray *)objects atIndexes:(NSIndexSet *)indexes {
[self untrashObjects:objects atArrangedObjectIndexes:indexes];
[self rearrangeObjects];
}
- (void)insertObjectsUnsynced:(NSArray *)objects atArrangedObjectIndexes:(NSIndexSet *)indexes {
[super insertObjects:objects atArrangedObjectIndexes:indexes];
[self rearrangeObjects];
if([self shuffle] != ShuffleOff) [self resetShuffleList];
}
- (void)insertObjects:(NSArray *)objects atArrangedObjectIndexes:(NSIndexSet *)indexes {
// Special case, somehow someone is undoing while a search filter is in place
__block NSUInteger index = NSNotFound;
[indexes enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) {
if(range.location < index) {
index = range.location;
}
}];
NSUInteger count = [[self content] count];
if(index > count) {
// Ah, oops, bodge fix
__block NSMutableIndexSet *replacementIndexes = [[NSMutableIndexSet alloc] init];
[indexes enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) {
[replacementIndexes addIndexesInRange:NSMakeRange(range.location - index + count, range.length)];
}];
indexes = replacementIndexes;
}
[[[self undoManager] prepareWithInvocationTarget:self]
removeObjectsAtIndexes:[self disarrangeIndexes:indexes]];
NSString *actionName =
[NSString stringWithFormat:@"Adding %lu entries", (unsigned long)[objects count]];
[[self undoManager] setActionName:actionName];
for(PlaylistEntry *pe in objects) {
pe.deLeted = NO;
}
// further bodge fix
@try {
[super insertObjects:objects atArrangedObjectIndexes:indexes];
}
@catch(id anException) {
indexes = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(count, [objects count])];
[super insertObjects:objects atArrangedObjectIndexes:indexes];
}
[self commitPersistentStore];
if([self shuffle] != ShuffleOff) [self resetShuffleList];
}
- (void)untrashObjects:(NSArray *)objects atArrangedObjectIndexes:(NSIndexSet *)indexes {
[[[self undoManager] prepareWithInvocationTarget:self]
trashObjectsAtIndexes:[self disarrangeIndexes:indexes]];
NSString *actionName =
[NSString stringWithFormat:@"Restoring %lu entries from trash", (unsigned long)[objects count]];
[[self undoManager] setActionName:actionName];
for(PlaylistEntry *pe in objects) {
if(pe.deLeted && pe.trashUrl) {
NSError *error = nil;
[[NSFileManager defaultManager] moveItemAtURL:pe.trashUrl toURL:pe.url error:&error];
}
pe.deLeted = NO;
pe.trashUrl = nil;
}
[super insertObjects:objects atArrangedObjectIndexes:indexes];
[self commitPersistentStore];
if([self shuffle] != ShuffleOff) [self resetShuffleList];
}
- (void)removeObjectsAtIndexes:(NSIndexSet *)indexes {
[self removeObjectsAtArrangedObjectIndexes:[self rearrangeIndexes:indexes]];
}
- (void)trashObjectsAtIndexes:(NSIndexSet *)indexes {
[self trashObjectsAtArrangedObjectIndexes:[self rearrangeIndexes:indexes]];
}
- (void)removeObjectsAtArrangedObjectIndexes:(NSIndexSet *)indexes {
NSArray *objects = [[self arrangedObjects] objectsAtIndexes:indexes];
[[[self undoManager] prepareWithInvocationTarget:self]
insertObjects:[self disarrangeObjects:objects]
atIndexes:[self disarrangeIndexes:indexes]];
NSString *actionName =
[NSString stringWithFormat:@"Removing %lu entries", (unsigned long)[indexes count]];
[[self undoManager] setActionName:actionName];
DLog(@"Removing indexes: %@", indexes);
DLog(@"Current index: %lli", currentEntry.index);
NSMutableIndexSet *unarrangedIndexes = [[NSMutableIndexSet alloc] init];
for(PlaylistEntry *pe in objects) {
[unarrangedIndexes addIndex:[pe index]];
pe.deLeted = YES;
}
if([indexes containsIndex:currentEntry.index]) {
[self updateNextAfterDeleted:currentEntry withDeleteIndexes:indexes];
} else if(nextEntryAfterDeleted &&
[indexes containsIndex:nextEntryAfterDeleted.index]) {
[self updateNextAfterDeleted:nextEntryAfterDeleted withDeleteIndexes:indexes];
}
if(currentEntry.index >= 0 && [unarrangedIndexes containsIndex:currentEntry.index]) {
currentEntry.index = -currentEntry.index - 1;
DLog(@"Current removed: %lli", currentEntry.index);
}
if(currentEntry.index < 0) // Need to update the negative index
{
NSInteger i = -currentEntry.index - 1;
DLog(@"I is %li", i);
NSInteger j;
for(j = i - 1; j >= 0; j--) {
if([unarrangedIndexes containsIndex:j]) {
DLog(@"Removing 1");
i--;
}
}
currentEntry.index = -i - 1;
}
[super removeObjectsAtArrangedObjectIndexes:indexes];
[self commitPersistentStore];
if([self shuffle] != ShuffleOff) [self resetShuffleList];
[playbackController playlistDidChange:self];
}
- (void)trashObjectsAtArrangedObjectIndexes:(NSIndexSet *)indexes {
NSArray *objects = [[self arrangedObjects] objectsAtIndexes:indexes];
[[[self undoManager] prepareWithInvocationTarget:self]
untrashObjects:[self disarrangeObjects:objects]
atIndexes:[self disarrangeIndexes:indexes]];
NSString *actionName =
[NSString stringWithFormat:@"Trashing %lu entries", (unsigned long)[indexes count]];
[[self undoManager] setActionName:actionName];
DLog(@"Trashing indexes: %@", indexes);
DLog(@"Current index: %lli", currentEntry.index);
NSMutableIndexSet *unarrangedIndexes = [[NSMutableIndexSet alloc] init];
for(PlaylistEntry *pe in objects) {
[unarrangedIndexes addIndex:[pe index]];
pe.deLeted = YES;
}
if([indexes containsIndex:currentEntry.index]) {
[self updateNextAfterDeleted:currentEntry withDeleteIndexes:indexes];
if(nextEntryAfterDeleted) {
[playbackController playEntry:nextEntryAfterDeleted];
nextEntryAfterDeleted = nil;
} else {
[playbackController stop:nil];
}
}
[super removeObjectsAtArrangedObjectIndexes:indexes];
[self commitPersistentStore];
if([self shuffle] != ShuffleOff) [self resetShuffleList];
[playbackController playlistDidChange:self];
for(PlaylistEntry *pe in objects) {
if([pe.url isFileURL]) {
NSURL *removed = nil;
NSError *error = nil;
[[NSFileManager defaultManager] trashItemAtURL:pe.url resultingItemURL:&removed error:&error];
pe.trashUrl = removed;
}
}
}
- (void)setSortDescriptors:(NSArray *)sortDescriptors {
DLog(@"Current: %@, setting: %@", [self sortDescriptors], sortDescriptors);
// Cheap hack so the index column isn't sorted
if([sortDescriptors count] != 0) {
if([[((NSSortDescriptor *)(sortDescriptors[0])) key] caseInsensitiveCompare:@"index"] == NSOrderedSame) {
// Remove the sort descriptors
[super setSortDescriptors:@[]];
[self rearrangeObjects];
return;
}
}
[super setSortDescriptors:sortDescriptors];
[self rearrangeObjects];
[playbackController playlistDidChange:self];
}
- (IBAction)randomizeList:(id)sender {
[self setSortDescriptors:@[]];
NSArray *unrandomized = [self content];
[[[self undoManager] prepareWithInvocationTarget:self] unrandomizeList:unrandomized];
[self setContent:[Shuffle shuffleList:[self content]]];
if([self shuffle] != ShuffleOff) [self resetShuffleList];
[[self undoManager] setActionName:NSLocalizedString(@"PlaylistRandomizationAction", @"")];
}
- (void)unrandomizeList:(NSArray *)entries {
[[[self undoManager] prepareWithInvocationTarget:self] randomizeList:self];
[self setContent:entries];
}
- (IBAction)toggleShuffle:(id)sender {
ShuffleMode shuffle = [self shuffle];
if(shuffle == ShuffleOff) {
[self setShuffle:ShuffleAlbums];
} else if(shuffle == ShuffleAlbums) {
[self setShuffle:ShuffleAll];
} else if(shuffle == ShuffleAll) {
[self setShuffle:ShuffleOff];
}
}
- (IBAction)toggleRepeat:(id)sender {
RepeatMode repeat = [self repeat];
if(repeat == RepeatModeNoRepeat) {
[self setRepeat:RepeatModeRepeatOne];
} else if(repeat == RepeatModeRepeatOne) {
[self setRepeat:RepeatModeRepeatAlbum];
} else if(repeat == RepeatModeRepeatAlbum) {
[self setRepeat:RepeatModeRepeatAll];
} else if(repeat == RepeatModeRepeatAll) {
[self setRepeat:RepeatModeNoRepeat];
}
}
- (PlaylistEntry *)entryAtIndex:(NSInteger)i {
if([[self arrangedObjects] count] == 0) return nil;
RepeatMode repeat = [self repeat];
if(i < 0 || i >= [[self arrangedObjects] count]) {
if(repeat != RepeatModeRepeatAll) return nil;
while(i < 0) i += [[self arrangedObjects] count];
if(i >= [[self arrangedObjects] count]) i %= [[self arrangedObjects] count];
}
return [[self arrangedObjects] objectAtIndex:i];
}
- (IBAction)remove:(id)sender {
// It's a kind of magic.
// Plain old NSArrayController's remove: isn't working properly for some reason.
// The method is definitely called but (overridden) removeObjectsAtArrangedObjectIndexes: isn't
// called and no entries are removed. Putting explicit call to
// removeObjectsAtArrangedObjectIndexes: here for now.
// TODO: figure it out
NSIndexSet *selected = [self selectionIndexes];
if([selected count] > 0) {
[self removeObjectsAtArrangedObjectIndexes:selected];
}
}
- (IBAction)trash:(id)sender {
// Someone asked for this, so they're getting it.
// Trash the selection, and advance playback to the next untrashed file if necessary.
NSIndexSet *selected = [self selectionIndexes];
if([selected count] > 0) {
[self trashObjectsAtArrangedObjectIndexes:selected];
}
}
- (IBAction)removeDuplicates:(id)sender {
NSMutableArray *originals = [[NSMutableArray alloc] init];
NSMutableArray *duplicates = [[NSMutableArray alloc] init];
for(PlaylistEntry *pe in [self content]) {
if([originals containsObject:pe.url])
[duplicates addObject:pe];
else
[originals addObject:pe.url];
}
if([duplicates count] > 0) {
NSArray *arrangedContent = [self arrangedObjects];
NSMutableIndexSet *duplicatesIndex = [[NSMutableIndexSet alloc] init];
for(PlaylistEntry *pe in duplicates) {
[duplicatesIndex addIndex:[arrangedContent indexOfObject:pe]];
}
[self removeObjectsAtArrangedObjectIndexes:duplicatesIndex];
}
}
- (IBAction)removeDeadItems:(id)sender {
NSMutableArray *deadItems = [[NSMutableArray alloc] init];
for(PlaylistEntry *pe in [self content]) {
NSURL *url = pe.url;
if([url isFileURL])
if(![[NSFileManager defaultManager] fileExistsAtPath:[url path]])
[deadItems addObject:pe];
}
if([deadItems count] > 0) {
NSArray *arrangedContent = [self arrangedObjects];
NSMutableIndexSet *deadItemsIndex = [[NSMutableIndexSet alloc] init];
for(PlaylistEntry *pe in deadItems) {
[deadItemsIndex addIndex:[arrangedContent indexOfObject:pe]];
}
[self removeObjectsAtArrangedObjectIndexes:deadItemsIndex];
}
}
- (PlaylistEntry *)shuffledEntryAtIndex:(NSInteger)i {
RepeatMode repeat = [self repeat];
while(i < 0) {
if(repeat == RepeatModeRepeatAll) {
[self addShuffledListToFront];
// change i appropriately
i += [[self arrangedObjects] count];
} else {
return nil;
}
}
while(i >= [shuffleList count]) {
if(repeat == RepeatModeRepeatAll) {
[self addShuffledListToBack];
} else {
return nil;
}
}
return shuffleList[i];
}
- (PlaylistEntry *)getNextEntry:(PlaylistEntry *)pe {
return [self getNextEntry:pe ignoreRepeatOne:NO];
}
- (PlaylistEntry *)getNextEntry:(PlaylistEntry *)pe ignoreRepeatOne:(BOOL)ignoreRepeatOne {
if(!ignoreRepeatOne && [self repeat] == RepeatModeRepeatOne) {
return pe;
}
if([queueList count] > 0) {
pe = queueList[0];
[queueList removeObjectAtIndex:0];
pe.queued = NO;
[pe setQueuePosition:-1];
int i;
for(i = 0; i < [queueList count]; i++) {
PlaylistEntry *queueItem = queueList[i];
[queueItem setQueuePosition:i];
}
[self commitPersistentStore];
return pe;
}
if([self shuffle] != ShuffleOff) {
return [self shuffledEntryAtIndex:(pe.shuffleIndex + 1)];
} else {
NSInteger i;
if(pe.deLeted) // Was a current entry, now removed.
{
if(nextEntryAfterDeleted)
i = nextEntryAfterDeleted.index;
else
i = 0;
nextEntryAfterDeleted = nil;
} else {
i = pe.index + 1;
}
if([self repeat] == RepeatModeRepeatAlbum) {
PlaylistEntry *next = [self entryAtIndex:i];
if((i > [[self arrangedObjects] count] - 1) ||
([[next album] caseInsensitiveCompare:[pe album]]) || ([next album] == nil)) {
NSArray *filtered = [self filterPlaylistOnAlbum:[pe album]];
if([pe album] == nil || !filtered || [filtered count] < 1)
i--;
else
i = [(PlaylistEntry *)filtered[0] index];
}
}
return [self entryAtIndex:i];
}
}
- (NSArray *)filterPlaylistOnAlbum:(NSString *)album {
NSPredicate *predicate;
if([album length] > 0)
predicate = [NSPredicate predicateWithFormat:@"album == %@", album];
else
predicate = [NSPredicate predicateWithFormat:@"album == nil || album == %@", @""];
return [[self arrangedObjects] filteredArrayUsingPredicate:predicate];
}
- (PlaylistEntry *)getPrevEntry:(PlaylistEntry *)pe {
return [self getPrevEntry:pe ignoreRepeatOne:NO];
}
- (PlaylistEntry *)getPrevEntry:(PlaylistEntry *)pe ignoreRepeatOne:(BOOL)ignoreRepeatOne {
if(!ignoreRepeatOne && [self repeat] == RepeatModeRepeatOne) {
return pe;
}
if([self shuffle] != ShuffleOff) {
return [self shuffledEntryAtIndex:(pe.shuffleIndex - 1)];
} else {
NSInteger i;
if(pe.index < 0) // Was a current entry, now removed.
{
i = -pe.index - 2;
} else {
i = pe.index - 1;
}
return [self entryAtIndex:i];
}
}
- (BOOL)next {
PlaylistEntry *pe;
pe = [self getNextEntry:[self currentEntry] ignoreRepeatOne:YES];
if(pe == nil) return NO;
[self setCurrentEntry:pe];
return YES;
}
- (BOOL)prev {
PlaylistEntry *pe;
pe = [self getPrevEntry:[self currentEntry] ignoreRepeatOne:YES];
if(pe == nil) return NO;
[self setCurrentEntry:pe];
return YES;
}
- (NSArray *)shuffleAlbums {
NSArray *newList = [self arrangedObjects];
NSMutableArray *temp = [[NSMutableArray alloc] init];
NSSortDescriptor *sortDescriptorTrack = [[NSSortDescriptor alloc] initWithKey:@"track"
ascending:YES];
NSSortDescriptor *sortDescriptorDisc = [[NSSortDescriptor alloc] initWithKey:@"disc"
ascending:YES];
NSArray *albums = [newList valueForKey:@"album"];
albums = [[NSSet setWithArray:albums] allObjects];
NSArray *tempList = [Shuffle shuffleList:albums];
temp = [[NSMutableArray alloc] init];
BOOL blankAdded = NO;
for(NSString *album in tempList) {
NSString *theAlbum = album;
if((id)album == [NSNull null]) {
if(blankAdded)
continue;
else
theAlbum = @"";
blankAdded = YES;
} else if([album isEqualToString:@""]) {
if(blankAdded) continue;
blankAdded = YES;
}
NSArray *albumContent = [self filterPlaylistOnAlbum:theAlbum];
NSArray *sortedContent =
[albumContent sortedArrayUsingDescriptors:@[sortDescriptorDisc, sortDescriptorTrack]];
if(sortedContent && [sortedContent count])
[temp addObjectsFromArray:sortedContent];
}
return temp;
}
- (void)readQueueFromDataStore {
NSPredicate *hasUrlPredicate = [NSPredicate predicateWithFormat:@"urlString != nil && urlString != %@", @""];
NSPredicate *deletedPredicate = [NSPredicate predicateWithFormat:@"deLeted == NO || deLeted == nil"];
NSPredicate *queuedPredicate = [NSPredicate predicateWithFormat:@"queued == YES"];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"queuePosition" ascending:YES];
NSCompoundPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:@[deletedPredicate, hasUrlPredicate, queuedPredicate]];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"PlaylistEntry"];
request.predicate = predicate;
request.sortDescriptors = @[sortDescriptor];
[self.persistentContainer.viewContext performBlockAndWait:^{
NSError *error = nil;
NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error];
if(results && [results count] > 0) {
[queueList removeAllObjects];
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [results count])];
[queueList insertObjects:results atIndexes:indexSet];
}
}];
}
- (void)readShuffleListFromDataStore {
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"deLeted == NO || deLeted == nil || urlString == nil"];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"shuffleIndex" ascending:YES];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"PlaylistEntry"];
request.predicate = predicate;
request.sortDescriptors = @[sortDescriptor];
[self.persistentContainer.viewContext performBlockAndWait:^{
NSError *error = nil;
NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error];
if(results && [results count] > 0) {
[shuffleList removeAllObjects];
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [results count])];
[shuffleList insertObjects:results atIndexes:indexSet];
}
}];
}
- (void)addShuffledListToFront {
NSArray *newList;
NSIndexSet *indexSet;
if([self shuffle] == ShuffleAlbums) {
newList = [self shuffleAlbums];
} else {
newList = [Shuffle shuffleList:[self arrangedObjects]];
}
indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [newList count])];
[shuffleList insertObjects:newList atIndexes:indexSet];
int i;
for(i = 0; i < [shuffleList count]; i++) {
[shuffleList[i] setShuffleIndex:i];
}
[self commitPersistentStore];
}
- (void)addShuffledListToBack {
NSArray *newList;
NSIndexSet *indexSet;
if([self shuffle] == ShuffleAlbums) {
newList = [self shuffleAlbums];
} else {
newList = [Shuffle shuffleList:[self arrangedObjects]];
}
indexSet =
[NSIndexSet indexSetWithIndexesInRange:NSMakeRange([shuffleList count], [newList count])];
[shuffleList insertObjects:newList atIndexes:indexSet];
unsigned long i;
for(i = ([shuffleList count] - [newList count]); i < [shuffleList count]; i++) {
[shuffleList[i] setShuffleIndex:(int)i];
}
[self commitPersistentStore];
}
- (void)resetShuffleList {
[shuffleList removeAllObjects];
[self addShuffledListToFront];
if(currentEntry && currentEntry.index >= 0) {
if([self shuffle] == ShuffleAlbums) {
NSString *currentAlbum = currentEntry.album;
if(!currentAlbum) currentAlbum = @"";
NSArray *wholeAlbum = [self filterPlaylistOnAlbum:currentAlbum];
// First prune the shuffle list of the currently playing album
long i, j;
for(i = 0; i < [shuffleList count];) {
if([wholeAlbum containsObject:shuffleList[i]]) {
[shuffleList removeObjectAtIndex:i];
} else {
++i;
}
}
// Then insert the playing album at the start
NSIndexSet *indexSet =
[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [wholeAlbum count])];
[shuffleList insertObjects:wholeAlbum atIndexes:indexSet];
// Oops, gotta reset the shuffle indexes
for(i = 0, j = [shuffleList count]; i < j; ++i) {
[shuffleList[i] setShuffleIndex:(int)i];
}
[self commitPersistentStore];
} else {
[shuffleList insertObject:currentEntry atIndex:0];
[currentEntry setShuffleIndex:0];
// Need to rejigger so the current entry is at the start now...
long i, j;
BOOL found = NO;
for(i = 1, j = [shuffleList count]; i < j && !found; i++) {
if(shuffleList[i] == currentEntry) {
found = YES;
[shuffleList removeObjectAtIndex:i];
} else {
[shuffleList[i] setShuffleIndex:(int)i];
}
}
[self commitPersistentStore];
}
}
}
- (void)setCurrentEntry:(PlaylistEntry *)pe {
if(pe == currentEntry || kAppControllerShuttingDown) return;
if(currentEntry) {
currentEntry.current = NO;
currentEntry.stopAfter = NO;
currentEntry.currentPosition = 0.0;
currentEntry.countAdded = NO;
}
if(pe) {
pe.current = YES;
}
[self commitPersistentStore];
NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init];
if(currentEntry != nil && !currentEntry.deLeted && currentEntry.index < NSNotFound) [refreshSet addIndex:currentEntry.index];
if(pe != nil && !pe.deLeted && pe.index < NSNotFound) [refreshSet addIndex:pe.index];
// Refresh entire row to refresh tooltips
unsigned long columns = [[self.tableView tableColumns] count];
[self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]];
if(pe != nil) [self.tableView scrollRowToVisible:pe.index];
currentEntry = pe;
}
- (void)setShuffle:(ShuffleMode)s {
[[NSUserDefaults standardUserDefaults] setInteger:s forKey:@"shuffle"];
if(s != ShuffleOff) [self resetShuffleList];
[playbackController playlistDidChange:self];
}
- (ShuffleMode)shuffle {
return (ShuffleMode)[[NSUserDefaults standardUserDefaults] integerForKey:@"shuffle"];
}
- (void)setRepeat:(RepeatMode)r {
[[NSUserDefaults standardUserDefaults] setInteger:r forKey:@"repeat"];
[playbackController playlistDidChange:self];
}
- (RepeatMode)repeat {
return (RepeatMode)[[NSUserDefaults standardUserDefaults] integerForKey:@"repeat"];
}
- (IBAction)clear:(id)sender {
[self setFilterPredicate:nil];
[self
removeObjectsAtArrangedObjectIndexes:
[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [[self arrangedObjects] count])]];
}
- (IBAction)clearFilterPredicate:(id)sender {
[self setFilterPredicate:nil];
}
- (void)setFilterPredicate:(NSPredicate *)filterPredicate {
[super setFilterPredicate:filterPredicate];
}
- (IBAction)showEntryInFinder:(id)sender {
if([[self selectedObjects] count] == 0) return;
NSWorkspace *ws = [NSWorkspace sharedWorkspace];
NSURL *url = [[self selectedObjects][0] url];
if([url isFileURL]) [ws selectFile:[url path] inFileViewerRootedAtPath:[url path]];
}
/*
- (IBAction)showTagEditor:(id)sender
{
// call the editor & pass the url
if ([self selectionIndex] < 0)
return;
NSURL *url = [[[self selectedObjects] objectAtIndex:0] URL];
if ([url isFileURL])
[TagEditorController openTagEditor:url sender:sender];
}
*/
- (IBAction)searchByArtist:(id)sender;
{
PlaylistEntry *entry = [[self arrangedObjects] objectAtIndex:[self selectionIndex]];
[spotlightWindowController searchForArtist:[entry artist]];
}
- (IBAction)searchByAlbum:(id)sender;
{
PlaylistEntry *entry = [[self arrangedObjects] objectAtIndex:[self selectionIndex]];
[spotlightWindowController searchForAlbum:[entry album]];
}
- (NSMutableArray *)queueList {
return queueList;
}
- (IBAction)emptyQueueList:(id)sender {
[self emptyQueueListUnsynced];
}
- (void)emptyQueueListUnsynced {
NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init];
for(PlaylistEntry *queueItem in queueList) {
queueItem.queued = NO;
[queueItem setQueuePosition:-1];
[refreshSet addIndex:[queueItem index]];
}
[queueList removeAllObjects];
[self commitPersistentStore];
// Refresh entire row to refresh tooltips
unsigned long columns = [[self.tableView tableColumns] count];
[self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]];
}
- (IBAction)toggleQueued:(id)sender {
NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init];
for(PlaylistEntry *queueItem in [self selectedObjects]) {
if(queueItem.queued) {
[queueList removeObject:queueItem];
queueItem.queued = NO;
queueItem.queuePosition = -1;
} else {
queueItem.queued = YES;
queueItem.queuePosition = (int)[queueList count];
[queueList addObject:queueItem];
}
[refreshSet addIndex:[queueItem index]];
DLog(@"TOGGLE QUEUED: %i", queueItem.queued);
}
for(PlaylistEntry *queueItem in queueList) {
if(![[self selectedObjects] containsObject:queueItem])
[refreshSet addIndex:[queueItem index]];
}
int i = 0;
for(PlaylistEntry *cur in queueList) {
cur.queuePosition = i++;
}
[self commitPersistentStore];
// Refresh entire row to refresh tooltips
unsigned long columns = [[self.tableView tableColumns] count];
[self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]];
}
- (IBAction)removeFromQueue:(id)sender {
NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init];
for(PlaylistEntry *queueItem in [self selectedObjects]) {
queueItem.queued = NO;
queueItem.queuePosition = -1;
[queueList removeObject:queueItem];
[refreshSet addIndex:[queueItem index]];
}
for(PlaylistEntry *queueItem in queueList) {
[refreshSet addIndex:[queueItem index]];
}
int i = 0;
for(PlaylistEntry *cur in queueList) {
cur.queuePosition = i++;
}
[self commitPersistentStore];
// Refresh entire row to refresh tooltips
unsigned long columns = [[self.tableView tableColumns] count];
[self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]];
}
- (IBAction)addToQueue:(id)sender {
NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init];
for(PlaylistEntry *queueItem in [self selectedObjects]) {
queueItem.queued = YES;
queueItem.queuePosition = (int)[queueList count];
[queueList addObject:queueItem];
}
for(PlaylistEntry *queueItem in queueList) {
[refreshSet addIndex:[queueItem index]];
}
int i = 0;
for(PlaylistEntry *cur in queueList) {
cur.queuePosition = i++;
}
[self commitPersistentStore];
// Refresh entire row to refresh tooltips
unsigned long columns = [[self.tableView tableColumns] count];
[self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]];
}
- (IBAction)stopAfterCurrent:(id)sender {
currentEntry.stopAfter = !currentEntry.stopAfter;
[self commitPersistentStore];
NSIndexSet *refreshSet = [NSIndexSet indexSetWithIndex:[currentEntry index]];
// Refresh entire row to refresh tooltips
unsigned long columns = [[self.tableView tableColumns] count];
[self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]];
}
- (IBAction)stopAfterSelection:(id)sender {
NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init];
for(PlaylistEntry *pe in [self selectedObjects]) {
pe.stopAfter = !pe.stopAfter;
[refreshSet addIndex:pe.index];
}
[self commitPersistentStore];
// Refresh entire row of all affected items to update tooltips
unsigned long columns = [[self.tableView tableColumns] count];
[self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]];
}
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem {
SEL action = [menuItem action];
if(action == @selector(removeFromQueue:)) {
for(PlaylistEntry *q in [self selectedObjects])
if(q.queuePosition >= 0) return YES;
return NO;
}
if(action == @selector(emptyQueueList:) && ([queueList count] < 1)) return NO;
if(action == @selector(stopAfterCurrent:) && currentEntry.stopAfter) return NO;
// if nothing is selected, gray out these
if([[self selectedObjects] count] < 1) {
if(action == @selector(remove:)) return NO;
if(action == @selector(addToQueue:)) return NO;
if(action == @selector(toggleQueued:)) return NO;
if(action == @selector(searchByArtist:)) return NO;
if(action == @selector(searchByAlbum:)) return NO;
if(action == @selector(stopAfterSelection:)) return NO;
}
return YES;
}
// Asynchronous event inlets:
- (void)addURLsInBackground:(NSDictionary *)input {
NSUInteger row = [[self content] count];
NSMutableDictionary *_input = [input mutableCopy];
[_input setObject:@(row) forKey:@"index"];
[self insertURLsInBackground:_input];
}
static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_block_t block) {
if(dispatch_queue_get_label(queue) == dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)) {
block();
} else {
dispatch_sync(queue, block);
}
}
- (void)insertURLsInBackground:(NSDictionary *)input {
NSArray *entries = [input objectForKey:@"entries"];
NSUInteger row = [[input objectForKey:@"index"] integerValue];
BOOL sort = [[input objectForKey:@"sort"] boolValue];
URLOrigin origin = (URLOrigin)[[input objectForKey:@"origin"] integerValue];
NSUInteger countNow = [[self content] count];
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
[self willInsertURLs:entries origin:origin];
});
if(countNow && row && ![[self content] count]) {
row = 0;
}
NSArray *urlsAccepted = [playlistLoader insertURLs:entries atIndex:row sort:sort];
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
[self didInsertURLs:urlsAccepted origin:origin];
});
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
if([self shuffle] != ShuffleOff) [self resetShuffleList];
});
}
// Event inlets:
- (void)willInsertURLs:(NSArray *)urls origin:(URLOrigin)origin {
if(![urls count]) return;
CGEventRef event = CGEventCreate(NULL /*default event source*/);
CGEventFlags mods = CGEventGetFlags(event);
CFRelease(event);
BOOL modifierPressed =
((mods & kCGEventFlagMaskCommand) != 0) & ((mods & kCGEventFlagMaskControl) != 0);
modifierPressed |= ((mods & kCGEventFlagMaskShift) != 0);
NSString *behavior =
[[NSUserDefaults standardUserDefaults] valueForKey:@"openingFilesBehavior"];
if(modifierPressed) {
behavior =
[[NSUserDefaults standardUserDefaults] valueForKey:@"openingFilesAlteredBehavior"];
}
BOOL shouldClear =
modifierPressed; // By default, internal sources should not clear the playlist
if(origin == URLOriginExternal) { // For external insertions, we look at the preference
// possible settings are "clearAndPlay", "enqueue", "enqueueAndPlay"
shouldClear = [behavior isEqualToString:@"clearAndPlay"];
}
if(shouldClear) {
[self clear:self];
}
}
- (void)didInsertURLs:(NSArray *)urls origin:(URLOrigin)origin {
if(![urls count]) return;
NSArray *nsurls = [urls valueForKey:@"url"];
if(![[SandboxBroker sharedSandboxBroker] areAllPathsSafe:nsurls]) {
[appController showPathSuggester];
}
CGEventRef event = CGEventCreate(NULL);
CGEventFlags mods = CGEventGetFlags(event);
CFRelease(event);
BOOL modifierPressed =
((mods & kCGEventFlagMaskCommand) != 0) & ((mods & kCGEventFlagMaskControl) != 0);
modifierPressed |= ((mods & kCGEventFlagMaskShift) != 0);
NSString *behavior =
[[NSUserDefaults standardUserDefaults] valueForKey:@"openingFilesBehavior"];
if(modifierPressed) {
behavior =
[[NSUserDefaults standardUserDefaults] valueForKey:@"openingFilesAlteredBehavior"];
}
BOOL shouldPlay = modifierPressed; // The default is NO for internal insertions
if(origin == URLOriginExternal) { // For external insertions, we look at the preference
shouldPlay = [behavior isEqualToString:@"clearAndPlay"] ||
[behavior isEqualToString:@"enqueueAndPlay"];
;
}
// Auto start playback
if(shouldPlay && [[self content] count] > 0) {
[playbackController playEntry:urls[0]];
}
}
- (IBAction)reloadTags:(id)sender {
NSArray *selectedobjects = [self selectedObjects];
if([selectedobjects count]) {
for(PlaylistEntry *pe in selectedobjects) {
pe.metadataLoaded = NO;
}
[playlistLoader performSelectorInBackground:@selector(loadInfoForEntries:) withObject:selectedobjects];
}
}
- (IBAction)resetPlaycounts:(id)sender {
NSArray *selectedobjects = [self selectedObjects];
if([selectedobjects count]) {
for(PlaylistEntry *pe in selectedobjects) {
[self resetPlayCountForTrack:pe];
}
[self commitPersistentStore];
}
}
- (IBAction)removeRatings:(id)sender {
NSArray *selectedobjects = [self selectedObjects];
if([selectedobjects count]) {
for(PlaylistEntry *pe in selectedobjects) {
[self removeRatingForTrack:pe];
}
[self commitPersistentStore];
NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init];
for(PlaylistEntry *pe in selectedobjects) {
if(pe.index >= 0 && pe.index < NSNotFound) {
[refreshSet addIndex:pe.index];
}
}
// Refresh entire row to refresh tooltips
unsigned long columns = [[self.tableView tableColumns] count];
[self.tableView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]];
}
}
- (BOOL)pathSuggesterEmpty {
__block BOOL rval;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"SandboxToken"];
[self.persistentContainer.viewContext performBlockAndWait:^{
NSError *error = nil;
NSArray *results = [self.persistentContainer.viewContext executeFetchRequest:request error:&error];
if(!results || [results count] < 1)
rval = YES;
else
rval = NO;
}];
return rval;
}
@end