MLMediaLibrary.m 37.1 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
#if TARGET_OS_IOS
43 44 45
#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
- (instancetype)init
{
    self = [super init];
    if (self) {
105 106 107 108 109 110 111 112 113 114

        NSString *key = @"MLKitGroupIdentifier";
        _applicationGroupIdentifier = [[[NSBundle bundleForClass:self.class] infoDictionary] valueForKey:key];
        if (!_applicationGroupIdentifier) {
            _applicationGroupIdentifier = [[[NSBundle mainBundle] infoDictionary] valueForKey:key];
        }
        if (!_applicationGroupIdentifier) {
            _applicationGroupIdentifier = @"group.org.videolan.vlc-ios";
        }

115 116 117 118 119 120
        [self _setupLibraryPathPriorToMigration];
        APLog(@"Initializing db in %@", [self databaseFolderPath]);
    }
    return self;
}

121 122 123 124 125 126
- (void)dealloc
{
    if (_managedObjectContext)
        [_managedObjectContext removeObserver:self forKeyPath:@"hasChanges"];
}

Pierre's avatar
Pierre committed
127 128 129 130
- (NSFetchRequest *)fetchRequestForEntity:(NSString *)entity
{
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    NSManagedObjectContext *moc = [self managedObjectContext];
131 132 133
    if (!moc || moc.persistentStoreCoordinator == nil)
        return nil;

Pierre's avatar
Pierre committed
134
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:entity inManagedObjectContext:moc];
135 136
    if (!entityDescription)
        return nil;
Pierre's avatar
Pierre committed
137
    [request setEntity:entityDescription];
138
    return request;
Pierre's avatar
Pierre committed
139 140 141 142 143
}

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

Pierre's avatar
Pierre committed
147 148 149
    return [NSEntityDescription insertNewObjectForEntityForName:entity inManagedObjectContext:moc];
}

150 151
- (void)removeObject:(NSManagedObject *)object
{
152 153 154 155
    NSManagedObjectContext *moc = [self managedObjectContext];

    if (moc)
        [[self managedObjectContext] deleteObject:object];
156 157
}

158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
#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
189 190 191 192 193 194
#pragma mark -
#pragma mark Media Library
- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel)
        return _managedObjectModel;
195

196
    NSString *path = [[NSBundle bundleForClass:self.class] pathForResource:@"MediaLibrary" ofType:@"momd"];
197 198 199
    NSURL *momURL = [NSURL fileURLWithPath:path];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];

Pierre's avatar
Pierre committed
200 201 202
    return _managedObjectModel;
}

203
#pragma mark - Path handling
204 205 206 207 208 209 210 211
- (void)setLibraryBasePath:(NSString *)libraryBasePath
{
    _libraryBasePath = [libraryBasePath copy];
    _databaseFolderPath = nil;
    _thumbnailFolderPath = nil;
    _persistentStoreURL = nil;
}

212 213 214 215 216
- (NSString *)databaseFolderPath
{
    if (_databaseFolderPath.length == 0) {
        _databaseFolderPath = self.libraryBasePath;
    }
217
    return _databaseFolderPath;
Pierre's avatar
Pierre committed
218 219
}

220 221
- (NSString *)thumbnailFolderPath
{
222 223
    if (_thumbnailFolderPath.length == 0) {
        _thumbnailFolderPath = [self.libraryBasePath stringByAppendingPathComponent:@"Thumbnails"];
224 225
    }
    return _thumbnailFolderPath;
226 227
}

228 229 230 231 232 233 234 235
- (NSString *)documentFolderPath
{
    if (_documentFolderPath) {
        if (_documentFolderPath.length > 0)
            return _documentFolderPath;
    }
    int directory = NSDocumentDirectory;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
236
    _documentFolderPath = paths.firstObject;
237 238 239
    return _documentFolderPath;
}

240 241 242 243 244 245 246 247
- (NSURL *)persistentStoreURL
{
    if (_persistentStoreURL == nil) {
        NSString *databaseFolderPath = [self databaseFolderPath];
        NSString *path = [databaseFolderPath stringByAppendingPathComponent: @"MediaLibrary.sqlite"];
        _persistentStoreURL = [NSURL fileURLWithPath:path];
    }
    return _persistentStoreURL;
248 249
}

250 251 252 253 254 255 256 257 258 259
- (NSString *)pathRelativeToDocumentsFolderFromAbsolutPath:(NSString *)absolutPath
{
    return [absolutPath stringByReplacingOccurrencesOfString:self.documentFolderPath withString:@""];
}
- (NSString *)absolutPathFromPathRelativeToDocumentsFolder:(NSString *)relativePath
{
    return [self.documentFolderPath stringByAppendingPathComponent:relativePath];
}

#pragma mark -
260

