diff --git a/app/cli.py b/app/cli.py index 36acc44..0638e0d 100644 --- a/app/cli.py +++ b/app/cli.py @@ -1,9 +1,11 @@ # app/cli.py import argparse +import subprocess +import os +import shutil from . import downloader from . import setup_config -import subprocess def main(): parser = argparse.ArgumentParser(description="Fetchtastic - Meshtastic Firmware and APK Downloader") @@ -31,8 +33,9 @@ def main(): if not setup_config.config_exists(): print("No configuration found. Running setup.") setup_config.run_setup() - # Run the downloader - downloader.main() + else: + # Run the downloader + downloader.main() elif args.command == 'topic': # Display the NTFY topic and prompt to copy to clipboard config = setup_config.load_config() @@ -52,7 +55,7 @@ def main(): print("Notifications are not set up. Run 'fetchtastic setup' to configure notifications.") elif args.command == 'clean': # Run the clean process - setup_config.run_clean() + run_clean() elif args.command is None: # No command provided print("No command provided.") @@ -66,5 +69,48 @@ def copy_to_clipboard_termux(text): except Exception as e: print(f"An error occurred while copying to clipboard: {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: no): ").strip().lower() or 'n' + if confirm != 'y': + print("Clean operation cancelled.") + return + + # Remove configuration file + config_file = setup_config.CONFIG_FILE + if os.path.exists(config_file): + os.remove(config_file) + print(f"Removed configuration file: {config_file}") + + # Remove download directory + download_dir = setup_config.DEFAULT_CONFIG_DIR + if os.path.exists(download_dir): + shutil.rmtree(download_dir) + print(f"Removed download directory: {download_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}") + + # Remove boot script if exists + boot_script = os.path.expanduser("~/.termux/boot/fetchtastic.sh") + if os.path.exists(boot_script): + os.remove(boot_script) + print(f"Removed boot script: {boot_script}") + + print("Fetchtastic has been cleaned from your system.") + print("If you installed Fetchtastic via pip and wish to uninstall it, run 'pip uninstall fetchtastic'.") + if __name__ == "__main__": main() diff --git a/app/downloader.py b/app/downloader.py index 99cdf8c..d5683d6 100644 --- a/app/downloader.py +++ b/app/downloader.py @@ -4,6 +4,7 @@ import requests import zipfile import time +import json from datetime import datetime from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry @@ -50,14 +51,20 @@ def log_message(message): log.write(f"{datetime.now()}: {message}\n") print(message) - def send_ntfy_notification(message): + def send_ntfy_notification(message, title=None): if ntfy_server and ntfy_topic: try: ntfy_url = f"{ntfy_server.rstrip('/')}/{ntfy_topic}" - response = requests.post(ntfy_url, data=message.encode('utf-8')) + headers = { + 'Content-Type': 'text/plain; charset=utf-8', + } + if title: + headers['Title'] = title + response = requests.post(ntfy_url, data=message.encode('utf-8'), headers=headers) response.raise_for_status() + log_message(f"Notification sent to {ntfy_url}") except requests.exceptions.RequestException as e: - log_message(f"Error sending notification: {e}") + log_message(f"Error sending notification to {ntfy_url}: {e}") else: log_message("Notifications are not configured.") @@ -96,7 +103,13 @@ def download_file(url, download_path): def is_connected_to_wifi(): try: result = os.popen('termux-wifi-connectioninfo').read() - if '"connected":true' in result: + if not result: + # If result is empty, assume not connected + return False + data = json.loads(result) + supplicant_state = data.get('supplicant_state', '') + ip_address = data.get('ip', '') + if supplicant_state == 'COMPLETED' and ip_address != '': return True else: return False @@ -104,7 +117,7 @@ def is_connected_to_wifi(): log_message(f"Error checking Wi-Fi connection: {e}") return False - # Updated extract_files function + # Function to extract files from zip archives def extract_files(zip_path, extract_dir, patterns): try: with zipfile.ZipFile(zip_path, 'r') as zip_ref: @@ -112,7 +125,6 @@ def extract_files(zip_path, extract_dir, patterns): for file_info in zip_ref.infolist(): file_name = file_info.filename base_name = os.path.basename(file_name) - log_message(f"Checking file: {base_name}") for pattern in patterns: if pattern in base_name: # Extract and flatten directory structure @@ -148,6 +160,7 @@ def cleanup_old_versions(directory, releases_to_keep): # 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_patterns=None): downloaded_versions = [] + new_versions_available = [] if not os.path.exists(download_dir): os.makedirs(download_dir) @@ -161,6 +174,14 @@ def check_and_download(releases, latest_release_file, release_type, download_dir # Determine which releases to download releases_to_download = releases[:versions_to_keep] + if downloads_skipped: + # Collect new versions available + for release in releases_to_download: + release_tag = release['tag_name'] + if release_tag != saved_release_tag: + new_versions_available.append(release_tag) + return downloaded_versions, new_versions_available + for release in releases_to_download: release_tag = release['tag_name'] release_dir = os.path.join(download_dir, release_tag) @@ -170,22 +191,12 @@ def check_and_download(releases, latest_release_file, release_type, download_dir else: # Proceed to download this version os.makedirs(release_dir, exist_ok=True) - log_message(f"New {release_type} version {release_tag} is available.") - # Check Wi-Fi connection before downloading - if wifi_only and not is_connected_to_wifi(): - log_message("Not connected to Wi-Fi. Skipping download.") - send_ntfy_notification(f"New {release_type} version {release_tag} is available, but not connected to Wi-Fi. Download will proceed when connected.") - continue # Skip downloading this release + log_message(f"Downloading new {release_type} version: {release_tag}") for asset in release['assets']: file_name = asset['name'] - # Modify the matching logic here + # Matching logic if selected_patterns: - matched = False - for pattern in selected_patterns: - if pattern in file_name: - matched = True - break - if not matched: + if not any(pattern in file_name for pattern in selected_patterns): continue # Skip this asset download_path = os.path.join(release_dir, file_name) download_file(asset['browser_download_url'], download_path) @@ -193,23 +204,40 @@ def check_and_download(releases, latest_release_file, release_type, download_dir extract_files(download_path, release_dir, extract_patterns) downloaded_versions.append(release_tag) - # Update latest_release_file with the most recent tag - if releases_to_download: + # Only update latest_release_file if downloads occurred + if downloaded_versions: with open(latest_release_file, 'w') as f: - f.write(releases_to_download[0]['tag_name']) + f.write(downloaded_versions[0]) # Create a list of all release tags to keep release_tags_to_keep = [release['tag_name'] for release in releases_to_download] # Clean up old versions cleanup_old_versions(download_dir, release_tags_to_keep) - return downloaded_versions + + # Collect new versions available + for release in releases_to_download: + release_tag = release['tag_name'] + if release_tag != saved_release_tag and release_tag not in downloaded_versions: + new_versions_available.append(release_tag) + + return downloaded_versions, new_versions_available start_time = time.time() log_message("Starting Fetchtastic...") + # Check Wi-Fi connection before starting downloads + wifi_connected = is_connected_to_wifi() + downloads_skipped = False + + if wifi_only and not wifi_connected: + downloads_skipped = True + + # Initialize variables downloaded_firmwares = [] downloaded_apks = [] + new_firmware_versions = [] + new_apk_versions = [] # URLs for releases android_releases_url = "https://api.github.com/repos/meshtastic/Meshtastic-Android/releases" @@ -224,7 +252,7 @@ def check_and_download(releases, latest_release_file, release_type, download_dir if save_firmware and selected_firmware_patterns: versions_to_download = firmware_versions_to_keep latest_firmware_releases = get_latest_releases(firmware_releases_url, releases_to_scan) - downloaded_firmwares = check_and_download( + fw_downloaded, fw_new_versions = check_and_download( latest_firmware_releases, latest_firmware_release_file, "Firmware", @@ -233,6 +261,8 @@ def check_and_download(releases, latest_release_file, release_type, download_dir extract_patterns, selected_patterns=selected_firmware_patterns ) + downloaded_firmwares.extend(fw_downloaded) + new_firmware_versions.extend(fw_new_versions) log_message(f"Latest Firmware releases: {', '.join(release['tag_name'] for release in latest_firmware_releases[:versions_to_download])}") elif not selected_firmware_patterns: log_message("No firmware assets selected. Skipping firmware download.") @@ -240,7 +270,7 @@ def check_and_download(releases, latest_release_file, release_type, download_dir if save_apks and selected_apk_patterns: versions_to_download = android_versions_to_keep latest_android_releases = get_latest_releases(android_releases_url, releases_to_scan) - downloaded_apks = check_and_download( + apk_downloaded, apk_new_versions = check_and_download( latest_android_releases, latest_android_release_file, "Android APK", @@ -249,6 +279,8 @@ def check_and_download(releases, latest_release_file, release_type, download_dir extract_patterns, selected_patterns=selected_apk_patterns ) + downloaded_apks.extend(apk_downloaded) + new_apk_versions.extend(apk_new_versions) log_message(f"Latest Android APK releases: {', '.join(release['tag_name'] for release in latest_android_releases[:versions_to_download])}") elif not selected_apk_patterns: log_message("No APK assets selected. Skipping APK download.") @@ -257,26 +289,36 @@ def check_and_download(releases, latest_release_file, release_type, download_dir total_time = end_time - start_time log_message(f"Finished the Meshtastic downloader. Total time taken: {total_time:.2f} seconds") - # Send notification if there are new downloads - if downloaded_firmwares or downloaded_apks: - message = "" + if downloads_skipped: + log_message("Not connected to Wi-Fi. Skipping all downloads.") + # Prepare notification message + message_lines = ["New releases are available but downloads were skipped because the device is not connected to Wi-Fi."] + if new_firmware_versions: + message_lines.append(f"Firmware versions available: {', '.join(new_firmware_versions)}") + if new_apk_versions: + message_lines.append(f"Android APK versions available: {', '.join(new_apk_versions)}") + notification_message = '\n'.join(message_lines) + f"\n{datetime.now()}" + log_message('\n'.join(message_lines)) + send_ntfy_notification(notification_message, title="Fetchtastic Downloads Skipped") + elif downloaded_firmwares or downloaded_apks: + # Prepare notification messages + notification_messages = [] if downloaded_firmwares: - message += f"New Firmware releases {', '.join(downloaded_firmwares)} downloaded.\n" + message = f"Downloaded Firmware versions: {', '.join(downloaded_firmwares)}" + notification_messages.append(message) if downloaded_apks: - message += f"New Android APK releases {', '.join(downloaded_apks)} downloaded.\n" - message += f"{datetime.now()}" - send_ntfy_notification(message) + message = f"Downloaded Android APK versions: {', '.join(downloaded_apks)}" + notification_messages.append(message) + notification_message = '\n'.join(notification_messages) + f"\n{datetime.now()}" + send_ntfy_notification(notification_message, title="Fetchtastic Download Completed") else: - if latest_firmware_releases or latest_android_releases: - message = ( - f"No new downloads. All Firmware and Android APK versions are up to date or waiting for Wi-Fi connection.\n" - f"Latest Firmware releases: {', '.join(release['tag_name'] for release in latest_firmware_releases[:versions_to_download])}\n" - f"Latest Android APK releases: {', '.join(release['tag_name'] for release in latest_android_releases[:versions_to_download])}\n" - f"{datetime.now()}" - ) - send_ntfy_notification(message) - else: - log_message("No releases found to check for updates.") + # No new downloads; everything is up to date + message = ( + f"No new downloads. All Firmware and Android APK versions are up to date.\n" + f"{datetime.now()}" + ) + log_message(message) + send_ntfy_notification(message, title="Fetchtastic Up to Date") if __name__ == "__main__": main() diff --git a/app/setup_config.py b/app/setup_config.py index 7cc23f7..73fa716 100644 --- a/app/setup_config.py +++ b/app/setup_config.py @@ -1,6 +1,7 @@ # app/setup_config.py import os +import sys import yaml import subprocess import random @@ -37,27 +38,50 @@ def config_exists(): def is_termux(): return 'com.termux' in os.environ.get('PREFIX', '') +def check_storage_setup(): + # Check if the Termux storage directory and Downloads are set up and writable + storage_dir = os.path.expanduser("~/storage") + storage_downloads = os.path.expanduser("~/storage/downloads") + + if os.path.exists(storage_dir) and os.path.exists(storage_downloads) and os.access(storage_downloads, os.W_OK): + print("Termux storage access is already set up.") + return True + else: + print("Termux storage access is not set up.") + return False + def run_setup(): print("Running Fetchtastic Setup...") # Install required Termux packages first if is_termux(): install_termux_packages() - # Run termux-setup-storage - setup_storage() - # After setting up storage, inform the user and exit - print("Termux storage has been set up, and required packages have been installed.") - print("Please restart Termux and run 'fetchtastic setup' again to continue.") - exit() - - # The rest of your setup process continues here + # Check if storage is set up + if not check_storage_setup(): + # Run termux-setup-storage + setup_storage() + # After setting up storage, inform the user and exit + print("Termux storage has been set up, and required packages have been installed.") + print("Please restart Termux and run 'fetchtastic setup' again to continue.") + sys.exit() + else: + print("Termux storage is already set up.") + + # Proceed with the rest of the setup if not os.path.exists(DEFAULT_CONFIG_DIR): os.makedirs(DEFAULT_CONFIG_DIR) config = {} + if config_exists(): + # Load existing configuration + config = load_config() + print("Existing configuration found. You can keep current settings or change them.") + else: + # Initialize default configuration + config = {} # Prompt to save APKs, firmware, or both - save_choice = input("Would you like to download APKs, firmware, or both? [a/f/b] (default: both): ").strip().lower() or 'b' + save_choice = input(f"Would you like to download APKs, firmware, or both? [a/f/b] (default: both): ").strip().lower() or 'both' if save_choice == 'a': save_apks = True save_firmware = False @@ -96,28 +120,45 @@ def run_setup(): # Prompt for number of versions to keep if save_apks: - android_versions_to_keep = input("How many versions of the Android app would you like to keep? (default is 2): ").strip() or '2' + current_versions = config.get('ANDROID_VERSIONS_TO_KEEP', 2) + android_versions_to_keep = input(f"How many versions of the Android app would you like to keep? (default is {current_versions}): ").strip() or str(current_versions) config['ANDROID_VERSIONS_TO_KEEP'] = int(android_versions_to_keep) if save_firmware: - firmware_versions_to_keep = input("How many versions of the firmware would you like to keep? (default is 2): ").strip() or '2' + current_versions = config.get('FIRMWARE_VERSIONS_TO_KEEP', 2) + firmware_versions_to_keep = input(f"How many versions of the firmware would you like to keep? (default is {current_versions}): ").strip() or str(current_versions) config['FIRMWARE_VERSIONS_TO_KEEP'] = int(firmware_versions_to_keep) # Prompt for automatic extraction - auto_extract = input("Would you like to automatically extract specific files from firmware zip archives? [y/n] (default: no): ").strip().lower() or 'n' + auto_extract_default = 'yes' if config.get('AUTO_EXTRACT', False) else 'no' + auto_extract = input(f"Would you like to automatically extract specific files from firmware zip archives? [y/n] (default: {auto_extract_default}): ").strip().lower() or auto_extract_default[0] if auto_extract == 'y': print("Enter the keywords to match for extraction from the firmware zip files, separated by spaces.") 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 - config['EXTRACT_PATTERNS'] = extract_patterns.split() + if config.get('EXTRACT_PATTERNS'): + current_patterns = ' '.join(config.get('EXTRACT_PATTERNS', [])) + print(f"Current patterns: {current_patterns}") + extract_patterns = input("Extraction patterns (leave blank to keep current): ").strip() + if extract_patterns: + config['AUTO_EXTRACT'] = True + config['EXTRACT_PATTERNS'] = extract_patterns.split() + else: + # Keep existing patterns + config['AUTO_EXTRACT'] = True else: - config['AUTO_EXTRACT'] = False + extract_patterns = input("Extraction patterns: ").strip() + if extract_patterns: + config['AUTO_EXTRACT'] = True + config['EXTRACT_PATTERNS'] = extract_patterns.split() + else: + config['AUTO_EXTRACT'] = False + print("No extraction patterns provided. Extraction will be skipped.") + print("You can run 'fetchtastic setup' again to set extraction patterns.") else: config['AUTO_EXTRACT'] = False # Ask if the user wants to only download when connected to Wi-Fi - wifi_only = input("Do you want to only download when connected to Wi-Fi? [y/n] (default: yes): ").strip().lower() or 'y' + wifi_only_default = 'yes' if config.get('WIFI_ONLY', True) else 'no' + wifi_only = input(f"Do you want to only download when connected to Wi-Fi? [y/n] (default: {wifi_only_default}): ").strip().lower() or wifi_only_default[0] config['WIFI_ONLY'] = True if wifi_only == 'y' else False # Set the download directory to the same as the config directory @@ -129,22 +170,34 @@ def run_setup(): yaml.dump(config, f) # Ask if the user wants to set up a cron job - setup_cron = input("Would you like to schedule Fetchtastic to run daily at 3 AM? [y/n] (default: yes): ").strip().lower() or 'y' + cron_default = 'yes' # Default to 'yes' + setup_cron = input(f"Would you like to schedule Fetchtastic to run daily at 3 AM? [y/n] (default: {cron_default}): ").strip().lower() or cron_default[0] if setup_cron == 'y': install_crond() setup_cron_job() else: - print("Skipping cron job setup.") + remove_cron_job() + print("Cron job has been removed.") + + # Ask if the user wants to run Fetchtastic on boot + boot_default = 'yes' # Default to 'yes' + run_on_boot = input(f"Do you want Fetchtastic to run on device boot? [y/n] (default: {boot_default}): ").strip().lower() or boot_default[0] + if run_on_boot == 'y': + setup_boot_script() + else: + remove_boot_script() + print("Boot script has been removed.") # Prompt for NTFY server configuration - notifications = input("Would you like to set up notifications via NTFY? [y/n] (default: yes): ").strip().lower() or 'y' + notifications_default = 'yes' # Default to 'yes' + notifications = input(f"Would you like to set up notifications via NTFY? [y/n] (default: {notifications_default}): ").strip().lower() or 'y' if notifications == 'y': - ntfy_server = input("Enter the NTFY server (default: ntfy.sh): ").strip() or 'ntfy.sh' + ntfy_server = input(f"Enter the NTFY server (current: {config.get('NTFY_SERVER', 'ntfy.sh')}): ").strip() or config.get('NTFY_SERVER', 'ntfy.sh') if not ntfy_server.startswith('http://') and not ntfy_server.startswith('https://'): ntfy_server = 'https://' + ntfy_server - 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 + current_topic = config.get('NTFY_TOPIC', 'fetchtastic-' + ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))) + topic_name = input(f"Enter a unique topic name (current: {current_topic}): ").strip() or current_topic config['NTFY_TOPIC'] = topic_name config['NTFY_SERVER'] = ntfy_server @@ -164,14 +217,12 @@ def run_setup(): else: print("You can copy the topic name from above.") - print("Run 'fetchtastic topic' to view your current topic.") - print("Run 'fetchtastic setup' again or edit the YAML file to change the topic.") else: config['NTFY_TOPIC'] = '' config['NTFY_SERVER'] = '' with open(CONFIG_FILE, 'w') as f: yaml.dump(config, f) - print("Notifications have not been set up.") + print("Notifications have been disabled.") # Ask if the user wants to perform a first run perform_first_run = input("Would you like to start the first run now? [y/n] (default: yes): ").strip().lower() or 'y' @@ -206,7 +257,12 @@ def install_termux_packages(): def setup_storage(): # Run termux-setup-storage print("Setting up Termux storage access...") - subprocess.run(['termux-setup-storage'], check=True) + try: + subprocess.run(['termux-setup-storage'], check=True) + except subprocess.CalledProcessError as e: + print("An error occurred while setting up Termux storage.") + print("Please grant storage permissions when prompted.") + sys.exit() def install_crond(): try: @@ -233,50 +289,18 @@ def setup_cron_job(): 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: yes): ").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]) - # 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("Cron job updated.") - else: - print("Keeping existing crontab entry.") - else: - # 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.") + # Remove existing fetchtastic cron jobs + new_cron = '\n'.join([line for line in existing_cron.split('\n') if 'fetchtastic download' not in line]) + # 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("Cron job added to run Fetchtastic daily at 3 AM.") 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: no): ").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): - shutil.rmtree(DEFAULT_CONFIG_DIR) - print(f"Removed download directory: {DEFAULT_CONFIG_DIR}") - - # Remove cron job entries +def remove_cron_job(): try: # Get current crontab entries result = subprocess.run(['crontab', '-l'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) @@ -287,12 +311,38 @@ def run_clean(): # Update crontab process = subprocess.Popen(['crontab', '-'], stdin=subprocess.PIPE, text=True) process.communicate(input=new_cron) - print("Removed Fetchtastic cron job entries.") + print("Cron job removed.") except Exception as e: - print(f"An error occurred while removing cron jobs: {e}") + print(f"An error occurred while removing the cron job: {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 is_cron_job_set(): + try: + result = subprocess.run(['crontab', '-l'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if result.returncode == 0 and 'fetchtastic download' in result.stdout: + return True + else: + return False + except Exception: + return False + +def setup_boot_script(): + boot_dir = os.path.expanduser("~/.termux/boot") + boot_script = os.path.join(boot_dir, "fetchtastic.sh") + if not os.path.exists(boot_dir): + print("It seems that Termux:Boot is not installed or hasn't been run yet.") + print("Please install Termux:Boot from the Play Store or F-Droid and run it once to create the necessary directories.") + return + with open(boot_script, 'w') as f: + f.write("#!/data/data/com.termux/files/usr/bin/sh\n") + f.write("fetchtastic download\n") + os.chmod(boot_script, 0o700) + print("Boot script created to run Fetchtastic on device boot.") + +def remove_boot_script(): + boot_script = os.path.expanduser("~/.termux/boot/fetchtastic.sh") + if os.path.exists(boot_script): + os.remove(boot_script) + print("Boot script removed.") def load_config(): if not config_exists():