MLMediaLibrary.m 34.3 KB
Newer Older
1 2 3 4 5
/*****************************************************************************
 * MLMediaLibrary.m
 * MobileMediaLibraryKit
 *****************************************************************************
 * Copyright (C) 2010 Pierre d'Herbemont
6
 * Copyright (C) 2010-2015 VLC authors and VideoLAN
7 8 9 10
 * $Id$
 *
 * Authors: Pierre d'Herbemont <pdherbemont # videolan.org>
 *          Felix Paul Kühne <fkuehne # videolan.org>
11
 *          Tobias Conradi <videolan # tobias-conradi.de>
12
 *
13 14 15
 * 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
16 17 18 19
 * (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
20 21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
22
 *
23 24 25
 * 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.
26
 *****************************************************************************/
Pierre's avatar
Pierre committed
27 28

#import "MLMediaLibrary.h"
Pierre's avatar
Pierre committed
29
#import "MLTitleDecrapifier.h"
Pierre's avatar
Pierre committed
30 31 32 33
#import "MLFile.h"
#import "MLLabel.h"
#import "MLShowEpisode.h"
#import "MLShow.h"
34 35 36
#import "MLThumbnailerQueue.h"
#import "MLAlbumTrack.h"
#import "MLAlbum.h"
37
#import "MLFileParserQueue.h"
38
#import "MLCrashPreventer.h"
39
#import "MLMediaLibrary+Migration.h"
40
#import <sys/sysctl.h> // for sysctlbyname
Pierre's avatar
Pierre committed
41

42 43 44 45
#if TARGET_OS_IPHONE
#import <CoreSpotlight/CoreSpotlight.h>
#endif

Felix Paul Kühne's avatar
Felix Paul Kühne committed
46 47 48 49 50 51
#if HAVE_BLOCK
#import "MLMovieInfoGrabber.h"
#import "MLTVShowInfoGrabber.h"
#import "MLTVShowEpisodesInfoGrabber.h"
#endif

52 53
@interface MLMediaLibrary ()
{
54 55 56 57
    NSManagedObjectContext *_managedObjectContext;
    NSManagedObjectModel   *_managedObjectModel;

    BOOL _allowNetworkAccess;
58
    int _deviceSpeedCategory;
59

60 61
    NSString *_thumbnailFolderPath;
    NSString *_databaseFolderPath;
62
    NSString *_documentFolderPath;
63
    NSString *_libraryBasePath;
64 65 66
}
@end

Pierre's avatar
Pierre committed
67

Pierre's avatar
Pierre committed
68 69
// Pref key
static NSString *kLastTVDBUpdateServerTime = @"MLLastTVDBUpdateServerTime";
70
static NSString *kDecrapifyTitles = @"MLDecrapifyTitles";
Pierre's avatar
Pierre committed
71

Pierre's avatar
Pierre committed
72 73
#if HAVE_BLOCK
@interface MLMediaLibrary () <MLMovieInfoGrabberDelegate, MLTVShowEpisodesInfoGrabberDelegate, MLTVShowInfoGrabberDelegate>
Felix Paul Kühne's avatar
Felix Paul Kühne committed
74 75
#else
@interface MLMediaLibrary ()
Pierre's avatar
Pierre committed
76
#endif
Pierre's avatar
Pierre committed
77
- (NSManagedObjectContext *)managedObjectContext;
Pierre's avatar
Pierre committed
78
- (NSString *)databaseFolderPath;
Pierre's avatar
Pierre committed
79 80
@end

Pierre's avatar
Pierre committed
81
@implementation MLMediaLibrary
82 83
+ (void)initialize
{
84
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
85
    [defaults registerDefaults:@{kDecrapifyTitles : @YES}];
86 87
}

Pierre's avatar
Pierre committed
88 89 90
+ (id)sharedMediaLibrary
{
    static id sharedMediaLibrary = nil;
Pierre's avatar
Pierre committed
91
    if (!sharedMediaLibrary) {
Pierre's avatar
Pierre committed
92
        sharedMediaLibrary = [[[self class] alloc] init];
93 94 95 96

        // 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
97
    }
Pierre's avatar
Pierre committed
98 99 100
    return sharedMediaLibrary;
}

101 102 103 104 105 106 107 108 109 110 111
- (instancetype)init
{
    self = [super init];
    if (self) {
        _applicationGroupIdentifier = @"group.org.videolan.vlc-ios";
        [self _setupLibraryPathPriorToMigration];
        APLog(@"Initializing db in %@", [self databaseFolderPath]);
    }
    return self;
}

112 113 114 115 116 117
- (void)dealloc
{
    if (_managedObjectContext)
        [_managedObjectContext removeObserver:self forKeyPath:@"hasChanges"];
}

Pierre's avatar
Pierre committed
118 119 120 121
- (NSFetchRequest *)fetchRequestForEntity:(NSString *)entity
{
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    NSManagedObjectContext *moc = [self managedObjectContext];
122 123 124
    if (!moc || moc.persistentStoreCoordinator == nil)
        return nil;

Pierre's avatar
Pierre committed
125
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:entity inManagedObjectContext:moc];
Pierre's avatar
Pierre committed
126
    NSAssert(entityDescription, @"No entity");
