MLMediaLibrary.m 32.4 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
    return sharedMediaLibrary;
}

79 80 81 82 83 84 85 86
- (void)dealloc
{
    if (_managedObjectContext)
        [_managedObjectContext removeObserver:self forKeyPath:@"hasChanges"];
    [_managedObjectContext release];
    [super dealloc];
}

Pierre's avatar
Pierre committed
87 88 89 90 91
- (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
92
    NSAssert(entityDescription, @"No entity");
Pierre's avatar
Pierre committed
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
    [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
113
- (NSString *)databaseFolderPath
Pierre's avatar
Pierre committed
114
{
Pierre's avatar
Pierre committed
115 116
    int directory = NSLibraryDirectory;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
117
    NSString *directoryPath = paths[0];
118
#if DELETE_LIBRARY_ON_EACH_LAUNCH
Pierre's avatar
Pierre committed
119
    [[NSFileManager defaultManager] removeItemAtPath:directoryPath error:nil];
120
#endif
Pierre's avatar
Pierre committed
121
    return directoryPath;
Pierre's avatar
Pierre committed
122 123
}

Pierre's avatar
Pierre committed
124 125 126 127 128

- (NSString *)thumbnailFolderPath
{
    int directory = NSLibraryDirectory;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
129
    NSString *directoryPath = paths[0];
Pierre's avatar
Pierre committed
130 131 132 133 134 135
#if DELETE_LIBRARY_ON_EACH_LAUNCH
    [[NSFileManager defaultManager] removeItemAtPath:directoryPath error:nil];
#endif
    return [directoryPath stringByAppendingPathComponent:@"Thumbnails"];
}

Pierre's avatar
Pierre committed
136 137 138 139 140
- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext)
        return _managedObjectContext;

Pierre's avatar
Pierre committed
141
    NSString *databaseFolderPath = [self databaseFolderPath];
Pierre's avatar
Pierre committed
142

Pierre's avatar
Pierre committed
143 144
    NSString *path = [databaseFolderPath stringByAppendingPathComponent: @"MediaLibrary.sqlite"];
    NSURL *url = [NSURL fileURLWithPath:path];
Pierre's avatar
Pierre committed
145 146
    NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];

147
    NSNumber *yes = @YES;
148 149
    NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : yes,
                             NSInferMappingModelAutomaticallyOption : yes};
Pierre's avatar
Pierre committed
150 151

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

    if (!persistentStore) {
Pierre's avatar
Pierre committed
155
#if! TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
156 157 158 159
        // 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
160
        [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
Pierre's avatar
Pierre committed
161
#else
Pierre's avatar
Pierre committed
162
        [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
Pierre's avatar
Pierre committed
163
#endif
Pierre's avatar
Pierre committed
164 165
        persistentStore = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:options error:&error];
        if (!persistentStore) {
166 167
            if (coordinator)
                [coordinator release];
Pierre's avatar
Pierre committed
168 169 170 171 172
#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];
173
            [alert autorelease];
Pierre's avatar
Pierre committed
174 175 176 177
#endif
            // Probably assert instead.
            return nil;
        }
Pierre's avatar
Pierre committed
178
    }
Pierre's avatar
Pierre committed
179 180 181

    _managedObjectContext = [[NSManagedObjectContext alloc] init];
    [_managedObjectContext setPersistentStoreCoordinator:coordinator];
Pierre's avatar
Pierre committed
182 183 184 185 186 187 188 189 190
    [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
191 192 193
    NSError *error = nil;
    BOOL success = [[self managedObjectContext] save:&error];
    NSAssert1(success, @"Can't save: %@", error);
Pierre's avatar
Pierre committed
194
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
195 196 197 198 199 200
    NSProcessInfo *process = [NSProcessInfo processInfo];
    if ([process respondsToSelector:@selector(enableSuddenTermination)])
        [process enableSuddenTermination];
#endif
}

201 202 203 204 205 206 207
- (void)save
{
    NSError *error = nil;
    BOOL success = [[self managedObjectContext] save:&error];
    NSAssert1(success, @"Can't save: %@", error);
}

Pierre's avatar
Pierre committed
208
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
Pierre's avatar
Pierre committed
209 210
{
    if ([keyPath isEqualToString:@"hasChanges"] && object == _managedObjectContext) {
Pierre's avatar
Pierre committed
211
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
212 213 214 215 216
        NSProcessInfo *process = [NSProcessInfo processInfo];
        if ([process respondsToSelector:@selector(disableSuddenTermination)])
            [process disableSuddenTermination];
#endif

Pierre's avatar
Pierre committed
217 218 219 220
        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
221 222 223 224 225
        return;
    }
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

Pierre's avatar
Pierre committed
226 227
#pragma mark -
#pragma mark No meta data fallbacks
Pierre's avatar
Pierre committed
228

Pierre's avatar
Pierre committed
229
- (void)computeThumbnailForFile:(MLFile *)file
Pierre's avatar
Pierre committed
230
{
Pierre's avatar
Pierre committed
231
    if (!file.computedThumbnail) {
232
        APLog(@"Computing thumbnail for %@", file.title);
Pierre's avatar
Pierre committed
233 234
        [[MLThumbnailerQueue sharedThumbnailerQueue] addFile:file];
    }
Pierre's avatar
Pierre committed
235
}
Pierre's avatar
Pierre committed
236
- (void)errorWhenFetchingMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
237
{
238
    APLog(@"Error when fetching for '%@'", file.title);
Pierre's avatar
Pierre committed
239

Pierre's avatar
Pierre committed
240 241
    [self computeThumbnailForFile:file];
}
Pierre's avatar
Pierre committed
242

Pierre's avatar
Pierre committed
243 244 245 246 247 248 249
- (void)errorWhenFetchingMetaDataForShow:(MLShow *)show
{
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self errorWhenFetchingMetaDataForFile:file];
    }
}
Pierre's avatar
Pierre committed
250

