Created
June 14, 2025 15:05
-
-
Save EthanArbuckle/eee5e4398045a4e0167a9a58799993b4 to your computer and use it in GitHub Desktop.
appstore bulk download
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>com.apple.appstored.jobmanager</key> | |
<true/> | |
<key>com.apple.accounts.appleaccount.fullaccess</key> | |
<true/> | |
<key>com.apple.appstored.private</key> | |
<true/> | |
<key>com.apple.appstored.xpc.updates</key> | |
<true/> | |
<key>com.apple.authkit.client.internal</key> | |
<true/> | |
<key>com.apple.itunesstored.private</key> | |
<true/> | |
<key>com.apple.private.accounts.allaccounts</key> | |
<true/> | |
</dict> | |
</plist> | |
*/ | |
#import <Foundation/Foundation.h> | |
#import <objc/runtime.h> | |
#import <objc/message.h> | |
#import <dlfcn.h> | |
kern_return_t performBatchAppDownload(NSArray<NSString *> *appIds, BOOL redownload, void (^batchCompletion)(NSDictionary *failures)) { | |
NSMutableDictionary *failures = [[NSMutableDictionary alloc] init]; | |
NSMutableArray *items = [[NSMutableArray alloc] init]; | |
NSMutableDictionary *appIdToIndex = [[NSMutableDictionary alloc] init]; | |
NSString *downloadType = redownload ? @"STDRDL" : @"STDQ"; | |
for (int i = 0; i < (int)appIds.count; i++) { | |
NSString *appId = appIds[i]; | |
NSString *buyParams = [NSString stringWithFormat:@"productType=C&price=0&salableAdamId=%@&pricingParameters=%@", appId, downloadType]; | |
NSDictionary *lookupDict = @{@"kind": @"iosSoftware", @"offers": @[@{@"buyParams": buyParams}]}; | |
id allocatedItem = ((id (*)(Class, SEL))objc_msgSend)(objc_getClass("SKUIItem"), sel_registerName("alloc")); | |
id item = ((id (*)(id, SEL, NSDictionary *))objc_msgSend)(allocatedItem, sel_registerName("initWithLookupDictionary:"), lookupDict); | |
if (item == NULL) { | |
failures[appId] = [NSError errorWithDomain:@"com.ea.downloader" code:0 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"couldnt create SKUIItem for %@", appId]}]; | |
continue; | |
} | |
[items addObject:item]; | |
appIdToIndex[appId] = @(items.count - 1); | |
} | |
if (items.count == 0) { | |
if (batchCompletion != NULL) { | |
batchCompletion(failures); | |
} | |
return KERN_SUCCESS; | |
} | |
id center = ((id (*)(Class, SEL))objc_msgSend)(objc_getClass("SKUIItemStateCenter"), sel_registerName("defaultCenter")); | |
NSArray *purchases = ((NSArray * (*)(id, SEL, NSArray *))objc_msgSend)(center, sel_registerName("_newPurchasesWithItems:"), items); | |
if (purchases == NULL || purchases.count == 0) { | |
for (NSString *appId in appIdToIndex) { | |
failures[appId] = [NSError errorWithDomain:@"com.ea.downloader" code:0 userInfo:@{NSLocalizedDescriptionKey: @"couldnt create SSPurchase batch"}]; | |
} | |
if (batchCompletion != NULL) { | |
batchCompletion(failures); | |
} | |
return KERN_SUCCESS; | |
} | |
((void (*)(id, SEL, NSArray *, BOOL, id, void (^)(NSArray *, int)))objc_msgSend)(center, sel_registerName("_performPurchases:hasBundlePurchase:withClientContext:completionBlock:"), purchases, NO, nil, ^(NSArray *responses, int flags) { | |
if (responses.count == 0) { | |
for (NSString *appId in appIdToIndex) { | |
failures[appId] = [NSError errorWithDomain:@"com.ea.downloader" code:0 userInfo:@{NSLocalizedDescriptionKey: @"received no response"}]; | |
} | |
} | |
else { | |
for (id response in responses) { | |
NSError *responseError = ((NSError * (*)(id, SEL))objc_msgSend)(response, sel_registerName("error")); | |
if (responseError) { | |
id purchase = ((id (*)(id, SEL))objc_msgSend)(response, sel_registerName("purchase")); | |
if (purchase) { | |
NSString *buyParams = ((NSString * (*)(id, SEL))objc_msgSend)(purchase, sel_registerName("buyParameters")); | |
if (buyParams) { | |
NSRange idRange = [buyParams rangeOfString:@"salableAdamId="]; | |
if (idRange.location != NSNotFound) { | |
NSUInteger start = idRange.location + idRange.length; | |
NSRange endRange = [buyParams rangeOfString:@"&" options:0 range:NSMakeRange(start, buyParams.length - start)]; | |
NSUInteger length = (endRange.location != NSNotFound) ? endRange.location - start : buyParams.length - start; | |
NSString *extractedId = [buyParams substringWithRange:NSMakeRange(start, length)]; | |
failures[extractedId] = responseError; | |
} | |
} | |
} | |
} | |
} | |
} | |
if (batchCompletion) { | |
batchCompletion(failures); | |
} | |
}); | |
return KERN_SUCCESS; | |
} | |
int main(int argc, char **argv) { | |
dlopen("/System/Library/PrivateFrameworks/StoreKitUI.framework/StoreKitUI", RTLD_NOW); | |
if (argc != 2) { | |
NSLog(@"usage: %s app_id_list.txt", argv[0]); | |
return 1; | |
} | |
NSString *inputFilePath = [NSString stringWithUTF8String:argv[1]]; | |
if (!inputFilePath) { | |
NSLog(@"invalid input path"); | |
return 1; | |
} | |
NSString *contents = [NSString stringWithContentsOfFile:inputFilePath encoding:NSUTF8StringEncoding error:nil]; | |
NSArray *lines = [contents componentsSeparatedByString:@"\n"]; | |
NSMutableArray *appIds = [[NSMutableArray alloc] init]; | |
for (NSString *line in lines) { | |
NSString *trimmed = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; | |
if (trimmed != NULL && trimmed.length > 0) { | |
[appIds addObject:trimmed]; | |
} | |
} | |
dispatch_semaphore_t sem = dispatch_semaphore_create(0); | |
performBatchAppDownload(appIds, NO, ^(NSDictionary *failures) { | |
if (failures.count == 0) { | |
NSLog(@"all apps downloaded"); | |
} | |
else { | |
for (NSString *appId in failures) { | |
NSLog(@"Failed to download %@: %@", appId, failures[appId]); | |
} | |
} | |
dispatch_semaphore_signal(sem); | |
}); | |
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment