commit 863504c789a7c7b3f4619b17128e977baa284081 Author: root Date: Thu Jul 2 05:52:57 2026 +0200 Eerste versie diff --git a/__pycache__/octotrigger.cpython-311.pyc b/__pycache__/octotrigger.cpython-311.pyc new file mode 100644 index 0000000..a8883ba Binary files /dev/null and b/__pycache__/octotrigger.cpython-311.pyc differ diff --git a/cron.log b/cron.log new file mode 100644 index 0000000..0faa211 --- /dev/null +++ b/cron.log @@ -0,0 +1,70 @@ +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +❌ Login Failed: HTTPSConnectionPool(host='navidrome.wouterverduin.nl', port=443): Max retries exceeded with url: /auth/login (Caused by NewConnectionError(': Failed to establish a new connection: [Errno -3] Temporary failure in name resolution')) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +❌ Login Failed: HTTPSConnectionPool(host='navidrome.wouterverduin.nl', port=443): Max retries exceeded with url: /auth/login (Caused by NewConnectionError(': Failed to establish a new connection: [Errno -3] Temporary failure in name resolution')) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) +Sending data to webhook... +✅ Successfully sent to webhook (Status: 200) diff --git a/instructie.txt b/instructie.txt new file mode 100644 index 0000000..c6dab9c --- /dev/null +++ b/instructie.txt @@ -0,0 +1,55 @@ +sudo systemctl daemon-reload +sudo systemctl restart octotrigger + +sudo systemctl restart octotrigger +/etc/systemd/system/octotrigger.service + + +Command: + +curl -X POST http://192.168.1.72:5000/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "playlist_name": "Cyberpunk Industrial Ritual", + "tracks": [ + ["In This Moment", "The In-Between"], + ["In This Moment", "Roots"], + ["Poppy", "Bloodmoney"], + ["Poppy", "Her"], + ["Void of Vision", "ALTAR"] + ] + }' + + +curl -X POST http://192.168.1.72:5000/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "playlist_name": "Melodic Fury & Siren Screams", + "tracks": [ + ["Jinjer", "Pisces"], + ["Amaranthe", "Archangel"], + ["Nightwish", "Noise"], + ["Halestorm", "I Miss The Misery"], + ["In This Moment", "Whore"], + ["Beyond The Black", "Reincarnation"], + ["Ad Infinitum", "Seth"], + ["The Agonist", "Remnants in Time"], + ["Epica", "Abyss of Time - Countdown to Singularity"], + ["New Years Day", "Angel Eyes"], + ["Seven Spires", "Gods of Debauchery"], + ["Delain", "Burning Bridges"], + ["Cellar Darling", "Black Moon"], + ["Battle Beast", "Eye of the Storm"], + ["Eluveitie", "Rebirth"], + ["Enemy Inside", "Falling Away"], + ["Once Human", "Deadlock"], + ["Visions of Atlantis", "Legion of the Seas"], + ["Dream State", "Primordial"], + ["Flyleaf", "I'm So Sick"], + ["Sirenia", "Into the Night"], + ["Eleine", "We Shall Remain"], + ["Xandria", "You Will Never Be Our God"], + ["Leaves' Eyes", "Dark Love Empress"], + ["Ignea", "Nomad's Luck"] + ] + }' diff --git a/last.fm b/last.fm new file mode 100644 index 0000000..ea678b3 --- /dev/null +++ b/last.fm @@ -0,0 +1,4 @@ +api +c436f6b00e30221fe9b791bb1b3204ad +shared secret +5e157ac8fba6c1480caa6bbf03fed58d diff --git a/ocotrigger.log b/ocotrigger.log new file mode 100644 index 0000000..e69de29 diff --git a/octo_backup.py b/octo_backup.py new file mode 100644 index 0000000..9467d5f --- /dev/null +++ b/octo_backup.py @@ -0,0 +1,380 @@ +import requests +import hashlib +import secrets +import time +import sys +import re +import os +import json +import argparse +import logging +from datetime import datetime + +# ========================================== +# CONFIGURATION +# ========================================== + +# Logging Configuration +LOG_FILE = "octotrigger.log" + +# Setup Logger +logger = logging.getLogger("OctoTrigger") +logger.setLevel(logging.INFO) + +# Formatter +formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') + +# File Handler +file_handler = logging.FileHandler(LOG_FILE) +file_handler.setFormatter(formatter) +logger.addHandler(file_handler) + +# Console Handler +stream_handler = logging.StreamHandler(sys.stdout) +stream_handler.setFormatter(formatter) +logger.addHandler(stream_handler) + +# URLs +ND_PROXY_URL = os.getenv("ND_PROXY_URL", "https://ndproxy.wouterverduin.nl") +ND_DIRECT_URL = os.getenv("ND_DIRECT_URL", "https://navidrome.wouterverduin.nl") + +# Credentials (Environment variables preferred, fallback to hardcoded) +ND_USERNAME = os.getenv("ND_USERNAME", "wouser") +ND_PASSWORD = os.getenv("ND_PASSWORD", "G53^cZI3QVOMge*1x3$b%D2bJ$v*D&!") + +# Client identification +CLIENT_NAME = "OctoTriggerScript" +CLIENT_VERSION = "2.1.0" + +# Delays +SCAN_DELAY_SECONDS = 60 +POST_SCAN_WAIT_SECONDS = 30 + +# ========================================== +# AUTH & HELPERS +# ========================================== + +def get_auth_params(): + salt = secrets.token_hex(6) + token = hashlib.md5((ND_PASSWORD + salt).encode('utf-8')).hexdigest() + return { + 'u': ND_USERNAME, + 't': token, + 's': salt, + 'v': '1.16.1', + 'c': CLIENT_NAME, + 'f': 'json' + } + +def validate_input(json_data): + """Validate format of incoming JSON""" + if 'playlist_name' not in json_data: + raise ValueError("Missing 'playlist_name' in JSON") + if 'tracks' not in json_data or not isinstance(json_data['tracks'], list): + raise ValueError("Missing 'tracks' list in JSON") + return True + +def is_local_file(song): + """ + Determines if a song object from the API is a local file. + """ + song_id = song.get('id', '') + path = song.get('path', '').lower() + suffix = song.get('suffix', '').lower() + + local_exts = ('.mp3', '.flac', '.m4a', '.ogg', '.opus', '.wav', '.alac', '.ape') + + # CHECK 1: Valid File Extension in Path or Suffix + if path.endswith(local_exts) or suffix in [e.replace('.', '') for e in local_exts]: + return True + + # CHECK 2: Standard Navidrome MD5 ID (32 chars, hex only) + if len(song_id) == 32 and re.match(r'^[0-9a-fA-F]+$', song_id): + return True + + return False + +def find_best_match(songs, artist, title): + """ + Prioritizes LOCAL matches over remote ones. + """ + artist_clean = artist.lower().strip() + title_clean = title.lower().strip() + + local_matches = [] + remote_matches = [] + + for song in songs: + res_artist = song.get('artist', '').lower() + res_title = song.get('title', '').lower() + + if artist_clean in res_artist and title_clean in res_title: + if is_local_file(song): + local_matches.append(song) + else: + remote_matches.append(song) + + if local_matches: + logger.info(f" [+] Found {len(local_matches)} local candidate(s). Selecting local.") + return local_matches[0] + + if remote_matches: + logger.info(f" [-] No local copy found. Selected remote candidate.") + return remote_matches[0] + + return None + +def search_song(session, artist, title, base_url=ND_PROXY_URL): + query = f"{artist} {title}" + params = get_auth_params() + params['query'] = query + params['songCount'] = 20 + + target_name = "Proxy" if base_url == ND_PROXY_URL else "Direct" + logger.info(f"[*] Searching ({target_name}): {artist} - {title}...") + + try: + url = f"{base_url}/rest/search3" + r = session.get(url, params=params, timeout=15) + r.raise_for_status() + + data = r.json() + subsonic_resp = data.get('subsonic-response', {}) + songs = subsonic_resp.get('searchResult3', {}).get('song', []) + + if not songs: + logger.warning(f" ! No results found for: {artist} - {title}") + return None + + selected_song = find_best_match(songs, artist, title) + + if selected_song: + origin = "LOCAL" if is_local_file(selected_song) else "REMOTE" + logger.info(f" -> Match ({origin}): {selected_song['artist']} - {selected_song['title']} (ID: {selected_song['id']})") + + return selected_song + + except Exception as e: + logger.error(f" ! Error searching: {e}") + return None + +def trigger_stream_download(session, song_id): + params = get_auth_params() + params['id'] = song_id + params['maxBitRate'] = 320 + + logger.info(f" -> Triggering proxy download (Streaming)...") + + try: + url = f"{ND_PROXY_URL}/rest/stream" + + with session.get(url, params=params, stream=True, timeout=20) as r: + r.raise_for_status() + + total_bytes = 0 + for chunk in r.iter_content(chunk_size=8192): + if chunk: + total_bytes += len(chunk) + if total_bytes > 2 * 1024 * 1024: + break + + logger.info(f" [OK] Trigger sent. (Read {total_bytes/1024:.2f} KB)") + return True + + except Exception as e: + logger.error(f" [X] Stream failed: {e}") + return False + +def trigger_navidrome_scan(session): + """ + Triggers a library scan in Navidrome after a delay. + """ + logger.info("=" * 40) + logger.info(f"Waiting {SCAN_DELAY_SECONDS} seconds for downloads to settle before scanning...") + time.sleep(SCAN_DELAY_SECONDS) + + logger.info(f"[*] Triggering Navidrome Library Scan...") + params = get_auth_params() + + try: + url = f"{ND_PROXY_URL}/rest/startScan" + r = session.get(url, params=params, timeout=15) + r.raise_for_status() + + data = r.json() + resp = data.get('subsonic-response', {}) + + if resp.get('status') == 'ok': + scan_status = resp.get('scanStatus', {}) + count = scan_status.get('count', 'Unknown') + logger.info(f" [OK] Scan started successfully. (Current Count: {count})") + + # Post-Scan Wait + logger.info(f"[*] Waiting {POST_SCAN_WAIT_SECONDS} seconds for scan initialization...") + time.sleep(POST_SCAN_WAIT_SECONDS) + logger.info(" [OK] Wait complete.") + + else: + logger.warning(f" [!] Scan trigger request completed but status was not 'ok'.") + + except Exception as e: + logger.error(f" [X] Failed to trigger scan: {e}") + +def update_navidrome_playlist(session, playlist_name, song_ids): + """ + Creates or updates a playlist on the direct Navidrome instance. + """ + logger.info("=" * 40) + logger.info(f"[*] Configuring Playlist: '{playlist_name}'") + logger.info(f" Tracks to add: {len(song_ids)}") + + if not song_ids: + logger.warning(" [!] No tracks to add. Aborting playlist update.") + return + + params = get_auth_params() + + try: + # 1. Check for existing playlist to avoid duplicates + url_get = f"{ND_DIRECT_URL}/rest/getPlaylists" + r = session.get(url_get, params=params, timeout=15) + r.raise_for_status() + + data = r.json() + playlists = data.get('subsonic-response', {}).get('playlists', {}).get('playlist', []) + + target_id = None + for pl in playlists: + if pl.get('name') == playlist_name: + target_id = pl.get('id') + logger.info(f" [i] Found existing playlist ID: {target_id} (Updating)") + break + + if not target_id: + logger.info(f" [i] Playlist does not exist. Creating new.") + + # 2. Create or Update Playlist + params = get_auth_params() + params['name'] = playlist_name + params['songId'] = song_ids # requests handles list -> songId=1&songId=2 + + if target_id: + params['playlistId'] = target_id + + url_create = f"{ND_DIRECT_URL}/rest/createPlaylist" + r = session.get(url_create, params=params, timeout=15) + r.raise_for_status() + + logger.info(f" [OK] Playlist '{playlist_name}' successfully saved.") + + except Exception as e: + logger.error(f" [X] Failed to update playlist: {e}") + +# ========================================== +# MAIN PROCESS EXECUTION +# ========================================== + +def run_process(playlist_data): + """ + Main Logic Function. + Can be called by CLI main() or external Import. + """ + + # 1. Prepare Data + base_playlist_name = playlist_data.get("playlist_name", "Unknown Playlist") + track_list = playlist_data.get("tracks", []) + + # 2. Append Date (DD-MM-YY) + current_date = datetime.now().strftime("%d-%m-%y") + #final_playlist_name = f"{base_playlist_name} ({current_date})" + final_playlist_name = f"{current_date} {base_playlist_name}" #deze zelf gemaakt ipv bovenste + + logger.info(f"--- {CLIENT_NAME} v{CLIENT_VERSION} ---") + logger.info(f"Playlist Target: {final_playlist_name}") + logger.info(f"Tracks Loaded: {len(track_list)}") + + session = requests.Session() + stats = {'cached': 0, 'downloaded': 0, 'failed': 0} + + # --------------------------------------------------------- + # PHASE 1: Download Missing Tracks (Using Proxy) + # --------------------------------------------------------- + logger.info(">>> PHASE 1: Checking and Downloading Tracks") + for artist, title in track_list: + song = search_song(session, artist, title, base_url=ND_PROXY_URL) + + if song: + if is_local_file(song): + logger.info(f" [i] File exists locally.") + stats['cached'] += 1 + else: + logger.info(f" [!] NEW SONG DETECTED") + if trigger_stream_download(session, song['id']): + stats['downloaded'] += 1 + else: + stats['failed'] += 1 + else: + logger.warning(" [X] Not found in Search") + stats['failed'] += 1 + + logger.info("-" * 40) + time.sleep(1) + + logger.info(f"Phase 1 Done: {stats['cached']} Existing, {stats['downloaded']} New, {stats['failed']} Failed.") + + # --------------------------------------------------------- + # PHASE 2: Trigger Scan (If needed) + # --------------------------------------------------------- + if stats['downloaded'] > 0: + trigger_navidrome_scan(session) + else: + logger.info("No new downloads. Skipping library scan.") + + # --------------------------------------------------------- + # PHASE 3: Build Playlist (Using Direct URL) + # --------------------------------------------------------- + logger.info(">>> PHASE 3: Resolving Local IDs for Playlist") + + final_song_ids = [] + + for artist, title in track_list: + song = search_song(session, artist, title, base_url=ND_DIRECT_URL) + + if song: + final_song_ids.append(song['id']) + else: + logger.warning(f" [X] Could not resolve ID for: {artist} - {title}") + + time.sleep(0.5) + + update_navidrome_playlist(session, final_playlist_name, final_song_ids) + + logger.info("--- End of Script ---") + + +def main(): + # Argument Parsing + parser = argparse.ArgumentParser(description="Octo-Fiesta Smart Trigger") + parser.add_argument('json_input', help='JSON string containing playlist name and tracks') + args = parser.parse_args() + + # Load JSON + try: + playlist_data = json.loads(args.json_input) + validate_input(playlist_data) + + # Call the logic + run_process(playlist_data) + + except json.JSONDecodeError as e: + logger.critical(f"Invalid JSON provided: {e}") + sys.exit(1) + except ValueError as e: + logger.critical(f"Invalid Data Structure: {e}") + sys.exit(1) + except Exception as e: + logger.critical(f"Unexpected Error: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/octo_backup2.py b/octo_backup2.py new file mode 100644 index 0000000..e84bcfa --- /dev/null +++ b/octo_backup2.py @@ -0,0 +1,397 @@ +import requests +import hashlib +import secrets +import time +import sys +import re +import os +import json +import argparse +import logging +from datetime import datetime + +# ========================================== +# CONFIGURATION +# ========================================== + +# Logging Configuration +LOG_FILE = "octotrigger.log" + +# Setup Logger +logger = logging.getLogger("OctoTrigger") +logger.setLevel(logging.INFO) + +# Formatter +formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') + +# File Handler +file_handler = logging.FileHandler(LOG_FILE) +file_handler.setFormatter(formatter) +logger.addHandler(file_handler) + +# Console Handler +stream_handler = logging.StreamHandler(sys.stdout) +stream_handler.setFormatter(formatter) +logger.addHandler(stream_handler) + +# URLs +ND_PROXY_URL = os.getenv("ND_PROXY_URL", "https://ndproxy.wouterverduin.nl") +ND_DIRECT_URL = os.getenv("ND_DIRECT_URL", "https://navidrome.wouterverduin.nl") + +# Credentials (Environment variables preferred, fallback to hardcoded) +ND_USERNAME = os.getenv("ND_USERNAME", "wouser") +ND_PASSWORD = os.getenv("ND_PASSWORD", "G53^cZI3QVOMge*1x3$b%D2bJ$v*D&!") + +# Client identification +CLIENT_NAME = "OctoTriggerScript" +CLIENT_VERSION = "2.1.0" + +# Delays +SCAN_DELAY_SECONDS = 60 +POST_SCAN_WAIT_SECONDS = 30 + +# ========================================== +# AUTH & HELPERS +# ========================================== + +def get_auth_params(): + salt = secrets.token_hex(6) + token = hashlib.md5((ND_PASSWORD + salt).encode('utf-8')).hexdigest() + return { + 'u': ND_USERNAME, + 't': token, + 's': salt, + 'v': '1.16.1', + 'c': CLIENT_NAME, + 'f': 'json' + } + +def validate_input(json_data): + """Validate format of incoming JSON""" + if 'playlist_name' not in json_data: + raise ValueError("Missing 'playlist_name' in JSON") + if 'tracks' not in json_data or not isinstance(json_data['tracks'], list): + raise ValueError("Missing 'tracks' list in JSON") + return True + +def is_local_file(song): + """ + Determines if a song object from the API is a local file. + """ + song_id = song.get('id', '') + path = song.get('path', '').lower() + suffix = song.get('suffix', '').lower() + + local_exts = ('.mp3', '.flac', '.m4a', '.ogg', '.opus', '.wav', '.alac', '.ape') + + # CHECK 1: Valid File Extension in Path or Suffix + if path.endswith(local_exts) or suffix in [e.replace('.', '') for e in local_exts]: + return True + + # CHECK 2: Standard Navidrome MD5 ID (32 chars, hex only) + if len(song_id) == 32 and re.match(r'^[0-9a-fA-F]+$', song_id): + return True + + return False + +def find_best_match(songs, artist, title): + """ + Prioritizes LOCAL matches over remote ones. + """ + artist_clean = artist.lower().strip() + title_clean = title.lower().strip() + + local_matches = [] + remote_matches = [] + + for song in songs: + res_artist = song.get('artist', '').lower() + res_title = song.get('title', '').lower() + + if artist_clean in res_artist and title_clean in res_title: + if is_local_file(song): + local_matches.append(song) + else: + remote_matches.append(song) + + if local_matches: + logger.info(f" [+] Found {len(local_matches)} local candidate(s). Selecting local.") + return local_matches[0] + + if remote_matches: + logger.info(f" [-] No local copy found. Selected remote candidate.") + return remote_matches[0] + + return None + +def search_song(session, artist, title, base_url=ND_PROXY_URL): + query = f"{artist} {title}" + params = get_auth_params() + params['query'] = query + params['songCount'] = 20 + + target_name = "Proxy" if base_url == ND_PROXY_URL else "Direct" + logger.info(f"[*] Searching ({target_name}): {artist} - {title}...") + + try: + url = f"{base_url}/rest/search3" + r = session.get(url, params=params, timeout=15) + r.raise_for_status() + + data = r.json() + subsonic_resp = data.get('subsonic-response', {}) + songs = subsonic_resp.get('searchResult3', {}).get('song', []) + + if not songs: + logger.warning(f" ! No results found for: {artist} - {title}") + return None + + selected_song = find_best_match(songs, artist, title) + + if selected_song: + origin = "LOCAL" if is_local_file(selected_song) else "REMOTE" + logger.info(f" -> Match ({origin}): {selected_song['artist']} - {selected_song['title']} (ID: {selected_song['id']})") + + return selected_song + + except Exception as e: + logger.error(f" ! Error searching: {e}") + return None + +def trigger_stream_download(session, song_id): + params = get_auth_params() + params['id'] = song_id + params['maxBitRate'] = 320 + + max_retries = 2 + # The first number is connection timeout, the second is the read timeout (3 mins) + current_timeout = (10, 180) + + for attempt in range(1, max_retries + 1): + logger.info(f" -> Triggering proxy download (Streaming) - Attempt {attempt}/{max_retries}...") + + try: + # We keep stream=True to avoid loading the whole file into RAM + with session.get(ND_PROXY_URL + "/rest/stream", params=params, stream=True, timeout=current_timeout) as r: + r.raise_for_status() + + total_bytes = 0 + # We still want to read the 2MB threshold you originally had to ensure + # the proxy has actually started piping data back to us. + for chunk in r.iter_content(chunk_size=16384): + if chunk: + total_bytes += len(chunk) + if total_bytes > 2 * 1024 * 1024: + break + + logger.info(f" [OK] Trigger successful. (Read {total_bytes/1024:.2f} KB)") + return True + + except (requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError) as e: + logger.warning(f" [!] Attempt {attempt} timed out after {current_timeout[1]} seconds.") + if attempt < max_retries: + wait_time = 15 + logger.info(f" [i] Waiting {wait_time} seconds before retrying...") + time.sleep(wait_time) + # Optionally increase timeout for the second attempt + current_timeout = (10, 300) + else: + logger.error(f" [X] Stream failed after {max_retries} attempts: {e}") + return False + except Exception as e: + logger.error(f" [X] Unexpected error during stream trigger: {e}") + return False + +def trigger_navidrome_scan(session): + """ + Triggers a library scan in Navidrome after a delay. + """ + logger.info("=" * 40) + logger.info(f"Waiting {SCAN_DELAY_SECONDS} seconds for downloads to settle before scanning...") + time.sleep(SCAN_DELAY_SECONDS) + + logger.info(f"[*] Triggering Navidrome Library Scan...") + params = get_auth_params() + + try: + url = f"{ND_PROXY_URL}/rest/startScan" + r = session.get(url, params=params, timeout=15) + r.raise_for_status() + + data = r.json() + resp = data.get('subsonic-response', {}) + + if resp.get('status') == 'ok': + scan_status = resp.get('scanStatus', {}) + count = scan_status.get('count', 'Unknown') + logger.info(f" [OK] Scan started successfully. (Current Count: {count})") + + # Post-Scan Wait + logger.info(f"[*] Waiting {POST_SCAN_WAIT_SECONDS} seconds for scan initialization...") + time.sleep(POST_SCAN_WAIT_SECONDS) + logger.info(" [OK] Wait complete.") + + else: + logger.warning(f" [!] Scan trigger request completed but status was not 'ok'.") + + except Exception as e: + logger.error(f" [X] Failed to trigger scan: {e}") + +def update_navidrome_playlist(session, playlist_name, song_ids): + """ + Creates or updates a playlist on the direct Navidrome instance. + """ + logger.info("=" * 40) + logger.info(f"[*] Configuring Playlist: '{playlist_name}'") + logger.info(f" Tracks to add: {len(song_ids)}") + + if not song_ids: + logger.warning(" [!] No tracks to add. Aborting playlist update.") + return + + params = get_auth_params() + + try: + # 1. Check for existing playlist to avoid duplicates + url_get = f"{ND_DIRECT_URL}/rest/getPlaylists" + r = session.get(url_get, params=params, timeout=15) + r.raise_for_status() + + data = r.json() + playlists = data.get('subsonic-response', {}).get('playlists', {}).get('playlist', []) + + target_id = None + for pl in playlists: + if pl.get('name') == playlist_name: + target_id = pl.get('id') + logger.info(f" [i] Found existing playlist ID: {target_id} (Updating)") + break + + if not target_id: + logger.info(f" [i] Playlist does not exist. Creating new.") + + # 2. Create or Update Playlist + params = get_auth_params() + params['name'] = playlist_name + params['songId'] = song_ids # requests handles list -> songId=1&songId=2 + + if target_id: + params['playlistId'] = target_id + + url_create = f"{ND_DIRECT_URL}/rest/createPlaylist" + r = session.get(url_create, params=params, timeout=15) + r.raise_for_status() + + logger.info(f" [OK] Playlist '{playlist_name}' successfully saved.") + + except Exception as e: + logger.error(f" [X] Failed to update playlist: {e}") + +# ========================================== +# MAIN PROCESS EXECUTION +# ========================================== + +def run_process(playlist_data): + """ + Main Logic Function. + Can be called by CLI main() or external Import. + """ + + # 1. Prepare Data + base_playlist_name = playlist_data.get("playlist_name", "Unknown Playlist") + track_list = playlist_data.get("tracks", []) + + # 2. Append Date (DD-MM-YY) + current_date = datetime.now().strftime("%d-%m-%y") + #final_playlist_name = f"{base_playlist_name} ({current_date})" + final_playlist_name = f"{current_date} {base_playlist_name}" #deze zelf gemaakt ipv bovenste + + logger.info(f"--- {CLIENT_NAME} v{CLIENT_VERSION} ---") + logger.info(f"Playlist Target: {final_playlist_name}") + logger.info(f"Tracks Loaded: {len(track_list)}") + + session = requests.Session() + stats = {'cached': 0, 'downloaded': 0, 'failed': 0} + + # --------------------------------------------------------- + # PHASE 1: Download Missing Tracks (Using Proxy) + # --------------------------------------------------------- + logger.info(">>> PHASE 1: Checking and Downloading Tracks") + for artist, title in track_list: + song = search_song(session, artist, title, base_url=ND_PROXY_URL) + + if song: + if is_local_file(song): + logger.info(f" [i] File exists locally.") + stats['cached'] += 1 + else: + logger.info(f" [!] NEW SONG DETECTED") + if trigger_stream_download(session, song['id']): + stats['downloaded'] += 1 + else: + stats['failed'] += 1 + else: + logger.warning(" [X] Not found in Search") + stats['failed'] += 1 + + logger.info("-" * 40) + time.sleep(1) + + logger.info(f"Phase 1 Done: {stats['cached']} Existing, {stats['downloaded']} New, {stats['failed']} Failed.") + + # --------------------------------------------------------- + # PHASE 2: Trigger Scan (If needed) + # --------------------------------------------------------- + if stats['downloaded'] > 0: + trigger_navidrome_scan(session) + else: + logger.info("No new downloads. Skipping library scan.") + + # --------------------------------------------------------- + # PHASE 3: Build Playlist (Using Direct URL) + # --------------------------------------------------------- + logger.info(">>> PHASE 3: Resolving Local IDs for Playlist") + + final_song_ids = [] + + for artist, title in track_list: + song = search_song(session, artist, title, base_url=ND_DIRECT_URL) + + if song: + final_song_ids.append(song['id']) + else: + logger.warning(f" [X] Could not resolve ID for: {artist} - {title}") + + time.sleep(0.5) + + update_navidrome_playlist(session, final_playlist_name, final_song_ids) + + logger.info("--- End of Script ---") + + +def main(): + # Argument Parsing + parser = argparse.ArgumentParser(description="Octo-Fiesta Smart Trigger") + parser.add_argument('json_input', help='JSON string containing playlist name and tracks') + args = parser.parse_args() + + # Load JSON + try: + playlist_data = json.loads(args.json_input) + validate_input(playlist_data) + + # Call the logic + run_process(playlist_data) + + except json.JSONDecodeError as e: + logger.critical(f"Invalid JSON provided: {e}") + sys.exit(1) + except ValueError as e: + logger.critical(f"Invalid Data Structure: {e}") + sys.exit(1) + except Exception as e: + logger.critical(f"Unexpected Error: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/octotrigger.py b/octotrigger.py new file mode 100644 index 0000000..9fc2ffb --- /dev/null +++ b/octotrigger.py @@ -0,0 +1,411 @@ +import requests +import hashlib +import secrets +import time +import sys +import re +import os +import json +import argparse +import logging +from datetime import datetime + +# ========================================== +# CONFIGURATION +# ========================================== + +# Logging Configuration + +# Get the absolute path of the directory where this script lives +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +LOG_FILE = os.path.join(BASE_DIR, "octotrigger.log") +#LOG_FILE = "octotrigger.log" Vorige regel + +# Setup Logger +logger = logging.getLogger("OctoTrigger") +logger.setLevel(logging.INFO) + +# Formatter +formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') + +# File Handler +file_handler = logging.FileHandler(LOG_FILE) +file_handler.setFormatter(formatter) +logger.addHandler(file_handler) + +# Console Handler +stream_handler = logging.StreamHandler(sys.stdout) +stream_handler.setFormatter(formatter) +logger.addHandler(stream_handler) + +# URLs +ND_PROXY_URL = os.getenv("ND_PROXY_URL", "https://ndproxy.wouterverduin.nl") +ND_DIRECT_URL = os.getenv("ND_DIRECT_URL", "https://navidrome.wouterverduin.nl") + +# Credentials (Environment variables preferred, fallback to hardcoded) +ND_USERNAME = os.getenv("ND_USERNAME", "wouser") +ND_PASSWORD = os.getenv("ND_PASSWORD", "G53^cZI3QVOMge*1x3$b%D2bJ$v*D&!") + +# Client identification +CLIENT_NAME = "OctoTriggerScript" +CLIENT_VERSION = "2.1.0" + +# Delays +SCAN_DELAY_SECONDS = 60 +POST_SCAN_WAIT_SECONDS = 30 + +# ========================================== +# AUTH & HELPERS +# ========================================== + +def get_auth_params(): + salt = secrets.token_hex(6) + token = hashlib.md5((ND_PASSWORD + salt).encode('utf-8')).hexdigest() + return { + 'u': ND_USERNAME, + 't': token, + 's': salt, + 'v': '1.16.1', + 'c': CLIENT_NAME, + 'f': 'json' + } + +def validate_input(json_data): + """Validate format of incoming JSON""" + if 'playlist_name' not in json_data: + raise ValueError("Missing 'playlist_name' in JSON") + if 'tracks' not in json_data or not isinstance(json_data['tracks'], list): + raise ValueError("Missing 'tracks' list in JSON") + return True + +def is_local_file(song): + """ + Determines if a song object from the API is a local file. + """ + song_id = song.get('id', '') + path = song.get('path', '').lower() + suffix = song.get('suffix', '').lower() + + local_exts = ('.mp3', '.flac', '.m4a', '.ogg', '.opus', '.wav', '.alac', '.ape') + + # CHECK 1: Valid File Extension in Path or Suffix + if path.endswith(local_exts) or suffix in [e.replace('.', '') for e in local_exts]: + return True + + # CHECK 2: Standard Navidrome MD5 ID (32 chars, hex only) + if len(song_id) == 32 and re.match(r'^[0-9a-fA-F]+$', song_id): + return True + + return False + +def find_best_match(songs, artist, title): + """ + Prioritizes LOCAL matches over remote ones. + """ + artist_clean = artist.lower().strip() + title_clean = title.lower().strip() + + local_matches = [] + remote_matches = [] + + for song in songs: + res_artist = song.get('artist', '').lower() + res_title = song.get('title', '').lower() + + if artist_clean in res_artist and title_clean in res_title: + if is_local_file(song): + local_matches.append(song) + else: + remote_matches.append(song) + + if local_matches: + logger.info(f" [+] Found {len(local_matches)} local candidate(s). Selecting local.") + return local_matches[0] + + if remote_matches: + logger.info(f" [-] No local copy found. Selected remote candidate.") + return remote_matches[0] + + return None + +def search_song(session, artist, title, base_url=ND_PROXY_URL): + query = f"{artist} {title}" + params = get_auth_params() + params['query'] = query + params['songCount'] = 20 + + target_name = "Proxy" if base_url == ND_PROXY_URL else "Direct" + logger.info(f"[*] Searching ({target_name}): {artist} - {title}...") + + try: + url = f"{base_url}/rest/search3" + r = session.get(url, params=params, timeout=15) + r.raise_for_status() + + data = r.json() + subsonic_resp = data.get('subsonic-response', {}) + songs = subsonic_resp.get('searchResult3', {}).get('song', []) + + if not songs: + logger.warning(f" ! No results found for: {artist} - {title}") + return None + + selected_song = find_best_match(songs, artist, title) + + if selected_song: + origin = "LOCAL" if is_local_file(selected_song) else "REMOTE" + logger.info(f" -> Match ({origin}): {selected_song['artist']} - {selected_song['title']} (ID: {selected_song['id']})") + + return selected_song + + except Exception as e: + logger.error(f" ! Error searching: {e}") + return None + +def trigger_stream_download(session, song_id): + params = get_auth_params() + params['id'] = song_id + params['maxBitRate'] = 320 + + max_retries = 2 + current_timeout = (10, 480) # Increased to 8 mins based on your album download info + + for attempt in range(1, max_retries + 1): + logger.info(f" -> Triggering proxy download (Streaming) - Attempt {attempt}/{max_retries}...") + + try: + # Note: We use stream=True. + # If the server returns a 500, raise_for_status() will trigger the except block. + with session.get(ND_PROXY_URL + "/rest/stream", params=params, stream=True, timeout=current_timeout) as r: + + # If the proxy returns 500, this will raise a requests.exceptions.HTTPError + r.raise_for_status() + + total_bytes = 0 + for chunk in r.iter_content(chunk_size=32768): + if chunk: + total_bytes += len(chunk) + if total_bytes > 2 * 1024 * 1024: + break + + logger.info(f" [OK] Trigger successful. (Read {total_bytes/1024:.2f} KB)") + return True + + except requests.exceptions.HTTPError as e: + # Handle the 500 error specifically + status_code = e.response.status_code + logger.warning(f" [!] Proxy returned {status_code} Server Error for this track.") + + if attempt < max_retries: + # If it's a 500, the proxy might be overwhelmed. Wait longer before retry. + wait_time = 30 + logger.info(f" [i] Waiting {wait_time}s to let proxy recover...") + time.sleep(wait_time) + else: + logger.error(f" [X] Skipping song: Proxy failed after {max_retries} attempts.") + return False + + except (requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError) as e: + logger.warning(f" [!] Attempt {attempt} timed out.") + if attempt < max_retries: + time.sleep(15) + current_timeout = (10, 600) # Give it 10 mins on second attempt + else: + return False + except Exception as e: + logger.error(f" [X] Unexpected error: {e}") + return False + +def trigger_navidrome_scan(session): + """ + Triggers a library scan in Navidrome after a delay. + """ + logger.info("=" * 40) + logger.info(f"Waiting {SCAN_DELAY_SECONDS} seconds for downloads to settle before scanning...") + time.sleep(SCAN_DELAY_SECONDS) + + logger.info(f"[*] Triggering Navidrome Library Scan...") + params = get_auth_params() + + try: + url = f"{ND_PROXY_URL}/rest/startScan" + r = session.get(url, params=params, timeout=15) + r.raise_for_status() + + data = r.json() + resp = data.get('subsonic-response', {}) + + if resp.get('status') == 'ok': + scan_status = resp.get('scanStatus', {}) + count = scan_status.get('count', 'Unknown') + logger.info(f" [OK] Scan started successfully. (Current Count: {count})") + + # Post-Scan Wait + logger.info(f"[*] Waiting {POST_SCAN_WAIT_SECONDS} seconds for scan initialization...") + time.sleep(POST_SCAN_WAIT_SECONDS) + logger.info(" [OK] Wait complete.") + + else: + logger.warning(f" [!] Scan trigger request completed but status was not 'ok'.") + + except Exception as e: + logger.error(f" [X] Failed to trigger scan: {e}") + +def update_navidrome_playlist(session, playlist_name, song_ids): + """ + Creates or updates a playlist on the direct Navidrome instance. + """ + logger.info("=" * 40) + logger.info(f"[*] Configuring Playlist: '{playlist_name}'") + logger.info(f" Tracks to add: {len(song_ids)}") + + if not song_ids: + logger.warning(" [!] No tracks to add. Aborting playlist update.") + return + + params = get_auth_params() + + try: + # 1. Check for existing playlist to avoid duplicates + url_get = f"{ND_DIRECT_URL}/rest/getPlaylists" + r = session.get(url_get, params=params, timeout=15) + r.raise_for_status() + + data = r.json() + playlists = data.get('subsonic-response', {}).get('playlists', {}).get('playlist', []) + + target_id = None + for pl in playlists: + if pl.get('name') == playlist_name: + target_id = pl.get('id') + logger.info(f" [i] Found existing playlist ID: {target_id} (Updating)") + break + + if not target_id: + logger.info(f" [i] Playlist does not exist. Creating new.") + + # 2. Create or Update Playlist + params = get_auth_params() + params['name'] = playlist_name + params['songId'] = song_ids # requests handles list -> songId=1&songId=2 + + if target_id: + params['playlistId'] = target_id + + url_create = f"{ND_DIRECT_URL}/rest/createPlaylist" + r = session.get(url_create, params=params, timeout=15) + r.raise_for_status() + + logger.info(f" [OK] Playlist '{playlist_name}' successfully saved.") + + except Exception as e: + logger.error(f" [X] Failed to update playlist: {e}") + +# ========================================== +# MAIN PROCESS EXECUTION +# ========================================== + +def run_process(playlist_data): + """ + Main Logic Function. + Can be called by CLI main() or external Import. + """ + + # 1. Prepare Data + base_playlist_name = playlist_data.get("playlist_name", "Unknown Playlist") + track_list = playlist_data.get("tracks", []) + + # 2. Append Date (DD-MM-YY) + current_date = datetime.now().strftime("%d-%m-%y") + #final_playlist_name = f"{base_playlist_name} ({current_date})" + final_playlist_name = f"{current_date} {base_playlist_name}" #deze zelf gemaakt ipv bovenste + + logger.info(f"--- {CLIENT_NAME} v{CLIENT_VERSION} ---") + logger.info(f"Playlist Target: {final_playlist_name}") + logger.info(f"Tracks Loaded: {len(track_list)}") + + session = requests.Session() + stats = {'cached': 0, 'downloaded': 0, 'failed': 0} + + # --------------------------------------------------------- + # PHASE 1: Download Missing Tracks (Using Proxy) + # --------------------------------------------------------- + logger.info(">>> PHASE 1: Checking and Downloading Tracks") + for artist, title in track_list: + song = search_song(session, artist, title, base_url=ND_PROXY_URL) + + if song: + if is_local_file(song): + logger.info(f" [i] File exists locally.") + stats['cached'] += 1 + else: + logger.info(f" [!] NEW SONG DETECTED") + if trigger_stream_download(session, song['id']): + stats['downloaded'] += 1 + else: + stats['failed'] += 1 + else: + logger.warning(" [X] Not found in Search") + stats['failed'] += 1 + + logger.info("-" * 40) + time.sleep(1) + + logger.info(f"Phase 1 Done: {stats['cached']} Existing, {stats['downloaded']} New, {stats['failed']} Failed.") + + # --------------------------------------------------------- + # PHASE 2: Trigger Scan (If needed) + # --------------------------------------------------------- + if stats['downloaded'] > 0: + trigger_navidrome_scan(session) + else: + logger.info("No new downloads. Skipping library scan.") + + # --------------------------------------------------------- + # PHASE 3: Build Playlist (Using Direct URL) + # --------------------------------------------------------- + logger.info(">>> PHASE 3: Resolving Local IDs for Playlist") + + final_song_ids = [] + + for artist, title in track_list: + song = search_song(session, artist, title, base_url=ND_DIRECT_URL) + + if song: + final_song_ids.append(song['id']) + else: + logger.warning(f" [X] Could not resolve ID for: {artist} - {title}") + + time.sleep(0.5) + + update_navidrome_playlist(session, final_playlist_name, final_song_ids) + + logger.info("--- End of Script ---") + + +def main(): + # Argument Parsing + parser = argparse.ArgumentParser(description="Octo-Fiesta Smart Trigger") + parser.add_argument('json_input', help='JSON string containing playlist name and tracks') + args = parser.parse_args() + + # Load JSON + try: + playlist_data = json.loads(args.json_input) + validate_input(playlist_data) + + # Call the logic + run_process(playlist_data) + + except json.JSONDecodeError as e: + logger.critical(f"Invalid JSON provided: {e}") + sys.exit(1) + except ValueError as e: + logger.critical(f"Invalid Data Structure: {e}") + sys.exit(1) + except Exception as e: + logger.critical(f"Unexpected Error: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..0bf9654 --- /dev/null +++ b/readme.md @@ -0,0 +1,83 @@ +Van dit project is een service gemaakt: + +cat /etc/systemd/system/octotrigger.service +[Unit] +Description=OctoTrigger Web Listener +After=network.target + +[Service] +# Replace with your actual username (e.g., pi, root, wouter) +User=root +Group=root + +# Replace with the output of 'pwd' +WorkingDirectory=/root/octotrigger + +# Replace with output of 'which python3' and your script path +ExecStart=/usr/bin/python3 -u server.py + +# Restart automatically if it crashes +Restart=always +RestartSec=5 + +# Output logs to system journal +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target + +Relevante bijbehorende commandos bij de service: +sudo systemctl daemon-reload +sudo systemctl restart octotrigger +/etc/systemd/system/octotrigger.service + + +Twee voorbeeld commandos om de service aan te roepen: + +curl -X POST http://192.168.1.72:5000/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "playlist_name": "Cyberpunk Industrial Ritual", + "tracks": [ + ["In This Moment", "The In-Between"], + ["In This Moment", "Roots"], + ["Poppy", "Bloodmoney"], + ["Poppy", "Her"], + ["Void of Vision", "ALTAR"] + ] + }' + + +curl -X POST http://192.168.1.72:5000/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "playlist_name": "Melodic Fury & Siren Screams", + "tracks": [ + ["Jinjer", "Pisces"], + ["Amaranthe", "Archangel"], + ["Nightwish", "Noise"], + ["Halestorm", "I Miss The Misery"], + ["In This Moment", "Whore"], + ["Beyond The Black", "Reincarnation"], + ["Ad Infinitum", "Seth"], + ["The Agonist", "Remnants in Time"], + ["Epica", "Abyss of Time - Countdown to Singularity"], + ["New Years Day", "Angel Eyes"], + ["Seven Spires", "Gods of Debauchery"], + ["Delain", "Burning Bridges"], + ["Cellar Darling", "Black Moon"], + ["Battle Beast", "Eye of the Storm"], + ["Eluveitie", "Rebirth"], + ["Enemy Inside", "Falling Away"], + ["Once Human", "Deadlock"], + ["Visions of Atlantis", "Legion of the Seas"], + ["Dream State", "Primordial"], + ["Flyleaf", "I'm So Sick"], + ["Sirenia", "Into the Night"], + ["Eleine", "We Shall Remain"], + ["Xandria", "You Will Never Be Our God"], + ["Leaves' Eyes", "Dark Love Empress"], + ["Ignea", "Nomad's Luck"] + ] + }' diff --git a/send.py b/send.py new file mode 100755 index 0000000..7b4c079 --- /dev/null +++ b/send.py @@ -0,0 +1,116 @@ +import requests +import json + +# ========================================== +# CONFIGURATION +# ========================================== +ND_PASSWORD = "G53^cZI3QVOMge*1x3$b%D2bJ$v*D&!" +ND_DIRECT_URL = "https://navidrome.wouterverduin.nl" +ND_USERNAME = "wouser" + +# The webhook where data will be sent +#WEBHOOK_URL = "https://n8n.wouterverduin.nl/webhook-test/c3129d3a-dca8-43ae-8003-8d94b7f9097d" +WEBHOOK_URL = "https://n8n.wouterverduin.nl/webhook/c3129d3a-dca8-43ae-8003-8d94b7f9097d" + +# ========================================== +# INTERNAL API AUTHENTICATION +# ========================================== +def get_auth_token(): + auth_url = f"{ND_DIRECT_URL}/auth/login" + credentials = { + "username": ND_USERNAME, + "password": ND_PASSWORD + } + + try: + response = requests.post(auth_url, json=credentials) + response.raise_for_status() + return response.json().get('token') + except requests.exceptions.RequestException as e: + print(f"❌ Login Failed: {e}") + return None + +# ========================================== +# DATA RETRIEVAL +# ========================================== +def get_song_stats(token): + headers = { + 'x-nd-authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + api_url = f"{ND_DIRECT_URL}/api/song" + + payload = { + "recently_played": [], + "most_played": [] + } + + # 1. Get Recently Played Songs + recent_params = { + '_end': 10, + '_order': 'DESC', + '_sort': 'play_date', + '_start': 0 + } + + try: + res = requests.get(api_url, headers=headers, params=recent_params) + res.raise_for_status() + songs = res.json() + for song in songs: + payload["recently_played"].append({ + "artist": song.get('artist', 'Unknown'), + "title": song.get('title', 'Unknown') + }) + except Exception as e: + print(f"Error fetching recent songs: {e}") + + # 2. Get Most Played Songs (All Time) + top_params = { + '_end': 10, + '_order': 'DESC', + '_sort': 'play_count', + '_start': 0 + } + + try: + res = requests.get(api_url, headers=headers, params=top_params) + res.raise_for_status() + songs = res.json() + for song in songs: + payload["most_played"].append({ + "artist": song.get('artist', 'Unknown'), + "title": song.get('title', 'Unknown') + }) + except Exception as e: + print(f"Error fetching top songs: {e}") + + return payload + +# ========================================== +# WEBHOOK DELIVERY +# ========================================== +def send_to_webhook(data): + try: + print(f"Sending data to webhook...") + response = requests.post(WEBHOOK_URL, json=data) + response.raise_for_status() + print(f"✅ Successfully sent to webhook (Status: {response.status_code})") + except Exception as e: + print(f"❌ Failed to send to webhook: {e}") + +# ========================================== +# MAIN +# ========================================== +if __name__ == "__main__": + if ND_PASSWORD == "YOUR_NEW_PASSWORD_HERE": + print("⚠️ Please update the ND_PASSWORD variable in the script first.") + else: + token = get_auth_token() + + if token: + # Gather the data + stats_data = get_song_stats(token) + + # Send the data to n8n + send_to_webhook(stats_data) diff --git a/server.py b/server.py new file mode 100644 index 0000000..82b112e --- /dev/null +++ b/server.py @@ -0,0 +1,47 @@ +from flask import Flask, request, jsonify +import logging +import threading + +# Import your existing script +# NOTE: Ensure octotrigger.py is in the same folder +import octotrigger + +# Add this to server.py to see if the logger is alive +logger = logging.getLogger("OctoTrigger") + +app = Flask(__name__) + +@app.route('/trigger', methods=['POST']) +def trigger_download(): + # 1. Get JSON from the request + try: + data = request.get_json(force=True) + logger.info(f"Received request for playlist: {data.get('playlist_name')}") + # Validate input using the function we wrote in octotrigger.py + octotrigger.validate_input(data) + except Exception as e: + return jsonify({"status": "error", "message": str(e)}), 400 + + # 2. Run the script logic in a background thread + # (so the web request doesn't time out while downloading) + def run_job(json_data): + # We need to hack sys.argv or modify main() to accept args directly. + # The cleanest way without rewriting octotrigger.py is to modify + # octotrigger.py slightly to accept a dict in main(), + # but here is a wrapper approach: + + # We will create a temporary version of the main logic here + # Or ideally, modify octotrigger.py's main to look like this: + # def run(playlist_data): ... + + # For now, let's assume we update octotrigger.py slightly (see below) + octotrigger.run_process(json_data) + + thread = threading.Thread(target=run_job, args=(data,)) + thread.start() + + return jsonify({"status": "accepted", "message": "Job started in background"}), 202 + +if __name__ == '__main__': + # Listen on all network interfaces (0.0.0.0) on port 5000 + app.run(host='0.0.0.0', port=5000)