Pierre's avatar
Pierre committed
251 252
- (void)noMetaDataInRemoteDBForFile:(MLFile *)file
{
253
    file.noOnlineMetaData = @YES;
Pierre's avatar
Pierre committed
254
    [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
255 256
}

Pierre's avatar
Pierre committed
257
- (void)noMetaDataInRemoteDBForShow:(MLShow *)show
Pierre's avatar
Pierre committed
258
{
Pierre's avatar
Pierre committed
259 260 261
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self noMetaDataInRemoteDBForFile:file];
Pierre's avatar
Pierre committed
262 263 264
    }
}

Pierre's avatar
Pierre committed
265 266 267 268
#pragma mark -
#pragma mark Getter

- (void)addNewLabelWithName:(NSString *)name
Pierre's avatar
Pierre committed
269
{
Pierre's avatar
Pierre committed
270 271
    MLLabel *label = [self createObjectForEntity:@"Label"];
    label.name = name;
Pierre's avatar
Pierre committed
272 273
}

Pierre's avatar
Pierre committed
274 275 276 277 278 279 280
/**
 * TV MLShow Episodes
 */

#pragma mark -
#pragma mark Online meta data grabbing

Pierre's avatar
Pierre committed
281 282 283
#if !HAVE_BLOCK
- (void)tvShowEpisodesInfoGrabberDidFinishGrabbing:(MLTVShowEpisodesInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
284
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
285 286

    NSArray *results = grabber.episodesResults;
287
    [show setValue:(grabber.results)[@"serieArtworkURL"] forKey:@"artworkURL"];
Pierre's avatar
Pierre committed
288
    for (id result in results) {
289
        if ([result[@"serie"] boolValue]) {
Pierre's avatar
Pierre committed
290 291
            continue;
        }
292 293 294 295 296
        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
297 298 299 300 301
        if (!showEpisode.artworkURL) {
            for (MLFile *file in showEpisode.files)
                [self computeThumbnailForFile:file];
        }

Pierre's avatar
Pierre committed
302 303 304 305 306
        showEpisode.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
    show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
}

Pierre's avatar
Pierre committed
307 308 309 310 311 312
- (void)tvShowEpisodesInfoGrabber:(MLTVShowEpisodesInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
313 314
- (void)tvShowInfoGrabberDidFinishGrabbing:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
315
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
316 317
    NSArray *results = grabber.results;
    if ([results count] > 0) {
318 319
        NSDictionary *result = results[0];
        NSString *showId = result[@"id"];
Pierre's avatar
Pierre committed
320 321

        show.theTVDBID = showId;
322 323 324
        show.name = result[@"title"];
        show.shortSummary = result[@"shortSummary"];
        show.releaseYear = result[@"releaseYear"];
Pierre's avatar
Pierre committed
325 326 327 328 329 330 331 332 333

        // 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
334
        [self noMetaDataInRemoteDBForShow:show];
Pierre's avatar
Pierre committed
335 336 337 338
        show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
}

Pierre's avatar
Pierre committed
339 340 341 342 343 344
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
345 346
- (void)tvShowInfoGrabberDidFetchServerTime:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
347
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
348 349 350

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

Pierre's avatar
Pierre committed
351
    // First fetch the MLShow ID
Pierre's avatar
Pierre committed
352 353 354 355
    MLTVShowInfoGrabber *showInfoGrabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
    showInfoGrabber.delegate = self;
    showInfoGrabber.userData = show;

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

Pierre's avatar
Pierre committed
358 359 360 361
    [showInfoGrabber lookUpForTitle:show.name];
}
#endif

Pierre's avatar
Pierre committed
362
- (void)fetchMetaDataForShow:(MLShow *)show
Pierre's avatar
Pierre committed
363
{
Pierre's avatar
Pierre committed
364 365
    if (!_allowNetworkAccess)
        return;
366
    APLog(@"Fetching show server time");
Pierre's avatar
Pierre committed
367

Pierre's avatar
Pierre committed
368
    // First fetch the serverTime, so that we can update each entry.
Pierre's avatar
Pierre committed
369 370
#if HAVE_BLOCK
    [MLTVShowInfoGrabber fetchServerTimeAndExecuteBlock:^(NSNumber *serverDate) {
Pierre's avatar
Pierre committed
371 372 373

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

374
        APLog(@"Fetching show information on %@", show.name);
Pierre's avatar
Pierre committed
375 376 377

        // First fetch the MLShow ID
        MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
378 379 380 381 382 383 384 385 386 387 388
        [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"];

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

Pierre's avatar
Pierre committed
391
                // Fetch episode info
Pierre's avatar
Pierre committed
392
                MLTVShowEpisodesInfoGrabber *grabber = [[[MLTVShowEpisodesInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
393 394 395 396 397 398 399
                [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;
                        }
400
                        MLShowEpisode *showEpisode = [MLShowEpisode episodeWithShow:show episodeNumber:[result objectForKey:@"episodeNumber"] seasonNumber:[result objectForKey:@"seasonNumber"] createIfNeeded:YES];
Pierre's avatar
Pierre committed
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
                        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
417
#else
Pierre's avatar
Pierre committed
418 419 420 421
    MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
    grabber.delegate = self;
    grabber.userData = show;
    [grabber fetchServerTime];
Pierre's avatar
Pierre committed
422
#endif
Pierre's avatar
Pierre committed
423 424
}

Pierre's avatar
Pierre committed
425
- (void)addTVShowEpisodeWithInfo:(NSDictionary *)tvShowEpisodeInfo andFile:(MLFile *)file
Pierre's avatar
Pierre committed
426
{
Pierre's avatar
Pierre committed
427
    file.type = kMLFileTypeTVShowEpisode;
Pierre's avatar
Pierre committed
428

429 430 431
    NSNumber *seasonNumber = tvShowEpisodeInfo[@"season"];
    NSNumber *episodeNumber = tvShowEpisodeInfo[@"episode"];
    NSString *tvShowName = tvShowEpisodeInfo[@"tvShowName"];
432
    NSString *tvEpisodeName = tvShowEpisodeInfo[@"tvEpisodeName"];
Pierre's avatar
Pierre committed
433 434
    BOOL hasNoTvShow = NO;
    if (!tvShowName) {
435
        tvShowName = @"";
Pierre's avatar
Pierre committed
436 437 438
        hasNoTvShow = YES;
    }
    BOOL wasInserted = NO;
Pierre's avatar
Pierre committed
439 440 441 442
    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
443 444 445 446
    if (wasInserted && !hasNoTvShow) {
        show.name = tvShowName;
        [self fetchMetaDataForShow:show];
    }
447
    episode.name = tvEpisodeName;
Pierre's avatar
Pierre committed
448

Felix Paul Kühne's avatar
Felix Paul Kühne committed
449
    if (episode.name.length < 1)
Pierre's avatar
Pierre committed
450 451 452
        episode.name = file.title;
    file.seasonNumber = seasonNumber;
    file.episodeNumber = episodeNumber;
453
    episode.shouldBeDisplayed = @YES;
Pierre's avatar
Pierre committed
454 455

    [episode addFilesObject:file];
Pierre's avatar
Pierre committed
456
    file.showEpisode = episode;
Pierre's avatar
Pierre committed
457

Pierre's avatar
Pierre committed
458
    // The rest of the meta data will be fetched using the MLShow
459
    file.hasFetchedInfo = @YES;
Pierre's avatar
Pierre committed
460 461
}

462 463 464 465
- (void)addAudioContentWithInfo:(NSDictionary *)audioContentInfo andFile:(MLFile *)file
{
    file.type = kMLFileTypeAudio;

466 467 468
    file.title = audioContentInfo[VLCMetaInformationTitle];

    /* all further meta data is set by the FileParserQueue */
469 470 471 472

    file.hasFetchedInfo = @YES;
}

Pierre's avatar
Pierre committed
473
/**
Pierre's avatar
Pierre committed
474
 * MLFile auto detection
Pierre's avatar
Pierre committed
475 476
 */

Pierre's avatar
Pierre committed
477
#if !HAVE_BLOCK
Pierre's avatar
Pierre committed
478 479 480 481 482 483
- (void)movieInfoGrabber:(MLMovieInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLFile *file = grabber.userData;
    [self errorWhenFetchingMetaDataForFile:file];
}

Pierre's avatar
Pierre committed
484 485
- (void)movieInfoGrabberDidFinishGrabbing:(MLMovieInfoGrabber *)grabber
{
486
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
487 488

    NSArray *results = grabber.results;
Pierre's avatar
Pierre committed
489
    MLFile *file = grabber.userData;
Pierre's avatar
Pierre committed
490
    if ([results count] > 0) {
491 492 493 494 495
        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
496
    }
Pierre's avatar
Pierre committed
497 498 499 500
    else {
        [self noMetaDataInRemoteDBForFile:file];
    }

Pierre's avatar
Pierre committed
501 502 503 504
    file.hasFetchedInfo = yes;
}
#endif

Pierre's avatar
Pierre committed
505
- (void)fetchMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
506
{
507
    APLog(@"Fetching meta data for %@", file.title);
Pierre's avatar
Pierre committed
508

509
    [[MLFileParserQueue sharedFileParserQueue] addFile:file];
510

Pierre's avatar
Pierre committed
511 512 513 514 515
    if (!_allowNetworkAccess) {
        // Automatically compute the thumbnail
        [self computeThumbnailForFile:file];
    }

Pierre's avatar
Pierre committed
516
    NSDictionary *tvShowEpisodeInfo = [MLTitleDecrapifier tvShowEpisodeInfoFromString:file.title];
Pierre's avatar
Pierre committed
517 518 519 520 521
    if (tvShowEpisodeInfo) {
        [self addTVShowEpisodeWithInfo:tvShowEpisodeInfo andFile:file];
        return;
    }

522 523 524 525 526 527 528 529
    if ([file isSupportedAudioFile]) {
        NSDictionary *audioContentInfo = [MLTitleDecrapifier audioContentInfoFromFile:file];
        if (audioContentInfo && ![file videoTrack]) {
            [self addAudioContentWithInfo:audioContentInfo andFile:file];
            return;
        }
    }

Pierre's avatar
Pierre committed
530 531 532
    if (!_allowNetworkAccess)
        return;

Pierre's avatar
Pierre committed
533
    // Go online and fetch info.
Pierre's avatar
Pierre committed
534 535 536

    // 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
537
    MLMovieInfoGrabber *grabber = [[[MLMovieInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
538

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

Pierre's avatar
Pierre committed
541
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
542
    [grabber lookUpForTitle:file.title andExecuteBlock:^(NSError *err){
Pierre's avatar
Pierre committed
543 544
        if (err) {
            [self errorWhenFetchingMetaDataForFile:file];
Pierre's avatar
Pierre committed
545
            return;
Pierre's avatar
Pierre committed
546
        }
Pierre's avatar
Pierre committed
547 548 549 550 551

        NSArray *results = grabber.results;
        if ([results count] > 0) {
            NSDictionary *result = [results objectAtIndex:0];
            file.artworkURL = [result objectForKey:@"artworkURL"];
Pierre's avatar
Pierre committed
552 553
            if (!file.artworkURL)
                [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
554 555 556
            file.title = [result objectForKey:@"title"];
            file.shortSummary = [result objectForKey:@"shortSummary"];
            file.releaseYear = [result objectForKey:@"releaseYear"];
Pierre's avatar
Pierre committed
557 558 559
        } else
            [self noMetaDataInRemoteDBForFile:file];
        file.hasFetchedInfo = [NSNumber numberWithBool:YES];
Pierre's avatar
Pierre committed
560
    }];
Pierre's avatar
Pierre committed
561
#else
Pierre's avatar
Pierre committed
562 563 564
    grabber.userData = file;
    grabber.delegate = self;
    [grabber lookUpForTitle:file.title];
Pierre's avatar
Pierre committed
565
#endif
Pierre's avatar
Pierre committed
566 567
}

Pierre's avatar
Pierre committed
568 569 570
#pragma mark -
#pragma mark Adding file to the DB

Pierre's avatar
Pierre committed
571
- (void)addFilePath:(NSString *)filePath
Pierre's avatar
Pierre committed
572
{
573
    APLog(@"Adding Path %@", filePath);
Pierre's avatar
Pierre committed
574

Pierre's avatar
Pierre committed
575
    NSURL *url = [NSURL fileURLWithPath:filePath];
Pierre's avatar
Pierre committed
576 577
    NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
    NSString *title = [filePath lastPathComponent];
Pierre's avatar
Pierre committed
578
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
579 580
    NSDate *openedDate = nil; // FIXME kMDItemLastUsedDate
    NSDate *modifiedDate = nil; // FIXME [result valueForAttribute:@"kMDItemFSContentChangeDate"];
Pierre's avatar
Pierre committed
581
#endif
582
    NSNumber *size = attributes[NSFileSize]; // FIXME [result valueForAttribute:@"kMDItemFSSize"];
Pierre's avatar
Pierre committed
583

Pierre's avatar
Pierre committed
584
    MLFile *file = [self createObjectForEntity:@"File"];
Pierre's avatar
Pierre committed
585
    file.url = [url absoluteString];
Pierre's avatar
Pierre committed
586 587 588 589

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

590 591
    NSNumber *no = @NO;
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
592 593

    file.currentlyWatching = no;
594 595
    file.lastPosition = @0.0;
    file.remainingTime = @0.0;
Pierre's avatar
Pierre committed
596 597
    file.unread = yes;

Pierre's avatar
Pierre committed
598
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
599 600 601 602
    if ([openedDate isGreaterThan:modifiedDate]) {
        file.playCount = [NSNumber numberWithDouble:1];
        file.unread = no;
    }
Pierre's avatar
Pierre committed
603 604
#endif

Pierre's avatar
Pierre committed
605
    file.title = [MLTitleDecrapifier decrapify:[title stringByDeletingPathExtension]];
Pierre's avatar
Pierre committed
606 607

    if ([size longLongValue] < 150000000) /* 150 MB */
Pierre's avatar
Pierre committed
608
        file.type = kMLFileTypeClip;
Pierre's avatar
Pierre committed
609
    else
Pierre's avatar
Pierre committed
610
        file.type = kMLFileTypeMovie;
Pierre's avatar
Pierre committed
611 612 613 614

    [self fetchMetaDataForFile:file];
}

Pierre's avatar
Pierre committed
615
- (void)addFilePaths:(NSArray *)filepaths
Pierre's avatar
Pierre committed
616
{
Pierre's avatar
Pierre committed
617
    NSUInteger count = [filepaths count];
Pierre's avatar
Pierre committed
618 619 620 621
    NSMutableArray *fetchPredicates = [NSMutableArray arrayWithCapacity:count];
    NSMutableDictionary *urlToObject = [NSMutableDictionary dictionaryWithCapacity:count];

    // Prepare a fetch request for all items
Pierre's avatar
Pierre committed
622
    for (NSString *path in filepaths) {
Pierre's avatar
Pierre committed
623
        NSURL *url = [NSURL fileURLWithPath:path];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
624
        NSString *urlString = [url absoluteString];
Pierre's avatar
Pierre committed
625
        [fetchPredicates addObject:[NSPredicate predicateWithFormat:@"url == %@", urlString]];
626
        urlToObject[urlString] = path;
Pierre's avatar
Pierre committed
627 628 629 630 631 632
    }

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

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

633
    APLog(@"Fetching");
Pierre's avatar
Pierre committed
634
    NSArray *dbResults = [[self managedObjectContext] executeFetchRequest:request error:nil];
635
    APLog(@"Done");
Pierre's avatar
Pierre committed
636

Pierre's avatar
Pierre committed
637
    NSMutableArray *filePathsToAdd = [NSMutableArray arrayWithArray:filepaths];
Pierre's avatar
Pierre committed
638 639

    // Remove objects that are already in db.
Pierre's avatar
Pierre committed
640 641
    for (MLFile *dbResult in dbResults) {
        NSString *urlString = dbResult.url;
642
        [filePathsToAdd removeObject:urlToObject[urlString]];
Pierre's avatar
Pierre committed
643 644 645
    }

    // Add only the newly added items
Pierre's avatar
Pierre committed
646 647
    for (NSString* path in filePathsToAdd)
        [self addFilePath:path];
Pierre's avatar
Pierre committed
648 649
}

Pierre's avatar
Pierre committed
650 651 652 653

#pragma mark -
#pragma mark DB Updates

Pierre's avatar
Pierre committed
654 655 656 657 658 659
#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
660
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
661 662 663 664
        [self fetchMetaDataForShow:show];
}
#endif

665 666 667 668 669 670 671 672 673
- (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
674
    [self libraryDidDisappear];
675 676 677
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSFileManager *fileManager = [NSFileManager defaultManager];

678
    /* remove potential empty albums left over by previous releases */
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 705 706 707 708 709 710 711 712
    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;
713 714

    /* remove potential empty shows left over by previous releases */
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742
    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];
        }
    }

743 744 745 746 747
    /* 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
748 749
    NSString *currentFilePath;
    for (NSUInteger x = 0; x < allFilesCount; x++) {
750
        currentFile = allFiles[x];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
751 752
        currentFilePath = [currentFile.url stringByReplacingOccurrencesOfString:@"/localhost/" withString:@"//"];
        if ([seenFiles containsObject:currentFilePath])
753 754
            [[self managedObjectContext] deleteObject:currentFile];
        else
Felix Paul Kühne's avatar
Felix Paul Kühne committed
755
            [seenFiles addObject:currentFilePath];
756 757
    }

Felix Paul Kühne's avatar
Felix Paul Kühne committed
758 759 760 761
    [defaults setBool:YES forKey:kUpdatedToTheMojoWireDatabaseFormat];
    [defaults synchronize];

    [self libraryDidAppear];
762 763 764 765
    if ([self.delegate respondsToSelector:@selector(libraryUpgradeComplete)])
        [self.delegate libraryUpgradeComplete];
}

766
- (void)updateMediaDatabase
Pierre's avatar
Pierre committed
767
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
768
    [self libraryDidDisappear];
Pierre's avatar
Pierre committed
769
    // Remove no more present files
Pierre's avatar
Pierre committed
770 771
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];
    NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
772
    NSFileManager *fileManager = [NSFileManager defaultManager];
773

774 775 776
    unsigned int count = results.count;
    for (unsigned int x = 0; x < count; x++) {
        MLFile *file = results[x];
Pierre's avatar
Pierre committed
777 778
        NSString *urlString = [file url];
        NSURL *fileURL = [NSURL URLWithString:urlString];
779
        BOOL exists = [fileManager fileExistsAtPath:[fileURL path]];
780
        if (!exists) {
781
            APLog(@"Marking - %@", [fileURL absoluteString]);
782
            file.isSafe = YES; // It doesn't exists, it's safe.
783 784
            if (file.isAlbumTrack) {
                MLAlbum *album = file.albumTrack.album;
785 786 787 788 789 790 791 792
                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");
                    }
                }
793 794 795
            }
            if (file.isShowEpisode) {
                MLShow *show = file.showEpisode.show;
796 797 798 799 800 801 802 803
                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");
                    }
                }
804
            }
805
#if TARGET_OS_IPHONE
806
            NSString *thumbPath = [[[self thumbnailFolderPath] stringByAppendingPathComponent:[[file.objectID URIRepresentation] path]] stringByAppendingString:@".png"];
807 808
            bool thumbExists = [fileManager fileExistsAtPath:thumbPath];
            if (thumbExists)
809
                [fileManager removeItemAtPath:thumbPath error:nil];
810 811
            [[self managedObjectContext] deleteObject:file];
#endif
812
        }
813 814 815
#if !TARGET_OS_IPHONE
    file.isOnDisk = @(exists);
#endif
Pierre's avatar
Pierre committed
816
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
817
    [self libraryDidAppear];
Pierre's avatar
Pierre committed
818

819 820 821 822 823 824 825
    // 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
826 827 828
    if (!_allowNetworkAccess) {
        // Always attempt to fetch
        request = [self fetchRequestForEntity:@"File"];
Pierre's avatar
Pierre committed
829
        [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES"]];
Pierre's avatar
Pierre committed
830
        results = [[self managedObjectContext] executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
831 832 833 834
        for (MLFile *file in results) {
            if (!file.computedThumbnail)
                [self computeThumbnailForFile:file];
        }
Pierre's avatar
Pierre committed
835 836 837
        return;
    }

Pierre's avatar
Pierre committed
838 839
    // Get the thumbnails to compute
    request = [self fetchRequestForEntity:@"File"];
Pierre's avatar
Pierre committed
840
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && hasFetchedInfo == 1 && artworkURL == nil"]];
Pierre's avatar
Pierre committed
841 842
    results = [[self managedObjectContext] executeFetchRequest:request error:nil];
    for (MLFile *file in results)
843
        if (!file.computedThumbnail && ![file isAlbumTrack])
Pierre's avatar
Pierre committed
844
            [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
845 846 847 848 849 850

    // 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
851 852
        [self fetchMetaDataForFile:file];

Pierre's avatar
Pierre committed
853
    // Get to fetch show info
Pierre's avatar
Pierre committed
854 855 856
    request = [self fetchRequestForEntity:@"Show"];
    [request setPredicate:[NSPredicate predicateWithFormat:@"lastSyncDate == 0"]];
    results = [[self managedObjectContext] executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
857
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
858 859
        [self fetchMetaDataForShow:show];

Pierre's avatar
Pierre committed
860
    // Get updated TV Shows
861
    NSNumber *lastServerTime = @([[NSUserDefaults standardUserDefaults] integerForKey:kLastTVDBUpdateServerTime]);
Pierre's avatar
Pierre committed
862
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
863
    [MLTVShowInfoGrabber fetchUpdatesSinceServerTime:lastServerTime andExecuteBlock:^(NSArray *updates){
Pierre's avatar
Pierre committed
864 865 866
        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
867
        for (MLShow *show in results)
Pierre's avatar
Pierre committed
868 869
            [self fetchMetaDataForShow:show];
    }];
Pierre's avatar
Pierre committed
870 871 872 873
#else
    MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
    grabber.delegate = self;
    [grabber fetchUpdatesSinceServerTime:lastServerTime];
Pierre's avatar
Pierre committed
874
#endif
Pierre's avatar
Pierre committed
875
    /* Update every hour - FIXME: Preferences key */
Felix Paul Kühne's avatar
Felix Paul Kühne committed
876
    [self performSelector:@selector(updateMediaDatabase) withObject:nil afterDelay:60 * 60];
Pierre's avatar
Pierre committed
877
}
Pierre's avatar
Pierre committed
878

879 880 881 882 883
- (void)applicationWillExit
{
    [[MLCrashPreventer sharedPreventer] cancelAllFileParse];
}

Pierre's avatar
Pierre committed
884 885 886 887 888
- (void)applicationWillStart
{
    [[MLCrashPreventer sharedPreventer] markCrasherFiles];
}

Pierre's avatar
Pierre committed
889 890 891 892 893 894 895 896 897 898 899
- (void)libraryDidDisappear
{
    // Stop expansive work
    [[MLThumbnailerQueue sharedThumbnailerQueue] stop];
}

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