MLMediaLibrary.m 32.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
/*****************************************************************************
 * MLMediaLibrary.m
 * MobileMediaLibraryKit
 *****************************************************************************
 * Copyright (C) 2010 Pierre d'Herbemont
 * Copyright (C) 2010-2013 VLC authors and VideoLAN
 * $Id$
 *
 * Authors: Pierre d'Herbemont <pdherbemont # videolan.org>
 *          Felix Paul Kühne <fkuehne # videolan.org>
 *
12 13 14
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
15 16 17 18
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
21
 *
22 23 24
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
25
 *****************************************************************************/
Pierre's avatar
Pierre committed
26 27

#import "MLMediaLibrary.h"
Pierre's avatar
Pierre committed
28 29
#import "MLTitleDecrapifier.h"
#import "MLMovieInfoGrabber.h"
Pierre's avatar
Pierre committed
30 31
#import "MLTVShowInfoGrabber.h"
#import "MLTVShowEpisodesInfoGrabber.h"
Pierre's avatar
Pierre committed
32 33 34 35
#import "MLFile.h"
#import "MLLabel.h"
#import "MLShowEpisode.h"
#import "MLShow.h"
36 37 38
#import "MLThumbnailerQueue.h"
#import "MLAlbumTrack.h"
#import "MLAlbum.h"
39
#import "MLFileParserQueue.h"
40
#import "MLCrashPreventer.h"
Pierre's avatar
Pierre committed
41 42

#define DEBUG 1
43 44
// To debug
#define DELETE_LIBRARY_ON_EACH_LAUNCH 0
Pierre's avatar
Pierre committed
45

Pierre's avatar
Pierre committed
46 47
// Pref key
static NSString *kLastTVDBUpdateServerTime = @"MLLastTVDBUpdateServerTime";
48
static NSString *kUpdatedToTheMojoWireDatabaseFormat = @"upgradedToDatabaseFormat 2.2";
Pierre's avatar
Pierre committed
49

Pierre's avatar
Pierre committed
50
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
51
@interface MLMediaLibrary ()
Pierre's avatar
Pierre committed
52 53 54
#else
@interface MLMediaLibrary () <MLMovieInfoGrabberDelegate, MLTVShowEpisodesInfoGrabberDelegate, MLTVShowInfoGrabberDelegate>
#endif
Pierre's avatar
Pierre committed
55
- (NSManagedObjectContext *)managedObjectContext;
Pierre's avatar
Pierre committed
56
- (NSString *)databaseFolderPath;
Pierre's avatar
Pierre committed
57 58
@end

Pierre's avatar
Pierre committed
59
@implementation MLMediaLibrary
60 61 62 63 64
+ (void)initialize
{
    [[NSUserDefaults standardUserDefaults] registerDefaults:@{kUpdatedToTheMojoWireDatabaseFormat : [NSNumber numberWithBool:NO]}];
}

Pierre's avatar
Pierre committed
65 66 67
+ (id)sharedMediaLibrary
{
    static id sharedMediaLibrary = nil;
Pierre's avatar
Pierre committed
68
    if (!sharedMediaLibrary) {
Pierre's avatar
Pierre committed
69
        sharedMediaLibrary = [[[self class] alloc] init];
70
        APLog(@"Initializing db in %@", [sharedMediaLibrary databaseFolderPath]);
71 72 73 74

        // Also force to init the crash preventer
        // Because it will correctly set up the parser and thumbnail queue
        [MLCrashPreventer sharedPreventer];
Pierre's avatar
Pierre committed
75
    }
Pierre's avatar
Pierre committed
76 77 78 79 80 81 82 83
    return sharedMediaLibrary;
}

- (NSFetchRequest *)fetchRequestForEntity:(NSString *)entity
{
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    NSManagedObjectContext *moc = [self managedObjectContext];
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:entity inManagedObjectContext:moc];
Pierre's avatar
Pierre committed
84
    NSAssert(entityDescription, @"No entity");
Pierre's avatar
Pierre committed
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
    [request setEntity:entityDescription];
    return [request autorelease];
}

- (id)createObjectForEntity:(NSString *)entity
{
    NSManagedObjectContext *moc = [self managedObjectContext];
    return [NSEntityDescription insertNewObjectForEntityForName:entity inManagedObjectContext:moc];
}

#pragma mark -
#pragma mark Media Library
- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel)
        return _managedObjectModel;
    _managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];
    return _managedObjectModel;
}

Pierre's avatar
Pierre committed
105
- (NSString *)databaseFolderPath
Pierre's avatar
Pierre committed
106
{
Pierre's avatar
Pierre committed
107 108
    int directory = NSLibraryDirectory;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
109
    NSString *directoryPath = paths[0];
110
#if DELETE_LIBRARY_ON_EACH_LAUNCH
Pierre's avatar
Pierre committed
111
    [[NSFileManager defaultManager] removeItemAtPath:directoryPath error:nil];
112
#endif
Pierre's avatar
Pierre committed
113
    return directoryPath;
Pierre's avatar
Pierre committed
114 115
}

Pierre's avatar
Pierre committed
116 117 118 119 120

- (NSString *)thumbnailFolderPath
{
    int directory = NSLibraryDirectory;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
121
    NSString *directoryPath = paths[0];
Pierre's avatar
Pierre committed
122 123 124 125 126 127
#if DELETE_LIBRARY_ON_EACH_LAUNCH
    [[NSFileManager defaultManager] removeItemAtPath:directoryPath error:nil];
#endif
    return [directoryPath stringByAppendingPathComponent:@"Thumbnails"];
}

