MLFileParserQueue.m 14 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
/*****************************************************************************
 * MLFileParserQueue.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 26
 *****************************************************************************/

27 28
#import <CommonCrypto/CommonDigest.h> // for MD5

29 30 31
#import "MLFileParserQueue.h"
#import "MLFile.h"
#import "MLMediaLibrary.h"
32
#import "MLCrashPreventer.h"
33 34 35
#import "MLAlbumTrack.h"
#import "MLAlbum.h"
#import "MLTitleDecrapifier.h"
36
#import "MLThumbnailerQueue.h"
37

Felix Paul Kühne's avatar
Felix Paul Kühne committed
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
#ifdef MLKIT_READONLY_TARGET

@implementation MLFileParserQueue

+ (MLFileParserQueue *)sharedFileParserQueue
{
    static MLFileParserQueue *shared = nil;
    if (!shared) {
        shared = [[MLFileParserQueue alloc] init];
    }
    return shared;
}

- (void)addFile:(MLFile *)file
{
}

- (void)setHighPriorityForFile:(MLFile *)file
{
}

- (void)setDefaultPriorityForFile:(MLFile *)file
{
}

- (void)stop
{
}

- (void)resume
{
}

@end

#else

Felix Paul Kühne's avatar
Felix Paul Kühne committed
75 76 77 78 79 80 81
@interface MLFileParserQueue ()
{
    NSDictionary *_fileDescriptionToOperation;
    NSOperationQueue *_queue;
}
@end

82
@interface MLParsingOperation : NSOperation <VLCMediaDelegate>
83 84 85
{
    MLFile *_file;
    VLCMedia *_media;
86 87
    BOOL _parsing;
    NSTimer *_timeOutTimer;
88
}
89
@property (strong,readwrite) MLFile *file;
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
@end

@interface MLFileParserQueue ()
- (void)didFinishOperation:(MLParsingOperation *)op;
@end

@implementation MLParsingOperation
@synthesize file=_file;
- (id)initWithFile:(MLFile *)file;
{
    if (!(self = [super init]))
        return nil;
    self.file = file;
    return self;
}

- (void)parse
{
    NSAssert(!_media, @"We are already parsing");
109

110
    MLFile *file = self.file;
111
    APLog(@"Starting PARSE %@", file);
112
    [[MLCrashPreventer sharedPreventer] willParseFile:file];
113

114 115 116
    _parsing = YES;
    _timeOutTimer = [NSTimer scheduledTimerWithTimeInterval:3. target:self selector:@selector(cancelParsing:) userInfo:nil repeats:NO];

117
    _media = [VLCMedia mediaWithURL:file.url];
118 119 120
    _media.delegate = self;
    [_media parse];
    MLFileParserQueue *parserQueue = [MLFileParserQueue sharedFileParserQueue];
121
    [parserQueue.queue setSuspended:YES]; // Balanced in -mediaDidFinishParsing
122
     // Balanced in -mediaDidFinishParsing:
123
}
124

125 126 127 128 129 130 131 132 133 134 135
- (void)cancelParsing:(NSTimer *)timer
{
    if (_parsing && _media) {
        [self mediaDidFinishParsing:_media];
        return;
    }

    _parsing = NO;
    [self endParsing];
}

136 137 138 139 140 141 142
- (void)main
{
    [self performSelectorOnMainThread:@selector(parse) withObject:nil waitUntilDone:YES];
}

