Skip to content

Commit

Permalink
Add sanitize filenames for playlist folders and files. Cleanup more t…
Browse files Browse the repository at this point in the history
…odo's. Some reindentation.
  • Loading branch information
tattwamasi committed Apr 13, 2015
1 parent de3bcfc commit 018d1f6
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 60 deletions.
132 changes: 79 additions & 53 deletions TeslaTunes/CopyConvertDirs.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@

#include "flac_utils.h"

// NSString *disallowedCharsRegEx=@"[;:,/|!@#$%&*\()^]";
NSString *disallowedCharsRegEx=@"[/|]";

NSString *sanitizeFilename(NSString* f) {

NSString *sanitizedString = [f stringByReplacingOccurrencesOfString:disallowedCharsRegEx withString:@"_"
options:NSRegularExpressionSearch
range: NSMakeRange(0, [f length]) ];
return sanitizedString;
}

// make the destination filename out of the base path of destination and the relative path of the new item
NSURL* makeDestURL(const NSURL *dstBasePath, const NSURL *basePathToStrip, const NSURL* srcURL) {
// Need to create the destination file/dir URL to check it's existance. Use dst and the relative path
Expand All @@ -24,23 +35,28 @@
// May well be a better way. Till then...
NSArray *basePathComponents = [basePathToStrip pathComponents];
NSArray *srcPathComponents = [srcURL pathComponents];

NSRange relPathRange;

// sanity check
relPathRange.location = 0;
relPathRange.length = basePathComponents.count; // start the relative path at the end of the base path

if ((srcPathComponents.count <= basePathComponents.count) || ![basePathComponents isEqualToArray: [srcPathComponents subarrayWithRange:relPathRange]]) {
NSLog(@"When stripping base path from source filename to create destination filename, detected base path wasn't actually a common path.\nBase path was \"%@\".\nSource path was \"%@\".\n", basePathToStrip, srcURL);

if ((srcPathComponents.count <= basePathComponents.count) ||
![basePathComponents isEqualToArray: [srcPathComponents subarrayWithRange:relPathRange]]) {
NSLog(@"When stripping base path from source filename to create destination filename, "
"detected base path wasn't actually a common path.\nBase path was \"%@\".\nSource path was \"%@\".\n",
basePathToStrip, srcURL);
// set NSRange object to take whole srcURL
relPathRange.location = 0;
relPathRange.length = srcPathComponents.count;
} else {
relPathRange.location = basePathComponents.count; // start the relative path at the end of the base path
relPathRange.length = srcPathComponents.count - basePathComponents.count;
}
NSURL *dstURL = [NSURL fileURLWithPathComponents:[[dstBasePath pathComponents] arrayByAddingObjectsFromArray:[srcPathComponents subarrayWithRange:relPathRange]]];
NSURL *dstURL = [NSURL fileURLWithPathComponents:[[dstBasePath pathComponents]
arrayByAddingObjectsFromArray:[srcPathComponents
subarrayWithRange:relPathRange]]];

return dstURL;
}
Expand Down Expand Up @@ -113,7 +129,7 @@ @interface CopyConvertDirs ()
@implementation CopyConvertDirs {

NSSet *extensionsToCopy;

NSMutableArray *convertOps;
NSMutableArray *copyOps;
NSMutableArray *delOps;
Expand All @@ -129,7 +145,7 @@ - (instancetype)init {
_skippedExtensions=nil;
_copiedExtensions=nil;
_filesChecked=0;

convertOps = nil;
copyOps = nil;
delOps = nil;
Expand All @@ -139,21 +155,21 @@ - (instancetype)init {
// NSOperationQueueDefaultMaxConcurrentOperationCount was default but was creating what seemed to be a large number of threads
opSubQ.maxConcurrentOperationCount = 4;
opSubQ.name = @"TeslaTunes subprocessing queue";


// file types (types, not extensions) Tesla will play: mp3, mp4/aac, flac, wma, wma lossless, aiff (16 bit), wav
// Per an email from Tesla, .MP3 .OGG .OGA .FLAC .MPC .WV .SPX .TTA .M4A .M4B .M4P .MP4 .3G2 .WMA .ASF .AIF .AIFF .WAV .APE .AAC

extensionsToCopy = [NSSet setWithObjects:@"mp3", @"aac", @"m4a", @"flac", @"wma", @"aiff", @"wav", nil];

}
return self;
}

- (void) cancelOngoingOperations {
isCancelled = YES;
self.scanReady = NO;

NSLog(@"Cancelling all ongoing operations");
[opSubQ cancelAllOperations];
[queue cancelAllOperations];
Expand All @@ -162,7 +178,8 @@ - (void) cancelOngoingOperations {
// NSLog(@"OpQueue says all operations are cancelled. And isProcessing is %hhd", self.isProcessing);
}
// Uses NSOperationQueue and NSOperation to concurrently run. TODO: delegate or something to indicate when finished,etc.
- (void) startOperationOnDir: (DirOperation) opType withPlaylistSelections:(const PlaylistSelections *)playlistSelections andSourceDir:(const NSURL *)src toDestDir:(const NSURL *)dst {
- (void) startOperationOnDir: (DirOperation) opType withPlaylistSelections:(const PlaylistSelections *)playlistSelections
andSourceDir:(const NSURL *)src toDestDir:(const NSURL *)dst {
// if there are any operations still going on the queue, then this was an error to call. Log and return.
if (!queue || [queue operationCount]) {
NSLog(@"Error, tried to start directory operations when operation queue was %s", queue? "not created": "not empty");
Expand All @@ -174,15 +191,15 @@ - (void) startOperationOnDir: (DirOperation) opType withPlaylistSelections:(cons
_skippedExtensions = [[NSCountedSet alloc] init ];
_copiedExtensions = [[NSCountedSet alloc] init];


// if operation is to scan, then clear out any current list of pending operations to do
switch (opType) {
case CCScan:
case CCProcessWhileScanning: {
[queue addOperationWithBlock:^(void){
self.isProcessing = YES;
[self processOpsWithPlaylistSelections: playlistSelections andSourceDirectoryURL:src
toDestinationDirectoryURL:dst performScanOnly:(opType==CCScan)];
toDestinationDirectoryURL:dst performScanOnly:(opType==CCScan)];
[opSubQ waitUntilAllOperationsAreFinished];
self.isProcessing = NO;
}];
Expand All @@ -202,7 +219,7 @@ - (void) startOperationOnDir: (DirOperation) opType withPlaylistSelections:(cons
break;
}



}

Expand Down Expand Up @@ -238,7 +255,8 @@ -(void) convertWithSource:(const NSURL*)s Destination:(const NSURL*)d {
[opSubQ addOperationWithBlock:^(void){
NSError *theError;
NSFileManager *fileManager = [NSFileManager defaultManager];
if (NO ==[fileManager createDirectoryAtURL:[d URLByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:&theError]) {
if (NO ==[fileManager createDirectoryAtURL:[d URLByDeletingLastPathComponent]
withIntermediateDirectories:YES attributes:nil error:&theError]) {
// createDirectory returns YES even if the dir already exists because
// the "withIntermediateDirectories" flag is set, so if it fails, it's a real issue
NSLog(@"Couldn't make target directory, error was domain %@, desc %@ - fail reason %@, code (%ld)",
Expand All @@ -255,7 +273,7 @@ -(void) convertWithSource:(const NSURL*)s Destination:(const NSURL*)d {
}
}];
//NSLog(@"convert queued, current opSubQ depth:%lu", (unsigned long)opSubQ.operationCount);

}


Expand All @@ -265,7 +283,8 @@ -(void) copyWithSource:(const NSURL*)s Destination:(const NSURL*)d {
[opSubQ addOperationWithBlock:^(void){
NSError *theError;
NSFileManager *fileManager = [NSFileManager defaultManager];
if (NO ==[fileManager createDirectoryAtURL:[d URLByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:&theError]) {
if (NO ==[fileManager createDirectoryAtURL:[d URLByDeletingLastPathComponent]
withIntermediateDirectories:YES attributes:nil error:&theError]) {
// createDirectory returns YES even if the dir already exists because
// the "withIntermediateDirectories" flag is set, so if it fails, it's a real issue
// TODO: in one run early in development, the above statement was not true - got
Expand Down Expand Up @@ -320,7 +339,7 @@ - (void) processScannedItems {
// Returns the URL of the processed file at the destination, or nil in the event of error/cancellation
// TODO: check returns of copys/converts and return appropriately
- (NSURL *) processFileURL:(const NSURL *) file toDestination: destinationFile
performScanOnly: (BOOL) scanOnly setGenre: (NSString*) genre {
performScanOnly: (BOOL) scanOnly setGenre: (NSString*) genre {
@autoreleasepool {
if (isCancelled) return nil;
NSString *filename;
Expand Down Expand Up @@ -385,18 +404,18 @@ - (BOOL) pruneFilesNotInSet: (const NSSet*) fileSet inDirectory: (const NSURL*)
includingPropertiesForKeys:@[NSURLNameKey, NSURLIsDirectoryKey]
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:^BOOL(NSURL *url, NSError *error)
{
// this could happen without it being an error in the event the
// destination playlist doesn't exist yet, for example when doing
// a scan only for the first time on a playlist. If that's the case,
// it isn't an error, and there obviously won't be anything to remove.
if (error && !scanOnly) {
NSLog(@"Error when looking for destination playlist folder to prune [Error] %@ (%@)", error, url);
return NO;
}
return YES;
}];
{
// this could happen without it being an error in the event the
// destination playlist doesn't exist yet, for example when doing
// a scan only for the first time on a playlist. If that's the case,
// it isn't an error, and there obviously won't be anything to remove.
if (error && !scanOnly) {
NSLog(@"Error when looking for destination playlist folder to prune [Error] %@ (%@)", error, url);
return NO;
}

return YES;
}];

for (NSURL *fileURL in enumerator) {
if (isCancelled) {
Expand All @@ -415,7 +434,7 @@ - (BOOL) pruneFilesNotInSet: (const NSSet*) fileSet inDirectory: (const NSURL*)
// TODO: see above
NSLog(@"Couldn't remove from playlist folder item URL \"%@\".", [fileURL standardizedURL]);
}

}
}
}
Expand All @@ -430,14 +449,13 @@ - (BOOL) pruneFilesNotInSet: (const NSSet*) fileSet inDirectory: (const NSURL*)
// playlist order and eliminates collisions. Also, delete any other files in the folder
// that weren't in the playlist.
// Return NO if processing was interrupted, either by error, or by cancel flag being set, YES otherwise
//
// TODO: scan and replace illegal characters in playlist name and constructed filenames
- (BOOL) processPlaylistNode:(PlaylistNode *) node toDestinationDirectoryURL: destinationDir
performScanOnly: (BOOL) scanOnly {
NSURL *destinationFolderForPlaylist = [destinationDir URLByAppendingPathComponent:node.playlist.name];
NSURL *destinationFolderForPlaylist = [destinationDir
URLByAppendingPathComponent: sanitizeFilename(node.playlist.name)];
//NSLog(@"playlist %@ was selected and will be copied to %s", node.playlist.name, destinationFolderForPlaylist.fileSystemRepresentation);
NSMutableSet *playlistFilenames = [[NSMutableSet alloc] init];

int idx = 0;
NSUInteger number = node.playlist.items.count;
int digits = 0; do { number /= 10; digits++; } while (number != 0);
Expand All @@ -451,12 +469,16 @@ - (BOOL) processPlaylistNode:(PlaylistNode *) node toDestinationDirectoryURL: de
// if it is not.
if (!track.location) {
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = [NSString stringWithFormat:@"The track \"%@\" in playlist \"%@\" has no location specified. Do you want to skip this track, or stop processing altogether so you can try to fix the issue and start over?", track.title, node.playlist.name];
alert.informativeText = @"This can happen, for example, when you have your library stored on a networked or external drive and the drive isn't currently available. Make sure it is, and check that iTunes can play the track(s).";
alert.messageText = [NSString stringWithFormat:
@"The track \"%@\" in playlist \"%@\" has no location specified. Do you want to skip this track, "
"or stop processing altogether so you can try to fix the issue and start over?",
track.title, node.playlist.name];
alert.informativeText = @"This can happen, for example, when you have your library stored on a networked or external "
"drive and the drive isn't currently available. Make sure it is, and check that iTunes can play the track(s).";
[alert addButtonWithTitle:@"Stop processing"];
[alert addButtonWithTitle:@"Skip track"];

__block NSModalResponse response;
__block NSModalResponse response;
dispatch_sync(dispatch_get_main_queue(), ^(){
response = [alert runModal];
});
Expand All @@ -476,11 +498,10 @@ - (BOOL) processPlaylistNode:(PlaylistNode *) node toDestinationDirectoryURL: de
// in order to preserve the ability to have duplicates in the playlist and play in order.

// so given that, example filename is index-trackname-trackartist-trackalbum.extension
// Note: TODO: scan the result and replace any illegal characters.

NSString *filename = [NSString stringWithFormat:@"%0*d-%@-%@-%@.%@", digits,
idx, track.title, track.artist.name, track.album.title,
[track.location pathExtension] ];
NSString *filename = sanitizeFilename([NSString stringWithFormat:@"%0*d-%@-%@-%@.%@", digits,
idx, track.title, track.artist.name, track.album.title,
[track.location pathExtension] ]);

NSURL *destFileURL = [destinationFolderForPlaylist URLByAppendingPathComponent: filename];

#if 0
Expand All @@ -490,7 +511,8 @@ - (BOOL) processPlaylistNode:(PlaylistNode *) node toDestinationDirectoryURL: de
#endif

if (track.location) {
NSURL *result = [self processFileURL:track.location toDestination: destFileURL performScanOnly:scanOnly setGenre:self.hackGenre? node.playlist.name:nil];
NSURL *result = [self processFileURL:track.location toDestination: destFileURL performScanOnly:scanOnly
setGenre:self.hackGenre? node.playlist.name:nil];
if (!result) return NO;
[playlistFilenames addObject:[result lastPathComponent]];

Expand All @@ -507,12 +529,12 @@ - (BOOL) processPlaylistNode:(PlaylistNode *) node toDestinationDirectoryURL: de


- (void) processOpsWithPlaylistSelections: (const PlaylistSelections *)playlistSelections
andSourceDirectoryURL: (const NSURL *) sourceDir
toDestinationDirectoryURL: (const NSURL *) destinationDir
performScanOnly: (BOOL) scanOnly {
andSourceDirectoryURL: (const NSURL *) sourceDir
toDestinationDirectoryURL: (const NSURL *) destinationDir
performScanOnly: (BOOL) scanOnly {
self.filesChecked = 0;
self.filesToCopyConvert=0;


// if we're doing a scan only, make new mutable arrays for the convert and copy ops, otherwise make sure they are nil
if (scanOnly) {
Expand All @@ -526,6 +548,9 @@ - (void) processOpsWithPlaylistSelections: (const PlaylistSelections *)playlistS
delOps = nil;
}

NSURL *musicFolderURL = [destinationDir URLByAppendingPathComponent:@"Music"];
NSURL *playlistFolderURL = [destinationDir URLByAppendingPathComponent:@"Playlists"];


if (sourceDir) {
NSFileManager *fileManager = [NSFileManager defaultManager];
Expand All @@ -543,6 +568,7 @@ - (void) processOpsWithPlaylistSelections: (const PlaylistSelections *)playlistS

return YES;
}];

for (NSURL *fileURL in enumerator) {
if (isCancelled) break;
@autoreleasepool {
Expand All @@ -552,7 +578,7 @@ - (void) processOpsWithPlaylistSelections: (const PlaylistSelections *)playlistS
NSNumber *isDirectory;
[fileURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil];

NSURL* destFileURL = makeDestURL(destinationDir, sourceDir, fileURL);
NSURL* destFileURL = makeDestURL(musicFolderURL, sourceDir, fileURL);

// PLACEHOLDER - todo make a map of extensions/filetypes to handler operations

Expand All @@ -578,10 +604,10 @@ - (void) processOpsWithPlaylistSelections: (const PlaylistSelections *)playlistS
PlaylistNode *playlistTree = [playlistSelections getTree];
[playlistTree enumerateTreeUsingBlock:^(PlaylistNode *node, BOOL *stop) {
if (node.playlist && ([node.selectedState integerValue] == NSOnState) && node.playlist.items) {
if (![self processPlaylistNode:node toDestinationDirectoryURL:destinationDir performScanOnly:scanOnly]) {
if (![self processPlaylistNode:node toDestinationDirectoryURL: playlistFolderURL performScanOnly:scanOnly]) {
*stop = YES;
}

}
}];
}
Expand Down
6 changes: 0 additions & 6 deletions TeslaTunes/PlaylistSelections.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,6 @@
// since I didn't use bindings (after having trouble with them originally - I think they'd actually work fine now), and since
// each of the individual parts is simple, but involves the others, kind of seems like I should just put all this in the view controller.
//
// Also, make it so the selected dictionary is read from user defaults and used when the tree is built to pre-select previous selections,
// if still existing, then don't use the dict anymore. Make a new dict when the selection window goes away and save that to user defaults.
//
// Make methods to get data required out of the tree - probably just an enumerator returning the media entries and playlist names - or
// maybe just the playlist name and path to file
//
// Other todo - fix UI constraints. Make playlist tree look better - how? consider side/detail windows(s) as alternate design, with
// playlist selections, etc. on left, and operations details/status/progress right.
// Integrate playlist operations into what happens when you click do it.
Expand Down
Loading

0 comments on commit 018d1f6

Please sign in to comment.