source: trunk/SapphireMetaData.m @ 296

Revision 296, 60.8 KB checked in by gbooker, 6 years ago (diff)

Switch the cover art to a function of the metadata so the virtual folders can ovrride them.

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