#!/usr/bin/env python3 """ nas_strm_proxy.py ================= A lightweight redirect-only HTTP proxy to run permanently on your Synology NAS. How it works: 1. Plex stores proxy URLs like: http://127.0.0.1:8086/Movies/Hindi/Dune Part Two.mp4 (installed by nas_trigger_installer.py via SQLite triggers) 2. When Plex tries to play that URL, it sends a request to THIS server: GET /Movies/Hindi/Dune Part Two.mp4 3. This server: a. Strips .mp4 → adds .strm b. Looks for: /volume1/Plex/Library/Streams/Movies/Hindi/Dune Part Two.strm c. Reads the URL inside that .strm file d. Returns HTTP 302 → the real stream URL (e.g. https://cf.8k.yachts/...) 4. Plex Media Server follows the 302 on the server side. The browser only ever talks HTTPS to Plex — NO mixed content block. USAGE (SSH into Synology): python3 nas_strm_proxy.py python3 nas_strm_proxy.py --port 8086 --strm-root /volume1/Plex/Library/Streams python3 nas_strm_proxy.py --bind 0.0.0.0 # listen on all interfaces (not recommended) To run as a background service on Synology: nohup python3 /volume1/scripts/nas_strm_proxy.py > /volume1/scripts/proxy.log 2>&1 & Or add it to Synology Task Scheduler as a triggered task on boot. """ import argparse import os import re import sys from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import unquote # ───────────────────────────────────────────────────────────────────────────── # DEFAULTS — adjust to match your Synology NAS layout # ───────────────────────────────────────────────────────────────────────────── DEFAULT_STRM_ROOT = "/volume1/Plex/Library/Streams" DEFAULT_BIND_HOST = "127.0.0.1" # loopback only — Plex is on the same machine DEFAULT_PORT = 8086 class StrmRedirectHandler(BaseHTTPRequestHandler): """ Handles GET and HEAD requests from Plex. Reads the target .strm file and returns a 302 redirect to the URL inside it. """ strm_root: str # Set by make_handler() def log_message(self, format, *args): print(f"[{self.address_string()}] {format % args}", flush=True) def do_HEAD(self): self._handle(head_only=True) def do_GET(self): self._handle(head_only=False) def _handle(self, head_only: bool): # 1. Decode the request path raw_path = (self.path or "/").split("?")[0].split("#")[0] try: decoded_path = unquote(raw_path) except Exception: decoded_path = raw_path print(f"\n[→] Request: {'HEAD' if head_only else 'GET'} {decoded_path}", flush=True) # 2. Security: prevent path traversal full_path = os.path.normpath(os.path.join(self.strm_root, decoded_path.lstrip("/"))) if not full_path.startswith(self.strm_root): print(f"[-] Path traversal attempt blocked: {decoded_path}", flush=True) self._send(403, "Forbidden") return # 3. Map .mp4 → .strm (Plex requests .mp4, file on disk is .strm) strm_path = re.sub(r"\.(mp4|mkv|avi|mov|ts)$", ".strm", full_path, flags=re.IGNORECASE) # If Plex somehow sends the .strm extension directly, use as-is if not strm_path.endswith(".strm"): strm_path = full_path + ".strm" print(f"[*] Mapping to: {strm_path}", flush=True) # 4. Check the .strm file exists if not os.path.isfile(strm_path): print(f"[-] .strm file not found: {strm_path}", flush=True) self._send(404, f"STRM file not found: {os.path.basename(strm_path)}") return # 5. Read the URL from the .strm file try: with open(strm_path, "r", encoding="utf-8") as f: stream_url = f.read().strip() except OSError as e: print(f"[-] Cannot read .strm file: {e}", flush=True) self._send(500, f"Cannot read STRM file: {e}") return if not stream_url: print(f"[-] .strm file is empty: {strm_path}", flush=True) self._send(500, "STRM file is empty") return if not (stream_url.startswith("http://") or stream_url.startswith("https://")): print(f"[-] Invalid URL in .strm file: {stream_url[:80]}", flush=True) self._send(500, "STRM file does not contain a valid HTTP/HTTPS URL") return # 6. Return 302 redirect — Plex server will follow this, browser never sees it print(f"[→] 302 Redirect → {stream_url[:100]}", flush=True) self.send_response(302) self.send_header("Location", stream_url) self.send_header("Content-Length", "0") self.send_header("Cache-Control", "no-cache") self.end_headers() def _send(self, code: int, message: str): body = message.encode("utf-8") self.send_response(code) self.send_header("Content-Type", "text/plain; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def make_handler(strm_root: str): """Creates a handler class with the strm_root baked in.""" class Handler(StrmRedirectHandler): pass Handler.strm_root = strm_root return Handler def run_server(host: str, port: int, strm_root: str): strm_root = strm_root.rstrip("/") print("=" * 65) print(" PLEX STRM REDIRECT PROXY (NAS Edition)") print("=" * 65) print(f"[*] Listening on : http://{host}:{port}") print(f"[*] STRM root : {strm_root}") print(f"[*] Behaviour : GET /Movies/Film.mp4") print(f" → reads {strm_root}/Movies/Film.strm") print(f" → 302 redirect → real stream URL") print() if not os.path.isdir(strm_root): print(f"[!] WARNING: STRM root directory not found: {strm_root}") print("[!] The server will start but all requests will return 404.") print("[!] Check your --strm-root path.") print() handler = make_handler(strm_root) server = HTTPServer((host, port), handler) print(f"[+] Server started. Press CTRL+C to stop.\n", flush=True) try: server.serve_forever() except KeyboardInterrupt: print("\n[*] Shutting down proxy server...") server.server_close() def main(): parser = argparse.ArgumentParser( description="Redirect-only HTTP proxy for Plex .strm file playback on Synology NAS." ) parser.add_argument( "--strm-root", default=DEFAULT_STRM_ROOT, help=f"Root directory containing .strm files (default: {DEFAULT_STRM_ROOT})", ) parser.add_argument( "--port", type=int, default=DEFAULT_PORT, help=f"Port to listen on (default: {DEFAULT_PORT})", ) parser.add_argument( "--bind", default=DEFAULT_BIND_HOST, help=f"IP to bind to (default: {DEFAULT_BIND_HOST} — loopback only)", ) args = parser.parse_args() if not os.path.isdir(args.strm_root): print(f"[!] WARNING: STRM root not found: {args.strm_root}") run_server(args.bind, args.port, args.strm_root) if __name__ == "__main__": main()