261 262 263 264 265
- (NSPersistentStore *)addDefaultLibraryStoreToCoordinator:(NSPersistentStoreCoordinator *)coordinator withError:(NSError **)error {

    NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : @YES,
                              NSInferMappingModelAutomaticallyOption : @YES,
                              NSSQLitePragmasOption : @{@"journal_mode": @"DELETE"}};
Pierre's avatar
Pierre committed
266

267 268 269 270 271
    if (self.additionalPersitentStoreOptions.count > 0) {
        NSMutableDictionary *mutableOptions = options.mutableCopy;
        [mutableOptions addEntriesFromDictionary:self.additionalPersitentStoreOptions];
        options = mutableOptions;
    }
272 273 274 275 276 277 278 279 280 281
    return [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:self.persistentStoreURL options:options error:error];
}

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (_persistentStoreCoordinator) {
        return _persistentStoreCoordinator;
    }

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

283 284 285 286 287 288 289
    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
290
    NSError *error;
291
    NSPersistentStore *persistentStore = [self addDefaultLibraryStoreToCoordinator:coordinator withError:&error];
Pierre's avatar
Pierre committed
292 293

    if (!persistentStore) {
Pierre's avatar
Pierre committed
294
#if! TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
295 296 297 298
        // 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];
299
        [[NSFileManager defaultManager] removeItemAtPath:self.persistentStoreURL.path error:nil];
Pierre's avatar
Pierre committed
300
#else
301
        [[NSFileManager defaultManager] removeItemAtPath:self.persistentStoreURL.path error:nil];
Pierre's avatar
Pierre committed
302
#endif
303
        persistentStore = [self addDefaultLibraryStoreToCoordinator:coordinator withError:&error];
Pierre's avatar
Pierre committed
304 305 306 307
        if (!persistentStore) {
#if! TARGET_OS_IPHONE
            NSRunInformationalAlertPanel(@"Corrupted Media Library", @"There is nothing we can apparently do about it...", @"OK", nil, nil);
#else
Felix Paul Kühne's avatar
Felix Paul Kühne committed
308
#ifndef TARGET_OS_WATCH
Pierre's avatar
Pierre committed
309 310
            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];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
311
#endif
Pierre's avatar
Pierre committed
312 313 314 315
#endif
            // Probably assert instead.
            return nil;
        }
Pierre's avatar
Pierre committed
316
    }
Tobias's avatar
Tobias committed
317 318

    _persistentStoreCoordinator = coordinator;
319 320 321
    return coordinator;
}

322 323 324 325 326 327 328 329 330

- (void)overrideLibraryWithLibraryFromURL:(NSURL *)replacementURL {

    NSError *error;

    NSPersistentStoreCoordinator *psc = self.persistentStoreCoordinator;
    NSPersistentStore *store = [psc persistentStoreForURL:self.persistentStoreURL];
    if (store) {
        if(![psc removePersistentStore:store error:&error]) {
331
            APLog(@"%s failed to remove persistent store with error %@",__PRETTY_FUNCTION__,error);
332 333 334 335 336
            error = nil;
        }
    }

    NSFileManager *fileManager = [NSFileManager defaultManager];
Tobias's avatar
Tobias committed
337 338 339 340 341 342 343

    NSURL *finalTargetURL = self.persistentStoreURL;
    NSString *tmpName = [[NSUUID UUID] UUIDString];
    NSURL *tmpTargetURL = [[finalTargetURL URLByDeletingLastPathComponent] URLByAppendingPathComponent:tmpName];

    BOOL success = [fileManager copyItemAtURL:replacementURL toURL:tmpTargetURL error:&error];
    if (!success) {
344
        APLog(@"%s failed to copy store to tmp url with with error %@",__PRETTY_FUNCTION__,error);
Tobias's avatar
Tobias committed
345 346 347 348 349 350 351 352 353
        error = nil;
    }

    success = [fileManager replaceItemAtURL:self.persistentStoreURL
                              withItemAtURL:tmpTargetURL
                             backupItemName:nil
                                    options:0
                           resultingItemURL:nil
                                      error:&error];
354
    if (!success) {
355
        APLog(@"%s failed to replace store with error %@",__PRETTY_FUNCTION__,error);
356 357 358 359
        error = nil;
    }

    if(![self addDefaultLibraryStoreToCoordinator:psc withError:&error]) {
360
        APLog(@"%s failed to add store with error %@",__PRETTY_FUNCTION__,error);
361 362 363
    }
}

364 365 366 367
- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext)
        return _managedObjectContext;
Pierre's avatar
Pierre committed
368

369 370 371 372
    NSPersistentStoreCoordinator *coodinator = self.persistentStoreCoordinator;
    if (!coodinator) {
        return nil;
    }
373
    _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
374
    [_managedObjectContext setPersistentStoreCoordinator:coodinator];
