MLMediaLibrary.m 39.4 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 11
 * $Id$
 *
 * Authors: Pierre d'Herbemont <pdherbemont # videolan.org>
 *          Felix Paul Kühne <fkuehne # videolan.org>
 *
12 13 14
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
15 16 17 18
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
21
 *
22 23 24
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
25
 *****************************************************************************/
Pierre's avatar
Pierre committed
26 27

#import "MLMediaLibrary.h"
Pierre's avatar
Pierre committed
28 29
#import "MLTitleDecrapifier.h"
#import "MLMovieInfoGrabber.h"
Pierre's avatar
Pierre committed
30 31
#import "MLTVShowInfoGrabber.h"
#import "MLTVShowEpisodesInfoGrabber.h"
Pierre's avatar
Pierre committed
32 33 34 35
#import "MLFile.h"
#import "MLLabel.h"
#import "MLShowEpisode.h"
#import "MLShow.h"
36 37 38
#import "MLThumbnailerQueue.h"
#import "MLAlbumTrack.h"
#import "MLAlbum.h"
39
#import "MLFileParserQueue.h"
40
#import "MLCrashPreventer.h"
Pierre's avatar
Pierre committed
41

42 43
@interface MLMediaLibrary ()
{
44 45 46 47 48
    NSManagedObjectContext *_managedObjectContext;
    NSManagedObjectModel   *_managedObjectModel;

    BOOL _allowNetworkAccess;

49 50
    NSString *_thumbnailFolderPath;
    NSString *_databaseFolderPath;
51
    NSString *_documentFolderPath;
52
    NSString *_libraryBasePath;
53 54 55
}
@end

Pierre's avatar
Pierre committed
56
#define DEBUG 1
57 58
// To debug
#define DELETE_LIBRARY_ON_EACH_LAUNCH 0
Pierre's avatar
Pierre committed
59

Pierre's avatar
Pierre committed
60 61
// Pref key
static NSString *kLastTVDBUpdateServerTime = @"MLLastTVDBUpdateServerTime";
62
static NSString *kUpdatedToTheGreatSharkHuntDatabaseFormat = @"upgradedToDatabaseFormat 2.3";
63
static NSString *kDecrapifyTitles = @"MLDecrapifyTitles";
Pierre's avatar
Pierre committed
64

Pierre's avatar
Pierre committed
65
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
66
@interface MLMediaLibrary ()
Pierre's avatar
Pierre committed
67 68 69
#else
@interface MLMediaLibrary () <MLMovieInfoGrabberDelegate, MLTVShowEpisodesInfoGrabberDelegate, MLTVShowInfoGrabberDelegate>
#endif
Pierre's avatar
Pierre committed
70
- (NSManagedObjectContext *)managedObjectContext;
Pierre's avatar
Pierre committed
71
- (NSString *)databaseFolderPath;
Pierre's avatar
Pierre committed
72 73
@end

Pierre's avatar
Pierre committed
74
@implementation MLMediaLibrary
75 76
+ (void)initialize
{
77
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
78
    [defaults registerDefaults:@{kUpdatedToTheGreatSharkHuntDatabaseFormat : @NO, kDecrapifyTitles : @YES}];
79 80
}

Pierre's avatar
Pierre committed
81 82 83
+ (id)sharedMediaLibrary
{
    static id sharedMediaLibrary = nil;
Pierre's avatar
Pierre committed
84
    if (!sharedMediaLibrary) {
Pierre's avatar
Pierre committed
85
        sharedMediaLibrary = [[[self class] alloc] init];
86
        APLog(@"Initializing db in %@", [sharedMediaLibrary databaseFolderPath]);
87 88 89 90

        // 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
91
    }
Pierre's avatar
Pierre committed
92 93 94
    return sharedMediaLibrary;
}

95 96 97 98 99 100
- (void)dealloc
{
    if (_managedObjectContext)
        [_managedObjectContext removeObserver:self forKeyPath:@"hasChanges"];
}

Pierre's avatar
Pierre committed
101 102 103 104
- (NSFetchRequest *)fetchRequestForEntity:(NSString *)entity
{
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    NSManagedObjectContext *moc = [self managedObjectContext];
105 106 107
    if (!moc || moc.persistentStoreCoordinator == nil)
        return nil;

Pierre's avatar
Pierre committed
108
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:entity inManagedObjectContext:moc];
Pierre's avatar
Pierre committed
109
    NSAssert(entityDescription, @"No entity");
Pierre's avatar
Pierre committed
110
    [request setEntity:entityDescription];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
111
    return request;
Pierre's avatar
Pierre committed
112 113 114 115 116
}

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

Pierre's avatar
Pierre committed
120 121 122
    return [NSEntityDescription insertNewObjectForEntityForName:entity inManagedObjectContext:moc];
}

123 124
- (void)removeObject:(NSManagedObject *)object
{
125 126 127 128
    NSManagedObjectContext *moc = [self managedObjectContext];

    if (moc)
        [[self managedObjectContext] deleteObject:object];
129 130
}

