Add plex/nas_strm_proxy.py
This commit is contained in:
parent
9520a30386
commit
fb23fd537a
204
plex/nas_strm_proxy.py
Normal file
204
plex/nas_strm_proxy.py
Normal 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()
|
||||||
Loading…
x
Reference in New Issue
Block a user