Tobias's avatar
Tobias committed
375 376
    if (_managedObjectContext.persistentStoreCoordinator == nil) {
        _managedObjectContext = nil;
377
        return nil;
Tobias's avatar
Tobias committed
378
    }
Pierre's avatar
Pierre committed
379 380 381 382 383 384 385 386
    [_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
387
    NSError *error = nil;
388 389 390 391
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;

392 393 394 395 396
    BOOL success = NO;
    @try {
        success = [[self managedObjectContext] save:&error];
    }
    @catch (NSException *exception) {
397
        APLog(@"Saving pending changes failed");
398
    }
Pierre's avatar
Pierre committed
399
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
400 401 402 403 404 405
    NSProcessInfo *process = [NSProcessInfo processInfo];
    if ([process respondsToSelector:@selector(enableSuddenTermination)])
        [process enableSuddenTermination];
#endif
}

406 407 408
- (void)save
{
    NSError *error = nil;
409 410 411 412
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;

413 414 415 416 417
    BOOL success = NO;
    @try {
        success = [moc save:&error];
    }
    @catch (NSException *exception) {
418
        APLog(@"Saving changes failed");
419
    }
420 421
}

Pierre's avatar
Pierre committed
422
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
Pierre's avatar
Pierre committed
423 424
{
    if ([keyPath isEqualToString:@"hasChanges"] && object == _managedObjectContext) {
Pierre's avatar
Pierre committed
425
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
426 427 428 429 430
        NSProcessInfo *process = [NSProcessInfo processInfo];
        if ([process respondsToSelector:@selector(disableSuddenTermination)])
            [process disableSuddenTermination];
#endif

Pierre's avatar
Pierre committed
431 432 433 434
        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
435 436 437 438 439
        return;
    }
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

440 441 442 443 444
- (NSManagedObject *)objectForURIRepresentation:(NSURL *)uriRepresenation {
    if (uriRepresenation == nil) {
        return nil;
    }
    NSManagedObjectID *objectID = [self.persistentStoreCoordinator managedObjectIDForURIRepresentation:uriRepresenation];
Tobias's avatar
Tobias committed
445 446 447 448
    if (objectID) {
        return [self.managedObjectContext objectWithID:objectID];
    }
    return nil;
449 450
}

Pierre's avatar
Pierre committed
451 452
#pragma mark -
#pragma mark No meta data fallbacks
Pierre's avatar
Pierre committed
453

Pierre's avatar
Pierre committed
454
- (void)computeThumbnailForFile:(MLFile *)file
Pierre's avatar
Pierre committed
455
{
456
    if (!file.computedThumbnail && ![file isKindOfType:kMLFileTypeAudio] && [file.hasFetchedInfo boolValue]) {
457
        APLog(@"Computing thumbnail for %@", file.title);
Pierre's avatar
Pierre committed
458 459
        [[MLThumbnailerQueue sharedThumbnailerQueue] addFile:file];
    }
Pierre's avatar
Pierre committed
460
}
461

Pierre's avatar
Pierre committed
462
- (void)errorWhenFetchingMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
463
{
464
    APLog(@"Error when fetching for '%@'", file.title);
Pierre's avatar
Pierre committed
465

Pierre's avatar
Pierre committed
466 467
    [self computeThumbnailForFile:file];
}
Pierre's avatar
Pierre committed
468

Pierre's avatar
Pierre committed
469 470 471 472 473 474 475
- (void)errorWhenFetchingMetaDataForShow:(MLShow *)show
{
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self errorWhenFetchingMetaDataForFile:file];
    }
}
Pierre's avatar
Pierre committed
476

Pierre's avatar
Pierre committed
477 478
- (void)noMetaDataInRemoteDBForFile:(MLFile *)file
{
479
    file.noOnlineMetaData = @YES;
Pierre's avatar
Pierre committed
480
    [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
481 482
}

Pierre's avatar
Pierre committed
483
- (void)noMetaDataInRemoteDBForShow:(MLShow *)show
Pierre's avatar
Pierre committed
484
{
Pierre's avatar
Pierre committed
485 486 487
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self noMetaDataInRemoteDBForFile:file];
Pierre's avatar
Pierre committed
488 489 490
    }
}

Pierre's avatar
Pierre committed
491 492 493 494
#pragma mark -
#pragma mark Getter

- (void)addNewLabelWithName:(NSString *)name
Pierre's avatar
Pierre committed
495
{
Pierre's avatar
Pierre committed
496 497
    MLLabel *label = [self createObjectForEntity:@"Label"];
    label.name = name;
Pierre's avatar
Pierre committed
498 499
}

Pierre's avatar
Pierre committed
500 501 502 503 504 505 506
/**
 * TV MLShow Episodes
 */

