source: trunk/SapphireMetaData.m @ 292

Last change on this file since 292 was 292, checked in by gbooker, 10 years ago

Added ability to join together multiple files into one. Great for movies which were segmented.

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