Pierre's avatar
Pierre committed
128 129 130 131 132
- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext)
        return _managedObjectContext;

Pierre's avatar
Pierre committed
133
    NSString *databaseFolderPath = [self databaseFolderPath];
Pierre's avatar
Pierre committed
134

Pierre's avatar
Pierre committed
135 136
    NSString *path = [databaseFolderPath stringByAppendingPathComponent: @"MediaLibrary.sqlite"];
    NSURL *url = [NSURL fileURLWithPath:path];
Pierre's avatar
Pierre committed
137 138
    NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];

139
    NSNumber *yes = @YES;
140 141
    NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : yes,
                             NSInferMappingModelAutomaticallyOption : yes};
Pierre's avatar
Pierre committed
142 143

    NSError *error;
Pierre's avatar
Pierre committed
144 145 146
    NSPersistentStore *persistentStore = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:options error:&error];

    if (!persistentStore) {
Pierre's avatar
Pierre committed
147
#if! TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
148 149 150 151
        // FIXME: Deal with versioning
        NSInteger ret = NSRunAlertPanel(@"Error", @"The Media Library you have on your disk is not compatible with the one Lunettes can read. Do you want to create a new one?", @"No", @"Yes", nil);
        if (ret == NSOKButton)
            [NSApp terminate:nil];
Pierre's avatar
Pierre committed
152
        [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
Pierre's avatar
Pierre committed
153
#else
Pierre's avatar
Pierre committed
154
        [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
Pierre's avatar
Pierre committed
155
#endif
Pierre's avatar
Pierre committed
156 157
        persistentStore = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:options error:&error];
        if (!persistentStore) {
158 159
            if (coordinator)
                [coordinator release];
Pierre's avatar
Pierre committed
160 161 162 163 164
#if! TARGET_OS_IPHONE
            NSRunInformationalAlertPanel(@"Corrupted Media Library", @"There is nothing we can apparently do about it...", @"OK", nil, nil);
#else
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Corrupted Media Library" message:@"There is nothing we can apparently do about it..." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
            [alert show];
165
            [alert autorelease];
Pierre's avatar
Pierre committed
166 167 168 169
#endif
            // Probably assert instead.
            return nil;
        }
Pierre's avatar
Pierre committed
170
    }
Pierre's avatar
Pierre committed
171 172 173

    _managedObjectContext = [[NSManagedObjectContext alloc] init];
    [_managedObjectContext setPersistentStoreCoordinator:coordinator];
Pierre's avatar
Pierre committed
174 175 176 177 178 179 180 181 182
    [coordinator release];
    [_managedObjectContext setUndoManager:nil];
    [_managedObjectContext addObserver:self forKeyPath:@"hasChanges" options:NSKeyValueObservingOptionInitial context:nil];
    return _managedObjectContext;
}

- (void)savePendingChanges
{
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(savePendingChanges) object:nil];
Pierre's avatar
Pierre committed
183 184 185
    NSError *error = nil;
    BOOL success = [[self managedObjectContext] save:&error];
    NSAssert1(success, @"Can't save: %@", error);
Pierre's avatar
Pierre committed
186
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
187 188 189 190 191 192
    NSProcessInfo *process = [NSProcessInfo processInfo];
    if ([process respondsToSelector:@selector(enableSuddenTermination)])
        [process enableSuddenTermination];
#endif
}

193 194 195 196 197 198 199
- (void)save
{
    NSError *error = nil;
    BOOL success = [[self managedObjectContext] save:&error];
    NSAssert1(success, @"Can't save: %@", error);
}

Pierre's avatar
Pierre committed
200
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
Pierre's avatar
Pierre committed
201 202
{
    if ([keyPath isEqualToString:@"hasChanges"] && object == _managedObjectContext) {
Pierre's avatar
Pierre committed
203
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
204 205 206 207 208
        NSProcessInfo *process = [NSProcessInfo processInfo];
        if ([process respondsToSelector:@selector(disableSuddenTermination)])
            [process disableSuddenTermination];
#endif

Pierre's avatar
Pierre committed
209 210 211 212
        if ([[self managedObjectContext] hasChanges]) {
            [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(savePendingChanges) object:nil];
            [self performSelector:@selector(savePendingChanges) withObject:nil afterDelay:1.];
        }
Pierre's avatar
Pierre committed
213 214 215 216 217
        return;
    }
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

Pierre's avatar
Pierre committed
218 219
#pragma mark -
#pragma mark No meta data fallbacks
Pierre's avatar
Pierre committed
220

Pierre's avatar
Pierre committed
221
- (void)computeThumbnailForFile:(MLFile *)file
Pierre's avatar
Pierre committed
222
{
Pierre's avatar
Pierre committed
223
    if (!file.computedThumbnail) {
224
        APLog(@"Computing thumbnail for %@", file.title);
Pierre's avatar
Pierre committed
225 226
        [[MLThumbnailerQueue sharedThumbnailerQueue] addFile:file];
    }
Pierre's avatar
Pierre committed
227
}
Pierre's avatar
Pierre committed
228
- (void)errorWhenFetchingMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
229
{
230
    APLog(@"Error when fetching for '%@'", file.title);
Pierre's avatar
Pierre committed
231

Pierre's avatar
Pierre committed
232 233
    [self computeThumbnailForFile:file];
}
Pierre's avatar
Pierre committed
234

Pierre's avatar
Pierre committed
235 236 237 238 239 240 241
- (void)errorWhenFetchingMetaDataForShow:(MLShow *)show
{
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self errorWhenFetchingMetaDataForFile:file];
    }
}
Pierre's avatar
Pierre committed
242