Pierre's avatar
Pierre committed
127
    [request setEntity:entityDescription];
128
    return request;
Pierre's avatar
Pierre committed
129 130 131 132 133
}

- (id)createObjectForEntity:(NSString *)entity
{
    NSManagedObjectContext *moc = [self managedObjectContext];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
134
    if (!moc || moc.persistentStoreCoordinator == nil)
135 136
        return nil;

Pierre's avatar
Pierre committed
137 138 139
    return [NSEntityDescription insertNewObjectForEntityForName:entity inManagedObjectContext:moc];
}

140 141
- (void)removeObject:(NSManagedObject *)object
{
142 143 144 145
    NSManagedObjectContext *moc = [self managedObjectContext];

    if (moc)
        [[self managedObjectContext] deleteObject:object];
146 147
}

148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
#pragma mark - helper
- (int)deviceSpeedCategory
{
    if (_deviceSpeedCategory > 0)
        return _deviceSpeedCategory;

    size_t size;
    sysctlbyname("hw.machine", NULL, &size, NULL, 0);

    char *answer = malloc(size);
    sysctlbyname("hw.machine", answer, &size, NULL, 0);

    NSString *currentMachine = @(answer);
    free(answer);

    if ([currentMachine hasPrefix:@"iPhone2"] || [currentMachine hasPrefix:@"iPhone3"] || [currentMachine hasPrefix:@"iPhone4"] || [currentMachine hasPrefix:@"iPod3"] || [currentMachine hasPrefix:@"iPod4"] || [currentMachine hasPrefix:@"iPad2"]) {
        // iPhone 3GS, iPhone 4, 3rd and 4th generation iPod touch, iPad 2, iPad mini (1st gen)
        _deviceSpeedCategory = 1;
    } else if ([currentMachine hasPrefix:@"iPad3,1"] || [currentMachine hasPrefix:@"iPad3,2"] || [currentMachine hasPrefix:@"iPad3,3"] || [currentMachine hasPrefix:@"iPod5"]) {
        // iPod 5, iPad 3
        _deviceSpeedCategory = 2;
    } else if ([currentMachine hasPrefix:@"iPhone5"] || [currentMachine hasPrefix:@"iPhone6"] || [currentMachine hasPrefix:@"iPad4"]) {
        // iPhone 5 + 5S, iPad 4, iPad Air, iPad mini 2G
        _deviceSpeedCategory = 3;
    } else
        // iPhone 6, 2014 iPads
        _deviceSpeedCategory = 4;

    return _deviceSpeedCategory;
}

Pierre's avatar
Pierre committed
179 180 181 182 183 184
#pragma mark -
#pragma mark Media Library
- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel)
        return _managedObjectModel;
185

186
    NSString *path = [[NSBundle mainBundle] pathForResource:@"MediaLibrary" ofType:@"momd"];
187 188 189
    NSURL *momURL = [NSURL fileURLWithPath:path];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];

Pierre's avatar
Pierre committed
190 191 192
    return _managedObjectModel;
}

193
#pragma mark - Path handling
194 195 196 197 198 199 200 201
- (void)setLibraryBasePath:(NSString *)libraryBasePath
{
    _libraryBasePath = [libraryBasePath copy];
    _databaseFolderPath = nil;
    _thumbnailFolderPath = nil;
    _persistentStoreURL = nil;
}

202 203 204 205 206
- (NSString *)databaseFolderPath
{
    if (_databaseFolderPath.length == 0) {
        _databaseFolderPath = self.libraryBasePath;
    }
207
    return _databaseFolderPath;
Pierre's avatar
Pierre committed
208 209
}

210 211
- (NSString *)thumbnailFolderPath
{
212 213
    if (_thumbnailFolderPath.length == 0) {
        _thumbnailFolderPath = [self.libraryBasePath stringByAppendingPathComponent:@"Thumbnails"];
214 215
    }
    return _thumbnailFolderPath;
216 217
}

218 219 220 221 222 223 224 225
- (NSString *)documentFolderPath
{
    if (_documentFolderPath) {
        if (_documentFolderPath.length > 0)
            return _documentFolderPath;
    }
    int directory = NSDocumentDirectory;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
226
    _documentFolderPath = paths.firstObject;
227 228 229
    return _documentFolderPath;
}

230 231 232 233 234 235 236 237
- (NSURL *)persistentStoreURL
{
    if (_persistentStoreURL == nil) {
        NSString *databaseFolderPath = [self databaseFolderPath];
        NSString *path = [databaseFolderPath stringByAppendingPathComponent: @"MediaLibrary.sqlite"];
        _persistentStoreURL = [NSURL fileURLWithPath:path];
    }
    return _persistentStoreURL;
238 239
}

240 241 242 243 244 245 246 247 248 249
- (NSString *)pathRelativeToDocumentsFolderFromAbsolutPath:(NSString *)absolutPath
{
    return [absolutPath stringByReplacingOccurrencesOfString:self.documentFolderPath withString:@""];
}
- (NSString *)absolutPathFromPathRelativeToDocumentsFolder:(NSString *)relativePath
{
    return [self.documentFolderPath stringByAppendingPathComponent:relativePath];
}

