Initial
This commit is contained in:
567
src_mac/ipatool-py/main.py
Executable file
567
src_mac/ipatool-py/main.py
Executable file
@@ -0,0 +1,567 @@
|
||||
#!/usr/bin/python3
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
import time
|
||||
import zipfile
|
||||
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3 import Retry
|
||||
|
||||
from reqs.itunes import *
|
||||
from reqs.store import *
|
||||
import reprlib
|
||||
|
||||
reprlib.aRepr.maxstring = 200
|
||||
|
||||
import argparse
|
||||
|
||||
import logging
|
||||
from rich.logging import RichHandler
|
||||
from rich.console import Console
|
||||
import rich
|
||||
|
||||
rich.get_console().file = sys.stderr
|
||||
if rich.get_console().width < 100:
|
||||
rich.get_console().width = 100
|
||||
|
||||
logging_handler = RichHandler(rich_tracebacks=True)
|
||||
logging.basicConfig(
|
||||
level="INFO",
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[logging_handler]
|
||||
)
|
||||
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
||||
retryLogger = logging.getLogger('urllib3.util.retry')
|
||||
retryLogger.setLevel(logging.DEBUG)
|
||||
retryLogger.handlers = [logging_handler]
|
||||
retryLogger.propagate = False
|
||||
|
||||
logger = logging.getLogger('main')
|
||||
|
||||
import requests
|
||||
|
||||
def get_zipinfo_datetime(timestamp=None):
|
||||
# Some applications need reproducible .whl files, but they can't do this without forcing
|
||||
# the timestamp of the individual ZipInfo objects. See issue #143.
|
||||
timestamp = int(timestamp or time.time())
|
||||
return time.gmtime(timestamp)[0:6]
|
||||
|
||||
def downloadFile(url, outfile, retries=10):
|
||||
for retry in range(retries):
|
||||
try:
|
||||
_downloadFile(url, outfile)
|
||||
break
|
||||
except Exception as e:
|
||||
logger.info("Error during downloading %s (retry %d/%d), error %s", url, retry, retries, e)
|
||||
os.remove(outfile)
|
||||
logger.info("Download success in retry %d", retry)
|
||||
|
||||
download_sess = requests.Session()
|
||||
download_sess.headers = {"User-Agent": CONFIGURATOR_UA}
|
||||
DOWNLOAD_READ_TIMEOUT = 25.0
|
||||
def _downloadFile(url, outfile):
|
||||
with download_sess.get(url, stream=True, timeout=DOWNLOAD_READ_TIMEOUT) as r:
|
||||
if not r.headers.get('Content-Length'):
|
||||
raise Exception("server is not returning Content-Length!")
|
||||
totalLen = int(r.headers.get('Content-Length', '0'))
|
||||
downLen = 0
|
||||
r.raise_for_status()
|
||||
try:
|
||||
with open(outfile, 'wb') as f:
|
||||
lastLen = 0
|
||||
for chunk in r.iter_content(chunk_size=1 * 1024 * 1024):
|
||||
# If you have chunk encoded response uncomment if
|
||||
# and set chunk_size parameter to None.
|
||||
# if chunk:
|
||||
f.write(chunk)
|
||||
downLen += len(chunk)
|
||||
if totalLen and downLen - lastLen > totalLen * 0.05:
|
||||
logger.info("Download progress: %3.2f%% (%5.1fM /%5.1fM)" % (
|
||||
downLen / totalLen * 100, downLen / 1024 / 1024, totalLen / 1024 / 1024))
|
||||
lastLen = downLen
|
||||
finally:
|
||||
if downLen != totalLen: # ensure no partial downloaded files exists
|
||||
os.unlink(outfile)
|
||||
if downLen != totalLen:
|
||||
raise Exception("failed to completely download the IPA file")
|
||||
|
||||
return outfile
|
||||
|
||||
class IPATool(object):
|
||||
def __init__(self):
|
||||
self.sess = requests.Session()
|
||||
|
||||
retry_strategy = Retry(
|
||||
connect=4,
|
||||
read=4,
|
||||
# total=8,
|
||||
status=20,
|
||||
allowed_methods=None,
|
||||
status_forcelist=[429, 502, 503],
|
||||
backoff_factor=1.0,
|
||||
respect_retry_after_header=False,
|
||||
)
|
||||
self.sess.mount("https://", HTTPAdapter(max_retries=retry_strategy))
|
||||
self.sess.mount("http://", HTTPAdapter(max_retries=retry_strategy))
|
||||
IPATOOL_PROXY = os.getenv("IPATOOL_PROXY")
|
||||
if IPATOOL_PROXY is not None:
|
||||
self.sess.proxies.update(
|
||||
{'http': IPATOOL_PROXY, 'https': IPATOOL_PROXY})
|
||||
# self.sess.headers = {}
|
||||
self.sess.headers = {"Connection": "close"}
|
||||
self.sess.keep_alive = False
|
||||
|
||||
self.appId = None
|
||||
# self.appInfo = None
|
||||
self.appVerId = None
|
||||
self.appVerIds = None
|
||||
|
||||
self.jsonOut = None
|
||||
|
||||
def tool_main(self):
|
||||
commparser = argparse.ArgumentParser(description='IPATool-Python Commands.', add_help=False)
|
||||
subp = commparser.add_subparsers(dest='command', required=True)
|
||||
lookup_p = subp.add_parser('lookup')
|
||||
id_group = lookup_p.add_mutually_exclusive_group(required=True)
|
||||
id_group.add_argument('--bundle-id', '-b', dest='bundle_id')
|
||||
id_group.add_argument('--appId', '-i', dest='appId')
|
||||
lookup_p.add_argument('--country', '-c', dest='country', required=True)
|
||||
lookup_p.add_argument('--get-verid', dest='get_verid', action='store_true')
|
||||
lookup_p.set_defaults(func=self.handleLookup)
|
||||
|
||||
def add_auth_options(p):
|
||||
auth_p = p.add_argument_group('Auth Options', 'Must specify either Apple ID & Password, or iTunes Server URL')
|
||||
appleid = auth_p.add_argument('--appleid', '-e')
|
||||
passwd = auth_p.add_argument('--password', '-p')
|
||||
sessdir = auth_p.add_argument('--session-dir', dest='session_dir', default=None)
|
||||
|
||||
itunessrv = auth_p.add_argument('--itunes-server', '-s', dest='itunes_server')
|
||||
|
||||
## Multiple hack here just to achieve (appleid & password) || itunes_server
|
||||
# p._optionals.conflict_handler = 'ignore'
|
||||
# p._optionals._handle_conflict_ignore = lambda *args: None
|
||||
auth_p = p.add_mutually_exclusive_group(required=True)
|
||||
auth_p._group_actions.append(appleid)
|
||||
auth_p._group_actions.append(itunessrv)
|
||||
# auth_p._group_actions.append(sessdir)
|
||||
|
||||
auth_p = p.add_mutually_exclusive_group(required=True)
|
||||
auth_p._group_actions.append(passwd)
|
||||
auth_p._group_actions.append(itunessrv)
|
||||
|
||||
purchase_p = subp.add_parser('purchase')
|
||||
add_auth_options(purchase_p)
|
||||
purchase_p.add_argument('--appId', '-i', dest='appId')
|
||||
purchase_p.set_defaults(func=self.handlePurchase)
|
||||
|
||||
down_p = subp.add_parser('download')
|
||||
add_auth_options(down_p)
|
||||
down_p.add_argument('--appId', '-i', dest='appId')
|
||||
down_p.add_argument('--appVerId', dest='appVerId')
|
||||
down_p.add_argument('--purchase', action='store_true')
|
||||
down_p.add_argument('--downloadAllVersion', action='store_true')
|
||||
down_p.add_argument('--output-dir', '-o', dest='output_dir', default='.')
|
||||
down_p.set_defaults(func=self.handleDownload)
|
||||
|
||||
his_p = subp.add_parser('historyver')
|
||||
his_p.add_argument('--appId', '-i', dest='appId')
|
||||
his_p.add_argument('--purchase', action='store_true')
|
||||
his_p.add_argument('--output-dir', '-o', dest='output_dir', default='.')
|
||||
add_auth_options(his_p)
|
||||
his_p.set_defaults(func=self.handleHistoryVersion)
|
||||
|
||||
parser = argparse.ArgumentParser(description='IPATool-Python.', parents=[commparser])
|
||||
parser.add_argument('--log-level', '-l', dest='log_level', default='info',
|
||||
help='output level (default: info)')
|
||||
parser.add_argument('--json', dest='out_json', action='store_true',
|
||||
help='output json in stdout (log will always be put into stderr)')
|
||||
|
||||
# parse global flags & first comm's arguments
|
||||
args, rest = parser.parse_known_args()
|
||||
logging.getLogger().setLevel(args.log_level.upper())
|
||||
outJson = args.out_json
|
||||
|
||||
while True:
|
||||
args.func(args)
|
||||
if not rest:
|
||||
break
|
||||
args, rest = commparser.parse_known_args(rest)
|
||||
|
||||
if outJson and self.jsonOut:
|
||||
print(json.dumps(self.jsonOut, ensure_ascii=False))
|
||||
|
||||
def _outputJson(self, obj):
|
||||
self.jsonOut = obj
|
||||
|
||||
def handleLookup(self, args):
|
||||
if args.bundle_id:
|
||||
s = 'BundleID %s' % args.bundle_id
|
||||
else:
|
||||
s = 'AppID %s' % args.appId
|
||||
logger.info('Looking up app in country %s with BundleID %s' % (args.country, s))
|
||||
iTunes = iTunesClient(self.sess)
|
||||
appInfos = iTunes.lookup(bundleId=args.bundle_id, appId=args.appId, country=args.country)
|
||||
if appInfos.resultCount != 1:
|
||||
logger.fatal("Failed to find app in country %s with %s" % (args.country, s))
|
||||
return
|
||||
appInfo = appInfos.results[0]
|
||||
logger.info("Found app:\n\tName: %s\n\tVersion: %s\n\tbundleId: %s\n\tappId: %s" % (appInfo.trackName, appInfo.version, appInfo.bundleId, appInfo.trackId))
|
||||
self.appId = appInfo.trackId
|
||||
# self.appInfo = appInfo
|
||||
|
||||
ret = {
|
||||
"name": appInfo.trackName,
|
||||
"version": appInfo.version,
|
||||
"appId": appInfo.trackId,
|
||||
"bundleId": appInfo.bundleId,
|
||||
}
|
||||
|
||||
if args.get_verid:
|
||||
logger.info("Retrieving verId using iTunes webpage...")
|
||||
verId = iTunes.getAppVerId(self.appId, args.country)
|
||||
logger.info("Got current verId using iTunes webpage: %s" % verId)
|
||||
ret["appVerId"] = verId
|
||||
|
||||
self._outputJson(ret)
|
||||
|
||||
storeClientCache = {}
|
||||
def _get_StoreClient(self, args):
|
||||
cachekey = args.itunes_server or args.appleid
|
||||
store, lastseen = self.storeClientCache.get(cachekey, (None, None,))
|
||||
if store:
|
||||
if time.time() - lastseen < 30.0:
|
||||
return store
|
||||
del self.storeClientCache[cachekey]
|
||||
|
||||
newSess = pickle.loads(pickle.dumps(self.sess))
|
||||
Store = StoreClient(newSess)
|
||||
|
||||
if args.itunes_server:
|
||||
logger.info("Using iTunes interface %s to download app!" % args.itunes_server)
|
||||
servUrl = args.itunes_server
|
||||
def handle_iTunes_provider(url):
|
||||
startTime = time.time()
|
||||
r = requests.get(servUrl, params={
|
||||
'url': url
|
||||
})
|
||||
logger.debug("got itunes header in %.2f seconds", time.time() - startTime)
|
||||
|
||||
ret = r.json()
|
||||
kbsync = bytes.fromhex(ret.pop('kbsync'))
|
||||
guid = ret.pop('guid')
|
||||
retHdrs = ret.pop('headers')
|
||||
handled = {
|
||||
'headers': retHdrs,
|
||||
'kbsync': kbsync,
|
||||
'guid': guid,
|
||||
}
|
||||
if 'sbsync' in ret:
|
||||
handled['sbsync'] = bytes.fromhex(ret.pop('sbsync'))
|
||||
if 'afds' in ret:
|
||||
handled['afds'] = ret.pop('afds')
|
||||
return handled
|
||||
Store.iTunes_provider = handle_iTunes_provider
|
||||
else:
|
||||
appleid = args.appleid
|
||||
applepass = args.password
|
||||
|
||||
needLogin = True
|
||||
session_cache = os.path.join(args.session_dir, args.appleid) if args.session_dir else None
|
||||
if session_cache and os.path.exists(session_cache):
|
||||
needLogin = False
|
||||
try:
|
||||
# inside try in case the file format changed
|
||||
with open(session_cache, "r") as f:
|
||||
content = f.read()
|
||||
Store.authenticate_load_session(content)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error loading session {session_cache}")
|
||||
os.unlink(session_cache)
|
||||
needLogin = True
|
||||
else:
|
||||
logger.info('Loaded session for %s' % (str(Store.authInfo)))
|
||||
if needLogin:
|
||||
logger.info("Logging into iTunes as %s ..." % appleid)
|
||||
|
||||
Store.authenticate(appleid, applepass)
|
||||
logger.info('Logged in as %s' % (str(Store.authInfo)))
|
||||
|
||||
if session_cache:
|
||||
with open(session_cache, "w") as f:
|
||||
f.write(Store.authenticate_save_session())
|
||||
|
||||
def authedPost(*args, **kwargs):
|
||||
if 'MZFinance.woa/wa/authenticate' in args[0]:
|
||||
return Store.sess.original_post(*args, **kwargs)
|
||||
for i in range(3):
|
||||
r = Store.sess.original_post(*args, **kwargs)
|
||||
isAuthFail = False
|
||||
try:
|
||||
d = plistlib.loads(r.content)
|
||||
if str(d['failureType']) in ("2034", "1008"):
|
||||
isAuthFail = True
|
||||
except:
|
||||
return r
|
||||
if not isAuthFail:
|
||||
return r
|
||||
Store.authenticate(appleid, applepass)
|
||||
if session_cache:
|
||||
with open(session_cache, "w") as f:
|
||||
f.write(Store.authenticate_save_session())
|
||||
continue
|
||||
Store.sess.original_post = Store.sess.post
|
||||
Store.sess.post = authedPost
|
||||
|
||||
self.storeClientCache[cachekey] = (Store, time.time(),)
|
||||
return Store
|
||||
|
||||
def _handleStoreException(self, _e):
|
||||
e = _e # type: StoreException
|
||||
logger.fatal("Store %s failed! Message: %s%s" % (e.req, e.errMsg, " (errorType %s)" % e.errType if e.errType else ''))
|
||||
logger.fatal(" Raw Response: %s" % (e.resp))
|
||||
|
||||
def handlePurchase(self, args):
|
||||
Store = self._get_StoreClient(args)
|
||||
logger.info('Try to purchase appId %s' % (self.appId))
|
||||
try:
|
||||
Store.purchase(self.appId)
|
||||
except StoreException as e:
|
||||
if e.errMsg == 'purchased_before':
|
||||
logger.warning('You have already purchased appId %s before' % (self.appId))
|
||||
else:
|
||||
raise
|
||||
|
||||
def handleHistoryVersion(self, args, caches=True):
|
||||
if args.appId:
|
||||
self.appId = args.appId
|
||||
|
||||
if not self.appId:
|
||||
logger.fatal("appId not supplied!")
|
||||
return
|
||||
|
||||
versionsJsonPath = args.output_dir + f"/historyver_{self.appId}.json"
|
||||
if caches:
|
||||
if os.path.exists(versionsJsonPath):
|
||||
cacheContent = None
|
||||
try:
|
||||
with open(versionsJsonPath) as f:
|
||||
cacheContent = json.load(f)
|
||||
except:
|
||||
pass
|
||||
if cacheContent is not None:
|
||||
logger.info('Loaded history version cache for appId %s' % self.appId)
|
||||
self.appVerIds = cacheContent['appVerIds']
|
||||
return
|
||||
|
||||
logger.info('Retrieving history version for appId %s' % self.appId)
|
||||
|
||||
try:
|
||||
Store = self._get_StoreClient(args)
|
||||
|
||||
logger.info('Retrieving download info for appId %s' % (self.appId))
|
||||
if args.purchase:
|
||||
logger.info('Purchasing appId %s' % (self.appId))
|
||||
# We have already successfully purchased, so don't purchase again :)
|
||||
self.handlePurchase(args)
|
||||
args.purchase = False
|
||||
|
||||
downResp = Store.download(self.appId, '', isRedownload=not args.purchase)
|
||||
logger.debug('Got download info: %s', downResp)
|
||||
if args.purchase:
|
||||
# We have already successfully purchased, so don't purchase again :)
|
||||
args.purchase = False
|
||||
|
||||
if not downResp.songList:
|
||||
logger.fatal("failed to get app download info!")
|
||||
raise StoreException('download', downResp, 'no songList')
|
||||
downInfo = downResp.songList[0]
|
||||
logger.info('Got available version ids %s', downInfo.metadata.softwareVersionExternalIdentifiers)
|
||||
self._outputJson({
|
||||
"appVerIds": downInfo.metadata.softwareVersionExternalIdentifiers
|
||||
})
|
||||
self.appVerIds = downInfo.metadata.softwareVersionExternalIdentifiers
|
||||
if caches:
|
||||
with open(versionsJsonPath, 'w') as f:
|
||||
json.dump({
|
||||
'appVerIds': self.appVerIds,
|
||||
}, f)
|
||||
except StoreException as e:
|
||||
self._handleStoreException(e)
|
||||
if not e.errMsg.startswith('http error status') and not e.errMsg.startswith(
|
||||
'We are temporarily unable to process your request') and not e.errMsg.startswith(
|
||||
"License not found"):
|
||||
# this error is persistent (e.g. app deleted)
|
||||
self.appVerIds = []
|
||||
if caches:
|
||||
with open(versionsJsonPath, 'w') as f:
|
||||
json.dump({
|
||||
'appVerIds': self.appVerIds,
|
||||
'error': str(e),
|
||||
'errorResp': str(e.resp),
|
||||
}, f)
|
||||
|
||||
def handleDownload(self, args):
|
||||
os.makedirs(args.output_dir, exist_ok=True)
|
||||
if args.downloadAllVersion:
|
||||
if os.path.exists(args.output_dir + "/all_done"):
|
||||
logger.info('Already fully finished, skipping!')
|
||||
return
|
||||
self.handleHistoryVersion(args, caches=True)
|
||||
if not self.appVerIds:
|
||||
logger.fatal('failed to retrive history versions for appId %s', args.appId)
|
||||
return
|
||||
everything_succ = True
|
||||
for appVerId in self.appVerIds:
|
||||
self.jsonOut = None
|
||||
stateFile = args.output_dir + '/' + str(appVerId) + '.finish'
|
||||
if os.path.exists(stateFile):
|
||||
logger.info('Skipping already downloaded')
|
||||
continue
|
||||
try:
|
||||
self.appVerId = appVerId
|
||||
self.downloadOne(args)
|
||||
if args.out_json and self.jsonOut:
|
||||
print(json.dumps(self.jsonOut, ensure_ascii=False))
|
||||
if self.jsonOut is not None: # successfully finished
|
||||
with open(stateFile, 'w') as f:
|
||||
f.write('1')
|
||||
except Exception as e:
|
||||
logger.fatal("error during downloading appVerId %s", appVerId, exc_info=1)
|
||||
everything_succ = False
|
||||
finally:
|
||||
self.jsonOut = None
|
||||
if everything_succ:
|
||||
with open(args.output_dir + "/all_done", 'w') as f:
|
||||
f.write("1")
|
||||
else:
|
||||
self.downloadOne(args)
|
||||
|
||||
def downloadOne(self, args):
|
||||
if args.appId:
|
||||
self.appId = args.appId
|
||||
if args.appVerId:
|
||||
self.appVerId = args.appVerId
|
||||
|
||||
if not self.appId:
|
||||
logger.fatal("appId not supplied!")
|
||||
return
|
||||
|
||||
logger.info("Downloading appId %s appVerId %s", self.appId, self.appVerId)
|
||||
try:
|
||||
appleid = args.appleid
|
||||
Store = self._get_StoreClient(args)
|
||||
|
||||
if args.purchase:
|
||||
self.handlePurchase(args)
|
||||
|
||||
logger.info('Retrieving download info for appId %s%s' % (self.appId, " with versionId %s" % self.appVerId if self.appVerId else ""))
|
||||
|
||||
downResp = Store.download(self.appId, self.appVerId, isRedownload=not args.purchase)
|
||||
logger.debug('Got download info: %s', downResp.as_dict())
|
||||
|
||||
if not downResp.songList:
|
||||
logger.fatal("failed to get app download info!")
|
||||
raise StoreException('download', downResp, 'no songList')
|
||||
downInfo = downResp.songList[0]
|
||||
|
||||
appName = downInfo.metadata.bundleDisplayName
|
||||
appId = downInfo.songId
|
||||
appBundleId = downInfo.metadata.softwareVersionBundleId
|
||||
appVerId = downInfo.metadata.softwareVersionExternalIdentifier
|
||||
# when downloading history versions, bundleShortVersionString will always give a wrong version number (the newest one)
|
||||
# should use bundleVersion in these cases
|
||||
appVer = downInfo.metadata.bundleShortVersionString if not self.appVerId else downInfo.metadata.bundleVersion
|
||||
|
||||
logger.info(f'Downloading app {appName} ({appBundleId}) with appId {appId} (version {appVer}, versionId {appVerId})')
|
||||
|
||||
# if self.appInfo:
|
||||
filename = '%s-%s-%s-%s.ipa' % (appBundleId,
|
||||
appVer,
|
||||
appId,
|
||||
appVerId)
|
||||
# else:
|
||||
# filename = '%s-%s.ipa' % (self.appId, appVerId)
|
||||
|
||||
filepath = os.path.join(args.output_dir, filename)
|
||||
logger.info("Downloading ipa to %s" % filepath)
|
||||
downloadFile(downInfo.URL, filepath)
|
||||
metadata = downInfo.metadata.as_dict()
|
||||
if appleid:
|
||||
metadata["apple-id"] = appleid
|
||||
metadata["userName"] = appleid
|
||||
logger.info("Writing out iTunesMetadata.plist...")
|
||||
if zipfile.is_zipfile(filepath):
|
||||
with zipfile.ZipFile(filepath, 'a') as ipaFile:
|
||||
logger.debug("Writing iTunesMetadata.plist")
|
||||
ipaFile.writestr(zipfile.ZipInfo("iTunesMetadata.plist", get_zipinfo_datetime()), plistlib.dumps(metadata))
|
||||
logger.debug("Writing IPAToolInfo.plist")
|
||||
ipaFile.writestr(zipfile.ZipInfo("IPAToolInfo.plist", get_zipinfo_datetime()), plistlib.dumps(downResp.as_dict()))
|
||||
|
||||
def findAppContentPath(c):
|
||||
if not c.startswith('Payload/'):
|
||||
return False
|
||||
pathparts = c.strip('/').split('/')
|
||||
if len(pathparts) != 2:
|
||||
return False
|
||||
if not pathparts[1].endswith(".app"):
|
||||
return False
|
||||
return True
|
||||
appContentDirChoices = [c for c in ipaFile.namelist() if findAppContentPath(c)]
|
||||
if len(appContentDirChoices) != 1:
|
||||
raise Exception("failed to find appContentDir, choices %s", appContentDirChoices)
|
||||
appContentDir = appContentDirChoices[0].rstrip('/')
|
||||
|
||||
processedSinf = False
|
||||
if (appContentDir + '/SC_Info/Manifest.plist') in ipaFile.namelist():
|
||||
#Try to get the Manifest.plist file, since it doesn't always exist.
|
||||
scManifestData = ipaFile.read(appContentDir + '/SC_Info/Manifest.plist')
|
||||
logger.debug("Got SC_Info/Manifest.plist: %s", scManifestData)
|
||||
scManifest = plistlib.loads(scManifestData)
|
||||
sinfs = {c.id: c.sinf for c in downInfo.sinfs}
|
||||
if 'SinfPaths' in scManifest:
|
||||
for i, sinfPath in enumerate(scManifest['SinfPaths']):
|
||||
logger.debug("Writing sinf to %s", sinfPath)
|
||||
ipaFile.writestr(appContentDir + '/' + sinfPath, sinfs[i])
|
||||
processedSinf = True
|
||||
if not processedSinf:
|
||||
logger.info('Manifest.plist does not exist! Assuming it is an old app without one...')
|
||||
infoListData = ipaFile.read(appContentDir + '/Info.plist') #Is this not loaded anywhere yet?
|
||||
infoList = plistlib.loads(infoListData)
|
||||
sinfPath = appContentDir + '/SC_Info/'+infoList['CFBundleExecutable']+".sinf"
|
||||
logger.debug("Writing sinf to %s", sinfPath)
|
||||
#Assuming there is only one .sinf file, hence the 0
|
||||
ipaFile.writestr(sinfPath, downInfo.sinfs[0].sinf)
|
||||
processedSinf = True
|
||||
|
||||
logger.info("Downloaded ipa to %s" % filename)
|
||||
else:
|
||||
plist = filepath[:-4]+".info.plist"
|
||||
with open(plist, "wb") as f:
|
||||
f.write(plistlib.dumps(downResp.as_dict()))
|
||||
plist = filepath[:-4]+".plist"
|
||||
with open(plist, "wb") as f:
|
||||
f.write(plistlib.dumps(metadata))
|
||||
logger.info("Downloaded ipa to %s and plist to %s" % (filename, plist))
|
||||
|
||||
self._outputJson({
|
||||
"appName": appName,
|
||||
"appBundleId": appBundleId,
|
||||
"appVer": appVer,
|
||||
"appId": appId,
|
||||
"appVerId": appVerId,
|
||||
|
||||
"downloadedIPA": filepath,
|
||||
"downloadedVerId": appVerId,
|
||||
"downloadURL": downInfo.URL,
|
||||
})
|
||||
except StoreException as e:
|
||||
self._handleStoreException(e)
|
||||
|
||||
def main():
|
||||
tool = IPATool()
|
||||
tool.tool_main()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user