/* 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