Ethan Dalool
4a9051e617
With pathclass.glob_many, we can clean up and feel more confident about many programs that use pipeable to take glob patterns. Added return 0 to all programs that didn't have it, so we have consistent and explicit command line return values. Other linting and whitespace.
149 lines
4.4 KiB
Python
149 lines
4.4 KiB
Python
'''
|
|
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.
|
|
|
|
--debug:
|
|
Add this flag to see more detailed information.
|
|
|
|
--operatornotify:
|
|
Add this flag to get any warning messages sent to your operatornotify.
|
|
See voussoirkit.operatornotify.py for details.
|
|
'''
|
|
import argparse
|
|
import bs4
|
|
import requests
|
|
import sys
|
|
import tenacity
|
|
|
|
from voussoirkit import betterhelp
|
|
from voussoirkit import downloady
|
|
from voussoirkit import operatornotify
|
|
from voussoirkit import pathclass
|
|
from voussoirkit import pipeable
|
|
from voussoirkit import vlogging
|
|
|
|
log = vlogging.getLogger(__name__, 'fdroidapk')
|
|
vlogging.getLogger('urllib3').setLevel(vlogging.SILENT)
|
|
|
|
session = requests.Session()
|
|
my_tenacity = tenacity.retry(
|
|
retry=tenacity.retry_if_exception_type(requests.exceptions.ConnectionError),
|
|
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,
|
|
)
|
|
|
|
@my_tenacity
|
|
def get_apk_url(package_name):
|
|
url = f'https://f-droid.org/en/packages/{package_name}'
|
|
log.debug('Downloading page %s.', url)
|
|
response = session.get(url, timeout=30)
|
|
response.raise_for_status()
|
|
soup = bs4.BeautifulSoup(response.text, 'html.parser')
|
|
li = soup.find('li', {'class': 'package-version'})
|
|
aa = li.find_all('a')
|
|
aa = [a for a in aa if a.get('href', '').endswith('.apk')]
|
|
apk_url = aa[0]['href']
|
|
return apk_url
|
|
|
|
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)
|
|
|
|
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
|
|
|
|
@pipeable.ctrlc_return1
|
|
def fpk_argparse(args):
|
|
destination = pathclass.Path(args.destination)
|
|
destination.assert_is_directory()
|
|
|
|
return_status = 0
|
|
|
|
packages = args.packages
|
|
if packages == ['*']:
|
|
packages = ls_packages(pathclass.cwd())
|
|
|
|
for package in packages:
|
|
package = normalize_package_name(package)
|
|
log.info('Checking %s.', package)
|
|
|
|
try:
|
|
apk_url = get_apk_url(package)
|
|
except Exception as exc:
|
|
log.error('%s was unable to get apk url (%s).', package, exc)
|
|
return_status = 1
|
|
continue
|
|
|
|
apk_basename = downloady.basename_from_url(apk_url)
|
|
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)
|
|
|
|
if this_dest.exists:
|
|
log.debug('%s exists.', this_dest.absolute_path)
|
|
continue
|
|
|
|
log.info('Downloading %s.', this_dest.absolute_path)
|
|
|
|
try:
|
|
download_file(apk_url, this_dest)
|
|
except Exception as exc:
|
|
log.error('%s was unable to download apk (%s).', package, exc)
|
|
return_status = 1
|
|
continue
|
|
|
|
return return_status
|
|
|
|
@operatornotify.main_decorator(subject='fdroidapk.py')
|
|
@vlogging.main_decorator
|
|
def main(argv):
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
|
|
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__)
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main(sys.argv[1:]))
|