From 405bcd72f8ac0922c4ad3fde5c7bd5813cead06e Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:15:07 -0600 Subject: [PATCH 1/9] First platform specific changes --- app/downloader.py | 34 +++++---- app/setup_config.py | 167 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 159 insertions(+), 42 deletions(-) diff --git a/app/downloader.py b/app/downloader.py index d545e3e..ee03718 100644 --- a/app/downloader.py +++ b/app/downloader.py @@ -5,6 +5,7 @@ import time import zipfile from datetime import datetime +import platform import requests from requests.adapters import HTTPAdapter @@ -113,21 +114,26 @@ def download_file(url, download_path): log_message(f"Error downloading {url}: {e}") def is_connected_to_wifi(): - try: - result = os.popen("termux-wifi-connectioninfo").read() - 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: + if setup_config.is_termux(): + # Termux-specific Wi-Fi check + try: + result = os.popen("termux-wifi-connectioninfo").read() + 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 + except Exception as e: + log_message(f"Error checking Wi-Fi connection: {e}") return False - except Exception as e: - log_message(f"Error checking Wi-Fi connection: {e}") - return False + else: + # For non-Termux environments, assume connected + return True # Function to extract files from zip archives def extract_files(zip_path, extract_dir, patterns, exclude_patterns): diff --git a/app/setup_config.py b/app/setup_config.py index 6e4eb00..ad54b40 100644 --- a/app/setup_config.py +++ b/app/setup_config.py @@ -5,6 +5,7 @@ import shutil import string import subprocess +import platform # Added to check the environment import yaml @@ -14,7 +15,7 @@ def get_downloads_dir(): # For Termux, use ~/storage/downloads - if "com.termux" in os.environ.get("PREFIX", ""): + if is_termux(): storage_downloads = os.path.expanduser("~/storage/downloads") if os.path.exists(storage_downloads): return storage_downloads @@ -43,6 +44,19 @@ def is_termux(): return "com.termux" in os.environ.get("PREFIX", "") +def get_platform(): + if is_termux(): + return "termux" + elif platform.system() == "Windows": + return "windows" + elif platform.system() == "Darwin": + return "mac" + elif platform.system() == "Linux": + return "linux" + else: + return "unknown" + + def check_storage_setup(): # Check if the Termux storage directory and Downloads are set up and writable storage_dir = os.path.expanduser("~/storage") @@ -235,7 +249,11 @@ def run_setup(): config["EXCLUDE_PATTERNS"] = [] # Ask if the user wants to only download when connected to Wi-Fi - wifi_only_default = "yes" if config.get("WIFI_ONLY", True) else "no" + # For non-Termux environments, default to 'no' since Wi-Fi check is not implemented + if is_termux(): + wifi_only_default = "yes" if config.get("WIFI_ONLY", True) else "no" + else: + wifi_only_default = "no" wifi_only = ( input( f"Do you want to only download when connected to Wi-Fi? [y/n] (default: {wifi_only_default}): " @@ -265,7 +283,8 @@ def run_setup(): or cron_default[0] ) if setup_cron == "y": - install_crond() + if is_termux(): + install_crond() setup_cron_job() else: remove_cron_job() @@ -282,10 +301,17 @@ def run_setup(): or boot_default[0] ) if run_on_boot == "y": - setup_boot_script() + if is_termux(): + setup_boot_script() + else: + setup_reboot_cron_job() else: - remove_boot_script() - print("Boot script has been removed.") + if is_termux(): + remove_boot_script() + print("Boot script has been removed.") + else: + remove_reboot_cron_job() + print("Reboot cron job has been removed.") # Prompt for NTFY server configuration notifications_default = "yes" # Default to 'yes' @@ -336,7 +362,7 @@ def run_setup(): or "y" ) if copy_to_clipboard == "y": - copy_to_clipboard_termux(topic_name) + copy_to_clipboard_func(topic_name) print("Topic name copied to clipboard.") else: print("You can copy the topic name from above.") @@ -362,11 +388,37 @@ def run_setup(): print("Setup complete. Run 'fetchtastic download' to start downloading.") -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 copy_to_clipboard_func(text): + if is_termux(): + 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}") + else: + system = platform.system() + try: + if system == "Darwin": + # macOS + subprocess.run("pbcopy", text=True, input=text, check=True) + elif system == "Linux": + # Linux + if shutil.which("xclip"): + subprocess.run("xclip -selection clipboard", input=text.encode("utf-8"), shell=True) + elif shutil.which("xsel"): + subprocess.run("xsel --clipboard --input", input=text.encode("utf-8"), shell=True) + else: + print("xclip or xsel not found. Install xclip or xsel to use clipboard functionality.") + elif system == "Windows": + try: + import ctypes + command = f'echo {text.strip()}|clip' + os.system(command) + except Exception as e: + print("Clipboard functionality is not available. Install 'pywin32' package.") + else: + print("Clipboard functionality is not supported on this platform.") + except Exception as e: + print(f"An error occurred while copying to clipboard: {e}") def install_termux_packages(): @@ -400,20 +452,24 @@ def setup_storage(): def install_crond(): - try: - crond_path = shutil.which("crond") - if crond_path is None: - print("Installing cronie...") - # Install cronie - subprocess.run(["pkg", "install", "cronie", "-y"], check=True) - print("cronie installed.") - else: - print("cronie is already installed.") - # Enable crond service - subprocess.run(["sv-enable", "crond"], check=True) - print("crond service enabled.") - except Exception as e: - print(f"An error occurred while installing or enabling crond: {e}") + if is_termux(): + try: + crond_path = shutil.which("crond") + if crond_path is None: + print("Installing cronie...") + # Install cronie + subprocess.run(["pkg", "install", "cronie", "-y"], check=True) + print("cronie installed.") + else: + print("cronie is already installed.") + # Enable crond service + subprocess.run(["sv-enable", "crond"], check=True) + print("crond service enabled.") + except Exception as e: + print(f"An error occurred while installing or enabling crond: {e}") + else: + # For non-Termux environments, crond installation is not needed + pass def setup_cron_job(): @@ -432,7 +488,7 @@ def setup_cron_job(): [ line for line in existing_cron.split("\n") - if "fetchtastic download" not in line + if "fetchtastic download" not in line or line.strip().startswith("@reboot") ] ) # Add new cron job @@ -458,7 +514,7 @@ def remove_cron_job(): [ line for line in existing_cron.split("\n") - if "fetchtastic download" not in line + if "fetchtastic download" not in line or line.strip().startswith("@reboot") ] ) # Update crontab @@ -499,6 +555,61 @@ def remove_boot_script(): print("Boot script removed.") +def setup_reboot_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 + + # Remove existing @reboot fetchtastic cron jobs + new_cron = "\n".join( + [ + line + for line in existing_cron.split("\n") + if not (line.strip().startswith("@reboot") and "fetchtastic download" in line) + ] + ) + # Add new @reboot cron job + new_cron += "\n@reboot fetchtastic download\n" + # Update crontab + process = subprocess.Popen(["crontab", "-"], stdin=subprocess.PIPE, text=True) + process.communicate(input=new_cron) + print("Reboot cron job added to run Fetchtastic on system startup.") + except Exception as e: + print(f"An error occurred while setting up the reboot cron job: {e}") + + +def remove_reboot_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 = result.stdout + # Remove existing @reboot fetchtastic cron jobs + new_cron = "\n".join( + [ + line + for line in existing_cron.split("\n") + if not (line.strip().startswith("@reboot") and "fetchtastic download" in line) + ] + ) + # Update crontab + process = subprocess.Popen( + ["crontab", "-"], stdin=subprocess.PIPE, text=True + ) + process.communicate(input=new_cron) + print("Reboot cron job removed.") + except Exception as e: + print(f"An error occurred while removing the reboot cron job: {e}") + + def load_config(): if not config_exists(): return None From b8ced8f6484566c54a0237f7e1039b440c1c339b Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:21:13 -0600 Subject: [PATCH 2/9] Update .gitignore --- .trunk/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.trunk/.gitignore b/.trunk/.gitignore index 15966d0..46e2582 100644 --- a/.trunk/.gitignore +++ b/.trunk/.gitignore @@ -7,3 +7,4 @@ plugins user_trunk.yaml user.yaml tmp +app/__pycache__ \ No newline at end of file From d3ca87ba370bc6dc39a9f9937cc3ec2130a2bd4a Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:22:28 -0600 Subject: [PATCH 3/9] Update .gitignore --- .trunk/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.trunk/.gitignore b/.trunk/.gitignore index 46e2582..98c34a7 100644 --- a/.trunk/.gitignore +++ b/.trunk/.gitignore @@ -7,4 +7,5 @@ plugins user_trunk.yaml user.yaml tmp -app/__pycache__ \ No newline at end of file +app/__pycache__ +fetchtastic.egg-info \ No newline at end of file From df94af62857c59dec41a860e04cb8d6129a1bcc4 Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:50:33 -0600 Subject: [PATCH 4/9] Update .gitignore --- .gitignore | 2 ++ .trunk/.gitignore | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 1fc4257..7cd094d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ topic.txt fetchtastic.log .aider* +app/__pycache__/ +fetchtastic.egg-info/ \ No newline at end of file diff --git a/.trunk/.gitignore b/.trunk/.gitignore index 98c34a7..072b680 100644 --- a/.trunk/.gitignore +++ b/.trunk/.gitignore @@ -6,6 +6,4 @@ plugins user_trunk.yaml user.yaml -tmp -app/__pycache__ -fetchtastic.egg-info \ No newline at end of file +tmp \ No newline at end of file From 080d0a38dcb9aa7ddecaea9bf0007c036aafd20d Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:54:55 -0600 Subject: [PATCH 5/9] cross-platform support --- app/cli.py | 154 +++++++++++++++++---- app/menu_apk.py | 2 +- app/menu_firmware.py | 3 +- app/setup_config.py | 321 ++++++++++++++++++++++++++++--------------- setup.cfg | 5 +- 5 files changed, 351 insertions(+), 134 deletions(-) diff --git a/app/cli.py b/app/cli.py index e498b54..bb0adf3 100644 --- a/app/cli.py +++ b/app/cli.py @@ -4,6 +4,8 @@ import os import shutil import subprocess +import platform +import sys from . import downloader, setup_config @@ -28,6 +30,12 @@ def main(): "clean", help="Remove Fetchtastic configuration, downloads, and cron jobs" ) + # Command to display version + subparsers.add_parser("version", help="Display Fetchtastic version") + + # Command to display help + subparsers.add_parser("help", help="Display help information") + args = parser.parse_args() if args.command == "setup": @@ -50,19 +58,31 @@ def main(): full_url = f"{ntfy_server}/{ntfy_topic}" print(f"Current NTFY topic URL: {full_url}") print(f"Topic name: {ntfy_topic}") + + if setup_config.is_termux(): + copy_prompt_text = "Do you want to copy the topic name to the clipboard? [y/n] (default: yes): " + text_to_copy = ntfy_topic + else: + copy_prompt_text = "Do you want to copy the topic URL to the clipboard? [y/n] (default: yes): " + text_to_copy = full_url + copy_to_clipboard = ( - input( - "Do you want to copy the topic name to the clipboard? [y/n] (default: yes): " - ) + input(copy_prompt_text) .strip() .lower() or "y" ) if copy_to_clipboard == "y": - copy_to_clipboard_termux(ntfy_topic) - print("Topic name copied to clipboard.") + success = copy_to_clipboard_func(text_to_copy) + if success: + if setup_config.is_termux(): + print("Topic name copied to clipboard.") + else: + print("Topic URL copied to clipboard.") + else: + print("Failed to copy to clipboard.") else: - print("You can copy the topic name from above.") + print("You can copy the topic information from above.") else: print( "Notifications are not set up. Run 'fetchtastic setup' to configure notifications." @@ -70,19 +90,77 @@ def main(): elif args.command == "clean": # Run the clean process run_clean() + elif args.command == "version": + print(f"Fetchtastic version {get_fetchtastic_version()}") + elif args.command == "help": + parser.print_help() elif args.command is None: # No command provided - print("No command provided.") - print("For help and available commands, run 'fetchtastic --help'.") + parser.print_help() else: parser.print_help() -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 copy_to_clipboard_func(text): + if setup_config.is_termux(): + try: + subprocess.run(["termux-clipboard-set"], input=text.encode("utf-8"), check=True) + return True + except Exception as e: + print(f"An error occurred while copying to clipboard: {e}") + return False + else: + system = platform.system() + try: + if system == "Darwin": + # macOS + subprocess.run("pbcopy", text=True, input=text, check=True) + return True + elif system == "Linux": + # Linux + if shutil.which("xclip"): + subprocess.run( + "xclip -selection clipboard", + input=text.encode("utf-8"), + shell=True, + ) + return True + elif shutil.which("xsel"): + subprocess.run( + "xsel --clipboard --input", + input=text.encode("utf-8"), + shell=True, + ) + return True + else: + print( + "xclip or xsel not found. Install xclip or xsel to use clipboard functionality." + ) + return False + elif system == "Windows": + try: + import ctypes + + ctypes.windll.user32.OpenClipboard(0) + ctypes.windll.user32.EmptyClipboard() + hCd = ctypes.windll.kernel32.GlobalAlloc(0x2000, len(text) + 1) + pchData = ctypes.windll.kernel32.GlobalLock(hCd) + ctypes.cdll.msvcrt.strcpy(ctypes.c_char_p(pchData), text.encode("utf-8")) + ctypes.windll.kernel32.GlobalUnlock(hCd) + ctypes.windll.user32.SetClipboardData(1, hCd) + ctypes.windll.user32.CloseClipboard() + return True + except Exception as e: + print( + "Clipboard functionality is not available. Install 'pywin32' package if you require this." + ) + return False + else: + print("Clipboard functionality is not supported on this platform.") + return False + except Exception as e: + print(f"An error occurred while copying to clipboard: {e}") + return False def run_clean(): @@ -103,11 +181,21 @@ def run_clean(): os.remove(config_file) print(f"Removed configuration file: {config_file}") - # Remove download directory + # Remove contents of 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}") + for item in os.listdir(download_dir): + item_path = os.path.join(download_dir, item) + try: + if os.path.isfile(item_path) or os.path.islink(item_path): + os.remove(item_path) + print(f"Removed file: {item_path}") + elif os.path.isdir(item_path): + shutil.rmtree(item_path) + print(f"Removed directory: {item_path}") + except Exception as e: + print(f"Failed to delete {item_path}. Reason: {e}") + print(f"Cleaned contents of download directory: {download_dir}") # Remove cron job entries try: @@ -116,15 +204,21 @@ def run_clean(): ["crontab", "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) if result.returncode == 0: - existing_cron = result.stdout + existing_cron = result.stdout.strip() # Remove existing fetchtastic cron jobs - new_cron = "\n".join( - [ - line - for line in existing_cron.split("\n") - if "fetchtastic download" not in line - ] - ) + cron_lines = [ + line for line in existing_cron.splitlines() if line.strip() + ] + cron_lines = [ + line + for line in cron_lines + if "# fetchtastic" not in line and "fetchtastic download" not in line + ] + # Join cron lines + new_cron = "\n".join(cron_lines) + # Ensure new_cron ends with a newline + if not new_cron.endswith("\n"): + new_cron += "\n" # Update crontab process = subprocess.Popen( ["crontab", "-"], stdin=subprocess.PIPE, text=True @@ -146,5 +240,17 @@ def run_clean(): ) +def get_fetchtastic_version(): + try: + from importlib.metadata import version + except ImportError: + # For Python < 3.8 + from importlib_metadata import version + try: + return version("fetchtastic") + except Exception: + return "unknown" + + if __name__ == "__main__": main() diff --git a/app/menu_apk.py b/app/menu_apk.py index c0abce7..5f0ec59 100644 --- a/app/menu_apk.py +++ b/app/menu_apk.py @@ -16,7 +16,7 @@ def fetch_apk_assets(): # 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")] + asset_names = sorted([asset["name"] for asset in assets if asset["name"].endswith(".apk")]) # Sorted alphabetically return asset_names diff --git a/app/menu_firmware.py b/app/menu_firmware.py index 4171658..f9208f3 100644 --- a/app/menu_firmware.py +++ b/app/menu_firmware.py @@ -14,7 +14,7 @@ def fetch_firmware_assets(): # Get the latest release latest_release = releases[0] assets = latest_release["assets"] - asset_names = [asset["name"] for asset in assets] + asset_names = sorted([asset["name"] for asset in assets]) # Sorted alphabetically return asset_names @@ -56,3 +56,4 @@ def run_menu(): except Exception as e: print(f"An error occurred: {e}") return None + diff --git a/app/setup_config.py b/app/setup_config.py index ad54b40..d4d2de6 100644 --- a/app/setup_config.py +++ b/app/setup_config.py @@ -5,7 +5,7 @@ import shutil import string import subprocess -import platform # Added to check the environment +import platform import yaml @@ -13,6 +13,23 @@ from . import menu_apk, menu_firmware +def is_termux(): + return "com.termux" in os.environ.get("PREFIX", "") + + +def get_platform(): + if is_termux(): + return "termux" + elif platform.system() == "Windows": + return "windows" + elif platform.system() == "Darwin": + return "mac" + elif platform.system() == "Linux": + return "linux" + else: + return "unknown" + + def get_downloads_dir(): # For Termux, use ~/storage/downloads if is_termux(): @@ -40,23 +57,6 @@ def config_exists(): return os.path.exists(CONFIG_FILE) -def is_termux(): - return "com.termux" in os.environ.get("PREFIX", "") - - -def get_platform(): - if is_termux(): - return "termux" - elif platform.system() == "Windows": - return "windows" - elif platform.system() == "Darwin": - return "mac" - elif platform.system() == "Linux": - return "linux" - else: - return "unknown" - - def check_storage_setup(): # Check if the Termux storage directory and Downloads are set up and writable storage_dir = os.path.expanduser("~/storage") @@ -101,9 +101,11 @@ def run_setup(): print( "Existing configuration found. You can keep current settings or change them." ) + is_first_run = False else: # Initialize default configuration config = {} + is_first_run = True # Prompt to save APKs, firmware, or both save_choice = ( @@ -153,19 +155,24 @@ def run_setup(): # Prompt for number of versions to keep if save_apks: 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) + if is_first_run: + prompt_text = f"How many versions of the Android app would you like to keep? (default is {current_versions}): " + else: + prompt_text = f"How many versions of the Android app would you like to keep? (current: {current_versions}): " + android_versions_to_keep = input(prompt_text).strip() or str(current_versions) config["ANDROID_VERSIONS_TO_KEEP"] = int(android_versions_to_keep) if save_firmware: 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) + if is_first_run: + prompt_text = f"How many versions of the firmware would you like to keep? (default is {current_versions}): " + else: + prompt_text = f"How many versions of the firmware would you like to keep? (current: {current_versions}): " + firmware_versions_to_keep = input(prompt_text).strip() or str(current_versions) config["FIRMWARE_VERSIONS_TO_KEEP"] = int(firmware_versions_to_keep) # Prompt for automatic extraction - auto_extract_default = "yes" if config.get("AUTO_EXTRACT", False) else "no" + auto_extract_current = config.get("AUTO_EXTRACT", False) + auto_extract_default = "yes" if auto_extract_current else "no" auto_extract = ( input( f"Would you like to automatically extract specific files from firmware zip archives? [y/n] (default: {auto_extract_default}): " @@ -183,18 +190,14 @@ def run_setup(): current_patterns = " ".join(config.get("EXTRACT_PATTERNS", [])) print(f"Current patterns: {current_patterns}") extract_patterns = input( - "Extraction patterns (leave blank to keep current, enter '!' to clear): " + "Extraction patterns (leave blank to keep current): " ).strip() - if extract_patterns == "!": - config["AUTO_EXTRACT"] = False - config["EXTRACT_PATTERNS"] = [] - print("Extraction patterns cleared. No files will be extracted.") - elif extract_patterns: + if extract_patterns: config["AUTO_EXTRACT"] = True config["EXTRACT_PATTERNS"] = extract_patterns.split() else: # Keep existing patterns - config["AUTO_EXTRACT"] = True + pass else: extract_patterns = input("Extraction patterns: ").strip() if extract_patterns: @@ -209,8 +212,11 @@ def run_setup(): config["EXCLUDE_PATTERNS"] = [] # Prompt for exclude patterns if extraction is enabled if config.get("AUTO_EXTRACT", False) and config.get("EXTRACT_PATTERNS"): - exclude_prompt = "Would you like to exclude any patterns from extraction? [y/n] (default: no): " - exclude_choice = input(exclude_prompt).strip().lower() or "n" + exclude_default = ( + "yes" if config.get("EXCLUDE_PATTERNS") else "no" + ) + exclude_prompt = f"Would you like to exclude any patterns from extraction? [y/n] (default: {exclude_default}): " + exclude_choice = input(exclude_prompt).strip().lower() or exclude_default[0] if exclude_choice == "y": print( "Enter the keywords to exclude from extraction, separated by spaces." @@ -220,14 +226,9 @@ def run_setup(): current_excludes = " ".join(config.get("EXCLUDE_PATTERNS", [])) print(f"Current exclude patterns: {current_excludes}") exclude_patterns = input( - "Exclude patterns (leave blank to keep current, enter '!' to clear): " + "Exclude patterns (leave blank to keep current): " ).strip() - if exclude_patterns == "!": - config["EXCLUDE_PATTERNS"] = [] - print( - "Exclude patterns cleared. No files will be excluded." - ) - elif exclude_patterns: + if exclude_patterns: config["EXCLUDE_PATTERNS"] = exclude_patterns.split() else: # Keep existing patterns @@ -248,21 +249,21 @@ def run_setup(): config["EXTRACT_PATTERNS"] = [] config["EXCLUDE_PATTERNS"] = [] - # Ask if the user wants to only download when connected to Wi-Fi - # For non-Termux environments, default to 'no' since Wi-Fi check is not implemented + # Ask if the user wants to only download when connected to Wi-Fi (Termux only) if is_termux(): wifi_only_default = "yes" if config.get("WIFI_ONLY", True) else "no" - else: - wifi_only_default = "no" - wifi_only = ( - input( - f"Do you want to only download when connected to Wi-Fi? [y/n] (default: {wifi_only_default}): " + 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] ) - .strip() - .lower() - or wifi_only_default[0] - ) - config["WIFI_ONLY"] = True if wifi_only == "y" else False + config["WIFI_ONLY"] = True if wifi_only == "y" else False + else: + # For non-Termux environments, set WIFI_ONLY to False + config["WIFI_ONLY"] = False # Set the download directory to the same as the config directory download_dir = DEFAULT_CONFIG_DIR @@ -350,22 +351,35 @@ def run_setup(): full_topic_url = f"{ntfy_server.rstrip('/')}/{topic_name}" print(f"Notifications set up using topic: {topic_name}") - print("Subscribe by pasting the topic name in the ntfy app.") + if is_termux(): + print("Subscribe by pasting the topic name in the ntfy app.") + else: + print("Subscribe by visiting the full topic URL in your browser or ntfy app.") print(f"Full topic URL: {full_topic_url}") + if is_termux(): + copy_prompt_text = "Do you want to copy the topic name to the clipboard? [y/n] (default: yes): " + else: + copy_prompt_text = "Do you want to copy the topic URL to the clipboard? [y/n] (default: yes): " copy_to_clipboard = ( - input( - "Do you want to copy the topic name to the clipboard? [y/n] (default: yes): " - ) + input(copy_prompt_text) .strip() .lower() or "y" ) if copy_to_clipboard == "y": - copy_to_clipboard_func(topic_name) - print("Topic name copied to clipboard.") + success = copy_to_clipboard_func( + full_topic_url if not is_termux() else topic_name + ) + if success: + if is_termux(): + print("Topic name copied to clipboard.") + else: + print("Topic URL copied to clipboard.") + else: + print("Failed to copy to clipboard.") else: - print("You can copy the topic name from above.") + print("You can copy the topic information from above.") else: config["NTFY_TOPIC"] = "" @@ -392,33 +406,62 @@ def copy_to_clipboard_func(text): if is_termux(): try: subprocess.run(["termux-clipboard-set"], input=text.encode("utf-8"), check=True) + return True except Exception as e: print(f"An error occurred while copying to clipboard: {e}") + return False else: system = platform.system() try: if system == "Darwin": # macOS subprocess.run("pbcopy", text=True, input=text, check=True) + return True elif system == "Linux": # Linux if shutil.which("xclip"): - subprocess.run("xclip -selection clipboard", input=text.encode("utf-8"), shell=True) + subprocess.run( + "xclip -selection clipboard", + input=text.encode("utf-8"), + shell=True, + ) + return True elif shutil.which("xsel"): - subprocess.run("xsel --clipboard --input", input=text.encode("utf-8"), shell=True) + subprocess.run( + "xsel --clipboard --input", + input=text.encode("utf-8"), + shell=True, + ) + return True else: - print("xclip or xsel not found. Install xclip or xsel to use clipboard functionality.") + print( + "xclip or xsel not found. Install xclip or xsel to use clipboard functionality." + ) + return False elif system == "Windows": try: import ctypes - command = f'echo {text.strip()}|clip' - os.system(command) + + ctypes.windll.user32.OpenClipboard(0) + ctypes.windll.user32.EmptyClipboard() + hCd = ctypes.windll.kernel32.GlobalAlloc(0x2000, len(text) + 1) + pchData = ctypes.windll.kernel32.GlobalLock(hCd) + ctypes.cdll.msvcrt.strcpy(ctypes.c_char_p(pchData), text.encode("utf-8")) + ctypes.windll.kernel32.GlobalUnlock(hCd) + ctypes.windll.user32.SetClipboardData(1, hCd) + ctypes.windll.user32.CloseClipboard() + return True except Exception as e: - print("Clipboard functionality is not available. Install 'pywin32' package.") + print( + "Clipboard functionality is not available. Install 'pywin32' package." + ) + return False else: print("Clipboard functionality is not supported on this platform.") + return False except Exception as e: print(f"An error occurred while copying to clipboard: {e}") + return False def install_termux_packages(): @@ -476,25 +519,44 @@ def setup_cron_job(): try: # Get current crontab entries result = subprocess.run( - ["crontab", "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ["crontab", "-l"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, ) if result.returncode != 0: existing_cron = "" else: - existing_cron = result.stdout + existing_cron = result.stdout.strip() + + # Remove existing fetchtastic cron jobs (excluding @reboot ones) + cron_lines = [ + line for line in existing_cron.splitlines() if line.strip() + ] + cron_lines = [ + line + for line in cron_lines + if "# fetchtastic" not in line + and not ( + line.strip().startswith("0 3 * * *") + and "fetchtastic download" in line + ) + ] - # Remove existing fetchtastic cron jobs - new_cron = "\n".join( - [ - line - for line in existing_cron.split("\n") - if "fetchtastic download" not in line or line.strip().startswith("@reboot") - ] - ) # Add new cron job - new_cron += "\n0 3 * * * fetchtastic download\n" + cron_lines.append("0 3 * * * fetchtastic download # fetchtastic") + + # Join cron lines + new_cron = "\n".join(cron_lines) + + # Ensure new_cron ends with a newline + if not new_cron.endswith("\n"): + new_cron += "\n" + # Update crontab - process = subprocess.Popen(["crontab", "-"], stdin=subprocess.PIPE, text=True) + 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: @@ -505,18 +567,31 @@ def remove_cron_job(): try: # Get current crontab entries result = subprocess.run( - ["crontab", "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ["crontab", "-l"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, ) if result.returncode == 0: - existing_cron = result.stdout + existing_cron = result.stdout.strip() # Remove existing fetchtastic cron jobs - new_cron = "\n".join( - [ - line - for line in existing_cron.split("\n") - if "fetchtastic download" not in line or line.strip().startswith("@reboot") - ] - ) + cron_lines = [ + line for line in existing_cron.splitlines() if line.strip() + ] + cron_lines = [ + line + for line in cron_lines + if "# fetchtastic" not in line + and not ( + line.strip().startswith("0 3 * * *") + and "fetchtastic download" in line + ) + ] + # Join cron lines + new_cron = "\n".join(cron_lines) + # Ensure new_cron ends with a newline + if not new_cron.endswith("\n"): + new_cron += "\n" # Update crontab process = subprocess.Popen( ["crontab", "-"], stdin=subprocess.PIPE, text=True @@ -559,25 +634,44 @@ def setup_reboot_cron_job(): try: # Get current crontab entries result = subprocess.run( - ["crontab", "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ["crontab", "-l"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, ) if result.returncode != 0: existing_cron = "" else: - existing_cron = result.stdout + existing_cron = result.stdout.strip() # Remove existing @reboot fetchtastic cron jobs - new_cron = "\n".join( - [ - line - for line in existing_cron.split("\n") - if not (line.strip().startswith("@reboot") and "fetchtastic download" in line) - ] - ) + cron_lines = [ + line for line in existing_cron.splitlines() if line.strip() + ] + cron_lines = [ + line + for line in cron_lines + if "# fetchtastic" not in line + and not ( + line.strip().startswith("@reboot") + and "fetchtastic download" in line + ) + ] + # Add new @reboot cron job - new_cron += "\n@reboot fetchtastic download\n" + cron_lines.append("@reboot fetchtastic download # fetchtastic") + + # Join cron lines + new_cron = "\n".join(cron_lines) + + # Ensure new_cron ends with a newline + if not new_cron.endswith("\n"): + new_cron += "\n" + # Update crontab - process = subprocess.Popen(["crontab", "-"], stdin=subprocess.PIPE, text=True) + process = subprocess.Popen( + ["crontab", "-"], stdin=subprocess.PIPE, text=True + ) process.communicate(input=new_cron) print("Reboot cron job added to run Fetchtastic on system startup.") except Exception as e: @@ -588,18 +682,31 @@ def remove_reboot_cron_job(): try: # Get current crontab entries result = subprocess.run( - ["crontab", "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ["crontab", "-l"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, ) if result.returncode == 0: - existing_cron = result.stdout + existing_cron = result.stdout.strip() # Remove existing @reboot fetchtastic cron jobs - new_cron = "\n".join( - [ - line - for line in existing_cron.split("\n") - if not (line.strip().startswith("@reboot") and "fetchtastic download" in line) - ] - ) + cron_lines = [ + line for line in existing_cron.splitlines() if line.strip() + ] + cron_lines = [ + line + for line in cron_lines + if "# fetchtastic" not in line + and not ( + line.strip().startswith("@reboot") + and "fetchtastic download" in line + ) + ] + # Join cron lines + new_cron = "\n".join(cron_lines) + # Ensure new_cron ends with a newline + if not new_cron.endswith("\n"): + new_cron += "\n" # Update crontab process = subprocess.Popen( ["crontab", "-"], stdin=subprocess.PIPE, text=True diff --git a/setup.cfg b/setup.cfg index 6035b28..fa6507c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = fetchtastic -version = 0.1.11 +version = 0.1.12 author = Jeremiah K author_email = jeremiahk@gmx.com description = Meshtastic Firmware and APK Downloader @@ -12,6 +12,9 @@ classifiers = Programming Language :: Python :: 3 License :: OSI Approved :: MIT License Operating System :: Android + Operating System :: POSIX :: Linux + Operating System :: MacOS + Operating System :: Microsoft :: Windows [options] packages = find: From 1ae9f242a878982b35663dc6893f9dcee8a2f2c6 Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:10:44 -0600 Subject: [PATCH 6/9] Windows specific changes --- app/cli.py | 29 +++++++++++++---------------- app/setup_config.py | 38 ++++++++++++++++++-------------------- setup.cfg | 1 + 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/app/cli.py b/app/cli.py index bb0adf3..dac40a8 100644 --- a/app/cli.py +++ b/app/cli.py @@ -103,6 +103,7 @@ def main(): def copy_to_clipboard_func(text): if setup_config.is_termux(): + # Termux environment try: subprocess.run(["termux-clipboard-set"], input=text.encode("utf-8"), check=True) return True @@ -110,6 +111,7 @@ def copy_to_clipboard_func(text): print(f"An error occurred while copying to clipboard: {e}") return False else: + # Other platforms system = platform.system() try: if system == "Darwin": @@ -120,16 +122,16 @@ def copy_to_clipboard_func(text): # Linux if shutil.which("xclip"): subprocess.run( - "xclip -selection clipboard", + ["xclip", "-selection", "clipboard"], input=text.encode("utf-8"), - shell=True, + check=True, ) return True elif shutil.which("xsel"): subprocess.run( - "xsel --clipboard --input", + ["xsel", "--clipboard", "--input"], input=text.encode("utf-8"), - shell=True, + check=True, ) return True else: @@ -138,22 +140,17 @@ def copy_to_clipboard_func(text): ) return False elif system == "Windows": + # Windows try: - import ctypes + import win32clipboard - ctypes.windll.user32.OpenClipboard(0) - ctypes.windll.user32.EmptyClipboard() - hCd = ctypes.windll.kernel32.GlobalAlloc(0x2000, len(text) + 1) - pchData = ctypes.windll.kernel32.GlobalLock(hCd) - ctypes.cdll.msvcrt.strcpy(ctypes.c_char_p(pchData), text.encode("utf-8")) - ctypes.windll.kernel32.GlobalUnlock(hCd) - ctypes.windll.user32.SetClipboardData(1, hCd) - ctypes.windll.user32.CloseClipboard() + win32clipboard.OpenClipboard() + win32clipboard.EmptyClipboard() + win32clipboard.SetClipboardText(text) + win32clipboard.CloseClipboard() return True except Exception as e: - print( - "Clipboard functionality is not available. Install 'pywin32' package if you require this." - ) + print(f"An error occurred while copying to clipboard: {e}") return False else: print("Clipboard functionality is not supported on this platform.") diff --git a/app/setup_config.py b/app/setup_config.py index d4d2de6..bc764e7 100644 --- a/app/setup_config.py +++ b/app/setup_config.py @@ -359,8 +359,11 @@ def run_setup(): if is_termux(): copy_prompt_text = "Do you want to copy the topic name to the clipboard? [y/n] (default: yes): " + text_to_copy = topic_name else: copy_prompt_text = "Do you want to copy the topic URL to the clipboard? [y/n] (default: yes): " + text_to_copy = full_topic_url + copy_to_clipboard = ( input(copy_prompt_text) .strip() @@ -368,9 +371,7 @@ def run_setup(): or "y" ) if copy_to_clipboard == "y": - success = copy_to_clipboard_func( - full_topic_url if not is_termux() else topic_name - ) + success = copy_to_clipboard_func(text_to_copy) if success: if is_termux(): print("Topic name copied to clipboard.") @@ -404,6 +405,7 @@ def run_setup(): def copy_to_clipboard_func(text): if is_termux(): + # Termux environment try: subprocess.run(["termux-clipboard-set"], input=text.encode("utf-8"), check=True) return True @@ -411,6 +413,7 @@ def copy_to_clipboard_func(text): print(f"An error occurred while copying to clipboard: {e}") return False else: + # Other platforms system = platform.system() try: if system == "Darwin": @@ -421,16 +424,16 @@ def copy_to_clipboard_func(text): # Linux if shutil.which("xclip"): subprocess.run( - "xclip -selection clipboard", + ["xclip", "-selection", "clipboard"], input=text.encode("utf-8"), - shell=True, + check=True, ) return True elif shutil.which("xsel"): subprocess.run( - "xsel --clipboard --input", + ["xsel", "--clipboard", "--input"], input=text.encode("utf-8"), - shell=True, + check=True, ) return True else: @@ -439,22 +442,17 @@ def copy_to_clipboard_func(text): ) return False elif system == "Windows": + # Windows try: - import ctypes - - ctypes.windll.user32.OpenClipboard(0) - ctypes.windll.user32.EmptyClipboard() - hCd = ctypes.windll.kernel32.GlobalAlloc(0x2000, len(text) + 1) - pchData = ctypes.windll.kernel32.GlobalLock(hCd) - ctypes.cdll.msvcrt.strcpy(ctypes.c_char_p(pchData), text.encode("utf-8")) - ctypes.windll.kernel32.GlobalUnlock(hCd) - ctypes.windll.user32.SetClipboardData(1, hCd) - ctypes.windll.user32.CloseClipboard() + import win32clipboard + + win32clipboard.OpenClipboard() + win32clipboard.EmptyClipboard() + win32clipboard.SetClipboardText(text) + win32clipboard.CloseClipboard() return True except Exception as e: - print( - "Clipboard functionality is not available. Install 'pywin32' package." - ) + print(f"An error occurred while copying to clipboard: {e}") return False else: print("Clipboard functionality is not supported on this platform.") diff --git a/setup.cfg b/setup.cfg index fa6507c..80a2b6a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,7 @@ install_requires = pick PyYAML urllib3 + pywin32; platform_system == "Windows" [options.packages.find] where = . From 17530c1673dd4317ee2de0cb0afeb2d3793c4e2f Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:25:20 -0600 Subject: [PATCH 7/9] Setup tweaks --- app/setup_config.py | 61 +++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/app/setup_config.py b/app/setup_config.py index bc764e7..7c3342c 100644 --- a/app/setup_config.py +++ b/app/setup_config.py @@ -152,9 +152,12 @@ def run_setup(): print("Run 'fetchtastic setup' again and select at least one asset.") return + # Determine default number of versions to keep based on platform + default_versions_to_keep = 2 if is_termux() else 3 + # Prompt for number of versions to keep if save_apks: - current_versions = config.get("ANDROID_VERSIONS_TO_KEEP", 2) + current_versions = config.get("ANDROID_VERSIONS_TO_KEEP", default_versions_to_keep) if is_first_run: prompt_text = f"How many versions of the Android app would you like to keep? (default is {current_versions}): " else: @@ -162,7 +165,7 @@ def run_setup(): android_versions_to_keep = input(prompt_text).strip() or str(current_versions) config["ANDROID_VERSIONS_TO_KEEP"] = int(android_versions_to_keep) if save_firmware: - current_versions = config.get("FIRMWARE_VERSIONS_TO_KEEP", 2) + current_versions = config.get("FIRMWARE_VERSIONS_TO_KEEP", default_versions_to_keep) if is_first_run: prompt_text = f"How many versions of the firmware would you like to keep? (default is {current_versions}): " else: @@ -346,6 +349,7 @@ def run_setup(): config["NTFY_TOPIC"] = topic_name config["NTFY_SERVER"] = ntfy_server + # Save configuration with NTFY settings with open(CONFIG_FILE, "w") as f: yaml.dump(config, f) @@ -382,9 +386,26 @@ def run_setup(): else: print("You can copy the topic information from above.") + # Ask if the user wants notifications only when new files are downloaded + notify_on_download_only_default = "yes" if config.get("NOTIFY_ON_DOWNLOAD_ONLY", False) else "no" + notify_on_download_only = ( + input( + f"Do you want to receive notifications only when new files are downloaded? [y/n] (default: {notify_on_download_only_default}): " + ) + .strip() + .lower() + or notify_on_download_only_default[0] + ) + config["NOTIFY_ON_DOWNLOAD_ONLY"] = True if notify_on_download_only == "y" else False + + # Save configuration with the new setting + with open(CONFIG_FILE, "w") as f: + yaml.dump(config, f) + else: config["NTFY_TOPIC"] = "" config["NTFY_SERVER"] = "" + config["NOTIFY_ON_DOWNLOAD_ONLY"] = False with open(CONFIG_FILE, "w") as f: yaml.dump(config, f) print("Notifications have been disabled.") @@ -527,17 +548,16 @@ def setup_cron_job(): else: existing_cron = result.stdout.strip() - # Remove existing fetchtastic cron jobs (excluding @reboot ones) + # Remove existing Fetchtastic cron jobs (excluding @reboot ones) cron_lines = [ line for line in existing_cron.splitlines() if line.strip() ] cron_lines = [ line for line in cron_lines - if "# fetchtastic" not in line - and not ( - line.strip().startswith("0 3 * * *") - and "fetchtastic download" in line + if not ( + ("# fetchtastic" in line or "fetchtastic download" in line) + and not line.strip().startswith("@reboot") ) ] @@ -572,17 +592,16 @@ def remove_cron_job(): ) if result.returncode == 0: existing_cron = result.stdout.strip() - # Remove existing fetchtastic cron jobs + # Remove existing Fetchtastic cron jobs (excluding @reboot) cron_lines = [ line for line in existing_cron.splitlines() if line.strip() ] cron_lines = [ line for line in cron_lines - if "# fetchtastic" not in line - and not ( - line.strip().startswith("0 3 * * *") - and "fetchtastic download" in line + if not ( + ("# fetchtastic" in line or "fetchtastic download" in line) + and not line.strip().startswith("@reboot") ) ] # Join cron lines @@ -642,17 +661,16 @@ def setup_reboot_cron_job(): else: existing_cron = result.stdout.strip() - # Remove existing @reboot fetchtastic cron jobs + # Remove existing @reboot Fetchtastic cron jobs cron_lines = [ line for line in existing_cron.splitlines() if line.strip() ] cron_lines = [ line for line in cron_lines - if "# fetchtastic" not in line - and not ( - line.strip().startswith("@reboot") - and "fetchtastic download" in line + if not ( + ("# fetchtastic" in line or "fetchtastic download" in line) + and line.strip().startswith("@reboot") ) ] @@ -687,17 +705,16 @@ def remove_reboot_cron_job(): ) if result.returncode == 0: existing_cron = result.stdout.strip() - # Remove existing @reboot fetchtastic cron jobs + # Remove existing @reboot Fetchtastic cron jobs cron_lines = [ line for line in existing_cron.splitlines() if line.strip() ] cron_lines = [ line for line in cron_lines - if "# fetchtastic" not in line - and not ( - line.strip().startswith("@reboot") - and "fetchtastic download" in line + if not ( + ("# fetchtastic" in line or "fetchtastic download" in line) + and line.strip().startswith("@reboot") ) ] # Join cron lines From 75c4c6c2d8e92293891aa8eef93b72b3288554a9 Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:38:54 -0600 Subject: [PATCH 8/9] Update README.md --- README.md | 110 +++++++++++++++++++++++++++++++++++++++++++++++------ app/cli.py | 5 +-- 2 files changed, 99 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9e740d5..04d8b9c 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,82 @@ -# Fetchtastic Termux Setup +# Fetchtastic -Fetchtastic is a utility for downloading and managing the latest Meshtastic Android app and Firmware releases on your phone using Termux. It also provides optional notifications via NTFY. +Fetchtastic is a utility for downloading and managing the latest Meshtastic Android app and Firmware releases. It also provides optional notifications via NTFY. -## Prerequisites +## Table of Contents -### Install Termux and Add-ons +- [Installation](#installation) + - [Termux Installation (Android)](#termux-installation-android) + - [Windows/Mac/Linux Installation](#windowsmaclinux-installation) +- [Usage](#usage) + - [Setup Process](#setup-process) + - [Command List](#command-list) + - [Files and Directories](#files-and-directories) + - [Scheduling with Cron](#scheduling-with-cron) + - [Notifications via NTFY](#notifications-via-ntfy) +- [Contributing](#contributing) + +## Installation + +### Termux Installation (Android) + +Fetchtastic can be installed on your Android device using Termux. + +#### Prerequisites 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. -## Installation +#### Install Dependencies -### Step 1: Install Dependencies +Open Termux and run: ```bash pkg install python python-pip openssl -y ``` -### Step 2: Install Fetchtastic +#### Install Fetchtastic + +```bash +pip install fetchtastic +``` + +### Windows/Mac/Linux Installation + +Fetchtastic can also be installed on Windows, macOS, or Linux systems. + +#### Install with pipx (Recommended) + +It's recommended to use `pipx` to install Fetchtastic in an isolated environment. + +1. **Install pipx**: + + - **On macOS/Linux**: + + ```bash + python3 -m pip install --user pipx + python3 -m pipx ensurepath + ``` + + - **On Windows**: + + ```powershell + python -m pip install --user pipx + python -m pipx ensurepath + ``` + + Restart your terminal or command prompt after installing pipx. + +2. **Install Fetchtastic with pipx**: + + ```bash + pipx install fetchtastic + ``` + +#### Install with pip + +Alternatively, you can install Fetchtastic using pip: ```bash pip install fetchtastic @@ -27,7 +84,7 @@ pip install fetchtastic ## Usage -### Run the Setup Process +### Setup Process Run the setup command and follow the prompts to configure Fetchtastic: @@ -39,18 +96,22 @@ 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. +- Set the number of versions to keep (default is 2 on Termux, 3 on desktop platforms). - Configure automatic extraction of firmware files. (Optional) - Set up notifications via NTFY. (Optional) + - Choose to receive notifications only when new files are downloaded. (Optional) - Add a cron job to run Fetchtastic regularly. (Optional) + - On Termux, Fetchtastic can be scheduled to run daily at 3 AM using Termux's cron. + - On Windows/Mac/Linux, Fetchtastic can be scheduled using the system's cron scheduler. -### Command list +### Command List - **setup**: Run the setup process. - **download**: Download firmware and APKs. - **topic**: Display the current NTFY topic. - **clean**: Remove configuration, downloads, and cron jobs. -- **--help**: Show help and usage instructions. +- **version**: Display Fetchtastic version. +- **help**: Show help and usage instructions. ### Files and Directories @@ -67,6 +128,28 @@ You can manually edit the configuration file to change the settings. During setup, you have the option to add a cron job that runs Fetchtastic daily at 3 AM. +#### Termux + +The setup process will configure the cron job using Termux's cron implementation. + +To modify the cron job, you can run: + +```bash +crontab -e +``` + +#### Windows + +You can schedule Fetchtastic to run automatically using the Task Scheduler. + +1. Open **Task Scheduler**. +2. Create a new **Basic Task**. +3. Set the action to **Start a program** and enter `fetchtastic download`. + +#### macOS/Linux + +The setup process will configure the cron job using the system's cron scheduler. + To modify the cron job, you can run: ```bash @@ -77,6 +160,9 @@ crontab -e If you choose to set up notifications, Fetchtastic will send updates to your specified NTFY topic. -### Contributing +- You can subscribe to the topic using the ntfy app or by visiting the topic URL in a browser. +- You can choose to receive notifications **only when new files are downloaded**. + +## Contributing Contributions are welcome! Feel free to open issues or submit pull requests. diff --git a/app/cli.py b/app/cli.py index dac40a8..377c6e3 100644 --- a/app/cli.py +++ b/app/cli.py @@ -231,10 +231,7 @@ def run_clean(): 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'." - ) + print("The downloaded files and Fetchtastic configuration have been removed from your system.") def get_fetchtastic_version(): From 8eb6e24bf3289b7d315bdf12accb913601e1cac5 Mon Sep 17 00:00:00 2001 From: Jeremiah K <17190268+jeremiah-k@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:48:36 -0600 Subject: [PATCH 9/9] Change notification defaults --- app/setup_config.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/setup_config.py b/app/setup_config.py index 7c3342c..d911fbe 100644 --- a/app/setup_config.py +++ b/app/setup_config.py @@ -387,7 +387,7 @@ def run_setup(): print("You can copy the topic information from above.") # Ask if the user wants notifications only when new files are downloaded - notify_on_download_only_default = "yes" if config.get("NOTIFY_ON_DOWNLOAD_ONLY", False) else "no" + notify_on_download_only_default = "yes" if config.get("NOTIFY_ON_DOWNLOAD_ONLY", False) else "yes" notify_on_download_only = ( input( f"Do you want to receive notifications only when new files are downloaded? [y/n] (default: {notify_on_download_only_default}): " diff --git a/setup.cfg b/setup.cfg index 80a2b6a..ae34139 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = fetchtastic -version = 0.1.12 +version = 0.2.0 author = Jeremiah K author_email = jeremiahk@gmx.com description = Meshtastic Firmware and APK Downloader