1/*  HBJob.m $
2
3 This file is part of the HandBrake source code.
4 Homepage: <http://handbrake.fr/>.
5 It may be used under the terms of the GNU General Public License. */
6
7#import "HBJob.h"
8#import "HBJob+Private.h"
9#import "HBTitle+Private.h"
10
11#import "HBAudioDefaults.h"
12#import "HBSubtitlesDefaults.h"
13#import "HBMutablePreset.h"
14
15#import "HBCodingUtilities.h"
16#import "HBLocalizationUtilities.h"
17#import "HBUtilities.h"
18#import "HBSecurityAccessToken.h"
19
20#include "handbrake/handbrake.h"
21
22NSString *HBContainerChangedNotification = @"HBContainerChangedNotification";
23NSString *HBChaptersChangedNotification  = @"HBChaptersChangedNotification";
24
25@interface HBJob ()
26
27@property (nonatomic, readonly) NSString *name;
28
29/**
30 Store the security scoped bookmarks, so we don't
31 regenerate it each time
32 */
33@property (nonatomic, readonly) NSData *fileURLBookmark;
34@property (nonatomic, readwrite) NSData *outputURLFolderBookmark;
35
36/**
37 Keep track of security scoped resources status.
38 */
39@property (nonatomic, readwrite) HBSecurityAccessToken *fileURLToken;
40@property (nonatomic, readwrite) HBSecurityAccessToken *outputURLToken;
41@property (nonatomic, readwrite) HBSecurityAccessToken *subtitlesToken;
42@property (nonatomic, readwrite) NSInteger accessCount;
43
44@end
45
46@implementation HBJob
47
48- (nullable instancetype)initWithTitle:(HBTitle *)title preset:(HBPreset *)preset
49{
50    self = [super init];
51    if (self) {
52        NSParameterAssert(title);
53        NSParameterAssert(preset);
54
55        _title = title;
56        _titleIdx = title.index;
57
58        _name = [title.name copy];
59        _fileURL = title.url;
60
61        _container = HB_MUX_MP4;
62        _angle = 1;
63
64        _range = [[HBRange alloc] initWithTitle:title];
65        _video = [[HBVideo alloc] initWithJob:self];
66        _picture = [[HBPicture alloc] initWithTitle:title];
67        _filters = [[HBFilters alloc] init];
68
69        _audio = [[HBAudio alloc] initWithJob:self];
70        _subtitles = [[HBSubtitles alloc] initWithJob:self];
71
72        _chapterTitles = [title.chapters copy];
73        _metadataPassthru = YES;
74        _presetName = @"";
75
76        if ([self applyPreset:preset error:NULL] == NO)
77        {
78            return nil;
79        }
80    }
81
82    return self;
83}
84
85#pragma mark - HBPresetCoding
86
87- (BOOL)applyPreset:(HBPreset *)preset error:(NSError * __autoreleasing *)outError
88{
89    NSAssert(self.title, @"HBJob: calling applyPreset: without a valid title loaded");
90
91    NSDictionary *jobSettings = [self.title jobSettingsWithPreset:preset];
92
93    if (jobSettings)
94    {
95        self.presetName = preset.name;
96
97        self.container = hb_container_get_from_name([preset[@"FileFormat"] UTF8String]);
98
99        // MP4 specifics options.
100        self.mp4HttpOptimize = [preset[@"Mp4HttpOptimize"] boolValue];
101        self.mp4iPodCompatible = [preset[@"Mp4iPodCompatible"] boolValue];
102
103        self.alignAVStart = [preset[@"AlignAVStart"] boolValue];
104
105        self.chaptersEnabled = [preset[@"ChapterMarkers"] boolValue];
106        self.metadataPassthru = [preset[@"MetadataPassthrough"] boolValue];
107
108        [self.audio applyPreset:preset jobSettings:jobSettings];
109        [self.subtitles applyPreset:preset jobSettings:jobSettings];
110        [self.video applyPreset:preset jobSettings:jobSettings];
111        [self.picture applyPreset:preset jobSettings:jobSettings];
112        [self.filters applyPreset:preset jobSettings:jobSettings];
113
114        return YES;
115    }
116    else
117    {
118        if (outError != NULL)
119        {
120            *outError = [NSError errorWithDomain:@"HBError" code:0 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Invalid preset", @"HBJob -> invalid preset"),
121                                                                          NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The preset is not a valid, try to select a different one.", @"Job preset -> invalid preset recovery suggestion")}];
122        }
123
124        return NO;
125    }
126}
127
128- (void)writeToPreset:(HBMutablePreset *)preset
129{
130    preset.name = self.presetName;
131
132    preset[@"FileFormat"] = @(hb_container_get_short_name(self.container));
133
134    // MP4 specifics options.
135    preset[@"Mp4HttpOptimize"] = @(self.mp4HttpOptimize);
136    preset[@"AlignAVStart"] = @(self.alignAVStart);
137    preset[@"Mp4iPodCompatible"] = @(self.mp4iPodCompatible);
138
139    preset[@"ChapterMarkers"] = @(self.chaptersEnabled);
140    preset[@"MetadataPassthrough"] = @(self.metadataPassthru);
141
142    [@[self.video, self.filters, self.picture, self.audio, self.subtitles] makeObjectsPerformSelector:@selector(writeToPreset:)
143                                                                                                           withObject:preset];
144}
145
146- (void)setUndo:(NSUndoManager *)undo
147{
148    _undo = undo;
149    [@[self.video, self.range, self.filters, self.picture, self.audio, self.subtitles] makeObjectsPerformSelector:@selector(setUndo:)
150                                                                                                       withObject:_undo];
151    [self.chapterTitles makeObjectsPerformSelector:@selector(setUndo:) withObject:_undo];
152}
153
154- (void)setPresetName:(NSString *)presetName
155{
156    if (![presetName isEqualToString:_presetName])
157    {
158        [[self.undo prepareWithInvocationTarget:self] setPresetName:_presetName];
159    }
160    _presetName = [presetName copy];
161}
162
163- (void)setOutputURL:(NSURL *)outputURL
164{
165    if (![outputURL isEqualTo:_outputURL])
166    {
167        [[self.undo prepareWithInvocationTarget:self] setOutputURL:_outputURL];
168    }
169    _outputURL = [outputURL copy];
170
171#ifdef __SANDBOX_ENABLED__
172    // Clear the bookmark to regenerate it later
173    self.outputURLFolderBookmark = nil;
174#endif
175}
176
177- (void)setOutputFileName:(NSString *)outputFileName
178{
179    if (![outputFileName isEqualTo:_outputFileName])
180    {
181        [[self.undo prepareWithInvocationTarget:self] setOutputFileName:_outputFileName];
182    }
183    _outputFileName = [outputFileName copy];
184}
185
186- (BOOL)validateOutputFileName:(id *)ioValue error:(NSError * __autoreleasing *)outError
187{
188    BOOL retval = YES;
189
190    if (nil != *ioValue)
191    {
192        NSString *value = *ioValue;
193
194        if ([value rangeOfString:@"/"].location != NSNotFound)
195        {
196            if (outError)
197            {
198                *outError = [NSError errorWithDomain:@"HBError" code:0 userInfo:@{NSLocalizedDescriptionKey: HBKitLocalizedString(@"Invalid name", @"HBJob -> invalid name error description"),
199                                                                                  NSLocalizedRecoverySuggestionErrorKey: HBKitLocalizedString(@"The file name can't contain the / character.", @"HBJob -> invalid name error recovery suggestion")}];
200            }
201            return NO;
202        }
203        if (value.length == 0)
204        {
205            if (outError)
206            {
207                *outError = [NSError errorWithDomain:@"HBError" code:0 userInfo:@{NSLocalizedDescriptionKey: HBKitLocalizedString(@"Invalid name", @"HBJob -> invalid name error description"),
208                                                                                  NSLocalizedRecoverySuggestionErrorKey: HBKitLocalizedString(@"The file name can't be empty.", @"HBJob -> invalid name error recovery suggestion")}];
209            }
210            return NO;
211        }
212    }
213
214    if (*ioValue == nil)
215    {
216        if (outError)
217        {
218            *outError = [NSError errorWithDomain:@"HBError" code:0 userInfo:@{NSLocalizedDescriptionKey: HBKitLocalizedString(@"Invalid name", @"HBJob -> invalid name error description"),
219                                                                              NSLocalizedRecoverySuggestionErrorKey: HBKitLocalizedString(@"The file name can't be empty.", @"HBJob -> invalid name error recovery suggestion")}];
220        }
221        return NO;
222    }
223
224    return retval;
225}
226
227- (NSURL *)completeOutputURL
228{
229    return [self.outputURL URLByAppendingPathComponent:self.outputFileName];
230}
231
232- (void)setContainer:(int)container
233{
234    if (container != _container)
235    {
236        [[self.undo prepareWithInvocationTarget:self] setContainer:_container];
237    }
238
239    _container = container;
240
241    [self.audio setContainer:container];
242    [self.subtitles setContainer:container];
243    [self.video containerChanged];
244
245    // post a notification for any interested observers to indicate that our video container has changed
246    [[NSNotificationCenter defaultCenter] postNotificationName:HBContainerChangedNotification object:self];
247}
248
249- (void)setAngle:(int)angle
250{
251    if (angle != _angle)
252    {
253        [[self.undo prepareWithInvocationTarget:self] setAngle:_angle];
254    }
255    _angle = angle;
256}
257
258- (void)setTitle:(HBTitle *)title
259{
260    _title = title;
261    self.range.title = title;
262}
263
264- (void)setMp4HttpOptimize:(BOOL)mp4HttpOptimize
265{
266    if (mp4HttpOptimize != _mp4HttpOptimize)
267    {
268        [[self.undo prepareWithInvocationTarget:self] setMp4HttpOptimize:_mp4HttpOptimize];
269    }
270    _mp4HttpOptimize = mp4HttpOptimize;
271
272    [[NSNotificationCenter defaultCenter] postNotificationName:HBContainerChangedNotification object:self];
273}
274
275- (void)setAlignAVStart:(BOOL)alignAVStart
276{
277    if (alignAVStart != _alignAVStart)
278    {
279        [[self.undo prepareWithInvocationTarget:self] setAlignAVStart:_alignAVStart];
280    }
281    _alignAVStart = alignAVStart;
282
283    [[NSNotificationCenter defaultCenter] postNotificationName:HBContainerChangedNotification object:self];
284}
285
286- (void)setMp4iPodCompatible:(BOOL)mp4iPodCompatible
287{
288    if (mp4iPodCompatible != _mp4iPodCompatible)
289    {
290        [[self.undo prepareWithInvocationTarget:self] setMp4iPodCompatible:_mp4iPodCompatible];
291    }
292    _mp4iPodCompatible = mp4iPodCompatible;
293
294    [[NSNotificationCenter defaultCenter] postNotificationName:HBContainerChangedNotification object:self];
295}
296
297- (void)setChaptersEnabled:(BOOL)chaptersEnabled
298{
299    if (chaptersEnabled != _chaptersEnabled)
300    {
301        [[self.undo prepareWithInvocationTarget:self] setChaptersEnabled:_chaptersEnabled];
302    }
303    _chaptersEnabled = chaptersEnabled;
304    [[NSNotificationCenter defaultCenter] postNotificationName:HBChaptersChangedNotification object:self];
305}
306
307- (void)setMetadataPassthru:(BOOL)metadataPassthru
308{
309    if (metadataPassthru != _metadataPassthru)
310    {
311        [[self.undo prepareWithInvocationTarget:self] setMetadataPassthru:_metadataPassthru];
312    }
313    _metadataPassthru = metadataPassthru;
314    [[NSNotificationCenter defaultCenter] postNotificationName:HBContainerChangedNotification object:self];
315}
316
317- (NSString *)description
318{
319    return self.name;
320}
321
322- (void)refreshSecurityScopedResources
323{
324    if (_fileURLBookmark)
325    {
326        NSURL *resolvedURL = [HBUtilities URLFromBookmark:_fileURLBookmark];
327        if (resolvedURL)
328        {
329            _fileURL = resolvedURL;
330        }
331    }
332    if (_outputURLFolderBookmark)
333    {
334        NSURL *resolvedURL = [HBUtilities URLFromBookmark:_outputURLFolderBookmark];
335        if (resolvedURL)
336        {
337            _outputURL = resolvedURL;
338        }
339    }
340    [self.subtitles refreshSecurityScopedResources];
341}
342
343- (BOOL)startAccessingSecurityScopedResource
344{
345#ifdef __SANDBOX_ENABLED__
346    if (self.accessCount == 0)
347    {
348        self.fileURLToken = [HBSecurityAccessToken tokenWithObject:self.fileURL];
349        self.outputURLToken = [HBSecurityAccessToken tokenWithObject:self.outputURL];
350        self.subtitlesToken = [HBSecurityAccessToken tokenWithObject:self.subtitles];
351    }
352    self.accessCount += 1;
353    return YES;
354#else
355    return NO;
356#endif
357}
358
359- (void)stopAccessingSecurityScopedResource
360{
361#ifdef __SANDBOX_ENABLED__
362    self.accessCount -= 1;
363    NSAssert(self.accessCount >= 0, @"[HBJob stopAccessingSecurityScopedResource:] unbalanced call");
364    if (self.accessCount == 0)
365    {
366        self.fileURLToken = nil;
367        self.outputURLToken = nil;
368        self.subtitlesToken = nil;
369    }
370#endif
371}
372
373#pragma mark - NSCopying
374
375- (instancetype)copyWithZone:(NSZone *)zone
376{
377    HBJob *copy = [[[self class] alloc] init];
378
379    if (copy)
380    {
381        copy->_name = [_name copy];
382        copy->_presetName = [_presetName copy];
383        copy->_titleIdx = _titleIdx;
384
385        copy->_fileURLBookmark = [_fileURLBookmark copy];
386        copy->_outputURLFolderBookmark = [_outputURLFolderBookmark copy];
387
388        copy->_fileURL = [_fileURL copy];
389        copy->_outputURL = [_outputURL copy];
390        copy->_outputFileName = [_outputFileName copy];
391
392        copy->_container = _container;
393        copy->_angle = _angle;
394        copy->_mp4HttpOptimize = _mp4HttpOptimize;
395        copy->_mp4iPodCompatible = _mp4iPodCompatible;
396        copy->_alignAVStart = _alignAVStart;
397
398        copy->_range = [_range copy];
399        copy->_video = [_video copy];
400        copy->_picture = [_picture copy];
401        copy->_filters = [_filters copy];
402
403        copy->_video.job = copy;
404
405        copy->_audio = [_audio copy];
406        copy->_audio.job = copy;
407        copy->_subtitles = [_subtitles copy];
408        copy->_subtitles.job = copy;
409
410        copy->_chaptersEnabled = _chaptersEnabled;
411        copy->_chapterTitles = [[NSArray alloc] initWithArray:_chapterTitles copyItems:YES];
412
413        copy->_metadataPassthru = _metadataPassthru;
414    }
415
416    return copy;
417}
418
419#pragma mark - NSCoding
420
421+ (BOOL)supportsSecureCoding
422{
423    return YES;
424}
425
426- (void)encodeWithCoder:(NSCoder *)coder
427{
428    [coder encodeInt:5 forKey:@"HBJobVersion"];
429
430    encodeObject(_name);
431    encodeObject(_presetName);
432    encodeInt(_titleIdx);
433
434#ifdef __SANDBOX_ENABLED__
435    if (!_fileURLBookmark)
436    {
437        _fileURLBookmark = [HBUtilities bookmarkFromURL:_fileURL
438                                                options:NSURLBookmarkCreationWithSecurityScope |
439                                                        NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess];
440    }
441
442    encodeObject(_fileURLBookmark);
443
444    if (!_outputURLFolderBookmark)
445    {
446        __attribute__((unused)) HBSecurityAccessToken *token = [HBSecurityAccessToken tokenWithObject:_outputURL];
447        _outputURLFolderBookmark = [HBUtilities bookmarkFromURL:_outputURL];
448        token = nil;
449    }
450
451    encodeObject(_outputURLFolderBookmark);
452
453#endif
454
455    encodeObject(_fileURL);
456    encodeObject(_outputURL);
457    encodeObject(_outputFileName);
458
459    encodeInt(_container);
460    encodeInt(_angle);
461    encodeBool(_mp4HttpOptimize);
462    encodeBool(_mp4iPodCompatible);
463    encodeBool(_alignAVStart);
464
465    encodeObject(_range);
466    encodeObject(_video);
467    encodeObject(_picture);
468    encodeObject(_filters);
469
470    encodeObject(_audio);
471    encodeObject(_subtitles);
472
473    encodeBool(_chaptersEnabled);
474    encodeObject(_chapterTitles);
475
476    encodeBool(_metadataPassthru);
477}
478
479- (instancetype)initWithCoder:(NSCoder *)decoder
480{
481    int version = [decoder decodeIntForKey:@"HBJobVersion"];
482
483    if (version == 5 && (self = [super init]))
484    {
485        decodeObjectOrFail(_name, NSString);
486        decodeObjectOrFail(_presetName, NSString);
487        decodeInt(_titleIdx); if (_titleIdx < 0) { goto fail; }
488
489#ifdef __SANDBOX_ENABLED__
490        decodeObject(_fileURLBookmark, NSData)
491        decodeObject(_outputURLFolderBookmark, NSData)
492#endif
493        decodeObjectOrFail(_fileURL, NSURL);
494        decodeObject(_outputURL, NSURL);
495        decodeObject(_outputFileName, NSString);
496
497        decodeInt(_container); if (_container != HB_MUX_MP4 && _container != HB_MUX_MKV && _container != HB_MUX_WEBM) { goto fail; }
498        decodeInt(_angle); if (_angle < 0) { goto fail; }
499        decodeBool(_mp4HttpOptimize);
500        decodeBool(_mp4iPodCompatible);
501        decodeBool(_alignAVStart);
502
503        decodeObjectOrFail(_range, HBRange);
504        decodeObjectOrFail(_video, HBVideo);
505        decodeObjectOrFail(_picture, HBPicture);
506        decodeObjectOrFail(_filters, HBFilters);
507
508        _video.job = self;
509
510        decodeObjectOrFail(_audio, HBAudio);
511        decodeObjectOrFail(_subtitles, HBSubtitles);
512
513        _audio.job = self;
514        _video.job = self;
515
516        decodeBool(_chaptersEnabled);
517        decodeCollectionOfObjectsOrFail(_chapterTitles, NSArray, HBChapter);
518
519        decodeBool(_metadataPassthru);
520
521        return self;
522    }
523
524fail:
525    return nil;
526}
527
528@end
529