#pragma mark -
250 251
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
252

Pierre's avatar
Pierre committed
253 254
    NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];

255
    NSNumber *yes = @YES;
256
    NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : yes,
257
                              NSInferMappingModelAutomaticallyOption : yes};
258 259 260 261 262
    if (self.additionalPersitentStoreOptions.count > 0) {
        NSMutableDictionary *mutableOptions = options.mutableCopy;
        [mutableOptions addEntriesFromDictionary:self.additionalPersitentStoreOptions];
        options = mutableOptions;
    }
Pierre's avatar
Pierre committed
263

264 265 266 267 268 269 270
    if ([[self.additionalPersitentStoreOptions objectForKey:NSReadOnlyPersistentStoreOption] boolValue] == YES) {
        if (![[NSFileManager defaultManager] fileExistsAtPath:self.persistentStoreURL.path]) {
            APLog(@"no library was found in read-only mode, hence no functionality will be available in this session");
            return nil;
        }
    }

Pierre's avatar
Pierre committed
271
    NSError *error;
272
    NSPersistentStore *persistentStore = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:self.persistentStoreURL options:options error:&error];
Pierre's avatar
Pierre committed
273 274

    if (!persistentStore) {
Pierre's avatar
Pierre committed
275
#if! TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
276 277 278 279
        // 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];
280
        [[NSFileManager defaultManager] removeItemAtPath:self.persistentStoreURL.path error:nil];
Pierre's avatar
Pierre committed
281
#else
282
        [[NSFileManager defaultManager] removeItemAtPath:self.persistentStoreURL.path error:nil];
Pierre's avatar
Pierre committed
283
#endif
284
        persistentStore = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:self.persistentStoreURL options:options error:&error];
Pierre's avatar
Pierre committed
285 286 287 288 289 290 291 292 293 294
        if (!persistentStore) {
#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];
#endif
            // Probably assert instead.
            return nil;
        }
Pierre's avatar
Pierre committed
295
    }
296 297 298 299 300 301 302
    return coordinator;
}

- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext)
        return _managedObjectContext;
Pierre's avatar
Pierre committed
303 304

    _managedObjectContext = [[NSManagedObjectContext alloc] init];
305
    [_managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
306 307
    if (_managedObjectContext.persistentStoreCoordinator == nil)
        return nil;
Pierre's avatar
Pierre committed
308 309 310 311 312 313 314 315
    [_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
316
    NSError *error = nil;
317 318 319 320
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;

Pierre's avatar
Pierre committed
321 322
    BOOL success = [[self managedObjectContext] save:&error];
    NSAssert1(success, @"Can't save: %@", error);
Pierre's avatar
Pierre committed
323
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
324 325 326 327 328 329
    NSProcessInfo *process = [NSProcessInfo processInfo];
    if ([process respondsToSelector:@selector(enableSuddenTermination)])
        [process enableSuddenTermination];
#endif
}

330 331 332
- (void)save
{
    NSError *error = nil;
333 334 335 336 337
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;

    BOOL success = [moc save:&error];
338 339 340
    NSAssert1(success, @"Can't save: %@", error);
}

Pierre's avatar
Pierre committed
341
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
Pierre's avatar
Pierre committed
342 343
{
    if ([keyPath isEqualToString:@"hasChanges"] && object == _managedObjectContext) {
Pierre's avatar
Pierre committed
344
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
345 346 347 348 349
        NSProcessInfo *process = [NSProcessInfo processInfo];
        if ([process respondsToSelector:@selector(disableSuddenTermination)])
            [process disableSuddenTermination];
#endif

Pierre's avatar
Pierre committed
350 351 352 353
        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
354 355 356 357 358
        return;
    }
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

359 360 361 362 363 364 365 366 367
- (NSManagedObject *)objectForURIRepresentation:(NSURL *)uriRepresenation {
    if (uriRepresenation == nil) {
        return nil;
    }
    NSManagedObjectID *objectID = [self.persistentStoreCoordinator managedObjectIDForURIRepresentation:uriRepresenation];
    NSManagedObject *managedObject = [self.managedObjectContext objectWithID:objectID];
    return managedObject;
}

Pierre's avatar
Pierre committed
368 369
#pragma mark -
#pragma mark No meta data fallbacks
Pierre's avatar
Pierre committed
370

Pierre's avatar
Pierre committed
371
- (void)computeThumbnailForFile:(MLFile *)file
Pierre's avatar
Pierre committed
372
{
373
    if (!file.computedThumbnail && ![file isKindOfType:kMLFileTypeAudio]) {
374
        APLog(@"Computing thumbnail for %@", file.title);
Pierre's avatar
Pierre committed
375 376
        [[MLThumbnailerQueue sharedThumbnailerQueue] addFile:file];
    }
Pierre's avatar
Pierre committed
377
}
378

Pierre's avatar
Pierre committed
379
- (void)errorWhenFetchingMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
380
{
381
    APLog(@"Error when fetching for '%@'", file.title);
Pierre's avatar
Pierre committed
382

Pierre's avatar
Pierre committed
383 384
    [self computeThumbnailForFile:file];
}
Pierre's avatar
Pierre committed
385

Pierre's avatar
Pierre committed
386 387 388 389 390 391 392
- (void)errorWhenFetchingMetaDataForShow:(MLShow *)show
{
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self errorWhenFetchingMetaDataForFile:file];
    }
}
Pierre's avatar
Pierre committed
393

