cog/Playlist/PlaylistLoader.m

927 lines
27 KiB
Matlab
Raw Normal View History

2007-03-07 01:26:50 +00:00
//
// PlaylistLoader.m
// Cog
//
// Created by Vincent Spader on 3/05/07.
// Copyright 2007 Vincent Spader All rights reserved.
//
#include <objc/runtime.h>
#include <mach/semaphore.h>
#import "Cog-Swift.h"
#import <CoreData/CoreData.h>
#import "AppController.h"
2007-03-07 01:26:50 +00:00
#import "PlaylistController.h"
#import "PlaylistEntry.h"
#import "PlaylistLoader.h"
2007-03-07 01:26:50 +00:00
#import "NSFileHandle+CreateFile.h"
#import "CogAudio/AudioContainer.h"
#import "CogAudio/AudioMetadataReader.h"
#import "CogAudio/AudioPlayer.h"
#import "CogAudio/AudioPropertiesReader.h"
2018-06-28 10:59:59 +00:00
#import "XmlContainer.h"
#import "NSData+MD5.h"
2018-06-28 10:59:59 +00:00
#import "NSString+FinderCompare.h"
#import "SQLiteStore.h"
#import "Logging.h"
#import "NSDictionary+Merge.h"
#import "RedundantPlaylistDataStore.h"
@import Firebase;
extern NSMutableDictionary<NSString *, AlbumArtwork *> *__artworkDictionary;
2007-03-07 01:26:50 +00:00
@implementation PlaylistLoader
- (id)init {
self = [super init];
if(self) {
[self initDefaults];
queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:8];
}
return self;
}
- (void)initDefaults {
NSDictionary *defaultsDictionary = @{ @"readCueSheetsInFolders": @(YES) };
[[NSUserDefaults standardUserDefaults] registerDefaults:defaultsDictionary];
}
- (BOOL)save:(NSString *)filename {
2007-03-07 01:26:50 +00:00
NSString *ext = [filename pathExtension];
if([ext isEqualToString:@"pls"]) {
2007-03-07 01:26:50 +00:00
return [self save:filename asType:kPlaylistPls];
} else {
2007-03-07 01:26:50 +00:00
return [self save:filename asType:kPlaylistM3u];
}
}
2007-03-07 01:26:50 +00:00
- (BOOL)save:(NSString *)filename asType:(PlaylistType)type {
if(type == kPlaylistM3u) {
2007-03-07 01:26:50 +00:00
return [self saveM3u:filename];
} else if(type == kPlaylistPls) {
2007-03-07 01:26:50 +00:00
return [self savePls:filename];
} else if(type == kPlaylistXml) {
return [self saveXml:filename];
2007-03-07 01:26:50 +00:00
}
return NO;
}
- (NSString *)relativePathFrom:(NSString *)filename toURL:(NSURL *)entryURL {
2007-03-07 01:26:50 +00:00
NSString *basePath = [[[filename stringByStandardizingPath] stringByDeletingLastPathComponent] stringByAppendingString:@"/"];
if([entryURL isFileURL]) {
// We want relative paths.
NSMutableString *entryPath = [[[entryURL path] stringByStandardizingPath] mutableCopy];
2007-03-07 01:26:50 +00:00
[entryPath replaceOccurrencesOfString:basePath withString:@"" options:(NSAnchoredSearch | NSLiteralSearch | NSCaseInsensitiveSearch) range:NSMakeRange(0, [entryPath length])];
if([entryURL fragment]) {
[entryPath appendString:@"#"];
[entryPath appendString:[entryURL fragment]];
}
2007-03-07 01:26:50 +00:00
return entryPath;
} else {
// Write [entryURL absoluteString] to file
2007-03-07 01:26:50 +00:00
return [entryURL absoluteString];
}
}
- (BOOL)saveM3u:(NSString *)filename {
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:filename createFile:YES];
if(!fileHandle) {
ALog(@"Error saving m3u!");
return NO;
2007-03-07 01:26:50 +00:00
}
[fileHandle truncateFileAtOffset:0];
[fileHandle writeData:[@"#\n" dataUsingEncoding:NSUTF8StringEncoding]];
for(PlaylistEntry *pe in [playlistController arrangedObjects]) {
NSString *path = [self relativePathFrom:filename toURL:pe.url];
2007-03-07 01:26:50 +00:00
[fileHandle writeData:[[path stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
}
[fileHandle closeFile];
2007-03-07 01:26:50 +00:00
return YES;
}
- (BOOL)savePls:(NSString *)filename {
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:filename createFile:YES];
if(!fileHandle) {
2007-03-07 01:26:50 +00:00
return NO;
}
[fileHandle truncateFileAtOffset:0];
[fileHandle writeData:[[NSString stringWithFormat:@"[playlist]\nnumberOfEntries=%lu\n\n", (unsigned long)[[playlistController content] count]] dataUsingEncoding:NSUTF8StringEncoding]];
2007-03-07 01:26:50 +00:00
int i = 1;
for(PlaylistEntry *pe in [playlistController arrangedObjects]) {
NSString *path = [self relativePathFrom:filename toURL:pe.url];
NSString *entry = [NSString stringWithFormat:@"File%i=%@\n", i, path];
2007-03-07 01:26:50 +00:00
[fileHandle writeData:[entry dataUsingEncoding:NSUTF8StringEncoding]];
i++;
2007-03-07 01:26:50 +00:00
}
[fileHandle writeData:[@"\nVERSION=2" dataUsingEncoding:NSUTF8StringEncoding]];
[fileHandle closeFile];
2007-03-07 01:26:50 +00:00
return YES;
}
NSMutableDictionary *dictionaryWithPropertiesOfObject(id obj, NSArray *filterList) {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
Class class = [obj class];
do {
unsigned count;
objc_property_t *properties = class_copyPropertyList(class, &count);
for(int i = 0; i < count; i++) {
NSString *key = [NSString stringWithUTF8String:property_getName(properties[i])];
if([filterList containsObject:key]) continue;
Class classObject = NSClassFromString([key capitalizedString]);
if(classObject) {
id subObj = dictionaryWithPropertiesOfObject([obj valueForKey:key], filterList);
[dict setObject:subObj forKey:key];
} else {
id value = [obj valueForKey:key];
if(value) [dict setObject:value forKey:key];
}
}
free(properties);
if(count) break;
class = [class superclass];
} while(class);
return dict;
}
- (BOOL)saveXml:(NSString *)filename {
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:filename createFile:YES];
if(!fileHandle) {
return NO;
}
[fileHandle truncateFileAtOffset:0];
NSArray *filterList = @[@"display", @"length", @"path", @"filename", @"status", @"statusMessage", @"spam", @"lengthText", @"positionText", @"stopAfter", @"shuffleIndex", @"index", @"current", @"queued", @"currentPosition", @"queuePosition", @"error", @"removed", @"URL", @"albumArt"];
NSMutableDictionary *albumArtSet = [[NSMutableDictionary alloc] init];
NSMutableArray *topLevel = [[NSMutableArray alloc] init];
for(PlaylistEntry *pe in [playlistController arrangedObjects]) {
BOOL error = [pe error];
NSMutableDictionary *dict = dictionaryWithPropertiesOfObject(pe, filterList);
NSString *path = [self relativePathFrom:filename toURL:pe.url];
[dict setObject:path forKey:@"URL"];
NSData *albumArt = [dict objectForKey:@"albumArtInternal"];
if(albumArt) {
[dict removeObjectForKey:@"albumArtInternal"];
NSString *hash = [albumArt MD5];
if(![albumArtSet objectForKey:hash])
[albumArtSet setObject:albumArt forKey:hash];
[dict setObject:hash forKey:@"albumArt"];
}
if(error)
[dict removeObjectForKey:@"metadataLoaded"];
[topLevel addObject:dict];
}
NSMutableArray *queueList = [[NSMutableArray alloc] init];
for(PlaylistEntry *pe in [playlistController queueList]) {
[queueList addObject:@(pe.index)];
}
NSDictionary *dictionary = @{@"albumArt": albumArtSet, @"queue": queueList, @"items": topLevel};
NSError *err;
NSData *data = [NSPropertyListSerialization dataWithPropertyList:dictionary format:NSPropertyListXMLFormat_v1_0 options:0 error:&err];
[fileHandle writeData:data];
[fileHandle closeFile];
return YES;
}
- (NSArray *)fileURLsAtPath:(NSString *)path {
NSFileManager *manager = [NSFileManager defaultManager];
NSMutableArray *urls = [NSMutableArray array];
NSArray *subpaths = [manager subpathsAtPath:path];
for(NSString *subpath in subpaths) {
NSString *absoluteSubpath = [NSString pathWithComponents:@[path, subpath]];
BOOL isDir;
if([manager fileExistsAtPath:absoluteSubpath isDirectory:&isDir] && isDir == NO) {
if([[absoluteSubpath pathExtension] caseInsensitiveCompare:@"cue"] != NSOrderedSame ||
[[NSUserDefaults standardUserDefaults] boolForKey:@"readCueSheetsInFolders"]) {
[urls addObject:[NSURL fileURLWithPath:absoluteSubpath]];
}
}
}
NSSortDescriptor *sd_path = [[NSSortDescriptor alloc] initWithKey:@"path" ascending:YES];
[urls sortUsingDescriptors:@[sd_path]];
return urls;
}
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)beginProgress:(NSString *)localizedDescription {
while(playbackController.progressOverall) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
}
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
self->playbackController.progressOverall = [NSProgress progressWithTotalUnitCount:100000];
self->playbackController.progressOverall.localizedDescription = localizedDescription;
});
}
- (void)completeProgress {
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
if(self->playbackController.progressJob) {
[self->playbackController.progressJob setCompletedUnitCount:100000];
self->playbackController.progressJob = nil;
}
[self->playbackController.progressOverall setCompletedUnitCount:100000];
self->playbackController.progressOverall = nil;
});
}
- (void)beginProgressJob:(NSString *)localizedDescription percentOfTotal:(double)percent {
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
NSUInteger jobCount = (NSUInteger)ceil(1000.0 * percent);
self->playbackController.progressJob = [NSProgress progressWithTotalUnitCount:100000];
self->playbackController.progressJob.localizedDescription = localizedDescription;
[self->playbackController.progressOverall addChild:self->playbackController.progressJob withPendingUnitCount:jobCount];
});
}
- (void)completeProgressJob {
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
[self->playbackController.progressJob setCompletedUnitCount:100000];
self->playbackController.progressJob = nil;
});
}
- (void)setProgressStatus:(double)status {
if(status >= 0) {
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
NSUInteger jobCount = (NSUInteger)ceil(1000.0 * status);
[self->playbackController.progressOverall setCompletedUnitCount:jobCount];
});
} else {
[self completeProgress];
}
}
- (void)setProgressJobStatus:(double)status {
if(status >= 0) {
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
NSUInteger jobCount = (NSUInteger)ceil(1000.0 * status);
[self->playbackController.progressJob setCompletedUnitCount:jobCount];
});
} else {
[self completeProgressJob];
}
}
- (NSArray *)insertURLs:(NSArray *)urls atIndex:(NSInteger)index sort:(BOOL)sort {
2007-05-28 14:58:08 +00:00
NSMutableSet *uniqueURLs = [NSMutableSet set];
2007-05-28 14:58:08 +00:00
NSMutableArray *expandedURLs = [NSMutableArray array];
2007-10-19 02:23:10 +00:00
NSMutableArray *containedURLs = [NSMutableArray array];
NSMutableArray *fileURLs = [NSMutableArray array];
2007-05-28 14:58:08 +00:00
NSMutableArray *validURLs = [NSMutableArray array];
NSDictionary *xmlData = nil;
double progress;
if(!urls) {
[self completeProgress];
return @[];
}
[self beginProgress:NSLocalizedString(@"ProgressActionLoader", @"playlist loader inserting files")];
[self beginProgressJob:NSLocalizedString(@"ProgressSubActionLoaderListingFiles", @"collecting files") percentOfTotal:20.0];
if(index < 0)
index = 0;
progress = 0.0;
double progressstep = [urls count] ? 100.0 / (double)([urls count]) : 0;
NSURL *url;
for(url in urls) {
if([url isFileURL]) {
BOOL isDir;
if([[NSFileManager defaultManager] fileExistsAtPath:[url path] isDirectory:&isDir]) {
if(isDir == YES) {
// Get subpaths
2007-05-28 14:58:08 +00:00
[expandedURLs addObjectsFromArray:[self fileURLsAtPath:[url path]]];
} else {
[expandedURLs addObject:[NSURL fileURLWithPath:[url path]]];
}
}
} else {
// Non-file URL..
2007-05-28 14:58:08 +00:00
[expandedURLs addObject:url];
}
progress += progressstep;
[self setProgressJobStatus:progress];
}
[self completeProgressJob];
progress = 0.0;
DLog(@"Expanded urls: %@", expandedURLs);
2007-05-28 14:58:08 +00:00
NSArray *sortedURLs;
if(sort == YES) {
sortedURLs = [expandedURLs sortedArrayUsingSelector:@selector(finderCompare:)];
// sortedURLs = [expandedURLs sortedArrayUsingSelector:@selector(compareTrackNumbers:)];
} else {
2009-02-28 17:41:37 +00:00
sortedURLs = expandedURLs;
2007-05-28 14:58:08 +00:00
}
[self beginProgressJob:NSLocalizedString(@"ProgressSubActionLoaderFilteringContainerFiles", @"handling container file types") percentOfTotal:20.0];
progressstep = [sortedURLs count] ? 100.0 / (double)([sortedURLs count]) : 0;
for(url in sortedURLs) {
// Container vs non-container url
if([[self acceptableContainerTypes] containsObject:[[url pathExtension] lowercaseString]]) {
NSArray *urls = [AudioContainer urlsForContainerURL:url];
if(urls != nil && [urls count] != 0) {
[containedURLs addObjectsFromArray:urls];
// Make sure the container isn't added twice.
[uniqueURLs addObject:url];
} else {
/* Fall back on adding the raw file if all container parsers have failed. */
[fileURLs addObject:url];
}
} else if([[[url pathExtension] lowercaseString] isEqualToString:@"xml"]) {
xmlData = [XmlContainer entriesForContainerURL:url];
} else {
2007-10-19 02:23:10 +00:00
[fileURLs addObject:url];
2007-05-28 14:58:08 +00:00
}
progress += progressstep;
[self setProgressJobStatus:progress];
2007-05-28 14:58:08 +00:00
}
progress = 0.0;
[self completeProgressJob];
if([fileURLs count] > 0) {
[self beginProgressJob:NSLocalizedString(@"ProgressSubActionLoaderFilteringFiles", @"eliminating unsupported file types") percentOfTotal:20.0];
} else {
[self setProgressStatus:60.0];
}
2007-05-28 14:58:08 +00:00
DLog(@"File urls: %@", fileURLs);
2007-10-19 02:23:10 +00:00
DLog(@"Contained urls: %@", containedURLs);
2007-10-19 02:23:10 +00:00
progressstep = [fileURLs count] ? 100.0 / (double)([fileURLs count]) : 0;
for(url in fileURLs) {
progress += progressstep;
if(![[AudioPlayer schemes] containsObject:[url scheme]])
continue;
NSString *ext = [[url pathExtension] lowercaseString];
// Need a better way to determine acceptable file types than basing it on extensions.
if([url isFileURL] && ![[AudioPlayer fileTypes] containsObject:ext])
continue;
if(![uniqueURLs containsObject:url]) {
[validURLs addObject:url];
[uniqueURLs addObject:url];
}
[self setProgressJobStatus:progress];
}
progress = 0.0;
if([fileURLs count] > 0) {
[self completeProgressJob];
}
if([containedURLs count] > 0) {
[self beginProgressJob:NSLocalizedString(@"ProgressSubActionLoaderFilteringContainedFiles", @"eliminating unsupported file types from containers") percentOfTotal:20.0];
} else {
[self setProgressStatus:80.0];
}
DLog(@"Valid urls: %@", validURLs);
2007-10-19 02:23:10 +00:00
progressstep = [containedURLs count] ? 100.0 / (double)([containedURLs count]) : 0;
for(url in containedURLs) {
progress += progressstep;
if(![[AudioPlayer schemes] containsObject:[url scheme]])
2007-10-19 02:23:10 +00:00
continue;
// Need a better way to determine acceptable file types than basing it on extensions.
if([url isFileURL] && ![[AudioPlayer fileTypes] containsObject:[[url pathExtension] lowercaseString]])
2007-10-19 02:23:10 +00:00
continue;
[validURLs addObject:url];
[self setProgressJobStatus:progress];
}
progress = 0.0;
if([containedURLs count] > 0) {
[self completeProgressJob];
}
// Create actual entries
int count = (int)[validURLs count];
if(xmlData) count += [[xmlData objectForKey:@"entries"] count];
// no valid URLs, or they use an unsupported URL scheme
if(!count) {
[self completeProgress];
return @[];
}
[self beginProgressJob:NSLocalizedString(@"ProgressSubActionLoaderAddingEntries", @"creating and adding playlist entries") percentOfTotal:20.0];
progressstep = 100.0 / (double)(count);
NSInteger i = 0;
NSMutableArray *entries = [NSMutableArray arrayWithCapacity:count];
for(NSURL *url in validURLs) {
PlaylistEntry *pe = [NSEntityDescription insertNewObjectForEntityForName:@"PlaylistEntry" inManagedObjectContext:playlistController.persistentContainer.viewContext];
pe.url = url;
pe.index = index + i;
pe.rawTitle = [[url path] lastPathComponent];
pe.queuePosition = -1;
[entries addObject:pe];
++i;
progress += progressstep;
[self setProgressJobStatus:progress];
}
NSInteger j = index + i;
if(xmlData) {
for(NSDictionary *entry in [xmlData objectForKey:@"entries"]) {
PlaylistEntry *pe = [NSEntityDescription insertNewObjectForEntityForName:@"PlaylistEntry" inManagedObjectContext:playlistController.persistentContainer.viewContext];
[pe setValuesForKeysWithDictionary:entry];
pe.index = index + i;
pe.queuePosition = -1;
[entries addObject:pe];
++i;
}
}
[self completeProgress];
NSIndexSet *is = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(index, [entries count])];
[playlistController insertObjects:entries atArrangedObjectIndexes:is];
if(xmlData && [[xmlData objectForKey:@"queue"] count]) {
[playlistController emptyQueueList:self];
i = 0;
for(NSNumber *index in [xmlData objectForKey:@"queue"]) {
NSInteger indexVal = [index intValue] + j;
PlaylistEntry *pe = [entries objectAtIndex:indexVal];
pe.queuePosition = i;
pe.queued = YES;
[[playlistController queueList] addObject:pe];
++i;
}
}
// Clear the selection
[playlistController setSelectionIndexes:[NSIndexSet indexSet]];
{
NSArray *arrayFirst = @[[entries objectAtIndex:0]];
NSMutableArray *arrayRest = [entries mutableCopy];
[arrayRest removeObjectAtIndex:0];
metadataLoadInProgress = YES;
[self beginProgress:NSLocalizedString(@"ProgressActionLoadingMetadata", @"loading metadata for tracks")];
[self beginProgressJob:NSLocalizedString(@"ProgressSubActionLoadingMetadata", @"processing files") percentOfTotal:50.0];
[self performSelectorOnMainThread:@selector(syncLoadInfoForEntries:) withObject:arrayFirst waitUntilDone:YES];
progressstep = 100.0 / (double)([entries count]);
progress = progressstep;
[self setProgressJobStatus:progress];
if([arrayRest count])
[self performSelectorInBackground:@selector(loadInfoForEntries:) withObject:arrayRest];
else {
[playlistController commitPersistentStore];
[self completeProgress];
}
return entries;
}
}
- (void)loadInfoForEntries:(NSArray *)entries {
NSMutableIndexSet *update_indexes = [[NSMutableIndexSet alloc] init];
long i, j;
NSMutableIndexSet *load_info_indexes = [[NSMutableIndexSet alloc] init];
__block double progress = 0.0;
double progressstep;
if(metadataLoadInProgress && [entries count]) {
progressstep = 100.0 / (double)([entries count] + 1);
progress = progressstep;
} else if([entries count]) {
[self beginProgress:NSLocalizedString(@"ProgressActionLoadingMetadata", @"loading metadata for tracks")];
[self beginProgressJob:NSLocalizedString(@"ProgressSubActionLoadingMetadata", @"processing files") percentOfTotal:50.0];
progressstep = 100.0 / (double)([entries count]);
progress = 0.0;
}
i = 0;
j = 0;
for(PlaylistEntry *pe in entries) {
long idx = j++;
if([pe metadataLoaded]) continue;
[update_indexes addIndex:pe.index];
[load_info_indexes addIndex:idx];
++i;
}
if(!i) {
[playlistController performSelectorOnMainThread:@selector(updateTotalTime) withObject:nil waitUntilDone:NO];
[self completeProgress];
metadataLoadInProgress = NO;
return;
}
NSLock *outLock = [[NSLock alloc] init];
NSMutableArray *outArray = [[NSMutableArray alloc] init];
RedundantPlaylistDataStore *dataStore = [[RedundantPlaylistDataStore alloc] init];
__block NSLock *weakLock = outLock;
__block NSMutableArray *weakArray = outArray;
__block RedundantPlaylistDataStore *weakDataStore = dataStore;
{
[load_info_indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *_Nonnull stop) {
__block PlaylistEntry *weakPe = [entries objectAtIndex:idx];
NSBlockOperation *op = [[NSBlockOperation alloc] init];
[op addExecutionBlock:^{
if(weakPe.deLeted || !weakPe.url) {
[weakLock lock];
if(!weakPe.url) {
weakPe.error = YES;
weakPe.errorMessage = NSLocalizedString(@"ErrorMessageBadFile", @"");
}
progress += progressstep;
[self setProgressJobStatus:progress];
[weakLock unlock];
return;
}
DLog(@"Loading metadata for %@", weakPe.url);
[[FIRCrashlytics crashlytics] logWithFormat:@"Loading metadata for %@", weakPe.url];
NSDictionary *entryProperties = [AudioPropertiesReader propertiesForURL:weakPe.url];
if(entryProperties == nil)
return;
NSDictionary *entryMetadata = [AudioMetadataReader metadataForURL:weakPe.url];
NSDictionary *entryInfo = [NSDictionary dictionaryByMerging:entryProperties with:entryMetadata];
[weakLock lock];
entryInfo = [weakDataStore coalesceEntryInfo:entryInfo];
[weakArray addObject:weakPe];
[weakArray addObject:entryInfo];
progress += progressstep;
[self setProgressJobStatus:progress];
[weakLock unlock];
}];
[queue addOperation:op];
}];
}
[queue waitUntilAllOperationsAreFinished];
progress = 0.0;
[self completeProgressJob];
[self beginProgressJob:NSLocalizedString(@"ProgressSubActionMetadataApply", @"applying info to playlist storage") percentOfTotal:50.0];
progressstep = 200.0 / (double)([outArray count]);
for(i = 0, j = [outArray count]; i < j; i += 2) {
__block PlaylistEntry *weakPe = [outArray objectAtIndex:i];
__block NSDictionary *entryInfo = [outArray objectAtIndex:i + 1];
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
if(!weakPe.deLeted) {
[weakPe setMetadata:entryInfo];
}
progress += progressstep;
[self setProgressJobStatus:progress];
});
}
[playlistController commitPersistentStore];
[playlistController performSelectorOnMainThread:@selector(updateTotalTime) withObject:nil waitUntilDone:NO];
{
__block NSScrollView *weakPlaylistView = playlistView;
__block NSIndexSet *weakIndexSet = update_indexes;
dispatch_sync_reentrant(dispatch_get_main_queue(), ^{
unsigned long columns = [[[weakPlaylistView documentView] tableColumns] count];
[weakPlaylistView.documentView reloadDataForRowIndexes:weakIndexSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]];
});
}
[self completeProgress];
metadataLoadInProgress = NO;
}
// To be called on main thread only
- (void)syncLoadInfoForEntries:(NSArray *)entries {
NSMutableIndexSet *update_indexes = [[NSMutableIndexSet alloc] init];
long i, j;
NSMutableIndexSet *load_info_indexes = [[NSMutableIndexSet alloc] init];
i = 0;
j = 0;
for(PlaylistEntry *pe in entries) {
long idx = j++;
if([pe metadataLoaded]) continue;
[update_indexes addIndex:pe.index];
[load_info_indexes addIndex:idx];
++i;
}
if(!i) {
[self->playlistController updateTotalTime];
return;
}
[load_info_indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *_Nonnull stop) {
PlaylistEntry *pe = [entries objectAtIndex:idx];
DLog(@"Loading metadata for %@", pe.url);
[[FIRCrashlytics crashlytics] logWithFormat:@"Loading metadata for %@", pe.url];
NSDictionary *entryProperties = [AudioPropertiesReader propertiesForURL:pe.url];
if(entryProperties == nil)
return;
NSDictionary *entryInfo = [NSDictionary dictionaryByMerging:entryProperties with:[AudioMetadataReader metadataForURL:pe.url]];
[pe setMetadata:entryInfo];
}];
[self->playlistController updateTotalTime];
{
unsigned long columns = [[[self->playlistView documentView] tableColumns] count];
[self->playlistView.documentView reloadDataForRowIndexes:update_indexes columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]];
}
}
- (void)clear:(id)sender {
[playlistController clear:sender];
}
- (NSArray *)addURLs:(NSArray *)urls sort:(BOOL)sort {
return [self insertURLs:urls atIndex:(int)[[playlistController content] count] sort:sort];
}
- (NSArray *)addURL:(NSURL *)url {
return [self insertURLs:@[url] atIndex:(int)[[playlistController content] count] sort:NO];
}
- (BOOL)addDataStore {
NSPersistentContainer *pc = playlistController.persistentContainer;
if(pc) {
NSManagedObjectContext *moc = pc.viewContext;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"AlbumArtwork"];
NSError *error = nil;
NSArray *results = [moc executeFetchRequest:request error:&error];
if(!results) {
ALog(@"Error fetching AlbumArtwork objects: %@\n%@", [error localizedDescription], [error userInfo]);
abort();
}
for(AlbumArtwork *art in results) {
[__artworkDictionary setObject:art forKey:art.artHash];
}
request = [NSFetchRequest fetchRequestWithEntityName:@"PlaylistEntry"];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"index" ascending:YES];
request.sortDescriptors = @[sortDescriptor];
results = [moc executeFetchRequest:request error:&error];
if(!results) {
ALog(@"Error fetching PlaylistEntry objects: %@\n%@", [error localizedDescription], [error userInfo]);
abort();
}
if([results count] == 0) return NO;
NSMutableArray *resultsCopy = [results mutableCopy];
NSMutableIndexSet *pruneSet = [[NSMutableIndexSet alloc] init];
NSUInteger index = 0;
for(PlaylistEntry *pe in resultsCopy) {
if(pe.deLeted) {
[pruneSet addIndex:index];
[moc deleteObject:pe];
}
++index;
}
[resultsCopy removeObjectsAtIndexes:pruneSet];
if([pruneSet count]) {
[playlistController commitPersistentStore];
}
{
NSIndexSet *is = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [results count])];
[playlistController insertObjectsUnsynced:results atArrangedObjectIndexes:is];
}
[playlistController emptyQueueListUnsynced];
NSMutableDictionary *queueList = [[NSMutableDictionary alloc] init];
for(PlaylistEntry *pe in results) {
if(pe.queued && pe.queuePosition >= 0) {
NSString *queuePos = [NSString stringWithFormat:@"%llu", pe.queuePosition];
[queueList setObject:pe forKey:queuePos];
}
}
if([queueList count]) {
for(size_t i = 0, j = [queueList count]; i < j; ++i) {
NSString *queuePos = [NSString stringWithFormat:@"%zu", i];
PlaylistEntry *pe = [queueList objectForKey:queuePos];
[[playlistController queueList] addObject:pe];
}
}
return YES;
}
return NO;
}
- (NSArray *)addDatabase {
SQLiteStore *store = [SQLiteStore sharedStore];
int64_t count = [store playlistGetCount];
NSInteger i = 0;
NSMutableArray *entries = [NSMutableArray arrayWithCapacity:count];
for(i = 0; i < count; ++i) {
PlaylistEntry *pe = [store playlistGetCachedItem:i];
pe.queuePosition = -1;
[entries addObject:pe];
}
NSIndexSet *is = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [entries count])];
[playlistController insertObjectsUnsynced:entries atArrangedObjectIndexes:is];
count = [store queueGetCount];
if(count) {
NSMutableIndexSet *refreshSet = [[NSMutableIndexSet alloc] init];
[playlistController emptyQueueListUnsynced];
for(i = 0; i < count; ++i) {
NSInteger indexVal = [store queueGetEntry:i];
PlaylistEntry *pe = [entries objectAtIndex:indexVal];
pe.queuePosition = i;
pe.queued = YES;
[[playlistController queueList] addObject:pe];
[refreshSet addIndex:[pe index]];
}
// Refresh entire row to refresh tooltips
unsigned long columns = [[playlistView.documentView tableColumns] count];
[playlistView.documentView reloadDataForRowIndexes:refreshSet columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, columns)]];
}
// Clear the selection
[playlistController setSelectionIndexes:[NSIndexSet indexSet]];
if([entries count]) {
[self performSelectorInBackground:@selector(loadInfoForEntries:) withObject:entries];
}
return entries;
}
- (NSArray *)acceptableFileTypes {
return [[self acceptableContainerTypes] arrayByAddingObjectsFromArray:[AudioPlayer fileTypes]];
}
- (NSArray *)acceptablePlaylistTypes {
return @[@"m3u", @"pls"];
}
- (NSArray *)acceptableContainerTypes {
return [AudioPlayer containerTypes];
2007-03-07 01:26:50 +00:00
}
- (void)willInsertURLs:(NSArray *)urls origin:(URLOrigin)origin {
2009-02-28 22:22:33 +00:00
[playlistController willInsertURLs:urls origin:origin];
}
- (void)didInsertURLs:(NSArray *)urls origin:(URLOrigin)origin {
2009-02-28 22:22:33 +00:00
[playlistController didInsertURLs:urls origin:origin];
}
2007-03-07 01:26:50 +00:00
@end