Pierre's avatar
Pierre committed
131 132 133 134 135 136
#pragma mark -
#pragma mark Media Library
- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel)
        return _managedObjectModel;
137

138
    NSString *path = [[NSBundle mainBundle] pathForResource:@"MediaLibrary" ofType:@"momd"];
139 140 141
    NSURL *momURL = [NSURL fileURLWithPath:path];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];

Pierre's avatar
Pierre committed
142 143 144
    return _managedObjectModel;
}

145
- (NSString *)libraryBasePath
Pierre's avatar
Pierre committed
146
{
147 148 149 150
    if (_libraryBasePath.length == 0) {
        int directory = NSLibraryDirectory;
        NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
        NSString *directoryPath = paths.firstObject;
151
#if DELETE_LIBRARY_ON_EACH_LAUNCH
152
        [[NSFileManager defaultManager] removeItemAtPath:directoryPath error:nil];
153
#endif
154 155 156 157 158
        _libraryBasePath = directoryPath;
    }
    return _libraryBasePath;
}

159 160 161 162 163 164 165 166
- (void)setLibraryBasePath:(NSString *)libraryBasePath
{
    _libraryBasePath = [libraryBasePath copy];
    _databaseFolderPath = nil;
    _thumbnailFolderPath = nil;
    _persistentStoreURL = nil;
}

167 168 169 170 171
- (NSString *)databaseFolderPath
{
    if (_databaseFolderPath.length == 0) {
        _databaseFolderPath = self.libraryBasePath;
    }
172
    return _databaseFolderPath;
Pierre's avatar
Pierre committed
173 174
}

Pierre's avatar
Pierre committed
175 176
- (NSString *)thumbnailFolderPath
{
177 178
    if (_thumbnailFolderPath.length == 0) {
        _thumbnailFolderPath = [self.libraryBasePath stringByAppendingPathComponent:@"Thumbnails"];
179 180
    }
    return _thumbnailFolderPath;
Pierre's avatar
Pierre committed
181 182
}

183 184 185 186 187 188 189 190
- (NSString *)documentFolderPath
{
    if (_documentFolderPath) {
        if (_documentFolderPath.length > 0)
            return _documentFolderPath;
    }
    int directory = NSDocumentDirectory;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
191
    _documentFolderPath = [NSString stringWithFormat:@"file://%@", paths[0]];
192 193 194
    return _documentFolderPath;
}

195 196 197 198 199 200 201 202
- (NSURL *)persistentStoreURL
{
    if (_persistentStoreURL == nil) {
        NSString *databaseFolderPath = [self databaseFolderPath];
        NSString *path = [databaseFolderPath stringByAppendingPathComponent: @"MediaLibrary.sqlite"];
        _persistentStoreURL = [NSURL fileURLWithPath:path];
    }
    return _persistentStoreURL;
203 204
}

205 206
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
207

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

210
    NSNumber *yes = @YES;
211
    NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : yes,
212
                              NSInferMappingModelAutomaticallyOption : yes};
213 214 215 216 217
    if (self.additionalPersitentStoreOptions.count > 0) {
        NSMutableDictionary *mutableOptions = options.mutableCopy;
        [mutableOptions addEntriesFromDictionary:self.additionalPersitentStoreOptions];
        options = mutableOptions;
    }
Pierre's avatar
Pierre committed
218

219 220 221 222 223 224 225
    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
226
    NSError *error;
227
    NSPersistentStore *persistentStore = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:self.persistentStoreURL options:options error:&error];
Pierre's avatar
Pierre committed
228 229

    if (!persistentStore) {
Pierre's avatar
Pierre committed
230
#if! TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
231 232 233 234
        // 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];
235
        [[NSFileManager defaultManager] removeItemAtPath:self.persistentStoreURL.path error:nil];
Pierre's avatar
Pierre committed
236
#else
237
        [[NSFileManager defaultManager] removeItemAtPath:self.persistentStoreURL.path error:nil];
Pierre's avatar
Pierre committed
238
#endif
239
        persistentStore = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:self.persistentStoreURL options:options error:&error];
Pierre's avatar
Pierre committed
240 241 242 243 244 245 246 247 248 249
        if (!persistentStore) {
#if! TARGET_OS_IPHONE
            NSRunInformationalAlertPanel(@"Corrupted Media Library", @"There is nothing we can apparently do about it...", @"OK", nil, nil);
#else
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Corrupted Media Library" message:@"There is nothing we can apparently do about it..." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
            [alert show];
#endif
            // Probably assert instead.
            return nil;
        }
Pierre's avatar
Pierre committed
250
    }
251 252 253 254 255 256 257
    return coordinator;
}

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

    _managedObjectContext = [[NSManagedObjectContext alloc] init];
