Add plex/nas_strm_files.py

This commit is contained in:
vijay 2026-06-10 07:26:37 +00:00
parent 25f0174c19
commit 39427df4fa

204
plex/nas_strm_files.py Normal file
View File

@ -0,0 +1,204 @@
#!/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()