/* HBUtilities.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 "HBUtilities.h" #import "HBDirectUtilities.h" #import #include "handbrake/lang.h" static BOOL hb_resolveBookmarks = YES; HB_OBJC_DIRECT_MEMBERS @interface HBURLPair : NSObject @property (nonatomic) NSURL *URL; @property (nonatomic) NSURL *volumeURL; @end HB_OBJC_DIRECT_MEMBERS @implementation HBURLPair @end @implementation HBUtilities + (NSString *)handBrakeVersion { NSDictionary *infoDictionary = NSBundle.mainBundle.infoDictionary; return [NSString stringWithFormat:@"Handbrake Version: %@ (%@)", infoDictionary[@"CFBundleShortVersionString"], infoDictionary[@"CFBundleVersion"]]; } + (NSURL *)appSupportURL { NSFileManager *fileManager = NSFileManager.defaultManager; NSURL *appSupportURL = [[[fileManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] firstObject] URLByAppendingPathComponent:@"HandBrake" isDirectory:YES]; if (appSupportURL) { [fileManager createDirectoryAtURL:appSupportURL withIntermediateDirectories:YES attributes:nil error:NULL]; } return appSupportURL; } + (NSURL *)defaultDestinationFolderURL { return [[NSFileManager.defaultManager URLsForDirectory:NSMoviesDirectory inDomains:NSUserDomainMask] firstObject]; } + (NSURL *)documentationURL { return [NSURL URLWithString:@"https://handbrake.fr/docs/"]; } + (NSURL *)documentationBaseURL { return [NSURL URLWithString:@"https://handbrake.fr/docs/en/latest/"]; } + (void)writeToActivityLog:(const char *)format, ... { va_list args; va_start(args, format); if (format != nil) { char str[1024]; vsnprintf(str, 1024, format, args); time_t _now = time(NULL); struct tm *now = localtime(&_now); fprintf(stderr, "[%02d:%02d:%02d] macgui: %s\n", now->tm_hour, now->tm_min, now->tm_sec, str); } va_end(args); } + (void)writeErrorToActivityLog:(NSError *)error { [self writeToActivityLog:"Error domain: %s", error.domain.UTF8String]; [self writeToActivityLog:"Error code: %d", error.code]; if (error.localizedDescription) { [self writeToActivityLog:"Error description: %s", error.localizedDescription.UTF8String]; } if (error.debugDescription) { [self writeToActivityLog:"Error debug description: %s", error.debugDescription.UTF8String]; } } + (void)writeToActivityLogWithNoHeader:(NSString *)text { fprintf(stderr, "%s", text.UTF8String); } + (void)setResolveBookmarks:(BOOL)resolveBookmarks { hb_resolveBookmarks = resolveBookmarks; } + (BOOL)resolveBookmarks { return hb_resolveBookmarks; } + (nullable NSURL *)URLFromBookmark:(NSData *)bookmark { if (hb_resolveBookmarks == NO) { return nil; } NSParameterAssert(bookmark); NSError *error; BOOL isStale; NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope | NSURLBookmarkResolutionWithoutMounting relativeToURL:nil bookmarkDataIsStale:&isStale error:&error]; if (error) { [HBUtilities writeErrorToActivityLog:error]; } return isStale ? nil : url; } + (nullable NSData *)bookmarkFromURL:(NSURL *)url options:(NSURLBookmarkCreationOptions)options { NSParameterAssert(url); NSError *error; NSData *bookmark = [url bookmarkDataWithOptions:options includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; if (error) { NSString *error_message = [NSString stringWithFormat:@"Failed to create bookmark: %@", error]; [HBUtilities writeToActivityLog:"%s", error_message.UTF8String]; } return bookmark; } + (nullable NSData *)bookmarkFromURL:(NSURL *)url { return [HBUtilities bookmarkFromURL:url options:NSURLBookmarkCreationWithSecurityScope]; } + (NSURL *)commonParentOfURL:(NSURL *)a withURL:(NSURL *)b { if ([a isEqualTo:b]) { return a; } if ([b.path hasPrefix:a.path]) { return a; } NSArray *aComponents = a.pathComponents; NSArray *bComponents = b.pathComponents; NSMutableArray *commonComponents = [NSMutableArray array]; for (int idx = 0; idx < aComponents.count && idx < bComponents.count; idx++) { NSString *aComponent = aComponents[idx]; NSString *bComponent = bComponents[idx]; if ([aComponent isEqualToString:bComponent]) { [commonComponents addObject:aComponent]; } else { break; } } return [NSURL fileURLWithPathComponents:commonComponents]; } + (NSArray *)baseURLs:(NSArray *)fileURLs { NSMutableArray *pairs = [[NSMutableArray alloc] init]; NSMutableSet *volumeURLs = [[NSMutableSet alloc] init]; for (NSURL *fileURL in fileURLs) { NSURL *volumeURL = nil; [fileURL getResourceValue:&volumeURL forKey:NSURLVolumeURLKey error:nil]; if (volumeURL) { HBURLPair *pair = [[HBURLPair alloc] init]; pair.URL = fileURL.URLByDeletingLastPathComponent; pair.volumeURL = volumeURL; [pairs addObject:pair]; [volumeURLs addObject:volumeURL]; } } NSMutableArray *baseURLs = [[NSMutableArray alloc] init]; for (NSURL *volumeURL in volumeURLs) { NSURL *baseURL = nil; for (HBURLPair *pair in pairs) { if ([pair.volumeURL isEqualTo:volumeURL]) { if (baseURL == nil) { baseURL = pair.URL; } else { baseURL = [HBUtilities commonParentOfURL:baseURL withURL:pair.URL]; } } } if (baseURL) { [baseURLs addObject:baseURL]; } } return baseURLs; } + (NSURL *)eyetvMediaURL:(NSURL *)url { // We're looking at an EyeTV package - try to open its enclosed .mpg or .ts media file NSString *mediaName; NSUInteger count = [[url.path stringByAppendingString:@"/"] completePathIntoString:&mediaName caseSensitive:YES matchesIntoArray:nil filterTypes:@[@"mpg", @"ts"]]; if (count > 0) { // Found an mpeg inside the eyetv package, make it our scan path return [NSURL fileURLWithPath:mediaName]; } else { return nil; } } + (NSArray *)expandURLs:(NSArray *)fileURLs recursive:(BOOL)recursive { NSFileManager *manager = NSFileManager.defaultManager; NSMutableArray *mutableFileURLs = [NSMutableArray array]; NSDirectoryEnumerationOptions options = NSDirectoryEnumerationSkipsHiddenFiles | NSDirectoryEnumerationSkipsPackageDescendants | NSDirectoryEnumerationSkipsSubdirectoryDescendants; // Check first if it's a DVD-Video or Bluray if (fileURLs.count == 1) { NSURL *directoryURL = fileURLs.firstObject; NSNumber *isDirectory = nil; [directoryURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]; if (isDirectory.boolValue == YES) { if ([directoryURL.pathExtension isEqualToString:@"eyetv"]) { NSURL *eyetvMediaURL = [HBUtilities eyetvMediaURL:directoryURL]; if (eyetvMediaURL) { return @[eyetvMediaURL]; } } if ([directoryURL.pathExtension isEqualToString:@"dvdmedia"] || [directoryURL.lastPathComponent isEqualToString:@"VIDEO_TS"]) { return fileURLs; } NSArray *content = [manager contentsOfDirectoryAtURL:directoryURL includingPropertiesForKeys:nil options:options error:NULL]; for (NSURL *url in content) { if ([url.lastPathComponent isEqualToString:@"VIDEO_TS"] || [url.lastPathComponent isEqualToString:@"BDMV"]) { return fileURLs; } } } } if (recursive) { options &= ~NSDirectoryEnumerationSkipsSubdirectoryDescendants; } // If not, recursively enumerate all the files and directories for (NSURL *url in fileURLs) { NSNumber *isDirectory = nil; [url getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]; if (isDirectory.boolValue == NO) { [mutableFileURLs addObject:url]; } else if ([url.pathExtension isEqualToString:@"eyetv"]) { NSURL *eyetvMediaURL = [HBUtilities eyetvMediaURL:url]; if (eyetvMediaURL) { [mutableFileURLs addObject:eyetvMediaURL]; } } else if ([url.pathExtension isEqualToString:@"dvdmedia"] || [url.lastPathComponent isEqualToString:@"VIDEO_TS"]) { // Skip } else { NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:url includingPropertiesForKeys:@[NSURLIsDirectoryKey] options:options errorHandler:NULL]; for (NSURL *enumeratorURL in enumerator) { [enumeratorURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]; if (isDirectory.boolValue == YES) { if ([enumeratorURL.pathExtension isEqualToString:@"eyetv"]) { NSURL *eyetvMediaURL = [HBUtilities eyetvMediaURL:enumeratorURL]; if (eyetvMediaURL) { [mutableFileURLs addObject:enumeratorURL]; } [enumerator skipDescendants]; } else if ([enumeratorURL.pathExtension isEqualToString:@"dvdmedia"] || [enumeratorURL.lastPathComponent isEqualToString:@"VIDEO_TS"]) { [enumerator skipDescendants]; } } else { [mutableFileURLs addObject:enumeratorURL]; } } } } [mutableFileURLs sortUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { return [[obj1 path] localizedStandardCompare:[obj2 path]]; }]; return mutableFileURLs; } + (NSArray *)supportedExtensions { return @[@"srt", @"ssa", @"ass"]; } + (NSArray *)extractURLs:(NSArray *)fileURLs withExtension:(NSArray *)extensions { NSMutableArray *extractedFileURLs = [NSMutableArray array]; for (NSURL *fileURL in fileURLs) { BOOL isMatch = NO; for (NSString *extension in extensions) { if ([fileURL.pathExtension caseInsensitiveCompare:extension] == NSOrderedSame) { isMatch = YES; break; } } if (isMatch) { [extractedFileURLs addObject:fileURL]; } } return extractedFileURLs; } + (NSArray *)trimURLs:(NSArray *)fileURLs withExtension:(NSArray *)excludedExtensions { NSMutableArray *trimmedURLs = [NSMutableArray array]; for (NSURL *fileURL in fileURLs) { BOOL excluded = NO; NSString *extension = fileURL.pathExtension; if (extension) { for (NSString *excludedExtension in excludedExtensions) { if ([extension caseInsensitiveCompare:excludedExtension] == NSOrderedSame) { excluded = YES; break; } } } if (excluded == NO) { [trimmedURLs addObject:fileURL]; } } return trimmedURLs; } + (NSString *)isoCodeForNativeLang:(NSString *)language { const iso639_lang_t *lang = lang_get_next(NULL); for (lang = lang_get_next(lang); lang != NULL; lang = lang_get_next(lang)) { NSString *nativeLanguage = strlen(lang->native_name) ? @(lang->native_name) : @(lang->eng_name); if ([language isEqualToString:nativeLanguage]) { return @(lang->iso639_2); } } return @"und"; } + (NSString *)iso6392CodeFor:(NSString *)aLanguage { iso639_lang_t *lang = lang_for_english(aLanguage.UTF8String); if (lang) { return @(lang->iso639_2); } return @"und"; } + (NSString *)languageCodeForIso6392Code:(NSString *)aLanguage { iso639_lang_t *lang = lang_for_code2(aLanguage.UTF8String); if (lang) { return @(lang->eng_name); } return @"Unknown"; } + (HBPrivacyConsentState)determinePermissionToAutomateTarget:(NSString *)bundleIdentifier promptIfNeeded:(BOOL)promptIfNeeded { if (@available(macOS 10.14, *)) { const char *identifierCString = bundleIdentifier.UTF8String; AEAddressDesc addressDesc; if ([bundleIdentifier isEqualToString:@"com.apple.systemevents"]) { // Mare sure system events is running, if not the consent alert will not be shown. BOOL result = [NSWorkspace.sharedWorkspace launchAppWithBundleIdentifier:bundleIdentifier options:0 additionalEventParamDescriptor:nil launchIdentifier:NULL]; if (result == NO) { [HBUtilities writeToActivityLog:"Automation: couldn't launch %s.", bundleIdentifier.UTF8String]; } } OSErr descResult = AECreateDesc(typeApplicationBundleID, identifierCString, strlen(identifierCString), &addressDesc); if (descResult == noErr) { OSStatus permission = AEDeterminePermissionToAutomateTarget(&addressDesc, typeWildCard, typeWildCard, promptIfNeeded); AEDisposeDesc(&addressDesc); HBPrivacyConsentState result; switch (permission) { case -1744: //errAEEventWouldRequireUserConsent: 10.14 or later [HBUtilities writeToActivityLog:"Automation: request user consent for %s.", bundleIdentifier.UTF8String]; result = HBPrivacyConsentStateUnknown; break; case noErr: [HBUtilities writeToActivityLog:"Automation: permission granted for %s.", bundleIdentifier.UTF8String]; result = HBPrivacyConsentStateGranted; break; case errAEEventNotPermitted: [HBUtilities writeToActivityLog:"Automation: permission not granted for %s.", bundleIdentifier.UTF8String]; result = HBPrivacyConsentStateDenied; break; case procNotFound: default: [HBUtilities writeToActivityLog:"Automation: permission unknown."]; result = HBPrivacyConsentStateUnknown; break; } return result; } } return HBPrivacyConsentStateGranted; } @end