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

#import "MLMediaLibrary.h"
Pierre's avatar
Pierre committed
29
#import "MLTitleDecrapifier.h"
Pierre's avatar
Pierre committed
30 31 32 33
#import "MLFile.h"
#import "MLLabel.h"
#import "MLShowEpisode.h"
#import "MLShow.h"
34 35 36
#import "MLThumbnailerQueue.h"
#import "MLAlbumTrack.h"
#import "MLAlbum.h"
37
#import "MLFileParserQueue.h"
38
#import "MLCrashPreventer.h"
39
#import "MLMediaLibrary+Migration.h"
40
#import <sys/sysctl.h> // for sysctlbyname
Pierre's avatar
Pierre committed
41

Felix Paul Kühne's avatar
Felix Paul Kühne committed
42 43 44 45 46 47
#if HAVE_BLOCK
#import "MLMovieInfoGrabber.h"
#import "MLTVShowInfoGrabber.h"
#import "MLTVShowEpisodesInfoGrabber.h"
#endif

48 49
@interface MLMediaLibrary ()
{
50 51 52 53
    NSManagedObjectContext *_managedObjectContext;
    NSManagedObjectModel   *_managedObjectModel;

    BOOL _allowNetworkAccess;
54
    int _deviceSpeedCategory;
55

56 57
    NSString *_thumbnailFolderPath;
    NSString *_databaseFolderPath;
58
    NSString *_documentFolderPath;
59
    NSString *_libraryBasePath;
60 61 62
}
@end

Pierre's avatar
Pierre committed
63

Pierre's avatar
Pierre committed
64 65
// Pref key
static NSString *kLastTVDBUpdateServerTime = @"MLLastTVDBUpdateServerTime";
66
static NSString *kDecrapifyTitles = @"MLDecrapifyTitles";
Pierre's avatar
Pierre committed
67

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

Pierre's avatar
Pierre committed
77
@implementation MLMediaLibrary
78 79
+ (void)initialize
{
80
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
81
    [defaults registerDefaults:@{kDecrapifyTitles : @YES}];
82 83
}

Pierre's avatar
Pierre committed
84 85 86
+ (id)sharedMediaLibrary
{
    static id sharedMediaLibrary = nil;
Pierre's avatar
Pierre committed
87
    if (!sharedMediaLibrary) {
Pierre's avatar
Pierre committed
88
        sharedMediaLibrary = [[[self class] alloc] init];
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 103 104 105 106 107
- (instancetype)init
{
    self = [super init];
    if (self) {
        _applicationGroupIdentifier = @"group.org.videolan.vlc-ios";
        [self _setupLibraryPathPriorToMigration];
        APLog(@"Initializing db in %@", [self databaseFolderPath]);
    }
    return self;
}

108 109 110 111 112 113
- (void)dealloc
{
    if (_managedObjectContext)
        [_managedObjectContext removeObserver:self forKeyPath:@"hasChanges"];
}

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

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

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

Pierre's avatar
Pierre committed
133 134 135
    return [NSEntityDescription insertNewObjectForEntityForName:entity inManagedObjectContext:moc];
}

136 137
- (void)removeObject:(NSManagedObject *)object
{
138 139 140 141
    NSManagedObjectContext *moc = [self managedObjectContext];

    if (moc)
        [[self managedObjectContext] deleteObject:object];
142 143
}

144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
#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
175 176 177 178 179 180
#pragma mark -
#pragma mark Media Library
- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel)
        return _managedObjectModel;
181

182
    NSString *path = [[NSBundle mainBundle] pathForResource:@"MediaLibrary" ofType:@"momd"];
183 184 185
    NSURL *momURL = [NSURL fileURLWithPath:path];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];

Pierre's avatar
Pierre committed
186 187 188
    return _managedObjectModel;
}

189
#pragma mark - Path handling
190 191 192 193 194 195 196 197
- (void)setLibraryBasePath:(NSString *)libraryBasePath
{
    _libraryBasePath = [libraryBasePath copy];
    _databaseFolderPath = nil;
    _thumbnailFolderPath = nil;
    _persistentStoreURL = nil;
}

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

