From 25f0174c19a233ae206d35d6807ea39cf322731c Mon Sep 17 00:00:00 2001 From: vijay Date: Wed, 10 Jun 2026 07:25:35 +0000 Subject: [PATCH] Update plex/#!/usr/nas_setup.py --- plex/#!/usr/nas_setup.py | 254 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) diff --git a/plex/#!/usr/nas_setup.py b/plex/#!/usr/nas_setup.py index e69de29..91c6a91 100644 --- a/plex/#!/usr/nas_setup.py +++ b/plex/#!/usr/nas_setup.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +""" +nas_setup.py +============ +One-time setup script to run on your Synology NAS via SSH. +Installs SQLite triggers into the Plex database and starts the redirect proxy. + +This script reads automation_config.json for all paths — no arguments needed. + +USAGE: + python /volume1/Plex/scripts/nas_setup.py # Install triggers + patch existing + python /volume1/Plex/scripts/nas_setup.py --dry-run # Preview only, no changes + python /volume1/Plex/scripts/nas_setup.py --remove # Remove triggers + +Run order: + 1. Stop Plex Media Server + 2. Run this script (installs triggers, patches existing .strm rows) + 3. Start nas_strm_proxy.py in the background + 4. Start Plex Media Server +""" + +import os +import sys +import json +import sqlite3 +import argparse +import subprocess + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +CONFIG_FILE = os.path.join(SCRIPT_DIR, "automation_config.json") + +TRIGGER_NAMES = ["strm_proxy_insert", "strm_proxy_update"] + + +def load_config(): + if not os.path.exists(CONFIG_FILE): + print(f"[-] Cannot find {CONFIG_FILE}") + print(f" Make sure this script is in the same folder as automation_config.json") + sys.exit(1) + with open(CONFIG_FILE, "r") as f: + return json.load(f) + + +def build_triggers(strm_root: str, proxy_base: str) -> tuple: + root_len = len(strm_root.rstrip("/")) + like_pattern = f"{strm_root.rstrip('/')}/%.strm" + + # Convert stored NAS path → proxy URL: + # /volume1/Plex/Library/Streams/Movies/Hindi/Film.strm + # → http://127.0.0.1:8086/Movies/Hindi/Film.mp4 + url_expr = f"'{proxy_base}' || REPLACE(substr(NEW.file, {root_len + 1}), '.strm', '.mp4')" + + insert_sql = f""" +CREATE TRIGGER IF NOT EXISTS strm_proxy_insert +AFTER INSERT ON media_parts +WHEN NEW.file LIKE '{like_pattern}' +BEGIN + UPDATE media_parts SET file = {url_expr} WHERE id = NEW.id; +END; +""".strip() + + update_sql = f""" +CREATE TRIGGER IF NOT EXISTS strm_proxy_update +AFTER UPDATE OF file ON media_parts +WHEN NEW.file LIKE '{like_pattern}' +BEGIN + UPDATE media_parts SET file = {url_expr} WHERE id = NEW.id; +END; +""".strip() + + return insert_sql, update_sql + + +def check_plex_running(): + """Warn if Plex appears to be running — writing to a live DB risks corruption.""" + try: + out = subprocess.check_output(["pgrep", "-f", "Plex Media Server"], text=True).strip() + if out: + print("=" * 65) + print("[!] WARNING: Plex Media Server appears to be RUNNING!") + print("[!] Writing to the Plex database while Plex is open can") + print("[!] corrupt it. Stop Plex first, then run this script.") + print("[!] On Synology: synoservice --stop 'pkgctl-Plex Media Server'") + print("=" * 65) + ans = input("Continue anyway? (yes/no): ").strip().lower() + if ans != "yes": + print("[*] Aborted.") + sys.exit(0) + except (subprocess.CalledProcessError, FileNotFoundError): + pass # pgrep not found or no matching process — assume safe + + +def install_triggers(db_path: str, strm_root: str, proxy_base: str, dry_run: bool): + print(f"\n[*] Database : {db_path}") + print(f"[*] STRM root : {strm_root}") + print(f"[*] Proxy base : {proxy_base}") + + if not os.path.exists(db_path): + print(f"\n[-] ERROR: Plex database not found at:\n {db_path}") + print(" Has Plex been started at least once to create its database?") + sys.exit(1) + + insert_sql, update_sql = build_triggers(strm_root, proxy_base) + + if dry_run: + print("\n[DRY RUN] SQL that would be installed:\n") + print("-- INSERT TRIGGER:\n" + insert_sql) + print("\n-- UPDATE TRIGGER:\n" + update_sql) + print("\n[DRY RUN] No changes made.") + return + + try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Drop old triggers if they exist + cursor.execute( + "SELECT name FROM sqlite_master WHERE type='trigger' AND name IN (?,?)", + TRIGGER_NAMES, + ) + existing = [r[0] for r in cursor.fetchall()] + if existing: + print(f"[!] Removing old triggers: {existing}") + for name in existing: + cursor.execute(f"DROP TRIGGER IF EXISTS {name}") + conn.commit() + + print("[*] Installing INSERT trigger...") + cursor.executescript(insert_sql) + print("[*] Installing UPDATE trigger...") + cursor.executescript(update_sql) + conn.commit() + + cursor.execute( + "SELECT name FROM sqlite_master WHERE type='trigger' AND name IN (?,?)", + TRIGGER_NAMES, + ) + installed = [r[0] for r in cursor.fetchall()] + conn.close() + + print(f"[+] Triggers installed: {installed}") + + except sqlite3.Error as e: + print(f"[-] SQLite error: {e}") + sys.exit(1) + + +def patch_existing(db_path: str, strm_root: str, proxy_base: str, dry_run: bool): + """Directly UPDATE existing media_parts rows that still have raw .strm paths.""" + print("\n[*] Patching already-scanned .strm rows in media_parts...") + strm_root = strm_root.rstrip("/") + root_len = len(strm_root) + like = f"{strm_root}/%.strm" + + try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute( + "SELECT id, file FROM media_parts WHERE file LIKE ? AND deleted_at IS NULL", + (like,), + ) + rows = cursor.fetchall() + + if not rows: + print("[*] No unpatched .strm rows found — nothing to do.") + conn.close() + return + + print(f"[*] Found {len(rows)} row(s) to patch:") + for part_id, file_path in rows: + rel = file_path[root_len:] + proxy_url = proxy_base + rel.replace(".strm", ".mp4") + print(f" {file_path}") + print(f" → {proxy_url}") + if not dry_run: + cursor.execute("UPDATE media_parts SET file = ? WHERE id = ?", (proxy_url, part_id)) + + if not dry_run: + conn.commit() + print(f"\n[+] Patched {len(rows)} row(s).") + else: + print(f"\n[DRY RUN] Would patch {len(rows)} row(s) — no changes made.") + + conn.close() + + except sqlite3.Error as e: + print(f"[-] SQLite error during patch: {e}") + sys.exit(1) + + +def remove_triggers(db_path: str): + print("[*] Removing STRM proxy triggers...") + try: + conn = sqlite3.connect(db_path) + for name in TRIGGER_NAMES: + conn.execute(f"DROP TRIGGER IF EXISTS {name}") + print(f"[+] Dropped: {name}") + conn.commit() + conn.close() + print("[+] Done. Plex will now store raw .strm paths again on next scan.") + except sqlite3.Error as e: + print(f"[-] SQLite error: {e}") + sys.exit(1) + + +def main(): + parser = argparse.ArgumentParser( + description="One-time Plex DB trigger installer for NAS-native STRM playback." + ) + parser.add_argument("--dry-run", action="store_true", help="Preview changes, write nothing") + parser.add_argument("--remove", action="store_true", help="Remove installed triggers") + args = parser.parse_args() + + print("=" * 65) + print(" PLEX STRM TRIGGER SETUP") + print("=" * 65) + + cfg = load_config() + + db_path = cfg.get("plex_db_path", ( + "/volume1/Plex/Library/Application Support/" + "Plex Media Server/Plug-in Support/Databases/" + "com.plexapp.plugins.library.db" + )) + strm_root = cfg.get("output_dir", "/volume1/Plex/Library/Streams").rstrip("/") + proxy_port = cfg.get("nas_proxy_port", 8086) + proxy_base = f"http://127.0.0.1:{proxy_port}" + + if args.remove: + remove_triggers(db_path) + return + + check_plex_running() + install_triggers(db_path, strm_root, proxy_base, args.dry_run) + patch_existing(db_path, strm_root, proxy_base, args.dry_run) + + if not args.dry_run: + print() + print("=" * 65) + print("[+] Setup complete!") + print() + print("Next steps:") + print(f" 1. Start the redirect proxy (keep it running permanently):") + print(f" nohup python {SCRIPT_DIR}/nas_strm_proxy.py > /tmp/strm_proxy.log 2>&1 &") + print() + print(f" 2. Start Plex Media Server") + print(f" synoservice --start 'pkgctl-Plex Media Server'") + print() + print(f" 3. Trigger a library scan in Plex, or run run_automation.py") + print("=" * 65) + + +if __name__ == "__main__": + main()