#pragma mark -
#pragma mark Online meta data grabbing

Felix Paul Kühne's avatar
Felix Paul Kühne committed
507
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
508 509
- (void)tvShowEpisodesInfoGrabberDidFinishGrabbing:(MLTVShowEpisodesInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
510
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
511 512

    NSArray *results = grabber.episodesResults;
513
    [show setValue:(grabber.results)[@"serieArtworkURL"] forKey:@"artworkURL"];
Pierre's avatar
Pierre committed
514
    for (id result in results) {
515
        if ([result[@"serie"] boolValue]) {
Pierre's avatar
Pierre committed
516 517
            continue;
        }
518 519 520 521 522
        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
523 524 525 526 527
        if (!showEpisode.artworkURL) {
            for (MLFile *file in showEpisode.files)
                [self computeThumbnailForFile:file];
        }

Pierre's avatar
Pierre committed
528 529 530 531 532
        showEpisode.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
    show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
}

Pierre's avatar
Pierre committed
533 534 535 536 537 538
- (void)tvShowEpisodesInfoGrabber:(MLTVShowEpisodesInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
539 540
- (void)tvShowInfoGrabberDidFinishGrabbing:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
541
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
542 543
    NSArray *results = grabber.results;
    if ([results count] > 0) {
544 545
        NSDictionary *result = results[0];
        NSString *showId = result[@"id"];
Pierre's avatar
Pierre committed
546 547

        show.theTVDBID = showId;
548 549 550
        show.name = result[@"title"];
        show.shortSummary = result[@"shortSummary"];
        show.releaseYear = result[@"releaseYear"];
Pierre's avatar
Pierre committed
551 552

        // Fetch episodes info
553
        MLTVShowEpisodesInfoGrabber *grabber = [[MLTVShowEpisodesInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
554 555 556 557 558 559
        grabber.delegate = self;
        grabber.userData = show;
        [grabber lookUpForShowID:showId];
    }
    else {
        // Not found.
Pierre's avatar
Pierre committed
560
        [self noMetaDataInRemoteDBForShow:show];
Pierre's avatar
Pierre committed
561 562 563 564
        show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
}

Pierre's avatar
Pierre committed
565 566 567 568 569 570
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
571 572
- (void)tvShowInfoGrabberDidFetchServerTime:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
573
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
574 575 576

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

Pierre's avatar
Pierre committed
577
    // First fetch the MLShow ID
578
    MLTVShowInfoGrabber *showInfoGrabber = [[MLTVShowInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
579 580 581
    showInfoGrabber.delegate = self;
    showInfoGrabber.userData = show;

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

Pierre's avatar
Pierre committed
584 585 586 587
    [showInfoGrabber lookUpForTitle:show.name];
}
#endif

Pierre's avatar
Pierre committed
588
- (void)fetchMetaDataForShow:(MLShow *)show
Pierre's avatar
Pierre committed
589
{
590 591
    if (!_allowNetworkAccess)
        return;
592
    APLog(@"Fetching show server time");
Pierre's avatar
Pierre committed
593

Pierre's avatar
Pierre committed
594
    // First fetch the serverTime, so that we can update each entry.
Pierre's avatar
Pierre committed
595 596
#if HAVE_BLOCK
    [MLTVShowInfoGrabber fetchServerTimeAndExecuteBlock:^(NSNumber *serverDate) {
Pierre's avatar
Pierre committed
597 598 599

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

600
        APLog(@"Fetching show information on %@", show.name);
Pierre's avatar
Pierre committed
601 602 603

        // First fetch the MLShow ID
        MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
604 605 606 607 608 609 610 611 612 613 614
        [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"];

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

Pierre's avatar
Pierre committed
617
                // Fetch episode info
Pierre's avatar
Pierre committed
618
                MLTVShowEpisodesInfoGrabber *grabber = [[[MLTVShowEpisodesInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
619 620 621 622 623 624 625
                [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;
                        }
626
                        MLShowEpisode *showEpisode = [MLShowEpisode episodeWithShow:show episodeNumber:[result objectForKey:@"episodeNumber"] seasonNumber:[result objectForKey:@"seasonNumber"] createIfNeeded:YES];
Pierre's avatar
Pierre committed
627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642
                        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
643
#endif
Pierre's avatar
Pierre committed
644 645
}

Pierre's avatar
Pierre committed
646
- (void)addTVShowEpisodeWithInfo:(NSDictionary *)tvShowEpisodeInfo andFile:(MLFile *)file
Pierre's avatar
Pierre committed
647
{
Pierre's avatar
Pierre committed
648
    file.type = kMLFileTypeTVShowEpisode;
Pierre's avatar
Pierre committed
649

650 651 652
    NSNumber *seasonNumber = tvShowEpisodeInfo[@"season"];
    NSNumber *episodeNumber = tvShowEpisodeInfo[@"episode"];
    NSString *tvShowName = tvShowEpisodeInfo[@"tvShowName"];
653
    NSString *tvEpisodeName = tvShowEpisodeInfo[@"tvEpisodeName"];
Pierre's avatar
Pierre committed
654 655
    BOOL hasNoTvShow = NO;
    if (!tvShowName) {
656
        tvShowName = @"";
Pierre's avatar
Pierre committed
657 658 659
        hasNoTvShow = YES;
    }
    BOOL wasInserted = NO;
Pierre's avatar
Pierre committed
660 661
    MLShow *show = nil;
    MLShowEpisode *episode = [MLShowEpisode episodeWithShowName:tvShowName episodeNumber:episodeNumber seasonNumber:seasonNumber createIfNeeded:YES wasCreated:&wasInserted];
662 663

    if (episode) {
Pierre's avatar
Pierre committed
664
        show = episode.show;
665 666
        [show addEpisode:episode];
    }
Pierre's avatar
Pierre committed
667 668 669 670
    if (wasInserted && !hasNoTvShow) {
        show.name = tvShowName;
        [self fetchMetaDataForShow:show];
    }
671
    episode.name = tvEpisodeName;
Pierre's avatar
Pierre committed
672

673
    if (episode.name.length < 1)
Pierre's avatar
Pierre committed
674 675 676
        episode.name = file.title;
    file.seasonNumber = seasonNumber;
    file.episodeNumber = episodeNumber;
677
    episode.shouldBeDisplayed = @YES;
Pierre's avatar
Pierre committed
678 679

    [episode addFilesObject:file];
Pierre's avatar
Pierre committed
680
    file.showEpisode = episode;
Pierre's avatar
Pierre committed
681

Pierre's avatar
Pierre committed
682
    // The rest of the meta data will be fetched using the MLShow
683
    file.hasFetchedInfo = @YES;
Pierre's avatar
Pierre committed
684 685 686
}

/**
Pierre's avatar
Pierre committed
687
 * MLFile auto detection
Pierre's avatar
Pierre committed
688 689
 */

Felix Paul Kühne's avatar
Felix Paul Kühne committed
690
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
691 692 693 694 695 696
- (void)movieInfoGrabber:(MLMovieInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLFile *file = grabber.userData;
    [self errorWhenFetchingMetaDataForFile:file];
}

Pierre's avatar
Pierre committed
697 698
- (void)movieInfoGrabberDidFinishGrabbing:(MLMovieInfoGrabber *)grabber
{
699
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
700 701

    NSArray *results = grabber.results;
Pierre's avatar
Pierre committed
702
    MLFile *file = grabber.userData;
Pierre's avatar
Pierre committed
703
    if ([results count] > 0) {
704 705 706 707 708
        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
709
    }
Pierre's avatar
Pierre committed
710 711 712 713
    else {
        [self noMetaDataInRemoteDBForFile:file];
    }

Pierre's avatar
Pierre committed
714 715 716 717
    file.hasFetchedInfo = yes;
}
#endif

Pierre's avatar
Pierre committed
718
- (void)fetchMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
719
{
720
    APLog(@"Fetching meta data for %@", file.title);
Pierre's avatar
Pierre committed
721

Pierre's avatar
Pierre committed
722
    NSDictionary *tvShowEpisodeInfo = [MLTitleDecrapifier tvShowEpisodeInfoFromString:file.title];
Pierre's avatar
Pierre committed
723
    if (tvShowEpisodeInfo) {
724
        file.type = kMLFileTypeTVShowEpisode;
Pierre's avatar
Pierre committed
725 726 727 728
        [self addTVShowEpisodeWithInfo:tvShowEpisodeInfo andFile:file];
        return;
    }

729 730
    if (!_allowNetworkAccess)
        return;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
731
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
732
    // Go online and fetch info.
Pierre's avatar
Pierre committed
733 734 735

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

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

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

Pierre's avatar
Pierre committed
741
    [grabber lookUpForTitle:file.title andExecuteBlock:^(NSError *err){
Pierre's avatar
Pierre committed
742 743
        if (err) {
            [self errorWhenFetchingMetaDataForFile:file];
Pierre's avatar
Pierre committed
744
            return;
Pierre's avatar
Pierre committed
745
        }
Pierre's avatar
Pierre committed
746 747 748 749 750

        NSArray *results = grabber.results;
        if ([results count] > 0) {
            NSDictionary *result = [results objectAtIndex:0];
            file.artworkURL = [result objectForKey:@"artworkURL"];
Pierre's avatar
Pierre committed
751 752
            if (!file.artworkURL)
                [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
753 754 755
            file.title = [result objectForKey:@"title"];
            file.shortSummary = [result objectForKey:@"shortSummary"];
            file.releaseYear = [result objectForKey:@"releaseYear"];
Pierre's avatar
Pierre committed
756 757 758
        } else
            [self noMetaDataInRemoteDBForFile:file];
        file.hasFetchedInfo = [NSNumber numberWithBool:YES];
Pierre's avatar
Pierre committed
759
    }];
Pierre's avatar
Pierre committed
760
#endif
Pierre's avatar
Pierre committed
761 762
}

Pierre's avatar
Pierre committed
763 764 765
#pragma mark -
#pragma mark Adding file to the DB

766 767 768 769 770 771 772 773
#ifdef MLKIT_READONLY_TARGET

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

#else

Pierre's avatar
Pierre committed
774
- (void)addFilePath:(NSString *)filePath
Pierre's avatar
Pierre committed
775
{
776
    APLog(@"Adding Path %@", filePath);
Pierre's avatar
Pierre committed
777

Pierre's avatar
Pierre committed
778
    NSURL *url = [NSURL fileURLWithPath:filePath];
Pierre's avatar
Pierre committed
779
    NSString *title = [filePath lastPathComponent];
Pierre's avatar
Pierre committed
780
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
781 782
    NSDate *openedDate = nil; // FIXME kMDItemLastUsedDate
    NSDate *modifiedDate = nil; // FIXME [result valueForAttribute:@"kMDItemFSContentChangeDate"];
Pierre's avatar
Pierre committed
783
#endif
Pierre's avatar
Pierre committed
784

Pierre's avatar
Pierre committed
785
    MLFile *file = [self createObjectForEntity:@"File"];
786
    file.url = url;
Pierre's avatar
Pierre committed
787 788 789 790

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

791 792
    NSNumber *no = @NO;
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
793 794

    file.currentlyWatching = no;
795 796
    file.lastPosition = @0.0;
    file.remainingTime = @0.0;
Pierre's avatar
Pierre committed
797 798
    file.unread = yes;

Pierre's avatar
Pierre committed
799
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
800 801 802 803
    if ([openedDate isGreaterThan:modifiedDate]) {
        file.playCount = [NSNumber numberWithDouble:1];
        file.unread = no;
    }
Pierre's avatar
Pierre committed
804 805
#endif

806 807 808 809
    if ([[[NSUserDefaults standardUserDefaults] objectForKey:kDecrapifyTitles] boolValue] == YES)
        file.title = [MLTitleDecrapifier decrapify:[title stringByDeletingPathExtension]];
    else
        file.title = [title stringByDeletingPathExtension];
Pierre's avatar
Pierre committed
810

811
    [[MLFileParserQueue sharedFileParserQueue] addFile:file];
Pierre's avatar
Pierre committed
812 813
}

Pierre's avatar
Pierre committed
814
- (void)addFilePaths:(NSArray *)filepaths
Pierre's avatar
Pierre committed
815
{
Pierre's avatar
Pierre committed
816
    NSUInteger count = [filepaths count];
Pierre's avatar
Pierre committed
817 818 819 820
    NSMutableArray *fetchPredicates = [NSMutableArray arrayWithCapacity:count];
    NSMutableDictionary *urlToObject = [NSMutableDictionary dictionaryWithCapacity:count];

    // Prepare a fetch request for all items
Pierre's avatar
Pierre committed
821
    for (NSString *path in filepaths) {
822
        NSString *relativePath = path;
823
#if TARGET_OS_IPHONE
824 825
        // on iPhone we only save relative paths ins the DB
        relativePath = [self pathRelativeToDocumentsFolderFromAbsolutPath:path];
826
#endif
827
        relativePath = [relativePath decomposedStringWithCanonicalMapping];
828 829
        [urlToObject setObject:path forKey:relativePath];
        [fetchPredicates addObject:[NSPredicate predicateWithFormat:@"path == %@", relativePath]];
Pierre's avatar
Pierre committed
830 831
    }
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];
832 833
    if (!request)
        return;
Pierre's avatar
Pierre committed
834 835
    [request setPredicate:[NSCompoundPredicate orPredicateWithSubpredicates:fetchPredicates]];

836
    APLog(@"Fetching");
837 838 839 840
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;
    NSArray *dbResults = [moc executeFetchRequest:request error:nil];
841
    APLog(@"Done");
Pierre's avatar
Pierre committed
842

Pierre's avatar
Pierre committed
843
    NSMutableArray *filePathsToAdd = [NSMutableArray arrayWithArray:filepaths];
Pierre's avatar
Pierre committed
844 845

    // Remove objects that are already in db.
Pierre's avatar
Pierre committed
846
    for (MLFile *dbResult in dbResults) {
847
        NSString *path = dbResult.path;
848
        path = [path decomposedStringWithCanonicalMapping];
849
        [filePathsToAdd removeObject:[urlToObject objectForKey:path]];
Pierre's avatar
Pierre committed
850 851 852
    }

    // Add only the newly added items
Pierre's avatar
Pierre committed
853 854
    for (NSString* path in filePathsToAdd)
        [self addFilePath:path];
Pierre's avatar
Pierre committed
855
}
856
#endif
Pierre's avatar
Pierre committed
857 858 859 860

#pragma mark -
#pragma mark DB Updates

Felix Paul Kühne's avatar
Felix Paul Kühne committed
861
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
862 863 864
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFetchUpdates:(NSArray *)updates
{
    NSFetchRequest *request = [self fetchRequestForEntity:@"Show"];
865 866 867
    if (!request)
        return;

Pierre's avatar
Pierre committed
868 869
    [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
870
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
871 872 873 874
        [self fetchMetaDataForShow:show];
}
#endif

875 876 877 878 879 880 881 882
#ifdef MLKIT_READONLY_TARGET

- (void)updateMediaDatabase
{
}

#else

883
- (void)updateMediaDatabase
Pierre's avatar
Pierre committed
884
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
885
    [self libraryDidDisappear];
Pierre's avatar
Pierre committed
886
    // Remove no more present files
Pierre's avatar
Pierre committed
887
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];
888 889
    if (!request)
        return;
890 891 892
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;
893 894 895 896 897
    NSArray *results;
    @try {
        results = [moc executeFetchRequest:request error:nil];
    }
    @catch (NSException *exception) {
898
        APLog(@"media database update failed");
899 900 901
        return;
    }

Pierre's avatar
Pierre committed
902
    NSFileManager *fileManager = [NSFileManager defaultManager];
903

904
    unsigned int count = (unsigned int)results.count;
905 906
    for (unsigned int x = 0; x < count; x++) {
        MLFile *file = results[x];
907
       NSURL *fileURL = file.url;
908
        BOOL exists = [fileManager fileExistsAtPath:[fileURL path]];
909
        if (!exists) {
910
            APLog(@"Marking - %@", [fileURL absoluteString]);
911
            file.isSafe = YES; // It doesn't exist, it's safe.
912 913
            if (file.isAlbumTrack) {
                MLAlbum *album = file.albumTrack.album;
914 915 916 917 918 919 920 921 922 923 924
                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];
                }
925 926 927
            }
            if (file.isShowEpisode) {
                MLShow *show = file.showEpisode.show;
928 929 930 931 932 933 934 935 936 937 938
                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];
                }
939
            }
940
#if TARGET_OS_IOS
941
            NSString *thumbPath = [file thumbnailPath];
942 943
            bool thumbExists = [fileManager fileExistsAtPath:thumbPath];
            if (thumbExists)
944
                [fileManager removeItemAtPath:thumbPath error:nil];
945

946
            if ([CSSearchableIndex class]) {
947 948 949
            /* remove file from CoreSpotlight */
                [[CSSearchableIndex defaultSearchableIndex] deleteSearchableItemsWithIdentifiers:@[file.objectID.URIRepresentation.absoluteString]
                                                                               completionHandler:^(NSError * __nullable error) {
950
                                                                                   APLog(@"Removed %@ from index", file.objectID.URIRepresentation);
951 952 953
                                                                               }];
            }

954
            [moc deleteObject:file];
955
#endif
956
        }
957 958 959
#if !TARGET_OS_IPHONE
    file.isOnDisk = @(exists);
#endif
Pierre's avatar
Pierre committed
960
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
961
    [self libraryDidAppear];
Pierre's avatar
Pierre committed
962

963 964
    // Get the file to parse
    request = [self fetchRequestForEntity:@"File"];
965 966
    if (!request)
        return;
967
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && tracks.@count == 0"]];
968 969 970 971
    @try {
        results = [moc executeFetchRequest:request error:nil];
    }
    @catch (NSException *exception) {
972
        APLog(@"media database update failed");
973 974
        return;
    }
975 976 977
    for (MLFile *file in results)
        [[MLFileParserQueue sharedFileParserQueue] addFile:file];

978 979 980
    if (!_allowNetworkAccess) {
        // Always attempt to fetch
        request = [self fetchRequestForEntity:@"File"];
981 982
        if (!request)
            return;
983
        [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES"]];
984 985 986 987
        @try {
            results = [moc executeFetchRequest:request error:nil];
        }
        @catch (NSException *exception) {
988
            APLog(@"media database update failed");
989 990
            return;
        }
991
        for (MLFile *file in results) {
992
            if (!file.computedThumbnail && ![file isKindOfType:kMLFileTypeAudio] && [file.hasFetchedInfo boolValue])
993 994
                [self computeThumbnailForFile:file];
        }
995 996 997
        return;
    }

Pierre's avatar
Pierre committed
998 999
    // Get the thumbnails to compute
    request = [self fetchRequestForEntity:@"File"];
1000 1001
    if (!request)
        return;
1002
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && hasFetchedInfo == 1 && artworkURL == nil"]];
1003 1004 1005 1006
    @try {
        results = [moc executeFetchRequest:request error:nil];
    }
    @catch (NSException *exception) {
1007
        APLog(@"media database update failed");
1008 1009
        return;
    }
1010 1011
    for (MLFile *file in results) {
        if (!file.computedThumbnail) {
1012
            if (!file.albumTrack && ![file isKindOfType:kMLFileTypeAudio] && [file.hasFetchedInfo boolValue])
1013 1014 1015
                [self computeThumbnailForFile:file];
        }
    }
Pierre's avatar
Pierre committed
1016 1017 1018

    // Get to fetch meta data
    request = [self fetchRequestForEntity:@"File"];
1019 1020
    if (!request)
        return;
Pierre's avatar
Pierre committed
1021
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && hasFetchedInfo == 0"]];
1022 1023 1024 1025
    @try {
        results = [moc executeFetchRequest:request error:nil];
    }
    @catch (NSException *exception) {
1026
        APLog(@"media database update failed");
1027 1028
        return;
    }
Pierre's avatar
Pierre committed
1029
    for (MLFile *file in results)
1030
        [[MLFileParserQueue sharedFileParserQueue] addFile:file];
Pierre's avatar
Pierre committed
1031

Pierre's avatar
Pierre committed
1032
    // Get to fetch show info
Pierre's avatar
Pierre committed
1033
    request = [self fetchRequestForEntity:@"Show"];
1034 1035
    if (!request)
        return;
Pierre's avatar
Pierre committed
1036
    [request setPredicate:[NSPredicate predicateWithFormat:@"lastSyncDate == 0"]];
1037 1038 1039 1040
    @try {
        results = [moc executeFetchRequest:request error:nil];
    }
    @catch (NSException *exception) {
1041
        APLog(@"media database update failed");
1042 1043
        return;
    }
Pierre's avatar
Pierre committed
1044
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
1045 1046
        [self fetchMetaDataForShow:show];

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

Pierre's avatar
Pierre committed
1051
    [MLTVShowInfoGrabber fetchUpdatesSinceServerTime:lastServerTime andExecuteBlock:^(NSArray *updates){
Pierre's avatar
Pierre committed
1052
        NSFetchRequest *request = [self fetchRequestForEntity:@"Show"];
1053 1054
        if (!request)
            return;
Pierre's avatar
Pierre committed
1055
        [request setPredicate:[NSComparisonPredicate predicateWithLeftExpression:[NSExpression expressionForKeyPath:@"theTVDBID"] rightExpression:[NSExpression expressionForConstantValue:updates] modifier:NSDirectPredicateModifier type:NSInPredicateOperatorType options:0]];
1056
        NSArray *results = [moc executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
1057
        for (MLShow *show in results)
Pierre's avatar
Pierre committed
1058 1059
            [self fetchMetaDataForShow:show];
    }];
Pierre's avatar
Pierre committed
1060
#endif
Pierre's avatar
Pierre committed
1061
    /* Update every hour - FIXME: Preferences key */
1062
    [self performSelector:@selector(updateMediaDatabase) withObject:nil afterDelay:60 * 60];
Pierre's avatar
Pierre committed
1063
}
1064
#endif
Pierre's avatar
Pierre committed
1065

1066 1067
- (void)applicationWillExit
{
1068
    [[MLFileParserQueue sharedFileParserQueue] stop];
1069 1070 1071
    [[MLCrashPreventer sharedPreventer] cancelAllFileParse];
}

Pierre's avatar
Pierre committed
1072 1073 1074
- (void)applicationWillStart
{
    [[MLCrashPreventer sharedPreventer] markCrasherFiles];
1075
    [[MLFileParserQueue sharedFileParserQueue] resume];
Pierre's avatar
Pierre committed
1076 1077
}

Pierre's avatar
Pierre committed
1078 1079 1080 1081
- (void)libraryDidDisappear
{
    // Stop expansive work
    [[MLThumbnailerQueue sharedThumbnailerQueue] stop];
1082
    [[MLFileParserQueue sharedFileParserQueue] stop];
Pierre's avatar
Pierre committed
1083 1084 1085 1086 1087 1088
}

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

1092
#pragma mark - migrations
1093

1094 1095 1096 1097 1098 1099 1100
- (BOOL)libraryMigrationNeeded
{
    return [self _libraryMigrationNeeded];
}
- (void)migrateLibrary
{
    [self _migrateLibrary];
1101 1102
}

Pierre's avatar
Pierre committed
1103
@end