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];
387 388 389 390
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;

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

407 408
- (void)save
{
409 410 411
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;
412
    [moc performBlock:^{
413 414 415 416 417 418 419
        NSError *error = nil;
        @try {
            [moc save:&error];
        }
        @catch (NSException *exception) {
            APLog(@"Saving changes failed");
        }
420
    }];
421 422
}

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

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

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

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

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

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

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

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

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

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

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

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

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

#pragma mark -
#pragma mark Online meta data grabbing

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

767 768 769 770 771 772 773 774
#ifdef MLKIT_READONLY_TARGET

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

#else

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#pragma mark -
#pragma mark DB Updates

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

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

876 877 878 879 880 881 882 883
#ifdef MLKIT_READONLY_TARGET

- (void)updateMediaDatabase
{
}

#else

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1093
#pragma mark - migrations
1094

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

Pierre's avatar
Pierre committed
1104
@end