Pierre's avatar
Pierre committed
243 244
- (void)noMetaDataInRemoteDBForFile:(MLFile *)file
{
245
    file.noOnlineMetaData = @YES;
Pierre's avatar
Pierre committed
246
    [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
247 248
}

Pierre's avatar
Pierre committed
249
- (void)noMetaDataInRemoteDBForShow:(MLShow *)show
Pierre's avatar
Pierre committed
250
{
Pierre's avatar
Pierre committed
251 252 253
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self noMetaDataInRemoteDBForFile:file];
Pierre's avatar
Pierre committed
254 255 256
    }
}

Pierre's avatar
Pierre committed
257 258 259 260
#pragma mark -
#pragma mark Getter

- (void)addNewLabelWithName:(NSString *)name
Pierre's avatar
Pierre committed
261
{
Pierre's avatar
Pierre committed
262 263
    MLLabel *label = [self createObjectForEntity:@"Label"];
    label.name = name;
Pierre's avatar
Pierre committed
264 265
}

Pierre's avatar
Pierre committed
266 267 268 269 270 271 272
/**
 * TV MLShow Episodes
 */

#pragma mark -
#pragma mark Online meta data grabbing

Pierre's avatar
Pierre committed
273 274 275
#if !HAVE_BLOCK
- (void)tvShowEpisodesInfoGrabberDidFinishGrabbing:(MLTVShowEpisodesInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
276
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
277 278

    NSArray *results = grabber.episodesResults;
279
    [show setValue:(grabber.results)[@"serieArtworkURL"] forKey:@"artworkURL"];
Pierre's avatar
Pierre committed
280
    for (id result in results) {
281
        if ([result[@"serie"] boolValue]) {
Pierre's avatar
Pierre committed
282 283
            continue;
        }
284 285 286 287 288
        MLShowEpisode *showEpisode = [MLShowEpisode episodeWithShow:show episodeNumber:result[@"episodeNumber"] seasonNumber:result[@"seasonNumber"] createIfNeeded:YES];
        showEpisode.name = result[@"title"];
        showEpisode.theTVDBID = result[@"id"];
        showEpisode.shortSummary = result[@"shortSummary"];
        showEpisode.artworkURL = result[@"artworkURL"];
Pierre's avatar
Pierre committed
289 290 291 292 293
        if (!showEpisode.artworkURL) {
            for (MLFile *file in showEpisode.files)
                [self computeThumbnailForFile:file];
        }

Pierre's avatar
Pierre committed
294 295 296 297 298
        showEpisode.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
    show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
}

Pierre's avatar
Pierre committed
299 300 301 302 303 304
- (void)tvShowEpisodesInfoGrabber:(MLTVShowEpisodesInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
305 306
- (void)tvShowInfoGrabberDidFinishGrabbing:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
307
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
308 309
    NSArray *results = grabber.results;
    if ([results count] > 0) {
310 311
        NSDictionary *result = results[0];
        NSString *showId = result[@"id"];
Pierre's avatar
Pierre committed
312 313

        show.theTVDBID = showId;
314 315 316
        show.name = result[@"title"];
        show.shortSummary = result[@"shortSummary"];
        show.releaseYear = result[@"releaseYear"];
Pierre's avatar
Pierre committed
317 318 319 320 321 322 323 324 325

        // Fetch episodes info
        MLTVShowEpisodesInfoGrabber *grabber = [[[MLTVShowEpisodesInfoGrabber alloc] init] autorelease];
        grabber.delegate = self;
        grabber.userData = show;
        [grabber lookUpForShowID:showId];
    }
    else {
        // Not found.
Pierre's avatar
Pierre committed
326
        [self noMetaDataInRemoteDBForShow:show];
Pierre's avatar
Pierre committed
327 328 329 330
        show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
}

Pierre's avatar
Pierre committed
331 332 333 334 335 336
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
337 338
- (void)tvShowInfoGrabberDidFetchServerTime:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
339
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
340 341 342

    [[NSUserDefaults standardUserDefaults] setInteger:[[MLTVShowInfoGrabber serverTime] integerValue] forKey:kLastTVDBUpdateServerTime];

Pierre's avatar
Pierre committed
343
    // First fetch the MLShow ID
Pierre's avatar
Pierre committed
344 345 346 347
    MLTVShowInfoGrabber *showInfoGrabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
    showInfoGrabber.delegate = self;
    showInfoGrabber.userData = show;

348
    APLog(@"Fetching show information on %@", show.name);
Pierre's avatar
Pierre committed
349

Pierre's avatar
Pierre committed
350 351 352 353
    [showInfoGrabber lookUpForTitle:show.name];
}
#endif

Pierre's avatar
Pierre committed
354
- (void)fetchMetaDataForShow:(MLShow *)show
Pierre's avatar
Pierre committed
355
{
Pierre's avatar
Pierre committed
356 357
    if (!_allowNetworkAccess)
        return;
358
    APLog(@"Fetching show server time");
Pierre's avatar
Pierre committed
359

Pierre's avatar
Pierre committed
360
    // First fetch the serverTime, so that we can update each entry.
Pierre's avatar
Pierre committed
361 362
#if HAVE_BLOCK
    [MLTVShowInfoGrabber fetchServerTimeAndExecuteBlock:^(NSNumber *serverDate) {
Pierre's avatar
Pierre committed
363 364 365

        [[NSUserDefaults standardUserDefaults] setInteger:[serverDate integerValue] forKey:kLastTVDBUpdateServerTime];

366
        APLog(@"Fetching show information on %@", show.name);
Pierre's avatar
Pierre committed
367 368 369

        // First fetch the MLShow ID
        MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
370 371 372 373 374 375 376 377 378 379 380
        [grabber lookUpForTitle:show.name andExecuteBlock:^{
            NSArray *results = grabber.results;
            if ([results count] > 0) {
                NSDictionary *result = [results objectAtIndex:0];
                NSString *showId = [result objectForKey:@"id"];

                show.theTVDBID = showId;
                show.name = [result objectForKey:@"title"];
                show.shortSummary = [result objectForKey:@"shortSummary"];
                show.releaseYear = [result objectForKey:@"releaseYear"];

381
                APLog(@"Fetching show episode information on %@", showId);
Pierre's avatar
Pierre committed
382

Pierre's avatar
Pierre committed
383
                // Fetch episode info
Pierre's avatar
Pierre committed
384
                MLTVShowEpisodesInfoGrabber *grabber = [[[MLTVShowEpisodesInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
385 386 387 388 389 390 391
                [grabber lookUpForShowID:showId andExecuteBlock:^{
                    NSArray *results = grabber.episodesResults;
                    [show setValue:[grabber.results objectForKey:@"serieArtworkURL"] forKey:@"artworkURL"];
                    for (id result in results) {
                        if ([[result objectForKey:@"serie"] boolValue]) {
                            continue;
                        }
392
                        MLShowEpisode *showEpisode = [MLShowEpisode episodeWithShow:show episodeNumber:[result objectForKey:@"episodeNumber"] seasonNumber:[result objectForKey:@"seasonNumber"] createIfNeeded:YES];
Pierre's avatar
Pierre committed
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408
                        showEpisode.name = [result objectForKey:@"title"];
                        showEpisode.theTVDBID = [result objectForKey:@"id"];
                        showEpisode.shortSummary = [result objectForKey:@"shortSummary"];
                        showEpisode.artworkURL = [result objectForKey:@"artworkURL"];
                        showEpisode.lastSyncDate = serverDate;
                    }
                    show.lastSyncDate = serverDate;
                }];
            }
            else {
                // Not found.
                show.lastSyncDate = serverDate;
            }

        }];
    }];
Pierre's avatar
Pierre committed
409
#else
Pierre's avatar
Pierre committed
410 411 412 413
    MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
    grabber.delegate = self;
    grabber.userData = show;
    [grabber fetchServerTime];
Pierre's avatar
Pierre committed
414
#endif
Pierre's avatar
Pierre committed
415 416
}

Pierre's avatar
Pierre committed
417
- (void)addTVShowEpisodeWithInfo:(NSDictionary *)tvShowEpisodeInfo andFile:(MLFile *)file
Pierre's avatar
Pierre committed
418
{
Pierre's avatar
Pierre committed
419
    file.type = kMLFileTypeTVShowEpisode;
Pierre's avatar
Pierre committed
420

421 422 423
    NSNumber *seasonNumber = tvShowEpisodeInfo[@"season"];
    NSNumber *episodeNumber = tvShowEpisodeInfo[@"episode"];
    NSString *tvShowName = tvShowEpisodeInfo[@"tvShowName"];
424
    NSString *tvEpisodeName = tvShowEpisodeInfo[@"tvEpisodeName"];
Pierre's avatar
Pierre committed
425 426
    BOOL hasNoTvShow = NO;
    if (!tvShowName) {
427
        tvShowName = @"";
Pierre's avatar
Pierre committed
428 429 430
        hasNoTvShow = YES;
    }
    BOOL wasInserted = NO;
Pierre's avatar
Pierre committed
431 432 433 434
    MLShow *show = nil;
    MLShowEpisode *episode = [MLShowEpisode episodeWithShowName:tvShowName episodeNumber:episodeNumber seasonNumber:seasonNumber createIfNeeded:YES wasCreated:&wasInserted];
    if (episode)
        show = episode.show;
Pierre's avatar
Pierre committed
435 436 437 438
    if (wasInserted && !hasNoTvShow) {
        show.name = tvShowName;
        [self fetchMetaDataForShow:show];
    }
439
    episode.name = tvEpisodeName;
Pierre's avatar
Pierre committed
440

Felix Paul Kühne's avatar
Felix Paul Kühne committed
441
    if (episode.name.length < 1)
Pierre's avatar
Pierre committed
442 443 444
        episode.name = file.title;
    file.seasonNumber = seasonNumber;
    file.episodeNumber = episodeNumber;
445
    episode.shouldBeDisplayed = @YES;
Pierre's avatar
Pierre committed
446 447

    [episode addFilesObject:file];
Pierre's avatar
Pierre committed
448
    file.showEpisode = episode;
Pierre's avatar
Pierre committed
449

Pierre's avatar
Pierre committed
450
    // The rest of the meta data will be fetched using the MLShow
451
    file.hasFetchedInfo = @YES;
Pierre's avatar
Pierre committed
452 453
}

454 455 456 457
- (void)addAudioContentWithInfo:(NSDictionary *)audioContentInfo andFile:(MLFile *)file
{
    file.type = kMLFileTypeAudio;

458 459 460
    file.title = audioContentInfo[VLCMetaInformationTitle];

    /* all further meta data is set by the FileParserQueue */
461 462 463 464

    file.hasFetchedInfo = @YES;
}

Pierre's avatar
Pierre committed
465
/**
Pierre's avatar
Pierre committed
466
 * MLFile auto detection
Pierre's avatar
Pierre committed
467 468
 */

Pierre's avatar
Pierre committed
469
#if !HAVE_BLOCK
Pierre's avatar
Pierre committed
470 471 472 473 474 475
- (void)movieInfoGrabber:(MLMovieInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLFile *file = grabber.userData;
    [self errorWhenFetchingMetaDataForFile:file];
}

Pierre's avatar
Pierre committed
476 477
- (void)movieInfoGrabberDidFinishGrabbing:(MLMovieInfoGrabber *)grabber
{
478
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
479 480

    NSArray *results = grabber.results;
Pierre's avatar
Pierre committed
481
    MLFile *file = grabber.userData;
Pierre's avatar
Pierre committed
482
    if ([results count] > 0) {
483 484 485 486 487
        NSDictionary *result = results[0];
        file.artworkURL = result[@"artworkURL"];
        file.title = result[@"title"];
        file.shortSummary = result[@"shortSummary"];
        file.releaseYear = result[@"releaseYear"];
Pierre's avatar
Pierre committed
488
    }
Pierre's avatar
Pierre committed
489 490 491 492
    else {
        [self noMetaDataInRemoteDBForFile:file];
    }

Pierre's avatar
Pierre committed
493 494 495 496
    file.hasFetchedInfo = yes;
}
#endif

Pierre's avatar
Pierre committed
497
- (void)fetchMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
498
{
499
    APLog(@"Fetching meta data for %@", file.title);
Pierre's avatar
Pierre committed
500

501
    [[MLFileParserQueue sharedFileParserQueue] addFile:file];
502

Pierre's avatar
Pierre committed
503 504 505 506 507
    if (!_allowNetworkAccess) {
        // Automatically compute the thumbnail
        [self computeThumbnailForFile:file];
    }

Pierre's avatar
Pierre committed
508
    NSDictionary *tvShowEpisodeInfo = [MLTitleDecrapifier tvShowEpisodeInfoFromString:file.title];
Pierre's avatar
Pierre committed
509 510 511 512 513
    if (tvShowEpisodeInfo) {
        [self addTVShowEpisodeWithInfo:tvShowEpisodeInfo andFile:file];
        return;
    }

514 515 516 517 518 519 520 521
    if ([file isSupportedAudioFile]) {
        NSDictionary *audioContentInfo = [MLTitleDecrapifier audioContentInfoFromFile:file];
        if (audioContentInfo && ![file videoTrack]) {
            [self addAudioContentWithInfo:audioContentInfo andFile:file];
            return;
        }
    }

Pierre's avatar
Pierre committed
522 523 524
    if (!_allowNetworkAccess)
        return;

Pierre's avatar
Pierre committed
525
    // Go online and fetch info.
Pierre's avatar
Pierre committed
526 527 528

    // We don't care about keeping a reference to track the item during its life span
    // because we are a singleton
Pierre's avatar
Pierre committed
529
    MLMovieInfoGrabber *grabber = [[[MLMovieInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
530

531
    APLog(@"Looking up for Movie '%@'", file.title);
Pierre's avatar
Pierre committed
532

Pierre's avatar
Pierre committed
533
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
534
    [grabber lookUpForTitle:file.title andExecuteBlock:^(NSError *err){
Pierre's avatar
Pierre committed
535 536
        if (err) {
            [self errorWhenFetchingMetaDataForFile:file];
Pierre's avatar
Pierre committed
537
            return;
Pierre's avatar
Pierre committed
538
        }
Pierre's avatar
Pierre committed
539 540 541 542 543

        NSArray *results = grabber.results;
        if ([results count] > 0) {
            NSDictionary *result = [results objectAtIndex:0];
            file.artworkURL = [result objectForKey:@"artworkURL"];
Pierre's avatar
Pierre committed
544 545
            if (!file.artworkURL)
                [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
546 547 548
            file.title = [result objectForKey:@"title"];
            file.shortSummary = [result objectForKey:@"shortSummary"];
            file.releaseYear = [result objectForKey:@"releaseYear"];
Pierre's avatar
Pierre committed
549 550 551
        } else
            [self noMetaDataInRemoteDBForFile:file];
        file.hasFetchedInfo = [NSNumber numberWithBool:YES];
Pierre's avatar
Pierre committed
552
    }];
Pierre's avatar
Pierre committed
553
#else
Pierre's avatar
Pierre committed
554 555 556
    grabber.userData = file;
    grabber.delegate = self;
    [grabber lookUpForTitle:file.title];
Pierre's avatar
Pierre committed
557
#endif
Pierre's avatar
Pierre committed
558 559
}

Pierre's avatar
Pierre committed
560 561 562
#pragma mark -
#pragma mark Adding file to the DB

Pierre's avatar
Pierre committed
563
- (void)addFilePath:(NSString *)filePath
Pierre's avatar
Pierre committed
564
{
565
    APLog(@"Adding Path %@", filePath);
Pierre's avatar
Pierre committed
566

Pierre's avatar
Pierre committed
567
    NSURL *url = [NSURL fileURLWithPath:filePath];
Pierre's avatar
Pierre committed
568 569
    NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
    NSString *title = [filePath lastPathComponent];
Pierre's avatar
Pierre committed
570
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
571 572
    NSDate *openedDate = nil; // FIXME kMDItemLastUsedDate
    NSDate *modifiedDate = nil; // FIXME [result valueForAttribute:@"kMDItemFSContentChangeDate"];
Pierre's avatar
Pierre committed
573
#endif
574
    NSNumber *size = attributes[NSFileSize]; // FIXME [result valueForAttribute:@"kMDItemFSSize"];
Pierre's avatar
Pierre committed
575

Pierre's avatar
Pierre committed
576
    MLFile *file = [self createObjectForEntity:@"File"];
Pierre's avatar
Pierre committed
577
    file.url = [url absoluteString];
Pierre's avatar
Pierre committed
578 579 580 581

    // Yes, this is a negative number. VLCTime nicely display negative time
    // with "XX minutes remaining". And we are using this facility.

582 583
    NSNumber *no = @NO;
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
584 585

    file.currentlyWatching = no;
586 587
    file.lastPosition = @0.0;
    file.remainingTime = @0.0;
Pierre's avatar
Pierre committed
588 589
    file.unread = yes;

Pierre's avatar
Pierre committed
590
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
591 592 593 594
    if ([openedDate isGreaterThan:modifiedDate]) {
        file.playCount = [NSNumber numberWithDouble:1];
        file.unread = no;
    }
Pierre's avatar
Pierre committed
595 596
#endif

Pierre's avatar
Pierre committed
597
    file.title = [MLTitleDecrapifier decrapify:[title stringByDeletingPathExtension]];
Pierre's avatar
Pierre committed
598 599

    if ([size longLongValue] < 150000000) /* 150 MB */
Pierre's avatar
Pierre committed
600
        file.type = kMLFileTypeClip;
Pierre's avatar
Pierre committed
601
    else
Pierre's avatar
Pierre committed
602
        file.type = kMLFileTypeMovie;
Pierre's avatar
Pierre committed
603 604 605 606

    [self fetchMetaDataForFile:file];
}

Pierre's avatar
Pierre committed
607
- (void)addFilePaths:(NSArray *)filepaths
Pierre's avatar
Pierre committed
608
{
Pierre's avatar
Pierre committed
609
    NSUInteger count = [filepaths count];
Pierre's avatar
Pierre committed
610 611 612 613
    NSMutableArray *fetchPredicates = [NSMutableArray arrayWithCapacity:count];
    NSMutableDictionary *urlToObject = [NSMutableDictionary dictionaryWithCapacity:count];

    // Prepare a fetch request for all items
Pierre's avatar
Pierre committed
614
    for (NSString *path in filepaths) {
Pierre's avatar
Pierre committed
615
        NSURL *url = [NSURL fileURLWithPath:path];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
616
        NSString *urlString = [url absoluteString];
Pierre's avatar
Pierre committed
617
        [fetchPredicates addObject:[NSPredicate predicateWithFormat:@"url == %@", urlString]];
618
        urlToObject[urlString] = path;
Pierre's avatar
Pierre committed
619 620 621 622 623 624
    }

    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];

    [request setPredicate:[NSCompoundPredicate orPredicateWithSubpredicates:fetchPredicates]];

625
    APLog(@"Fetching");
Pierre's avatar
Pierre committed
626
    NSArray *dbResults = [[self managedObjectContext] executeFetchRequest:request error:nil];
627
    APLog(@"Done");
Pierre's avatar
Pierre committed
628

Pierre's avatar
Pierre committed
629
    NSMutableArray *filePathsToAdd = [NSMutableArray arrayWithArray:filepaths];
Pierre's avatar
Pierre committed
630 631

    // Remove objects that are already in db.
Pierre's avatar
Pierre committed
632 633
    for (MLFile *dbResult in dbResults) {
        NSString *urlString = dbResult.url;
634
        [filePathsToAdd removeObject:urlToObject[urlString]];
Pierre's avatar
Pierre committed
635 636 637
    }

    // Add only the newly added items
Pierre's avatar
Pierre committed
638 639
    for (NSString* path in filePathsToAdd)
        [self addFilePath:path];
Pierre's avatar
Pierre committed
640 641
}

Pierre's avatar
Pierre committed
642 643 644 645

#pragma mark -
#pragma mark DB Updates

Pierre's avatar
Pierre committed
646 647 648 649 650 651
#if !HAVE_BLOCK
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFetchUpdates:(NSArray *)updates
{
    NSFetchRequest *request = [self fetchRequestForEntity:@"Show"];
    [request setPredicate:[NSComparisonPredicate predicateWithLeftExpression:[NSExpression expressionForKeyPath:@"theTVDBID"] rightExpression:[NSExpression expressionForConstantValue:updates] modifier:NSDirectPredicateModifier type:NSInPredicateOperatorType options:0]];
    NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
652
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
653 654 655 656
        [self fetchMetaDataForShow:show];
}
#endif

657 658 659 660 661 662 663 664 665
- (BOOL)libraryNeedsUpgrade
{
    if (![[[NSUserDefaults standardUserDefaults] objectForKey:kUpdatedToTheMojoWireDatabaseFormat] boolValue])
        return YES;
    return NO;
}

- (void)upgradeLibrary
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
666
    [self libraryDidDisappear];
667 668 669
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSFileManager *fileManager = [NSFileManager defaultManager];

670
    /* remove potential empty albums left over by previous releases */
671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704
    NSArray *collection = [MLAlbum allAlbums];
    NSUInteger count = collection.count;
    MLAlbum *album;
    MLAlbumTrack *track;
    NSArray *secondaryCollection;
    NSURL *fileURL;
    NSUInteger secondaryCount = 0;
    NSArray *tertiaryCollection;
    NSUInteger tertiaryCount = 0;
    NSUInteger emptyAlbumCounter = 0;
    for (NSUInteger x = 0; x < count; x++) {
        album = collection[x];
        if (album.tracks.count < 1)
            [[self managedObjectContext] deleteObject:album];
        else {
            secondaryCollection = album.tracks.allObjects;
            secondaryCount = secondaryCollection.count;
            emptyAlbumCounter = 0;
            for (NSUInteger y = 0; y < secondaryCount; y++) {
                track = secondaryCollection[y];
                tertiaryCollection = track.files.allObjects;
                tertiaryCount = tertiaryCollection.count;
                for (NSUInteger z = 0; z < tertiaryCount; z++) {
                    fileURL = [NSURL URLWithString:[(MLFile *)tertiaryCollection[z] url]];
                    BOOL exists = [fileManager fileExistsAtPath:[fileURL path]];
                    if (exists)
                        emptyAlbumCounter++;
                }
            }
            if (emptyAlbumCounter == 0)
                [[self managedObjectContext] deleteObject:album];
        }
    }
    album = nil;
705 706

    /* remove potential empty shows left over by previous releases */
707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734
    collection = [MLShow allShows];
    MLShow *show;
    MLShowEpisode *showEpisode;
    count = collection.count;
    for (NSUInteger x = 0; x < count; x++) {
        show = collection[x];
        if (show.episodes.count < 1)
            [[self managedObjectContext] deleteObject:show];
        else {
            secondaryCollection = show.episodes.allObjects;
            secondaryCount = secondaryCollection.count;
            emptyAlbumCounter = 0;
            for (NSUInteger y = 0; y < secondaryCount; y++) {
                showEpisode = secondaryCollection[y];
                tertiaryCollection = showEpisode.files.allObjects;
                tertiaryCount = tertiaryCollection.count;
                for (NSUInteger z = 0; z < tertiaryCount; z++) {
                    fileURL = [NSURL URLWithString:[(MLFile *)tertiaryCollection[z] url]];
                    BOOL exists = [fileManager fileExistsAtPath:[fileURL path]];
                    if (exists)
                        emptyAlbumCounter++;
                }
            }
            if (emptyAlbumCounter == 0)
                [[self managedObjectContext] deleteObject:show];
        }
    }

735 736 737 738 739
    /* remove duplicates */
    NSArray *allFiles = [MLFile allFiles];
    NSUInteger allFilesCount = allFiles.count;
    NSMutableArray *seenFiles = [[NSMutableArray alloc] initWithCapacity:allFilesCount];
    MLFile *currentFile;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
740 741
    NSString *currentFilePath;
    for (NSUInteger x = 0; x < allFilesCount; x++) {
742
        currentFile = allFiles[x];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
743 744
        currentFilePath = [currentFile.url stringByReplacingOccurrencesOfString:@"/localhost/" withString:@"//"];
        if ([seenFiles containsObject:currentFilePath])
745 746
            [[self managedObjectContext] deleteObject:currentFile];
        else
Felix Paul Kühne's avatar
Felix Paul Kühne committed
747
            [seenFiles addObject:currentFilePath];
748 749
    }

Felix Paul Kühne's avatar
Felix Paul Kühne committed
750 751 752 753
    [defaults setBool:YES forKey:kUpdatedToTheMojoWireDatabaseFormat];
    [defaults synchronize];

    [self libraryDidAppear];
754 755 756 757
    if ([self.delegate respondsToSelector:@selector(libraryUpgradeComplete)])
        [self.delegate libraryUpgradeComplete];
}

758
- (void)updateMediaDatabase
Pierre's avatar
Pierre committed
759
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
760
    [self libraryDidDisappear];
Pierre's avatar
Pierre committed
761
    // Remove no more present files
Pierre's avatar
Pierre committed
762 763
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];
    NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
764
    NSFileManager *fileManager = [NSFileManager defaultManager];
765

766 767 768
    unsigned int count = results.count;
    for (unsigned int x = 0; x < count; x++) {
        MLFile *file = results[x];
Pierre's avatar
Pierre committed
769 770
        NSString *urlString = [file url];
        NSURL *fileURL = [NSURL URLWithString:urlString];
771
        BOOL exists = [fileManager fileExistsAtPath:[fileURL path]];
772
        if (!exists) {
773
            APLog(@"Marking - %@", [fileURL absoluteString]);
774
            file.isSafe = YES; // It doesn't exists, it's safe.
775 776
            if (file.isAlbumTrack) {
                MLAlbum *album = file.albumTrack.album;
777 778 779 780 781 782 783 784
                if (album.tracks.count <= 1) {
                    @try {
                        [[self managedObjectContext] deleteObject:album];
                    }
                    @catch (NSException *exception) {
                        APLog(@"failed to nuke object because it disappeared in front of us");
                    }
                }
785 786 787
            }
            if (file.isShowEpisode) {
                MLShow *show = file.showEpisode.show;
788 789 790 791 792 793 794 795
                if (show.episodes.count <= 1) {
                    @try {
                        [[self managedObjectContext] deleteObject:show];
                    }
                    @catch (NSException *exception) {
                        APLog(@"failed to nuke object because it disappeared in front of us");
                    }
                }
796
            }
797
#if TARGET_OS_IPHONE
798
            NSString *thumbPath = [[[self thumbnailFolderPath] stringByAppendingPathComponent:[[file.objectID URIRepresentation] path]] stringByAppendingString:@".png"];
799 800
            bool thumbExists = [fileManager fileExistsAtPath:thumbPath];
            if (thumbExists)
801
                [fileManager removeItemAtPath:thumbPath error:nil];
802 803
            [[self managedObjectContext] deleteObject:file];
#endif
804
        }
805 806 807
#if !TARGET_OS_IPHONE
    file.isOnDisk = @(exists);
#endif
Pierre's avatar
Pierre committed
808
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
809
    [self libraryDidAppear];
Pierre's avatar
Pierre committed
810

811 812 813 814 815 816 817
    // Get the file to parse
    request = [self fetchRequestForEntity:@"File"];
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && tracks.@count == 0"]];
    results = [[self managedObjectContext] executeFetchRequest:request error:nil];
    for (MLFile *file in results)
        [[MLFileParserQueue sharedFileParserQueue] addFile:file];

Pierre's avatar
Pierre committed
818 819 820
    if (!_allowNetworkAccess) {
        // Always attempt to fetch
        request = [self fetchRequestForEntity:@"File"];
Pierre's avatar
Pierre committed
821
        [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES"]];
Pierre's avatar
Pierre committed
822
        results = [[self managedObjectContext] executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
823 824 825 826
        for (MLFile *file in results) {
            if (!file.computedThumbnail)
                [self computeThumbnailForFile:file];
        }
Pierre's avatar
Pierre committed
827 828 829
        return;
    }

Pierre's avatar
Pierre committed
830 831
    // Get the thumbnails to compute
    request = [self fetchRequestForEntity:@"File"];
Pierre's avatar
Pierre committed
832
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && hasFetchedInfo == 1 && artworkURL == nil"]];
Pierre's avatar
Pierre committed
833 834
    results = [[self managedObjectContext] executeFetchRequest:request error:nil];
    for (MLFile *file in results)
835
        if (!file.computedThumbnail && ![file isAlbumTrack])
Pierre's avatar
Pierre committed
836
            [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
837 838 839 840 841 842

    // Get to fetch meta data
    request = [self fetchRequestForEntity:@"File"];
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && hasFetchedInfo == 0"]];
    results = [[self managedObjectContext] executeFetchRequest:request error:nil];
    for (MLFile *file in results)
Pierre's avatar
Pierre committed
843 844
        [self fetchMetaDataForFile:file];

Pierre's avatar
Pierre committed
845
    // Get to fetch show info
Pierre's avatar
Pierre committed
846 847 848
    request = [self fetchRequestForEntity:@"Show"];
    [request setPredicate:[NSPredicate predicateWithFormat:@"lastSyncDate == 0"]];
    results = [[self managedObjectContext] executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
849
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
850 851
        [self fetchMetaDataForShow:show];

Pierre's avatar
Pierre committed
852
    // Get updated TV Shows
853
    NSNumber *lastServerTime = @([[NSUserDefaults standardUserDefaults] integerForKey:kLastTVDBUpdateServerTime]);
Pierre's avatar
Pierre committed
854
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
855
    [MLTVShowInfoGrabber fetchUpdatesSinceServerTime:lastServerTime andExecuteBlock:^(NSArray *updates){
Pierre's avatar
Pierre committed
856 857 858
        NSFetchRequest *request = [self fetchRequestForEntity:@"Show"];
        [request setPredicate:[NSComparisonPredicate predicateWithLeftExpression:[NSExpression expressionForKeyPath:@"theTVDBID"] rightExpression:[NSExpression expressionForConstantValue:updates] modifier:NSDirectPredicateModifier type:NSInPredicateOperatorType options:0]];
        NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
859
        for (MLShow *show in results)
Pierre's avatar
Pierre committed
860 861
            [self fetchMetaDataForShow:show];
    }];
Pierre's avatar
Pierre committed
862 863 864 865
#else
    MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
    grabber.delegate = self;
    [grabber fetchUpdatesSinceServerTime:lastServerTime];
Pierre's avatar
Pierre committed
866
#endif
Pierre's avatar
Pierre committed
867
    /* Update every hour - FIXME: Preferences key */
Felix Paul Kühne's avatar
Felix Paul Kühne committed
868
    [self performSelector:@selector(updateMediaDatabase) withObject:nil afterDelay:60 * 60];
Pierre's avatar
Pierre committed
869
}
Pierre's avatar
Pierre committed
870

871 872 873 874 875
- (void)applicationWillExit
{
    [[MLCrashPreventer sharedPreventer] cancelAllFileParse];
}

Pierre's avatar
Pierre committed
876 877 878 879 880
- (void)applicationWillStart
{
    [[MLCrashPreventer sharedPreventer] markCrasherFiles];
}

Pierre's avatar
Pierre committed
881 882 883 884 885 886 887 888 889 890 891
- (void)libraryDidDisappear
{
    // Stop expansive work
    [[MLThumbnailerQueue sharedThumbnailerQueue] stop];
}

- (void)libraryDidAppear
{
    // Resume our work
    [[MLThumbnailerQueue sharedThumbnailerQueue] resume];
}
Pierre's avatar
Pierre committed
892
@end