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

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

#define DEBUG 1
43 44
// To debug
#define DELETE_LIBRARY_ON_EACH_LAUNCH 0
Pierre's avatar
Pierre committed
45

Pierre's avatar
Pierre committed
46 47
// Pref key
static NSString *kLastTVDBUpdateServerTime = @"MLLastTVDBUpdateServerTime";
Pierre's avatar
Pierre committed
48

Pierre's avatar
Pierre committed
49
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
50
@interface MLMediaLibrary ()
Pierre's avatar
Pierre committed
51 52 53
#else
@interface MLMediaLibrary () <MLMovieInfoGrabberDelegate, MLTVShowEpisodesInfoGrabberDelegate, MLTVShowInfoGrabberDelegate>
#endif
Pierre's avatar
Pierre committed
54
- (NSManagedObjectContext *)managedObjectContext;
55
- (void)startUpdateDB;
Pierre's avatar
Pierre committed
56
- (NSString *)databaseFolderPath;
Pierre's avatar
Pierre committed
57 58
@end

Pierre's avatar
Pierre committed
59 60 61 62
@implementation MLMediaLibrary
+ (id)sharedMediaLibrary
{
    static id sharedMediaLibrary = nil;
Pierre's avatar
Pierre committed
63
    if (!sharedMediaLibrary) {
Pierre's avatar
Pierre committed
64
        sharedMediaLibrary = [[[self class] alloc] init];
65
        APLog(@"Initializing db in %@", [sharedMediaLibrary databaseFolderPath]);
66 67 68 69

        // 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
70
    }
Pierre's avatar
Pierre committed
71 72 73 74 75 76 77 78
    return sharedMediaLibrary;
}

- (NSFetchRequest *)fetchRequestForEntity:(NSString *)entity
{
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    NSManagedObjectContext *moc = [self managedObjectContext];
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:entity inManagedObjectContext:moc];
Pierre's avatar
Pierre committed
79
    NSAssert(entityDescription, @"No entity");
Pierre's avatar
Pierre committed
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
    [request setEntity:entityDescription];
    return [request autorelease];
}

- (id)createObjectForEntity:(NSString *)entity
{
    NSManagedObjectContext *moc = [self managedObjectContext];
    return [NSEntityDescription insertNewObjectForEntityForName:entity inManagedObjectContext:moc];
}

#pragma mark -
#pragma mark Media Library
- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel)
        return _managedObjectModel;
    _managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];
    return _managedObjectModel;
}

Pierre's avatar
Pierre committed
100
- (NSString *)databaseFolderPath
Pierre's avatar
Pierre committed
101
{
Pierre's avatar
Pierre committed
102 103
    int directory = NSLibraryDirectory;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
104
    NSString *directoryPath = paths[0];
105
#if DELETE_LIBRARY_ON_EACH_LAUNCH
Pierre's avatar
Pierre committed
106
    [[NSFileManager defaultManager] removeItemAtPath:directoryPath error:nil];
107
#endif
Pierre's avatar
Pierre committed
108
    return directoryPath;
Pierre's avatar
Pierre committed
109 110
}

Pierre's avatar
Pierre committed
111 112 113 114 115

- (NSString *)thumbnailFolderPath
{
    int directory = NSLibraryDirectory;
    NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
116
    NSString *directoryPath = paths[0];
Pierre's avatar
Pierre committed
117 118 119 120 121 122
#if DELETE_LIBRARY_ON_EACH_LAUNCH
    [[NSFileManager defaultManager] removeItemAtPath:directoryPath error:nil];
#endif
    return [directoryPath stringByAppendingPathComponent:@"Thumbnails"];
}

Pierre's avatar
Pierre committed
123 124 125 126 127
- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext)
        return _managedObjectContext;

Pierre's avatar
Pierre committed
128
    NSString *databaseFolderPath = [self databaseFolderPath];
Pierre's avatar
Pierre committed
129

Pierre's avatar
Pierre committed
130 131
    NSString *path = [databaseFolderPath stringByAppendingPathComponent: @"MediaLibrary.sqlite"];
    NSURL *url = [NSURL fileURLWithPath:path];
Pierre's avatar
Pierre committed
132 133
    NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];

134
    NSNumber *yes = @YES;
