# -*- coding: utf-8 -*- #!/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. Reads all paths from automation_config.json automatically. USAGE: python3 /volume1/Plex/scripts/nas_setup.py # Install + patch existing rows python3 /volume1/Plex/scripts/nas_setup.py --dry-run # Preview SQL only, no changes python3 /volume1/Plex/scripts/nas_setup.py --remove # Remove installed triggers Run order (IMPORTANT): 1. Stop Plex: synoservice --stop "pkgctl-Plex Media Server" 2. Run this script 3. Start proxy: nohup python3 nas_strm_proxy.py > /tmp/strm_proxy.log 2>&1 & 4. Start Plex: synoservice --start "pkgctl-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("[-] Cannot find " + CONFIG_FILE) print(" 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, proxy_base): strm_root = strm_root.rstrip("/") root_len = len(strm_root) like_pattern = strm_root + "/%.strm" # SQL expression: strip the strm_root prefix, swap .strm -> .mp4, prepend proxy base # Example: /volume1/Plex/Library/Streams/Movies/Film.strm # -> http://127.0.0.1:8086/Movies/Film.mp4 url_expr = "'" + proxy_base + "' || REPLACE(substr(NEW.file, " + str(root_len + 1) + "), '.strm', '.mp4')" insert_sql = ( "CREATE TRIGGER IF NOT EXISTS strm_proxy_insert\n" "AFTER INSERT ON media_parts\n" "WHEN NEW.file LIKE '" + like_pattern + "'\n" "BEGIN\n" " UPDATE media_parts SET file = " + url_expr + " WHERE id = NEW.id;\n" "END;" ) update_sql = ( "CREATE TRIGGER IF NOT EXISTS strm_proxy_update\n" "AFTER UPDATE OF file ON media_parts\n" "WHEN NEW.file LIKE '" + like_pattern + "'\n" "BEGIN\n" " UPDATE media_parts SET file = " + url_expr + " WHERE id = NEW.id;\n" "END;" ) 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"], stderr=subprocess.STDOUT).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("[!] Stop with: synoservice --stop \"pkgctl-Plex Media Server\"") print("=" * 65) ans = raw_input("Continue anyway? (yes/no): ") if sys.version_info[0] < 3 else input("Continue anyway? (yes/no): ") if ans.strip().lower() != "yes": print("[*] Aborted.") sys.exit(0) except (subprocess.CalledProcessError, OSError): pass # pgrep found no match (CalledProcessError) or not installed (OSError) -- assume safe def install_triggers(db_path, strm_root, proxy_base, dry_run): print("\n[*] Database : " + db_path) print("[*] STRM root : " + strm_root) print("[*] Proxy base : " + proxy_base) if not os.path.exists(db_path): print("\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() # Remove old versions 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("[!] Removing old triggers: " + str(existing)) for name in existing: cursor.execute("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("[+] Triggers installed: " + str(installed)) except sqlite3.Error as e: print("[-] SQLite error: " + str(e)) sys.exit(1) def patch_existing(db_path, strm_root, proxy_base, dry_run): """Directly UPDATE existing media_parts rows that still hold raw .strm paths.""" print("\n[*] Patching already-scanned .strm rows in media_parts...") strm_root = strm_root.rstrip("/") root_len = len(strm_root) like = 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("[*] Found " + str(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(" " + file_path) print(" -> " + 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("\n[+] Patched " + str(len(rows)) + " row(s).") else: print("\n[DRY RUN] Would patch " + str(len(rows)) + " row(s) -- no changes made.") conn.close() except sqlite3.Error as e: print("[-] SQLite error during patch: " + str(e)) sys.exit(1) def remove_triggers(db_path): print("[*] Removing STRM proxy triggers...") try: conn = sqlite3.connect(db_path) for name in TRIGGER_NAMES: conn.execute("DROP TRIGGER IF EXISTS " + name) print("[+] Dropped: " + name) conn.commit() conn.close() print("[+] Done. Plex will store raw .strm paths again on next scan.") except sqlite3.Error as e: print("[-] SQLite error: " + str(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 SQL, 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 = "http://127.0.0.1:" + str(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(" 1. Start redirect proxy (keep running permanently):") print(" nohup python3 " + SCRIPT_DIR + "/nas_strm_proxy.py > /tmp/strm_proxy.log 2>&1 &") print() print(" 2. Start Plex Media Server:") print(" synoservice --start \"pkgctl-Plex Media Server\"") print() print(" 3. Run automation to refresh .strm files:") print(" python3 " + SCRIPT_DIR + "/run_automation.py") print("=" * 65) if __name__ == "__main__": main()