Add nas_strm_proxy.py
This commit is contained in:
parent
07a2cfae4b
commit
a712d5da15
193
nas_strm_proxy.py
Normal file
193
nas_strm_proxy.py
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
# -*- 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()
|
||||||
Loading…
x
Reference in New Issue
Block a user