260
    [_managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
261 262
    if (_managedObjectContext.persistentStoreCoordinator == nil)
        return nil;
Pierre's avatar
Pierre committed
263 264 265 266 267 268 269 270
    [_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
271
    NSError *error = nil;
272 273 274 275
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;

Pierre's avatar
Pierre committed
276 277
    BOOL success = [[self managedObjectContext] save:&error];
    NSAssert1(success, @"Can't save: %@", error);
Pierre's avatar
Pierre committed
278
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
279 280 281 282 283 284
    NSProcessInfo *process = [NSProcessInfo processInfo];
    if ([process respondsToSelector:@selector(enableSuddenTermination)])
        [process enableSuddenTermination];
#endif
}

285 286 287
- (void)save
{
    NSError *error = nil;
288 289 290 291 292
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;

    BOOL success = [moc save:&error];
293 294 295
    NSAssert1(success, @"Can't save: %@", error);
}

Pierre's avatar
Pierre committed
296
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
Pierre's avatar
Pierre committed
297 298
{
    if ([keyPath isEqualToString:@"hasChanges"] && object == _managedObjectContext) {
Pierre's avatar
Pierre committed
299
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
300 301 302 303 304
        NSProcessInfo *process = [NSProcessInfo processInfo];
        if ([process respondsToSelector:@selector(disableSuddenTermination)])
            [process disableSuddenTermination];
#endif

Pierre's avatar
Pierre committed
305 306 307 308
        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
309 310 311 312 313
        return;
    }
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

314 315 316 317 318 319 320 321 322
- (NSManagedObject *)objectForURIRepresentation:(NSURL *)uriRepresenation {
    if (uriRepresenation == nil) {
        return nil;
    }
    NSManagedObjectID *objectID = [self.persistentStoreCoordinator managedObjectIDForURIRepresentation:uriRepresenation];
    NSManagedObject *managedObject = [self.managedObjectContext objectWithID:objectID];
    return managedObject;
}

Pierre's avatar
Pierre committed
323 324
#pragma mark -
#pragma mark No meta data fallbacks
Pierre's avatar
Pierre committed
325

Pierre's avatar
Pierre committed
326
- (void)computeThumbnailForFile:(MLFile *)file
Pierre's avatar
Pierre committed
327
{
Pierre's avatar
Pierre committed
328
    if (!file.computedThumbnail) {
329
        APLog(@"Computing thumbnail for %@", file.title);
Pierre's avatar
Pierre committed
330 331
        [[MLThumbnailerQueue sharedThumbnailerQueue] addFile:file];
    }
Pierre's avatar
Pierre committed
332
}
Pierre's avatar
Pierre committed
333
- (void)errorWhenFetchingMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
334
{
335
    APLog(@"Error when fetching for '%@'", file.title);
Pierre's avatar
Pierre committed
336

Pierre's avatar
Pierre committed
337 338
    [self computeThumbnailForFile:file];
}
Pierre's avatar
Pierre committed
339

Pierre's avatar
Pierre committed
340 341 342 343 344 345 346
- (void)errorWhenFetchingMetaDataForShow:(MLShow *)show
{
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self errorWhenFetchingMetaDataForFile:file];
    }
}
Pierre's avatar
Pierre committed
347

Pierre's avatar
Pierre committed
348 349
- (void)noMetaDataInRemoteDBForFile:(MLFile *)file
{
350
    file.noOnlineMetaData = @YES;
Pierre's avatar
Pierre committed
351
    [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
352 353
}

Pierre's avatar
Pierre committed
354
- (void)noMetaDataInRemoteDBForShow:(MLShow *)show
Pierre's avatar
Pierre committed
355
{
Pierre's avatar
Pierre committed
356 357 358
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self noMetaDataInRemoteDBForFile:file];
Pierre's avatar
Pierre committed
359 360 361
    }
}

Pierre's avatar
Pierre committed
362 363 364 365
#pragma mark -
#pragma mark Getter

- (void)addNewLabelWithName:(NSString *)name
Pierre's avatar
Pierre committed
366
{
Pierre's avatar
Pierre committed
367 368
    MLLabel *label = [self createObjectForEntity:@"Label"];
    label.name = name;
Pierre's avatar
Pierre committed
369 370
}

Pierre's avatar
Pierre committed
371 372 373 374 375 376 377
/**
 * TV MLShow Episodes
 */

#pragma mark -
#pragma mark Online meta data grabbing

