MLMediaLibrary.m 35.9 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 105 106 107 108 109 110 111
- (instancetype)init
{
    self = [super init];
    if (self) {
        _applicationGroupIdentifier = @"group.org.videolan.vlc-ios";
        [self _setupLibraryPathPriorToMigration];
        APLog(@"Initializing db in %@", [self databaseFolderPath]);
    }
    return self;
}

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

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

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

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

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

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

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

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

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

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

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

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

    return _deviceSpeedCategory;
}

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

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

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

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

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

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

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

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

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

#pragma mark -
250

251 252 253 254 255
- (NSPersistentStore *)addDefaultLibraryStoreToCoordinator:(NSPersistentStoreCoordinator *)coordinator withError:(NSError **)error {

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

257 258 259 260 261
    if (self.additionalPersitentStoreOptions.count > 0) {
        NSMutableDictionary *mutableOptions = options.mutableCopy;
        [mutableOptions addEntriesFromDictionary:self.additionalPersitentStoreOptions];
        options = mutableOptions;
    }
262 263 264 265 266 267 268 269 270 271
    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
272

273 274 275 276 277 278 279
    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
280
    NSError *error;
281
    NSPersistentStore *persistentStore = [self addDefaultLibraryStoreToCoordinator:coordinator withError:&error];
Pierre's avatar
Pierre committed
282 283

    if (!persistentStore) {
Pierre's avatar
Pierre committed
284
#if! TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
285 286 287 288
        // 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];
289
        [[NSFileManager defaultManager] removeItemAtPath:self.persistentStoreURL.path error:nil];
Pierre's avatar
Pierre committed
290
#else
291
        [[NSFileManager defaultManager] removeItemAtPath:self.persistentStoreURL.path error:nil];
Pierre's avatar
Pierre committed
292
#endif
293
        persistentStore = [self addDefaultLibraryStoreToCoordinator:coordinator withError:&error];
Pierre's avatar
Pierre committed
294 295 296 297
        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
298
#ifndef TARGET_OS_WATCH
Pierre's avatar
Pierre committed
299 300
            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
301
#endif
Pierre's avatar
Pierre committed
302 303 304 305
#endif
            // Probably assert instead.
            return nil;
        }
Pierre's avatar
Pierre committed
306
    }
Tobias's avatar
Tobias committed
307 308

    _persistentStoreCoordinator = coordinator;
309 310 311
    return coordinator;
}

312 313 314 315 316 317 318 319 320 321 322 323 324 325 326

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

    NSError *error;

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

    NSFileManager *fileManager = [NSFileManager defaultManager];
Tobias's avatar
Tobias committed
327 328 329 330 331 332 333 334 335 336 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) {
        NSLog(@"%s failed to copy store to tmp url with with error %@",__PRETTY_FUNCTION__,error);
        error = nil;
    }

    success = [fileManager replaceItemAtURL:self.persistentStoreURL
                              withItemAtURL:tmpTargetURL
                             backupItemName:nil
                                    options:0
                           resultingItemURL:nil
                                      error:&error];
344 345 346 347 348 349 350 351 352 353
    if (!success) {
        NSLog(@"%s failed to replace store with error %@",__PRETTY_FUNCTION__,error);
        error = nil;
    }

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

354 355 356 357
- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext)
        return _managedObjectContext;
Pierre's avatar
Pierre committed
358

359 360 361 362
    NSPersistentStoreCoordinator *coodinator = self.persistentStoreCoordinator;
    if (!coodinator) {
        return nil;
    }
363
    _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
364
    [_managedObjectContext setPersistentStoreCoordinator:coodinator];
Tobias's avatar
Tobias committed
365 366
    if (_managedObjectContext.persistentStoreCoordinator == nil) {
        _managedObjectContext = nil;
367
        return nil;
Tobias's avatar
Tobias committed
368
    }
