cmd/fdroidapk.py

159 lines
4.6 KiB
Python
Raw Normal View History

2021-05-07 02:34:55 +00:00
'''
fdroidapk - F-Droid APK downloader
==================================
> fdroidapk package_names <flags>
package_names:
One or more package names to download, separated by spaces. You can find
the package name in the URL on f-droid.org.
For example, com.nutomic.syncthingandroid from the URL
https://f-droid.org/en/packages/com.nutomic.syncthingandroid/
--destination path:
Alternative path to download the apk files to. Default is cwd.
--folders:
If provided, each apk will be downloaded into a separate folder named after
the package.
'''
2021-02-26 08:40:22 +00:00
import argparse
import bs4
import io
import json
2021-02-26 08:40:22 +00:00
import requests
import sys
2021-09-23 03:25:52 +00:00
import tenacity
import traceback
import zipfile
2021-02-26 08:40:22 +00:00
from voussoirkit import betterhelp
from voussoirkit import downloady
from voussoirkit import httperrors
2021-05-07 02:34:55 +00:00
from voussoirkit import operatornotify
2021-02-26 08:40:22 +00:00
from voussoirkit import pathclass
2021-06-22 05:21:06 +00:00
from voussoirkit import pipeable
2021-02-26 08:40:22 +00:00
from voussoirkit import vlogging
2021-05-07 02:34:55 +00:00
log = vlogging.getLogger(__name__, 'fdroidapk')
2021-09-23 06:17:55 +00:00
vlogging.getLogger('urllib3').setLevel(vlogging.SILENT)
2021-11-08 19:38:40 +00:00
vlogging.getLogger('voussoirkit.downloady').setLevel(vlogging.WARNING)
2021-02-26 08:40:22 +00:00
session = requests.Session()
2021-09-23 03:25:52 +00:00
my_tenacity = tenacity.retry(
2021-09-23 06:17:55 +00:00
retry=tenacity.retry_if_exception_type(requests.exceptions.ConnectionError),
2021-09-23 03:25:52 +00:00
stop=tenacity.stop_after_attempt(5),
wait=tenacity.wait_exponential(multiplier=2, min=3, max=60),
reraise=True,
)
@my_tenacity
def download_file(url, path):
return downloady.download_file(
url,
path,
callback_progress=downloady.Progress2,
timeout=30,
)
def get_fdroid_index():
'''
Download the index-v1.json and return it as a dict.
'''
log.info('Downloading F-Droid package index.')
url = 'https://f-droid.org/repo/index-v1.jar'
response = requests.get(url)
httperrors.raise_for_status(response)
zf = zipfile.ZipFile(io.BytesIO(response.content))
index = json.load(zf.open('index-v1.json', 'r'))
return index
2021-02-26 08:40:22 +00:00
def ls_packages(path):
packages = set()
items = path.listdir()
for item in items:
if item.is_dir and '.' in item.basename:
packages.add(item.basename)
elif item.is_file and item.extension == 'apk':
package = item.basename.split('-')[0]
packages.add(package)
return sorted(packages)
2021-09-23 03:26:37 +00:00
def normalize_package_name(package_name):
package_name = package_name.strip()
# If it happens to be a URL.
package_name = package_name.strip('/')
package_name = package_name.rsplit('/', 1)[-1]
return package_name
2021-06-22 05:21:06 +00:00
@pipeable.ctrlc_return1
2021-02-26 08:40:22 +00:00
def fpk_argparse(args):
destination = pathclass.Path(args.destination)
destination.assert_is_directory()
2021-05-07 02:34:55 +00:00
return_status = 0
packages = args.packages
if packages == ['*']:
packages = ls_packages(pathclass.cwd())
download_count = 0
index = get_fdroid_index()
for package in packages:
2021-02-26 08:40:22 +00:00
package = normalize_package_name(package)
2021-05-07 02:34:55 +00:00
try:
this_packages = index['packages'][package]
except KeyError:
log.error('%s is not in the package index.', package)
2021-05-07 02:34:55 +00:00
return_status = 1
continue
2021-02-26 08:40:22 +00:00
most_recent = sorted(this_packages, key=lambda p: p['versionCode'])[-1]
apk_basename = most_recent['apkName']
log.debug('Most recent is %s', apk_basename)
apk_url = f'https://f-droid.org/repo/{apk_basename}'
2021-02-26 08:40:22 +00:00
if args.folders:
this_dest = destination.with_child(package)
this_dest.makedirs(exist_ok=True)
else:
this_dest = destination
this_dest = this_dest.with_child(apk_basename)
2021-05-07 02:34:55 +00:00
2021-02-26 08:40:22 +00:00
if this_dest.exists:
2021-09-23 03:26:16 +00:00
log.debug('%s exists.', this_dest.absolute_path)
2021-02-26 08:40:22 +00:00
continue
2021-05-07 02:34:55 +00:00
log.info('Downloading %s.', this_dest.absolute_path)
try:
2021-09-23 03:25:52 +00:00
download_file(apk_url, this_dest)
download_count += 1
except Exception as exc:
exc = traceback.format_exc()
log.error('%s was unable to download apk:\n%s', package, exc)
2021-05-07 02:34:55 +00:00
return_status = 1
continue
log.info('Downloaded %d apks.', download_count)
2021-05-07 02:34:55 +00:00
return return_status
2021-02-26 08:40:22 +00:00
@operatornotify.main_decorator(subject='fdroidapk.py')
@vlogging.main_decorator
2021-02-26 08:40:22 +00:00
def main(argv):
2021-05-07 02:34:55 +00:00
parser = argparse.ArgumentParser(description=__doc__)
2021-02-26 08:40:22 +00:00
parser.add_argument('packages', nargs='+')
parser.add_argument('--folders', action='store_true')
parser.add_argument('--destination', default='.')
parser.set_defaults(func=fpk_argparse)
return betterhelp.single_main(argv, parser, __doc__)
2021-02-26 08:40:22 +00:00
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))