/* HBPresets.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 "HBPresetsManager.h" #import "HBPreset.h" #import "HBUtilities.h" #import "NSJSONSerialization+HBAdditions.h" #include "preset.h" NSString *HBPresetsChangedNotification = @"HBPresetsChangedNotification"; @interface HBPresetsManager () @property (nonatomic, readonly, copy) NSURL *fileURL; @end @implementation HBPresetsManager - (instancetype)init { self = [super init]; if (self) { // Init the root of the tree, it won't never be shown in the UI _root = [[HBPreset alloc] initWithCategoryName:@"Root" builtIn:YES]; _root.delegate = self; } return self; } - (instancetype)initWithURL:(NSURL *)url { self = [self init]; if (self) { _fileURL = [url copy]; [self loadPresetsFromURL:url]; } return self; } #pragma mark - HBTreeNode delegate - (void)nodeDidChange:(id)node { [[NSNotificationCenter defaultCenter] postNotificationName:HBPresetsChangedNotification object:nil]; } - (void)treeDidRemoveNode:(id)node { if (node == self.defaultPreset) { // Select a new default preset [self selectNewDefault]; } } #pragma mark - Load/Save /** * Loads the old presets format (0.10 and earlier) plist * * @param url the url of the plist */ - (void)loadOldPresetsFromURL:(NSURL *)url { NSError *error; HBPreset *oldPresets = [[HBPreset alloc] initWithContentsOfURL:url error:&error]; for (HBPreset *preset in oldPresets.children) { [self.root insertObject:preset inChildrenAtIndex:self.root.countOfChildren]; } } - (BOOL)isNewer:(NSDictionary *)dict { int major, minor, micro; hb_presets_current_version(&major, &minor, µ); if ([dict[@"VersionMajor"] intValue] > major || ([dict[@"VersionMajor"] intValue] == major && [dict[@"VersionMinor"] intValue] > minor) || ([dict[@"VersionMajor"] intValue] == major && [dict[@"VersionMinor"] intValue] == minor && [dict[@"VersionMicro"] intValue] > micro)) { return YES; } return NO; } - (BOOL)isOutOfDate:(NSDictionary *)dict { int major, minor, micro; hb_presets_current_version(&major, &minor, µ); if (major != [dict[@"VersionMajor"] intValue] || minor != [dict[@"VersionMinor"] intValue] || micro != [dict[@"VersionMicro"] intValue]) { return YES; } return NO; } - (NSURL *)backupURLfromURL:(NSURL *)url versionMajor:(int)major minor:(int)minor micro:(int)micro { NSString *backupName = [NSString stringWithFormat:@"%@.%d.%d.%d.json", url.lastPathComponent.stringByDeletingPathExtension, major, minor, micro]; return [url.URLByDeletingLastPathComponent URLByAppendingPathComponent:backupName]; } typedef NS_ENUM(NSUInteger, HBPresetLoadingResult) { HBPresetLoadingResultOK, HBPresetLoadingResultOKUpgraded, HBPresetLoadingResultFailedNewer, HBPresetLoadingResultFailed }; - (NSDictionary *)dictionaryWithPresetsAtURL:(NSURL *)url backup:(BOOL)backup result:(HBPresetLoadingResult *)result; { NSData *presetData = [[NSData alloc] initWithContentsOfURL:url]; if (presetData) { const char *json = [[NSString alloc] initWithData:presetData encoding:NSUTF8StringEncoding].UTF8String; NSDictionary *presetsDict = [NSJSONSerialization HB_JSONObjectWithUTF8String:json options:0 error:NULL]; if ([presetsDict isKindOfClass:[NSDictionary class]]) { if ([self isNewer:presetsDict]) { *result = HBPresetLoadingResultFailedNewer; return nil; } else if ([self isOutOfDate:presetsDict]) { // There is a new presets file format, // make a backup of the old one for the previous versions. if (backup) { NSURL *backupURL = [self backupURLfromURL:self.fileURL versionMajor:[presetsDict[@"VersionMajor"] intValue] minor:[presetsDict[@"VersionMinor"] intValue] micro:[presetsDict[@"VersionMicro"] intValue]]; [[NSFileManager defaultManager] copyItemAtURL:url toURL:backupURL error:NULL]; } // Update the presets to the current format. char *updatedJson; hb_presets_import_json(json, &updatedJson); presetsDict = [NSJSONSerialization HB_JSONObjectWithUTF8String:updatedJson options:0 error:NULL]; free(updatedJson); *result = HBPresetLoadingResultOKUpgraded; } else { // Else, clean up the presets just to be sure // it's in the right format. char *cleanedJson = hb_presets_clean_json(json); presetsDict = [NSJSONSerialization HB_JSONObjectWithUTF8String:cleanedJson options:0 error:NULL]; free(cleanedJson); *result = HBPresetLoadingResultOK; } return presetsDict; } } *result = HBPresetLoadingResultFailed; return nil; } - (void)loadPresetsFromURL:(NSURL *)url; { HBPresetLoadingResult result; NSDictionary *presetsDict; // Load the presets presetsDict = [self dictionaryWithPresetsAtURL:url backup:YES result:&result]; if (result == HBPresetLoadingResultFailedNewer) { // Try a backup int major, minor, micro; hb_presets_current_version(&major, &minor, µ); NSURL *backupURL = [self backupURLfromURL:url versionMajor:major minor:minor micro:micro]; HBPresetLoadingResult backupResult; presetsDict = [self dictionaryWithPresetsAtURL:backupURL backup:NO result:&backupResult]; // Change the fileURL so we don't overwrite // the latest version presets. _fileURL = backupURL; } // Add the presets to the root if (presetsDict) { for (NSDictionary *child in presetsDict[@"PresetList"]) { [self.root insertObject:[[HBPreset alloc] initWithDictionary:child] inChildrenAtIndex:self.root.countOfChildren]; } if (result == HBPresetLoadingResultOKUpgraded) { [self generateBuiltInPresets]; } } // If there is no json presets file try to read the old plist if (result == HBPresetLoadingResultFailed && presetsDict == nil) { // Try to load to load the old presets file // if the new one is empty [self loadOldPresetsFromURL:[url.URLByDeletingPathExtension URLByAppendingPathExtension:@"plist"]]; [self generateBuiltInPresets]; } // If the preset list contains no leaf, // add back the built in presets. __block BOOL leafFound = NO; [self.root enumerateObjectsUsingBlock:^(id obj, NSIndexPath *idx, BOOL *stop) { if ([obj isLeaf]) { leafFound = YES; *stop = YES; } }]; if (!leafFound) { [self generateBuiltInPresets]; } [self selectNewDefault]; } - (BOOL)savePresetsToURL:(NSURL *)url { return [self.root writeToURL:url atomically:YES format:HBPresetFormatJson removeRoot:YES]; } - (BOOL)savePresets { return [self savePresetsToURL:self.fileURL]; } #pragma mark - Presets Management - (void)addPreset:(HBPreset *)preset { // Make sure no preset has the default flag enabled. [preset enumerateObjectsUsingBlock:^(id obj, NSIndexPath *idx, BOOL *stop) { [obj setIsDefault:NO]; }]; [self.root insertObject:preset inChildrenAtIndex:[self.root countOfChildren]]; [self savePresets]; } - (void)deletePresetAtIndexPath:(NSIndexPath *)idx { [self.root removeObjectAtIndexPath:idx]; } - (NSIndexPath *)indexPathOfPreset:(HBPreset *)preset { return [self.root indexPathOfObject:preset]; } #pragma mark - Default preset /** * Private method to select a new default after the default preset is deleted * or when the built-in presets are regenerated. */ - (void)selectNewDefault { __block HBPreset *normalPreset = nil; __block HBPreset *firstUserPreset = nil; __block HBPreset *firstBuiltInPreset = nil; __block BOOL defaultAlreadySetted = NO; // Search for a possible new default preset // Try to use "Normal" or the first user preset. [self.root enumerateObjectsUsingBlock:^(id obj, NSIndexPath *idx, BOOL *stop) { if ([obj isBuiltIn] && [obj isLeaf]) { if ([[obj name] isEqualToString:@"Fast 1080p30"]) { normalPreset = obj; } if (firstBuiltInPreset == nil) { firstBuiltInPreset = obj; } } else if ([obj isLeaf] && firstUserPreset == nil) { firstUserPreset = obj; } if ([obj isDefault]) { self.defaultPreset = obj; defaultAlreadySetted = YES; } [obj setIsDefault:NO]; }]; if (defaultAlreadySetted) { self.defaultPreset.isDefault = YES; return; } else if (normalPreset) { self.defaultPreset = normalPreset; normalPreset.isDefault = YES; } else if (firstUserPreset) { self.defaultPreset = firstUserPreset; firstUserPreset.isDefault = YES; } else if (firstBuiltInPreset) { self.defaultPreset = firstBuiltInPreset; firstBuiltInPreset.isDefault = YES; } } - (void)setDefaultPreset:(HBPreset *)defaultPreset { if (defaultPreset && defaultPreset.isLeaf) { if (_defaultPreset) { _defaultPreset.isDefault = NO; } defaultPreset.isDefault = YES; _defaultPreset = defaultPreset; } } #pragma mark - Built In Generation /** * Built-in preset categories at the root of the hierarchy */ - (void)generateBuiltInPresets { // Load the built-in presets from libhb. const char *presets = hb_presets_builtin_get_json(); NSData *presetsData = [NSData dataWithBytes:presets length:strlen(presets)]; NSError *error = nil; NSArray *presetsArray = [NSJSONSerialization JSONObjectWithData:presetsData options:NSJSONReadingAllowFragments error:&error]; if (presetsArray.count == 0) { [HBUtilities writeToActivityLog:"failed to update built-in presets"]; if (error) { [HBUtilities writeToActivityLog:"Error raised:\n%s", error.localizedFailureReason]; } } else { [self deleteBuiltInPresets]; for (NSDictionary *child in presetsArray.reverseObjectEnumerator) { HBPreset *preset = [[HBPreset alloc] initWithDictionary:child]; [self.root insertObject:preset inChildrenAtIndex:0]; } // set a new Default preset [self selectNewDefault]; [HBUtilities writeToActivityLog: "built-in presets updated"]; } } - (void)deleteBuiltInPresets { [self willChangeValueForKey:@"root"]; NSMutableArray *nodeToRemove = [[NSMutableArray alloc] init]; for (HBPreset *node in self.root.children) { if (node.isBuiltIn) { [nodeToRemove addObject:node]; } } [self.root.children removeObjectsInArray:nodeToRemove]; [self didChangeValueForKey:@"root"]; } @end