- (void)mediaDidFinishParsing:(VLCMedia *)media
{
143 144 145
    _parsing = NO;
    [_timeOutTimer invalidate];

146
    APLog(@"Parsed %@ - %lu tracks", media, (unsigned long)[[_media tracksInformation] count]);
147

148 149 150
    if (_media.delegate != self)
        return;

151 152
    MLFile *file = self.file;

153 154 155
    _media.delegate = nil;
    NSArray *tracks = [_media tracksInformation];
    NSMutableSet *tracksSet = [NSMutableSet setWithCapacity:[tracks count]];
156
    BOOL mediaHasVideo = NO;
157
    for (NSDictionary *track in tracks) {
158
        NSString *type = track[VLCMediaTracksInformationType];
159 160 161
        NSManagedObject *trackInfo = nil;
        if ([type isEqualToString:VLCMediaTracksInformationTypeVideo]) {
            trackInfo = [[MLMediaLibrary sharedMediaLibrary] createObjectForEntity:@"VideoTrackInformation"];
162 163
            [trackInfo setValue:track[VLCMediaTracksInformationVideoWidth] forKey:@"width"];
            [trackInfo setValue:track[VLCMediaTracksInformationVideoHeight] forKey:@"height"];
164
            mediaHasVideo = YES;
165 166
        } else if ([type isEqualToString:VLCMediaTracksInformationTypeAudio]) {
            trackInfo = [[MLMediaLibrary sharedMediaLibrary] createObjectForEntity:@"AudioTrackInformation"];
167
            [trackInfo setValue:track[VLCMediaTracksInformationAudioRate] forKey:@"sampleRate"];
168
            [trackInfo setValue:track[VLCMediaTracksInformationAudioChannelsNumber] forKey:@"channelsNumber"];
169 170 171
        } else if ([type isEqualToString:VLCMediaTracksInformationTypeText]) {
            trackInfo = [[MLMediaLibrary sharedMediaLibrary] createObjectForEntity:@"SubtitlesTrackInformation"];
            [trackInfo setValue:track[VLCMediaTracksInformationTextEncoding] forKey:@"textEncoding"];
172
        }
173 174 175

        if (trackInfo) {
            [trackInfo setValue:track[VLCMediaTracksInformationBitrate] forKey:@"bitrate"];
176 177
            [trackInfo setValue:[track[VLCMediaTracksInformationCodec] stringValue] forKey:@"codec"];
            [trackInfo setValue:track[VLCMediaTracksInformationCodec] forKey:@"codecFourCC"];
178 179 180
            [trackInfo setValue:track[VLCMediaTracksInformationCodecLevel] forKey:@"codecLevel"];
            [trackInfo setValue:track[VLCMediaTracksInformationCodecProfile] forKey:@"codecProfile"];
            [trackInfo setValue:track[VLCMediaTracksInformationLanguage] forKey:@"language"];
181
            [tracksSet addObject:trackInfo];
182
        }
183
    }
184

185 186 187 188 189 190
    @try {
        [file setTracks:tracksSet];
        [file setDuration:[[_media length] numberValue]];
    }
    @catch (NSException *exception) {
        APLog(@"we failed to write some metadata because the file disappeared in front of us");
191
    }
192

193
    if (!mediaHasVideo) {
194 195 196 197 198 199 200 201
        NSDictionary *audioContentInfo = [_media metaDictionary];
        if (audioContentInfo && audioContentInfo.count > 0) {
            NSString *title = audioContentInfo[VLCMetaInformationTitle];
            NSString *artist = audioContentInfo[VLCMetaInformationArtist];
            NSString *albumName = audioContentInfo[VLCMetaInformationAlbum];
            NSString *releaseYear = audioContentInfo[VLCMetaInformationDate];
            NSString *genre = audioContentInfo[VLCMetaInformationGenre];
            NSString *trackNumber = audioContentInfo[VLCMetaInformationTrackNumber];
202
            NSString *discNumber = audioContentInfo[VLCMetaInformationDiscNumber];
203 204 205 206

            MLAlbum *album = nil;

            BOOL wasCreated = NO;
207

208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
            @try {
                NSMutableDictionary *mutDict = [NSMutableDictionary dictionary];
                if (albumName)
                    [mutDict setObject:albumName forKey:MLAlbumTrackAlbumName];
                if (title)
                    [mutDict setObject:title forKey:MLAlbumTrackTrackName];
                if (trackNumber)
                    [mutDict setObject:@([trackNumber integerValue]) forKey:MLAlbumTrackNumber];
                if (discNumber)
                    [mutDict setObject:@([discNumber integerValue]) forKey:MLAlbumTrackDiscNumber];

                MLAlbumTrack *track = [MLAlbumTrack trackWithMetadata:[NSDictionary dictionaryWithDictionary:mutDict]
                                                       createIfNeeded:YES
                                                           wasCreated:&wasCreated];
                if (track) {
                    album = track.album;
                    track.title = title ? title : @"";
                    track.artist = artist ? artist : @"";
                    track.genre = genre ? genre : @"";
                    album.releaseYear = releaseYear ? releaseYear : @"";

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

                    [track addFilesObject:file];
                    file.albumTrack = track;
                }
235
            }
236 237 238 239 240 241 242 243 244 245
            @catch (NSException *exception) {
                APLog(@"album track creation failed");
            }
        }

        @try {
            file.type = kMLFileTypeAudio;
        }
        @catch (NSException *exception) {
            APLog(@"file type setting failed");
246 247
        }

248 249
        APLog(@"'%@' is an audio file, fetching artwork", file.title);
        NSString *artist, *albumName, *title;
250
        BOOL skipOperation = NO;
251 252 253 254

        if (file.isAlbumTrack) {
            artist = file.albumTrack.artist;
            albumName = file.albumTrack.album.name;
255 256 257

            if (!file.albumTrack.containsArtwork)
                skipOperation = YES;
258
        }
259

260 261 262 263 264 265
        title = file.title;

        if (!title) {
            NSDictionary *poorMansContentInfo = [MLTitleDecrapifier audioContentInfoFromFile:file];
            if (poorMansContentInfo)
                title = poorMansContentInfo[VLCMetaInformationTitle];
266 267 268 269 270 271
            @try {
                file.title = title;
            }
            @catch (NSException *exception) {
                APLog(@"file title setting failed");
            }
272
        }
273

274
        if (!skipOperation) {
275 276 277 278 279 280 281 282 283 284 285
            @try {
                NSString *artworkPath = [self artworkPathForMediaItemWithTitle:title Artist:artist andAlbumName:albumName];
                if ([[NSFileManager defaultManager] fileExistsAtPath:artworkPath]) {
                    file.computedThumbnail = [UIImage scaleImage:[UIImage imageWithContentsOfFile:artworkPath]
                                                       toFitRect:(CGRect){CGPointZero, [UIImage preferredThumbnailSizeForDevice]}];
                }
                if (file.computedThumbnail == nil)
                    file.albumTrack.containsArtwork = NO;
            }
            @catch (NSException *exception) {
                APLog(@"file thumbnail setting failed");
286 287
            }
        }
288 289 290 291
    } else {
        MLMediaLibrary *sharedLibrary = [MLMediaLibrary sharedMediaLibrary];
        [sharedLibrary computeThumbnailForFile:file];
        [sharedLibrary fetchMetaDataForFile:file];
292
    }
293 294 295 296 297 298
    @try {
        file.hasFetchedInfo = @(YES);
    }
    @catch (NSException *exception) {
        APLog(@"failed to set that we fetch info for the file");
    }
299
#if CS_ENABLED
300
    [file updateCoreSpotlightEntry];
301
#endif
302

303 304
    [self endParsing];
}
305