Pierre's avatar
Pierre committed
369 370 371 372 373 374 375 376
    [_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
377
    NSError *error = nil;
378 379 380 381
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;

382 383 384 385 386 387 388
    BOOL success = NO;
    @try {
        success = [[self managedObjectContext] save:&error];
    }
    @catch (NSException *exception) {
        NSLog(@"Saving pending changes failed");
    }
Pierre's avatar
Pierre committed
389
    NSAssert1(success, @"Can't save: %@", error);
Pierre's avatar
Pierre committed
390
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
391 392 393 394 395 396
    NSProcessInfo *process = [NSProcessInfo processInfo];
    if ([process respondsToSelector:@selector(enableSuddenTermination)])
        [process enableSuddenTermination];
#endif
}

397 398 399
- (void)save
{
    NSError *error = nil;
400 401 402 403
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;

404 405 406 407 408 409 410
    BOOL success = NO;
    @try {
        success = [moc save:&error];
    }
    @catch (NSException *exception) {
        NSLog(@"Saving changes failed");
    }
411 412 413
    NSAssert1(success, @"Can't save: %@", error);
}

Pierre's avatar
Pierre committed
414
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
Pierre's avatar
Pierre committed
415 416
{
    if ([keyPath isEqualToString:@"hasChanges"] && object == _managedObjectContext) {
Pierre's avatar
Pierre committed
417
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
418 419 420 421 422
        NSProcessInfo *process = [NSProcessInfo processInfo];
        if ([process respondsToSelector:@selector(disableSuddenTermination)])
            [process disableSuddenTermination];
#endif

Pierre's avatar
Pierre committed
423 424 425 426
        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
427 428 429 430 431
        return;
    }
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

432 433 434 435 436
- (NSManagedObject *)objectForURIRepresentation:(NSURL *)uriRepresenation {
    if (uriRepresenation == nil) {
        return nil;
    }
    NSManagedObjectID *objectID = [self.persistentStoreCoordinator managedObjectIDForURIRepresentation:uriRepresenation];
Tobias's avatar
Tobias committed
437 438 439 440
    if (objectID) {
        return [self.managedObjectContext objectWithID:objectID];
    }
    return nil;
441 442
}

Pierre's avatar
Pierre committed
443 444
#pragma mark -
#pragma mark No meta data fallbacks
Pierre's avatar
Pierre committed
445

Pierre's avatar
Pierre committed
446
- (void)computeThumbnailForFile:(MLFile *)file
Pierre's avatar
Pierre committed
447
{
448
    if (!file.computedThumbnail && ![file isKindOfType:kMLFileTypeAudio]) {
449
        APLog(@"Computing thumbnail for %@", file.title);
Pierre's avatar
Pierre committed
450 451
        [[MLThumbnailerQueue sharedThumbnailerQueue] addFile:file];
    }
Pierre's avatar
Pierre committed
452
}
453

Pierre's avatar
Pierre committed
454
- (void)errorWhenFetchingMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
455
{
456
    APLog(@"Error when fetching for '%@'", file.title);
Pierre's avatar
Pierre committed
457

Pierre's avatar
Pierre committed
458 459
    [self computeThumbnailForFile:file];
}
Pierre's avatar
Pierre committed
460

Pierre's avatar
Pierre committed
461 462 463 464 465 466 467
- (void)errorWhenFetchingMetaDataForShow:(MLShow *)show
{
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self errorWhenFetchingMetaDataForFile:file];
    }
}
Pierre's avatar
Pierre committed
468

Pierre's avatar
Pierre committed
469 470
- (void)noMetaDataInRemoteDBForFile:(MLFile *)file
{
471
    file.noOnlineMetaData = @YES;
Pierre's avatar
Pierre committed
472
    [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
473 474
}

Pierre's avatar
Pierre committed
475
- (void)noMetaDataInRemoteDBForShow:(MLShow *)show
Pierre's avatar
Pierre committed
476
{
Pierre's avatar
Pierre committed
477 478 479
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self noMetaDataInRemoteDBForFile:file];
Pierre's avatar
Pierre committed
480 481 482
    }
}

Pierre's avatar
Pierre committed
483 484 485 486
#pragma mark -
#pragma mark Getter

- (void)addNewLabelWithName:(NSString *)name
Pierre's avatar
Pierre committed
487
{
Pierre's avatar
Pierre committed
488 489
    MLLabel *label = [self createObjectForEntity:@"Label"];
    label.name = name;
Pierre's avatar
Pierre committed
490 491
}

Pierre's avatar
Pierre committed
492 493 494 495 496 497 498
/**
 * TV MLShow Episodes
 */