135 136
    NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : yes,
                             NSInferMappingModelAutomaticallyOption : yes};
Pierre's avatar
Pierre committed
137 138

    NSError *error;
Pierre's avatar
Pierre committed
139 140 141
    NSPersistentStore *persistentStore = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:options error:&error];

    if (!persistentStore) {
Pierre's avatar
Pierre committed
142
#if! TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
143 144 145 146
        // 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];
Pierre's avatar
Pierre committed
147
        [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
Pierre's avatar
Pierre committed
148
#else
Pierre's avatar
Pierre committed
149
        [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
Pierre's avatar
Pierre committed
150
#endif
Pierre's avatar
Pierre committed
151 152
        persistentStore = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:options error:&error];
        if (!persistentStore) {
153 154
            if (coordinator)
                [coordinator release];
Pierre's avatar
Pierre committed
155 156 157 158 159
#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];
160
            [alert autorelease];
Pierre's avatar
Pierre committed
161 162 163 164
#endif
            // Probably assert instead.
            return nil;
        }
Pierre's avatar
Pierre committed
165
    }
Pierre's avatar
Pierre committed
166 167 168

    _managedObjectContext = [[NSManagedObjectContext alloc] init];
    [_managedObjectContext setPersistentStoreCoordinator:coordinator];
Pierre's avatar
Pierre committed
169 170 171 172 173 174 175 176 177
    [coordinator release];
    [_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
178 179 180
    NSError *error = nil;
    BOOL success = [[self managedObjectContext] save:&error];
    NSAssert1(success, @"Can't save: %@", error);
Pierre's avatar
Pierre committed
181
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
182 183 184 185 186 187
    NSProcessInfo *process = [NSProcessInfo processInfo];
    if ([process respondsToSelector:@selector(enableSuddenTermination)])
        [process enableSuddenTermination];
#endif
}

Pierre's avatar
Pierre committed
188
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
Pierre's avatar
Pierre committed
189 190
{
    if ([keyPath isEqualToString:@"hasChanges"] && object == _managedObjectContext) {
Pierre's avatar
Pierre committed
191
#if !TARGET_OS_IPHONE && MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
Pierre's avatar
Pierre committed
192 193 194 195 196
        NSProcessInfo *process = [NSProcessInfo processInfo];
        if ([process respondsToSelector:@selector(disableSuddenTermination)])
            [process disableSuddenTermination];
#endif

Pierre's avatar
Pierre committed
197 198 199 200
        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
201 202 203 204 205
        return;
    }
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

Pierre's avatar
Pierre committed
206 207
#pragma mark -
#pragma mark No meta data fallbacks
Pierre's avatar
Pierre committed
208

Pierre's avatar
Pierre committed
209
- (void)computeThumbnailForFile:(MLFile *)file
Pierre's avatar
Pierre committed
210
{
Pierre's avatar
Pierre committed
211
    if (!file.computedThumbnail) {
212
        APLog(@"Computing thumbnail for %@", file.title);
Pierre's avatar
Pierre committed
213 214
        [[MLThumbnailerQueue sharedThumbnailerQueue] addFile:file];
    }
Pierre's avatar
Pierre committed
215
}
Pierre's avatar
Pierre committed
216
- (void)errorWhenFetchingMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
217
{
218
    APLog(@"Error when fetching for '%@'", file.title);
Pierre's avatar
Pierre committed
219

Pierre's avatar
Pierre committed
220 221
    [self computeThumbnailForFile:file];
}
Pierre's avatar
Pierre committed
222

Pierre's avatar
Pierre committed
223 224 225 226 227 228 229
- (void)errorWhenFetchingMetaDataForShow:(MLShow *)show
{
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self errorWhenFetchingMetaDataForFile:file];
    }
}
Pierre's avatar
Pierre committed
230