Pierre's avatar
Pierre committed
378 379 380
#if !HAVE_BLOCK
- (void)tvShowEpisodesInfoGrabberDidFinishGrabbing:(MLTVShowEpisodesInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
381
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
382 383

    NSArray *results = grabber.episodesResults;
384
    [show setValue:(grabber.results)[@"serieArtworkURL"] forKey:@"artworkURL"];
Pierre's avatar
Pierre committed
385
    for (id result in results) {
386
        if ([result[@"serie"] boolValue]) {
Pierre's avatar
Pierre committed
387 388
            continue;
        }
389 390 391 392 393
        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
394 395 396 397 398
        if (!showEpisode.artworkURL) {
            for (MLFile *file in showEpisode.files)
                [self computeThumbnailForFile:file];
        }

Pierre's avatar
Pierre committed
399 400 401 402 403
        showEpisode.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
    show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
}

Pierre's avatar
Pierre committed
404 405 406 407 408 409
- (void)tvShowEpisodesInfoGrabber:(MLTVShowEpisodesInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
410 411
- (void)tvShowInfoGrabberDidFinishGrabbing:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
412
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
413 414
    NSArray *results = grabber.results;
    if ([results count] > 0) {
415 416
        NSDictionary *result = results[0];
        NSString *showId = result[@"id"];
Pierre's avatar
Pierre committed
417 418

        show.theTVDBID = showId;
419 420 421
        show.name = result[@"title"];
        show.shortSummary = result[@"shortSummary"];
        show.releaseYear = result[@"releaseYear"];
Pierre's avatar
Pierre committed
422 423

        // Fetch episodes info
Felix Paul Kühne's avatar
Felix Paul Kühne committed
424
        MLTVShowEpisodesInfoGrabber *grabber = [[MLTVShowEpisodesInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
425 426 427 428 429 430
        grabber.delegate = self;
        grabber.userData = show;
        [grabber lookUpForShowID:showId];
    }
    else {
        // Not found.
Pierre's avatar
Pierre committed
431
        [self noMetaDataInRemoteDBForShow:show];
Pierre's avatar
Pierre committed
432 433 434 435
        show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
}

Pierre's avatar
Pierre committed
436 437 438 439 440 441
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
442 443
- (void)tvShowInfoGrabberDidFetchServerTime:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
444
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
445 446 447

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

Pierre's avatar
Pierre committed
448
    // First fetch the MLShow ID
Felix Paul Kühne's avatar
Felix Paul Kühne committed
449
    MLTVShowInfoGrabber *showInfoGrabber = [[MLTVShowInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
450 451 452
    showInfoGrabber.delegate = self;
    showInfoGrabber.userData = show;

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

Pierre's avatar
Pierre committed
455 456 457 458
    [showInfoGrabber lookUpForTitle:show.name];
}
#endif

Pierre's avatar
Pierre committed
459
- (void)fetchMetaDataForShow:(MLShow *)show
Pierre's avatar
Pierre committed
460
{
Pierre's avatar
Pierre committed
461 462
    if (!_allowNetworkAccess)
        return;
463
    APLog(@"Fetching show server time");
Pierre's avatar
Pierre committed
464

Pierre's avatar
Pierre committed
465
    // First fetch the serverTime, so that we can update each entry.
Pierre's avatar
Pierre committed
466 467
#if HAVE_BLOCK
    [MLTVShowInfoGrabber fetchServerTimeAndExecuteBlock:^(NSNumber *serverDate) {
Pierre's avatar
Pierre committed
468 469 470

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

471
        APLog(@"Fetching show information on %@", show.name);
Pierre's avatar
Pierre committed
472 473 474

        // First fetch the MLShow ID
        MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
475 476 477 478 479 480 481 482 483 484 485
        [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"];

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

Pierre's avatar
Pierre committed
488
                // Fetch episode info
Pierre's avatar
Pierre committed
489
                MLTVShowEpisodesInfoGrabber *grabber = [[[MLTVShowEpisodesInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
490 491 492 493 494 495 496
                [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;
                        }
497
                        MLShowEpisode *showEpisode = [MLShowEpisode episodeWithShow:show episodeNumber:[result objectForKey:@"episodeNumber"] seasonNumber:[result objectForKey:@"seasonNumber"] createIfNeeded:YES];
Pierre's avatar
Pierre committed
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513
                        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
514
#else
Felix Paul Kühne's avatar
Felix Paul Kühne committed
515
    MLTVShowInfoGrabber *grabber = [[MLTVShowInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
516 517 518
    grabber.delegate = self;
    grabber.userData = show;
    [grabber fetchServerTime];
Pierre's avatar
Pierre committed
519
#endif
Pierre's avatar
Pierre committed
520 521
}

Pierre's avatar
Pierre committed
522
- (void)addTVShowEpisodeWithInfo:(NSDictionary *)tvShowEpisodeInfo andFile:(MLFile *)file
Pierre's avatar
Pierre committed
523
{
Pierre's avatar
Pierre committed
524
    file.type = kMLFileTypeTVShowEpisode;
Pierre's avatar
Pierre committed
525

526 527 528
    NSNumber *seasonNumber = tvShowEpisodeInfo[@"season"];
    NSNumber *episodeNumber = tvShowEpisodeInfo[@"episode"];
    NSString *tvShowName = tvShowEpisodeInfo[@"tvShowName"];
529
    NSString *tvEpisodeName = tvShowEpisodeInfo[@"tvEpisodeName"];
Pierre's avatar
Pierre committed
530 531
    BOOL hasNoTvShow = NO;
    if (!tvShowName) {
532
        tvShowName = @"";
Pierre's avatar
Pierre committed
533 534 535
        hasNoTvShow = YES;
    }
    BOOL wasInserted = NO;
Pierre's avatar
Pierre committed
536 537
    MLShow *show = nil;
    MLShowEpisode *episode = [MLShowEpisode episodeWithShowName:tvShowName episodeNumber:episodeNumber seasonNumber:seasonNumber createIfNeeded:YES wasCreated:&wasInserted];
538 539

    if (episode) {
Pierre's avatar
Pierre committed
540
        show = episode.show;
541 542
        [show addEpisode:episode];
    }
Pierre's avatar
Pierre committed
543 544 545 546
    if (wasInserted && !hasNoTvShow) {
        show.name = tvShowName;
        [self fetchMetaDataForShow:show];
    }
547
    episode.name = tvEpisodeName;
Pierre's avatar
Pierre committed
548

Felix Paul Kühne's avatar
Felix Paul Kühne committed
549
    if (episode.name.length < 1)
Pierre's avatar
Pierre committed
550 551 552
        episode.name = file.title;
    file.seasonNumber = seasonNumber;
    file.episodeNumber = episodeNumber;
553
    episode.shouldBeDisplayed = @YES;
Pierre's avatar
Pierre committed
554 555

    [episode addFilesObject:file];
Pierre's avatar
Pierre committed
556
    file.showEpisode = episode;
Pierre's avatar
Pierre committed
557

Pierre's avatar
Pierre committed
558
    // The rest of the meta data will be fetched using the MLShow
559
    file.hasFetchedInfo = @YES;
Pierre's avatar
Pierre committed
560 561
}

562 563 564 565
- (void)addAudioContentWithInfo:(NSDictionary *)audioContentInfo andFile:(MLFile *)file
{
    file.type = kMLFileTypeAudio;

566 567 568
    file.title = audioContentInfo[VLCMetaInformationTitle];

    /* all further meta data is set by the FileParserQueue */
569 570 571 572

    file.hasFetchedInfo = @YES;
}

Pierre's avatar
Pierre committed
573
/**
Pierre's avatar
Pierre committed
574
 * MLFile auto detection
Pierre's avatar
Pierre committed
575 576
 */

Pierre's avatar
Pierre committed
577
#if !HAVE_BLOCK
Pierre's avatar
Pierre committed
578 579 580 581 582 583
- (void)movieInfoGrabber:(MLMovieInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLFile *file = grabber.userData;
    [self errorWhenFetchingMetaDataForFile:file];
}

Pierre's avatar
Pierre committed
584 585
- (void)movieInfoGrabberDidFinishGrabbing:(MLMovieInfoGrabber *)grabber
{
586
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
587 588

    NSArray *results = grabber.results;
Pierre's avatar
Pierre committed
589
    MLFile *file = grabber.userData;
Pierre's avatar
Pierre committed
590
    if ([results count] > 0) {
591 592 593 594 595
        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
596
    }
Pierre's avatar
Pierre committed
597 598 599 600
    else {
        [self noMetaDataInRemoteDBForFile:file];
    }

Pierre's avatar
Pierre committed
601 602 603 604
    file.hasFetchedInfo = yes;
}
#endif

Pierre's avatar
Pierre committed
605
- (void)fetchMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
606
{
607
    APLog(@"Fetching meta data for %@", file.title);
Pierre's avatar
Pierre committed
608

609
    [[MLFileParserQueue sharedFileParserQueue] addFile:file];
610

Pierre's avatar
Pierre committed
611 612 613 614 615
    if (!_allowNetworkAccess) {
        // Automatically compute the thumbnail
        [self computeThumbnailForFile:file];
    }

Pierre's avatar
Pierre committed
616
    NSDictionary *tvShowEpisodeInfo = [MLTitleDecrapifier tvShowEpisodeInfoFromString:file.title];
Pierre's avatar
Pierre committed
617 618 619 620 621
    if (tvShowEpisodeInfo) {
        [self addTVShowEpisodeWithInfo:tvShowEpisodeInfo andFile:file];
        return;
    }

622 623 624 625 626 627 628 629
    if ([file isSupportedAudioFile]) {
        NSDictionary *audioContentInfo = [MLTitleDecrapifier audioContentInfoFromFile:file];
        if (audioContentInfo && ![file videoTrack]) {
            [self addAudioContentWithInfo:audioContentInfo andFile:file];
            return;
        }
    }

Pierre's avatar
Pierre committed
630 631 632
    if (!_allowNetworkAccess)
        return;

Pierre's avatar
Pierre committed
633
    // Go online and fetch info.
Pierre's avatar
Pierre committed
634 635 636

    // We don't care about keeping a reference to track the item during its life span
    // because we are a singleton
Felix Paul Kühne's avatar
Felix Paul Kühne committed
637
    MLMovieInfoGrabber *grabber = [[MLMovieInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
638

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

Pierre's avatar
Pierre committed
641
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
642
    [grabber lookUpForTitle:file.title andExecuteBlock:^(NSError *err){
Pierre's avatar
Pierre committed
643 644
        if (err) {
            [self errorWhenFetchingMetaDataForFile:file];
Pierre's avatar
Pierre committed
645
            return;
Pierre's avatar
Pierre committed
646
        }
Pierre's avatar
Pierre committed
647 648 649 650 651

        NSArray *results = grabber.results;
        if ([results count] > 0) {
            NSDictionary *result = [results objectAtIndex:0];
            file.artworkURL = [result objectForKey:@"artworkURL"];
Pierre's avatar
Pierre committed
652 653
            if (!file.artworkURL)
                [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
654 655 656
            file.title = [result objectForKey:@"title"];
            file.shortSummary = [result objectForKey:@"shortSummary"];
            file.releaseYear = [result objectForKey:@"releaseYear"];
Pierre's avatar
Pierre committed
657 658 659
        } else
            [self noMetaDataInRemoteDBForFile:file];
        file.hasFetchedInfo = [NSNumber numberWithBool:YES];
Pierre's avatar
Pierre committed
660
    }];
Pierre's avatar
Pierre committed
661
#else
Pierre's avatar
Pierre committed
662 663 664
    grabber.userData = file;
    grabber.delegate = self;
    [grabber lookUpForTitle:file.title];
Pierre's avatar
Pierre committed
665
#endif
Pierre's avatar
Pierre committed
666 667
}

Pierre's avatar
Pierre committed
668 669 670
#pragma mark -
#pragma mark Adding file to the DB

Pierre's avatar
Pierre committed
671
- (void)addFilePath:(NSString *)filePath
Pierre's avatar
Pierre committed
672
{
673
    APLog(@"Adding Path %@", filePath);
Pierre's avatar
Pierre committed
674

Pierre's avatar
Pierre committed
675
    NSURL *url = [NSURL fileURLWithPath:filePath];
Pierre's avatar
Pierre committed
676 677
    NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
    NSString *title = [filePath lastPathComponent];
Pierre's avatar
Pierre committed
678
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
679 680
    NSDate *openedDate = nil; // FIXME kMDItemLastUsedDate
    NSDate *modifiedDate = nil; // FIXME [result valueForAttribute:@"kMDItemFSContentChangeDate"];
Pierre's avatar
Pierre committed
681
#endif
682
    NSNumber *size = attributes[NSFileSize]; // FIXME [result valueForAttribute:@"kMDItemFSSize"];
Pierre's avatar
Pierre committed
683

Pierre's avatar
Pierre committed
684
    MLFile *file = [self createObjectForEntity:@"File"];
Pierre's avatar
Pierre committed
685
    file.url = [url absoluteString];
Pierre's avatar
Pierre committed
686 687 688 689

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

690 691
    NSNumber *no = @NO;
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
692 693

    file.currentlyWatching = no;
694 695
    file.lastPosition = @0.0;
    file.remainingTime = @0.0;
Pierre's avatar
Pierre committed
696 697
    file.unread = yes;

Pierre's avatar
Pierre committed
698
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
699 700 701 702
    if ([openedDate isGreaterThan:modifiedDate]) {
        file.playCount = [NSNumber numberWithDouble:1];
        file.unread = no;
    }
Pierre's avatar
Pierre committed
703 704
#endif

705 706 707 708
    if ([[[NSUserDefaults standardUserDefaults] objectForKey:kDecrapifyTitles] boolValue] == YES)
        file.title = [MLTitleDecrapifier decrapify:[title stringByDeletingPathExtension]];
    else
        file.title = [title stringByDeletingPathExtension];
Pierre's avatar
Pierre committed
709 710

    if ([size longLongValue] < 150000000) /* 150 MB */
Pierre's avatar
Pierre committed
711
        file.type = kMLFileTypeClip;
Pierre's avatar
Pierre committed
712
    else
Pierre's avatar
Pierre committed
713
        file.type = kMLFileTypeMovie;
Pierre's avatar
Pierre committed
714 715 716 717

    [self fetchMetaDataForFile:file];
}

Pierre's avatar
Pierre committed
718
- (void)addFilePaths:(NSArray *)filepaths
Pierre's avatar
Pierre committed
719
{
Pierre's avatar
Pierre committed
720
    NSUInteger count = [filepaths count];
Pierre's avatar
Pierre committed
721 722
    NSMutableArray *fetchPredicates = [NSMutableArray arrayWithCapacity:count];
    NSMutableDictionary *urlToObject = [NSMutableDictionary dictionaryWithCapacity:count];
723
    NSString *documentFolderPath = [[MLMediaLibrary sharedMediaLibrary] documentFolderPath];
Pierre's avatar
Pierre committed
724 725

    // Prepare a fetch request for all items
726 727 728
    NSArray *pathComponents;
    NSUInteger componentCount;

Pierre's avatar
Pierre committed
729
    for (NSString *path in filepaths) {
730
#if TARGET_OS_IPHONE
731
        NSString *urlString;
732 733 734 735 736 737 738 739 740 741 742 743 744 745 746
        NSString *componentString = @"";

        pathComponents = [path componentsSeparatedByString:@"/"];
        componentCount = pathComponents.count;
        if ([pathComponents[componentCount - 2] isEqualToString:@"Documents"])
            componentString = [path lastPathComponent];
        else {
            NSUInteger firstElement = [pathComponents indexOfObject:@"Documents"] + 1;
            for (NSUInteger x = 0; x < componentCount - firstElement; x++) {
                if (x == 0)
                    componentString = [componentString stringByAppendingFormat:@"%@", pathComponents[firstElement + x]];
                else
                    componentString = [componentString stringByAppendingFormat:@"/%@", pathComponents[firstElement + x]];
            }
        }
747

748 749 750 751 752 753
        /* compose and escape string */
        urlString = [[NSString stringWithFormat:@"%@/%@", documentFolderPath, componentString] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

        /* check for the end of the paths */
        [fetchPredicates addObject:[NSPredicate predicateWithFormat:@"url CONTAINS %@", [urlString lastPathComponent]]];
        [urlToObject setObject:path forKey:urlString];
754
#else
755
        [fetchPredicates addObject:[NSPredicate predicateWithFormat:@"url == %@", path]];
756
#endif
Pierre's avatar
Pierre committed
757 758 759 760 761
    }
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];

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

762
    APLog(@"Fetching");
763 764 765 766
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;
    NSArray *dbResults = [moc executeFetchRequest:request error:nil];
767
    APLog(@"Done");
Pierre's avatar
Pierre committed
768

Pierre's avatar
Pierre committed
769
    NSMutableArray *filePathsToAdd = [NSMutableArray arrayWithArray:filepaths];
Pierre's avatar
Pierre committed
770 771

    // Remove objects that are already in db.
Pierre's avatar
Pierre committed
772 773
    for (MLFile *dbResult in dbResults) {
        NSString *urlString = dbResult.url;
774
        [filePathsToAdd removeObject:[urlToObject objectForKey:urlString]];
Pierre's avatar
Pierre committed
775 776 777
    }

    // Add only the newly added items
Pierre's avatar
Pierre committed
778 779
    for (NSString* path in filePathsToAdd)
        [self addFilePath:path];
Pierre's avatar
Pierre committed
780 781
}

Pierre's avatar
Pierre committed
782 783 784 785

#pragma mark -
#pragma mark DB Updates

Pierre's avatar
Pierre committed
786 787 788 789 790 791
#if !HAVE_BLOCK
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFetchUpdates:(NSArray *)updates
{
    NSFetchRequest *request = [self fetchRequestForEntity:@"Show"];
    [request setPredicate:[NSComparisonPredicate predicateWithLeftExpression:[NSExpression expressionForKeyPath:@"theTVDBID"] rightExpression:[NSExpression expressionForConstantValue:updates] modifier:NSDirectPredicateModifier type:NSInPredicateOperatorType options:0]];
    NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
792
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
793 794 795 796
        [self fetchMetaDataForShow:show];
}
#endif

797 798
- (BOOL)libraryNeedsUpgrade
{
799
    if (![[[NSUserDefaults standardUserDefaults] objectForKey:kUpdatedToTheGreatSharkHuntDatabaseFormat] boolValue])
800 801 802 803 804
        return YES;
    return NO;
}

- (void)upgradeLibrary
Felix Paul Kühne's avatar
Felix Paul Kühne committed
805 806 807 808 809 810
{
    if (![[[NSUserDefaults standardUserDefaults] objectForKey:kUpdatedToTheGreatSharkHuntDatabaseFormat] boolValue])
        [self _upgradeLibraryToGreatSharkHuntDatabaseFormat];
}

- (void)_upgradeLibraryToGreatSharkHuntDatabaseFormat
811
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
812
    [self libraryDidDisappear];
813 814 815
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSFileManager *fileManager = [NSFileManager defaultManager];

816
    /* remove potential empty albums left over by previous releases */
817 818 819 820 821 822 823 824 825 826
    NSArray *collection = [MLAlbum allAlbums];
    NSUInteger count = collection.count;
    MLAlbum *album;
    MLAlbumTrack *track;
    NSArray *secondaryCollection;
    NSURL *fileURL;
    NSUInteger secondaryCount = 0;
    NSArray *tertiaryCollection;
    NSUInteger tertiaryCount = 0;
    NSUInteger emptyAlbumCounter = 0;
827 828 829 830 831 832 833
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc) {
        [self libraryDidAppear];
        if ([self.delegate respondsToSelector:@selector(libraryUpgradeComplete)])
            [self.delegate libraryUpgradeComplete];
        return;
    }
834 835 836
    for (NSUInteger x = 0; x < count; x++) {
        album = collection[x];
        if (album.tracks.count < 1)
837
            [moc deleteObject:album];
838 839 840 841 842 843 844 845 846 847 848 849 850
        else {
            secondaryCollection = album.tracks.allObjects;
            secondaryCount = secondaryCollection.count;
            emptyAlbumCounter = 0;
            for (NSUInteger y = 0; y < secondaryCount; y++) {
                track = secondaryCollection[y];
                tertiaryCollection = track.files.allObjects;
                tertiaryCount = tertiaryCollection.count;
                for (NSUInteger z = 0; z < tertiaryCount; z++) {
                    fileURL = [NSURL URLWithString:[(MLFile *)tertiaryCollection[z] url]];
                    BOOL exists = [fileManager fileExistsAtPath:[fileURL path]];
                    if (exists)
                        emptyAlbumCounter++;
851 852
                    else
                        [album removeTrack:track];
853 854 855
                }
            }
            if (emptyAlbumCounter == 0)
856
                [moc deleteObject:album];
857 858 859
        }
    }
    album = nil;
860 861

    /* remove potential empty shows left over by previous releases */
862 863 864 865 866 867 868
    collection = [MLShow allShows];
    MLShow *show;
    MLShowEpisode *showEpisode;
    count = collection.count;
    for (NSUInteger x = 0; x < count; x++) {
        show = collection[x];
        if (show.episodes.count < 1)
869
            [moc deleteObject:show];
870 871 872 873 874 875 876 877 878 879 880 881 882
        else {
            secondaryCollection = show.episodes.allObjects;
            secondaryCount = secondaryCollection.count;
            emptyAlbumCounter = 0;
            for (NSUInteger y = 0; y < secondaryCount; y++) {
                showEpisode = secondaryCollection[y];
                tertiaryCollection = showEpisode.files.allObjects;
                tertiaryCount = tertiaryCollection.count;
                for (NSUInteger z = 0; z < tertiaryCount; z++) {
                    fileURL = [NSURL URLWithString:[(MLFile *)tertiaryCollection[z] url]];
                    BOOL exists = [fileManager fileExistsAtPath:[fileURL path]];
                    if (exists)
                        emptyAlbumCounter++;
883 884
                    else
                        [show removeEpisode:showEpisode];
885 886 887
                }
            }
            if (emptyAlbumCounter == 0)
888
                [moc deleteObject:show];
889 890 891
        }
    }

892 893 894 895 896
    /* remove duplicates */
    NSArray *allFiles = [MLFile allFiles];
    NSUInteger allFilesCount = allFiles.count;
    NSMutableArray *seenFiles = [[NSMutableArray alloc] initWithCapacity:allFilesCount];
    MLFile *currentFile;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
897 898
    NSString *currentFilePath;
    for (NSUInteger x = 0; x < allFilesCount; x++) {
899
        currentFile = allFiles[x];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
900 901
        currentFilePath = [currentFile.url stringByReplacingOccurrencesOfString:@"/localhost/" withString:@"//"];
        if ([seenFiles containsObject:currentFilePath])
902
            [moc deleteObject:currentFile];
903
        else
Felix Paul Kühne's avatar
Felix Paul Kühne committed
904
            [seenFiles addObject:currentFilePath];
905 906
    }

907
    [defaults setBool:YES forKey:kUpdatedToTheGreatSharkHuntDatabaseFormat];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
908 909 910
    [defaults synchronize];

    [self libraryDidAppear];
911 912 913 914
    if ([self.delegate respondsToSelector:@selector(libraryUpgradeComplete)])
        [self.delegate libraryUpgradeComplete];
}

915
- (void)updateMediaDatabase
Pierre's avatar
Pierre committed
916
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
917
    [self libraryDidDisappear];
Pierre's avatar
Pierre committed
918
    // Remove no more present files
Pierre's avatar
Pierre committed
919
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];
920 921 922 923
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;
    NSArray *results = [moc executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
924
    NSFileManager *fileManager = [NSFileManager defaultManager];
925

Felix Paul Kühne's avatar
Felix Paul Kühne committed
926
    unsigned int count = (unsigned int)results.count;
927 928
    for (unsigned int x = 0; x < count; x++) {
        MLFile *file = results[x];
Pierre's avatar
Pierre committed
929 930
        NSString *urlString = [file url];
        NSURL *fileURL = [NSURL URLWithString:urlString];
931
        BOOL exists = [fileManager fileExistsAtPath:[fileURL path]];
932
        if (!exists) {
933
            APLog(@"Marking - %@", [fileURL absoluteString]);
934
            file.isSafe = YES; // It doesn't exist, it's safe.
935 936
            if (file.isAlbumTrack) {
                MLAlbum *album = file.albumTrack.album;
937 938 939 940 941 942 943 944 945 946 947
                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];
                }
948 949 950
            }
            if (file.isShowEpisode) {
                MLShow *show = file.showEpisode.show;
951 952 953 954 955 956 957 958 959 960 961
                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];
                }
962
            }
963
#if TARGET_OS_IPHONE
964
            NSString *thumbPath = [file thumbnailPath];
965 966
            bool thumbExists = [fileManager fileExistsAtPath:thumbPath];
            if (thumbExists)
967
                [fileManager removeItemAtPath:thumbPath error:nil];
968
            [moc deleteObject:file];
969
#endif
970
        }
971 972 973
#if !TARGET_OS_IPHONE
    file.isOnDisk = @(exists);
#endif
Pierre's avatar
Pierre committed
974
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
975
    [self libraryDidAppear];
Pierre's avatar
Pierre committed
976

977 978 979
    // Get the file to parse
    request = [self fetchRequestForEntity:@"File"];
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && tracks.@count == 0"]];
980
    results = [moc executeFetchRequest:request error:nil];
981 982 983
    for (MLFile *file in results)
        [[MLFileParserQueue sharedFileParserQueue] addFile:file];

Pierre's avatar
Pierre committed
984 985 986
    if (!_allowNetworkAccess) {
        // Always attempt to fetch
        request = [self fetchRequestForEntity:@"File"];
Pierre's avatar
Pierre committed
987
        [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES"]];