# -*- coding: utf-8 -*- """ nas_strm_proxy.py ================= Lightweight redirect-only HTTP proxy for Plex .strm playback on Synology NAS. Compatible with Python 3.5+. How it works: 1. SQLite triggers (installed by nas_setup.py) store proxy URLs in Plex DB: http://127.0.0.1:8086/Movies/Hindi/Film.mp4 2. When Plex plays that item it sends: GET /Movies/Hindi/Film.mp4 to this server 3. This server: a. Strips .mp4 -> adds .strm b. Reads: /volume1/Plex/Library/Streams/Movies/Hindi/Film.strm c. Gets the raw IPTV URL from inside the file d. Returns HTTP 302 -> real stream URL 4. Plex server follows the 302 on the server side. Browser only 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 Run permanently on boot via Synology Task Scheduler (triggered task at boot): nohup python3 /volume1/Plex/scripts/nas_strm_proxy.py > /tmp/strm_proxy.log 2>&1 & """ import argparse import os import re import sys from http.server import BaseHTTPRequestHandler, HTTPServer try: from urllib.parse import unquote except ImportError: from urllib import unquote # Python 2 fallback (not expected) # ----------------------------------------------------------------------- # DEFAULTS -- adjust to match your Synology NAS layout # ----------------------------------------------------------------------- DEFAULT_STRM_ROOT = "/volume1/Plex/Library/Streams" DEFAULT_BIND_HOST = "127.0.0.1" # loopback -- Plex is on the same machine DEFAULT_PORT = 8086 def make_handler(strm_root): """Return a request handler class with strm_root baked in.""" class StrmRedirectHandler(BaseHTTPRequestHandler): def log_message(self, fmt, *args): print("[" + self.address_string() + "] " + (fmt % 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=False): method = "HEAD" if head_only else "GET" # 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("\n[->] Request: " + method + " " + decoded_path, flush=True) # 2. Security: prevent path traversal full_path = os.path.normpath(os.path.join(strm_root, decoded_path.lstrip("/"))) if not full_path.startswith(strm_root): print("[-] Path traversal attempt blocked: " + decoded_path, flush=True) self._send(403, "Forbidden") return # 3. Map .mp4 / .mkv / etc. -> .strm strm_path = re.sub(r"\.(mp4|mkv|avi|mov|ts)$", ".strm", full_path, flags=re.IGNORECASE) if not strm_path.endswith(".strm"): strm_path = full_path + ".strm" print("[*] Mapping to: " + strm_path, flush=True) # 4. Check the .strm file exists if not os.path.isfile(strm_path): print("[-] .strm file not found: " + strm_path, flush=True) self._send(404, "STRM file not found: " + os.path.basename(strm_path)) return # 5. Read the URL from the .strm file try: with open(strm_path, "r") as f: stream_url = f.read().strip() except OSError as e: print("[-] Cannot read .strm file: " + str(e), flush=True) self._send(500, "Cannot read STRM file: " + str(e)) return if not stream_url: print("[-] .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("[-] 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 follows this, browser never sees it print("[->] 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, message): 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) return StrmRedirectHandler def run_server(host, port, strm_root): strm_root = strm_root.rstrip("/") print("=" * 65) print(" PLEX STRM REDIRECT PROXY (NAS Edition)") print("=" * 65) print("[*] Listening on : http://" + host + ":" + str(port)) print("[*] STRM root : " + strm_root) print("[*] Behaviour : GET /Movies/Film.mp4") print(" reads " + strm_root + "/Movies/Film.strm") print(" 302 redirect -> real stream URL") print() if not os.path.isdir(strm_root): print("[!] 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("[+] 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="Root directory containing .strm files (default: " + DEFAULT_STRM_ROOT + ")", ) parser.add_argument( "--port", type=int, default=DEFAULT_PORT, help="Port to listen on (default: " + str(DEFAULT_PORT) + ")", ) parser.add_argument( "--bind", default=DEFAULT_BIND_HOST, help="IP to bind to (default: " + DEFAULT_BIND_HOST + " -- loopback only)", ) args = parser.parse_args() if not os.path.isdir(args.strm_root): print("[!] WARNING: STRM root not found: " + args.strm_root) run_server(args.bind, args.port, args.strm_root) if __name__ == "__main__": main()