Pierre's avatar
Pierre committed
231 232
- (void)noMetaDataInRemoteDBForFile:(MLFile *)file
{
233
    file.noOnlineMetaData = @YES;
Pierre's avatar
Pierre committed
234
    [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
235 236
}

Pierre's avatar
Pierre committed
237
- (void)noMetaDataInRemoteDBForShow:(MLShow *)show
Pierre's avatar
Pierre committed
238
{
Pierre's avatar
Pierre committed
239 240 241
    for (MLShowEpisode *episode in show.episodes) {
        for (MLFile *file in episode.files)
            [self noMetaDataInRemoteDBForFile:file];
Pierre's avatar
Pierre committed
242 243 244
    }
}

Pierre's avatar
Pierre committed
245 246 247 248
#pragma mark -
#pragma mark Getter

- (void)addNewLabelWithName:(NSString *)name
Pierre's avatar
Pierre committed
249
{
Pierre's avatar
Pierre committed
250 251
    MLLabel *label = [self createObjectForEntity:@"Label"];
    label.name = name;
Pierre's avatar
Pierre committed
252 253
}

Pierre's avatar
Pierre committed
254 255 256 257 258 259 260
/**
 * TV MLShow Episodes
 */

#pragma mark -
#pragma mark Online meta data grabbing

Pierre's avatar
Pierre committed
261 262 263
#if !HAVE_BLOCK
- (void)tvShowEpisodesInfoGrabberDidFinishGrabbing:(MLTVShowEpisodesInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
264
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
265 266

    NSArray *results = grabber.episodesResults;
267
    [show setValue:(grabber.results)[@"serieArtworkURL"] forKey:@"artworkURL"];
Pierre's avatar
Pierre committed
268
    for (id result in results) {
269
        if ([result[@"serie"] boolValue]) {
Pierre's avatar
Pierre committed
270 271
            continue;
        }
272 273 274 275 276
        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
277 278 279 280 281
        if (!showEpisode.artworkURL) {
            for (MLFile *file in showEpisode.files)
                [self computeThumbnailForFile:file];
        }

Pierre's avatar
Pierre committed
282 283 284 285 286
        showEpisode.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
    show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
}

Pierre's avatar
Pierre committed
287 288
- (void)save
{
289 290 291
    NSError *error = nil;
    BOOL success = [[self managedObjectContext] save:&error];
    NSAssert1(success, @"Can't save: %@", error);
Pierre's avatar
Pierre committed
292 293 294 295 296 297 298 299
}

- (void)tvShowEpisodesInfoGrabber:(MLTVShowEpisodesInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
300 301
- (void)tvShowInfoGrabberDidFinishGrabbing:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
302
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
303 304
    NSArray *results = grabber.results;
    if ([results count] > 0) {
305 306
        NSDictionary *result = results[0];
        NSString *showId = result[@"id"];
Pierre's avatar
Pierre committed
307 308

        show.theTVDBID = showId;
309 310 311
        show.name = result[@"title"];
        show.shortSummary = result[@"shortSummary"];
        show.releaseYear = result[@"releaseYear"];
Pierre's avatar
Pierre committed
312 313 314 315 316 317 318 319 320

        // Fetch episodes info
        MLTVShowEpisodesInfoGrabber *grabber = [[[MLTVShowEpisodesInfoGrabber alloc] init] autorelease];
        grabber.delegate = self;
        grabber.userData = show;
        [grabber lookUpForShowID:showId];
    }
    else {
        // Not found.
Pierre's avatar
Pierre committed
321
        [self noMetaDataInRemoteDBForShow:show];
Pierre's avatar
Pierre committed
322 323 324 325
        show.lastSyncDate = [MLTVShowInfoGrabber serverTime];
    }
}

Pierre's avatar
Pierre committed
326 327 328 329 330 331
- (void)tvShowInfoGrabber:(MLTVShowInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLShow *show = grabber.userData;
    [self errorWhenFetchingMetaDataForShow:show];
}

Pierre's avatar
Pierre committed
332 333
- (void)tvShowInfoGrabberDidFetchServerTime:(MLTVShowInfoGrabber *)grabber
{
Pierre's avatar
Pierre committed
334
    MLShow *show = grabber.userData;
Pierre's avatar
Pierre committed
335 336 337

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

Pierre's avatar
Pierre committed
338
    // First fetch the MLShow ID
Pierre's avatar
Pierre committed
339 340 341 342
    MLTVShowInfoGrabber *showInfoGrabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
    showInfoGrabber.delegate = self;
    showInfoGrabber.userData = show;

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

Pierre's avatar
Pierre committed
345 346 347 348
    [showInfoGrabber lookUpForTitle:show.name];
}
#endif

Pierre's avatar
Pierre committed
349
- (void)fetchMetaDataForShow:(MLShow *)show
Pierre's avatar
Pierre committed
350
{
Pierre's avatar
Pierre committed
351 352
    if (!_allowNetworkAccess)
        return;
353
    APLog(@"Fetching show server time");
Pierre's avatar
Pierre committed
354

Pierre's avatar
Pierre committed
355
    // First fetch the serverTime, so that we can update each entry.
Pierre's avatar
Pierre committed
356 357
#if HAVE_BLOCK
    [MLTVShowInfoGrabber fetchServerTimeAndExecuteBlock:^(NSNumber *serverDate) {
Pierre's avatar
Pierre committed
358 359 360

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

361
        APLog(@"Fetching show information on %@", show.name);
Pierre's avatar
Pierre committed
362 363 364

        // First fetch the MLShow ID
        MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
365 366 367 368 369 370 371 372 373 374 375
        [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"];

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

Pierre's avatar
Pierre committed
378
                // Fetch episode info
Pierre's avatar
Pierre committed
379
                MLTVShowEpisodesInfoGrabber *grabber = [[[MLTVShowEpisodesInfoGrabber alloc] init] autorelease];
Pierre's avatar
Pierre committed
380 381 382 383 384 385 386
                [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;
                        }
Pierre's avatar
Pierre committed
387
                        MLShowEpisode *showEpisode = [self showEpisodeWithShow:show episodeNumber:[result objectForKey:@"episodeNumber"] seasonNumber:[result objectForKey:@"seasonNumber"]];
Pierre's avatar
Pierre committed
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
                        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
404
#else
Pierre's avatar
Pierre committed
405 406 407 408
    MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
    grabber.delegate = self;
    grabber.userData = show;
    [grabber fetchServerTime];
Pierre's avatar
Pierre committed
409
#endif
Pierre's avatar
Pierre committed
410 411
}

Pierre's avatar
Pierre committed
412
- (void)addTVShowEpisodeWithInfo:(NSDictionary *)tvShowEpisodeInfo andFile:(MLFile *)file
Pierre's avatar
Pierre committed
413
{
Pierre's avatar
Pierre committed
414
    file.type = kMLFileTypeTVShowEpisode;
Pierre's avatar
Pierre committed
415

416 417 418
    NSNumber *seasonNumber = tvShowEpisodeInfo[@"season"];
    NSNumber *episodeNumber = tvShowEpisodeInfo[@"episode"];
    NSString *tvShowName = tvShowEpisodeInfo[@"tvShowName"];
419
    NSString *tvEpisodeName = tvShowEpisodeInfo[@"tvEpisodeName"];
Pierre's avatar
Pierre committed
420 421
    BOOL hasNoTvShow = NO;
    if (!tvShowName) {
422
        tvShowName = @"";
Pierre's avatar
Pierre committed
423 424 425
        hasNoTvShow = YES;
    }
    BOOL wasInserted = NO;
Pierre's avatar
Pierre committed
426 427 428 429
    MLShow *show = nil;
    MLShowEpisode *episode = [MLShowEpisode episodeWithShowName:tvShowName episodeNumber:episodeNumber seasonNumber:seasonNumber createIfNeeded:YES wasCreated:&wasInserted];
    if (episode)
        show = episode.show;
Pierre's avatar
Pierre committed
430 431 432 433
    if (wasInserted && !hasNoTvShow) {
        show.name = tvShowName;
        [self fetchMetaDataForShow:show];
    }
434
    episode.name = tvEpisodeName;
Pierre's avatar
Pierre committed
435

436
    if (!episode.name || [episode.name isEqualToString:@""])
Pierre's avatar
Pierre committed
437 438 439
        episode.name = file.title;
    file.seasonNumber = seasonNumber;
    file.episodeNumber = episodeNumber;
440
    episode.shouldBeDisplayed = @YES;
Pierre's avatar
Pierre committed
441 442

    [episode addFilesObject:file];
Pierre's avatar
Pierre committed
443
    file.showEpisode = episode;
Pierre's avatar
Pierre committed
444

Pierre's avatar
Pierre committed
445
    // The rest of the meta data will be fetched using the MLShow
446
    file.hasFetchedInfo = @YES;
Pierre's avatar
Pierre committed
447 448
}

449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
- (void)addAudioContentWithInfo:(NSDictionary *)audioContentInfo andFile:(MLFile *)file
{
    file.type = kMLFileTypeAudio;

    NSString *title = audioContentInfo[VLCMetaInformationTitle];
    NSString *artist = audioContentInfo[VLCMetaInformationArtist];
    NSString *albumName = audioContentInfo[VLCMetaInformationAlbum];
    NSString *releaseYear = audioContentInfo[VLCMetaInformationDate];
    NSString *genre = audioContentInfo[VLCMetaInformationGenre];
    NSString *trackNumber = audioContentInfo[VLCMetaInformationTrackNumber];

    MLAlbum *album = nil;

    BOOL wasCreated = NO;
    MLAlbumTrack *track = [MLAlbumTrack trackWithAlbumName:albumName trackNumber:[NSNumber numberWithInteger:[trackNumber integerValue]] createIfNeeded:YES wasCreated:&wasCreated];
    if (track)
        album = track.album;
    track.title = title;
    track.artist = artist;
    track.genre = genre;
    album.releaseYear = releaseYear;

    if (!track.title || [track.title isEqualToString:@""])
        track.title = file.title;

    [track addFilesObject:file];
    file.albumTrack = track;

    file.hasFetchedInfo = @YES;
}

Pierre's avatar
Pierre committed
480 481

/**
Pierre's avatar
Pierre committed
482
 * MLFile auto detection
Pierre's avatar
Pierre committed
483 484
 */

Pierre's avatar
Pierre committed
485

Pierre's avatar
Pierre committed
486
#if !HAVE_BLOCK
Pierre's avatar
Pierre committed
487 488 489 490 491 492
- (void)movieInfoGrabber:(MLMovieInfoGrabber *)grabber didFailWithError:(NSError *)error
{
    MLFile *file = grabber.userData;
    [self errorWhenFetchingMetaDataForFile:file];
}

Pierre's avatar
Pierre committed
493 494
- (void)movieInfoGrabberDidFinishGrabbing:(MLMovieInfoGrabber *)grabber
{
495
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
496 497

    NSArray *results = grabber.results;
Pierre's avatar
Pierre committed
498
    MLFile *file = grabber.userData;
Pierre's avatar
Pierre committed
499
    if ([results count] > 0) {
500 501 502 503 504
        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
505
    }
Pierre's avatar
Pierre committed
506 507 508 509
    else {
        [self noMetaDataInRemoteDBForFile:file];
    }

Pierre's avatar
Pierre committed
510 511 512 513
    file.hasFetchedInfo = yes;
}
#endif

Pierre's avatar
Pierre committed
514
- (void)fetchMetaDataForFile:(MLFile *)file
Pierre's avatar
Pierre committed
515
{
516
    APLog(@"Fetching meta data for %@", file.title);
Pierre's avatar
Pierre committed
517

518
    [[MLFileParserQueue sharedFileParserQueue] addFile:file];
519

Pierre's avatar
Pierre committed
520 521 522 523 524
    if (!_allowNetworkAccess) {
        // Automatically compute the thumbnail
        [self computeThumbnailForFile:file];
    }

Pierre's avatar
Pierre committed
525
    NSDictionary *tvShowEpisodeInfo = [MLTitleDecrapifier tvShowEpisodeInfoFromString:file.title];
Pierre's avatar
Pierre committed
526 527 528 529 530
    if (tvShowEpisodeInfo) {
        [self addTVShowEpisodeWithInfo:tvShowEpisodeInfo andFile:file];
        return;
    }

531 532 533 534 535 536 537 538
    if ([file isSupportedAudioFile]) {
        NSDictionary *audioContentInfo = [MLTitleDecrapifier audioContentInfoFromFile:file];
        if (audioContentInfo && ![file videoTrack]) {
            [self addAudioContentWithInfo:audioContentInfo andFile:file];
            return;
        }
    }

Pierre's avatar
Pierre committed
539 540 541
    if (!_allowNetworkAccess)
        return;

Pierre's avatar
Pierre committed
542
    // Go online and fetch info.
Pierre's avatar
Pierre committed
543 544 545

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

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

Pierre's avatar
Pierre committed
550
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
551
    [grabber lookUpForTitle:file.title andExecuteBlock:^(NSError *err){
Pierre's avatar
Pierre committed
552 553
        if (err) {
            [self errorWhenFetchingMetaDataForFile:file];
Pierre's avatar
Pierre committed
554
            return;
Pierre's avatar
Pierre committed
555
        }
Pierre's avatar
Pierre committed
556 557 558 559 560

        NSArray *results = grabber.results;
        if ([results count] > 0) {
            NSDictionary *result = [results objectAtIndex:0];
            file.artworkURL = [result objectForKey:@"artworkURL"];
Pierre's avatar
Pierre committed
561 562
            if (!file.artworkURL)
                [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
563 564 565
            file.title = [result objectForKey:@"title"];
            file.shortSummary = [result objectForKey:@"shortSummary"];
            file.releaseYear = [result objectForKey:@"releaseYear"];
Pierre's avatar
Pierre committed
566 567 568
        } else
            [self noMetaDataInRemoteDBForFile:file];
        file.hasFetchedInfo = [NSNumber numberWithBool:YES];
Pierre's avatar
Pierre committed
569
    }];
Pierre's avatar
Pierre committed
570
#else
Pierre's avatar
Pierre committed
571 572 573
    grabber.userData = file;
    grabber.delegate = self;
    [grabber lookUpForTitle:file.title];
Pierre's avatar
Pierre committed
574
#endif
Pierre's avatar
Pierre committed
575 576
}

Pierre's avatar
Pierre committed
577 578 579
#pragma mark -
#pragma mark Adding file to the DB

Pierre's avatar
Pierre committed
580
- (void)addFilePath:(NSString *)filePath
Pierre's avatar
Pierre committed
581
{
582
    APLog(@"Adding Path %@", filePath);
Pierre's avatar
Pierre committed
583

Pierre's avatar
Pierre committed
584
    NSURL *url = [NSURL fileURLWithPath:filePath];
Pierre's avatar
Pierre committed
585 586
    NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
    NSString *title = [filePath lastPathComponent];
Pierre's avatar
Pierre committed
587
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
588 589
    NSDate *openedDate = nil; // FIXME kMDItemLastUsedDate
    NSDate *modifiedDate = nil; // FIXME [result valueForAttribute:@"kMDItemFSContentChangeDate"];
Pierre's avatar
Pierre committed
590
#endif
591
    NSNumber *size = attributes[NSFileSize]; // FIXME [result valueForAttribute:@"kMDItemFSSize"];
Pierre's avatar
Pierre committed
592

Pierre's avatar
Pierre committed
593
    MLFile *file = [self createObjectForEntity:@"File"];
Pierre's avatar
Pierre committed
594
    file.url = [url absoluteString];
Pierre's avatar
Pierre committed
595 596 597 598

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

599 600
    NSNumber *no = @NO;
    NSNumber *yes = @YES;
Pierre's avatar
Pierre committed
601 602

    file.currentlyWatching = no;
603 604
    file.lastPosition = @0.0;
    file.remainingTime = @0.0;
Pierre's avatar
Pierre committed
605 606
    file.unread = yes;

Pierre's avatar
Pierre committed
607
#if !TARGET_OS_IPHONE
Pierre's avatar
Pierre committed
608 609 610 611
    if ([openedDate isGreaterThan:modifiedDate]) {
        file.playCount = [NSNumber numberWithDouble:1];
        file.unread = no;
    }
Pierre's avatar
Pierre committed
612 613
#endif

Pierre's avatar
Pierre committed
614
    file.title = [MLTitleDecrapifier decrapify:[title stringByDeletingPathExtension]];
Pierre's avatar
Pierre committed
615 616

    if ([size longLongValue] < 150000000) /* 150 MB */
Pierre's avatar
Pierre committed
617
        file.type = kMLFileTypeClip;
Pierre's avatar
Pierre committed
618
    else
Pierre's avatar
Pierre committed
619
        file.type = kMLFileTypeMovie;
Pierre's avatar
Pierre committed
620 621 622 623

    [self fetchMetaDataForFile:file];
}

Pierre's avatar
Pierre committed
624
- (void)addFilePaths:(NSArray *)filepaths
Pierre's avatar
Pierre committed
625
{
Pierre's avatar
Pierre committed
626
    NSUInteger count = [filepaths count];
Pierre's avatar
Pierre committed
627 628 629 630
    NSMutableArray *fetchPredicates = [NSMutableArray arrayWithCapacity:count];
    NSMutableDictionary *urlToObject = [NSMutableDictionary dictionaryWithCapacity:count];

    // Prepare a fetch request for all items
Pierre's avatar
Pierre committed
631
    for (NSString *path in filepaths) {
Pierre's avatar
Pierre committed
632
        NSURL *url = [NSURL fileURLWithPath:path];
Pierre's avatar
Pierre committed
633 634
        NSString *urlString = [url absoluteString];
        [fetchPredicates addObject:[NSPredicate predicateWithFormat:@"url == %@", urlString]];
635
        urlToObject[urlString] = path;
Pierre's avatar
Pierre committed
636

Pierre's avatar
Pierre committed
637 638 639 640 641 642
    }

    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];

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

643
    APLog(@"Fetching");
Pierre's avatar
Pierre committed
644
    NSArray *dbResults = [[self managedObjectContext] executeFetchRequest:request error:nil];
645
    APLog(@"Done");
Pierre's avatar
Pierre committed
646

Pierre's avatar
Pierre committed
647
    NSMutableArray *filePathsToAdd = [NSMutableArray arrayWithArray:filepaths];
Pierre's avatar
Pierre committed
648 649

    // Remove objects that are already in db.
Pierre's avatar
Pierre committed
650 651
    for (MLFile *dbResult in dbResults) {
        NSString *urlString = dbResult.url;
652
        [filePathsToAdd removeObject:urlToObject[urlString]];
Pierre's avatar
Pierre committed
653 654 655
    }

    // Add only the newly added items
Pierre's avatar
Pierre committed
656 657
    for (NSString* path in filePathsToAdd)
        [self addFilePath:path];
Pierre's avatar
Pierre committed
658 659
}

Pierre's avatar
Pierre committed
660 661 662 663

#pragma mark -
#pragma mark DB Updates

Pierre's avatar
Pierre committed
664 665 666 667 668 669
#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
670
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
671 672 673 674
        [self fetchMetaDataForShow:show];
}
#endif

675 676 677 678 679
- (void)updateMediaDatabase
{
    [self startUpdateDB];
}

680
- (void)startUpdateDB
Pierre's avatar
Pierre committed
681
{
Pierre's avatar
Pierre committed
682
    // Remove no more present files
Pierre's avatar
Pierre committed
683 684
    NSFetchRequest *request = [self fetchRequestForEntity:@"File"];
    NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
685
    NSFileManager *fileManager = [NSFileManager defaultManager];
686

687 688 689
    unsigned int count = results.count;
    for (unsigned int x = 0; x < count; x++) {
        MLFile *file = results[x];
Pierre's avatar
Pierre committed
690 691
        NSString *urlString = [file url];
        NSURL *fileURL = [NSURL URLWithString:urlString];
692
        BOOL exists = [fileManager fileExistsAtPath:[fileURL path]];
693
        if (!exists) {
694
            APLog(@"Marking - %@", [fileURL absoluteString]);
695
            file.isSafe = YES; // It doesn't exists, it's safe.
696
#if TARGET_OS_IPHONE
697
            NSString *thumbPath = [[[self thumbnailFolderPath] stringByAppendingPathComponent:[[file.objectID URIRepresentation] path]] stringByAppendingString:@".png"];
698 699
            bool thumbExists = [fileManager fileExistsAtPath:thumbPath];
            if (thumbExists)
700
                [fileManager removeItemAtPath:thumbPath error:nil];
701 702
            [[self managedObjectContext] deleteObject:file];
#endif
703
        }
704 705 706
#if !TARGET_OS_IPHONE
    file.isOnDisk = @(exists);
#endif
Pierre's avatar
Pierre committed
707
    }
Pierre's avatar
Pierre committed
708

709 710 711 712 713 714 715
    // Get the file to parse
    request = [self fetchRequestForEntity:@"File"];
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && tracks.@count == 0"]];
    results = [[self managedObjectContext] executeFetchRequest:request error:nil];
    for (MLFile *file in results)
        [[MLFileParserQueue sharedFileParserQueue] addFile:file];

Pierre's avatar
Pierre committed
716 717 718
    if (!_allowNetworkAccess) {
        // Always attempt to fetch
        request = [self fetchRequestForEntity:@"File"];
Pierre's avatar
Pierre committed
719
        [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES"]];
Pierre's avatar
Pierre committed
720
        results = [[self managedObjectContext] executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
721 722 723 724
        for (MLFile *file in results) {
            if (!file.computedThumbnail)
                [self computeThumbnailForFile:file];
        }
Pierre's avatar
Pierre committed
725 726 727
        return;
    }

Pierre's avatar
Pierre committed
728 729
    // Get the thumbnails to compute
    request = [self fetchRequestForEntity:@"File"];
Pierre's avatar
Pierre committed
730
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && hasFetchedInfo == 1 && artworkURL == nil"]];
Pierre's avatar
Pierre committed
731 732
    results = [[self managedObjectContext] executeFetchRequest:request error:nil];
    for (MLFile *file in results)
Pierre's avatar
Pierre committed
733 734
        if (!file.computedThumbnail)
            [self computeThumbnailForFile:file];
Pierre's avatar
Pierre committed
735 736 737 738 739 740

    // Get to fetch meta data
    request = [self fetchRequestForEntity:@"File"];
    [request setPredicate:[NSPredicate predicateWithFormat:@"isOnDisk == YES && hasFetchedInfo == 0"]];
    results = [[self managedObjectContext] executeFetchRequest:request error:nil];
    for (MLFile *file in results)
Pierre's avatar
Pierre committed
741 742
        [self fetchMetaDataForFile:file];

Pierre's avatar
Pierre committed
743
    // Get to fetch show info
Pierre's avatar
Pierre committed
744 745 746
    request = [self fetchRequestForEntity:@"Show"];
    [request setPredicate:[NSPredicate predicateWithFormat:@"lastSyncDate == 0"]];
    results = [[self managedObjectContext] executeFetchRequest:request error:nil];
Pierre's avatar
Pierre committed
747
    for (MLShow *show in results)
Pierre's avatar
Pierre committed
748 749
        [self fetchMetaDataForShow:show];

Pierre's avatar
Pierre committed
750
    // Get updated TV Shows
751
    NSNumber *lastServerTime = @([[NSUserDefaults standardUserDefaults] integerForKey:kLastTVDBUpdateServerTime]);
Pierre's avatar
Pierre committed
752
#if HAVE_BLOCK
Pierre's avatar
Pierre committed
753
    [MLTVShowInfoGrabber fetchUpdatesSinceServerTime:lastServerTime andExecuteBlock:^(NSArray *updates){
Pierre's avatar
Pierre committed
754 755 756
        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
757
        for (MLShow *show in results)
Pierre's avatar
Pierre committed
758 759
            [self fetchMetaDataForShow:show];
    }];
Pierre's avatar
Pierre committed
760 761 762 763
#else
    MLTVShowInfoGrabber *grabber = [[[MLTVShowInfoGrabber alloc] init] autorelease];
    grabber.delegate = self;
    [grabber fetchUpdatesSinceServerTime:lastServerTime];
Pierre's avatar
Pierre committed
764
#endif
Pierre's avatar
Pierre committed
765
    /* Update every hour - FIXME: Preferences key */
766
    [self performSelector:@selector(startUpdateDB) withObject:nil afterDelay:60 * 60];
Pierre's avatar
Pierre committed
767
}
Pierre's avatar
Pierre committed
768

769 770 771 772 773
- (void)applicationWillExit
{
    [[MLCrashPreventer sharedPreventer] cancelAllFileParse];
}

Pierre's avatar
Pierre committed
774 775 776 777 778
- (void)applicationWillStart
{
    [[MLCrashPreventer sharedPreventer] markCrasherFiles];
}

Pierre's avatar
Pierre committed
779 780 781 782 783 784 785 786 787 788 789
- (void)libraryDidDisappear
{
    // Stop expansive work
    [[MLThumbnailerQueue sharedThumbnailerQueue] stop];
}

- (void)libraryDidAppear
{
    // Resume our work
    [[MLThumbnailerQueue sharedThumbnailerQueue] resume];
}
Pierre's avatar
Pierre committed
790
@end