fetchtastic 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
app/__init__.py ADDED
File without changes
app/cli.py ADDED
@@ -0,0 +1,57 @@
1
+ # app/cli.py
2
+
3
+ import argparse
4
+ from . import downloader
5
+ from . import setup_config
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser(description="Fetchtastic - Meshtastic Firmware and APK Downloader")
9
+ subparsers = parser.add_subparsers(dest='command')
10
+
11
+ # Command to run setup
12
+ subparsers.add_parser('setup', help='Run the setup process')
13
+
14
+ # Command to download firmware and APKs
15
+ subparsers.add_parser('download', help='Download firmware and APKs')
16
+
17
+ # Command to display NTFY topic
18
+ subparsers.add_parser('topic', help='Display the current NTFY topic')
19
+
20
+ # Command to clean/remove Fetchtastic files and settings
21
+ subparsers.add_parser('clean', help='Remove Fetchtastic configuration, downloads, and cron jobs')
22
+
23
+ args = parser.parse_args()
24
+
25
+ if args.command == 'setup':
26
+ # Run the setup process
27
+ setup_config.run_setup()
28
+ elif args.command == 'download':
29
+ # Check if configuration exists
30
+ if not setup_config.config_exists():
31
+ print("No configuration found. Running setup.")
32
+ setup_config.run_setup()
33
+ # Run the downloader
34
+ downloader.main()
35
+ elif args.command == 'topic':
36
+ # Display the NTFY topic
37
+ config = setup_config.load_config()
38
+ if config and config.get('NTFY_SERVER') and config.get('NTFY_TOPIC'):
39
+ ntfy_server = config['NTFY_SERVER'].rstrip('/')
40
+ ntfy_topic = config['NTFY_TOPIC']
41
+ full_url = f"{ntfy_server}/{ntfy_topic}"
42
+ print(f"Current NTFY topic URL: {full_url}")
43
+ print(f"Topic name: {ntfy_topic}")
44
+ else:
45
+ print("Notifications are not set up. Run 'fetchtastic setup' to configure notifications.")
46
+ elif args.command == 'clean':
47
+ # Run the clean process
48
+ setup_config.run_clean()
49
+ elif args.command is None:
50
+ # No command provided
51
+ print("No command provided.")
52
+ print("For help and available commands, run 'fetchtastic --help'.")
53
+ else:
54
+ parser.print_help()
55
+
56
+ if __name__ == "__main__":
57
+ main()
app/downloader.py ADDED
@@ -0,0 +1,256 @@
1
+ # app/downloader.py
2
+
3
+ import os
4
+ import requests
5
+ import zipfile
6
+ import time
7
+ from datetime import datetime
8
+ from requests.adapters import HTTPAdapter
9
+ from urllib3.util.retry import Retry
10
+
11
+ from . import setup_config
12
+
13
+ def main():
14
+ # Load configuration
15
+ config = setup_config.load_config()
16
+ if not config:
17
+ print("Configuration not found. Please run 'fetchtastic setup' first.")
18
+ return
19
+
20
+ # Get configuration values
21
+ save_apks = config.get("SAVE_APKS", False)
22
+ save_firmware = config.get("SAVE_FIRMWARE", False)
23
+ ntfy_server = config.get("NTFY_SERVER", "")
24
+ ntfy_topic = config.get("NTFY_TOPIC", "")
25
+ android_versions_to_keep = config.get("ANDROID_VERSIONS_TO_KEEP", 2)
26
+ firmware_versions_to_keep = config.get("FIRMWARE_VERSIONS_TO_KEEP", 2)
27
+ auto_extract = config.get("AUTO_EXTRACT", False)
28
+ extract_patterns = config.get("EXTRACT_PATTERNS", [])
29
+
30
+ selected_apk_assets = config.get('SELECTED_APK_ASSETS', [])
31
+ selected_firmware_assets = config.get('SELECTED_FIRMWARE_ASSETS', [])
32
+
33
+ download_dir = config.get('DOWNLOAD_DIR', os.path.join(os.path.expanduser("~"), "storage", "downloads", "Meshtastic"))
34
+ firmware_dir = os.path.join(download_dir, "firmware")
35
+ apks_dir = os.path.join(download_dir, "apks")
36
+ latest_android_release_file = os.path.join(apks_dir, "latest_android_release.txt")
37
+ latest_firmware_release_file = os.path.join(firmware_dir, "latest_firmware_release.txt")
38
+
39
+ # Create necessary directories
40
+ for dir_path in [download_dir, firmware_dir, apks_dir]:
41
+ if not os.path.exists(dir_path):
42
+ os.makedirs(dir_path)
43
+
44
+ # Logging setup
45
+ log_file = os.path.join(download_dir, "fetchtastic.log")
46
+
47
+ def log_message(message):
48
+ with open(log_file, "a") as log:
49
+ log.write(f"{datetime.now()}: {message}\n")
50
+ print(message)
51
+
52
+ def send_ntfy_notification(message):
53
+ if ntfy_server and ntfy_topic:
54
+ try:
55
+ ntfy_url = f"{ntfy_server.rstrip('/')}/{ntfy_topic}"
56
+ response = requests.post(ntfy_url, data=message.encode('utf-8'))
57
+ response.raise_for_status()
58
+ except requests.exceptions.RequestException as e:
59
+ log_message(f"Error sending notification: {e}")
60
+ else:
61
+ log_message("Notifications are not configured.")
62
+
63
+ # Function to get the latest releases and sort by date
64
+ def get_latest_releases(url, versions_to_keep, scan_count=5):
65
+ response = requests.get(url)
66
+ response.raise_for_status()
67
+ releases = response.json()
68
+ # Sort releases by published date, descending order
69
+ sorted_releases = sorted(releases, key=lambda r: r['published_at'], reverse=True)
70
+ # Limit the number of releases to be scanned and downloaded
71
+ return sorted_releases[:scan_count][:versions_to_keep]
72
+
73
+ # Function to download a file with retry mechanism
74
+ def download_file(url, download_path):
75
+ session = requests.Session()
76
+ retry = Retry(connect=3, backoff_factor=1, status_forcelist=[502, 503, 504])
77
+ adapter = HTTPAdapter(max_retries=retry)
78
+ session.mount('https://', adapter)
79
+ session.mount('http://', adapter)
80
+
81
+ try:
82
+ if not os.path.exists(download_path):
83
+ log_message(f"Downloading {url}")
84
+ response = session.get(url, stream=True)
85
+ response.raise_for_status()
86
+ with open(download_path, 'wb') as file:
87
+ for chunk in response.iter_content(1024):
88
+ file.write(chunk)
89
+ log_message(f"Downloaded {download_path}")
90
+ else:
91
+ log_message(f"{download_path} already exists, skipping download.")
92
+ except requests.exceptions.RequestException as e:
93
+ log_message(f"Error downloading {url}: {e}")
94
+
95
+ # Updated extract_files function
96
+ def extract_files(zip_path, extract_dir, patterns):
97
+ try:
98
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
99
+ matched_files = []
100
+ for file_info in zip_ref.infolist():
101
+ file_name = file_info.filename
102
+ base_name = os.path.basename(file_name)
103
+ log_message(f"Checking file: {base_name}")
104
+ for pattern in patterns:
105
+ if pattern in base_name:
106
+ # Extract and flatten directory structure
107
+ source = zip_ref.open(file_info)
108
+ target_path = os.path.join(extract_dir, base_name)
109
+ with open(target_path, 'wb') as target_file:
110
+ target_file.write(source.read())
111
+ log_message(f"Extracted {base_name} to {extract_dir}")
112
+ matched_files.append(base_name)
113
+ break # Stop checking patterns for this file
114
+ if not matched_files:
115
+ log_message(f"No files matched the extraction patterns in {zip_path}.")
116
+ except zipfile.BadZipFile:
117
+ log_message(f"Error: {zip_path} is a bad zip file and cannot be opened.")
118
+ except Exception as e:
119
+ log_message(f"Error: An unexpected error occurred while extracting files from {zip_path}: {e}")
120
+
121
+ # Cleanup function to keep only a specific number of versions
122
+ def cleanup_old_versions(directory, keep_count):
123
+ versions = sorted(
124
+ (os.path.join(directory, d) for d in os.listdir(directory) if os.path.isdir(os.path.join(directory, d))),
125
+ key=os.path.getmtime
126
+ )
127
+ old_versions = versions[:-keep_count]
128
+ for version in old_versions:
129
+ for root, dirs, files in os.walk(version, topdown=False):
130
+ for name in files:
131
+ os.remove(os.path.join(root, name))
132
+ log_message(f"Removed file: {os.path.join(root, name)}")
133
+ for name in dirs:
134
+ os.rmdir(os.path.join(root, name))
135
+ os.rmdir(version)
136
+ log_message(f"Removed directory: {version}")
137
+
138
+ # Function to check for missing releases and download them if necessary
139
+ def check_and_download(releases, latest_release_file, release_type, download_dir, versions_to_keep, extract_patterns, selected_assets=None):
140
+ downloaded_versions = []
141
+
142
+ if not os.path.exists(download_dir):
143
+ os.makedirs(download_dir)
144
+
145
+ # Load the latest release tag from file if available
146
+ saved_release_tag = None
147
+ if os.path.exists(latest_release_file):
148
+ with open(latest_release_file, 'r') as f:
149
+ saved_release_tag = f.read().strip()
150
+
151
+ # Determine which releases to download
152
+ for release in releases:
153
+ release_tag = release['tag_name']
154
+ release_dir = os.path.join(download_dir, release_tag)
155
+
156
+ if os.path.exists(release_dir) or release_tag == saved_release_tag:
157
+ log_message(f"Skipping version {release_tag}, already exists.")
158
+ else:
159
+ # Proceed to download this version
160
+ os.makedirs(release_dir, exist_ok=True)
161
+ log_message(f"Downloading new version: {release_tag}")
162
+ for asset in release['assets']:
163
+ file_name = asset['name']
164
+ if selected_assets:
165
+ if file_name not in selected_assets:
166
+ continue
167
+ download_path = os.path.join(release_dir, file_name)
168
+ download_file(asset['browser_download_url'], download_path)
169
+ if auto_extract and file_name.endswith('.zip') and release_type == "Firmware":
170
+ extract_files(download_path, release_dir, extract_patterns)
171
+ downloaded_versions.append(release_tag)
172
+
173
+ # Update latest_release_file with the most recent tag
174
+ if releases:
175
+ with open(latest_release_file, 'w') as f:
176
+ f.write(releases[0]['tag_name'])
177
+
178
+ # Clean up old versions
179
+ cleanup_old_versions(download_dir, versions_to_keep)
180
+ return downloaded_versions
181
+
182
+ start_time = time.time()
183
+ log_message("Starting Fetchtastic...")
184
+
185
+ downloaded_firmwares = []
186
+ downloaded_apks = []
187
+
188
+ # URLs for releases
189
+ android_releases_url = "https://api.github.com/repos/meshtastic/Meshtastic-Android/releases"
190
+ firmware_releases_url = "https://api.github.com/repos/meshtastic/firmware/releases"
191
+
192
+ # Scan for the last 5 releases, download the latest versions_to_download
193
+ releases_to_scan = 5
194
+
195
+ latest_firmware_releases = []
196
+ latest_android_releases = []
197
+
198
+ if save_firmware and selected_firmware_assets:
199
+ versions_to_download = firmware_versions_to_keep
200
+ latest_firmware_releases = get_latest_releases(firmware_releases_url, versions_to_download, releases_to_scan)
201
+ downloaded_firmwares = check_and_download(
202
+ latest_firmware_releases,
203
+ latest_firmware_release_file,
204
+ "Firmware",
205
+ firmware_dir,
206
+ firmware_versions_to_keep,
207
+ extract_patterns,
208
+ selected_assets=selected_firmware_assets
209
+ )
210
+ log_message(f"Latest Firmware releases: {', '.join(release['tag_name'] for release in latest_firmware_releases)}")
211
+ elif not selected_firmware_assets:
212
+ log_message("No firmware assets selected. Skipping firmware download.")
213
+
214
+ if save_apks and selected_apk_assets:
215
+ versions_to_download = android_versions_to_keep
216
+ latest_android_releases = get_latest_releases(android_releases_url, versions_to_download, releases_to_scan)
217
+ downloaded_apks = check_and_download(
218
+ latest_android_releases,
219
+ latest_android_release_file,
220
+ "Android APK",
221
+ apks_dir,
222
+ android_versions_to_keep,
223
+ extract_patterns,
224
+ selected_assets=selected_apk_assets
225
+ )
226
+ log_message(f"Latest Android APK releases: {', '.join(release['tag_name'] for release in latest_android_releases)}")
227
+ elif not selected_apk_assets:
228
+ log_message("No APK assets selected. Skipping APK download.")
229
+
230
+ end_time = time.time()
231
+ total_time = end_time - start_time
232
+ log_message(f"Finished the Meshtastic downloader. Total time taken: {total_time:.2f} seconds")
233
+
234
+ # Send notification if there are new downloads
235
+ if downloaded_firmwares or downloaded_apks:
236
+ message = ""
237
+ if downloaded_firmwares:
238
+ message += f"New Firmware releases {', '.join(downloaded_firmwares)} downloaded.\n"
239
+ if downloaded_apks:
240
+ message += f"New Android APK releases {', '.join(downloaded_apks)} downloaded.\n"
241
+ message += f"{datetime.now()}"
242
+ send_ntfy_notification(message)
243
+ else:
244
+ if latest_firmware_releases or latest_android_releases:
245
+ message = (
246
+ f"All Firmware and Android APK versions are up to date.\n"
247
+ f"Latest Firmware releases: {', '.join(release['tag_name'] for release in latest_firmware_releases)}\n"
248
+ f"Latest Android APK releases: {', '.join(release['tag_name'] for release in latest_android_releases)}\n"
249
+ f"{datetime.now()}"
250
+ )
251
+ send_ntfy_notification(message)
252
+ else:
253
+ log_message("No releases found to check for updates.")
254
+
255
+ if __name__ == "__main__":
256
+ main()
app/menu_apk.py ADDED
@@ -0,0 +1,40 @@
1
+ # fetchtastic/menu_apk.py
2
+
3
+ import re
4
+ import requests
5
+ from pick import pick
6
+
7
+ def fetch_apk_assets():
8
+ apk_releases_url = "https://api.github.com/repos/meshtastic/Meshtastic-Android/releases"
9
+ response = requests.get(apk_releases_url)
10
+ response.raise_for_status()
11
+ releases = response.json()
12
+ # Get the latest release
13
+ latest_release = releases[0]
14
+ assets = latest_release['assets']
15
+ asset_names = [asset['name'] for asset in assets if asset['name'].endswith('.apk')]
16
+ return asset_names
17
+
18
+ def select_assets(assets):
19
+ title = '''Select the APK files you want to download (press SPACE to select, ENTER to confirm):
20
+ Note: These are files from the latest release. Version numbers may change in other releases.'''
21
+ options = assets
22
+ selected_options = pick(options, title, multiselect=True, min_selection_count=0, indicator='*')
23
+ selected_assets = [option[0] for option in selected_options]
24
+ if not selected_assets:
25
+ print("No APK files selected. APKs will not be downloaded.")
26
+ return None
27
+ return selected_assets
28
+
29
+ def run_menu():
30
+ try:
31
+ assets = fetch_apk_assets()
32
+ selected_assets = select_assets(assets)
33
+ if selected_assets is None:
34
+ return None
35
+ return {
36
+ 'selected_assets': selected_assets
37
+ }
38
+ except Exception as e:
39
+ print(f"An error occurred: {e}")
40
+ return None
app/menu_firmware.py ADDED
@@ -0,0 +1,39 @@
1
+ # app/menu_firmware.py
2
+
3
+ import requests
4
+ from pick import pick
5
+
6
+ def fetch_firmware_assets():
7
+ firmware_releases_url = "https://api.github.com/repos/meshtastic/firmware/releases"
8
+ response = requests.get(firmware_releases_url)
9
+ response.raise_for_status()
10
+ releases = response.json()
11
+ # Get the latest release
12
+ latest_release = releases[0]
13
+ assets = latest_release['assets']
14
+ asset_names = [asset['name'] for asset in assets]
15
+ return asset_names
16
+
17
+ def select_assets(assets):
18
+ title = '''Select the firmware files you want to download (press SPACE to select, ENTER to confirm):
19
+ Note: These are files from the latest release. Version numbers may change in other releases.'''
20
+ options = assets
21
+ selected_options = pick(options, title, multiselect=True, min_selection_count=0, indicator='*')
22
+ selected_assets = [option[0] for option in selected_options]
23
+ if not selected_assets:
24
+ print("No firmware files selected. Firmware will not be downloaded.")
25
+ return None
26
+ return selected_assets
27
+
28
+ def run_menu():
29
+ try:
30
+ assets = fetch_firmware_assets()
31
+ selected_assets = select_assets(assets)
32
+ if selected_assets is None:
33
+ return None
34
+ return {
35
+ 'selected_assets': selected_assets
36
+ }
37
+ except Exception as e:
38
+ print(f"An error occurred: {e}")
39
+ return None
app/setup_config.py ADDED
@@ -0,0 +1,288 @@
1
+ # app/setup_config.py
2
+
3
+ import os
4
+ import yaml
5
+ import subprocess
6
+ import random
7
+ import string
8
+ import shutil # Added for shutil.which()
9
+ from . import menu_apk
10
+ from . import menu_firmware
11
+ from . import downloader # Import downloader to perform first run
12
+
13
+ def get_downloads_dir():
14
+ # For Termux, use ~/storage/downloads
15
+ if 'com.termux' in os.environ.get('PREFIX', ''):
16
+ storage_downloads = os.path.expanduser("~/storage/downloads")
17
+ if os.path.exists(storage_downloads):
18
+ return storage_downloads
19
+ # For other environments, use standard Downloads directories
20
+ home_dir = os.path.expanduser("~")
21
+ downloads_dir = os.path.join(home_dir, 'Downloads')
22
+ if os.path.exists(downloads_dir):
23
+ return downloads_dir
24
+ downloads_dir = os.path.join(home_dir, 'Download')
25
+ if os.path.exists(downloads_dir):
26
+ return downloads_dir
27
+ # Fallback to home directory
28
+ return home_dir
29
+
30
+ DOWNLOADS_DIR = get_downloads_dir()
31
+ DEFAULT_CONFIG_DIR = os.path.join(DOWNLOADS_DIR, 'Meshtastic')
32
+ CONFIG_FILE = os.path.join(DEFAULT_CONFIG_DIR, 'fetchtastic.yaml')
33
+
34
+ def config_exists():
35
+ return os.path.exists(CONFIG_FILE)
36
+
37
+ def run_setup():
38
+ # Check if running in Termux
39
+ if not is_termux():
40
+ print("Warning: Fetchtastic is designed to run in Termux on Android.")
41
+ print("For more information, visit https://github.com/jeremiah-k/fetchtastic/")
42
+ return
43
+
44
+ print("Running Fetchtastic Setup...")
45
+ if not os.path.exists(DEFAULT_CONFIG_DIR):
46
+ os.makedirs(DEFAULT_CONFIG_DIR)
47
+
48
+ config = {}
49
+
50
+ # Prompt to save APKs, firmware, or both
51
+ save_choice = input("Do you want to save APKs, firmware, or both? [a/f/b] (default: b): ").strip().lower() or 'b'
52
+ if save_choice == 'a':
53
+ save_apks = True
54
+ save_firmware = False
55
+ elif save_choice == 'f':
56
+ save_apks = False
57
+ save_firmware = True
58
+ else:
59
+ save_apks = True
60
+ save_firmware = True
61
+ config['SAVE_APKS'] = save_apks
62
+ config['SAVE_FIRMWARE'] = save_firmware
63
+
64
+ # Run the menu scripts based on user choices
65
+ if save_apks:
66
+ apk_selection = menu_apk.run_menu()
67
+ if not apk_selection:
68
+ print("No APK assets selected. APKs will not be downloaded.")
69
+ save_apks = False
70
+ config['SAVE_APKS'] = False
71
+ else:
72
+ config['SELECTED_APK_ASSETS'] = apk_selection['selected_assets']
73
+ if save_firmware:
74
+ firmware_selection = menu_firmware.run_menu()
75
+ if not firmware_selection:
76
+ print("No firmware assets selected. Firmware will not be downloaded.")
77
+ save_firmware = False
78
+ config['SAVE_FIRMWARE'] = False
79
+ else:
80
+ config['SELECTED_FIRMWARE_ASSETS'] = firmware_selection['selected_assets']
81
+
82
+ # If both save_apks and save_firmware are False, inform the user and exit setup
83
+ if not save_apks and not save_firmware:
84
+ print("You must select at least one asset to download (APK or firmware).")
85
+ print("Please run 'fetchtastic setup' again and select at least one asset.")
86
+ return
87
+
88
+ # Prompt for number of versions to keep
89
+ if save_apks:
90
+ android_versions_to_keep = input("Enter the number of different versions of the Android app to keep (default: 2): ").strip() or '2'
91
+ config['ANDROID_VERSIONS_TO_KEEP'] = int(android_versions_to_keep)
92
+ if save_firmware:
93
+ firmware_versions_to_keep = input("Enter the number of different versions of the firmware to keep (default: 2): ").strip() or '2'
94
+ config['FIRMWARE_VERSIONS_TO_KEEP'] = int(firmware_versions_to_keep)
95
+
96
+ # Prompt for automatic extraction
97
+ auto_extract = input("Do you want to automatically extract specific files from firmware zips? [y/n] (default: n): ").strip().lower() or 'n'
98
+ if auto_extract == 'y':
99
+ print("Enter the strings to match for extraction from the firmware .zip files, separated by spaces.")
100
+ print("Example: rak4631- tbeam-2 t1000-e- tlora-v2-1-1_6-")
101
+ extract_patterns = input("Extraction patterns: ").strip()
102
+ if extract_patterns:
103
+ config['AUTO_EXTRACT'] = True
104
+ config['EXTRACT_PATTERNS'] = extract_patterns.split()
105
+ else:
106
+ config['AUTO_EXTRACT'] = False
107
+ else:
108
+ config['AUTO_EXTRACT'] = False
109
+
110
+ # Set the download directory to the same as the config directory
111
+ download_dir = DEFAULT_CONFIG_DIR
112
+ config['DOWNLOAD_DIR'] = download_dir
113
+
114
+ # Save configuration to YAML file before proceeding
115
+ with open(CONFIG_FILE, 'w') as f:
116
+ yaml.dump(config, f)
117
+
118
+ # Ask if the user wants to set up a cron job
119
+ 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
+ if setup_cron == 'y':
121
+ # Install crond if not already installed
122
+ install_crond()
123
+ # Call function to set up cron job
124
+ setup_cron_job()
125
+ else:
126
+ print("Skipping cron job setup.")
127
+
128
+ # Prompt for NTFY server configuration
129
+ notifications = input("Do you want to set up notifications via NTFY? [y/n] (default: y): ").strip().lower() or 'y'
130
+ if notifications == 'y':
131
+ ntfy_server = input("Enter the NTFY server (default: ntfy.sh): ").strip() or 'ntfy.sh'
132
+ if not ntfy_server.startswith('http://') and not ntfy_server.startswith('https://'):
133
+ ntfy_server = 'https://' + ntfy_server
134
+
135
+ # Generate a random topic name if the user doesn't provide one
136
+ default_topic = 'fetchtastic-' + ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
137
+ topic_name = input(f"Enter a unique topic name (default: {default_topic}): ").strip() or default_topic
138
+ # Save only the topic name in the config
139
+ config['NTFY_TOPIC'] = topic_name
140
+ config['NTFY_SERVER'] = ntfy_server
141
+
142
+ # Save updated configuration
143
+ with open(CONFIG_FILE, 'w') as f:
144
+ yaml.dump(config, f)
145
+
146
+ full_topic_url = f"{ntfy_server.rstrip('/')}/{topic_name}"
147
+ print(f"Notifications have been set up using the topic: {topic_name}")
148
+ print(f"You can subscribe to this topic in the ntfy app easily by pasting the topic name.")
149
+ print(f"Full topic URL: {full_topic_url}")
150
+ # Ask if the user wants to copy the topic name to the clipboard
151
+ copy_to_clipboard = input("Do you want to copy the topic name to the clipboard? [y/n] (default: y): ").strip().lower() or 'y'
152
+ if copy_to_clipboard == 'y':
153
+ copy_to_clipboard_termux(topic_name)
154
+ print("Topic name copied to clipboard.")
155
+ else:
156
+ print("You can copy the topic name from above.")
157
+
158
+ print("You can view your current topic at any time by running 'fetchtastic topic'.")
159
+ print("You can change the topic by running 'fetchtastic setup' again or editing the YAML file.")
160
+ else:
161
+ config['NTFY_TOPIC'] = ''
162
+ config['NTFY_SERVER'] = ''
163
+ # Save updated configuration
164
+ with open(CONFIG_FILE, 'w') as f:
165
+ yaml.dump(config, f)
166
+ print("Notifications have not been set up.")
167
+
168
+ # Ask if the user wants to perform a first run
169
+ perform_first_run = input("Do you want to perform a first run now? [y/n] (default: y): ").strip().lower() or 'y'
170
+ if perform_first_run == 'y':
171
+ print("Performing first run, this may take a few minutes...")
172
+ downloader.main()
173
+ else:
174
+ print("Setup complete. You can run 'fetchtastic download' to start downloading.")
175
+
176
+ def is_termux():
177
+ return 'com.termux' in os.environ.get('PREFIX', '')
178
+
179
+ def copy_to_clipboard_termux(text):
180
+ try:
181
+ subprocess.run(['termux-clipboard-set'], input=text.encode('utf-8'), check=True)
182
+ except Exception as e:
183
+ print(f"An error occurred while copying to clipboard: {e}")
184
+
185
+ def install_crond():
186
+ try:
187
+ # Check if crond is installed
188
+ crond_path = shutil.which('crond')
189
+ if crond_path is None:
190
+ print("Installing crond...")
191
+ subprocess.run(['pkg', 'install', 'termux-services', '-y'], check=True)
192
+ subprocess.run(['sv-enable', 'crond'], check=True)
193
+ print("crond installed and started.")
194
+ else:
195
+ print("crond is already installed.")
196
+ except Exception as e:
197
+ print(f"An error occurred while installing crond: {e}")
198
+
199
+ def setup_cron_job():
200
+ try:
201
+ # Get current crontab entries
202
+ result = subprocess.run(['crontab', '-l'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
203
+ if result.returncode != 0:
204
+ existing_cron = ''
205
+ else:
206
+ existing_cron = result.stdout
207
+
208
+ # Check for existing cron jobs related to fetchtastic
209
+ if 'fetchtastic download' in existing_cron:
210
+ print("An existing cron job for Fetchtastic was found:")
211
+ print(existing_cron)
212
+ keep_cron = input("Do you want to keep the existing crontab entry? [y/n] (default: y): ").strip().lower() or 'y'
213
+ if keep_cron == 'n':
214
+ # Remove existing fetchtastic cron jobs
215
+ new_cron = '\n'.join([line for line in existing_cron.split('\n') if 'fetchtastic download' not in line])
216
+ # Update crontab
217
+ process = subprocess.Popen(['crontab', '-'], stdin=subprocess.PIPE, text=True)
218
+ process.communicate(input=new_cron)
219
+ print("Existing Fetchtastic cron job removed.")
220
+ # Ask if they want to add a new cron job
221
+ 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'
222
+ if add_cron == 'y':
223
+ # Add new cron job
224
+ new_cron += f"\n0 3 * * * fetchtastic download\n"
225
+ # Update crontab
226
+ process = subprocess.Popen(['crontab', '-'], stdin=subprocess.PIPE, text=True)
227
+ process.communicate(input=new_cron)
228
+ print("New cron job added.")
229
+ else:
230
+ print("Skipping cron job installation.")
231
+ else:
232
+ print("Keeping existing crontab entry.")
233
+ else:
234
+ # No existing fetchtastic cron job
235
+ 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'
236
+ if add_cron == 'y':
237
+ # Add new cron job
238
+ new_cron = existing_cron.strip() + f"\n0 3 * * * fetchtastic download\n"
239
+ # Update crontab
240
+ process = subprocess.Popen(['crontab', '-'], stdin=subprocess.PIPE, text=True)
241
+ process.communicate(input=new_cron)
242
+ print("Cron job added to run Fetchtastic daily at 3 AM.")
243
+ else:
244
+ print("Skipping cron job installation.")
245
+ except Exception as e:
246
+ print(f"An error occurred while setting up the cron job: {e}")
247
+
248
+ def run_clean():
249
+ print("This will remove Fetchtastic configuration files, downloaded files, and cron job entries.")
250
+ confirm = input("Are you sure you want to proceed? [y/n] (default: n): ").strip().lower() or 'n'
251
+ if confirm != 'y':
252
+ print("Clean operation cancelled.")
253
+ return
254
+
255
+ # Remove configuration file
256
+ if os.path.exists(CONFIG_FILE):
257
+ os.remove(CONFIG_FILE)
258
+ print(f"Removed configuration file: {CONFIG_FILE}")
259
+
260
+ # Remove download directory
261
+ if os.path.exists(DEFAULT_CONFIG_DIR):
262
+ shutil.rmtree(DEFAULT_CONFIG_DIR)
263
+ print(f"Removed download directory: {DEFAULT_CONFIG_DIR}")
264
+
265
+ # Remove cron job entries
266
+ try:
267
+ # Get current crontab entries
268
+ result = subprocess.run(['crontab', '-l'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
269
+ if result.returncode == 0:
270
+ existing_cron = result.stdout
271
+ # Remove existing fetchtastic cron jobs
272
+ new_cron = '\n'.join([line for line in existing_cron.split('\n') if 'fetchtastic download' not in line])
273
+ # Update crontab
274
+ process = subprocess.Popen(['crontab', '-'], stdin=subprocess.PIPE, text=True)
275
+ process.communicate(input=new_cron)
276
+ print("Removed Fetchtastic cron job entries.")
277
+ except Exception as e:
278
+ print(f"An error occurred while removing cron jobs: {e}")
279
+
280
+ print("Fetchtastic has been cleaned from your system.")
281
+ print("If you installed Fetchtastic via pip and wish to uninstall it, run 'pip uninstall fetchtastic'.")
282
+
283
+ def load_config():
284
+ if not config_exists():
285
+ return None
286
+ with open(CONFIG_FILE, 'r') as f:
287
+ config = yaml.safe_load(f)
288
+ return config
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Jeremiah K
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.1
2
+ Name: fetchtastic
3
+ Version: 0.1.0
4
+ Summary: Meshtastic Firmware and APK Downloader
5
+ Home-page: https://github.com/jeremiah-k/fetchtastic
6
+ Author: Jeremiah K
7
+ Author-email: jeremiahk@gmx.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: requests
15
+ Requires-Dist: pick
16
+ Requires-Dist: PyYAML
17
+ Requires-Dist: urllib3
18
+
19
+ # Fetchtastic Termux Setup
20
+
21
+ 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.
22
+
23
+ ## Prerequisites
24
+
25
+ ### Install Termux and Add-ons
26
+
27
+ 1. **Install Termux**: Download and install [Termux](https://f-droid.org/en/packages/com.termux/) from F-Droid.
28
+ 2. **Install Termux Boot**: Download and install [Termux Boot](https://f-droid.org/en/packages/com.termux.boot/) from F-Droid.
29
+ 3. **Install Termux API**: Download and install [Termux API](https://f-droid.org/en/packages/com.termux.api/) from F-Droid.
30
+ 4. *(Optional)* **Install ntfy**: Download and install [ntfy](https://f-droid.org/en/packages/io.heckel.ntfy/) from F-Droid.
31
+
32
+ ### Request Storage Access for Termux
33
+
34
+ Open Termux and run the following command to grant storage access:
35
+
36
+ ```bash
37
+ termux-setup-storage
38
+ ```
39
+ ## Installation
40
+
41
+ ### Step 1: Install Python
42
+
43
+ ```bash
44
+ pkg install python -y
45
+ ```
46
+
47
+ ### Step 2: Install Fetchtastic
48
+
49
+ ```bash
50
+ pip install fetchtastic
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ ### Run the Setup Process
56
+
57
+ Run the setup command and follow the prompts to configure Fetchtastic:
58
+
59
+ ```bash
60
+ fetchtastic setup
61
+ ```
62
+
63
+ During setup, you will be able to:
64
+
65
+ - Choose whether to download APKs, firmware, or both.
66
+ - Select specific assets to download.
67
+ - Set the number of versions to keep.
68
+ - Configure automatic extraction of firmware files. (Optional)
69
+ - Set up notifications via NTFY. (Optional)
70
+ - Add a cron job to run Fetchtastic regularly. (Optional)
71
+
72
+ ### Perform Downloads
73
+
74
+ To manually start the download process, run:
75
+
76
+ ```bash
77
+ fetchtastic download
78
+ ```
79
+
80
+ This will download the latest versions of the selected assets and store them in the specified directories.
81
+
82
+ ### Command list
83
+
84
+ - **setup**: Run the setup process.
85
+ - **download**: Download firmware and APKs.
86
+ - **topic**: Display the current NTFY topic.
87
+ - **clean**: Remove configuration, downloads, and cron jobs.
88
+ - **--help**: Show help and usage instructions.
89
+
90
+ ### Files and Directories
91
+
92
+ By default, Fetchtastic saves files and configuration in the `Downloads/Fetchtastic` directory:
93
+
94
+ - **Configuration File**: `Downloads/Fetchtastic/fetchtastic.yaml`
95
+ - **Log File**: `Downloads/Fetchtastic/fetchtastic.log`
96
+ - **APKs**: `Downloads/Fetchtastic/apks`
97
+ - **Firmware**: `Downloads/Fetchtastic/firmware`
98
+
99
+ You can manually edit the configuration file to change the settings.
100
+
101
+
102
+ ### Scheduling with Cron
103
+
104
+ During setup, you have the option to add a cron job that runs Fetchtastic daily at 3 AM.
105
+
106
+ To modify cron job, you can run:
107
+ ```bash
108
+ crontab -e
109
+ ```
110
+
111
+ ### Notifications via NTFY
112
+
113
+ If you choose to set up notifications, Fetchtastic will send updates to your specified NTFY topic.
114
+
115
+ ### Contributing
116
+
117
+ Contributions are welcome! Feel free to open issues or submit pull requests.
@@ -0,0 +1,12 @@
1
+ app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ app/cli.py,sha256=0MXDY-WKgQSg3ndFsAY9EItBfrEabvevT6Z1Ey39R0s,2065
3
+ app/downloader.py,sha256=djXk9zkbkTr407bKDcrT9l-j3yOXRz0KZ_Unc64W6O8,11509
4
+ app/menu_apk.py,sha256=IzMQyvFLp9_LJCbeDK09cKkrnQ5iqaCVZhdvRlpoeyg,1399
5
+ app/menu_firmware.py,sha256=sKfgrpk-NN4mIhmNcklTBR33uHzMae8s3Y4NP-4sl_8,1376
6
+ app/setup_config.py,sha256=QGexpQVuRKZfoh4Dy83TxHMRRico5LsyUeezwqpeBVA,12780
7
+ fetchtastic-0.1.0.dist-info/LICENSE,sha256=spPlGh4STUWH8_Wge3VdIWSjXw8Kiroctn19wOrniMI,1067
8
+ fetchtastic-0.1.0.dist-info/METADATA,sha256=PjjSjfUuSjRQkB7_RVHrzyD9ISTKyWNfGRtj2o--O78,3365
9
+ fetchtastic-0.1.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
10
+ fetchtastic-0.1.0.dist-info/entry_points.txt,sha256=U_eZT-XoZI0JWwSus3520FnSoe4ux6F6BsRSJ-nuiZQ,45
11
+ fetchtastic-0.1.0.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
12
+ fetchtastic-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fetchtastic = app.cli:main
@@ -0,0 +1 @@
1
+ app