206 207
- (NSString *)thumbnailFolderPath
{
208 209
    if (_thumbnailFolderPath.length == 0) {
        _thumbnailFolderPath = [self.libraryBasePath stringByAppendingPathComponent:@"Thumbnails"];
210 211
    }
    return _thumbnailFolderPath;
212 213
}

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

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

236 237 238 239 240 241 242 243 244 245
- (NSString *)pathRelativeToDocumentsFolderFromAbsolutPath:(NSString *)absolutPath
{
    return [absolutPath stringByReplacingOccurrencesOfString:self.documentFolderPath withString:@""];
}
- (NSString *)absolutPathFromPathRelativeToDocumentsFolder:(NSString *)relativePath
{
    return [self.documentFolderPath stringByAppendingPathComponent:relativePath];
}

#pragma mark -
246 247
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
248

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

251
    NSNumber *yes = @YES;
252
    NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : yes,
253
                              NSInferMappingModelAutomaticallyOption : yes};
254 255 256 257 258
    if (self.additionalPersitentStoreOptions.count > 0) {
        NSMutableDictionary *mutableOptions = options.mutableCopy;
        [mutableOptions addEntriesFromDictionary:self.additionalPersitentStoreOptions];
        options = mutableOptions;
    }
Pierre's avatar
Pierre committed
259

260 261 262 263 264 265 266
    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
267
    NSError *error;
268
    NSPersistentStore *persistentStore = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:self.persistentStoreURL options:options error:&error];
Pierre's avatar
Pierre committed
269 270

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

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

    _managedObjectContext = [[NSManagedObjectContext alloc] init];
301
    [_managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
302 303
    if (_managedObjectContext.persistentStoreCoordinator == nil)
        return nil;
Pierre's avatar
Pierre committed
304 305 306 307 308 309 310 311
    [_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
312
    NSError *error = nil;
313 314 315 316
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;

Pierre's avatar
Pierre committed
317 318
    BOOL success = [[self managedObjectContext] save:&error];
    NSAssert1(success, @"Can't save: %@", error);
Pierre's avatar
Pierre committed
319
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
320 321 322 323 324 325
    NSProcessInfo *process = [NSProcessInfo processInfo];
    if ([process respondsToSelector:@selector(enableSuddenTermination)])
        [process enableSuddenTermination];
#endif
}

326 327 328
- (void)save
{
    NSError *error = nil;
329 330 331 332 333
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;

    BOOL success = [moc save:&error];
334 335 336
    NSAssert1(success, @"Can't save: %@", error);
}

Pierre's avatar
Pierre committed
337
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
Pierre's avatar
Pierre committed
338 339
{
    if ([keyPath isEqualToString:@"hasChanges"] && object == _managedObjectContext) {
Pierre's avatar
Pierre committed
340
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
341 342 343 344 345
        NSProcessInfo *process = [NSProcessInfo processInfo];
        if ([process respondsToSelector:@selector(disableSuddenTermination)])
            [process disableSuddenTermination];
#endif

Pierre's avatar
Pierre committed
346 347 348 349
        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
350 351 352 353 354
        return;
    }
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

355 356 357 358 359 360 361 362 363
- (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
364 365
#pragma mark -
#pragma mark No meta data fallbacks
Pierre's avatar
Pierre committed
366

Pierre's avatar
Pierre committed
367
- (void)computeThumbnailForFile:(MLFile *)file
Pierre's avatar
Pierre committed
368
{
369
    if (!file.computedThumbnail && ![file isKindOfType:kMLFileTypeAudio]) {
370
        APLog(@"Computing thumbnail for %@", file.title);
Pierre's avatar
Pierre committed
371 372
        [[MLThumbnailerQueue sharedThumbnailerQueue] addFile:file];
    }
Pierre's avatar
Pierre committed
373
}
374

Pierre's avatar
Pierre committed
375
- (void)errorWhenFetchingMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
376
{
377
    APLog(@"Error when fetching for '%@'", file.title);
Pierre's avatar
Pierre committed
378

Pierre's avatar
Pierre committed
379 380
    [self computeThumbnailForFile:file];
}
Pierre's avatar
Pierre committed
381

Pierre's avatar
Pierre committed
382 383 384 385 386 387 388
- (void)errorWhenFetchingMetaDataForShow:(MLShow *)show
{
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self errorWhenFetchingMetaDataForFile:file];
    }
}
Pierre's avatar
Pierre committed
389

Pierre's avatar
Pierre committed
390 391
- (void)noMetaDataInRemoteDBForFile:(MLFile *)file
{
392
    file.noOnlineMetaData = @YES;
Pierre's avatar
Pierre committed
393
    [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
394 395
}

Pierre's avatar
Pierre committed
396
- (void)noMetaDataInRemoteDBForShow:(MLShow *)show
Pierre's avatar
Pierre committed
397
{
Pierre's avatar
Pierre committed
398 399 400
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self noMetaDataInRemoteDBForFile:file];
Pierre's avatar
Pierre committed
401 402 403
    }
}

