/* HBPreset.m $ This file is part of the HandBrake source code. Homepage: . It may be used under the terms of the GNU General Public License. */ #import "HBPreset.h" #import "HBMutablePreset.h" #include "preset.h" #import "NSJSONSerialization+HBAdditions.h" #import "HBLocalizationUtilities.h" @interface HBPreset () /// The actual content of the preset. @property (nonatomic, strong, nullable) NSMutableDictionary *content; @end @implementation HBPreset - (instancetype)init { self = [super init]; if (self) { _name = @"New Preset"; _presetDescription = @""; _content = [[NSMutableDictionary alloc] init]; self.isLeaf = YES; } return self; } - (instancetype)initWithPreset:(HBPreset *)preset { self = [super init]; if (self) { _name = preset.name; _presetDescription = preset.presetDescription; _content = [preset.content mutableCopy]; self.isLeaf = preset.isLeaf; for (HBPreset *child in preset.children) { HBPreset *mutableChild = [[[self class] alloc] initWithPreset:child]; [self insertObject:mutableChild inChildrenAtIndex:0]; } } return self; } - (instancetype)initWithName:(NSString *)title content:(NSDictionary *)content builtIn:(BOOL)builtIn { self = [self init]; if (self) { _name = [title copy]; _isBuiltIn = builtIn; _content = [content mutableCopy]; if ([content[@"PresetDescription"] isKindOfClass:[NSString class]]) { _presetDescription = [content[@"PresetDescription"] copy]; } } return self; } - (instancetype)initWithCategoryName:(NSString *)title builtIn:(BOOL)builtIn { self = [self init]; if (self) { _name = [title copy]; _isBuiltIn = builtIn; self.isLeaf = NO; } return self; } - (instancetype)initWithDictionary:(NSDictionary *)dict { NSParameterAssert(dict); NSString *name = dict[@"PresetName"] ? dict[@"PresetName"] : @"Unnamed preset"; BOOL builtIn = [dict[@"Type"] boolValue] ? NO : YES; BOOL defaultPreset = [dict[@"Default"] boolValue]; if ([dict[@"Folder"] boolValue]) { self = [self initWithCategoryName:name builtIn:builtIn]; for (NSDictionary *childDict in [dict[@"ChildrenArray"] reverseObjectEnumerator]) { HBPreset *childPreset = [[HBPreset alloc] initWithDictionary:childDict]; [self insertObject:childPreset inChildrenAtIndex:0]; } } else { self = [self initWithName:name content:dict builtIn:builtIn]; self.isDefault = defaultPreset; } return self; } - (nullable instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError * __autoreleasing *)outError { NSParameterAssert(url); NSArray *presetsArray; NSString *presetsString; // Read a json file or the old plists format if ([url.pathExtension isEqualToString:@"json"]) { NSData *data = [[NSData alloc] initWithContentsOfURL:url]; presetsString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; } else { NSArray *array = [[NSArray alloc] initWithContentsOfURL:url]; if ([NSJSONSerialization isValidJSONObject:array]) { presetsString = [NSJSONSerialization HB_StringWithJSONObject:array options:0 error:NULL]; } } // Run the json through the libhb import function // to avoid importing unknowns keys. if (presetsString.length) { char *importedJson; hb_presets_import_json(presetsString.UTF8String, &importedJson); if (importedJson) { id importedPresets = [NSJSONSerialization HB_JSONObjectWithUTF8String:importedJson options:0 error:NULL]; free(importedJson); if ([importedPresets isKindOfClass:[NSDictionary class]]) { int hb_major, hb_minor, hb_micro; int major; hb_presets_current_version(&hb_major, &hb_minor, &hb_micro); major = [importedPresets[@"VersionMajor"] intValue]; if (major <= hb_major) { presetsArray = importedPresets[@"PresetList"]; } else { // Change in major indicates non-backward compatible preset changes. if (outError) { *outError = [self newerPresetErrorForUrl:url]; } return nil; } } else if ([importedPresets isKindOfClass:[NSArray class]]) { presetsArray = importedPresets; } } } // Convert the array to a HBPreset tree. if (presetsArray.count) { self = [self initWithCategoryName:@"Imported Presets" builtIn:NO]; if (self) { for (NSDictionary *dict in presetsArray) { HBPreset *preset = [[HBPreset alloc] initWithDictionary:dict]; [self.children addObject:preset]; } } return self; } else if (outError) { *outError = [self invalidPresetErrorForUrl:url]; } return nil; } - (NSError *)invalidPresetErrorForUrl:(NSURL *)url { NSString *description = [NSString stringWithFormat:HBKitLocalizedString(@"The preset \"%@\" could not be imported.", @"Preset -> import error description"), url.lastPathComponent]; NSString *reason = HBKitLocalizedString(@"The selected preset is invalid.", @"Preset -> import error reason"); return [NSError errorWithDomain:@"HBPresetDomain" code:1 userInfo:@{NSLocalizedDescriptionKey: description, NSLocalizedRecoverySuggestionErrorKey: reason}]; } - (NSError *)newerPresetErrorForUrl:(NSURL *)url { NSString *description = [NSString stringWithFormat:HBKitLocalizedString(@"The preset \"%@\" could not be imported.", @"Preset -> import error description"), url.lastPathComponent]; NSString *reason = HBKitLocalizedString(@"The selected preset was created with a newer version of HandBrake.", @"Preset -> import error reason"); return [NSError errorWithDomain:@"HBPresetDomain" code:2 userInfo:@{NSLocalizedDescriptionKey: description, NSLocalizedRecoverySuggestionErrorKey: reason}]; } /** * A dictionary representation of the preset. */ - (NSDictionary *)dictionary { NSMutableDictionary *output = [[NSMutableDictionary alloc] init]; [output addEntriesFromDictionary:self.content]; output[@"PresetName"] = self.name; output[@"PresetDescription"] = self.presetDescription; output[@"Folder"] = [NSNumber numberWithBool:!self.isLeaf]; output[@"Type"] = @(!self.isBuiltIn); output[@"Default"] = @(self.isDefault); if (!self.isLeaf) { NSMutableArray *childArray = [[NSMutableArray alloc] init]; for (HBPreset *child in self.children) { [childArray addObject:[child dictionary]]; } output[@"ChildrenArray"] = childArray; } return output; } - (BOOL)writeToURL:(NSURL *)url atomically:(BOOL)atomically format:(HBPresetFormat)format removeRoot:(BOOL)removeRoot { BOOL success = NO; NSArray *presetList; if (removeRoot) { presetList = self.dictionary[@"ChildrenArray"]; } else { presetList = @[self.dictionary]; } int major, minor, micro; hb_presets_current_version(&major, &minor, µ); NSDictionary *dict = @{ @"PresetList": presetList, @"VersionMajor": @(major), @"VersionMinor": @(minor), @"VersionMicro": @(micro) }; if (format == HBPresetFormatPlist) { success = [dict writeToURL:url atomically:atomically]; } else { NSUInteger sortKeys = (1UL << 1); // NSJSONWritingSortedKeys in 10.13 sdk; NSData *jsonPreset = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted | sortKeys error:NULL]; success = [jsonPreset writeToURL:url atomically:atomically]; } return success; } - (void)cleanUp { // Run the libhb clean function NSString *presetJson = [NSJSONSerialization HB_StringWithJSONObject:self.dictionary options:0 error:NULL]; if (presetJson.length) { char *cleanedJson = hb_presets_clean_json(presetJson.UTF8String); NSDictionary *cleanedDict = [NSJSONSerialization HB_JSONObjectWithUTF8String:cleanedJson options:0 error:NULL]; free(cleanedJson); if ([cleanedDict isKindOfClass:[NSDictionary class]]) { self.content = [cleanedDict mutableCopy]; } } } #pragma mark - NSCopying - (id)copyWithZone:(NSZone *)zone { HBPreset *node = [super copyWithZone:zone]; node->_name = [self.name copy]; node->_content = [self.content mutableCopy]; node->_presetDescription = [self.presetDescription copy]; return node; } - (id)mutableCopyWithZone:(NSZone *)zone { return [[HBMutablePreset allocWithZone:zone] initWithPreset:self]; } - (NSUInteger)hash { return self.name.hash + self.isBuiltIn + self.isLeaf; } #pragma mark - Properties - (void)setName:(NSString *)name { _name = [name copy]; [self.delegate nodeDidChange:self]; } - (void)setIsDefault:(BOOL)isDefault { _isDefault = isDefault; [self.delegate nodeDidChange:self]; } #pragma mark - Keys - (id)objectForKey:(NSString *)key { return [_content objectForKey:key]; } - (nullable id)objectForKeyedSubscript:(NSString *)key { return _content[key]; } #pragma mark - KVC - (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError { // Return an error is the name is empty if (![*ioValue length]) { if (outError) { *outError = [[NSError alloc] initWithDomain:@"HBErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey:@"The preset title cannot be empty.", NSLocalizedRecoverySuggestionErrorKey:@"Please enter a title."}]; } return NO; } return YES; } @end