source: trunk/SapphireMetaData.m @ 292

Revision 292, 58.9 KB checked in by gbooker, 6 years ago (diff)

Added ability to join together multiple files into one. Great for movies which were segmented.

Line 
1//
2//  SapphireMetaData.m
3//  Sapphire
4//
5//  Created by Graham Booker on 6/22/07.
6//  Copyright 2007 www.nanopi.net. All rights reserved.
7//
8
9#import "SapphireMetaData.h"
10#import <QTKit/QTKit.h>
11#include <sys/types.h>
12#include <sys/stat.h>
13#import "SapphireSettings.h"
14#import "SapphirePredicates.h"
15#import "SapphireMetaDataScanner.h"
16
17//Structure Specific Keys
18#define FILES_KEY                                       @"Files"
19#define DIRS_KEY                                        @"Dirs"
20#define META_VERSION_KEY                        @"Version"
21#define META_COLLECTION_OPTIONS         @"Options"
22#define META_COLLECTION_HIDE            @"Hide"
23#define META_COLLECTION_SKIP_SCAN       @"Skip"
24#define META_FILE_VERSION                       2
25#define META_COLLECTION_VERSION         4
26
27//File Specific Keys
28#define MODIFIED_KEY                            @"Modified"
29#define WATCHED_KEY                                     @"Watched"
30#define FAVORITE_KEY                            @"Favorite"
31#define RESUME_KEY                                      @"Resume Time"
32#define SIZE_KEY                                        @"Size"
33#define DURATION_KEY                            @"Duration"
34#define AUDIO_DESC_KEY                          @"Audio Description"
35#define SAMPLE_RATE_KEY                         @"Sample Rate"
36#define VIDEO_DESC_KEY                          @"Video Description"
37#define AUDIO_FORMAT_KEY                        @"Audio Format"
38#define JOINED_FILE_KEY                         @"Joined File"
39//#define FILE_CLASS_KEY                                @"File Class"
40
41@implementation NSString (episodeSorting)
42
43/*!
44 * @brief Comparison for episode names
45 *
46 * @param other The other string to compare
47 * @return The comparison result of the compare
48 */
49- (NSComparisonResult) directoryNameCompare:(NSString *)other
50{
51        return [self compare:other options:NSCaseInsensitiveSearch | NSNumericSearch];
52}
53
54@end
55
56@interface SapphireDirectoryMetaData (private)
57- (void)reloadDirectoryContents;
58- (SapphireFileMetaData *)cachedMetaDataForFile:(NSString *)file;
59- (void)invokeRecursivelyOnFiles:(NSInvocation *)fileInv withPredicate:(SapphirePredicate *)predicate;
60@end
61
62@implementation SapphireMetaData
63
64// Static set of file extensions to filter
65static NSSet *videoExtensions = nil;
66static NSSet *audioExtensions = nil;
67static NSSet *allExtensions = nil;
68
69+(void)load
70{
71        videoExtensions = [[NSSet alloc] initWithObjects:
72                @"avi", @"divx", @"xvid",
73                @"mov",
74                @"mpg", @"mpeg", @"m2v", @"ts",
75                @"wmv", @"asx", @"asf",
76                @"mkv",
77                @"flv",
78                @"mp4", @"m4v",
79                @"3gp",
80                @"pls",
81                @"avc",
82                @"ogm",
83                @"dv",
84                @"fli",
85                nil];
86        audioExtensions = [[NSSet alloc] initWithObjects:
87                @"m4b", @"m4a",
88                @"mp3", @"mp2",
89                @"wma",
90                @"wav",
91                @"aif", @"aiff",
92                @"flac",
93                @"alac",
94                @"m3u",
95                @"ac3",
96                nil];
97        NSMutableSet *mutSet = [videoExtensions mutableCopy];
98        [mutSet unionSet:audioExtensions];
99        allExtensions = [[NSSet alloc] initWithSet:mutSet];
100        [mutSet release];
101}
102
103/*!
104 * @brief Returns a set of all the video extensions
105 *
106 * @return The set of all video extensions
107 */
108+ (NSSet *)videoExtensions
109{
110        return videoExtensions;
111}
112
113/*!
114 * @brief Returns a set of all the audio extensions
115 *
116 * @return The set of all audio extensions
117 */
118+ (NSSet *)audioExtensions
119{
120        return audioExtensions;
121}
122
123/*!
124 * @brief Creates a new meta data object
125 *
126 * @param dict The configuration dictionary.  Note, this dictionary is copied and the copy is modified
127 * @param myParent The parent meta data
128 * @param The path for this meta data
129 * @return The meta data object
130 */
131- (id)initWithDictionary:(NSDictionary *)dict parent:(SapphireMetaData *)myParent path:(NSString *)myPath
132{
133        self = [super init];
134        if(!self)
135                return nil;
136       
137        /*Create the mutable dictionary*/
138        if(dict == nil)
139                metaData = [NSMutableDictionary new];
140        else
141                metaData = [dict retain];
142        path = [myPath retain];
143        parent = myParent;
144       
145        return self;
146}
147
148- (void)dealloc
149{
150        [metaData release];
151        [path release];
152        [super dealloc];
153}
154
155- (void)childDictionaryChanged:(SapphireMetaData *)child
156{
157}
158
159- (void)replaceInfoWithDict:(NSDictionary *)dict
160{
161        NSMutableDictionary *newDict = [dict mutableCopy];
162        [metaData release];
163        metaData = newDict;
164        [parent childDictionaryChanged:self];
165}
166
167/*!
168 * @brief Returns the mutable dictionary object containing all the meta data
169 *
170 * @return The dictionary
171 */
172- (NSMutableDictionary *)dict
173{
174        return metaData;
175}
176
177/*!
178 * @brief Returns the path of the current meta data
179 *
180 * @return The path
181 */
182- (NSString *)path
183{
184        return path;
185}
186
187/*!
188 * @brief Sets the delegate for the meta data
189 *
190 * @param newDelegate The new delegate
191 */
192- (void)setDelegate:(id <SapphireMetaDataDelegate>)newDelegate
193{
194        delegate = newDelegate;
195}
196
197/*!
198 * @brief Write all the meta data to a file.  This function is called on the parents
199 */
200- (void)writeMetaData
201{
202        [parent writeMetaData];
203}
204
205- (SapphireMetaDataCollection *)collection
206{
207        return nil;
208}
209
210- (BOOL)isDirectory:(NSString *)fullPath
211{
212        BOOL isDir = NO;
213        return [[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDir] && isDir;
214}
215
216/*!
217 * @brief Get the meta data for display
218 *
219 * @param order A pointer to an NSArray * in which to store the order in which the meta data is to be displayed
220 * @return The display meta data with the titles as keys
221 */
222- (NSMutableDictionary *)getDisplayedMetaDataInOrder:(NSArray * *)order
223{
224        return nil;
225}
226
227@end
228
229@interface SapphireMetaDataCollection (private)
230- (SapphireMetaData *)dataForSubPath:(NSString *)absPath inDirectory:(SapphireDirectoryMetaData *)directory;
231- (void)linkCollections;
232- (void)realWriteMetaData;
233@end
234
235@implementation SapphireMetaDataCollection
236
237- (void)insertDictionary:(NSDictionary *)dict atPath:(NSMutableArray *)pathComponents withinDictionary:(NSMutableDictionary *)source
238{
239        NSString *element = [pathComponents firstObject];
240        NSMutableDictionary *dir = [source objectForKey:element];
241        if(dir == nil)
242                dir = [[NSMutableDictionary alloc] init];
243        else
244                dir = [dir mutableCopy];
245        [source setObject:dir forKey:element];
246        [dir release];
247        if([pathComponents count] == 1)
248        {
249                /* insert here */
250                [dir setDictionary:dict];
251        }
252        else
253        {
254                NSMutableDictionary *dirs = [dir objectForKey:DIRS_KEY];
255                if(dirs == nil)
256                        dirs = [[NSMutableDictionary alloc] init];
257                else
258                        dirs = [dirs mutableCopy];
259                [dir setObject:dirs forKey:DIRS_KEY];
260                [dirs release];
261               
262                [pathComponents removeObjectAtIndex:0];
263                [self insertDictionary:dict atPath:pathComponents withinDictionary:dirs];
264        }
265}
266
267- (int)upgradeFromVersion1
268{
269        NSString *oldRoot = [NSHomeDirectory() stringByAppendingPathComponent:@"Movies"];
270        [metaData removeObjectForKey:META_VERSION_KEY];
271        NSMutableDictionary *newRoot = [NSMutableDictionary new];
272        NSMutableArray *pathComponents = [[oldRoot pathComponents] mutableCopy];
273        [self insertDictionary:metaData atPath:pathComponents withinDictionary:newRoot];
274        [pathComponents release];
275        metaData = newRoot;
276       
277        return 3;
278}
279
280- (int)finalUpgradeFromVersion1
281{
282        NSString *oldRoot = [NSHomeDirectory() stringByAppendingPathComponent:@"Movies"];
283        [(SapphireDirectoryMetaData *)[self dataForPath:oldRoot] setToImportFromSource:META_TVRAGE_IMPORT_KEY forPredicate:nil];
284
285        return 3;
286}
287
288- (int)upgradeFromVersion2
289{
290        NSString *oldRoot = [NSHomeDirectory() stringByAppendingPathComponent:@"Movies"];
291        NSMutableArray *pathComponents = [[oldRoot pathComponents] mutableCopy];
292        NSDictionary *info = [metaData objectForKey:oldRoot];
293        [self insertDictionary:info atPath:pathComponents withinDictionary:metaData];
294        [pathComponents release];
295        [metaData removeObjectForKey:oldRoot];
296        return 3;
297}
298
299- (int)finalUpgradeFromVersion2
300{
301        return 3;
302}
303
304void recurseSetFileClass(NSMutableDictionary *metaData)
305{
306        if(metaData == nil)
307                return;
308       
309        NSMutableDictionary *dirs = [metaData objectForKey:DIRS_KEY];
310        if(dirs != nil)
311        {
312                NSEnumerator *dirEnum = [dirs keyEnumerator];
313                NSString *dir = nil;
314                while((dir = [dirEnum nextObject]) != nil)
315                        recurseSetFileClass([dirs objectForKey:dir]);           
316        }
317       
318        NSMutableDictionary *files = [metaData objectForKey:FILES_KEY];
319        if(files == nil)
320                return;
321       
322        NSEnumerator *fileEnum = [files keyEnumerator];
323        NSString *file = nil;
324        while((file = [fileEnum nextObject]) != nil)
325        {
326                NSMutableDictionary *fileDict = [files objectForKey:file];
327                int epNum = [[[fileDict objectForKey:META_TVRAGE_IMPORT_KEY] objectForKey:META_EPISODE_NUMBER_KEY] intValue];
328                FileClass fileCls = [[fileDict objectForKey:FILE_CLASS_KEY] intValue];
329                if(epNum != 0 && fileCls == FILE_CLASS_UNKNOWN)
330                        [fileDict setObject:[NSNumber numberWithInt:FILE_CLASS_TV_SHOW] forKey:FILE_CLASS_KEY];
331        }
332}
333
334- (int)upgradeFromVersion3
335{
336        recurseSetFileClass([metaData objectForKey:@"/"]);
337        return 4;
338}
339
340- (int)finalUpgradeFromVersion3
341{
342        return 4;
343}
344
345/*!
346 * @brief Create a collection from a file and browsing a directory
347 *
348 * @param dictionary The path to the dictionary storing the meta data
349 * @param myPath The path to browse for the meta data
350 * @return The meta data collection
351 */
352- (id)initWithFile:(NSString *)dictionary
353{
354        /*Read the meta data*/
355        dictionaryPath = [dictionary retain];
356        NSData *fileData = [NSData dataWithContentsOfFile:dictionary];
357        NSString *error = nil;
358        NSMutableDictionary *mainDict = [NSPropertyListSerialization propertyListFromData:fileData mutabilityOption:NSPropertyListMutableContainersAndLeaves format:NULL errorDescription:&error];
359        [error release];
360        if(mainDict == nil)
361                mainDict = [NSMutableDictionary dictionary];
362        self = [super initWithDictionary:mainDict parent:nil path:nil];
363        if(!self)
364                return nil;
365       
366        /*Version upgrade*/
367        int version = [[metaData objectForKey:META_VERSION_KEY] intValue];
368        int oldVersion = version;
369
370        if(version < META_COLLECTION_VERSION)
371        {
372                if(version < 2)
373                        version = [self upgradeFromVersion1];
374                if(version < 3)
375                        version = [self upgradeFromVersion2];
376                if(version < 4)
377                        version = [self upgradeFromVersion3];
378        }
379        /*version it*/
380        [metaData setObject:[NSNumber numberWithInt:META_COLLECTION_VERSION] forKey:META_VERSION_KEY];
381
382        NSMutableDictionary *collectionOptions = [[metaData objectForKey:META_COLLECTION_OPTIONS] mutableCopy];
383        if(collectionOptions == nil)
384                collectionOptions = [[NSMutableDictionary alloc] init];
385        [metaData setObject:collectionOptions forKey:META_COLLECTION_OPTIONS];
386        [collectionOptions release];
387       
388        skipCollection = [[collectionOptions objectForKey:META_COLLECTION_SKIP_SCAN] mutableCopy];
389        if(skipCollection == nil)
390                skipCollection = [[NSMutableDictionary alloc] init];
391        [collectionOptions setObject:skipCollection forKey:META_COLLECTION_SKIP_SCAN];
392       
393        hideCollection = [[collectionOptions objectForKey:META_COLLECTION_HIDE] mutableCopy];
394        if(hideCollection == nil)
395                hideCollection = [[NSMutableDictionary alloc] init];
396        [collectionOptions setObject:hideCollection forKey:META_COLLECTION_HIDE];
397       
398        directories = [[NSMutableDictionary alloc] init];
399       
400        /* Hide and skip the / collection */
401        [self setHide:YES forCollection:@"/"];
402        [self setSkip:YES forCollection:@"/"];
403        SapphireDirectoryMetaData *slash = [[SapphireDirectoryMetaData alloc] initWithDictionary:[metaData objectForKey:@"/"] parent:self path:@"/"];
404        [metaData setObject:[slash dict] forKey:@"/"];
405        [directories setObject:slash forKey:@"/"];
406        [slash release];
407        [self linkCollections];
408        if(oldVersion < META_COLLECTION_VERSION)
409        {
410                if(oldVersion < 2)
411                        oldVersion = [self finalUpgradeFromVersion1];
412                if(oldVersion < 3)
413                        oldVersion = [self finalUpgradeFromVersion2];
414                if(oldVersion < 4)
415                        oldVersion = [self finalUpgradeFromVersion3];
416        }
417        [self writeMetaData];
418       
419        return self;
420}
421
422- (void)linkCollections
423{
424        NSMutableArray *collections = [[self collectionDirectories] mutableCopy];
425        [collections sortUsingSelector:@selector(compare:)];
426        NSEnumerator *collectionEnum = [collections objectEnumerator];
427        NSString *dir = nil;
428        SapphireDirectoryMetaData *highestMetaData = [directories objectForKey:[collectionEnum nextObject]];
429        while((dir = [collectionEnum nextObject]) != nil)
430        {
431                [directories setObject:[self dataForSubPath:dir inDirectory:highestMetaData] forKey:dir];
432        }
433       
434        [collections release];
435}
436
437- (void)dealloc
438{
439        [dictionaryPath release];
440        [skipCollection release];
441        [hideCollection release];
442        if(writeTimer != nil)
443        {
444                [writeTimer invalidate];
445                [self realWriteMetaData];
446        }
447        [super dealloc];
448}
449
450- (SapphireMetaData *)dataForSubPath:(NSString *)absPath inDirectory:(SapphireDirectoryMetaData *)directory
451{
452        SapphireMetaData *ret = directory;
453        NSString *dirPath = [directory path];
454        NSMutableArray *pathComp = [[absPath pathComponents] mutableCopy];
455        int prefixCount = [[dirPath pathComponents] count];
456        [pathComp removeObjectsInRange:NSMakeRange(0, prefixCount)];
457       
458        if([pathComp count])
459        {
460                NSString *subPath = [NSString pathWithComponents:pathComp];
461                ret = [directory metaDataForSubPath:subPath];
462        }
463        [pathComp release];
464        return ret;
465}
466
467/*!
468 * @brief Returns the meta data for a particular path
469 *
470 * @param path The path to find
471 * @return The directory meta data for the path, or nil if none exists
472 */
473- (SapphireMetaData *)dataForPath:(NSString *)absPath
474{
475        SapphireDirectoryMetaData *directory = [directories objectForKey:@"/"];
476        return [self dataForSubPath:absPath inDirectory:directory];
477}
478
479/*!
480 * @brief Returns the meta data for a particular path
481 *
482 * @param path The path to find
483 * @param data The meta data to use in place of the source's data
484 * @return The directory meta data for the path, or nil if none exists
485 */
486- (SapphireMetaData *)dataForPath:(NSString *)absPath withData:(NSDictionary *)data
487{
488        SapphireMetaData *ret = [self dataForPath:absPath];
489       
490        if([data count] != 0)
491                [ret replaceInfoWithDict:data];
492       
493        return ret;
494}
495
496/*!
497 * @brief Returns the directory meta data for a particular path
498 *
499 * @param path The path to find
500 * @return The directory meta data for the path, or nil if none exists
501 */
502- (SapphireDirectoryMetaData *)directoryForPath:(NSString *)absPath
503{
504        SapphireMetaData *ret = [self dataForPath:absPath];
505        if([ret isKindOfClass:[SapphireDirectoryMetaData class]])
506                return (SapphireDirectoryMetaData *)ret;
507        return nil;
508}
509
510/*!
511 * @brief Gets a listing of all valid collection directories.  These are all mounted disks
512 * plus homedir/Movies
513 *
514 * @return All the directory locations
515 */
516
517- (NSArray *)collectionDirectories
518{
519        NSWorkspace *mywork = [NSWorkspace sharedWorkspace];
520        NSMutableArray *ret = [[mywork mountedLocalVolumePaths] mutableCopy];
521        [ret removeObject:@"/mnt"];
522        [ret removeObject:@"/CIFS"];
523        [ret removeObject:NSHomeDirectory()];
524        [ret addObject:[NSHomeDirectory() stringByAppendingPathComponent:@"Movies"]];
525        return [ret autorelease];
526}
527
528/*Makes a director at a path, including its parents*/
529static void makeParentDir(NSFileManager *manager, NSString *dir)
530{
531        NSString *parent = [dir stringByDeletingLastPathComponent];
532       
533        /*See if parent exists, and make if not*/
534        BOOL isDir;
535        if(![manager fileExistsAtPath:parent isDirectory:&isDir])
536                makeParentDir(manager, parent);
537        else if(!isDir)
538                /*Can't work with this*/
539                return;
540       
541        /*Create our dir*/
542        [manager createDirectoryAtPath:dir attributes:nil];
543}
544
545/*!
546 * @brief Write all meta data to a file
547 */
548- (void)writeMetaData
549{
550        [writeTimer invalidate];
551        writeTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(realWriteMetaData) userInfo:nil repeats:NO];
552}
553
554- (void)realWriteMetaData
555{
556        [writeTimer invalidate];
557        writeTimer = nil;
558        makeParentDir([NSFileManager defaultManager], [dictionaryPath stringByDeletingLastPathComponent]);
559        NSString *error = nil;
560        NSData *data = [NSPropertyListSerialization dataFromPropertyList:metaData format:NSPropertyListBinaryFormat_v1_0 errorDescription:&error];
561        if(error == nil)
562                [data writeToFile:dictionaryPath atomically:YES];
563        else
564                [error release];
565}
566
567- (SapphireMetaDataCollection *)collection
568{
569        return self;
570}
571
572/*!
573 * @brief Returns whether the collection is hidden or not
574 *
575 * @return YES if the collection is hidden, NO otherwise
576 */
577- (BOOL)hideCollection:(NSString *)collection
578{
579        return [[hideCollection objectForKey:collection] boolValue];
580}
581
582/*!
583 * @brief Set whether to hide the collection or not
584 *
585 * @param hide YES to hide this collection, NO otherwise
586 */
587- (void)setHide:(BOOL)hide forCollection:(NSString *)collection
588{
589        [hideCollection setObject:[NSNumber numberWithBool:hide] forKey:collection];
590        [self writeMetaData];
591}
592
593/*!
594 * @brief Returns whether the collection is skipped or not
595 *
596 * @return YES if the collection is skipped, NO otherwise
597 */
598- (BOOL)skipCollection:(NSString *)collection
599{
600        return [[skipCollection objectForKey:collection] boolValue];
601}
602
603/*!
604 * @brief Set whether to skip the collection or not
605 *
606 * @param skip YES to skip this collection, NO otherwise
607 */
608- (void)setSkip:(BOOL)skip forCollection:(NSString *)collection
609{
610        [skipCollection setObject:[NSNumber numberWithBool:skip] forKey:collection];
611        [self writeMetaData];
612}
613
614@end
615
616@implementation SapphireDirectoryMetaData
617
618- (void)createSubDicts
619{
620        /*Get the file listing*/
621        metaFiles = [metaData objectForKey:FILES_KEY];
622        if(metaFiles == nil)
623                metaFiles = [NSMutableDictionary new];
624        else
625                metaFiles = [metaFiles mutableCopy];
626        [metaData setObject:metaFiles forKey:FILES_KEY];
627        [metaFiles release];
628       
629        /*Get the directory listing*/
630        metaDirs = [metaData objectForKey:DIRS_KEY];
631        if(metaDirs == nil)
632                metaDirs = [NSMutableDictionary new];
633        else
634                metaDirs = [metaDirs mutableCopy];
635        [metaData setObject:metaDirs forKey:DIRS_KEY];
636        [metaDirs release];     
637}
638
639/*!
640 * @brief Creates a new meta data object
641 *
642 * @param dict The configuration dictionary.  Note, this dictionary is copied and the copy is modified
643 * @param myParent The parent meta data
644 * @param myPath The path for this meta data object
645 * @return The meta data object
646 */
647- (id)initWithDictionary:(NSDictionary *)dict parent:(SapphireMetaData *)myParent path:(NSString *)myPath
648{
649        self = [super initWithDictionary:dict parent:myParent path:myPath];
650        if(!self)
651                return nil;
652       
653        [self createSubDicts];
654        directories = [[NSMutableArray alloc] init];
655        files = [[NSMutableArray alloc] init];
656       
657        /*Setup the cache*/
658        cachedMetaDirs = [NSMutableDictionary new];
659        cachedMetaFiles = [NSMutableDictionary new];
660       
661        return self;
662}
663
664- (void)postAllFilesRemoved
665{
666        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
667        NSEnumerator *filesEnum = [cachedMetaFiles objectEnumerator];
668        SapphireFileMetaData *meta = nil;
669        while((meta = [filesEnum nextObject]) != nil)
670                [nc postNotificationName:META_DATA_FILE_REMOVED_NOTIFICATION object:meta];     
671}
672
673- (void)dealloc
674{
675        [self postAllFilesRemoved];
676        [importTimer invalidate];
677        [importArray release];
678        [cachedMetaDirs release];
679        [cachedMetaFiles release];
680        [files release];
681        [directories release];
682        [super dealloc];
683}
684
685- (void)childDictionaryChanged:(SapphireMetaData *)child
686{
687        NSMutableDictionary *refDict = nil;
688        if([child isKindOfClass:[SapphireDirectoryMetaData class]])
689                refDict = metaDirs;
690        else if([child isKindOfClass:[SapphireFileMetaData class]])
691                refDict = metaFiles;
692        else
693                return;
694       
695        NSString *name = [[child path] lastPathComponent];
696        [refDict removeObjectForKey:name];
697        [refDict setObject:[child dict] forKey:name];
698}
699
700- (void)replaceInfoWithDict:(NSDictionary *)dict
701{
702        [self postAllFilesRemoved];
703        [super replaceInfoWithDict:dict];
704        [self createSubDicts];
705        [directories removeAllObjects];
706        [files removeAllObjects];
707        [cachedMetaDirs removeAllObjects];
708        [cachedMetaFiles removeAllObjects];
709        [importArray release];
710        importArray = nil;
711        [importTimer invalidate];
712        importTimer = nil;
713        scannedDirectory = NO;
714}
715
716/*!
717 * @brief Reloads the directory contents from what is present on disk
718 */
719- (void)reloadDirectoryContents
720{
721        /*Flush saved information*/
722        [files removeAllObjects];
723        [directories removeAllObjects];
724        NSMutableArray *fileMetas = [NSMutableArray array];
725       
726        /*Get content*/
727        NSArray *names = [[NSFileManager defaultManager] directoryContentsAtPath:path];
728       
729        NSEnumerator *nameEnum = [names objectEnumerator];
730        NSString *name = nil;
731        NSFileManager *fm = [NSFileManager defaultManager];
732        while((name = [nameEnum nextObject]) != nil)
733        {
734                /*Skip hidden files*/
735                if([name hasPrefix:@"."])
736                        continue;
737                /*Skip the Cover Art directory*/
738                if([name isEqualToString:@"Cover Art"])
739                        continue;
740                NSString *filePath = [path stringByAppendingPathComponent:name];
741                SapphireMetaData *resolvedObject = nil;
742                NSDictionary *attributes = [fm fileAttributesAtPath:filePath traverseLink:NO];
743                if([[attributes fileType] isEqualToString:NSFileTypeSymbolicLink])
744                {
745                        /* Symbolic link, handle with care */
746                        NSMutableDictionary *refDict = nil;
747                        NSString *resolvedPath = [filePath stringByResolvingSymlinksInPath];
748
749                        if([self isDirectory:resolvedPath])
750                                refDict = metaDirs;
751                        else
752                                refDict = metaFiles;
753                        resolvedObject = [[self collection] dataForPath:resolvedPath withData:[refDict objectForKey:name]];
754                        if(resolvedObject == nil)
755                                continue;
756                       
757                        BOOL rewrite = NO;
758                        if([refDict objectForKey:name] != nil)
759                                rewrite = YES;
760                        [refDict removeObjectForKey:name];
761                        if(rewrite)
762                                [self writeMetaData];
763                }
764                /*Only accept if it is a directory or right extension*/
765                NSString *extension = [name pathExtension];
766                if([self isDirectory:filePath])
767                {
768                        [directories addObject:name];
769                        if(resolvedObject != nil)
770                                [cachedMetaDirs setObject:resolvedObject forKey:name];
771                }
772                else if([allExtensions containsObject:[extension lowercaseString]])
773                {
774                        if(resolvedObject != nil)
775                                [cachedMetaFiles setObject:resolvedObject forKey:name];
776                        else
777                                resolvedObject = [self metaDataForFile:name];
778                        [fileMetas addObject:resolvedObject];
779                }
780        }
781        /*Sort them*/
782        [directories sortUsingSelector:@selector(directoryNameCompare:)];
783        [fileMetas sortUsingSelector:@selector(episodeCompare:)];
784        /*Create the file listing just containing names*/
785        nameEnum = [fileMetas objectEnumerator];
786        SapphireFileMetaData *fileMeta = nil;
787        while((fileMeta = [nameEnum nextObject]) != nil)
788        {
789                NSString *joinedPath = [fileMeta joinedFile];
790                if([fm fileExistsAtPath:joinedPath])
791                        continue;
792                [files addObject:[[fileMeta path] lastPathComponent]];         
793        }
794        /*Check to see if any data is out of date*/
795        [self updateMetaData];
796        if([importArray count] || [self pruneMetaData])
797                [self writeMetaData];
798        /*Mark directory as scanned*/
799        scannedDirectory = YES;
800}
801
802/*!
803 * @brief Retrieve a list of all file names
804 *
805 * @return An NSArray of all file names
806 */
807- (NSArray *)files
808{
809        return files;
810}
811
812/*!
813 * @brief Retrieve a list of all directory names
814 *
815 * @return An NSArray of all directory names
816 */
817- (NSArray *)directories
818{
819        return directories;
820}
821
822/*!
823 * @brief Returns whether the directory has any files which match the predicate
824 *
825 * @param predicate The predictate to match
826 * @return YES if a file exists, NO otherwise
827 */
828- (BOOL)hasPredicatedFiles:(SapphirePredicate *)predicate
829{
830        /*Get file listing*/
831        NSArray *filesToScan = files;
832        if(!scannedDirectory)
833                /*Don't do a scan, just returned cached data*/
834                filesToScan = [metaFiles allKeys];
835        NSEnumerator *fileEnum = [filesToScan objectEnumerator];
836        NSString *file = nil;
837        while((file = [fileEnum nextObject]) != nil)
838        {
839                /*Check predicate*/
840                BOOL include = NO;
841                SapphireFileMetaData *meta = [self cachedMetaDataForFile:file];
842                if(meta != nil)
843                        include = [predicate accept:[meta path] meta:meta];
844                else
845                        include = [predicate accept:[path stringByAppendingPathComponent:file] meta:nil];
846                if(include)
847                        /*Predicate matched*/
848                        return YES;
849        }
850        /*No matches found*/
851        return NO;
852}
853
854/*!
855 * @brief Returns whether the directory has any directories which match the predicate
856 *
857 * @param predicate The predicate to match
858 * @return YES if a file exists, NO otherwise
859 */
860- (BOOL)hasPredicatedDirectories:(SapphirePredicate *)predicate
861{
862        /*Get directory listing*/
863        NSArray *directoriesToScan = directories;
864        if(!scannedDirectory)
865                /*Don't do a scan, just return cached data*/
866                directoriesToScan = [metaDirs allKeys];
867        NSEnumerator *directoryEnum = [directoriesToScan objectEnumerator];
868        NSString *directory = nil;
869        while((directory = [directoryEnum nextObject]) != nil)
870        {
871                /*Check predicate*/
872                SapphireDirectoryMetaData *meta = [self metaDataForDirectory:directory];
873                /*If we are not fast, go ahead and scan*/
874                if(![[SapphireSettings sharedSettings] fastSwitching])
875                        [meta reloadDirectoryContents];
876               
877                /*If the dir has any files or any dirs, it matches*/
878                if([meta hasPredicatedFiles:predicate] || [meta hasPredicatedDirectories:predicate])
879                        return YES;
880        }
881        /*No matches found*/
882        return NO;
883}
884
885/*!
886 * @brief Get a listing of predicate files
887 *
888 * @param predicate The predicate to match
889 * @return An NSArray of matches
890 */
891- (NSArray *)predicatedFiles:(SapphirePredicate *)predicate
892{
893        /*Get file listing*/
894        NSMutableArray *ret = [NSMutableArray array];
895        NSArray *filesToScan = files;
896        if(!scannedDirectory)
897                /*Don't do a scan, just return cached data*/
898                filesToScan = [metaFiles allKeys];
899        NSEnumerator *fileEnum = [filesToScan objectEnumerator];
900        NSString *file = nil;
901        while((file = [fileEnum nextObject]) != nil)
902        {
903                /*Check predicate*/
904                BOOL include = NO;
905                SapphireFileMetaData *meta = [self cachedMetaDataForFile:file];
906                if(meta != nil)
907                        include = [predicate accept:[meta path] meta:meta];
908                else
909                        include = [predicate accept:[path stringByAppendingPathComponent:file] meta:nil];
910                if(include)
911                        /*Predicate matched, add to list*/
912                        [ret addObject:file];
913        }
914        /*Return the list*/
915        return ret;
916}
917
918/*!
919 * @brief Get a listing of predicated directories
920 *
921 * @param predicate The predicate to match
922 * @return An NSArray of matches
923 */
924- (NSArray *)predicatedDirectories:(SapphirePredicate *)predicate
925{
926        /*Get directory listing*/
927        NSMutableArray *ret = [NSMutableArray array];
928        NSArray *directoriesToScan = directories;
929        if(!scannedDirectory)
930                /*Don't do a scan, just return cached data*/
931                directoriesToScan = [metaDirs allKeys];
932        NSEnumerator *directoryEnum = [directoriesToScan objectEnumerator];
933        NSString *directory = nil;
934        while((directory = [directoryEnum nextObject]) != nil)
935        {
936                /*Check predicate*/
937                SapphireDirectoryMetaData *meta = [self metaDataForDirectory:directory];
938                if(![[SapphireSettings sharedSettings] fastSwitching])
939                        [meta reloadDirectoryContents];
940
941                /*If dir has any files or any dirs, it matches*/
942                if([meta hasPredicatedFiles:predicate] || [meta hasPredicatedDirectories:predicate])
943                        /*Add to list*/
944                        [ret addObject:directory];
945        }
946        /*Return the list*/
947        return ret;
948}
949
950/*!
951 * @brief Get the meta data object for a file.  Creates one if it doesn't already exist
952 *
953 * @param file The file within this dir
954 * @return The file's meta data
955 */
956- (SapphireFileMetaData *)metaDataForFile:(NSString *)file
957{
958        /*Check cache*/
959        SapphireFileMetaData *ret = [cachedMetaFiles objectForKey:file];
960        if(ret == nil)
961        {
962                /*Create it*/
963                ret = [[SapphireFileMetaData alloc] initWithDictionary:[metaFiles objectForKey:file] parent:self path:[path stringByAppendingPathComponent:file]];
964                [metaFiles setObject:[ret dict] forKey:file];
965                /*Add to cache*/
966                [cachedMetaFiles setObject:ret forKey:file];
967                [ret autorelease];
968        }
969        /*Return it*/
970        return ret;
971}
972
973- (SapphireFileMetaData *)cachedMetaDataForFile:(NSString *)file
974{
975        if([metaFiles objectForKey:file] != nil)
976                return [self metaDataForFile:file];
977        else
978                return [cachedMetaFiles objectForKey:file];
979}
980
981/*!
982 * @brief Get the meta data object for a directory.  Creates one if it doesn't alreay exist
983 *
984 * @param dir The directory within this dir
985 * @return The directory's meta data
986 */
987- (SapphireDirectoryMetaData *)metaDataForDirectory:(NSString *)dir
988{
989        /*Check cache*/
990        SapphireDirectoryMetaData *ret = [cachedMetaDirs objectForKey:dir];
991        if(ret == nil)
992        {
993                /*Create it*/
994                ret = [[SapphireDirectoryMetaData alloc] initWithDictionary:[metaDirs objectForKey:dir] parent:self path:[path stringByAppendingPathComponent:dir]];
995                [metaDirs setObject:[ret dict] forKey:dir];
996                /*Add to cache*/
997                [cachedMetaDirs setObject:ret forKey:dir];
998                [ret autorelease];             
999        }
1000        /*Return it*/
1001        return ret;
1002}
1003
1004/*!
1005 * @brief Prunes off non-existing files and directories from the meta data.  This does not prune a directory's content if it contains no files and directories.  In addition, broken sym links are also not pruned.  The theory is these may be the signs of missing mounts.
1006 *
1007 * @return YES if any data was pruned, NO otherwise
1008 */
1009- (BOOL)pruneMetaData
1010{
1011        BOOL ret = NO;
1012        /*Check for empty dir.  May be a missing mount, so skip*/
1013        if([files count] + [directories count] == 0)
1014                return ret;
1015        /*Get missing file list*/
1016        NSSet *existingSet = [NSSet setWithArray:files];
1017        NSArray *metaArray = [metaFiles allKeys];
1018        NSMutableSet *pruneSet = [NSMutableSet setWithArray:metaArray];
1019       
1020        [pruneSet minusSet:existingSet];
1021        /*Prune each item*/
1022        if([pruneSet anyObject] != nil)
1023        {
1024                NSEnumerator *pruneEnum = [pruneSet objectEnumerator];
1025                NSString *pruneKey = nil;
1026                NSFileManager *fm = [NSFileManager defaultManager];
1027                while((pruneKey = [pruneEnum nextObject]) != nil)
1028                {
1029                        NSString *filePath = [path stringByAppendingPathComponent:pruneKey];
1030                        NSDictionary *attributes = [[NSFileManager defaultManager] fileAttributesAtPath:filePath traverseLink:NO];
1031                        /*If it is a broken link, skip*/
1032                        if([[attributes objectForKey:NSFileType] isEqualToString:NSFileTypeSymbolicLink])
1033                                continue;
1034                        SapphireFileMetaData *meta = [cachedMetaFiles objectForKey:pruneKey];
1035                        /*If it is a joined File, skip*/
1036                        NSString *joinedPath = [meta joinedFile];
1037                        if([fm fileExistsAtPath:joinedPath])
1038                                continue;
1039                        /*Remove and mark as we did an update*/
1040                        if(meta != nil)
1041                                [[NSNotificationCenter defaultCenter] postNotificationName:META_DATA_FILE_REMOVED_NOTIFICATION object:meta];
1042                        [metaFiles removeObjectForKey:pruneKey];
1043                        [cachedMetaFiles removeObjectForKey:pruneKey];
1044                        ret = YES;
1045                }
1046        }
1047       
1048        /*Get missing directory list*/
1049        existingSet = [NSSet setWithArray:directories];
1050        metaArray = [metaDirs allKeys];
1051        pruneSet = [NSMutableSet setWithArray:metaArray];
1052       
1053        [pruneSet minusSet:existingSet];
1054        /*Prune each item*/
1055        if([pruneSet anyObject] != nil)
1056        {
1057                NSEnumerator *pruneEnum = [pruneSet objectEnumerator];
1058                NSString *pruneKey = nil;
1059                while((pruneKey = [pruneEnum nextObject]) != nil)
1060                {
1061                        NSString *filePath = [path stringByAppendingPathComponent:pruneKey];
1062                        NSDictionary *attributes = [[NSFileManager defaultManager] fileAttributesAtPath:filePath traverseLink:NO];
1063                        /*If it is a broken link, skip*/
1064                        if(![[attributes objectForKey:NSFileType] isEqualToString:NSFileTypeSymbolicLink])
1065                        {
1066                                /*Remove and mark as we did an update*/
1067                                [metaDirs removeObjectForKey:pruneKey];
1068                                [cachedMetaDirs removeObjectForKey:pruneKey];
1069                                ret = YES;
1070                        }
1071                }
1072        }
1073       
1074        /*Return whether we did a prune*/
1075        return ret;
1076}
1077
1078/*!
1079 * @brief See if any files need to be updated
1080 *
1081 * @return YES if any files need an update, NO otherwise
1082 */
1083- (BOOL)updateMetaData
1084{
1085        /*Look at each file*/
1086        NSEnumerator *fileEnum = [files objectEnumerator];
1087        NSString *fileName = nil;
1088        importArray = [[NSMutableArray alloc] init];
1089        while((fileName = [fileEnum nextObject]) != nil)
1090        {
1091                /*If the file exists, and no meta data, add to update list*/
1092                NSDictionary *fileMeta = [metaFiles objectForKey:fileName];
1093                if(fileMeta == nil)
1094                {
1095                        [self metaDataForFile:fileName];
1096                        [importArray addObject:fileName];
1097                }
1098                else
1099                {
1100                        /*If file has been modified since last import, add to update list*/
1101                        NSString *filePath = [path stringByAppendingPathComponent:fileName];
1102                        struct stat sb;
1103                        memset(&sb, 0, sizeof(struct stat));
1104                        stat([filePath fileSystemRepresentation], &sb);
1105                        long modTime = sb.st_mtimespec.tv_sec;
1106                        if([[fileMeta objectForKey:MODIFIED_KEY] intValue] != modTime || [[fileMeta objectForKey:META_VERSION_KEY] intValue] != META_FILE_VERSION)
1107                                [importArray addObject:fileName];
1108                }
1109        }
1110        /*We didn't do any updates yet, so return NO*/
1111        return NO;
1112}
1113
1114/*Timer function to process a single file*/
1115- (void)processFiles:(NSTimer *)timer
1116{
1117        NSString *file = [importArray objectAtIndex:0];
1118       
1119        /*Get the file and update it*/
1120        [[self metaDataForFile:file] updateMetaData];
1121       
1122        /*Write the file info out and tell delegate we updated*/
1123        [self writeMetaData];
1124        [delegate updateCompleteForFile:file];
1125       
1126        /*Remove from list and redo timer*/
1127        [importArray removeObjectAtIndex:0];
1128        [self resumeImport];
1129}
1130
1131/*!
1132 * @brief Cancel the import process
1133 */
1134- (void)cancelImport
1135{
1136        /*Kill the timer*/
1137        [importTimer invalidate];
1138        importTimer = nil;
1139}
1140
1141/*!
1142 * @brief Resume the import process
1143 */
1144- (void)resumeImport
1145{
1146        /*Sanity check*/
1147        [importTimer invalidate];
1148        /*Check if we need to import*/
1149        if([importArray count])
1150                /*Wait 1.1 seconds and do an import*/
1151                importTimer = [NSTimer scheduledTimerWithTimeInterval:1.1 target:self selector:@selector(processFiles:) userInfo:nil repeats:NO];
1152        else
1153        {
1154                /*No import, so clean up*/
1155                importTimer = nil;
1156                [importArray release];
1157                importArray = nil;
1158        }
1159}
1160
1161/*!
1162 * @brief Delay the import process a while before starting again
1163 */
1164- (void)resumeDelayedImport
1165{
1166        /*Sanity check*/
1167        [importTimer invalidate];
1168        /*Check if we need to import*/
1169        if([importArray count])
1170                /*Wait 5 seconds before starting the import process*/
1171                importTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(resumeImport) userInfo:nil repeats:NO];
1172        else
1173                /*No import, clean up*/
1174                importTimer = nil;
1175}
1176
1177/*!
1178 * @brief Get the meta data for some file or directory beneath this one
1179 *
1180 * @param subPath The subpath to get the meta data
1181 * @return The meta data object
1182 */
1183- (SapphireMetaData *)metaDataForSubPath:(NSString *)subPath
1184{
1185        /*Get next level to examine*/
1186        NSArray *components = [subPath pathComponents];
1187        if(![components count])
1188                /*Must mean ourself*/
1189                return self;
1190        NSString *file = [components objectAtIndex:0];
1191       
1192        NSString *fullPath = [path stringByAppendingPathComponent:file];
1193        /*Go to the next dir*/
1194        if([self isDirectory:fullPath])
1195        {
1196                NSMutableArray *newComp = [components mutableCopy];
1197                [newComp removeObjectAtIndex:0];
1198                [newComp autorelease];
1199                SapphireDirectoryMetaData *nextLevel = [self metaDataForDirectory:file];
1200                return [nextLevel metaDataForSubPath:[NSString pathWithComponents:newComp]];
1201        }
1202        /*If it matches a file, and more path components, this doesn't exist, return nil*/
1203        else if([components count] > 1 || ![[NSFileManager defaultManager] fileExistsAtPath:fullPath])
1204                return nil;
1205        /*Return our file's meta data*/
1206        return [self metaDataForFile:file];
1207}
1208
1209/*!
1210 * @brief Get the meta data for all the files contained within this directory tree
1211 *
1212 * @param subDelegate The delegate to inform when scan is complete
1213 * @param skip A set of directories to skip.  Note, this set is modified
1214 */
1215- (void)getSubFileMetasWithDelegate:(id <SapphireMetaDataScannerDelegate>)subDelegate skipDirectories:(NSMutableSet *)skip
1216{
1217        /*Scan dir and create scanner*/
1218        [self reloadDirectoryContents];
1219        SapphireMetaDataScanner *scanner = [[SapphireMetaDataScanner alloc] initWithDirectoryMetaData:self delegate:subDelegate];
1220        /*Add ourselves to not rescan*/
1221        [skip addObject:[self path]];
1222        [scanner setSkipDirectories:skip];
1223        /*We want results*/
1224        [scanner setGivesResults:YES];
1225        [scanner release];
1226}
1227
1228/*!
1229 * @brief Scan for all files contained within this directory tree
1230 *
1231 * @param subDelegate The delegate to inform when scan is complete
1232 * @param skip A set of directories to skip.  Note, this set is modified
1233 */
1234- (void)scanForNewFilesWithDelegate:(id <SapphireMetaDataScannerDelegate>)subDelegate skipDirectories:(NSMutableSet *)skip
1235{
1236        /*Scan dir and create scanner*/
1237        [self reloadDirectoryContents];
1238        SapphireMetaDataScanner *scanner = [[SapphireMetaDataScanner alloc] initWithDirectoryMetaData:self delegate:subDelegate];
1239        /*Add ourselves to not rescan*/
1240        [skip addObject:[self path]];
1241        [scanner setSkipDirectories:skip];
1242        /*We don't want results*/
1243        [scanner setGivesResults:NO];
1244        [scanner release];
1245}
1246
1247/*!
1248 * @brief Load all the cached meta data so that dynamic directories can build
1249 */
1250- (void)loadMetaData
1251{
1252        NSArray *keys = [NSArray arrayWithArray:[metaDirs allKeys]];
1253        NSEnumerator *dirEnum = [keys objectEnumerator];
1254        NSString *dir = nil;
1255        while((dir = [dirEnum nextObject]) != nil)
1256        {
1257                [[self metaDataForDirectory:dir] loadMetaData];
1258        }
1259        keys = [NSArray arrayWithArray:[metaFiles allKeys]];
1260        NSEnumerator *fileEnum = [keys objectEnumerator];
1261        NSString *file = nil;
1262        while((file = [fileEnum nextObject]) != nil)
1263        {
1264                [self metaDataForFile:file];
1265        }
1266}
1267
1268/*Quick function to setup file and directory lists for other functions*/
1269- (void)setupFiles:(NSArray * *)filesToScan andDirectories:(NSArray * *)directoriesToScan arraysForPredicate:(SapphirePredicate *)predicate
1270{
1271        if(predicate)
1272        {
1273                *filesToScan = [self predicatedFiles:predicate];
1274                *directoriesToScan = [self predicatedDirectories:predicate];
1275        }
1276        else if(!scannedDirectory)
1277        {
1278                /*Haven't scanned the directory yet, so use cached*/
1279                *filesToScan = [metaFiles allKeys];
1280                *directoriesToScan = [metaDirs allKeys];
1281        }
1282}
1283
1284/*Function to check a result in a subtree*/
1285- (BOOL)checkResult:(BOOL)result recursivelyOnFiles:(NSInvocation *)fileInv forPredicate:(SapphirePredicate *)predicate
1286{
1287        /*Get file and directory list*/
1288        NSArray *filesToScan = files;
1289        NSArray *directoriesToScan = directories;
1290        [self setupFiles:&filesToScan andDirectories:&directoriesToScan arraysForPredicate:predicate];
1291        NSEnumerator *fileEnum = [filesToScan objectEnumerator];
1292        NSString *file = nil;
1293        /*Check for a file which matches result*/
1294        while((file = [fileEnum nextObject]) != nil)
1295        {
1296                [fileInv invokeWithTarget:[self metaDataForFile:file]];
1297                BOOL thisResult = NO;
1298                [fileInv getReturnValue:&thisResult];
1299                if(thisResult == result)
1300                        /*Found, return it*/
1301                        return result;
1302        }
1303
1304        /*Check the directories now*/
1305        NSEnumerator *dirEnum = [directoriesToScan objectEnumerator];
1306        NSString *dir = nil;
1307        while((dir = [dirEnum nextObject]) != nil)
1308                if([[self metaDataForDirectory:dir] checkResult:result recursivelyOnFiles:fileInv forPredicate:predicate] == result)
1309                        /*Found, return it*/
1310                        return result;
1311       
1312        /*Not found*/
1313        return !result;
1314}
1315
1316/*Function to invoke a command on all files in a subtree*/
1317- (void)invokeRecursivelyOnFiles:(NSInvocation *)fileInv withPredicate:(SapphirePredicate *)predicate
1318{
1319        /*Get all files and dirs*/
1320        [self reloadDirectoryContents];
1321        NSEnumerator *dirEnum = [directories objectEnumerator];
1322        NSString *dir = nil;
1323        /*Invoke same thing on directories*/
1324        while((dir = [dirEnum nextObject]) != nil)
1325                [[self metaDataForDirectory:dir] invokeRecursivelyOnFiles:fileInv withPredicate:predicate];
1326       
1327        NSEnumerator *fileEnum = [files objectEnumerator];
1328        NSString *file = nil;
1329        /*Invoke on the files*/
1330        while((file = [fileEnum nextObject]) != nil)
1331        {
1332                SapphireFileMetaData *fileMeta = [self metaDataForFile:file];
1333                /*Only if they match a predicate, or if there is not predicate*/
1334                if(!predicate || [predicate accept:[fileMeta path] meta:fileMeta])
1335                        [fileInv invokeWithTarget:fileMeta];
1336        }
1337}
1338
1339/*!
1340 * @brief Returns if directory contains any watched files
1341 *
1342 * @param predicate The predicate to match on
1343 * @return YES if at least one exists, NO otherwise
1344 */
1345- (BOOL)watchedForPredicate:(SapphirePredicate *)predicate
1346{
1347        SEL select = @selector(watched);
1348        NSInvocation *fileInv = [NSInvocation invocationWithMethodSignature:[[SapphireFileMetaData class] instanceMethodSignatureForSelector:select]];
1349        [fileInv setSelector:select];
1350        return [self checkResult:NO recursivelyOnFiles:fileInv forPredicate:predicate];
1351}
1352
1353/*!
1354 * @brief Set subtree as watched
1355 *
1356 * @param watched YES if set to watched, NO if set to unwatched
1357 * @param predicate The predicate which to restrict setting
1358 */
1359- (void)setWatched:(BOOL)watched forPredicate:(SapphirePredicate *)predicate
1360{
1361        SEL select = @selector(setWatched:);
1362        NSInvocation *fileInv = [NSInvocation invocationWithMethodSignature:[[SapphireFileMetaData class] instanceMethodSignatureForSelector:select]];
1363        [fileInv setSelector:select];
1364        [fileInv setArgument:&watched atIndex:2];
1365        [self invokeRecursivelyOnFiles:fileInv withPredicate:predicate];
1366        [self writeMetaData];
1367}
1368
1369/*!
1370 * @brief Returns if directory contains any favorite files
1371 *
1372 * @param predicate The predicate to match on
1373 * @return YES if at least one exists, NO otherwise
1374 */
1375- (BOOL)favoriteForPredicate:(SapphirePredicate *)predicate
1376{
1377        SEL select = @selector(favorite);
1378        NSInvocation *fileInv = [NSInvocation invocationWithMethodSignature:[[SapphireFileMetaData class] instanceMethodSignatureForSelector:select]];
1379        [fileInv setSelector:select];
1380        return [self checkResult:YES recursivelyOnFiles:fileInv forPredicate:predicate];       
1381}
1382
1383/*!
1384 * @brief Set subtree as favorite
1385 *
1386 * @param watched YES if set to favorite, NO if set to not favorite
1387 * @param predicate The predicate which to restrict setting
1388 */
1389- (void)setFavorite:(BOOL)favorite forPredicate:(SapphirePredicate *)predicate
1390{
1391        SEL select = @selector(setFavorite:);
1392        NSInvocation *fileInv = [NSInvocation invocationWithMethodSignature:[[SapphireFileMetaData class] instanceMethodSignatureForSelector:select]];
1393        [fileInv setSelector:select];
1394        [fileInv setArgument:&favorite atIndex:2];
1395        [self invokeRecursivelyOnFiles:fileInv withPredicate:predicate];
1396        [self writeMetaData];
1397}
1398
1399/*!
1400 * @brief Set subtree to re-import from the specified source
1401 *
1402 * @param source The source on which to re-import
1403 * @param predicate The predicate which to restrict setting
1404 */
1405- (void)setToImportFromSource:(NSString *)source forPredicate:(SapphirePredicate *)predicate
1406{
1407        SEL select = @selector(setToImportFromSource:);
1408        NSInvocation *fileInv = [NSInvocation invocationWithMethodSignature:[[SapphireFileMetaData class] instanceMethodSignatureForSelector:select]];
1409        [fileInv setSelector:select];
1410        [fileInv setArgument:&source atIndex:2];
1411        [self invokeRecursivelyOnFiles:fileInv withPredicate:predicate];
1412        [self writeMetaData];
1413}
1414
1415/*!
1416 * @brief Set subtree to the specified class
1417 *
1418 * @param fileClass The file class
1419 * @param predicate The predicate which to restrict setting
1420 */
1421- (void)setFileClass:(FileClass)fileClass forPredicate:(SapphirePredicate *)predicate
1422{
1423        SEL select = @selector(setFileClass:);
1424        NSInvocation *fileInv = [NSInvocation invocationWithMethodSignature:[[SapphireFileMetaData class] instanceMethodSignatureForSelector:select]];
1425        [fileInv setSelector:select];
1426        [fileInv setArgument:&fileClass atIndex:2];
1427        [self invokeRecursivelyOnFiles:fileInv withPredicate:predicate];
1428        [self writeMetaData];
1429}
1430
1431- (SapphireMetaDataCollection *)collection
1432{
1433        if(collection == nil)
1434                collection = [parent collection];
1435       
1436        return collection;
1437}
1438
1439/*See super documentation*/
1440- (NSMutableDictionary *)getDisplayedMetaDataInOrder:(NSArray * *)order;
1441{
1442        if(order != nil)
1443                *order = nil;
1444        return [NSMutableDictionary dictionaryWithObjectsAndKeys:
1445                [path lastPathComponent], META_TITLE_KEY,
1446                nil];
1447}
1448
1449@end
1450
1451@interface SapphireFileMetaData (private)
1452- (void)constructCombinedData;
1453- (void)combinedDataChanged;
1454@end
1455
1456@implementation SapphireFileMetaData
1457
1458/*Makes meta data easier to deal with in terms of display*/
1459static NSDictionary *metaDataSubstitutions = nil;
1460static NSSet *displayedMetaData = nil;
1461static NSArray *displayedMetaDataOrder = nil;
1462
1463+ (void) initialize
1464{
1465        metaDataSubstitutions = [[NSDictionary alloc] initWithObjectsAndKeys:
1466                //These substitute keys in the meta data to nicer display keys
1467                BRLocalizedString(@"Video", @"Video format in metadata display"), VIDEO_DESC_KEY,
1468                BRLocalizedString(@"Audio", @"Audio format in metadata display"), AUDIO_DESC_KEY,
1469                BRLocalizedString(META_EPISODE_AND_SEASON_KEY, @"Season / Epsiode in metadata display"), META_EPISODE_AND_SEASON_KEY,
1470                BRLocalizedString(META_SEASON_NUMBER_KEY, @"Season in metadata display"), META_SEASON_NUMBER_KEY,
1471                BRLocalizedString(META_EPISODE_NUMBER_KEY, @"Epsiode in metadata display"), META_EPISODE_NUMBER_KEY,
1472                BRLocalizedString(SIZE_KEY, @"filesize in metadata display"), SIZE_KEY,
1473                BRLocalizedString(DURATION_KEY, @"file duration in metadata display"), DURATION_KEY,
1474                nil];
1475        //These keys are before the above translation
1476        displayedMetaDataOrder = [NSArray arrayWithObjects:
1477                //These are not shown in the list
1478                META_RATING_KEY,
1479                META_DESCRIPTION_KEY,
1480                META_MOVIE_PLOT_KEY,
1481                META_COPYRIGHT_KEY,
1482                META_TITLE_KEY,
1483                META_MOVIE_TITLE_KEY,
1484                META_SHOW_AIR_DATE,
1485                META_MOVIE_RELEASE_DATE_KEY,
1486                META_MOVIE_DIRECTOR_KEY,
1487                META_MOVIE_GENRES_KEY,
1488                META_MOVIE_CAST_KEY,
1489                META_MOVIE_WIRTERS_KEY,
1490                //These are displayed as line items
1491                META_EPISODE_AND_SEASON_KEY,
1492                META_SEASON_NUMBER_KEY,
1493                META_EPISODE_NUMBER_KEY,
1494                SIZE_KEY,
1495                DURATION_KEY,
1496                VIDEO_DESC_KEY,
1497                AUDIO_DESC_KEY,
1498                nil];
1499        displayedMetaData = [[NSSet alloc] initWithArray:displayedMetaDataOrder];
1500       
1501        /*Remove non-displayed data from the displayed order, and use the display keys*/
1502        int excludedKeys = 5;
1503        NSMutableArray *modified = [[displayedMetaDataOrder subarrayWithRange:NSMakeRange(excludedKeys, [displayedMetaDataOrder count] - excludedKeys)] mutableCopy];
1504       
1505        int i;
1506        for(i=0; i<[modified count]; i++)
1507        {
1508                NSString *newKey = [metaDataSubstitutions objectForKey:[modified objectAtIndex:i]];
1509                if(newKey != nil)
1510                        [modified replaceObjectAtIndex:i withObject:newKey];
1511        }
1512        displayedMetaDataOrder = [[NSArray alloc] initWithArray:modified];
1513        [modified release];
1514}
1515
1516- (void)fileClsUpgrade
1517{
1518        if([self fileClass] == FILE_CLASS_UNKNOWN && [self episodeNumber] != 0)
1519                [self setFileClass:FILE_CLASS_TV_SHOW];
1520}
1521
1522- (id)initWithDictionary:(NSDictionary *)dict parent:(SapphireMetaData *)myParent path:(NSString *)myPath
1523{
1524        self = [super initWithDictionary:dict parent:myParent path:myPath];
1525        if(self == nil)
1526                return nil;
1527       
1528        [[NSNotificationCenter defaultCenter] postNotificationName:META_DATA_FILE_ADDED_NOTIFICATION object:self];
1529       
1530        return self;
1531}
1532
1533- (void)dealloc
1534{
1535        [combinedInfo release];
1536        [super dealloc];
1537}
1538
1539- (void)replaceInfoWithDict:(NSDictionary *)dict
1540{
1541        [super replaceInfoWithDict:dict];
1542        [combinedInfo release];
1543        combinedInfo = nil;
1544}
1545
1546/*See super documentation*/
1547- (BOOL) updateMetaData
1548{
1549        /*Check modified date*/
1550        NSDictionary *props = [[NSFileManager defaultManager] fileAttributesAtPath:path traverseLink:YES];
1551        int modTime = [[props objectForKey:NSFileModificationDate] timeIntervalSince1970];
1552        BOOL updated =FALSE;
1553       
1554        if(props == nil)
1555                /*No file*/
1556                return FALSE;
1557       
1558        /*Has it been modified since last import?*/
1559        if(modTime != [self modified] || [[metaData objectForKey:META_VERSION_KEY] intValue] != META_FILE_VERSION)
1560        {
1561                /*We did an update*/
1562                updated=TRUE ;
1563                NSMutableDictionary *fileMeta = [NSMutableDictionary dictionary];
1564               
1565                /*Set modified, size, and version*/
1566                [fileMeta setObject:[NSNumber numberWithInt:modTime] forKey:MODIFIED_KEY];
1567                [fileMeta setObject:[props objectForKey:NSFileSize] forKey:SIZE_KEY];
1568                [fileMeta setObject:[NSNumber numberWithInt:META_FILE_VERSION] forKey:META_VERSION_KEY];
1569               
1570                /*Open the movie*/
1571                NSError *error = nil;
1572                QTMovie *movie = [QTMovie movieWithFile:path error:&error];
1573                QTTime duration = [movie duration];
1574                [fileMeta setObject:[NSNumber numberWithFloat:(float)duration.timeValue/(float)duration.timeScale] forKey:DURATION_KEY];
1575                NSArray *audioTracks = [movie tracksOfMediaType:@"soun"];
1576                NSNumber *audioSampleRate = nil;
1577                if([audioTracks count])
1578                {
1579                        /*Get the audio track*/
1580                        QTTrack *track = [audioTracks objectAtIndex:0];
1581                        QTMedia *media = [track media];
1582                        if(media != nil)
1583                        {
1584                                /*Get the audio format*/
1585                                Media qtMedia = [media quickTimeMedia];
1586                                Handle sampleDesc = NewHandle(1);
1587                                GetMediaSampleDescription(qtMedia, 1, (SampleDescriptionHandle)sampleDesc);
1588                                AudioStreamBasicDescription asbd;
1589                                ByteCount       propSize = 0;
1590                                QTSoundDescriptionGetProperty((SoundDescriptionHandle)sampleDesc, kQTPropertyClass_SoundDescription, kQTSoundDescriptionPropertyID_AudioStreamBasicDescription, sizeof(asbd), &asbd, &propSize);
1591                               
1592                                if(propSize != 0)
1593                                {
1594                                        /*Set the format and sample rate*/
1595                                        NSNumber *format = [NSNumber numberWithUnsignedInt:asbd.mFormatID];
1596                                        [fileMeta setObject:format forKey:AUDIO_FORMAT_KEY];
1597                                        audioSampleRate = [NSNumber numberWithDouble:asbd.mSampleRate];
1598                                }
1599                               
1600                                CFStringRef userText = nil;
1601                                propSize = 0;
1602                                QTSoundDescriptionGetProperty((SoundDescriptionHandle)sampleDesc, kQTPropertyClass_SoundDescription, kQTSoundDescriptionPropertyID_UserReadableText, sizeof(userText), &userText, &propSize);
1603                                if(userText != nil)
1604                                {
1605                                        /*Set the description*/
1606                                        [fileMeta setObject:(NSString *)userText forKey:AUDIO_DESC_KEY];
1607                                        CFRelease(userText);
1608                                }
1609                                DisposeHandle(sampleDesc);
1610                        }
1611                }
1612                /*Set the sample rate*/
1613                if(audioSampleRate != nil)
1614                        [fileMeta setObject:audioSampleRate forKey:SAMPLE_RATE_KEY];
1615                NSArray *videoTracks = [movie tracksOfMediaType:@"vide"];
1616                if([videoTracks count])
1617                {
1618                        /*Get the video track*/
1619                        QTTrack *track = [videoTracks objectAtIndex:0];
1620                        QTMedia *media = [track media];
1621                        if(media != nil)
1622                        {
1623                                /*Get the video description*/
1624                                Media qtMedia = [media quickTimeMedia];
1625                                Handle sampleDesc = NewHandle(1);
1626                                GetMediaSampleDescription(qtMedia, 1, (SampleDescriptionHandle)sampleDesc);
1627                                CFStringRef userText = nil;
1628                                ByteCount propSize = 0;
1629                                ICMImageDescriptionGetProperty((ImageDescriptionHandle)sampleDesc, kQTPropertyClass_ImageDescription, kICMImageDescriptionPropertyID_SummaryString, sizeof(userText), &userText, &propSize);
1630                                DisposeHandle(sampleDesc);
1631                               
1632                                if(userText != nil)
1633                                {
1634                                        /*Set the description*/
1635                                        [fileMeta setObject:(NSString *)userText forKey:VIDEO_DESC_KEY];
1636                                        CFRelease(userText);
1637                                }
1638                        }
1639                }
1640                /*Add the meta data*/
1641                [metaData addEntriesFromDictionary:fileMeta];
1642                [self combinedDataChanged];
1643        }
1644        return updated ;
1645}
1646
1647/*!
1648 * @brief Get date of last modification of the file
1649 *
1650 * @return Seconds since 1970 of last modification
1651 */
1652- (int)modified
1653{
1654        return [[metaData objectForKey:MODIFIED_KEY] intValue];
1655}
1656
1657/*!
1658 * @brief Returns whether the file has been watched
1659 *
1660 * @return YES if watched, NO otherwise
1661 */
1662- (BOOL)watched
1663{
1664        return [[metaData objectForKey:WATCHED_KEY] boolValue];
1665}
1666
1667/*!
1668 * @brief Sets the file as watch or not watched
1669 *
1670 * @param watched YES if set to watched, NO if set to unwatched
1671 */
1672- (void)setWatched:(BOOL)watched
1673{
1674        [metaData setObject:[NSNumber numberWithBool:watched] forKey:WATCHED_KEY];
1675}
1676
1677/*!
1678 * @brief Returns whether the file is favorite
1679 *
1680 * @return YES if favorite, NO otherwise
1681 */
1682- (BOOL)favorite
1683{
1684        return [[metaData objectForKey:FAVORITE_KEY] boolValue];
1685}
1686
1687/*!
1688 * @brief Sets the file as favorite or not favorite
1689 *
1690 * @param watched YES if set to favorite, NO if set to not favorite
1691 */
1692- (void)setFavorite:(BOOL)favorite
1693{
1694        [metaData setObject:[NSNumber numberWithBool:favorite] forKey:FAVORITE_KEY];
1695}
1696
1697/*!
1698 * @brief Returns the time of import from a source
1699 *
1700 * @param source The source to check
1701 * @return The seconds since 1970 of the import
1702 */
1703- (long)importedTimeFromSource:(NSString *)source
1704{
1705        return [[[metaData objectForKey:source] objectForKey:MODIFIED_KEY] longValue];
1706}
1707
1708/*!
1709 * @brief Sets the file to re-import from source
1710 *
1711 * @param source The source to re-import
1712 */
1713- (void)setToImportFromSource:(NSString *)source
1714{
1715        NSMutableDictionary *sourceDict = [[metaData objectForKey:source] mutableCopy];
1716        if(sourceDict != nil)
1717        {
1718                [metaData setObject:sourceDict forKey:source];
1719                [sourceDict removeObjectForKey:MODIFIED_KEY];
1720                [sourceDict release];
1721        }
1722}
1723
1724/*!
1725 * @brief Add data to import from a source
1726 *
1727 * @param newMeta The new meta data
1728 * @param source The source we imported from
1729 * @param modTime The modification time of the source
1730 */
1731- (void)importInfo:(NSMutableDictionary *)newMeta fromSource:(NSString *)source withTime:(long)modTime
1732{
1733        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
1734        NSDictionary *info = [NSDictionary dictionaryWithObject:source forKey:META_DATA_FILE_INFO_KIND];
1735        [nc postNotificationName:META_DATA_FILE_INFO_WILL_CHANGE_NOTIFICATION object:self userInfo:info];
1736        [newMeta setObject:[NSNumber numberWithInt:modTime] forKey:MODIFIED_KEY];
1737        [metaData setObject:newMeta forKey:source];
1738        [self combinedDataChanged];
1739        [nc postNotificationName:META_DATA_FILE_INFO_HAS_CHANGED_NOTIFICATION object:self userInfo:info];
1740}
1741
1742/*!
1743 * @brief The resume time of the file
1744 *
1745 * @return The number of seconds from the begining of the file to resume
1746 */
1747- (unsigned int)resumeTime
1748{
1749        return [[metaData objectForKey:RESUME_KEY] unsignedIntValue];
1750}
1751
1752/*!
1753 * @brief Sets the resume time of the file
1754 *
1755 * @param resumeTime The number of seconds from the beginning of the file to resume
1756 */
1757- (void)setResumeTime:(unsigned int)resumeTime
1758{
1759        [metaData setObject:[NSNumber numberWithUnsignedInt:resumeTime] forKey:RESUME_KEY];
1760}
1761
1762/*!
1763 * @brief The file type
1764 *
1765 * @return The file type
1766 */
1767- (FileClass)fileClass
1768{
1769        if([[metaData objectForKey:FILE_CLASS_KEY] intValue]==nil)
1770                return FILE_CLASS_UNKNOWN;
1771        else
1772        return [[metaData objectForKey:FILE_CLASS_KEY] intValue];
1773}
1774
1775/*!
1776 * @brief Sets the file type
1777 *
1778 * @param fileClass The file type
1779 */
1780- (void)setFileClass:(FileClass)fileClass
1781{
1782        [metaData setObject:[NSNumber numberWithInt:fileClass] forKey:FILE_CLASS_KEY];
1783}
1784
1785/*!
1786 * @brief The file this has been joined to
1787 *
1788 * @return The file this has been joined to
1789 */
1790- (NSString *)joinedFile;
1791{
1792        return [metaData objectForKey:JOINED_FILE_KEY];
1793}
1794
1795/*!
1796 * @brief Sets the file this has been joined to
1797 *
1798 * @param fileClass The file this has been joined to
1799 */
1800- (void)setJoinedFile:(NSString *)join
1801{
1802        if(join == nil)
1803                [metaData removeObjectForKey:JOINED_FILE_KEY];
1804        else
1805                [metaData setObject:join forKey:JOINED_FILE_KEY];
1806}
1807
1808/*!
1809 * @brief Returns the file size
1810 *
1811 * @return The file size
1812 */
1813- (long long)size
1814{
1815        return [[metaData objectForKey:SIZE_KEY] longLongValue];
1816}
1817
1818/*!
1819 * @brief Returns the file's duration
1820 *
1821 * @return The file's duration
1822 */
1823- (float)duration
1824{
1825        return [[metaData objectForKey:DURATION_KEY] floatValue];
1826}
1827
1828/*!
1829 * @brief Returns the sample rate of the file
1830 *
1831 * @return The sample rate of the file
1832 */
1833- (Float64)sampleRate
1834{
1835        return [[metaData objectForKey:SAMPLE_RATE_KEY] intValue];
1836}
1837
1838/*!
1839 * @brief Returns the audio format of the file
1840 *
1841 * @return The audio format of the file
1842 */
1843- (UInt32)audioFormatID
1844{
1845        return [[metaData objectForKey:AUDIO_FORMAT_KEY] unsignedIntValue];
1846}
1847
1848/*!
1849 * @brief Returns whether the file has video
1850 *
1851 * @return YES if the file has video, NO otherwise
1852 */
1853- (BOOL)hasVideo
1854{
1855        return [metaData objectForKey:VIDEO_DESC_KEY] != nil;
1856}
1857
1858/*Combine the meta data from multiple sources*/
1859- (void)constructCombinedData
1860{
1861        /*Return cached data*/
1862        if(combinedInfo != nil)
1863                return;
1864        /*Combine from in order of priority: xml, tvrage, and file*/
1865        NSMutableDictionary *ret = [metaData mutableCopy];
1866        [ret addEntriesFromDictionary:[ret objectForKey:META_TVRAGE_IMPORT_KEY]];
1867        [ret addEntriesFromDictionary:[ret objectForKey:META_IMDB_IMPORT_KEY]];
1868        [ret addEntriesFromDictionary:[ret objectForKey:META_XML_IMPORT_KEY]];
1869        combinedInfo = ret;
1870}
1871
1872/*Destroy cached meta data*/
1873- (void)combinedDataChanged
1874{
1875        /*Remove cached data*/
1876        [combinedInfo release];
1877        combinedInfo = nil;
1878}
1879
1880/*!
1881 * @brief Returns the epsiode number of the file
1882 *
1883 * @return The episode number of the file
1884 */
1885- (int)episodeNumber
1886{
1887        [self constructCombinedData];
1888        return [[combinedInfo objectForKey:META_EPISODE_NUMBER_KEY] intValue] ;
1889}
1890
1891/*!
1892 * @brief Returns the season number of the file
1893 *
1894 * @return The season number of the file
1895 */
1896- (int)seasonNumber
1897{
1898        [self constructCombinedData];
1899        return [[combinedInfo objectForKey:META_SEASON_NUMBER_KEY] intValue];
1900}
1901
1902/*!
1903 * @brief Returns the title of the file
1904 *
1905 * @return The title of the file
1906 */
1907- (NSString *)episodeTitle
1908{
1909        [self constructCombinedData];
1910        return [combinedInfo objectForKey:META_TITLE_KEY] ;
1911}
1912
1913/*!
1914* @brief Returns the title of the file
1915 *
1916 * @return The title of the file
1917 */
1918- (NSString *)movieTitle
1919{
1920        [self constructCombinedData];
1921        return [combinedInfo objectForKey:META_MOVIE_TITLE_KEY] ;
1922}
1923
1924/*!
1925 * @brief Returns the title of the file
1926 *
1927 * @return The title of the file
1928 */
1929- (NSDate *)movieReleaseDate
1930{
1931        [self constructCombinedData];
1932        return [combinedInfo objectForKey:META_MOVIE_RELEASE_DATE_KEY] ;
1933}
1934
1935/*!
1936 * @brief Returns the show ID of the file
1937 *
1938 * @return The show ID of the file
1939 */
1940- (NSString *)showID
1941{
1942        [self constructCombinedData];
1943        return [combinedInfo objectForKey:META_SHOW_IDENTIFIER_KEY];
1944}
1945
1946/*!
1947 * @brief Returns the show name of the file
1948 *
1949 * @return The show name of the file
1950 */
1951- (NSString *)showName
1952{
1953        [self constructCombinedData];
1954        return [combinedInfo objectForKey:META_SHOW_NAME_KEY];
1955}
1956
1957/*!
1958* @brief Returns the genre of the movie file
1959 *
1960 * @return The genre type of the movie file
1961 */
1962- (NSArray *)movieGenres
1963{
1964        [self constructCombinedData];
1965        return [combinedInfo objectForKey:META_MOVIE_GENRES_KEY];
1966}
1967
1968/*Makes a pretty size string for the file*/
1969- (NSString *)sizeString
1970{
1971        /*Get size*/
1972        float size = [self size];
1973        if(size == 0)
1974                return @"-";
1975        /*The letter for magnitude*/
1976        char letter = ' ';
1977        if(size >= 1024000)
1978        {
1979                if(size >= 1024*1024000)
1980                {
1981                        /*GB*/
1982                        size /= 1024 * 1024 * 1024;
1983                        letter = 'G';
1984                }
1985                else
1986                {
1987                        /*MB*/
1988                        size /= 1024 * 1024;
1989                        letter = 'M';
1990                }
1991        }
1992        else if (size >= 1000)
1993        {
1994                /*KB*/
1995                size /= 1024;
1996                letter = 'K';
1997        }
1998        return [NSString stringWithFormat:@"%.1f%cB", size, letter];   
1999}
2000
2001/*See super documentation*/
2002- (NSMutableDictionary *)getDisplayedMetaDataInOrder:(NSArray * *)order;
2003{
2004        NSString *name = [path lastPathComponent];
2005        /*Create duration string*/
2006        int duration = [self duration];
2007        int secs = duration % 60;
2008        int mins = (duration /60) % 60;
2009        int hours = duration / 3600;
2010        NSString *durationStr = nil;
2011        if(hours != 0)
2012                durationStr = [NSString stringWithFormat:@"%d:%02d:%02d", hours, mins, secs];
2013        else if (mins != 0)
2014                durationStr = [NSString stringWithFormat:@"%d:%02d", mins, secs];
2015        else
2016                durationStr = [NSString stringWithFormat:@"%ds", secs];
2017        /*Set the order*/
2018        if(order != nil)
2019                *order = displayedMetaDataOrder;
2020        [self constructCombinedData];
2021        NSMutableDictionary *ret = [combinedInfo mutableCopy];
2022        /*Remove keys we don't display*/
2023        NSMutableSet *currentKeys = [NSMutableSet setWithArray:[ret allKeys]];
2024        [currentKeys minusSet:displayedMetaData];
2025        [ret removeObjectsForKeys:[currentKeys allObjects]];
2026       
2027        /*Substitute display titles for internal keys*/
2028        NSEnumerator *subEnum = [metaDataSubstitutions keyEnumerator];
2029        NSString *key = nil;
2030        while((key = [subEnum nextObject]) != nil)
2031        {
2032                NSString *value = [ret objectForKey:key];
2033                if(value != nil)
2034                {
2035                        /*Found object at a key, set it for the display title*/
2036                        [ret setObject:value forKey:[metaDataSubstitutions objectForKey:key]];
2037                        [ret removeObjectForKey:key];
2038                }
2039        }
2040        if([self duration])
2041        {
2042                if([self size])
2043                {
2044                        /*If we have a duration and size, combine into a single line*/
2045                        [ret setObject:[NSString stringWithFormat:@"%@ (%@)", durationStr, [self sizeString]] forKey:DURATION_KEY];
2046                        [ret removeObjectForKey:SIZE_KEY];
2047                }
2048                else
2049                        /*Otherwse, just set the duration*/
2050                        [ret setObject:durationStr forKey:DURATION_KEY];
2051        }
2052        else if([self size])
2053                /*If no duration, set the size*/
2054                [ret setObject:[self sizeString] forKey:SIZE_KEY];
2055        else
2056                /*Otherwise, remove the size*/
2057                [ret removeObjectForKey:SIZE_KEY];
2058       
2059        /*Set the title*/
2060        if([ret objectForKey:META_TITLE_KEY] == nil)
2061                [ret setObject:name forKey:META_TITLE_KEY];
2062        /*Set the season and episode*/
2063        int season = [self seasonNumber];
2064        int ep = [self episodeNumber];
2065        if(season != 0 && ep != 0)
2066                [ret setObject:[NSString stringWithFormat:@"%@ - %d / %d",[self showName], season, ep] forKey:META_EPISODE_AND_SEASON_KEY];
2067        return ret;
2068}
2069
2070/*Custom TV Episode handler*/
2071- (NSComparisonResult) episodeCompare:(SapphireFileMetaData *)other
2072{
2073        /*Sort by show first*/
2074        /*Put items with no show at the bottom*/
2075        NSString *myShow = [self showName];
2076        NSString *theirShow = [other showName];
2077        if(myShow != nil || theirShow != nil)
2078        {
2079                if(myShow == nil)
2080                        return NSOrderedDescending;
2081                else if(theirShow == nil)
2082                        return NSOrderedAscending;
2083                else
2084                {
2085                        /*Both have a show*/
2086                        NSComparisonResult result = [myShow compare:theirShow options:NSCaseInsensitiveSearch];
2087                        if(result != NSOrderedSame)
2088                                return result;
2089                }               
2090        }
2091        /*Sort by season first*/
2092        /*Put shows with no season at the bottom*/
2093        int myNum = [self seasonNumber];
2094        int theirNum = [other seasonNumber];
2095        if(myNum == 0)
2096                myNum = INT_MAX;
2097        if(theirNum == 0)
2098                theirNum = INT_MAX;
2099        if(myNum > theirNum)
2100                return NSOrderedDescending;
2101        if(theirNum > myNum)
2102                return NSOrderedAscending;
2103       
2104        /*Sort by episode next*/
2105        myNum = [self episodeNumber];
2106        theirNum = [other episodeNumber];
2107        if(myNum == 0)
2108                myNum = INT_MAX;
2109        if(theirNum == 0)
2110                theirNum = INT_MAX;
2111        if(myNum > theirNum)
2112                return NSOrderedDescending;
2113        if(theirNum > myNum)
2114                return NSOrderedAscending;
2115        /*Finally sort by name*/
2116        return [[path lastPathComponent] compare:[[other path] lastPathComponent] options:NSCaseInsensitiveSearch | NSNumericSearch];
2117}
2118
2119@end
Note: See TracBrowser for help on using the repository browser.