2021-02-26 08:40:22 +00:00
|
|
|
import argparse
|
|
|
|
import bs4
|
2022-01-20 23:00:14 +00:00
|
|
|
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
|
2021-11-21 04:40:39 +00:00
|
|
|
import traceback
|
2022-01-20 23:00:14 +00:00
|
|
|
import zipfile
|
2021-02-26 08:40:22 +00:00
|
|
|
|
|
|
|
from voussoirkit import betterhelp
|
|
|
|
from voussoirkit import downloady
|
2022-01-20 23:00:14 +00:00
|
|
|
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
|
2022-02-13 03:50:00 +00:00
|
|
|
from voussoirkit import progressbars
|
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,
|
2022-02-13 03:50:00 +00:00
|
|
|
progressbar=progressbars.bar1_bytestring,
|
2021-09-23 03:25:52 +00:00
|
|
|
timeout=30,
|
|
|
|
)
|
|
|
|
|
2022-01-20 23:00:14 +00:00
|
|
|
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
|
|
|
|
2021-09-23 03:22:07 +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):
|
2022-02-13 03:50:00 +00:00
|
|
|
args.destination.assert_is_directory()
|
2021-05-07 02:34:55 +00:00
|
|
|
|
|
|
|
return_status = 0
|
|
|
|
|
2021-09-23 03:22:07 +00:00
|
|
|
packages = args.packages
|
|
|
|
if packages == ['*']:
|
|
|
|
packages = ls_packages(pathclass.cwd())
|
|
|
|
|
2021-11-08 19:38:56 +00:00
|
|
|
download_count = 0
|
|
|
|
|
2022-01-20 23:00:14 +00:00
|
|
|
index = get_fdroid_index()
|
|
|
|
|
2021-09-23 03:22:07 +00:00
|
|
|
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:
|
2022-01-20 23:00:14 +00:00
|
|
|
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
|
|
|
|
2022-01-20 23:00:14 +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:
|
2022-02-13 03:50:00 +00:00
|
|
|
this_dest = args.destination.with_child(package)
|
2021-02-26 08:40:22 +00:00
|
|
|
this_dest.makedirs(exist_ok=True)
|
|
|
|
else:
|
2022-02-13 03:50:00 +00:00
|
|
|
this_dest = args.destination
|
2021-02-26 08:40:22 +00:00
|
|
|
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)
|
2021-11-08 19:38:56 +00:00
|
|
|
download_count += 1
|
2021-07-01 22:53:32 +00:00
|
|
|
except Exception as exc:
|
2021-11-21 04:40:39 +00:00
|
|
|
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
|
|
|
|
|
2021-11-08 19:38:56 +00:00
|
|
|
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
|
|
|
|
2021-06-22 05:20:39 +00:00
|
|
|
@operatornotify.main_decorator(subject='fdroidapk.py')
|
2021-06-22 05:11:19 +00:00
|
|
|
@vlogging.main_decorator
|
2021-02-26 08:40:22 +00:00
|
|
|
def main(argv):
|
2022-02-13 03:50:00 +00:00
|
|
|
parser = argparse.ArgumentParser(description='F-Droid APK downloader.')
|
|
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
'packages',
|
|
|
|
nargs='+',
|
|
|
|
type=str,
|
|
|
|
help='''
|
|
|
|
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/
|
|
|
|
''',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--folders',
|
|
|
|
action='store_true',
|
|
|
|
help='''
|
|
|
|
If provided, each apk will be downloaded into a separate folder named after
|
|
|
|
the package.
|
|
|
|
If omitted, the apks are downloaded into the destination folder directly.
|
|
|
|
''',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--destination',
|
|
|
|
default=pathclass.cwd(),
|
|
|
|
type=pathclass.Path,
|
|
|
|
help='''
|
|
|
|
Alternative path to download the apk files to. Default is cwd.
|
|
|
|
''',
|
|
|
|
)
|
2021-02-26 08:40:22 +00:00
|
|
|
parser.set_defaults(func=fpk_argparse)
|
|
|
|
|
2022-02-13 03:50:00 +00:00
|
|
|
return betterhelp.go(parser, argv)
|
2021-02-26 08:40:22 +00:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
raise SystemExit(main(sys.argv[1:]))
|