#pragma mark -
#pragma mark Online meta data grabbing

Felix Paul Kühne's avatar
Felix Paul Kühne committed
499
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
500 501
- (void)tvShowEpisodesInfoGrabberDidFinishGrabbing:(MLTVShowEpisodesInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
502
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
503 504

    NSArray *results = grabber.episodesResults;
505
    [show setValue:(grabber.results)[@"serieArtworkURL"] forKey:@"artworkURL"];
Pierre's avatar
Pierre committed
506
    for (id result in results) {
507
        if ([result[@"serie"] boolValue]) {
Pierre's avatar
Pierre committed
508 509
            continue;
        }
510 511 512 513 514
        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
515 516 517 518 519
        if (!showEpisode.artworkURL) {
            for (MLFile *file in showEpisode.files)
                [self computeThumbnailForFile:file];
        }

Pierre's avatar
Pierre committed
520 521 522 523 524
        showEpisode.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
    show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
}

Pierre's avatar
Pierre committed
525 526 527 528 529 530
- (void)tvShowEpisodesInfoGrabber:(MLTVShowEpisodesInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
531 532
- (void)tvShowInfoGrabberDidFinishGrabbing:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
533
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
534 535
    NSArray *results = grabber.results;
    if ([results count] > 0) {
536 537
        NSDictionary *result = results[0];
        NSString *showId = result[@"id"];
Pierre's avatar
Pierre committed
538 539

        show.theTVDBID = showId;
540 541 542
        show.name = result[@"title"];
        show.shortSummary = result[@"shortSummary"];
        show.releaseYear = result[@"releaseYear"];
Pierre's avatar
Pierre committed
543 544

        // Fetch episodes info
545
        MLTVShowEpisodesInfoGrabber *grabber = [[MLTVShowEpisodesInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
546 547 548 549 550 551
        grabber.delegate = self;
        grabber.userData = show;
        [grabber lookUpForShowID:showId];
    }
    else {
        // Not found.
Pierre's avatar
Pierre committed
552
        [self noMetaDataInRemoteDBForShow:show];
Pierre's avatar
Pierre committed
553 554 555 556
        show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
}

Pierre's avatar
Pierre committed
557 558 559 560 561 562
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
563 564
- (void)tvShowInfoGrabberDidFetchServerTime:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
565
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
566 567 568

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

Pierre's avatar
Pierre committed
569
    // First fetch the MLShow ID
570
    MLTVShowInfoGrabber *showInfoGrabber = [[MLTVShowInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
571 572 573
    showInfoGrabber.delegate = self;
    showInfoGrabber.userData = show;

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

Pierre's avatar
Pierre committed
576 577 578 579
    [showInfoGrabber lookUpForTitle:show.name];
}
#endif

Pierre's avatar
Pierre committed
580
- (void)fetchMetaDataForShow:(MLShow *)show
Pierre's avatar
Pierre committed
581
{
582 583
    if (!_allowNetworkAccess)
        return;
584
    APLog(@"Fetching show server time");
Pierre's avatar
Pierre committed
585

Pierre's avatar
Pierre committed
586
    // First fetch the serverTime, so that we can update each entry.
Pierre's avatar
Pierre committed
587 588
#if HAVE_BLOCK
    [MLTVShowInfoGrabber fetchServerTimeAndExecuteBlock:^(NSNumber *serverDate) {
Pierre's avatar
Pierre committed
589 590 591

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

592
        APLog(@"Fetching show information on %@", show.name);
Pierre's avatar
Pierre committed
593 594 595

        // First fetch the MLShow ID
        MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
596 597 598 599 600 601 602 603 604 605 606
        [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"];

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

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

Pierre's avatar
Pierre committed
638
- (void)addTVShowEpisodeWithInfo:(NSDictionary *)tvShowEpisodeInfo andFile:(MLFile *)file
Pierre's avatar
Pierre committed
639
{
Pierre's avatar
Pierre committed
640
    file.type = kMLFileTypeTVShowEpisode;
Pierre's avatar
Pierre committed
641

642 643 644
    NSNumber *seasonNumber = tvShowEpisodeInfo[@"season"];
    NSNumber *episodeNumber = tvShowEpisodeInfo[@"episode"];
    NSString *tvShowName = tvShowEpisodeInfo[@"tvShowName"];
645
    NSString *tvEpisodeName = tvShowEpisodeInfo[@"tvEpisodeName"];
Pierre's avatar
Pierre committed
646 647
    BOOL hasNoTvShow = NO;
    if (!tvShowName) {
648
        tvShowName = @"";
Pierre's avatar
Pierre committed
649 650 651
        hasNoTvShow = YES;
    }
    BOOL wasInserted = NO;
Pierre's avatar
Pierre committed
652 653
    MLShow *show = nil;
    MLShowEpisode *episode = [MLShowEpisode episodeWithShowName:tvShowName episodeNumber:episodeNumber seasonNumber:seasonNumber createIfNeeded:YES wasCreated:&wasInserted];
654 655

    if (episode) {
Pierre's avatar
Pierre committed
656
        show = episode.show;
657 658
        [show addEpisode:episode];
    }
Pierre's avatar
Pierre committed
659 660 661 662
    if (wasInserted && !hasNoTvShow) {
        show.name = tvShowName;
        [self fetchMetaDataForShow:show];
    }
663
    episode.name = tvEpisodeName;
Pierre's avatar
Pierre committed
664

665
    if (episode.name.length < 1)
Pierre's avatar
Pierre committed
666 667 668
        episode.name = file.title;
    file.seasonNumber = seasonNumber;
    file.episodeNumber = episodeNumber;
669
    episode.shouldBeDisplayed = @YES;
Pierre's avatar
Pierre committed
670 671

    [episode addFilesObject:file];
Pierre's avatar
Pierre committed
672
    file.showEpisode = episode;
Pierre's avatar
Pierre committed
673

Pierre's avatar
Pierre committed
674
    // The rest of the meta data will be fetched using the MLShow
675
    file.hasFetchedInfo = @YES;
Pierre's avatar
Pierre committed
676 677 678
}

/**
Pierre's avatar
Pierre committed
679
 * MLFile auto detection
Pierre's avatar
Pierre committed
680 681
 */

Felix Paul Kühne's avatar
Felix Paul Kühne committed
682
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
683 684 685 686 687 688
- (void)movieInfoGrabber:(MLMovieInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLFile *file = grabber.userData;
    [self errorWhenFetchingMetaDataForFile:file];
}

Pierre's avatar
Pierre committed
689 690
- (void)movieInfoGrabberDidFinishGrabbing:(MLMovieInfoGrabber *)grabber
{
691
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
692 693

    NSArray *results = grabber.results;
Pierre's avatar
Pierre committed
694
    MLFile *file = grabber.userData;
Pierre's avatar
Pierre committed
695
    if ([results count] > 0) {
696 697 698 699 700
        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
701
    }
Pierre's avatar
Pierre committed
702 703 704 705
    else {
        [self noMetaDataInRemoteDBForFile:file];
    }

Pierre's avatar
Pierre committed
706 707 708 709
    file.hasFetchedInfo = yes;
}
#endif

Pierre's avatar
Pierre committed
710
- (void)fetchMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
711
{
712
    APLog(@"Fetching meta data for %@", file.title);
Pierre's avatar
Pierre committed
713

Pierre's avatar
Pierre committed
714
    NSDictionary *tvShowEpisodeInfo = [MLTitleDecrapifier tvShowEpisodeInfoFromString:file.title];
Pierre's avatar
Pierre committed
715
    if (tvShowEpisodeInfo) {
716
        file.type = kMLFileTypeTVShowEpisode;
Pierre's avatar
Pierre committed
717 718 719 720
        [self addTVShowEpisodeWithInfo:tvShowEpisodeInfo andFile:file];
        return;
    }

721 722
    if (!_allowNetworkAccess)
        return;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
723
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
724
    // Go online and fetch info.
Pierre's avatar
Pierre committed
725 726 727

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

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

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

Pierre's avatar
Pierre committed
733
    [grabber lookUpForTitle:file.title andExecuteBlock:^(NSError *err){
Pierre's avatar
Pierre committed
734 735
        if (err) {
            [self errorWhenFetchingMetaDataForFile:file];
Pierre's avatar
Pierre committed
736
            return;
Pierre's avatar
Pierre committed
737
        }
Pierre's avatar
Pierre committed
738 739 740 741 742

        NSArray *results = grabber.results;
        if ([results count] > 0) {
            NSDictionary *result = [results objectAtIndex:0];
            file.artworkURL = [result objectForKey:@"artworkURL"];
Pierre's avatar
Pierre committed
743 744
            if (!file.artworkURL)
                [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
745 746 747
            file.title = [result objectForKey:@"title"];
            file.shortSummary = [result objectForKey:@"shortSummary"];
            file.releaseYear = [result objectForKey:@"releaseYear"];
Pierre's avatar
Pierre committed
748 749 750
        } else
            [self noMetaDataInRemoteDBForFile:file];
        file.hasFetchedInfo = [NSNumber numberWithBool:YES];
Pierre's avatar
Pierre committed
751
    }];
Pierre's avatar
Pierre committed
752
#endif
Pierre's avatar
Pierre committed
753 754
}

Pierre's avatar
Pierre committed
755 756 757
#pragma mark -
#pragma mark Adding file to the DB

758 759 760 761 762 763 764 765
#ifdef MLKIT_READONLY_TARGET

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

#else

Pierre's avatar
Pierre committed
766
- (void)addFilePath:(NSString *)filePath
Pierre's avatar
Pierre committed
767
{
768
    APLog(@"Adding Path %@", filePath);
Pierre's avatar
Pierre committed
769

Pierre's avatar
Pierre committed
770
    NSURL *url = [NSURL fileURLWithPath:filePath];
Pierre's avatar
Pierre committed
771
    NSString *title = [filePath lastPathComponent];
Pierre's avatar
Pierre committed
772
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
773 774
    NSDate *openedDate = nil; // FIXME kMDItemLastUsedDate
    NSDate *modifiedDate = nil; // FIXME [result valueForAttribute:@"kMDItemFSContentChangeDate"];
Pierre's avatar
Pierre committed
775
#endif
Pierre's avatar
Pierre committed
776

Pierre's avatar
Pierre committed
777
    MLFile *file = [self createObjectForEntity:@"File"];
778
    file.url = url;
Pierre's avatar
Pierre committed
779 780 781 782

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

783 784
    NSNumber *no = @NO;
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
785 786

    file.currentlyWatching = no;
787 788
    file.lastPosition = @0.0;
    file.remainingTime = @0.0;
Pierre's avatar
Pierre committed
789 790
    file.unread = yes;

Pierre's avatar
Pierre committed
791
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
792 793 794 795
    if ([openedDate isGreaterThan:modifiedDate]) {
        file.playCount = [NSNumber numberWithDouble:1];
        file.unread = no;
    }
Pierre's avatar
Pierre committed
796 797
#endif

798 799 800 801
    if ([[[NSUserDefaults standardUserDefaults] objectForKey:kDecrapifyTitles] boolValue] == YES)
        file.title = [MLTitleDecrapifier decrapify:[title stringByDeletingPathExtension]];
    else
        file.title = [title stringByDeletingPathExtension];
Pierre's avatar
Pierre committed
802

803
    [[MLFileParserQueue sharedFileParserQueue] addFile:file];
Pierre's avatar
Pierre committed
804 805
}

Pierre's avatar
Pierre committed
806
- (void)addFilePaths:(NSArray *)filepaths
Pierre's avatar
Pierre committed
807
{
Pierre's avatar
Pierre committed
808
    NSUInteger count = [filepaths count];
Pierre's avatar
Pierre committed
809 810 811 812
    NSMutableArray *fetchPredicates = [NSMutableArray arrayWithCapacity:count];
    NSMutableDictionary *urlToObject = [NSMutableDictionary dictionaryWithCapacity:count];

    // Prepare a fetch request for all items
Pierre's avatar
Pierre committed
813
    for (NSString *path in filepaths) {
814
        NSString *relativePath = path;
815
#if TARGET_OS_IPHONE
816 817
        // on iPhone we only save relative paths ins the DB
        relativePath = [self pathRelativeToDocumentsFolderFromAbsolutPath:path];
818
#endif
819 820
        [urlToObject setObject:path forKey:relativePath];
        [fetchPredicates addObject:[NSPredicate predicateWithFormat:@"path == %@", relativePath]];
Pierre's avatar
Pierre committed
821 822
    }
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];
823 824
    if (!request)
        return;
Pierre's avatar
Pierre committed
825 826
    [request setPredicate:[NSCompoundPredicate orPredicateWithSubpredicates:fetchPredicates]];

827
    APLog(@"Fetching");
828 829 830 831
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;
    NSArray *dbResults = [moc executeFetchRequest:request error:nil];
832
    APLog(@"Done");
Pierre's avatar
Pierre committed
833

Pierre's avatar
Pierre committed
834
    NSMutableArray *filePathsToAdd = [NSMutableArray arrayWithArray:filepaths];
Pierre's avatar
Pierre committed
835 836

    // Remove objects that are already in db.
Pierre's avatar
Pierre committed
837
    for (MLFile *dbResult in dbResults) {
838 839
        NSString *path = dbResult.path;
        [filePathsToAdd removeObject:[urlToObject objectForKey:path]];
Pierre's avatar
Pierre committed
840 841 842
    }

    // Add only the newly added items
Pierre's avatar
Pierre committed
843 844
    for (NSString* path in filePathsToAdd)
        [self addFilePath:path];
Pierre's avatar
Pierre committed
845
}
846
#endif
Pierre's avatar
Pierre committed
847 848 849 850

#pragma mark -
#pragma mark DB Updates

Felix Paul Kühne's avatar
Felix Paul Kühne committed
851
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
852 853 854
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFetchUpdates:(NSArray *)updates
{
    NSFetchRequest *request = [self fetchRequestForEntity:@"Show"];
855 856 857
    if (!request)
        return;

Pierre's avatar
Pierre committed
858 859
    [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
860
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
861 862 863 864
        [self fetchMetaDataForShow:show];
}
#endif

865 866 867 868 869 870 871 872
#ifdef MLKIT_READONLY_TARGET

- (void)updateMediaDatabase
{
}

#else

873
- (void)updateMediaDatabase
Pierre's avatar
Pierre committed
874
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
875
    [self libraryDidDisappear];
Pierre's avatar
Pierre committed
876
    // Remove no more present files
Pierre's avatar
Pierre committed
877
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];
878 879
    if (!request)
        return;
880 881 882
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;
883 884 885 886 887 888 889 890 891
    NSArray *results;
    @try {
        results = [moc executeFetchRequest:request error:nil];
    }
    @catch (NSException *exception) {
        NSLog(@"media database update failed");
        return;
    }

Pierre's avatar
Pierre committed
892
    NSFileManager *fileManager = [NSFileManager defaultManager];
893

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

936
            if ([CSSearchableIndex class]) {
937 938 939 940 941 942 943
            /* remove file from CoreSpotlight */
                [[CSSearchableIndex defaultSearchableIndex] deleteSearchableItemsWithIdentifiers:@[file.objectID.URIRepresentation.absoluteString]
                                                                               completionHandler:^(NSError * __nullable error) {
                                                                                   NSLog(@"Removed %@ from index", file.objectID.URIRepresentation);
                                                                               }];
            }

944
            [moc deleteObject:file];
945
#endif
946
        }
947 948 949
#if !TARGET_OS_IPHONE
    file.isOnDisk = @(exists);
#endif
Pierre's avatar
Pierre committed
950
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
951
    [self libraryDidAppear];
Pierre's avatar
Pierre committed
952

953 954
    // Get the file to parse
    request = [self fetchRequestForEntity:@"File"];
955 956
    if (!request)
        return;
957
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && tracks.@count == 0"]];
958
    results = [moc executeFetchRequest:request error:nil];
959 960 961
    for (MLFile *file in results)
        [[MLFileParserQueue sharedFileParserQueue] addFile:file];

962 963 964
    if (!_allowNetworkAccess) {
        // Always attempt to fetch
        request = [self fetchRequestForEntity:@"File"];
965 966
        if (!request)
            return;
967
        [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES"]];
968
        results = [moc executeFetchRequest:request error:nil];
969
        for (MLFile *file in results) {
970
            if (!file.computedThumbnail && ![file isKindOfType:kMLFileTypeAudio])
971 972
                [self computeThumbnailForFile:file];
        }
973 974 975
        return;
    }

Pierre's avatar
Pierre committed
976 977
    // Get the thumbnails to compute
    request = [self fetchRequestForEntity:@"File"];
978 979
    if (!request)
        return;
980
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && hasFetchedInfo == 1 && artworkURL == nil"]];
981
    results = [moc executeFetchRequest:request error:nil];
982 983 984 985 986 987
    for (MLFile *file in results) {
        if (!file.computedThumbnail) {
            if (!file.albumTrack && ![file isKindOfType:kMLFileTypeAudio])
                [self computeThumbnailForFile:file];
        }
    }
Pierre's avatar
Pierre committed
988 989 990

    // Get to fetch meta data
    request = [self fetchRequestForEntity:@"File"];
991 992
    if (!request)
        return;
Pierre's avatar
Pierre committed
993
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && hasFetchedInfo == 0"]];
994
    results = [moc executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
995
    for (MLFile *file in results)
996
        [[MLFileParserQueue sharedFileParserQueue] addFile:file];
Pierre's avatar
Pierre committed
997

Pierre's avatar
Pierre committed
998
    // Get to fetch show info
Pierre's avatar
Pierre committed
999
    request = [self fetchRequestForEntity:@"Show"];
1000 1001
    if (!request)
        return;
Pierre's avatar
Pierre committed
1002
    [request setPredicate:[NSPredicate predicateWithFormat:@"lastSyncDate == 0"]];
1003
    results = [moc executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
1004
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
1005 1006
        [self fetchMetaDataForShow:show];

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

Pierre's avatar
Pierre committed
1011
    [MLTVShowInfoGrabber fetchUpdatesSinceServerTime:lastServerTime andExecuteBlock:^(NSArray *updates){
Pierre's avatar
Pierre committed
1012
        NSFetchRequest *request = [self fetchRequestForEntity:@"Show"];
1013 1014
        if (!request)
            return;
Pierre's avatar
Pierre committed
1015
        [request setPredicate:[NSComparisonPredicate predicateWithLeftExpression:[NSExpression expressionForKeyPath:@"theTVDBID"] rightExpression:[NSExpression expressionForConstantValue:updates] modifier:NSDirectPredicateModifier type:NSInPredicateOperatorType options:0]];
1016
        NSArray *results = [moc executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
1017
        for (MLShow *show in results)
Pierre's avatar
Pierre committed
1018 1019
            [self fetchMetaDataForShow:show];
    }];
Pierre's avatar
Pierre committed
1020
#endif
Pierre's avatar
Pierre committed
1021
    /* Update every hour - FIXME: Preferences key */
1022
    [self performSelector:@selector(updateMediaDatabase) withObject:nil afterDelay:60 * 60];
Pierre's avatar
Pierre committed
1023
}
1024
#endif
Pierre's avatar
Pierre committed
1025

1026 1027
- (void)applicationWillExit
{
1028
    [[MLFileParserQueue sharedFileParserQueue] stop];
1029 1030 1031
    [[MLCrashPreventer sharedPreventer] cancelAllFileParse];
}

Pierre's avatar
Pierre committed
1032 1033 1034
- (void)applicationWillStart
{
    [[MLCrashPreventer sharedPreventer] markCrasherFiles];
1035
    [[MLFileParserQueue sharedFileParserQueue] resume];
Pierre's avatar
Pierre committed
1036 1037
}

Pierre's avatar
Pierre committed
1038 1039 1040 1041
- (void)libraryDidDisappear
{
    // Stop expansive work
    [[MLThumbnailerQueue sharedThumbnailerQueue] stop];
1042
    [[MLFileParserQueue sharedFileParserQueue] stop];
Pierre's avatar
Pierre committed
1043 1044 1045 1046 1047 1048
}

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

1052
#pragma mark - migrations
1053

1054 1055 1056 1057 1058 1059 1060
- (BOOL)libraryMigrationNeeded
{
    return [self _libraryMigrationNeeded];
}
- (void)migrateLibrary
{
    [self _migrateLibrary];
1061 1062
}

Pierre's avatar
Pierre committed
1063
@end