source: trunk/SapphireMetaData.m @ 363

Revision 363, 64.4 KB checked in by gbooker, 6 years ago (diff)

Reverted some of [356] since it is incompatible with the new method of virtual dir cover art

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