Pierre's avatar
Pierre committed
404 405 406 407
#pragma mark -
#pragma mark Getter

- (void)addNewLabelWithName:(NSString *)name
Pierre's avatar
Pierre committed
408
{
Pierre's avatar
Pierre committed
409 410
    MLLabel *label = [self createObjectForEntity:@"Label"];
    label.name = name;
Pierre's avatar
Pierre committed
411 412
}

Pierre's avatar
Pierre committed
413 414 415 416 417 418 419
/**
 * TV MLShow Episodes
 */

#pragma mark -
#pragma mark Online meta data grabbing

Felix Paul Kühne's avatar
Felix Paul Kühne committed
420
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
421 422
- (void)tvShowEpisodesInfoGrabberDidFinishGrabbing:(MLTVShowEpisodesInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
423
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
424 425

    NSArray *results = grabber.episodesResults;
426
    [show setValue:(grabber.results)[@"serieArtworkURL"] forKey:@"artworkURL"];
Pierre's avatar
Pierre committed
427
    for (id result in results) {
428
        if ([result[@"serie"] boolValue]) {
Pierre's avatar
Pierre committed
429 430
            continue;
        }
431 432 433 434 435
        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
436 437 438 439 440
        if (!showEpisode.artworkURL) {
            for (MLFile *file in showEpisode.files)
                [self computeThumbnailForFile:file];
        }

Pierre's avatar
Pierre committed
441 442 443 444 445
        showEpisode.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
    show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
}

Pierre's avatar
Pierre committed
446 447 448 449 450 451
- (void)tvShowEpisodesInfoGrabber:(MLTVShowEpisodesInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
452 453
- (void)tvShowInfoGrabberDidFinishGrabbing:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
454
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
455 456
    NSArray *results = grabber.results;
    if ([results count] > 0) {
457 458
        NSDictionary *result = results[0];
        NSString *showId = result[@"id"];
Pierre's avatar
Pierre committed
459 460

        show.theTVDBID = showId;
461 462 463
        show.name = result[@"title"];
        show.shortSummary = result[@"shortSummary"];
        show.releaseYear = result[@"releaseYear"];
Pierre's avatar
Pierre committed
464 465

        // Fetch episodes info
466
        MLTVShowEpisodesInfoGrabber *grabber = [[MLTVShowEpisodesInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
467 468 469 470 471 472
        grabber.delegate = self;
        grabber.userData = show;
        [grabber lookUpForShowID:showId];
    }
    else {
        // Not found.
Pierre's avatar
Pierre committed
473
        [self noMetaDataInRemoteDBForShow:show];
Pierre's avatar
Pierre committed
474 475 476 477
        show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
}

Pierre's avatar
Pierre committed
478 479 480 481 482 483
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
484 485
- (void)tvShowInfoGrabberDidFetchServerTime:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
486
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
487 488 489

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

Pierre's avatar
Pierre committed
490
    // First fetch the MLShow ID
491
    MLTVShowInfoGrabber *showInfoGrabber = [[MLTVShowInfoGrabber alloc] init];
Pierre's avatar
Pierre committed
492 493 494
    showInfoGrabber.delegate = self;
    showInfoGrabber.userData = show;

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

Pierre's avatar
Pierre committed
497 498 499 500
    [showInfoGrabber lookUpForTitle:show.name];
}
#endif

Pierre's avatar
Pierre committed
501
- (void)fetchMetaDataForShow:(MLShow *)show
Pierre's avatar
Pierre committed
502
{
503 504
    if (!_allowNetworkAccess)
        return;
505
    APLog(@"Fetching show server time");
Pierre's avatar
Pierre committed
506

Pierre's avatar
Pierre committed
507
    // First fetch the serverTime, so that we can update each entry.
Pierre's avatar
Pierre committed
508 509
#if HAVE_BLOCK
    [MLTVShowInfoGrabber fetchServerTimeAndExecuteBlock:^(NSNumber *serverDate) {
Pierre's avatar
Pierre committed
510 511 512

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

513
        APLog(@"Fetching show information on %@", show.name);
Pierre's avatar
Pierre committed
514 515 516

        // First fetch the MLShow ID
        MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
517 518 519 520 521 522 523 524 525 526 527
        [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"];

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

Pierre's avatar
Pierre committed
530
                // Fetch episode info
Pierre's avatar
Pierre committed
531
                MLTVShowEpisodesInfoGrabber *grabber = [[[MLTVShowEpisodesInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
532 533 534 535 536 537 538
                [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;
                        }
539
                        MLShowEpisode *showEpisode = [MLShowEpisode episodeWithShow:show episodeNumber:[result objectForKey:@"episodeNumber"] seasonNumber:[result objectForKey:@"seasonNumber"] createIfNeeded:YES];
Pierre's avatar
Pierre committed
540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555
                        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
556
#endif
Pierre's avatar
Pierre committed
557 558
}

Pierre's avatar
Pierre committed
559
- (void)addTVShowEpisodeWithInfo:(NSDictionary *)tvShowEpisodeInfo andFile:(MLFile *)file
Pierre's avatar
Pierre committed
560
{
Pierre's avatar
Pierre committed
561
    file.type = kMLFileTypeTVShowEpisode;
Pierre's avatar
Pierre committed
562

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

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

586
    if (episode.name.length < 1)
Pierre's avatar
Pierre committed
587 588 589
        episode.name = file.title;
    file.seasonNumber = seasonNumber;
    file.episodeNumber = episodeNumber;
590
    episode.shouldBeDisplayed = @YES;
Pierre's avatar
Pierre committed
591 592

    [episode addFilesObject:file];
Pierre's avatar
Pierre committed
593
    file.showEpisode = episode;
Pierre's avatar
Pierre committed
594

Pierre's avatar
Pierre committed
595
    // The rest of the meta data will be fetched using the MLShow
596
    file.hasFetchedInfo = @YES;
Pierre's avatar
Pierre committed
597 598 599
}

/**
Pierre's avatar
Pierre committed
600
 * MLFile auto detection
Pierre's avatar
Pierre committed
601 602
 */

Felix Paul Kühne's avatar
Felix Paul Kühne committed
603
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
604 605 606 607 608 609
- (void)movieInfoGrabber:(MLMovieInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLFile *file = grabber.userData;
    [self errorWhenFetchingMetaDataForFile:file];
}

Pierre's avatar
Pierre committed
610 611
- (void)movieInfoGrabberDidFinishGrabbing:(MLMovieInfoGrabber *)grabber
{
612
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
613 614

    NSArray *results = grabber.results;
Pierre's avatar
Pierre committed
615
    MLFile *file = grabber.userData;
Pierre's avatar
Pierre committed
616
    if ([results count] > 0) {
617 618 619 620 621
        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
622
    }
Pierre's avatar
Pierre committed
623 624 625 626
    else {
        [self noMetaDataInRemoteDBForFile:file];
    }

Pierre's avatar
Pierre committed
627 628 629 630
    file.hasFetchedInfo = yes;
}
#endif

Pierre's avatar
Pierre committed
631
- (void)fetchMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
632
{
633
    APLog(@"Fetching meta data for %@", file.title);
Pierre's avatar
Pierre committed
634

Pierre's avatar
Pierre committed
635
    NSDictionary *tvShowEpisodeInfo = [MLTitleDecrapifier tvShowEpisodeInfoFromString:file.title];
Pierre's avatar
Pierre committed
636
    if (tvShowEpisodeInfo) {
637
        file.type = kMLFileTypeTVShowEpisode;
Pierre's avatar
Pierre committed
638 639 640 641
        [self addTVShowEpisodeWithInfo:tvShowEpisodeInfo andFile:file];
        return;
    }

642 643
    if (!_allowNetworkAccess)
        return;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
644
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
645
    // Go online and fetch info.
Pierre's avatar
Pierre committed
646 647 648

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

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

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

Pierre's avatar
Pierre committed
654
    [grabber lookUpForTitle:file.title andExecuteBlock:^(NSError *err){
Pierre's avatar
Pierre committed
655 656
        if (err) {
            [self errorWhenFetchingMetaDataForFile:file];
Pierre's avatar
Pierre committed
657
            return;
Pierre's avatar
Pierre committed
658
        }
Pierre's avatar
Pierre committed
659 660 661 662 663

        NSArray *results = grabber.results;
        if ([results count] > 0) {
            NSDictionary *result = [results objectAtIndex:0];
            file.artworkURL = [result objectForKey:@"artworkURL"];
Pierre's avatar
Pierre committed
664 665
            if (!file.artworkURL)
                [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
666 667 668
            file.title = [result objectForKey:@"title"];
            file.shortSummary = [result objectForKey:@"shortSummary"];
            file.releaseYear = [result objectForKey:@"releaseYear"];
Pierre's avatar
Pierre committed
669 670 671
        } else
            [self noMetaDataInRemoteDBForFile:file];
        file.hasFetchedInfo = [NSNumber numberWithBool:YES];
Pierre's avatar
Pierre committed
672
    }];
Pierre's avatar
Pierre committed
673
#endif
Pierre's avatar
Pierre committed
674 675
}

Pierre's avatar
Pierre committed
676 677 678
#pragma mark -
#pragma mark Adding file to the DB

Pierre's avatar
Pierre committed
679
- (void)addFilePath:(NSString *)filePath
Pierre's avatar
Pierre committed
680
{
681
    APLog(@"Adding Path %@", filePath);
Pierre's avatar
Pierre committed
682

Pierre's avatar
Pierre committed
683
    NSURL *url = [NSURL fileURLWithPath:filePath];
Pierre's avatar
Pierre committed
684
    NSString *title = [filePath lastPathComponent];
Pierre's avatar
Pierre committed
685
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
686 687
    NSDate *openedDate = nil; // FIXME kMDItemLastUsedDate
    NSDate *modifiedDate = nil; // FIXME [result valueForAttribute:@"kMDItemFSContentChangeDate"];
Pierre's avatar
Pierre committed
688
#endif
Pierre's avatar
Pierre committed
689

Pierre's avatar
Pierre committed
690
    MLFile *file = [self createObjectForEntity:@"File"];
691
    file.url = url;
Pierre's avatar
Pierre committed
692 693 694 695

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

696 697
    NSNumber *no = @NO;
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
698 699

    file.currentlyWatching = no;
700 701
    file.lastPosition = @0.0;
    file.remainingTime = @0.0;
Pierre's avatar
Pierre committed
702 703
    file.unread = yes;

Pierre's avatar
Pierre committed
704
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
705 706 707 708
    if ([openedDate isGreaterThan:modifiedDate]) {
        file.playCount = [NSNumber numberWithDouble:1];
        file.unread = no;
    }
Pierre's avatar
Pierre committed
709 710
#endif

711 712 713 714
    if ([[[NSUserDefaults standardUserDefaults] objectForKey:kDecrapifyTitles] boolValue] == YES)
        file.title = [MLTitleDecrapifier decrapify:[title stringByDeletingPathExtension]];
    else
        file.title = [title stringByDeletingPathExtension];
Pierre's avatar
Pierre committed
715

716
    [[MLFileParserQueue sharedFileParserQueue] addFile:file];
Pierre's avatar
Pierre committed
717 718
}

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

    // Prepare a fetch request for all items
Pierre's avatar
Pierre committed
726
    for (NSString *path in filepaths) {
727
        NSString *relativePath = path;
728
#if TARGET_OS_IPHONE
729 730
        // on iPhone we only save relative paths ins the DB
        relativePath = [self pathRelativeToDocumentsFolderFromAbsolutPath:path];
731
#endif
732 733
        [urlToObject setObject:path forKey:relativePath];
        [fetchPredicates addObject:[NSPredicate predicateWithFormat:@"path == %@", relativePath]];
Pierre's avatar
Pierre committed
734 735
    }
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];
736 737
    if (!request)
        return;
Pierre's avatar
Pierre committed
738 739
    [request setPredicate:[NSCompoundPredicate orPredicateWithSubpredicates:fetchPredicates]];

740
    APLog(@"Fetching");
741 742 743 744
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;
    NSArray *dbResults = [moc executeFetchRequest:request error:nil];
745
    APLog(@"Done");
Pierre's avatar
Pierre committed
746

Pierre's avatar
Pierre committed
747
    NSMutableArray *filePathsToAdd = [NSMutableArray arrayWithArray:filepaths];
Pierre's avatar
Pierre committed
748 749

    // Remove objects that are already in db.
Pierre's avatar
Pierre committed
750
    for (MLFile *dbResult in dbResults) {
751 752
        NSString *path = dbResult.path;
        [filePathsToAdd removeObject:[urlToObject objectForKey:path]];
Pierre's avatar
Pierre committed
753 754 755
    }

    // Add only the newly added items
Pierre's avatar
Pierre committed
756 757
    for (NSString* path in filePathsToAdd)
        [self addFilePath:path];
Pierre's avatar
Pierre committed
758 759
}

Pierre's avatar
Pierre committed
760 761 762 763

#pragma mark -
#pragma mark DB Updates

Felix Paul Kühne's avatar
Felix Paul Kühne committed
764
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
765 766 767
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFetchUpdates:(NSArray *)updates
{
    NSFetchRequest *request = [self fetchRequestForEntity:@"Show"];
768 769 770
    if (!request)
        return;

Pierre's avatar
Pierre committed
771 772
    [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
773
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
774 775 776 777
        [self fetchMetaDataForShow:show];
}
#endif

778
- (void)updateMediaDatabase
Pierre's avatar
Pierre committed
779
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
780
    [self libraryDidDisappear];
Pierre's avatar
Pierre committed
781
    // Remove no more present files
Pierre's avatar
Pierre committed
782
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];
783 784
    if (!request)
        return;
785 786 787 788
    NSManagedObjectContext *moc = [self managedObjectContext];
    if (!moc)
        return;
    NSArray *results = [moc executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
789
    NSFileManager *fileManager = [NSFileManager defaultManager];
790

791
    unsigned int count = (unsigned int)results.count;
792 793
    for (unsigned int x = 0; x < count; x++) {
        MLFile *file = results[x];
794
       NSURL *fileURL = file.url;
795
        BOOL exists = [fileManager fileExistsAtPath:[fileURL path]];
796
        if (!exists) {
797
            APLog(@"Marking - %@", [fileURL absoluteString]);
798
            file.isSafe = YES; // It doesn't exist, it's safe.
799 800
            if (file.isAlbumTrack) {
                MLAlbum *album = file.albumTrack.album;
801 802 803 804 805 806 807 808 809 810 811
                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];
                }
812 813 814
            }
            if (file.isShowEpisode) {
                MLShow *show = file.showEpisode.show;
815 816 817 818 819 820 821 822 823 824 825
                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];
                }
826
            }
827
#if TARGET_OS_IPHONE
828
            NSString *thumbPath = [file thumbnailPath];
829 830
            bool thumbExists = [fileManager fileExistsAtPath:thumbPath];
            if (thumbExists)
831
                [fileManager removeItemAtPath:thumbPath error:nil];
832
            [moc deleteObject:file];
833
#endif
834
        }
835 836 837
#if !TARGET_OS_IPHONE
    file.isOnDisk = @(exists);
#endif
Pierre's avatar
Pierre committed
838
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
839
    [self libraryDidAppear];
Pierre's avatar
Pierre committed
840

841 842
    // Get the file to parse
    request = [self fetchRequestForEntity:@"File"];
843 844
    if (!request)
        return;
845
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && tracks.@count == 0"]];
846
    results = [moc executeFetchRequest:request error:nil];
847 848 849
    for (MLFile *file in results)
        [[MLFileParserQueue sharedFileParserQueue] addFile:file];

850 851 852
    if (!_allowNetworkAccess) {
        // Always attempt to fetch
        request = [self fetchRequestForEntity:@"File"];
853 854
        if (!request)
            return;
855
        [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES"]];
856
        results = [moc executeFetchRequest:request error:nil];
857
        for (MLFile *file in results) {
858
            if (!file.computedThumbnail && ![file isKindOfType:kMLFileTypeAudio])
859 860
                [self computeThumbnailForFile:file];
        }
861 862 863
        return;
    }

Pierre's avatar
Pierre committed
864 865
    // Get the thumbnails to compute
    request = [self fetchRequestForEntity:@"File"];
866 867
    if (!request)
        return;
868
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && hasFetchedInfo == 1 && artworkURL == nil"]];
869
    results = [moc executeFetchRequest:request error:nil];
870 871 872 873 874 875
    for (MLFile *file in results) {
        if (!file.computedThumbnail) {
            if (!file.albumTrack && ![file isKindOfType:kMLFileTypeAudio])
                [self computeThumbnailForFile:file];
        }
    }
Pierre's avatar
Pierre committed
876 877 878

    // Get to fetch meta data
    request = [self fetchRequestForEntity:@"File"];
879 880
    if (!request)
        return;
Pierre's avatar
Pierre committed
881
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && hasFetchedInfo == 0"]];
882
    results = [moc executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
883
    for (MLFile *file in results)
884
        [[MLFileParserQueue sharedFileParserQueue] addFile:file];
Pierre's avatar
Pierre committed
885

Pierre's avatar
Pierre committed
886
    // Get to fetch show info
Pierre's avatar
Pierre committed
887
    request = [self fetchRequestForEntity:@"Show"];
888 889
    if (!request)
        return;
Pierre's avatar
Pierre committed
890
    [request setPredicate:[NSPredicate predicateWithFormat:@"lastSyncDate == 0"]];
891
    results = [moc executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
892
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
893 894
        [self fetchMetaDataForShow:show];

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

Pierre's avatar
Pierre committed
899
    [MLTVShowInfoGrabber fetchUpdatesSinceServerTime:lastServerTime andExecuteBlock:^(NSArray *updates){
Pierre's avatar
Pierre committed
900
        NSFetchRequest *request = [self fetchRequestForEntity:@"Show"];
901 902
        if (!request)
            return;
Pierre's avatar
Pierre committed
903
        [request setPredicate:[NSComparisonPredicate predicateWithLeftExpression:[NSExpression expressionForKeyPath:@"theTVDBID"] rightExpression:[NSExpression expressionForConstantValue:updates] modifier:NSDirectPredicateModifier type:NSInPredicateOperatorType options:0]];
904
        NSArray *results = [moc executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
905
        for (MLShow *show in results)
Pierre's avatar
Pierre committed
906 907
            [self fetchMetaDataForShow:show];
    }];
Pierre's avatar
Pierre committed
908
#endif
Pierre's avatar
Pierre committed
909
    /* Update every hour - FIXME: Preferences key */
910
    [self performSelector:@selector(updateMediaDatabase) withObject:nil afterDelay:60 * 60];
Pierre's avatar
Pierre committed
911
}
Pierre's avatar
Pierre committed
912

913 914
- (void)applicationWillExit
{
915
    [[MLFileParserQueue sharedFileParserQueue] stop];
916 917 918
    [[MLCrashPreventer sharedPreventer] cancelAllFileParse];
}

Pierre's avatar
Pierre committed
919 920 921
- (void)applicationWillStart
{
    [[MLCrashPreventer sharedPreventer] markCrasherFiles];
922
    [[MLFileParserQueue sharedFileParserQueue] resume];
Pierre's avatar
Pierre committed
923 924
}

Pierre's avatar
Pierre committed
925 926 927 928
- (void)libraryDidDisappear
{
    // Stop expansive work
    [[MLThumbnailerQueue sharedThumbnailerQueue] stop];
929
    [[MLFileParserQueue sharedFileParserQueue] stop];
Pierre's avatar
Pierre committed
930 931 932 933 934 935
}

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

939
#pragma mark - migrations
940

941 942 943 944 945 946 947
- (BOOL)libraryMigrationNeeded
{
    return [self _libraryMigrationNeeded];
}
- (void)migrateLibrary
{
    [self _migrateLibrary];
948 949
}

Pierre's avatar
Pierre committed
950
@end