source: trunk/SapphireMetaData.m @ 252

Revision 252, 57.8 KB checked in by pmerrill, 7 years ago (diff)
  • Added a media preview for movie specific metadata.
  • Code cleanup.


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