Pierre's avatar
Pierre committed
394 395
- (void)noMetaDataInRemoteDBForFile:(MLFile *)file
{
396
    file.noOnlineMetaData = @YES;
Pierre's avatar
Pierre committed
397
    [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
398 399
}

Pierre's avatar
Pierre committed
400
- (void)noMetaDataInRemoteDBForShow:(MLShow *)show
Pierre's avatar
Pierre committed
401
{
Pierre's avatar
Pierre committed
402 403 404
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self noMetaDataInRemoteDBForFile:file];
Pierre's avatar
Pierre committed
405 406 407
    }
}

Pierre's avatar
Pierre committed
408 409 410 411
#pragma mark -
#pragma mark Getter

- (void)addNewLabelWithName:(NSString *)name
Pierre's avatar
Pierre committed
412
{
Pierre's avatar
Pierre committed
413 414
    MLLabel *label = [self createObjectForEntity:@"Label"];
    label.name = name;
Pierre's avatar
Pierre committed
415 416
}

Pierre's avatar
Pierre committed
417 418 419 420 421 422 423
/**
 * TV MLShow Episodes
 */

#pragma mark -
#pragma mark Online meta data grabbing

Felix Paul Kühne's avatar
Felix Paul Kühne committed
424
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
425 426
- (void)tvShowEpisodesInfoGrabberDidFinishGrabbing:(MLTVShowEpisodesInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
427
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
428 429

    NSArray *results = grabber.episodesResults;
430
    [show setValue:(grabber.results)[@"serieArtworkURL"] forKey:@"artworkURL"];
Pierre's avatar
Pierre committed
431
    for (id result in results) {
432
        if ([result[@"serie"] boolValue]) {
Pierre's avatar
Pierre committed
433 434
            continue;
        }
435 436 437 438 439
        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
440 441 442 443 444
        if (!showEpisode.artworkURL) {
            for (MLFile *file in showEpisode.files)
                [self computeThumbnailForFile:file];
        }

Pierre's avatar
Pierre committed
445 446 447 448 449
        showEpisode.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
    show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
}

Pierre's avatar
Pierre committed
450 451 452 453 454 455
- (void)tvShowEpisodesInfoGrabber:(MLTVShowEpisodesInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
456 457
- (void)tvShowInfoGrabberDidFinishGrabbing:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
458
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
459 460
    NSArray *results = grabber.results;
    if ([results count] > 0) {
461 462
        NSDictionary *result = results[0];
        NSString *showId = result[@"id"];
Pierre's avatar
Pierre committed
463 464

        show.theTVDBID = showId;
465 466 467
        show.name = result[@"title"];
        show.shortSummary = result[@"shortSummary"];
        show.releaseYear = result[@"releaseYear"];
Pierre's avatar
Pierre committed
468 469

        // Fetch episodes info
470
        MLTVShowEpisodesInfoGrabber *grabber = [[MLTVShowEpisodesInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
471 472 473 474 475 476
        grabber.delegate = self;
        grabber.userData = show;
        [grabber lookUpForShowID:showId];
    }
    else {
        // Not found.
Pierre's avatar
Pierre committed
477
        [self noMetaDataInRemoteDBForShow:show];
Pierre's avatar
Pierre committed
478 479 480 481
        show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
}

Pierre's avatar
Pierre committed
482 483 484 485 486 487
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
488 489
- (void)tvShowInfoGrabberDidFetchServerTime:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
490
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
491 492 493

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

Pierre's avatar
Pierre committed
494
    // First fetch the MLShow ID
495
    MLTVShowInfoGrabber *showInfoGrabber = [[MLTVShowInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
496 497 498
    showInfoGrabber.delegate = self;
    showInfoGrabber.userData = show;

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

Pierre's avatar
Pierre committed
501 502 503 504
    [showInfoGrabber lookUpForTitle:show.name];
}
#endif

Pierre's avatar
Pierre committed
505
- (void)fetchMetaDataForShow:(MLShow *)show
Pierre's avatar
Pierre committed
506
{
507 508
    if (!_allowNetworkAccess)
        return;
509
    APLog(@"Fetching show server time");
Pierre's avatar
Pierre committed
510

Pierre's avatar
Pierre committed
511
    // First fetch the serverTime, so that we can update each entry.
Pierre's avatar
Pierre committed
512 513
#if HAVE_BLOCK
    [MLTVShowInfoGrabber fetchServerTimeAndExecuteBlock:^(NSNumber *serverDate) {
Pierre's avatar
Pierre committed
514 515 516

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

517
        APLog(@"Fetching show information on %@", show.name);
Pierre's avatar
Pierre committed
518 519 520

        // First fetch the MLShow ID
        MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
521 522 523 524 525 526 527 528 529 530 531
        [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"];

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

Pierre's avatar
Pierre committed
534
                // Fetch episode info
Pierre's avatar
Pierre committed
535
                MLTVShowEpisodesInfoGrabber *grabber = [[[MLTVShowEpisodesInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
536 537 538 539 540 541 542
                [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;
                        }
543
                        MLShowEpisode *showEpisode = [MLShowEpisode episodeWithShow:show episodeNumber:[result objectForKey:@"episodeNumber"] seasonNumber:[result objectForKey:@"seasonNumber"] createIfNeeded:YES];
Pierre's avatar
Pierre committed
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559
                        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
560
#endif
Pierre's avatar
Pierre committed
561 562
}

Pierre's avatar
Pierre committed
563
- (void)addTVShowEpisodeWithInfo:(NSDictionary *)tvShowEpisodeInfo andFile:(MLFile *)file
Pierre's avatar
Pierre committed
564
{
Pierre's avatar
Pierre committed
565
    file.type = kMLFileTypeTVShowEpisode;
Pierre's avatar
Pierre committed
566

567 568 569
    NSNumber *seasonNumber = tvShowEpisodeInfo[@"season"];
    NSNumber *episodeNumber = tvShowEpisodeInfo[@"episode"];
    NSString *tvShowName = tvShowEpisodeInfo[@"tvShowName"];
570
    NSString *tvEpisodeName = tvShowEpisodeInfo[@"tvEpisodeName"];
Pierre's avatar
Pierre committed
571 572
    BOOL hasNoTvShow = NO;
    if (!tvShowName) {
573
        tvShowName = @"";
Pierre's avatar
Pierre committed
574 575 576
        hasNoTvShow = YES;
    }
    BOOL wasInserted = NO;
Pierre's avatar
Pierre committed
577 578
    MLShow *show = nil;
    MLShowEpisode *episode = [MLShowEpisode episodeWithShowName:tvShowName episodeNumber:episodeNumber seasonNumber:seasonNumber createIfNeeded:YES wasCreated:&wasInserted];
579 580

    if (episode) {
Pierre's avatar
Pierre committed
581
        show = episode.show;
582 583
        [show addEpisode:episode];
    }
Pierre's avatar
Pierre committed
584 585 586 587
    if (wasInserted && !hasNoTvShow) {
        show.name = tvShowName;
        [self fetchMetaDataForShow:show];
    }
588
    episode.name = tvEpisodeName;
Pierre's avatar
Pierre committed
589

590
    if (episode.name.length < 1)
Pierre's avatar
Pierre committed
591 592 593
        episode.name = file.title;
    file.seasonNumber = seasonNumber;
    file.episodeNumber = episodeNumber;
594
    episode.shouldBeDisplayed = @YES;
Pierre's avatar
Pierre committed
595 596

    [episode addFilesObject:file];
Pierre's avatar
Pierre committed
597
    file.showEpisode = episode;
Pierre's avatar
Pierre committed
598

Pierre's avatar
Pierre committed
599
    // The rest of the meta data will be fetched using the MLShow
600
    file.hasFetchedInfo = @YES;
Pierre's avatar
Pierre committed
601 602 603
}

/**
Pierre's avatar
Pierre committed
604
 * MLFile auto detection
Pierre's avatar
Pierre committed
605 606
 */

Felix Paul Kühne's avatar
Felix Paul Kühne committed
607
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
608 609 610 611 612 613
- (void)movieInfoGrabber:(MLMovieInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLFile *file = grabber.userData;
    [self errorWhenFetchingMetaDataForFile:file];
}

Pierre's avatar
Pierre committed
614 615
- (void)movieInfoGrabberDidFinishGrabbing:(MLMovieInfoGrabber *)grabber
{
616
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
617 618

    NSArray *results = grabber.results;
Pierre's avatar
Pierre committed
619
    MLFile *file = grabber.userData;
Pierre's avatar
Pierre committed
620
    if ([results count] > 0) {
621 622 623 624 625
        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
626
    }
Pierre's avatar
Pierre committed
627 628 629 630
    else {
        [self noMetaDataInRemoteDBForFile:file];
    }

Pierre's avatar
Pierre committed
631 632 633 634
    file.hasFetchedInfo = yes;
}
#endif

Pierre's avatar
Pierre committed
635
- (void)fetchMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
636
{
637
    APLog(@"Fetching meta data for %@", file.title);
Pierre's avatar
Pierre committed
638

Pierre's avatar
Pierre committed
639
    NSDictionary *tvShowEpisodeInfo = [MLTitleDecrapifier tvShowEpisodeInfoFromString:file.title];
Pierre's avatar
Pierre committed
640
    if (tvShowEpisodeInfo) {
641
        file.type = kMLFileTypeTVShowEpisode;
Pierre's avatar
Pierre committed
642 643 644 645
        [self addTVShowEpisodeWithInfo:tvShowEpisodeInfo andFile:file];
        return;
    }

646 647
    if (!_allowNetworkAccess)
        return;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
648
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
649
    // Go online and fetch info.
Pierre's avatar
Pierre committed
650 651 652

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

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

Felix Paul Kühne's avatar
Felix Paul Kühne committed
657

Pierre's avatar
Pierre committed
658
    [grabber lookUpForTitle:file.title andExecuteBlock:^(NSError *err){
Pierre's avatar
Pierre committed
659 660
        if (err) {
            [self errorWhenFetchingMetaDataForFile:file];
Pierre's avatar
Pierre committed
661
            return;
Pierre's avatar
Pierre committed
662
        }
Pierre's avatar
Pierre committed
663 664 665 666 667

        NSArray *results = grabber.results;
        if ([results count] > 0) {
            NSDictionary *result = [results objectAtIndex:0];
            file.artworkURL = [result objectForKey:@"artworkURL"];
Pierre's avatar
Pierre committed
668 669
            if (!file.artworkURL)
                [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
670 671 672
            file.title = [result objectForKey:@"title"];
            file.shortSummary = [result objectForKey:@"shortSummary"];
            file.releaseYear = [result objectForKey:@"releaseYear"];
Pierre's avatar
Pierre committed
673 674 675
        } else
            [self noMetaDataInRemoteDBForFile:file];
        file.hasFetchedInfo = [NSNumber numberWithBool:YES];
Pierre's avatar
Pierre committed
676
    }];
Pierre's avatar
Pierre committed
677
#endif
Pierre's avatar
Pierre committed
678 679
}

Pierre's avatar
Pierre committed
680 681 682
#pragma mark -
#pragma mark Adding file to the DB

683 684 685 686 687 688 689 690
#ifdef MLKIT_READONLY_TARGET

- (void)addFilePaths:(NSArray *)filepaths
{
}

#else

Pierre's avatar
Pierre committed
691
- (void)addFilePath:(NSString *)filePath
Pierre's avatar
Pierre committed
692
{
693
    APLog(@"Adding Path %@", filePath);
Pierre's avatar
Pierre committed
694

Pierre's avatar
Pierre committed
695
    NSURL *url = [NSURL fileURLWithPath:filePath];
Pierre's avatar
Pierre committed
696
    NSString *title = [filePath lastPathComponent];
Pierre's avatar
Pierre committed
697
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
698 699
    NSDate *openedDate = nil; // FIXME kMDItemLastUsedDate
    NSDate *modifiedDate = nil; // FIXME [result valueForAttribute:@"kMDItemFSContentChangeDate"];
Pierre's avatar
Pierre committed
700
#endif
Pierre's avatar
Pierre committed
701

Pierre's avatar
Pierre committed
702
    MLFile *file = [self createObjectForEntity:@"File"];
703
    file.url = url;
Pierre's avatar
Pierre committed
704 705 706 707

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

708 709
    NSNumber *no = @NO;
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
710 711

    file.currentlyWatching = no;
712 713
    file.lastPosition = @0.0;
    file.remainingTime = @0.0;
Pierre's avatar
Pierre committed
714 715
    file.unread = yes;

Pierre's avatar
Pierre committed
716
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
717 718 719 720
    if ([openedDate isGreaterThan:modifiedDate]) {
        file.playCount = [NSNumber numberWithDouble:1];
        file.unread = no;
    }
Pierre's avatar
Pierre committed
721 722
#endif

723 724 725 726
    if ([[[NSUserDefaults standardUserDefaults] objectForKey:kDecrapifyTitles] boolValue] == YES)
        file.title = [MLTitleDecrapifier decrapify:[title stringByDeletingPathExtension]];
    else
        file.title = [title stringByDeletingPathExtension];
Pierre's avatar
Pierre committed
727

728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746
#if TARGET_OS_IPHONE
    if (SYSTEM_RUNS_IOS9) {
        /* add a preliminary CS item, which will be replaced once we have more information */
        CSSearchableItemAttributeSet* attributeSet = [[CSSearchableItemAttributeSet alloc] initWithItemContentType:@"public.audiovisual-content"];
        attributeSet.title = file.title;
        attributeSet.displayName = file.title;
        attributeSet.metadataModificationDate = [NSDate date];

        CSSearchableItem *item;
        item = [[CSSearchableItem alloc] initWithUniqueIdentifier:file.objectID.URIRepresentation.absoluteString
                                                 domainIdentifier:_applicationGroupIdentifier
                                                     attributeSet:attributeSet];
        // Index the item.
        [[CSSearchableIndex defaultSearchableIndex] indexSearchableItems:@[item] completionHandler:^(NSError * __nullable error) {
            NSLog(@"Search item indexed");
        }];
    }
#endif

747
    [[MLFileParserQueue sharedFileParserQueue] addFile:file];
Pierre's avatar
Pierre committed
748 749
}

Pierre's avatar
Pierre committed
750
- (void)addFilePaths:(NSArray *)filepaths
Pierre's avatar
Pierre committed
751
{
Pierre's avatar
Pierre committed
752
    NSUInteger count = [filepaths count];
Pierre's avatar
Pierre committed
753 754 755 756
    NSMutableArray *fetchPredicates = [NSMutableArray arrayWithCapacity:count];
    NSMutableDictionary *urlToObject = [NSMutableDictionary dictionaryWithCapacity:count];

    // Prepare a fetch request for all items
Pierre's avatar
Pierre committed
757
    for (NSString *path in filepaths) {
758
        NSString *relativePath = path;
759
#if TARGET_OS_IPHONE
760 761
        // on iPhone we only save relative paths ins the DB
        relativePath = [self pathRelativeToDocumentsFolderFromAbsolutPath:path];
762
#endif
763 764
        [urlToObject setObject:path forKey:relativePath];
        [fetchPredicates addObject:[NSPredicate predicateWithFormat:@"path == %@", relativePath]];
Pierre's avatar
Pierre committed
765 766
    }
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];
767 768
    if (!request)
        return;
Pierre's avatar
Pierre committed
769 770
    [request setPredicate:[NSCompoundPredicate orPredicateWithSubpredicates:fetchPredicates]];

771
    APLog(@"Fetching");
772 773 774 775
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;
    NSArray *dbResults = [moc executeFetchRequest:request error:nil];
776
    APLog(@"Done");
Pierre's avatar
Pierre committed
777

Pierre's avatar
Pierre committed
778
    NSMutableArray *filePathsToAdd = [NSMutableArray arrayWithArray:filepaths];
Pierre's avatar
Pierre committed
779 780

    // Remove objects that are already in db.
Pierre's avatar
Pierre committed
781
    for (MLFile *dbResult in dbResults) {
782 783
        NSString *path = dbResult.path;
        [filePathsToAdd removeObject:[urlToObject objectForKey:path]];
Pierre's avatar
Pierre committed
784 785 786
    }

    // Add only the newly added items
Pierre's avatar
Pierre committed
787 788
    for (NSString* path in filePathsToAdd)
        [self addFilePath:path];
Pierre's avatar
Pierre committed
789
}
790
#endif
Pierre's avatar
Pierre committed
791 792 793 794

#pragma mark -
#pragma mark DB Updates

Felix Paul Kühne's avatar
Felix Paul Kühne committed
795
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
796 797 798
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFetchUpdates:(NSArray *)updates
{
    NSFetchRequest *request = [self fetchRequestForEntity:@"Show"];
799 800 801
    if (!request)
        return;

Pierre's avatar
Pierre committed
802 803
    [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
804
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
805 806 807 808
        [self fetchMetaDataForShow:show];
}
#endif

809 810 811 812 813 814 815 816
#ifdef MLKIT_READONLY_TARGET

- (void)updateMediaDatabase
{
}

#else

817
- (void)updateMediaDatabase
Pierre's avatar
Pierre committed
818
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
819
    [self libraryDidDisappear];
Pierre's avatar
Pierre committed
820
    // Remove no more present files
Pierre's avatar
Pierre committed
821
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];
822 823
    if (!request)
        return;
824 825 826 827
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;
    NSArray *results = [moc executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
828
    NSFileManager *fileManager = [NSFileManager defaultManager];
829

830
    unsigned int count = (unsigned int)results.count;
831 832
    for (unsigned int x = 0; x < count; x++) {
        MLFile *file = results[x];
833
       NSURL *fileURL = file.url;
834
        BOOL exists = [fileManager fileExistsAtPath:[fileURL path]];
835
        if (!exists) {
836
            APLog(@"Marking - %@", [fileURL absoluteString]);
837
            file.isSafe = YES; // It doesn't exist, it's safe.
838 839
            if (file.isAlbumTrack) {
                MLAlbum *album = file.albumTrack.album;
840 841 842 843 844 845 846 847 848 849 850
                if (album != nil) {
                    if (album.tracks.count <= 1) {
                        @try {
                            [moc deleteObject:album];
                        }
                        @catch (NSException *exception) {
                            APLog(@"failed to nuke object because it disappeared in front of us");
                        }
                    } else
                        [album removeTrack:file.albumTrack];
                }
851 852 853
            }
            if (file.isShowEpisode) {
                MLShow *show = file.showEpisode.show;
854 855 856 857 858 859 860 861 862 863 864
                if (show != nil) {
                    if (show.episodes.count <= 1) {
                        @try {
                            [moc deleteObject:show];
                        }
                        @catch (NSException *exception) {
                            APLog(@"failed to nuke object because it disappeared in front of us");
                        }
                    } else
                        [show removeEpisode:file.showEpisode];
                }
865
            }
866
#if TARGET_OS_IPHONE
867
            NSString *thumbPath = [file thumbnailPath];
868 869
            bool thumbExists = [fileManager fileExistsAtPath:thumbPath];
            if (thumbExists)
870
                [fileManager removeItemAtPath:thumbPath error:nil];
871 872 873 874 875 876 877 878 879

            if (SYSTEM_RUNS_IOS9) {
            /* remove file from CoreSpotlight */
                [[CSSearchableIndex defaultSearchableIndex] deleteSearchableItemsWithIdentifiers:@[file.objectID.URIRepresentation.absoluteString]
                                                                               completionHandler:^(NSError * __nullable error) {
                                                                                   NSLog(@"Removed %@ from index", file.objectID.URIRepresentation);
                                                                               }];
            }

880
            [moc deleteObject:file];
881
#endif
882
        }
883 884 885
#if !TARGET_OS_IPHONE
    file.isOnDisk = @(exists);
#endif
Pierre's avatar
Pierre committed
886
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
887
    [self libraryDidAppear];
Pierre's avatar
Pierre committed
888

889 890
    // Get the file to parse
    request = [self fetchRequestForEntity:@"File"];
891 892
    if (!request)
        return;
893
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && tracks.@count == 0"]];
894
    results = [moc executeFetchRequest:request error:nil];
895 896 897
    for (MLFile *file in results)
        [[MLFileParserQueue sharedFileParserQueue] addFile:file];

898 899 900
    if (!_allowNetworkAccess) {
        // Always attempt to fetch
        request = [self fetchRequestForEntity:@"File"];
901 902
        if (!request)
            return;
903
        [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES"]];
904
        results = [moc executeFetchRequest:request error:nil];
905
        for (MLFile *file in results) {
906
            if (!file.computedThumbnail && ![file isKindOfType:kMLFileTypeAudio])
907 908
                [self computeThumbnailForFile:file];
        }
909 910 911
        return;
    }

Pierre's avatar
Pierre committed
912 913
    // Get the thumbnails to compute
    request = [self fetchRequestForEntity:@"File"];
914 915
    if (!request)
        return;
916
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && hasFetchedInfo == 1 && artworkURL == nil"]];
917
    results = [moc executeFetchRequest:request error:nil];
918 919 920 921 922 923
    for (MLFile *file in results) {
        if (!file.computedThumbnail) {
            if (!file.albumTrack && ![file isKindOfType:kMLFileTypeAudio])
                [self computeThumbnailForFile:file];
        }
    }
Pierre's avatar
Pierre committed
924 925 926

    // Get to fetch meta data
    request = [self fetchRequestForEntity:@"File"];
927 928
    if (!request)
        return;
Pierre's avatar
Pierre committed
929
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && hasFetchedInfo == 0"]];
930
    results = [moc executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
931
    for (MLFile *file in results)
932
        [[MLFileParserQueue sharedFileParserQueue] addFile:file];
Pierre's avatar
Pierre committed
933

Pierre's avatar
Pierre committed
934
    // Get to fetch show info
Pierre's avatar
Pierre committed
935
    request = [self fetchRequestForEntity:@"Show"];
936 937
    if (!request)
        return;
Pierre's avatar
Pierre committed
938
    [request setPredicate:[NSPredicate predicateWithFormat:@"lastSyncDate == 0"]];
939
    results = [moc executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
940
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
941 942
        [self fetchMetaDataForShow:show];

Felix Paul Kühne's avatar
Felix Paul Kühne committed
943
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
944
    // Get updated TV Shows
945
    NSNumber *lastServerTime = @([[NSUserDefaults standardUserDefaults] integerForKey:kLastTVDBUpdateServerTime]);
Felix Paul Kühne's avatar
Felix Paul Kühne committed
946

Pierre's avatar
Pierre committed
947
    [MLTVShowInfoGrabber fetchUpdatesSinceServerTime:lastServerTime andExecuteBlock:^(NSArray *updates){
Pierre's avatar
Pierre committed
948
        NSFetchRequest *request = [self fetchRequestForEntity:@"Show"];
949 950
        if (!request)
            return;
Pierre's avatar
Pierre committed
951
        [request setPredicate:[NSComparisonPredicate predicateWithLeftExpression:[NSExpression expressionForKeyPath:@"theTVDBID"] rightExpression:[NSExpression expressionForConstantValue:updates] modifier:NSDirectPredicateModifier type:NSInPredicateOperatorType options:0]];
952
        NSArray *results = [moc executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
953
        for (MLShow *show in results)
Pierre's avatar
Pierre committed
954 955
            [self fetchMetaDataForShow:show];
    }];
Pierre's avatar
Pierre committed
956
#endif
Pierre's avatar
Pierre committed
957
    /* Update every hour - FIXME: Preferences key */
958
    [self performSelector:@selector(updateMediaDatabase) withObject:nil afterDelay:60 * 60];
Pierre's avatar
Pierre committed
959
}
960
#endif
Pierre's avatar
Pierre committed
961

962 963
- (void)applicationWillExit
{
964
    [[MLFileParserQueue sharedFileParserQueue] stop];
965 966 967
    [[MLCrashPreventer sharedPreventer] cancelAllFileParse];
}

Pierre's avatar
Pierre committed
968 969 970
- (void)applicationWillStart
{
    [[MLCrashPreventer sharedPreventer] markCrasherFiles];
971
    [[MLFileParserQueue sharedFileParserQueue] resume];
Pierre's avatar
Pierre committed
972 973
}

Pierre's avatar
Pierre committed
974 975 976 977
- (void)libraryDidDisappear
{
    // Stop expansive work
    [[MLThumbnailerQueue sharedThumbnailerQueue] stop];
978
    [[MLFileParserQueue sharedFileParserQueue] stop];
Pierre's avatar
Pierre committed
979 980 981 982 983 984
}

- (void)libraryDidAppear
{
    // Resume our work
    [[MLThumbnailerQueue sharedThumbnailerQueue] resume];
985
    [[MLFileParserQueue sharedFileParserQueue] resume];
Pierre's avatar
Pierre committed
986
}
987

988
#pragma mark - migrations
989

990 991 992 993 994 995 996
- (BOOL)libraryMigrationNeeded
{
    return [self _libraryMigrationNeeded];
}
- (void)migrateLibrary
{
    [self _migrateLibrary];
997 998
}

Pierre's avatar
Pierre committed
999
@end