From 64ba27caab6ab24980eaa711b28287455ad90961 Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Sat, 12 Oct 2024 11:25:54 -0500 Subject: [PATCH 01/14] Create python-publish.yml --- .github/workflows/python-publish.yml | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..86ff3d9 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,31 @@ +name: Upload Python Package to PyPI + +on: + release: + types: [created] + +jobs: + build-and-publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build the package + run: python -m build + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: python -m twine upload dist/* From c6638bb9544604401f43cbe339328809db7efa6d Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Sat, 12 Oct 2024 11:31:51 -0500 Subject: [PATCH 02/14] Create python-test.yml --- .github/workflows/python-test.yml | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/python-test.yml diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml new file mode 100644 index 0000000..5e4c317 --- /dev/null +++ b/.github/workflows/python-test.yml @@ -0,0 +1,40 @@ +name: Python test + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Test with pytest + run: | + pytest From 4f6602397b1bf69a0b64b793fa239d1ad0c3cf60 Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Sat, 12 Oct 2024 11:40:36 -0500 Subject: [PATCH 03/14] Rename python-publish.yml to pypi-publish.yml --- .github/workflows/{python-publish.yml => pypi-publish.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{python-publish.yml => pypi-publish.yml} (100%) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/pypi-publish.yml similarity index 100% rename from .github/workflows/python-publish.yml rename to .github/workflows/pypi-publish.yml From 12c0f7da8c0ac69a8d2e3b3138502b63710c40b8 Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Sat, 12 Oct 2024 07:22:08 -0500 Subject: [PATCH 04/14] Move to new file structure --- app/cli.py | 33 ++++++ app/downloader.py | 227 +++++++++++++++++++++++++++++++++++++++++ app/menu_apk.py | 40 ++++++++ app/menu_firmware.py | 39 +++++++ app/setup_config.py | 100 ++++++++++++++++++ fetchtastic.py | 236 ------------------------------------------- menu_apk.py | 78 -------------- menu_firmware.py | 78 -------------- setup.cfg | 26 +++++ setup.py | 3 + setup.sh | 204 ------------------------------------- uninstall.sh | 81 --------------- 12 files changed, 468 insertions(+), 677 deletions(-) create mode 100644 app/cli.py create mode 100644 app/downloader.py create mode 100644 app/menu_apk.py create mode 100644 app/menu_firmware.py create mode 100644 app/setup_config.py delete mode 100644 fetchtastic.py delete mode 100644 menu_apk.py delete mode 100644 menu_firmware.py create mode 100644 setup.cfg create mode 100644 setup.py delete mode 100755 setup.sh delete mode 100755 uninstall.sh diff --git a/app/cli.py b/app/cli.py new file mode 100644 index 0000000..cc1f734 --- /dev/null +++ b/app/cli.py @@ -0,0 +1,33 @@ +# app/cli.py + +import argparse +from . import downloader +from . import setup_config + +def main(): + parser = argparse.ArgumentParser(description="Fetchtastic - Meshtastic Firmware and APK Downloader") + subparsers = parser.add_subparsers(dest='command') + + # Command to run setup + parser_setup = subparsers.add_parser('setup', help='Run the setup process') + + # Command to download firmware and APKs + parser_download = subparsers.add_parser('download', help='Download firmware and APKs') + + args = parser.parse_args() + + if args.command == 'setup': + # Run the setup process + setup_config.run_setup() + elif args.command == 'download' or args.command is None: + # Check if configuration exists + if not setup_config.config_exists(): + print("No configuration found. Running setup.") + setup_config.run_setup() + # Run the downloader + downloader.main() + else: + parser.print_help() + +if __name__ == "__main__": + main() diff --git a/app/downloader.py b/app/downloader.py new file mode 100644 index 0000000..a9f74c2 --- /dev/null +++ b/app/downloader.py @@ -0,0 +1,227 @@ +# app/downloader.py + +import os +import requests +import zipfile +import time +from datetime import datetime +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +from . import setup_config + +def main(): + # Load configuration + config = setup_config.load_config() + if not config: + print("Configuration not found. Please run 'fetchtastic setup' first.") + return + + # Get configuration values + save_apks = config.get("SAVE_APKS", False) + save_firmware = config.get("SAVE_FIRMWARE", False) + ntfy_server = config.get("NTFY_SERVER", "") + android_versions_to_keep = config.get("ANDROID_VERSIONS_TO_KEEP", 2) + firmware_versions_to_keep = config.get("FIRMWARE_VERSIONS_TO_KEEP", 2) + auto_extract = config.get("AUTO_EXTRACT", False) + extract_patterns = config.get("EXTRACT_PATTERNS", []) + + selected_apk_assets = config.get('SELECTED_APK_ASSETS', []) + selected_firmware_assets = config.get('SELECTED_FIRMWARE_ASSETS', []) + + download_dir = config.get('DOWNLOAD_DIR', os.path.join(os.path.expanduser("~"), "fetchtastic_downloads")) + firmware_dir = os.path.join(download_dir, "firmware") + apks_dir = os.path.join(download_dir, "apks") + latest_android_release_file = os.path.join(apks_dir, "latest_android_release.txt") + latest_firmware_release_file = os.path.join(firmware_dir, "latest_firmware_release.txt") + + # Logging setup + log_file = os.path.join(download_dir, "fetchtastic.log") + + def log_message(message): + with open(log_file, "a") as log: + log.write(f"{datetime.now()}: {message}\n") + print(message) + + def send_ntfy_notification(message): + if ntfy_server: + try: + response = requests.post(ntfy_server, data=message.encode('utf-8')) + response.raise_for_status() + except requests.exceptions.RequestException as e: + log_message(f"Error sending notification: {e}") + + # Function to get the latest releases and sort by date + def get_latest_releases(url, versions_to_keep, scan_count=5): + response = requests.get(url) + response.raise_for_status() + releases = response.json() + # Sort releases by published date, descending order + sorted_releases = sorted(releases, key=lambda r: r['published_at'], reverse=True) + # Limit the number of releases to be scanned and downloaded + return sorted_releases[:scan_count][:versions_to_keep] + + # Function to download a file with retry mechanism + def download_file(url, download_path): + session = requests.Session() + retry = Retry(connect=3, backoff_factor=1, status_forcelist=[502, 503, 504]) + adapter = HTTPAdapter(max_retries=retry) + session.mount('https://', adapter) + session.mount('http://', adapter) + + try: + if not os.path.exists(download_path): + log_message(f"Downloading {url}") + response = session.get(url, stream=True) + response.raise_for_status() + with open(download_path, 'wb') as file: + for chunk in response.iter_content(1024): + file.write(chunk) + log_message(f"Downloaded {download_path}") + else: + log_message(f"{download_path} already exists, skipping download.") + except requests.exceptions.RequestException as e: + log_message(f"Error downloading {url}: {e}") + + # Function to extract files based on the given patterns + def extract_files(zip_path, extract_dir, patterns): + try: + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + for file_name in zip_ref.namelist(): + if any(pattern in file_name for pattern in patterns): + zip_ref.extract(file_name, extract_dir) + log_message(f"Extracted {file_name} to {extract_dir}") + except zipfile.BadZipFile: + log_message(f"Error: {zip_path} is a bad zip file and cannot be opened.") + except Exception as e: + log_message(f"Error: An unexpected error occurred while extracting files from {zip_path}: {e}") + + # Cleanup function to keep only a specific number of versions + def cleanup_old_versions(directory, keep_count): + versions = sorted( + (os.path.join(directory, d) for d in os.listdir(directory) if os.path.isdir(os.path.join(directory, d))), + key=os.path.getmtime + ) + old_versions = versions[:-keep_count] + for version in old_versions: + for root, dirs, files in os.walk(version, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + log_message(f"Removed file: {os.path.join(root, name)}") + for name in dirs: + os.rmdir(os.path.join(root, name)) + os.rmdir(version) + log_message(f"Removed directory: {version}") + + # Function to check for missing releases and download them if necessary + def check_and_download(releases, latest_release_file, release_type, download_dir, versions_to_keep, extract_patterns, selected_assets=None): + downloaded_versions = [] + + if not os.path.exists(download_dir): + os.makedirs(download_dir) + + # Load the latest release tag from file if available + saved_release_tag = None + if os.path.exists(latest_release_file): + with open(latest_release_file, 'r') as f: + saved_release_tag = f.read().strip() + + # Determine which releases to download + for release in releases: + release_tag = release['tag_name'] + release_dir = os.path.join(download_dir, release_tag) + + if os.path.exists(release_dir) or release_tag == saved_release_tag: + log_message(f"Skipping version {release_tag}, already exists.") + else: + # Proceed to download this version + os.makedirs(release_dir, exist_ok=True) + log_message(f"Downloading new version: {release_tag}") + for asset in release['assets']: + file_name = asset['name'] + if selected_assets: + if file_name not in selected_assets: + continue + download_path = os.path.join(release_dir, file_name) + download_file(asset['browser_download_url'], download_path) + if auto_extract and file_name.endswith('.zip') and release_type == "Firmware": + extract_files(download_path, release_dir, extract_patterns) + downloaded_versions.append(release_tag) + + # Update latest_release_file with the most recent tag + if releases: + with open(latest_release_file, 'w') as f: + f.write(releases[0]['tag_name']) + + # Clean up old versions + cleanup_old_versions(download_dir, versions_to_keep) + return downloaded_versions + + start_time = time.time() + log_message("Starting Fetchtastic...") + + downloaded_firmwares = [] + downloaded_apks = [] + + # URLs for releases + android_releases_url = "https://api.github.com/repos/meshtastic/Meshtastic-Android/releases" + firmware_releases_url = "https://api.github.com/repos/meshtastic/firmware/releases" + + # Scan for the last 5 releases, download the latest versions_to_download + releases_to_scan = 5 + + if save_firmware and selected_firmware_assets: + versions_to_download = firmware_versions_to_keep + latest_firmware_releases = get_latest_releases(firmware_releases_url, versions_to_download, releases_to_scan) + downloaded_firmwares = check_and_download( + latest_firmware_releases, + latest_firmware_release_file, + "Firmware", + firmware_dir, + firmware_versions_to_keep, + extract_patterns, + selected_assets=selected_firmware_assets + ) + log_message(f"Latest Firmware releases: {', '.join(release['tag_name'] for release in latest_firmware_releases)}") + elif not selected_firmware_assets: + log_message("No firmware assets selected. Skipping firmware download.") + + if save_apks and selected_apk_assets: + versions_to_download = android_versions_to_keep + latest_android_releases = get_latest_releases(android_releases_url, versions_to_download, releases_to_scan) + downloaded_apks = check_and_download( + latest_android_releases, + latest_android_release_file, + "Android APK", + apks_dir, + android_versions_to_keep, + extract_patterns, + selected_assets=selected_apk_assets + ) + log_message(f"Latest Android APK releases: {', '.join(release['tag_name'] for release in latest_android_releases)}") + elif not selected_apk_assets: + log_message("No APK assets selected. Skipping APK download.") + + end_time = time.time() + log_message(f"Finished the Meshtastic downloader. Total time taken: {end_time - start_time:.2f} seconds") + + # Send notification if there are new downloads + if downloaded_firmwares or downloaded_apks: + message = "" + if downloaded_firmwares: + message += f"New Firmware releases {', '.join(downloaded_firmwares)} downloaded.\n" + if downloaded_apks: + message += f"New Android APK releases {', '.join(downloaded_apks)} downloaded.\n" + message += f"{datetime.now()}" + send_ntfy_notification(message) + else: + message = ( + f"All Firmware and Android APK versions are up to date.\n" + f"Latest Firmware releases: {', '.join(release['tag_name'] for release in latest_firmware_releases)}\n" + f"Latest Android APK releases: {', '.join(release['tag_name'] for release in latest_android_releases)}\n" + f"{datetime.now()}" + ) + send_ntfy_notification(message) + +if __name__ == "__main__": + main() diff --git a/app/menu_apk.py b/app/menu_apk.py new file mode 100644 index 0000000..f44c697 --- /dev/null +++ b/app/menu_apk.py @@ -0,0 +1,40 @@ +# fetchtastic/menu_apk.py + +import re +import requests +from pick import pick + +def fetch_apk_assets(): + apk_releases_url = "https://api.github.com/repos/meshtastic/Meshtastic-Android/releases" + response = requests.get(apk_releases_url) + response.raise_for_status() + releases = response.json() + # Get the latest release + latest_release = releases[0] + assets = latest_release['assets'] + asset_names = [asset['name'] for asset in assets if asset['name'].endswith('.apk')] + return asset_names + +def select_assets(assets): + title = '''Select the APK files you want to download (press SPACE to select, ENTER to confirm): +Note: These are files from the latest release. Version numbers may change in other releases.''' + options = assets + selected_options = pick(options, title, multiselect=True, min_selection_count=0, indicator='*') + selected_assets = [option[0] for option in selected_options] + if not selected_assets: + print("No APK files selected. APKs will not be downloaded.") + return None + return selected_assets + +def run_menu(): + try: + assets = fetch_apk_assets() + selected_assets = select_assets(assets) + if selected_assets is None: + return None + return { + 'selected_assets': selected_assets + } + except Exception as e: + print(f"An error occurred: {e}") + return None diff --git a/app/menu_firmware.py b/app/menu_firmware.py new file mode 100644 index 0000000..e108445 --- /dev/null +++ b/app/menu_firmware.py @@ -0,0 +1,39 @@ +# app/menu_firmware.py + +import requests +from pick import pick + +def fetch_firmware_assets(): + firmware_releases_url = "https://api.github.com/repos/meshtastic/firmware/releases" + response = requests.get(firmware_releases_url) + response.raise_for_status() + releases = response.json() + # Get the latest release + latest_release = releases[0] + assets = latest_release['assets'] + asset_names = [asset['name'] for asset in assets] + return asset_names + +def select_assets(assets): + title = '''Select the firmware files you want to download (press SPACE to select, ENTER to confirm): +Note: These are files from the latest release. Version numbers may change in other releases.''' + options = assets + selected_options = pick(options, title, multiselect=True, min_selection_count=0, indicator='*') + selected_assets = [option[0] for option in selected_options] + if not selected_assets: + print("No firmware files selected. Firmware will not be downloaded.") + return None + return selected_assets + +def run_menu(): + try: + assets = fetch_firmware_assets() + selected_assets = select_assets(assets) + if selected_assets is None: + return None + return { + 'selected_assets': selected_assets + } + except Exception as e: + print(f"An error occurred: {e}") + return None diff --git a/app/setup_config.py b/app/setup_config.py new file mode 100644 index 0000000..e9488ee --- /dev/null +++ b/app/setup_config.py @@ -0,0 +1,100 @@ +# app/setup_config.py + +import os +import yaml +from . import menu_apk +from . import menu_firmware + +# Define the default configuration directory +DEFAULT_CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".fetchtastic") +CONFIG_FILE = os.path.join(DEFAULT_CONFIG_DIR, "fetchtastic.yaml") + +def config_exists(): + return os.path.exists(CONFIG_FILE) + +def run_setup(): + print("Running Fetchtastic Setup...") + if not os.path.exists(DEFAULT_CONFIG_DIR): + os.makedirs(DEFAULT_CONFIG_DIR) + + config = {} + + # Prompt to save APKs, firmware, or both + save_choice = input("Do you want to save APKs, firmware, or both? [a/f/b] (default: b): ").strip().lower() or 'b' + if save_choice == 'a': + save_apks = True + save_firmware = False + elif save_choice == 'f': + save_apks = False + save_firmware = True + else: + save_apks = True + save_firmware = True + config['SAVE_APKS'] = save_apks + config['SAVE_FIRMWARE'] = save_firmware + + # Run the menu scripts based on user choices + if save_apks: + apk_selection = menu_apk.run_menu() + if not apk_selection: + save_apks = False + config['SAVE_APKS'] = False + else: + config['SELECTED_APK_ASSETS'] = apk_selection['selected_assets'] + if save_firmware: + firmware_selection = menu_firmware.run_menu() + if not firmware_selection: + save_firmware = False + config['SAVE_FIRMWARE'] = False + else: + config['SELECTED_FIRMWARE_ASSETS'] = firmware_selection['selected_assets'] + + # Prompt for number of versions to keep + if save_apks: + android_versions_to_keep = input("Enter the number of different versions of the Android app to keep (default: 2): ").strip() or '2' + config['ANDROID_VERSIONS_TO_KEEP'] = int(android_versions_to_keep) + if save_firmware: + firmware_versions_to_keep = input("Enter the number of different versions of the firmware to keep (default: 2): ").strip() or '2' + config['FIRMWARE_VERSIONS_TO_KEEP'] = int(firmware_versions_to_keep) + + # Prompt for automatic extraction + auto_extract = input("Do you want to automatically extract specific files from firmware zips? [y/n] (default: n): ").strip().lower() or 'n' + if auto_extract == 'y': + extract_patterns = input("Enter the strings to match for extraction from the firmware .zip files, separated by spaces: ").strip() + if extract_patterns: + config['AUTO_EXTRACT'] = True + config['EXTRACT_PATTERNS'] = extract_patterns.split() + else: + config['AUTO_EXTRACT'] = False + else: + config['AUTO_EXTRACT'] = False + + # Prompt for custom download directory + default_download_dir = os.path.join(os.path.expanduser("~"), "fetchtastic_downloads") + download_dir = input(f"Enter the download directory (default: {default_download_dir}): ").strip() or default_download_dir + config['DOWNLOAD_DIR'] = download_dir + + # Prompt for NTFY server configuration + notifications = input("Do you want to set up notifications via NTFY? [y/n] (default: y): ").strip().lower() or 'y' + if notifications == 'y': + ntfy_server = input("Enter the NTFY server (default: ntfy.sh): ").strip() or 'ntfy.sh' + if not ntfy_server.startswith('http://') and not ntfy_server.startswith('https://'): + ntfy_server = 'https://' + ntfy_server + topic_name = input("Enter a unique topic name (default: fetchtastic): ").strip() or 'fetchtastic' + ntfy_topic = f"{ntfy_server}/{topic_name}" + config['NTFY_SERVER'] = ntfy_topic + else: + config['NTFY_SERVER'] = '' + + # Save configuration to YAML file + with open(CONFIG_FILE, 'w') as f: + yaml.dump(config, f) + + print(f"Setup complete. Configuration saved at {CONFIG_FILE}") + +def load_config(): + if not config_exists(): + return None + with open(CONFIG_FILE, 'r') as f: + config = yaml.safe_load(f) + return config diff --git a/fetchtastic.py b/fetchtastic.py deleted file mode 100644 index 3e9041e..0000000 --- a/fetchtastic.py +++ /dev/null @@ -1,236 +0,0 @@ -import os -import requests -import zipfile -import time -from datetime import datetime -from dotenv import load_dotenv -from requests.adapters import HTTPAdapter -from requests.packages.urllib3.util.retry import Retry -import re - -# Change to the script's directory -os.chdir(os.path.dirname(os.path.abspath(__file__))) - -# Load configuration -env_file = ".env" -load_dotenv(env_file) - -# Environment variables -save_apks = os.getenv("SAVE_APKS", "true") == "true" -save_firmware = os.getenv("SAVE_FIRMWARE", "true") == "true" -ntfy_server = os.getenv("NTFY_SERVER", "") -android_versions_to_keep = int(os.getenv("ANDROID_VERSIONS_TO_KEEP", 2)) -firmware_versions_to_keep = int(os.getenv("FIRMWARE_VERSIONS_TO_KEEP", 2)) -auto_extract = os.getenv("AUTO_EXTRACT", "no") == "yes" -extract_patterns = os.getenv("EXTRACT_PATTERNS", "").split() - -apk_patterns_str = os.getenv("APK_PATTERNS", "") -apk_patterns = apk_patterns_str.split() - -firmware_patterns_str = os.getenv("FIRMWARE_PATTERNS", "") -firmware_patterns = firmware_patterns_str.split() - -# Paths for storage -android_releases_url = "https://api.github.com/repos/meshtastic/Meshtastic-Android/releases" -firmware_releases_url = "https://api.github.com/repos/meshtastic/firmware/releases" -download_dir = "/storage/emulated/0/Download/Meshtastic" -firmware_dir = os.path.join(download_dir, "firmware") -apks_dir = os.path.join(download_dir, "apks") -latest_android_release_file = os.path.join(apks_dir, "latest_android_release.txt") -latest_firmware_release_file = os.path.join(firmware_dir, "latest_firmware_release.txt") - -# Logging setup -log_file = "fetchtastic.log" - -def log_message(message): - with open(log_file, "a") as log: - log.write(f"{datetime.now()}: {message}\n") - print(message) - -def send_ntfy_notification(message): - if ntfy_server: - try: - response = requests.post(ntfy_server, data=message.encode('utf-8')) - response.raise_for_status() - except requests.exceptions.RequestException as e: - log_message(f"Error sending notification: {e}") - -# Function to get the latest releases and sort by date -def get_latest_releases(url, versions_to_keep, scan_count=5): - response = requests.get(url) - response.raise_for_status() - releases = response.json() - # Sort releases by published date, descending order - sorted_releases = sorted(releases, key=lambda r: r['published_at'], reverse=True) - # Limit the number of releases to be scanned and downloaded - return sorted_releases[:scan_count][:versions_to_keep] - -# Function to download a file with retry mechanism -def download_file(url, download_path): - session = requests.Session() - retry = Retry(connect=3, backoff_factor=1, status_forcelist=[502, 503, 504]) - adapter = HTTPAdapter(max_retries=retry) - session.mount('http://', adapter) - session.mount('https://', adapter) - - try: - if not os.path.exists(download_path): - log_message(f"Downloading {url}") - response = session.get(url, stream=True) - response.raise_for_status() - with open(download_path, 'wb') as file: - for chunk in response.iter_content(1024): - file.write(chunk) - log_message(f"Downloaded {download_path}") - else: - log_message(f"{download_path} already exists, skipping download.") - except requests.exceptions.RequestException as e: - log_message(f"Error downloading {url}: {e}") - -# Function to extract files based on the given patterns -def extract_files(zip_path, extract_dir, patterns): - try: - with zipfile.ZipFile(zip_path, 'r') as zip_ref: - for file_name in zip_ref.namelist(): - if any(pattern in file_name for pattern in patterns): - zip_ref.extract(file_name, extract_dir) - log_message(f"Extracted {file_name} to {extract_dir}") - except zipfile.BadZipFile: - log_message(f"Error: {zip_path} is a bad zip file and cannot be opened.") - except Exception as e: - log_message(f"Error: An unexpected error occurred while extracting files from {zip_path}: {e}") - -# Cleanup function to keep only a specific number of versions -def cleanup_old_versions(directory, keep_count): - versions = sorted( - (os.path.join(directory, d) for d in os.listdir(directory) if os.path.isdir(os.path.join(directory, d))), - key=os.path.getmtime - ) - old_versions = versions[:-keep_count] - for version in old_versions: - for root, dirs, files in os.walk(version, topdown=False): - for name in files: - os.remove(os.path.join(root, name)) - log_message(f"Removed file: {os.path.join(root, name)}") - for name in dirs: - os.rmdir(os.path.join(root, name)) - os.rmdir(version) - log_message(f"Removed directory: {version}") - -# Function to check for missing releases and download them if necessary -def check_and_download(releases, latest_release_file, release_type, download_dir, versions_to_keep, extract_patterns, patterns=None): - downloaded_versions = [] - - if not os.path.exists(download_dir): - os.makedirs(download_dir) - - # Load the latest release tag from file if available - saved_release_tag = None - if os.path.exists(latest_release_file): - with open(latest_release_file, 'r') as f: - saved_release_tag = f.read().strip() - - # Determine which releases to download - for release in releases: - release_tag = release['tag_name'] - release_dir = os.path.join(download_dir, release_tag) - - if os.path.exists(release_dir) or release_tag == saved_release_tag: - log_message(f"Skipping version {release_tag}, already exists.") - else: - # Proceed to download this version - os.makedirs(release_dir, exist_ok=True) - log_message(f"Downloading new version: {release_tag}") - for asset in release['assets']: - file_name = asset['name'] - # Generate pattern from asset filename - asset_pattern = re.sub(r'[-_.]?v?\d+.*', '', file_name) - # Check if any pattern matches the asset filename - if patterns: - if not any(asset_pattern.startswith(pattern) for pattern in patterns): - # Suppress messages for known non-relevant files - if file_name not in ['version_info.txt']: - pass # Optionally, you can log a debug message here - continue - download_path = os.path.join(release_dir, file_name) - download_file(asset['browser_download_url'], download_path) - if auto_extract and file_name.endswith('.zip') and release_type == "Firmware": - extract_files(download_path, release_dir, extract_patterns) - downloaded_versions.append(release_tag) - - # Update latest_release_file with the most recent tag - if releases: - with open(latest_release_file, 'w') as f: - f.write(releases[0]['tag_name']) - - # Clean up old versions - cleanup_old_versions(download_dir, versions_to_keep) - return downloaded_versions - -# Main function to run the downloader -def main(): - start_time = time.time() - log_message("Starting Fetchtastic...") - - downloaded_firmwares = [] - downloaded_apks = [] - - # Scan for the last 5 releases, download the latest versions_to_download - releases_to_scan = 5 - - if save_firmware and firmware_patterns: - versions_to_download = firmware_versions_to_keep - latest_firmware_releases = get_latest_releases(firmware_releases_url, versions_to_download, releases_to_scan) - downloaded_firmwares = check_and_download( - latest_firmware_releases, - latest_firmware_release_file, - "Firmware", - firmware_dir, - firmware_versions_to_keep, - extract_patterns, - patterns=firmware_patterns - ) - log_message(f"Latest Firmware releases: {', '.join(release['tag_name'] for release in latest_firmware_releases)}") - elif not firmware_patterns: - log_message("No firmware patterns selected. Skipping firmware download.") - - if save_apks and apk_patterns: - versions_to_download = android_versions_to_keep - latest_android_releases = get_latest_releases(android_releases_url, versions_to_download, releases_to_scan) - downloaded_apks = check_and_download( - latest_android_releases, - latest_android_release_file, - "Android APK", - apks_dir, - android_versions_to_keep, - extract_patterns, - patterns=apk_patterns - ) - log_message(f"Latest Android APK releases: {', '.join(release['tag_name'] for release in latest_android_releases)}") - elif not apk_patterns: - log_message("No APK patterns selected. Skipping APK download.") - - end_time = time.time() - log_message(f"Finished the Meshtastic downloader. Total time taken: {end_time - start_time:.2f} seconds") - - # Send notification if there are new downloads - if downloaded_firmwares or downloaded_apks: - message = "" - if downloaded_firmwares: - message += f"New Firmware releases {', '.join(downloaded_firmwares)} downloaded.\n" - if downloaded_apks: - message += f"New Android APK releases {', '.join(downloaded_apks)} downloaded.\n" - message += f"{datetime.now()}" - send_ntfy_notification(message) - else: - message = ( - f"All Firmware and Android APK versions are up to date.\n" - f"Latest Firmware releases: {', '.join(release['tag_name'] for release in latest_firmware_releases)}\n" - f"Latest Android APK releases: {', '.join(release['tag_name'] for release in latest_android_releases)}\n" - f"{datetime.now()}" - ) - send_ntfy_notification(message) - -# Run the main function if the script is executed directly -if __name__ == "__main__": - main() diff --git a/menu_apk.py b/menu_apk.py deleted file mode 100644 index cc0c41d..0000000 --- a/menu_apk.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/data/data/com.termux/files/usr/bin/python - -import os -import sys -import re -from pick import pick -import requests -from dotenv import load_dotenv - -# Load existing .env or create a new one -env_file = ".env" -if not os.path.exists(env_file): - open(env_file, 'a').close() - -load_dotenv(env_file) - -# Function to fetch the latest APK release assets -def fetch_apk_assets(): - apk_releases_url = "https://api.github.com/repos/meshtastic/Meshtastic-Android/releases" - response = requests.get(apk_releases_url) - response.raise_for_status() - releases = response.json() - # Get the latest release - latest_release = releases[0] - assets = latest_release['assets'] - asset_names = [asset['name'] for asset in assets if asset['name'].endswith('.apk')] - return asset_names - -# Function to present a menu to the user to select assets -def select_assets(assets): - title = '''Select the APK files you want to download (press SPACE to select, ENTER to confirm): -Note: These are files from the latest release. Version numbers may change in other releases.''' - options = assets - selected_options = pick(options, title, multiselect=True, min_selection_count=0, indicator='*') - selected_assets = [option[0] for option in selected_options] - if not selected_assets: - print("No APK files selected. APKs will not be downloaded.") - # Update .env to reflect that APKs should not be saved - with open(env_file, 'a') as f: - f.write('SAVE_APKS=false\n') - return None - return selected_assets - -def extract_patterns(selected_assets): - patterns = [] - for asset in selected_assets: - # Remove version numbers and extensions to create patterns - pattern = re.sub(r'[-_.]?v?\d+.*', '', asset) - patterns.append(pattern) - return patterns - -def main(): - try: - assets = fetch_apk_assets() - selected_assets = select_assets(assets) - if selected_assets is None: - return - # Save the selected assets to .env - selected_assets_str = ' '.join(selected_assets) - # Remove existing SELECTED_APK_ASSETS and APK_PATTERNS lines from .env - with open(env_file, 'r') as f: - lines = f.readlines() - with open(env_file, 'w') as f: - for line in lines: - if not line.startswith('SELECTED_APK_ASSETS=') and not line.startswith('APK_PATTERNS='): - f.write(line) - f.write(f'SELECTED_APK_ASSETS="{selected_assets_str}"\n') - # Generate patterns - patterns = extract_patterns(selected_assets) - patterns_str = ' '.join(patterns) - f.write(f'APK_PATTERNS="{patterns_str}"\n') - print("Selected APK assets saved to .env") - except Exception as e: - print(f"An error occurred: {e}") - sys.exit(1) - -if __name__ == "__main__": - main() diff --git a/menu_firmware.py b/menu_firmware.py deleted file mode 100644 index dffac53..0000000 --- a/menu_firmware.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/data/data/com.termux/files/usr/bin/python - -import os -import sys -import re -from pick import pick -import requests -from dotenv import load_dotenv - -# Load existing .env or create a new one -env_file = ".env" -if not os.path.exists(env_file): - open(env_file, 'a').close() - -load_dotenv(env_file) - -# Function to fetch the latest firmware release assets -def fetch_firmware_assets(): - firmware_releases_url = "https://api.github.com/repos/meshtastic/firmware/releases" - response = requests.get(firmware_releases_url) - response.raise_for_status() - releases = response.json() - # Get the latest release - latest_release = releases[0] - assets = latest_release['assets'] - asset_names = [asset['name'] for asset in assets] - return asset_names - -# Function to present a menu to the user to select assets -def select_assets(assets): - title = '''Select the firmware files you want to download (press SPACE to select, ENTER to confirm): -Note: These are files from the latest release. Version numbers may change in other releases.''' - options = assets - selected_options = pick(options, title, multiselect=True, min_selection_count=0, indicator='*') - selected_assets = [option[0] for option in selected_options] - if not selected_assets: - print("No firmware files selected. Firmware will not be downloaded.") - # Update .env to reflect that firmware should not be saved - with open(env_file, 'a') as f: - f.write('SAVE_FIRMWARE=false\n') - return None - return selected_assets - -def extract_patterns(selected_assets): - patterns = [] - for asset in selected_assets: - # Remove version numbers and extensions to create patterns - pattern = re.sub(r'[-_.]?v?\d+.*', '', asset) - patterns.append(pattern) - return patterns - -def main(): - try: - assets = fetch_firmware_assets() - selected_assets = select_assets(assets) - if selected_assets is None: - return - # Save the selected assets to .env - selected_assets_str = ' '.join(selected_assets) - # Remove existing SELECTED_FIRMWARE_ASSETS and FIRMWARE_PATTERNS lines from .env - with open(env_file, 'r') as f: - lines = f.readlines() - with open(env_file, 'w') as f: - for line in lines: - if not line.startswith('SELECTED_FIRMWARE_ASSETS=') and not line.startswith('FIRMWARE_PATTERNS='): - f.write(line) - f.write(f'SELECTED_FIRMWARE_ASSETS="{selected_assets_str}"\n') - # Generate patterns - patterns = extract_patterns(selected_assets) - patterns_str = ' '.join(patterns) - f.write(f'FIRMWARE_PATTERNS="{patterns_str}"\n') - print("Selected firmware assets saved to .env") - except Exception as e: - print(f"An error occurred: {e}") - sys.exit(1) - -if __name__ == "__main__": - main() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..5d9b6ec --- /dev/null +++ b/setup.cfg @@ -0,0 +1,26 @@ +[metadata] +name = fetchtastic +version = 0.1.0 +author = Jeremiah K +author_email = jeremiahk@gmx.com +description = Meshtastic Firmware and APK Downloader +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/jeremiah-k/fetchtastic +license = MIT +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: MIT License + Operating System :: OS Independent + +[options] +packages = find: +install_requires = + requests + pick + PyYAML + urllib3 + +[options.entry_points] +console_scripts = + fetchtastic = app.cli:main diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1030e43 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup, find_packages + +setup() diff --git a/setup.sh b/setup.sh deleted file mode 100755 index cd73f7e..0000000 --- a/setup.sh +++ /dev/null @@ -1,204 +0,0 @@ -#!/data/data/com.termux/files/usr/bin/sh - -# Ensure necessary packages are installed -pkg update -y -pkg install -y cronie python openssl termux-api python-pip - -# Install Python modules -LDFLAGS=" -lm -lcompiler_rt" pip install requests python-dotenv pick - -# Create the boot script directory if it doesn't exist -mkdir -p ~/.termux/boot - -# Create the start-crond.sh script in the boot directory -echo '#!/data/data/com.termux/files/usr/bin/sh' > ~/.termux/boot/start-crond.sh -echo 'crond' >> ~/.termux/boot/start-crond.sh -echo 'python /data/data/com.termux/files/home/fetchtastic/fetchtastic.py' >> ~/.termux/boot/start-crond.sh -chmod +x ~/.termux/boot/start-crond.sh - -# Add a separator and some spacing -echo "--------------------------------------------------------" -echo - -# Get the directory of the setup.sh script -SCRIPT_DIR=$(dirname "$(readlink -f "$0")") - -# Load existing .env if it exists -ENV_FILE="$SCRIPT_DIR/.env" -if [ -f "$ENV_FILE" ]; then - # Load current settings - . "$ENV_FILE" - echo "Existing configuration found. Do you want to update it? [y/n] (default: n): " - read update_config - update_config=${update_config:-n} -else - update_config="y" -fi - -if [ "$update_config" = "y" ]; then - # Prompt to save APKs, firmware, or both - echo "Do you want to save APKs, firmware, or both? [a/f/b] (default: ${SAVE_CHOICE:-b}): " - read save_choice - save_choice=${save_choice:-${SAVE_CHOICE:-b}} - case "$save_choice" in - a|A) save_apks=true; save_firmware=false ;; - f|F) save_apks=false; save_firmware=true ;; - *) save_apks=true; save_firmware=true ;; - esac - SAVE_CHOICE="$save_choice" - - # Save the initial configuration to .env - echo "SAVE_APKS=$save_apks" > "$ENV_FILE" - echo "SAVE_FIRMWARE=$save_firmware" >> "$ENV_FILE" - echo "SAVE_CHOICE=$SAVE_CHOICE" >> "$ENV_FILE" - - # Run the menu scripts based on user choices - if [ "$save_apks" = true ]; then - python menu_apk.py - fi - if [ "$save_firmware" = true ]; then - python menu_firmware.py - fi - - # Prompt for number of versions to keep for Android app if saving APKs - if [ "$save_apks" = true ]; then - echo "Enter the number of different versions of the Android app to keep (default: ${ANDROID_VERSIONS_TO_KEEP:-2}): " - read android_versions_to_keep - android_versions_to_keep=${android_versions_to_keep:-${ANDROID_VERSIONS_TO_KEEP:-2}} - echo "ANDROID_VERSIONS_TO_KEEP=$android_versions_to_keep" >> "$ENV_FILE" - fi - - # Prompt for number of versions to keep for firmware if saving firmware - if [ "$save_firmware" = true ]; then - echo "Enter the number of different versions of the firmware to keep (default: ${FIRMWARE_VERSIONS_TO_KEEP:-2}): " - read firmware_versions_to_keep - firmware_versions_to_keep=${firmware_versions_to_keep:-${FIRMWARE_VERSIONS_TO_KEEP:-2}} - echo "FIRMWARE_VERSIONS_TO_KEEP=$firmware_versions_to_keep" >> "$ENV_FILE" - fi - - # Prompt for automatic extraction of firmware files if saving firmware - if [ "$save_firmware" = true ]; then - default_extract=${AUTO_EXTRACT_YN:-n} - if [ "$default_extract" = "yes" ]; then - default_extract="y" - elif [ "$default_extract" = "no" ]; then - default_extract="n" - fi - echo "Do you want to automatically extract specific files from firmware zips? [y/n] (default: $default_extract): " - read auto_extract - auto_extract=${auto_extract:-$default_extract} - if [ "$auto_extract" = "y" ]; then - default_patterns="${EXTRACT_PATTERNS}" - echo "Enter the strings to match for extraction from the firmware .zip files, separated by spaces (current: '${default_patterns}'):" - read extract_patterns - extract_patterns=${extract_patterns:-${default_patterns}} - if [ -z "$extract_patterns" ]; then - echo "AUTO_EXTRACT=no" >> "$ENV_FILE" - else - echo "AUTO_EXTRACT=yes" >> "$ENV_FILE" - echo "EXTRACT_PATTERNS=\"$extract_patterns\"" >> "$ENV_FILE" - fi - else - echo "AUTO_EXTRACT=no" >> "$ENV_FILE" - fi - AUTO_EXTRACT_YN="$auto_extract" - fi - - # Prompt for NTFY server configuration - default_notifications=${NOTIFICATIONS:-y} - echo "Do you want to set up notifications via NTFY? [y/n] (default: $default_notifications): " - read notifications - notifications=${notifications:-$default_notifications} - NOTIFICATIONS="$notifications" - - if [ "$notifications" = "y" ]; then - # Prompt for the NTFY server - echo "Enter the NTFY server (default: ${NTFY_SERVER_URL:-ntfy.sh}): " - read ntfy_server - ntfy_server=${ntfy_server:-${NTFY_SERVER_URL:-ntfy.sh}} - # Add https:// if not included - case "$ntfy_server" in - http://*|https://*) ;; # Do nothing if it already starts with http:// or https:// - *) ntfy_server="https://$ntfy_server" ;; - esac - - # Generate a random topic name if not set - random_topic=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 5 | head -n 1) - default_topic_name=${NTFY_TOPIC_NAME:-fetchtastic-$random_topic} - - # Prompt for the topic name - echo "Enter a unique topic name (default: $default_topic_name): " - read topic_name - topic_name=${topic_name:-$default_topic_name} - - # Construct the full NTFY topic URL - ntfy_topic="$ntfy_server/$topic_name" - - # Save the NTFY configuration to .env - echo "NTFY_SERVER=\"$ntfy_topic\"" >> "$ENV_FILE" - echo "NTFY_SERVER_URL=\"$ntfy_server\"" >> "$ENV_FILE" - echo "NTFY_TOPIC_NAME=\"$topic_name\"" >> "$ENV_FILE" - - # Save the topic URL to topic.txt - echo "$ntfy_topic" > "$SCRIPT_DIR/topic.txt" - - echo "Notification setup complete. Your NTFY topic URL is: $ntfy_topic" - else - echo "Skipping notification setup." - echo "NTFY_SERVER=" >> "$ENV_FILE" - rm -f "$SCRIPT_DIR/topic.txt" # Remove the topic.txt file if notifications are disabled - fi -else - echo "Keeping existing configuration." -fi - -# Check for existing cron jobs related to fetchtastic.py -existing_cron=$(crontab -l 2>/dev/null | grep 'fetchtastic.py') - -if [ -n "$existing_cron" ]; then - echo "An existing cron job for fetchtastic.py was found:" - echo "$existing_cron" - read -p "Do you want to keep the existing crontab entry for running the script daily at 3 AM? [y/n] (default: y): " keep_cron - keep_cron=${keep_cron:-y} - - if [ "$keep_cron" = "n" ]; then - (crontab -l 2>/dev/null | grep -v 'fetchtastic.py') | crontab - - echo "Crontab entry removed." - read -p "Do you want to add a new crontab entry to run the script daily at 3 AM? [y/n] (default: y): " add_cron - add_cron=${add_cron:-y} - if [ "$add_cron" = "y" ]; then - (crontab -l 2>/dev/null; echo "0 3 * * * python /data/data/com.termux/files/home/fetchtastic/fetchtastic.py") | crontab - - echo "Crontab entry added." - else - echo "Skipping crontab installation." - fi - else - echo "Keeping existing crontab entry." - fi -else - read -p "Do you want to add a crontab entry to run the script daily at 3 AM? [y/n] (default: y): " add_cron - add_cron=${add_cron:-y} - if [ "$add_cron" = "y" ]; then - (crontab -l 2>/dev/null; echo "0 3 * * * python /data/data/com.termux/files/home/fetchtastic/fetchtastic.py") | crontab - - echo "Crontab entry added." - else - echo "Skipping crontab installation." - fi -fi - -# Run the script once after setup and show the latest version -echo -echo "Performing first run, this may take a few minutes..." -latest_output=$(python /data/data/com.termux/files/home/fetchtastic/fetchtastic.py) - -echo -echo "Setup complete. The Meshtastic downloader script will run on boot and also daily at 3 AM (if crontab entry was added)." -echo "The downloaded files will be stored in '/storage/emulated/0/Download/Meshtastic' with subdirectories 'firmware' and 'apks'." -echo "$latest_output" - -# New final message logic -if [ "$NOTIFICATIONS" = "y" ]; then - echo "Your NTFY topic URL is: $ntfy_topic" -else - echo "Notifications are not set. To configure notifications, please rerun setup.sh." -fi diff --git a/uninstall.sh b/uninstall.sh deleted file mode 100755 index e5c2910..0000000 --- a/uninstall.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/data/data/com.termux/files/usr/bin/sh - -# Remove the crontab entry for fetchtastic.py -existing_cron=$(crontab -l 2>/dev/null | grep 'fetchtastic.py') - -if [ -n "$existing_cron" ]; then - (crontab -l 2>/dev/null | grep -v 'fetchtastic.py') | crontab - - echo "Crontab entry for fetchtastic.py removed." -else - echo "No crontab entry for fetchtastic.py found." -fi - -# Remove the start-crond.sh script from the boot directory -if [ -f ~/.termux/boot/start-crond.sh ]; then - rm ~/.termux/boot/start-crond.sh - echo "start-crond.sh removed from the boot directory." -else - echo "start-crond.sh not found in the boot directory." -fi - -# Remove the .env file and topic.txt if they exist -if [ -f ~/fetchtastic/.env ]; then - rm ~/fetchtastic/.env - echo ".env file removed." -else - echo ".env file not found." -fi - -if [ -f ~/fetchtastic/topic.txt ]; then - rm ~/fetchtastic/topic.txt - echo "topic.txt removed." -else - echo "topic.txt not found." -fi - -# Remove version tracking files if they exist -if [ -f /storage/emulated/0/Download/Meshtastic/apks/latest_android_release.txt ]; then - rm /storage/emulated/0/Download/Meshtastic/apks/latest_android_release.txt - echo "latest_android_release.txt removed." -else - echo "latest_android_release.txt not found." -fi - -if [ -f /storage/emulated/0/Download/Meshtastic/firmware/latest_firmware_release.txt ]; then - rm /storage/emulated/0/Download/Meshtastic/firmware/latest_firmware_release.txt - echo "latest_firmware_release.txt removed." -else - echo "latest_firmware_release.txt not found." -fi - -# Ask to remove downloaded firmware directory -read -p "Do you want to remove the downloaded firmware directory? [y/n] (default: n): " remove_firmware_dir -remove_firmware_dir=${remove_firmware_dir:-n} -if [ "$remove_firmware_dir" = "y" ]; then - if [ -d /storage/emulated/0/Download/Meshtastic/firmware ]; then - rm -r /storage/emulated/0/Download/Meshtastic/firmware - echo "Firmware directory removed." - else - echo "Firmware directory not found." - fi -else - echo "Skipping removal of firmware directory." -fi - -# Ask to remove downloaded APKs directory -read -p "Do you want to remove the downloaded APKs directory? [y/n] (default: n): " remove_apks_dir -remove_apks_dir=${remove_apks_dir:-n} -if [ "$remove_apks_dir" = "y" ]; then - if [ -d /storage/emulated/0/Download/Meshtastic/apks ]; then - rm -r /storage/emulated/0/Download/Meshtastic/apks - echo "APKs directory removed." - else - echo "APKs directory not found." - fi -else - echo "Skipping removal of APKs directory." -fi - -# Inform user about remaining files -echo "Uninstall complete. If you want to remove the remaining downloaded files, delete the '/storage/emulated/0/Download/Meshtastic' directory." -echo "You may also want to remove the fetchtastic repository by deleting the '~/fetchtastic' directory." From 397f4d455870197a1d3a92fd4d33d0b6a3f4d536 Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Sat, 12 Oct 2024 08:03:19 -0500 Subject: [PATCH 05/14] Add __init__.py, adjust setup files --- app/__init__.py | 0 setup.cfg | 4 ++++ setup.py | 18 +++++++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 app/__init__.py diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/setup.cfg b/setup.cfg index 5d9b6ec..0098a0a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,6 +15,10 @@ classifiers = [options] packages = find: + +[options.packages.find] +where = . + install_requires = requests pick diff --git a/setup.py b/setup.py index 1030e43..bcf05d2 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,19 @@ from setuptools import setup, find_packages -setup() +setup( + name='fetchtastic', + version='0.1.0', + packages=find_packages(), + install_requires=[ + 'requests', + 'pick', + 'PyYAML', + 'urllib3', + ], + entry_points={ + 'console_scripts': [ + 'fetchtastic=app.cli:main', + ], + }, + # Include other metadata as needed +) From e737d2ea31d204493412b765221406f95cc7aa4c Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Sat, 12 Oct 2024 09:53:07 -0500 Subject: [PATCH 06/14] Create directories if needed --- app/downloader.py | 7 ++++++- app/setup_config.py | 27 ++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/downloader.py b/app/downloader.py index a9f74c2..62e0830 100644 --- a/app/downloader.py +++ b/app/downloader.py @@ -29,12 +29,17 @@ def main(): selected_apk_assets = config.get('SELECTED_APK_ASSETS', []) selected_firmware_assets = config.get('SELECTED_FIRMWARE_ASSETS', []) - download_dir = config.get('DOWNLOAD_DIR', os.path.join(os.path.expanduser("~"), "fetchtastic_downloads")) + download_dir = config.get('DOWNLOAD_DIR', os.path.join(os.path.expanduser("~"), "Downloads", "Fetchtastic")) firmware_dir = os.path.join(download_dir, "firmware") apks_dir = os.path.join(download_dir, "apks") latest_android_release_file = os.path.join(apks_dir, "latest_android_release.txt") latest_firmware_release_file = os.path.join(firmware_dir, "latest_firmware_release.txt") + # Create necessary directories + for dir_path in [download_dir, firmware_dir, apks_dir]: + if not os.path.exists(dir_path): + os.makedirs(dir_path) + # Logging setup log_file = os.path.join(download_dir, "fetchtastic.log") diff --git a/app/setup_config.py b/app/setup_config.py index e9488ee..8637f64 100644 --- a/app/setup_config.py +++ b/app/setup_config.py @@ -4,10 +4,22 @@ import yaml from . import menu_apk from . import menu_firmware +from . import downloader # Import downloader to perform first run # Define the default configuration directory -DEFAULT_CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".fetchtastic") -CONFIG_FILE = os.path.join(DEFAULT_CONFIG_DIR, "fetchtastic.yaml") +HOME_DIR = os.path.expanduser("~") + +# Try to find the Downloads directory +DOWNLOADS_DIR = os.path.join(HOME_DIR, 'Downloads') +if not os.path.exists(DOWNLOADS_DIR): + # Try other common locations + DOWNLOADS_DIR = os.path.join(HOME_DIR, 'Download') + if not os.path.exists(DOWNLOADS_DIR): + # Use HOME_DIR if Downloads directory is not found + DOWNLOADS_DIR = HOME_DIR + +DEFAULT_CONFIG_DIR = os.path.join(DOWNLOADS_DIR, 'Fetchtastic') +CONFIG_FILE = os.path.join(DEFAULT_CONFIG_DIR, 'fetchtastic.yaml') def config_exists(): return os.path.exists(CONFIG_FILE) @@ -69,9 +81,8 @@ def run_setup(): else: config['AUTO_EXTRACT'] = False - # Prompt for custom download directory - default_download_dir = os.path.join(os.path.expanduser("~"), "fetchtastic_downloads") - download_dir = input(f"Enter the download directory (default: {default_download_dir}): ").strip() or default_download_dir + # Set the download directory to the same as the config directory + download_dir = DEFAULT_CONFIG_DIR config['DOWNLOAD_DIR'] = download_dir # Prompt for NTFY server configuration @@ -92,6 +103,12 @@ def run_setup(): print(f"Setup complete. Configuration saved at {CONFIG_FILE}") + # Ask if the user wants to perform a first run + perform_first_run = input("Do you want to perform a first run now? [y/n] (default: y): ").strip().lower() or 'y' + if perform_first_run == 'y': + print("Performing first run, this may take a few minutes...") + downloader.main() + def load_config(): if not config_exists(): return None From 18bee6b4f460a7a3ffa008bd9e707d240712765f Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Sat, 12 Oct 2024 13:22:30 -0500 Subject: [PATCH 07/14] create pyproject.toml, requirements.txt, cron changes --- README.md | 106 ++++++++++++++++++++++++++++++++++++-------- app/cli.py | 6 ++- app/setup_config.py | 74 +++++++++++++++++++++++++++++++ pyproject.toml | 3 ++ requirements.txt | 4 ++ 5 files changed, 174 insertions(+), 19 deletions(-) create mode 100644 pyproject.toml create mode 100644 requirements.txt diff --git a/README.md b/README.md index 463c712..b4b1b89 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,105 @@ # Fetchtastic Termux Setup -This repository contains a set of scripts to download the latest Meshtastic Android app and Firmware releases to your phone via Termux. It also provides optional notifications via a NTFY server. Follow the steps below to set up and run the script. +Fetchtastic is a tool to download the latest Meshtastic Android app and Firmware releases to your phone via Termux. It also provides optional notifications via an NTFY server. This guide will help you set up and run Fetchtastic on your device. -## Setup Steps +## Prerequisites -### Step 1: Install **Termux** and addons. +### Install Termux and Add-ons -1. Install Termux: Download and install [Termux](https://f-droid.org/en/packages/com.termux/) from F-Droid. -2. Install Termux Boot: Download and install [Termux Boot](https://f-droid.org/en/packages/com.termux.boot/) from F-Droid. -3. Install Termux API: Download and install [Termux API](https://f-droid.org/en/packages/com.termux.api/) from F-Droid. -4. (Optional) Install ntfy: Download and install [ntfy](https://f-droid.org/en/packages/io.heckel.ntfy/) from F-Droid. +1. **Install Termux**: Download and install [Termux](https://f-droid.org/en/packages/com.termux/) from F-Droid. +2. **Install Termux Boot**: Download and install [Termux Boot](https://f-droid.org/en/packages/com.termux.boot/) from F-Droid. +3. **Install Termux API**: Download and install [Termux API](https://f-droid.org/en/packages/com.termux.api/) from F-Droid. +4. *(Optional)* **Install ntfy**: Download and install [ntfy](https://f-droid.org/en/packages/io.heckel.ntfy/) from F-Droid. -### Step 2: Request storage access for Termux API +### Request Storage Access for Termux -Open Termux and run this command, allowing Termux API storage access: -``` +Open Termux and run the following command to grant storage access: + +```bash termux-setup-storage ``` +## Installation + +### Step 1: Install Python + +```bash +pkg install python -y +``` + +### Step 2: Install Fetchtastic + +```bash +pip install fetchtastic +``` + +## Usage + +### Run the Setup Process + +Run the setup command and follow the proompts to configure Fetchtastic: + +```bash +fetchtastic setup +``` + +During setup, you will be able to: + +- Choose whether to download APKs, firmware, or both. +- Select specific assets to download. +- Set the number of versions to keep. +- Configure automatic extraction of firmware files. (Optional) +- Set up notifications via NTFY. (Optional) +- Add a cron job to run Fetchtastic regularly. (Optional) + +### Perform Downloads -### Step 3: Install Git and Clone the Repository +To manually start the download process, run: -Next run these commands to install git and clone the repository: +```bash +fetchtastic download ``` -pkg install git -y -git clone https://github.com/jeremiah-k/fetchtastic.git -cd fetchtastic + +This will download the latest versions of the selected assets and store them in the specified directories. + +### Help and Reconfiguration + +To view help and usage instructions, run: + +```bash +fetchtastic --help ``` -### Step 3: Run the Setup Script +If you need to reconfigure Fetchtastic, run: -Run setup.sh and follow the prompts to complete the setup. +```bash +fetchtastic setup ``` -sh setup.sh + +### Files and Directories + +By default, Fetchtastic saves files and configuration in the `Downloads/Fetchtastic` directory: + + - **Configuration File**: `Downloads/Fetchtastic/fetchtastic.yaml` + - **Log File**: `Downloads/Fetchtastic/fetchtastic.log` + - **APKs**: `Downloads/Fetchtastic/apks` + - **Firmware**: `Downloads/Fetchtastic/firmware` + +You can manually edit the configuration file to change the settings. + + +### Scheduling with Cron + +During setup, you have the option to add a cron job that runs Fetchtastic daily at 3 AM. + +To modify cron job, you can run: +```bash +crontab -e ``` + +### Notifications via NTFY + +If you choose to set up notifications, Fetchtastic will send updates to your specified NTFY topic. + +### Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests. \ No newline at end of file diff --git a/app/cli.py b/app/cli.py index cc1f734..d4fbae3 100644 --- a/app/cli.py +++ b/app/cli.py @@ -19,13 +19,17 @@ def main(): if args.command == 'setup': # Run the setup process setup_config.run_setup() - elif args.command == 'download' or args.command is None: + elif args.command == 'download': # Check if configuration exists if not setup_config.config_exists(): print("No configuration found. Running setup.") setup_config.run_setup() # Run the downloader downloader.main() + elif args.command is None: + # No command provided + print("No command provided.") + print("For help and available commands, run 'fetchtastic --help'.") else: parser.print_help() diff --git a/app/setup_config.py b/app/setup_config.py index 8637f64..e61aeba 100644 --- a/app/setup_config.py +++ b/app/setup_config.py @@ -2,6 +2,7 @@ import os import yaml +import subprocess from . import menu_apk from . import menu_firmware from . import downloader # Import downloader to perform first run @@ -109,6 +110,79 @@ def run_setup(): print("Performing first run, this may take a few minutes...") downloader.main() + # Ask if the user wants to set up a cron job + setup_cron = input("Do you want to add a cron job to run Fetchtastic daily at 3 AM? [y/n] (default: y): ").strip().lower() or 'y' + if setup_cron == 'y': + # Install crond if not already installed + install_crond() + # Call function to set up cron job + setup_cron_job() + else: + print("Skipping cron job setup.") + +def install_crond(): + try: + # Check if crond is installed + result = subprocess.run(['which', 'crond'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if result.returncode != 0: + print("Installing crond...") + subprocess.run(['pkg', 'install', 'termux-services', '-y'], check=True) + subprocess.run(['sv-enable', 'crond'], check=True) + print("crond installed and started.") + else: + print("crond is already installed.") + except Exception as e: + print(f"An error occurred while installing crond: {e}") + +def setup_cron_job(): + try: + # Get current crontab entries + result = subprocess.run(['crontab', '-l'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if result.returncode != 0: + existing_cron = '' + else: + existing_cron = result.stdout + + # Check for existing cron jobs related to fetchtastic + if 'fetchtastic download' in existing_cron: + print("An existing cron job for Fetchtastic was found:") + print(existing_cron) + keep_cron = input("Do you want to keep the existing crontab entry? [y/n] (default: y): ").strip().lower() or 'y' + if keep_cron == 'n': + # Remove existing fetchtastic cron jobs + new_cron = '\n'.join([line for line in existing_cron.split('\n') if 'fetchtastic download' not in line]) + # Update crontab + process = subprocess.Popen(['crontab', '-'], stdin=subprocess.PIPE, text=True) + process.communicate(input=new_cron) + print("Existing Fetchtastic cron job removed.") + # Ask if they want to add a new cron job + add_cron = input("Do you want to add a new crontab entry to run Fetchtastic daily at 3 AM? [y/n] (default: y): ").strip().lower() or 'y' + if add_cron == 'y': + # Add new cron job + new_cron += f"\n0 3 * * * fetchtastic download\n" + # Update crontab + process = subprocess.Popen(['crontab', '-'], stdin=subprocess.PIPE, text=True) + process.communicate(input=new_cron) + print("New cron job added.") + else: + print("Skipping cron job installation.") + else: + print("Keeping existing crontab entry.") + else: + # No existing fetchtastic cron job + add_cron = input("Do you want to add a crontab entry to run Fetchtastic daily at 3 AM? [y/n] (default: y): ").strip().lower() or 'y' + if add_cron == 'y': + # Add new cron job + new_cron = existing_cron.strip() + f"\n0 3 * * * fetchtastic download\n" + # Update crontab + process = subprocess.Popen(['crontab', '-'], stdin=subprocess.PIPE, text=True) + process.communicate(input=new_cron) + print("Cron job added to run Fetchtastic daily at 3 AM.") + else: + print("Skipping cron job installation.") + except Exception as e: + print(f"An error occurred while setting up the cron job: {e}") + def load_config(): if not config_exists(): return None diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b61373e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5d0978d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +requests +pick +PyYAML +urllib3 From 640378b40014bcfec97776053a91a8df7dbfe174 Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Sat, 12 Oct 2024 13:34:33 -0500 Subject: [PATCH 08/14] Add topic command, add example extraction pattern --- app/cli.py | 10 +++++++ app/setup_config.py | 69 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/app/cli.py b/app/cli.py index d4fbae3..e09f069 100644 --- a/app/cli.py +++ b/app/cli.py @@ -14,6 +14,9 @@ def main(): # Command to download firmware and APKs parser_download = subparsers.add_parser('download', help='Download firmware and APKs') + # Command to display NTFY topic + parser_topic = subparsers.add_parser('topic', help='Display the current NTFY topic') + args = parser.parse_args() if args.command == 'setup': @@ -26,6 +29,13 @@ def main(): setup_config.run_setup() # Run the downloader downloader.main() + elif args.command == 'topic': + # Display the NTFY topic + config = setup_config.load_config() + if config and config.get('NTFY_SERVER'): + print(f"Current NTFY topic URL: {config['NTFY_SERVER']}") + else: + print("Notifications are not set up. Run 'fetchtastic setup' to configure notifications.") elif args.command is None: # No command provided print("No command provided.") diff --git a/app/setup_config.py b/app/setup_config.py index e61aeba..6487993 100644 --- a/app/setup_config.py +++ b/app/setup_config.py @@ -3,6 +3,8 @@ import os import yaml import subprocess +import random +import string from . import menu_apk from . import menu_firmware from . import downloader # Import downloader to perform first run @@ -26,6 +28,13 @@ def config_exists(): return os.path.exists(CONFIG_FILE) def run_setup(): + # Check if running in Termux + if not is_termux(): + print("Warning: Fetchtastic is designed to run in Termux on Android.") + print("Please install Termux and try again.") + print("For more information, visit https://github.com/jeremiah-k/fetchtastic/") + return + print("Running Fetchtastic Setup...") if not os.path.exists(DEFAULT_CONFIG_DIR): os.makedirs(DEFAULT_CONFIG_DIR) @@ -73,7 +82,9 @@ def run_setup(): # Prompt for automatic extraction auto_extract = input("Do you want to automatically extract specific files from firmware zips? [y/n] (default: n): ").strip().lower() or 'n' if auto_extract == 'y': - extract_patterns = input("Enter the strings to match for extraction from the firmware .zip files, separated by spaces: ").strip() + print("Enter the strings to match for extraction from the firmware .zip files, separated by spaces.") + print("Example: rak4631- tbeam- tbeamonetank-") + extract_patterns = input("Extraction patterns: ").strip() if extract_patterns: config['AUTO_EXTRACT'] = True config['EXTRACT_PATTERNS'] = extract_patterns.split() @@ -86,29 +97,50 @@ def run_setup(): download_dir = DEFAULT_CONFIG_DIR config['DOWNLOAD_DIR'] = download_dir + # Save configuration to YAML file before setting up notifications + with open(CONFIG_FILE, 'w') as f: + yaml.dump(config, f) + + # Ask if the user wants to perform a first run + perform_first_run = input("Do you want to perform a first run now? [y/n] (default: y): ").strip().lower() or 'y' + if perform_first_run == 'y': + print("Performing first run, this may take a few minutes...") + downloader.main() + # Prompt for NTFY server configuration notifications = input("Do you want to set up notifications via NTFY? [y/n] (default: y): ").strip().lower() or 'y' if notifications == 'y': ntfy_server = input("Enter the NTFY server (default: ntfy.sh): ").strip() or 'ntfy.sh' if not ntfy_server.startswith('http://') and not ntfy_server.startswith('https://'): ntfy_server = 'https://' + ntfy_server - topic_name = input("Enter a unique topic name (default: fetchtastic): ").strip() or 'fetchtastic' + + # Generate a random topic name if the user doesn't provide one + default_topic = 'fetchtastic-' + ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) + topic_name = input(f"Enter a unique topic name (default: {default_topic}): ").strip() or default_topic ntfy_topic = f"{ntfy_server}/{topic_name}" config['NTFY_SERVER'] = ntfy_topic - else: - config['NTFY_SERVER'] = '' - # Save configuration to YAML file - with open(CONFIG_FILE, 'w') as f: - yaml.dump(config, f) + # Save updated configuration + with open(CONFIG_FILE, 'w') as f: + yaml.dump(config, f) - print(f"Setup complete. Configuration saved at {CONFIG_FILE}") + print(f"Notifications have been set up using the topic: {ntfy_topic}") + # Ask if the user wants to copy the topic name to the clipboard + copy_to_clipboard = input("Do you want to copy the topic URL to the clipboard? [y/n] (default: y): ").strip().lower() or 'y' + if copy_to_clipboard == 'y': + copy_to_clipboard_termux(ntfy_topic) + print("Topic URL copied to clipboard.") + else: + print("You can copy the topic URL from above.") - # Ask if the user wants to perform a first run - perform_first_run = input("Do you want to perform a first run now? [y/n] (default: y): ").strip().lower() or 'y' - if perform_first_run == 'y': - print("Performing first run, this may take a few minutes...") - downloader.main() + print("You can view your current topic at any time by running 'fetchtastic topic'.") + print("You can change the topic by running 'fetchtastic setup' again or editing the YAML file.") + else: + config['NTFY_SERVER'] = '' + # Save updated configuration + with open(CONFIG_FILE, 'w') as f: + yaml.dump(config, f) + print("Notifications have not been set up.") # Ask if the user wants to set up a cron job setup_cron = input("Do you want to add a cron job to run Fetchtastic daily at 3 AM? [y/n] (default: y): ").strip().lower() or 'y' @@ -120,10 +152,19 @@ def run_setup(): else: print("Skipping cron job setup.") +def is_termux(): + return 'com.termux' in os.environ.get('PREFIX', '') + +def copy_to_clipboard_termux(text): + try: + subprocess.run(['termux-clipboard-set'], input=text.encode('utf-8'), check=True) + except Exception as e: + print(f"An error occurred while copying to clipboard: {e}") + def install_crond(): try: # Check if crond is installed - result = subprocess.run(['which', 'crond'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + result = subprocess.run(['command', '-v', 'crond'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) if result.returncode != 0: print("Installing crond...") subprocess.run(['pkg', 'install', 'termux-services', '-y'], check=True) From fdad69c07c050b31d420ad8af5e11ae7df2e65fd Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Sat, 12 Oct 2024 13:34:34 -0500 Subject: [PATCH 09/14] Minor changes --- app/setup_config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/setup_config.py b/app/setup_config.py index 6487993..b5e3adb 100644 --- a/app/setup_config.py +++ b/app/setup_config.py @@ -31,7 +31,6 @@ def run_setup(): # Check if running in Termux if not is_termux(): print("Warning: Fetchtastic is designed to run in Termux on Android.") - print("Please install Termux and try again.") print("For more information, visit https://github.com/jeremiah-k/fetchtastic/") return From 490d56d505e692cb2b1521fc71c711de16e2c0a9 Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Sat, 12 Oct 2024 14:27:35 -0500 Subject: [PATCH 10/14] Add clean command, more error handling --- app/cli.py | 6 ++++++ app/setup_config.py | 49 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/app/cli.py b/app/cli.py index e09f069..7c758f3 100644 --- a/app/cli.py +++ b/app/cli.py @@ -17,6 +17,9 @@ def main(): # Command to display NTFY topic parser_topic = subparsers.add_parser('topic', help='Display the current NTFY topic') + # Command to clean/remove Fetchtastic files and settings + parser_clean = subparsers.add_parser('clean', help='Remove Fetchtastic configuration, downloads, and cron jobs') + args = parser.parse_args() if args.command == 'setup': @@ -36,6 +39,9 @@ def main(): print(f"Current NTFY topic URL: {config['NTFY_SERVER']}") else: print("Notifications are not set up. Run 'fetchtastic setup' to configure notifications.") + elif args.command == 'clean': + # Run the clean process + setup_config.run_clean() elif args.command is None: # No command provided print("No command provided.") diff --git a/app/setup_config.py b/app/setup_config.py index b5e3adb..774cb97 100644 --- a/app/setup_config.py +++ b/app/setup_config.py @@ -55,9 +55,11 @@ def run_setup(): config['SAVE_FIRMWARE'] = save_firmware # Run the menu scripts based on user choices + # Adjust SAVE_APKS and SAVE_FIRMWARE based on selections if save_apks: apk_selection = menu_apk.run_menu() if not apk_selection: + print("No APK assets selected. APKs will not be downloaded.") save_apks = False config['SAVE_APKS'] = False else: @@ -65,11 +67,18 @@ def run_setup(): if save_firmware: firmware_selection = menu_firmware.run_menu() if not firmware_selection: + print("No firmware assets selected. Firmware will not be downloaded.") save_firmware = False config['SAVE_FIRMWARE'] = False else: config['SELECTED_FIRMWARE_ASSETS'] = firmware_selection['selected_assets'] + # If both save_apks and save_firmware are False, inform the user and restart setup + if not save_apks and not save_firmware: + print("You must select at least one asset to download (APK or firmware).") + print("Please run 'fetchtastic setup' again and select at least one asset.") + return + # Prompt for number of versions to keep if save_apks: android_versions_to_keep = input("Enter the number of different versions of the Android app to keep (default: 2): ").strip() or '2' @@ -124,7 +133,7 @@ def run_setup(): yaml.dump(config, f) print(f"Notifications have been set up using the topic: {ntfy_topic}") - # Ask if the user wants to copy the topic name to the clipboard + # Ask if the user wants to copy the topic URL to the clipboard copy_to_clipboard = input("Do you want to copy the topic URL to the clipboard? [y/n] (default: y): ").strip().lower() or 'y' if copy_to_clipboard == 'y': copy_to_clipboard_termux(ntfy_topic) @@ -164,7 +173,7 @@ def install_crond(): try: # Check if crond is installed result = subprocess.run(['command', '-v', 'crond'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - if result.returncode != 0: + if result.returncode != 0 or not result.stdout.strip(): print("Installing crond...") subprocess.run(['pkg', 'install', 'termux-services', '-y'], check=True) subprocess.run(['sv-enable', 'crond'], check=True) @@ -223,6 +232,42 @@ def setup_cron_job(): except Exception as e: print(f"An error occurred while setting up the cron job: {e}") +def run_clean(): + print("This will remove Fetchtastic configuration files, downloaded files, and cron job entries.") + confirm = input("Are you sure you want to proceed? [y/n] (default: n): ").strip().lower() or 'n' + if confirm != 'y': + print("Clean operation cancelled.") + return + + # Remove configuration file + if os.path.exists(CONFIG_FILE): + os.remove(CONFIG_FILE) + print(f"Removed configuration file: {CONFIG_FILE}") + + # Remove download directory + if os.path.exists(DEFAULT_CONFIG_DIR): + import shutil + shutil.rmtree(DEFAULT_CONFIG_DIR) + print(f"Removed download directory: {DEFAULT_CONFIG_DIR}") + + # Remove cron job entries + try: + # Get current crontab entries + result = subprocess.run(['crontab', '-l'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if result.returncode == 0: + existing_cron = result.stdout + # Remove existing fetchtastic cron jobs + new_cron = '\n'.join([line for line in existing_cron.split('\n') if 'fetchtastic download' not in line]) + # Update crontab + process = subprocess.Popen(['crontab', '-'], stdin=subprocess.PIPE, text=True) + process.communicate(input=new_cron) + print("Removed Fetchtastic cron job entries.") + except Exception as e: + print(f"An error occurred while removing cron jobs: {e}") + + print("Fetchtastic has been cleaned from your system.") + print("If you installed Fetchtastic via pip and wish to uninstall it, run 'pip uninstall fetchtastic'.") + def load_config(): if not config_exists(): return None From 7db1e63e27b3325068a425f7c05a1eb3390d9c96 Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Sat, 12 Oct 2024 16:46:30 -0500 Subject: [PATCH 11/14] Update example extraction patterns --- app/setup_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/setup_config.py b/app/setup_config.py index 774cb97..ccd3689 100644 --- a/app/setup_config.py +++ b/app/setup_config.py @@ -91,7 +91,7 @@ def run_setup(): auto_extract = input("Do you want to automatically extract specific files from firmware zips? [y/n] (default: n): ").strip().lower() or 'n' if auto_extract == 'y': print("Enter the strings to match for extraction from the firmware .zip files, separated by spaces.") - print("Example: rak4631- tbeam- tbeamonetank-") + print("Example: rak4631- tbeam-2 t1000-e- tlora-v2-1-1_6-") extract_patterns = input("Extraction patterns: ").strip() if extract_patterns: config['AUTO_EXTRACT'] = True From 737c73097ca1a01cf59d0c0c5a17d0ae4e92d763 Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Sat, 12 Oct 2024 11:25:54 -0500 Subject: [PATCH 12/14] Create python-publish.yml --- .github/workflows/python-publish.yml | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..86ff3d9 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,31 @@ +name: Upload Python Package to PyPI + +on: + release: + types: [created] + +jobs: + build-and-publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build the package + run: python -m build + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: python -m twine upload dist/* From 378a093ee1abb9a09418d4c9cc52b39e108b74c8 Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Sat, 12 Oct 2024 11:40:36 -0500 Subject: [PATCH 13/14] Rename python-publish.yml to pypi-publish.yml --- .github/workflows/python-publish.yml | 31 ---------------------------- 1 file changed, 31 deletions(-) delete mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index 86ff3d9..0000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Upload Python Package to PyPI - -on: - release: - types: [created] - -jobs: - build-and-publish: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: '3.x' - - - name: Install build dependencies - run: | - python -m pip install --upgrade pip - pip install build twine - - - name: Build the package - run: python -m build - - - name: Publish to PyPI - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: python -m twine upload dist/* From 5fc202cbb8f722479aadee20d3ffcf98f0f57b10 Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Sat, 12 Oct 2024 17:14:26 -0500 Subject: [PATCH 14/14] Fix questions ordering --- app/setup_config.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/app/setup_config.py b/app/setup_config.py index ccd3689..b73f80d 100644 --- a/app/setup_config.py +++ b/app/setup_config.py @@ -105,10 +105,20 @@ def run_setup(): download_dir = DEFAULT_CONFIG_DIR config['DOWNLOAD_DIR'] = download_dir - # Save configuration to YAML file before setting up notifications + # Save configuration to YAML file before proceeding with open(CONFIG_FILE, 'w') as f: yaml.dump(config, f) + # Ask if the user wants to set up a cron job + setup_cron = input("Do you want to add a cron job to run Fetchtastic daily at 3 AM? [y/n] (default: y): ").strip().lower() or 'y' + if setup_cron == 'y': + # Install crond if not already installed + install_crond() + # Call function to set up cron job + setup_cron_job() + else: + print("Skipping cron job setup.") + # Ask if the user wants to perform a first run perform_first_run = input("Do you want to perform a first run now? [y/n] (default: y): ").strip().lower() or 'y' if perform_first_run == 'y': @@ -132,14 +142,14 @@ def run_setup(): with open(CONFIG_FILE, 'w') as f: yaml.dump(config, f) - print(f"Notifications have been set up using the topic: {ntfy_topic}") - # Ask if the user wants to copy the topic URL to the clipboard - copy_to_clipboard = input("Do you want to copy the topic URL to the clipboard? [y/n] (default: y): ").strip().lower() or 'y' + print(f"Notifications have been set up using the topic: {topic_name}") + # Ask if the user wants to copy the topic name to the clipboard + copy_to_clipboard = input("Do you want to copy the topic name to the clipboard? [y/n] (default: y): ").strip().lower() or 'y' if copy_to_clipboard == 'y': - copy_to_clipboard_termux(ntfy_topic) - print("Topic URL copied to clipboard.") + copy_to_clipboard_termux(topic_name) + print("Topic name copied to clipboard.") else: - print("You can copy the topic URL from above.") + print("You can copy the topic name from above.") print("You can view your current topic at any time by running 'fetchtastic topic'.") print("You can change the topic by running 'fetchtastic setup' again or editing the YAML file.") @@ -150,16 +160,6 @@ def run_setup(): yaml.dump(config, f) print("Notifications have not been set up.") - # Ask if the user wants to set up a cron job - setup_cron = input("Do you want to add a cron job to run Fetchtastic daily at 3 AM? [y/n] (default: y): ").strip().lower() or 'y' - if setup_cron == 'y': - # Install crond if not already installed - install_crond() - # Call function to set up cron job - setup_cron_job() - else: - print("Skipping cron job setup.") - def is_termux(): return 'com.termux' in os.environ.get('PREFIX', '')