Skip to content

Instantly share code, notes, and snippets.

@EthanArbuckle
Created June 14, 2025 15:05
Show Gist options
  • Save EthanArbuckle/eee5e4398045a4e0167a9a58799993b4 to your computer and use it in GitHub Desktop.
Save EthanArbuckle/eee5e4398045a4e0167a9a58799993b4 to your computer and use it in GitHub Desktop.
appstore bulk download
/*
<!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