From 9520a30386d64793a3dcf01fbc7e4e128a31bbec Mon Sep 17 00:00:00 2001 From: vijay Date: Wed, 10 Jun 2026 07:37:35 +0000 Subject: [PATCH] Add plex/nas_setup.py --- plex/nas_setup.py | 257 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 plex/nas_setup.py diff --git a/plex/nas_setup.py b/plex/nas_setup.py new file mode 100644 index 0000000..ca07777 --- /dev/null +++ b/plex/nas_setup.py @@ -0,0 +1,257 @@ +# -*- 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()