MLMediaLibrary.m 40.8 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"
41
#import <sys/sysctl.h> // for sysctlbyname
Pierre's avatar
Pierre committed
42

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

    BOOL _allowNetworkAccess;
49
    int _deviceSpeedCategory;
50

51 52
    NSString *_thumbnailFolderPath;
    NSString *_databaseFolderPath;
53
    NSString *_documentFolderPath;
54
    NSString *_libraryBasePath;
55 56 57
}
@end

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

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

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

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

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

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

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

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

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

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

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

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

    if (moc)
        [[self managedObjectContext] deleteObject:object];
131 132
}

133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
#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
164 165 166 167 168 169
#pragma mark -
#pragma mark Media Library
- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel)
        return _managedObjectModel;
170

171
    NSString *path = [[NSBundle mainBundle] pathForResource:@"MediaLibrary" ofType:@"momd"];
172 173 174
    NSURL *momURL = [NSURL fileURLWithPath:path];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];

Pierre's avatar
Pierre committed
175 176 177
    return _managedObjectModel;
}

178
- (NSString *)libraryBasePath
Pierre's avatar
Pierre committed
179
{
180 181 182 183
    if (_libraryBasePath.length == 0) {
        int directory = NSLibraryDirectory;
        NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
        NSString *directoryPath = paths.firstObject;
184
#if DELETE_LIBRARY_ON_EACH_LAUNCH
185
        [[NSFileManager defaultManager] removeItemAtPath:directoryPath error:nil];
186
#endif
187 188 189 190 191
        _libraryBasePath = directoryPath;
    }
    return _libraryBasePath;
}

192 193 194 195 196 197 198 199
- (void)setLibraryBasePath:(NSString *)libraryBasePath
{
    _libraryBasePath = [libraryBasePath copy];
    _databaseFolderPath = nil;
    _thumbnailFolderPath = nil;
    _persistentStoreURL = nil;
}

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

Pierre's avatar
Pierre committed
208 209
- (NSString *)thumbnailFolderPath
{
210 211
    if (_thumbnailFolderPath.length == 0) {
        _thumbnailFolderPath = [self.libraryBasePath stringByAppendingPathComponent:@"Thumbnails"];
212 213
    }
    return _thumbnailFolderPath;
Pierre's avatar
Pierre committed
214 215
}

216 217 218 219 220 221 222 223
- (NSString *)documentFolderPath
{
    if (_documentFolderPath) {
        if (_documentFolderPath.length > 0)
            return _documentFolderPath;
    }
    int directory = NSDocumentDirectory;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
224
    _documentFolderPath = [NSString stringWithFormat:@"file://%@", paths[0]];
225 226 227
    return _documentFolderPath;
}

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

238 239
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
240

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

243
    NSNumber *yes = @YES;
244
    NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : yes,
245
                              NSInferMappingModelAutomaticallyOption : yes};
246 247 248 249 250
    if (self.additionalPersitentStoreOptions.count > 0) {
        NSMutableDictionary *mutableOptions = options.mutableCopy;
        [mutableOptions addEntriesFromDictionary:self.additionalPersitentStoreOptions];
        options = mutableOptions;
    }
Pierre's avatar
Pierre committed
251

252 253 254 255 256 257 258
    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
259
    NSError *error;
260
    NSPersistentStore *persistentStore = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:self.persistentStoreURL options:options error:&error];
Pierre's avatar
Pierre committed
261 262

    if (!persistentStore) {
Pierre's avatar
Pierre committed
263
#if! TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
264 265 266 267
        // 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];
268
        [[NSFileManager defaultManager] removeItemAtPath:self.persistentStoreURL.path error:nil];
Pierre's avatar
Pierre committed
269
#else
270
        [[NSFileManager defaultManager] removeItemAtPath:self.persistentStoreURL.path error:nil];
Pierre's avatar
Pierre committed
271
#endif
272
        persistentStore = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:self.persistentStoreURL options:options error:&error];
Pierre's avatar
Pierre committed
273 274 275 276 277 278 279 280 281 282
        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
283
    }