306 307
- (void)endParsing
{
308
    MLFileParserQueue *parserQueue = [MLFileParserQueue sharedFileParserQueue];
309
    [[MLCrashPreventer sharedPreventer] didParseFile:self.file];
310 311
    [parserQueue.queue setSuspended:NO];
    [parserQueue didFinishOperation:self];
312 313
    _media = nil;
}
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346

#pragma mark - audio file specific code

- (NSString *)artworkPathForMediaItemWithTitle:(NSString *)title Artist:(NSString*)artist andAlbumName:(NSString*)albumname
{
    NSString *artworkURL;
    NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *cacheDir = searchPaths[0];
    cacheDir = [cacheDir stringByAppendingFormat:@"/%@", [[NSBundle mainBundle] bundleIdentifier]];

    if (artist.length == 0 || albumname.length == 0) {
        /* Use generated hash to find art */
        artworkURL = [cacheDir stringByAppendingFormat:@"/art/arturl/%@/art.jpg", [self _md5FromString:title]];
    } else {
        /* Otherwise, it was cached by artist and album */
        artworkURL = [cacheDir stringByAppendingFormat:@"/art/artistalbum/%@/%@/art.jpg", artist, albumname];
    }

    return artworkURL;
}

- (NSString *)_md5FromString:(NSString *)string
{
    const char *ptr = [string UTF8String];
    unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH];
    CC_MD5(ptr, (unsigned int)strlen(ptr), md5Buffer);
    NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
    for(int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
        [output appendFormat:@"%02x",md5Buffer[i]];

    return [NSString stringWithString:output];
}

