source: trunk/SapphireMetaData.m @ 364

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