284 285 286 287 288 289 290
    return coordinator;
}

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

    _managedObjectContext = [[NSManagedObjectContext alloc] init];
293
    [_managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
294 295
    if (_managedObjectContext.persistentStoreCoordinator == nil)
        return nil;
Pierre's avatar
Pierre committed
296 297 298 299 300 301 302 303
    [_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
304
    NSError *error = nil;
305 306 307 308
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;

Pierre's avatar
Pierre committed
309 310
    BOOL success = [[self managedObjectContext] save:&error];
    NSAssert1(success, @"Can't save: %@", error);
Pierre's avatar
Pierre committed
311
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
312 313 314 315 316 317
    NSProcessInfo *process = [NSProcessInfo processInfo];
    if ([process respondsToSelector:@selector(enableSuddenTermination)])
        [process enableSuddenTermination];
#endif
}

318 319 320
- (void)save
{
    NSError *error = nil;
321 322 323 324 325
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;

    BOOL success = [moc save:&error];
326 327 328
    NSAssert1(success, @"Can't save: %@", error);
}

Pierre's avatar
Pierre committed
329
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
Pierre's avatar
Pierre committed
330 331
{
    if ([keyPath isEqualToString:@"hasChanges"] && object == _managedObjectContext) {
Pierre's avatar
Pierre committed
332
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
333 334 335 336 337
        NSProcessInfo *process = [NSProcessInfo processInfo];
        if ([process respondsToSelector:@selector(disableSuddenTermination)])
            [process disableSuddenTermination];
#endif

Pierre's avatar
Pierre committed
338 339 340 341
        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
342 343 344 345 346
        return;
    }
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

347 348 349 350 351 352 353 354 355
- (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
356 357
#pragma mark -
#pragma mark No meta data fallbacks
Pierre's avatar
Pierre committed
358

Pierre's avatar
Pierre committed
359
- (void)computeThumbnailForFile:(MLFile *)file
Pierre's avatar
Pierre committed
360
{
Pierre's avatar
Pierre committed
361
    if (!file.computedThumbnail) {
362
        APLog(@"Computing thumbnail for %@", file.title);
Pierre's avatar
Pierre committed
363 364
        [[MLThumbnailerQueue sharedThumbnailerQueue] addFile:file];
    }
Pierre's avatar
Pierre committed
365
}
Pierre's avatar
Pierre committed
366
- (void)errorWhenFetchingMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
367
{
368
    APLog(@"Error when fetching for '%@'", file.title);
Pierre's avatar
Pierre committed
369

Pierre's avatar
Pierre committed
370 371
    [self computeThumbnailForFile:file];
}
Pierre's avatar
Pierre committed
372

Pierre's avatar
Pierre committed
373 374 375 376 377 378 379
- (void)errorWhenFetchingMetaDataForShow:(MLShow *)show
{
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self errorWhenFetchingMetaDataForFile:file];
    }
}
Pierre's avatar
Pierre committed
380

Pierre's avatar
Pierre committed
381 382
- (void)noMetaDataInRemoteDBForFile:(MLFile *)file
{
383
    file.noOnlineMetaData = @YES;
Pierre's avatar
Pierre committed
384
    [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
385 386
}

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

Pierre's avatar
Pierre committed
395 396 397 398
#pragma mark -
#pragma mark Getter

- (void)addNewLabelWithName:(NSString *)name
Pierre's avatar
Pierre committed
399
{
Pierre's avatar
Pierre committed
400 401
    MLLabel *label = [self createObjectForEntity:@"Label"];
    label.name = name;
Pierre's avatar
Pierre committed
402 403
}

Pierre's avatar
Pierre committed
404 405 406 407 408 409 410
/**
 * TV MLShow Episodes
 */

#pragma mark -
#pragma mark Online meta data grabbing

Pierre's avatar
Pierre committed
411 412 413
#if !HAVE_BLOCK
- (void)tvShowEpisodesInfoGrabberDidFinishGrabbing:(MLTVShowEpisodesInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
414
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
415 416

    NSArray *results = grabber.episodesResults;
417
    [show setValue:(grabber.results)[@"serieArtworkURL"] forKey:@"artworkURL"];
Pierre's avatar
Pierre committed
418
    for (id result in results) {
419
        if ([result[@"serie"] boolValue]) {
Pierre's avatar
Pierre committed
420 421
            continue;
        }
422 423 424 425 426
        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
427 428 429 430 431
        if (!showEpisode.artworkURL) {
            for (MLFile *file in showEpisode.files)
                [self computeThumbnailForFile:file];
        }

Pierre's avatar
Pierre committed
432 433 434 435 436
        showEpisode.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
    show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
}

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

Pierre's avatar
Pierre committed
443 444
- (void)tvShowInfoGrabberDidFinishGrabbing:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
445
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
446 447
    NSArray *results = grabber.results;
    if ([results count] > 0) {
448 449
        NSDictionary *result = results[0];
        NSString *showId = result[@"id"];
Pierre's avatar
Pierre committed
450 451

        show.theTVDBID = showId;
452 453 454
        show.name = result[@"title"];
        show.shortSummary = result[@"shortSummary"];
        show.releaseYear = result[@"releaseYear"];
Pierre's avatar
Pierre committed
455 456

        // Fetch episodes info
Felix Paul Kühne's avatar
Felix Paul Kühne committed
457
        MLTVShowEpisodesInfoGrabber *grabber = [[MLTVShowEpisodesInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
458 459 460 461 462 463
        grabber.delegate = self;
        grabber.userData = show;
        [grabber lookUpForShowID:showId];
    }
    else {
        // Not found.
Pierre's avatar
Pierre committed
464
        [self noMetaDataInRemoteDBForShow:show];
Pierre's avatar
Pierre committed
465 466 467 468
        show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
}

Pierre's avatar
Pierre committed
469 470 471 472 473 474
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
475 476
- (void)tvShowInfoGrabberDidFetchServerTime:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
477
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
478 479 480

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

Pierre's avatar
Pierre committed
481
    // First fetch the MLShow ID
Felix Paul Kühne's avatar
Felix Paul Kühne committed
482
    MLTVShowInfoGrabber *showInfoGrabber = [[MLTVShowInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
483 484 485
    showInfoGrabber.delegate = self;
    showInfoGrabber.userData = show;

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

Pierre's avatar
Pierre committed
488 489 490 491
    [showInfoGrabber lookUpForTitle:show.name];
}
#endif

Pierre's avatar
Pierre committed
492
- (void)fetchMetaDataForShow:(MLShow *)show
Pierre's avatar
Pierre committed
493
{
Pierre's avatar
Pierre committed
494 495
    if (!_allowNetworkAccess)
        return;
496
    APLog(@"Fetching show server time");
Pierre's avatar
Pierre committed
497

Pierre's avatar
Pierre committed
498
    // First fetch the serverTime, so that we can update each entry.
Pierre's avatar
Pierre committed
499 500
#if HAVE_BLOCK
    [MLTVShowInfoGrabber fetchServerTimeAndExecuteBlock:^(NSNumber *serverDate) {
Pierre's avatar
Pierre committed
501 502 503

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

504
        APLog(@"Fetching show information on %@", show.name);
Pierre's avatar
Pierre committed
505 506 507

        // First fetch the MLShow ID
        MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
508 509 510 511 512 513 514 515 516 517 518
        [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"];

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

Pierre's avatar
Pierre committed
521
                // Fetch episode info
Pierre's avatar
Pierre committed
522
                MLTVShowEpisodesInfoGrabber *grabber = [[[MLTVShowEpisodesInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
523 524 525 526 527 528 529
                [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;
                        }
530
                        MLShowEpisode *showEpisode = [MLShowEpisode episodeWithShow:show episodeNumber:[result objectForKey:@"episodeNumber"] seasonNumber:[result objectForKey:@"seasonNumber"] createIfNeeded:YES];
Pierre's avatar
Pierre committed
531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
                        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
547
#else
Felix Paul Kühne's avatar
Felix Paul Kühne committed
548
    MLTVShowInfoGrabber *grabber = [[MLTVShowInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
549 550 551
    grabber.delegate = self;
    grabber.userData = show;
    [grabber fetchServerTime];
Pierre's avatar
Pierre committed
552
#endif
Pierre's avatar
Pierre committed
553 554
}

Pierre's avatar
Pierre committed
555
- (void)addTVShowEpisodeWithInfo:(NSDictionary *)tvShowEpisodeInfo andFile:(MLFile *)file
Pierre's avatar
Pierre committed
556
{
Pierre's avatar
Pierre committed
557
    file.type = kMLFileTypeTVShowEpisode;
Pierre's avatar
Pierre committed
558

559 560 561
    NSNumber *seasonNumber = tvShowEpisodeInfo[@"season"];
    NSNumber *episodeNumber = tvShowEpisodeInfo[@"episode"];
    NSString *tvShowName = tvShowEpisodeInfo[@"tvShowName"];
562
    NSString *tvEpisodeName = tvShowEpisodeInfo[@"tvEpisodeName"];
Pierre's avatar
Pierre committed
563 564
    BOOL hasNoTvShow = NO;
    if (!tvShowName) {
565
        tvShowName = @"";
Pierre's avatar
Pierre committed
566 567 568
        hasNoTvShow = YES;
    }
    BOOL wasInserted = NO;
Pierre's avatar
Pierre committed
569 570
    MLShow *show = nil;
    MLShowEpisode *episode = [MLShowEpisode episodeWithShowName:tvShowName episodeNumber:episodeNumber seasonNumber:seasonNumber createIfNeeded:YES wasCreated:&wasInserted];
571 572

    if (episode) {
Pierre's avatar
Pierre committed
573
        show = episode.show;
574 575
        [show addEpisode:episode];
    }
Pierre's avatar
Pierre committed
576 577 578 579
    if (wasInserted && !hasNoTvShow) {
        show.name = tvShowName;
        [self fetchMetaDataForShow:show];
    }
580
    episode.name = tvEpisodeName;
Pierre's avatar
Pierre committed
581

Felix Paul Kühne's avatar
Felix Paul Kühne committed
582
    if (episode.name.length < 1)
Pierre's avatar
Pierre committed
583 584 585
        episode.name = file.title;
    file.seasonNumber = seasonNumber;
    file.episodeNumber = episodeNumber;
586
    episode.shouldBeDisplayed = @YES;
Pierre's avatar
Pierre committed
587 588

    [episode addFilesObject:file];
Pierre's avatar
Pierre committed
589
    file.showEpisode = episode;
Pierre's avatar
Pierre committed
590

Pierre's avatar
Pierre committed
591
    // The rest of the meta data will be fetched using the MLShow
592
    file.hasFetchedInfo = @YES;
Pierre's avatar
Pierre committed
593 594
}

595 596 597 598
- (void)addAudioContentWithInfo:(NSDictionary *)audioContentInfo andFile:(MLFile *)file
{
    file.type = kMLFileTypeAudio;

599 600 601
    file.title = audioContentInfo[VLCMetaInformationTitle];

    /* all further meta data is set by the FileParserQueue */
602 603 604 605

    file.hasFetchedInfo = @YES;
}

Pierre's avatar
Pierre committed
606
/**
Pierre's avatar
Pierre committed
607
 * MLFile auto detection
Pierre's avatar
Pierre committed
608 609
 */

Pierre's avatar
Pierre committed
610
#if !HAVE_BLOCK
Pierre's avatar
Pierre committed
611 612 613 614 615 616
- (void)movieInfoGrabber:(MLMovieInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLFile *file = grabber.userData;
    [self errorWhenFetchingMetaDataForFile:file];
}

Pierre's avatar
Pierre committed
617 618
- (void)movieInfoGrabberDidFinishGrabbing:(MLMovieInfoGrabber *)grabber
{
619
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
620 621

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

Pierre's avatar
Pierre committed
634 635 636 637
    file.hasFetchedInfo = yes;
}
#endif

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

642
    [[MLFileParserQueue sharedFileParserQueue] addFile:file];
643

Pierre's avatar
Pierre committed
644 645 646 647 648
    if (!_allowNetworkAccess) {
        // Automatically compute the thumbnail
        [self computeThumbnailForFile:file];
    }

Pierre's avatar
Pierre committed
649
    NSDictionary *tvShowEpisodeInfo = [MLTitleDecrapifier tvShowEpisodeInfoFromString:file.title];
Pierre's avatar
Pierre committed
650 651 652 653 654
    if (tvShowEpisodeInfo) {
        [self addTVShowEpisodeWithInfo:tvShowEpisodeInfo andFile:file];
        return;
    }

655 656 657 658 659 660 661 662
    if ([file isSupportedAudioFile]) {
        NSDictionary *audioContentInfo = [MLTitleDecrapifier audioContentInfoFromFile:file];
        if (audioContentInfo && ![file videoTrack]) {
            [self addAudioContentWithInfo:audioContentInfo andFile:file];
            return;
        }
    }

Pierre's avatar
Pierre committed
663 664 665
    if (!_allowNetworkAccess)
        return;

Pierre's avatar
Pierre committed
666
    // Go online and fetch info.
Pierre's avatar
Pierre committed
667 668 669

    // 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
670
    MLMovieInfoGrabber *grabber = [[MLMovieInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
671

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

Pierre's avatar
Pierre committed
674
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
675
    [grabber lookUpForTitle:file.title andExecuteBlock:^(NSError *err){
Pierre's avatar
Pierre committed
676 677
        if (err) {
            [self errorWhenFetchingMetaDataForFile:file];
Pierre's avatar
Pierre committed
678
            return;
Pierre's avatar
Pierre committed
679
        }
Pierre's avatar
Pierre committed
680 681 682 683 684

        NSArray *results = grabber.results;
        if ([results count] > 0) {
            NSDictionary *result = [results objectAtIndex:0];
            file.artworkURL = [result objectForKey:@"artworkURL"];
Pierre's avatar
Pierre committed
685 686
            if (!file.artworkURL)
                [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
687 688 689
            file.title = [result objectForKey:@"title"];
            file.shortSummary = [result objectForKey:@"shortSummary"];
            file.releaseYear = [result objectForKey:@"releaseYear"];
Pierre's avatar
Pierre committed
690 691 692
        } else
            [self noMetaDataInRemoteDBForFile:file];
        file.hasFetchedInfo = [NSNumber numberWithBool:YES];
Pierre's avatar
Pierre committed
693
    }];
Pierre's avatar
Pierre committed
694
#else
Pierre's avatar
Pierre committed
695 696 697
    grabber.userData = file;
    grabber.delegate = self;
    [grabber lookUpForTitle:file.title];
Pierre's avatar
Pierre committed
698
#endif
Pierre's avatar
Pierre committed
699 700
}

Pierre's avatar
Pierre committed
701 702 703
#pragma mark -
#pragma mark Adding file to the DB

Pierre's avatar
Pierre committed
704
- (void)addFilePath:(NSString *)filePath
Pierre's avatar
Pierre committed
705
{
706
    APLog(@"Adding Path %@", filePath);
Pierre's avatar
Pierre committed
707

Pierre's avatar
Pierre committed
708
    NSURL *url = [NSURL fileURLWithPath:filePath];
Pierre's avatar
Pierre committed
709 710
    NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
    NSString *title = [filePath lastPathComponent];
Pierre's avatar
Pierre committed
711
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
712 713
    NSDate *openedDate = nil; // FIXME kMDItemLastUsedDate
    NSDate *modifiedDate = nil; // FIXME [result valueForAttribute:@"kMDItemFSContentChangeDate"];
Pierre's avatar
Pierre committed
714
#endif
715
    NSNumber *size = attributes[NSFileSize]; // FIXME [result valueForAttribute:@"kMDItemFSSize"];
Pierre's avatar
Pierre committed
716

Pierre's avatar
Pierre committed
717
    MLFile *file = [self createObjectForEntity:@"File"];
Pierre's avatar
Pierre committed
718
    file.url = [url absoluteString];
Pierre's avatar
Pierre committed
719 720 721 722

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

723 724
    NSNumber *no = @NO;
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
725 726

    file.currentlyWatching = no;
727 728
    file.lastPosition = @0.0;
    file.remainingTime = @0.0;
Pierre's avatar
Pierre committed
729 730
    file.unread = yes;

Pierre's avatar
Pierre committed
731
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
732 733 734 735
    if ([openedDate isGreaterThan:modifiedDate]) {
        file.playCount = [NSNumber numberWithDouble:1];
        file.unread = no;
    }
Pierre's avatar
Pierre committed
736 737
#endif

738 739 740 741
    if ([[[NSUserDefaults standardUserDefaults] objectForKey:kDecrapifyTitles] boolValue] == YES)
        file.title = [MLTitleDecrapifier decrapify:[title stringByDeletingPathExtension]];
    else
        file.title = [title stringByDeletingPathExtension];
Pierre's avatar
Pierre committed
742 743

    if ([size longLongValue] < 150000000) /* 150 MB */
Pierre's avatar
Pierre committed
744
        file.type = kMLFileTypeClip;
Pierre's avatar
Pierre committed
745
    else
Pierre's avatar
Pierre committed
746
        file.type = kMLFileTypeMovie;
Pierre's avatar
Pierre committed
747 748 749 750

    [self fetchMetaDataForFile:file];
}

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

    // Prepare a fetch request for all items
759 760 761
    NSArray *pathComponents;
    NSUInteger componentCount;

Pierre's avatar
Pierre committed
762
    for (NSString *path in filepaths) {
763
#if TARGET_OS_IPHONE
764
        NSString *urlString;
765 766 767 768 769 770 771 772 773 774 775 776 777 778 779
        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]];
            }
        }
780

781 782 783 784 785 786
        /* 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];
787
#else
788
        [fetchPredicates addObject:[NSPredicate predicateWithFormat:@"url == %@", path]];
789
#endif
Pierre's avatar
Pierre committed
790 791 792 793 794
    }
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];

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

795
    APLog(@"Fetching");
796 797 798 799
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;
    NSArray *dbResults = [moc executeFetchRequest:request error:nil];
800
    APLog(@"Done");
Pierre's avatar
Pierre committed
801

Pierre's avatar
Pierre committed
802
    NSMutableArray *filePathsToAdd = [NSMutableArray arrayWithArray:filepaths];
Pierre's avatar
Pierre committed
803 804

    // Remove objects that are already in db.
Pierre's avatar
Pierre committed
805 806
    for (MLFile *dbResult in dbResults) {
        NSString *urlString = dbResult.url;
807
        [filePathsToAdd removeObject:[urlToObject objectForKey:urlString]];
Pierre's avatar
Pierre committed
808 809 810
    }

    // Add only the newly added items
Pierre's avatar
Pierre committed
811 812
    for (NSString* path in filePathsToAdd)
        [self addFilePath:path];
Pierre's avatar
Pierre committed
813 814
}

Pierre's avatar
Pierre committed
815 816 817 818

#pragma mark -
#pragma mark DB Updates

Pierre's avatar
Pierre committed
819 820 821 822 823 824
#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
825
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
826 827 828 829
        [self fetchMetaDataForShow:show];
}
#endif

830 831
- (BOOL)libraryNeedsUpgrade
{
832
    if (![[[NSUserDefaults standardUserDefaults] objectForKey:kUpdatedToTheGreatSharkHuntDatabaseFormat] boolValue])
833 834 835 836 837
        return YES;
    return NO;
}

- (void)upgradeLibrary
Felix Paul Kühne's avatar
Felix Paul Kühne committed
838 839 840 841 842 843
{
    if (![[[NSUserDefaults standardUserDefaults] objectForKey:kUpdatedToTheGreatSharkHuntDatabaseFormat] boolValue])
        [self _upgradeLibraryToGreatSharkHuntDatabaseFormat];
}

- (void)_upgradeLibraryToGreatSharkHuntDatabaseFormat
844
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
845
    [self libraryDidDisappear];
846 847 848
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSFileManager *fileManager = [NSFileManager defaultManager];

849
    /* remove potential empty albums left over by previous releases */
850 851 852 853 854 855 856 857 858 859
    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;
860 861 862 863 864 865 866
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc) {
        [self libraryDidAppear];
        if ([self.delegate respondsToSelector:@selector(libraryUpgradeComplete)])
            [self.delegate libraryUpgradeComplete];
        return;
    }
867 868 869
    for (NSUInteger x = 0; x < count; x++) {
        album = collection[x];
        if (album.tracks.count < 1)
870
            [moc deleteObject:album];
871 872 873 874 875 876 877 878 879 880 881 882 883
        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++;
884 885
                    else
                        [album removeTrack:track];
886 887 888
                }
            }
            if (emptyAlbumCounter == 0)
889
                [moc deleteObject:album];
890 891 892
        }
    }
    album = nil;
893 894

    /* remove potential empty shows left over by previous releases */
895 896 897 898 899 900 901
    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)
902
            [moc deleteObject:show];
903 904 905 906 907 908 909 910 911 912 913 914 915
        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++;
916 917
                    else
                        [show removeEpisode:showEpisode];
918 919 920
                }
            }
            if (emptyAlbumCounter == 0)
921
                [moc deleteObject:show];
922 923 924
        }
    }

925 926 927 928 929
    /* 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
930 931
    NSString *currentFilePath;
    for (NSUInteger x = 0; x < allFilesCount; x++) {
932
        currentFile = allFiles[x];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
933 934
        currentFilePath = [currentFile.url stringByReplacingOccurrencesOfString:@"/localhost/" withString:@"//"];
        if ([seenFiles containsObject:currentFilePath])
935
            [moc deleteObject:currentFile];
936
        else
Felix Paul Kühne's avatar
Felix Paul Kühne committed
937
            [seenFiles addObject:currentFilePath];
938 939
    }

940
    [defaults setBool:YES forKey:kUpdatedToTheGreatSharkHuntDatabaseFormat];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
941 942 943
    [defaults synchronize];

    [self libraryDidAppear];
944 945 946 947
    if ([self.delegate respondsToSelector:@selector(libraryUpgradeComplete)])
        [self.delegate libraryUpgradeComplete];
}

948
- (void)updateMediaDatabase
Pierre's avatar
Pierre committed
949
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
950
    [self libraryDidDisappear];
Pierre's avatar
Pierre committed
951
    // Remove no more present files
Pierre's avatar
Pierre committed
952
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];
953 954 955 956
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;
    NSArray *results = [moc executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
957
    NSFileManager *fileManager = [NSFileManager defaultManager];
958

Felix Paul Kühne's avatar
Felix Paul Kühne committed
959
    unsigned int count = (unsigned int)results.count;
960 961
    for (unsigned int x = 0; x < count; x++) {
        MLFile *file = results[x];
Pierre's avatar
Pierre committed
962 963
        NSString *urlString = [file url];
        NSURL *fileURL = [NSURL URLWithString:urlString];
964
        BOOL exists = [fileManager fileExistsAtPath:[fileURL path]];
965
        if (!exists) {
966
            APLog(@"Marking - %@", [fileURL absoluteString]);
967
            file.isSafe = YES; // It doesn't exist, it's safe.
968 969
            if (file.isAlbumTrack) {
                MLAlbum *album = file.albumTrack.album;
970 971 972 973 974 975 976 977 978 979 980
                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];
                }
981 982 983
            }
            if (file.isShowEpisode) {
                MLShow *show = file.showEpisode.show;
984 985 986 987 988 989 990 991 992 993 994
                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];
                }
995
            }
996
#if TARGET_OS_IPHONE
997
            NSString *thumbPath = [file thumbnailPath];
998 999
            bool thumbExists = [fileManager fileExistsAtPath:thumbPath];
            if (thumbExists)
1000
                [fileManager removeItemAtPath:thumbPath error:nil];
1001
            [moc deleteObject:file];
1002
#endif
1003
        }