347 348 349
@end

@implementation MLFileParserQueue
350
@synthesize queue=_queue;
351 352 353 354 355 356 357 358 359 360 361 362 363 364
+ (MLFileParserQueue *)sharedFileParserQueue
{
    static MLFileParserQueue *shared = nil;
    if (!shared) {
        shared = [[MLFileParserQueue alloc] init];
    }
    return shared;
}

- (id)init
{
    self = [super init];
    if (self != nil) {
        _fileDescriptionToOperation = [[NSMutableDictionary alloc] init];
365 366
        _queue = [[NSOperationQueue alloc] init];
        [_queue setMaxConcurrentOperationCount:1];
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
    }
    return self;
}

static inline NSString *hashFromFile(MLFile *file)
{
    return [NSString stringWithFormat:@"%p", [[file objectID] URIRepresentation]];
}

- (void)didFinishOperation:(MLParsingOperation *)op
{
    [_fileDescriptionToOperation setValue:nil forKey:hashFromFile(op.file)];
}

- (void)addFile:(MLFile *)file
{
383
    if (_fileDescriptionToOperation[hashFromFile(file)])
384 385
        return;
    if (![[MLCrashPreventer sharedPreventer] isFileSafe:file]) {
386
        APLog(@"%@ is unsafe and will crash, ignoring", file);
387 388
        return;
    }
389 390 391 392
    if (file.isBeingParsed) {
        APLog(@"%@ is already being parsed, ignoring", file);
        return;
    }
393 394
    MLParsingOperation *op = [[MLParsingOperation alloc] initWithFile:file];
    [_fileDescriptionToOperation setValue:op forKey:hashFromFile(file)];
395
    [self.queue addOperation:op];
396 397 398 399
}

- (void)stop
{
400 401
    if (![[MLCrashPreventer sharedPreventer] fileParsingInProgress])
        [_queue setSuspended:YES];
402
    [_queue setMaxConcurrentOperationCount:0];
403 404 405 406
}

- (void)resume
{
407 408
    if (![[MLCrashPreventer sharedPreventer] fileParsingInProgress])
        [_queue setSuspended:NO];
409
    [_queue setMaxConcurrentOperationCount:1];
410 411 412 413
}

- (void)setHighPriorityForFile:(MLFile *)file
{
414
    MLParsingOperation *op = _fileDescriptionToOperation[hashFromFile(file)];
415 416 417 418 419
    [op setQueuePriority:NSOperationQueuePriorityHigh];
}

- (void)setDefaultPriorityForFile:(MLFile *)file
{
420
    MLParsingOperation *op = _fileDescriptionToOperation[hashFromFile(file)];
421 422 423 424
    [op setQueuePriority:NSOperationQueuePriorityNormal];
}

@end
Felix Paul Kühne's avatar
Felix Paul Kühne committed
425 426

#endif