source: trunk/SapphireMetaData.m @ 337

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