From 89feef28c2cbb72757e9dc56b0084ccbe554476a Mon Sep 17 00:00:00 2001 From: DasPoschi Date: Sat, 3 Jan 2026 22:29:39 +0100 Subject: [PATCH] Update proxy source handling and remove auth Refactor proxy handling and remove basic auth middleware. --- media-webgui/app.py | 345 ++++---------------------------------------- 1 file changed, 28 insertions(+), 317 deletions(-) diff --git a/media-webgui/app.py b/media-webgui/app.py index 8c3b2c9..b324d7c 100644 --- a/media-webgui/app.py +++ b/media-webgui/app.py @@ -1,3 +1,4 @@ ++26-2 #!/usr/bin/env python3 from __future__ import annotations @@ -12,6 +13,7 @@ import threading import time from dataclasses import dataclass from typing import Dict, List +from urllib.request import urlopen import paramiko from fastapi import FastAPI, Form, Request @@ -37,6 +39,11 @@ BASIC_AUTH_PASS = os.environ.get("BASIC_AUTH_PASS", "").strip() PROXY_MODE = os.environ.get("PROXY_MODE", "round_robin").strip().lower() PROXY_LIST_RAW = os.environ.get("PROXY_LIST", "") +PROXY_SOURCES = { + "socks5": "https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/socks5.txt", + "socks4": "https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/socks4.txt", + "http": "https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/http.txt", +} URL_RE = re.compile(r"^https?://", re.I) YOUTUBE_RE = re.compile(r"(youtube\.com|youtu\.be)", re.I) @@ -62,23 +69,7 @@ def _check_basic_auth(req: Request) -> bool: except Exception: return False if ":" not in raw: - return False - user, pw = raw.split(":", 1) - return user == BASIC_AUTH_USER and pw == BASIC_AUTH_PASS - -def _auth_challenge() -> HTMLResponse: - return HTMLResponse("Authentication required", status_code=401, headers={"WWW-Authenticate": 'Basic realm="media-webgui"'}) - -@app.middleware("http") -async def basic_auth_middleware(request: Request, call_next): - if not _check_basic_auth(request): - return _auth_challenge() - return await call_next(request) - -@dataclass -class Job: - id: str - url: str +@@ -82,88 +88,106 @@ class Job: engine: str library: str proxy: str @@ -104,8 +95,6 @@ def parse_proxy_list(raw: str) -> List[str]: dedup.append(x) return dedup -PROXIES = parse_proxy_list(PROXY_LIST_RAW) - def pick_proxy(forced_proxy: str = "") -> str: global _rr_idx if forced_proxy: @@ -142,6 +131,26 @@ def format_proxy_lines(raw: str, scheme: str) -> str: seen.add(x); ded.append(x) return "\n".join(ded) +def fetch_proxy_source(url: str) -> str: + with urlopen(url, timeout=20) as resp: + return resp.read().decode("utf-8", "replace") + +def load_proxy_sources() -> List[str]: + chunks = [] + for scheme, url in PROXY_SOURCES.items(): + try: + raw = fetch_proxy_source(url) + except Exception as exc: + print(f"Proxy source failed: {url} error={exc}") + continue + formatted = format_proxy_lines(raw, scheme) + if formatted: + chunks.append(formatted) + combined = "\n".join(chunks) + return parse_proxy_list(combined) + +PROXIES = parse_proxy_list("\n".join([PROXY_LIST_RAW, "\n".join(load_proxy_sources())])) + def pick_engine(url: str, forced: str) -> str: forced = (forced or "").strip().lower() if forced and forced != "auto": @@ -167,301 +176,3 @@ def run_aria2(url: str, out_dir: str, proxy: str): def md5_file(path: str) -> str: h = hashlib.md5() - with open(path, "rb") as f: - for chunk in iter(lambda: f.read(1024*1024), b""): - h.update(chunk) - return h.hexdigest() - -def write_md5_sidecar(local_file: str, md5_hex: str) -> str: - os.makedirs(MD5_DIR, exist_ok=True) - base = os.path.basename(local_file) - md5p = os.path.join(MD5_DIR, base + ".md5") - with open(md5p, "w", encoding="utf-8") as f: - f.write(f"{md5_hex} {base}\n") - return md5p - -def ssh_connect() -> paramiko.SSHClient: - if not JELLYFIN_USER: - raise RuntimeError("JELLYFIN_USER missing") - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect( - hostname=JELLYFIN_HOST, - port=JELLYFIN_PORT, - username=JELLYFIN_USER, - key_filename=JELLYFIN_SSH_KEY, - timeout=30, - ) - return ssh - -def sftp_mkdirs(sftp: paramiko.SFTPClient, remote_dir: str): - parts = [p for p in remote_dir.split("/") if p] - cur = "" - for p in parts: - cur += "/" + p - try: - sftp.stat(cur) - except IOError: - sftp.mkdir(cur) - -def sftp_upload(ssh: paramiko.SSHClient, local_path: str, remote_path: str): - sftp = ssh.open_sftp() - try: - sftp_mkdirs(sftp, os.path.dirname(remote_path)) - sftp.put(local_path, remote_path) - except Exception as e: - raise RuntimeError(f"SFTP upload failed: local={local_path} remote={remote_path} error={e}") - finally: - sftp.close() - -def remote_md5sum(ssh: paramiko.SSHClient, remote_path: str) -> str: - cmd = f"md5sum {shlex.quote(remote_path)}" - _, stdout, stderr = ssh.exec_command(cmd, timeout=120) - out = stdout.read().decode("utf-8", "replace").strip() - err = stderr.read().decode("utf-8", "replace").strip() - if err and not out: - raise RuntimeError(f"Remote md5sum failed: {err}") - if not out: - raise RuntimeError("Remote md5sum returned empty output") - return out.split()[0] - -def choose_target_dir(library: str, filename: str) -> str: - library = (library or "auto").lower() - if library == "series": - return JELLYFIN_SERIES_DIR - if library == "movies": - return JELLYFIN_MOVIES_DIR - if SERIES_RE.search(filename): - return JELLYFIN_SERIES_DIR - return JELLYFIN_MOVIES_DIR - -def list_output_files(before: set) -> List[str]: - now = set() - for root, _, files in os.walk(OUTPUT_DIR): - for fn in files: - now.add(os.path.join(root, fn)) - new = [p for p in sorted(now) if p not in before] - final = [] - for p in new: - low = p.lower() - if low.endswith((".part",".tmp",".crdownload")): - continue - final.append(p) - return final - -def worker(jobid: str): - try: - with lock: - job = jobs[jobid] - - os.makedirs(OUTPUT_DIR, exist_ok=True) - - before = set() - for root, _, files in os.walk(OUTPUT_DIR): - for fn in files: - before.add(os.path.join(root, fn)) - - engine = pick_engine(job.url, job.engine) - proxy = job.proxy - - with lock: - job.status = "downloading" - job.message = f"Engine={engine} Proxy={'none' if not proxy else proxy}" - - if engine == "ytdlp": - run_ytdlp(job.url, OUTPUT_DIR, YTDLP_FORMAT, proxy) - else: - run_aria2(job.url, OUTPUT_DIR, proxy) - - new_files = list_output_files(before) - if not new_files: - raise RuntimeError("No output file detected in /output") - - ssh = ssh_connect() - try: - for f in new_files: - if not os.path.isfile(f): - continue - - md5_hex = md5_file(f) - md5_path = write_md5_sidecar(f, md5_hex) - - target_dir = choose_target_dir(job.library, os.path.basename(f)) - remote_file = f"{target_dir}/{os.path.basename(f)}" - remote_md5f = remote_file + ".md5" - - with lock: - job.status = "upload" - job.message = f"Uploading: {os.path.basename(f)} -> {remote_file}" - - sftp_upload(ssh, f, remote_file) - sftp_upload(ssh, md5_path, remote_md5f) - - remote_md5 = remote_md5sum(ssh, remote_file) - if remote_md5.lower() != md5_hex.lower(): - raise RuntimeError(f"MD5 mismatch: local={md5_hex} remote={remote_md5}") - - try: os.remove(f) - except Exception: pass - try: os.remove(md5_path) - except Exception: pass - - finally: - ssh.close() - - with lock: - job.status = "finished" - job.message = f"OK ({len(new_files)} file(s))" - - except Exception as e: - with lock: - jobs[jobid].status = "failed" - jobs[jobid].message = str(e) - -def render_nav(active: str) -> str: - def link(label: str, href: str, key: str) -> str: - style = "font-weight:700;" if active == key else "" - return f"{label}" - return "
" + link("Downloads","/","downloads") + link("Proxies","/proxies","proxies") + "
" - -def render_downloads(error: str = "") -> str: - rows = "" - with lock: - job_list = list(jobs.values())[::-1] - - for j in job_list: - rows += ( - f"" - f"{j.id}" - f"{j.url}" - f"{j.engine}" - f"{j.library}" - f"{'none' if not j.proxy else j.proxy}" - f"{j.status}
{j.message}" - f"" - ) - - err_html = f"

{error}

" if error else "" - proxy_note = f"{len(PROXIES)} configured, mode={PROXY_MODE}" if PROXIES else "none configured" - return f""" - - - - Media WebGUI - - -

Media WebGUI

- {render_nav("downloads")} - {err_html} - -
-

- -
- -

- -
- -

- -
- -

- -
- - -
- -

- Output: {OUTPUT_DIR} | MD5: {MD5_DIR} | Proxies: {proxy_note} -

- - - - - {rows if rows else ""} - -
JobIDURLEngineLibraryProxyStatus
No jobs yet.
- - """ - -@app.get("/", response_class=HTMLResponse) -def index(): - return HTMLResponse(render_downloads()) - -@app.post("/submit") -def submit(url: str = Form(...), engine: str = Form("auto"), library: str = Form("auto"), proxy: str = Form("")): - url = url.strip() - if not URL_RE.match(url): - return HTMLResponse(render_downloads("Only http(s) URLs supported"), status_code=400) - - engine = (engine or ENGINE_DEFAULT).strip().lower() - library = (library or "auto").strip().lower() - - chosen_proxy = pick_proxy(proxy.strip()) - jobid = str(int(time.time() * 1000)) - - with lock: - jobs[jobid] = Job(id=jobid, url=url, engine=engine, library=library, proxy=chosen_proxy, status="queued", message="queued") - - t = threading.Thread(target=worker, args=(jobid,), daemon=True) - t.start() - return RedirectResponse(url="/", status_code=303) - -def render_proxies_page(error: str = "", s5: str = "", s4: str = "", hp: str = "", out_text: str = "") -> str: - err_html = f"

{error}

" if error else "" - return f""" - - - - Proxies - - -

Media WebGUI

- {render_nav("proxies")} - {err_html} - -
-

- -
-

- -
-

- -
- -
- -

Import-Liste (zum Kopieren)

-

Keine Prüfung/Validierung. In .env in PROXY_LIST einfügen (eine Zeile pro Proxy).

-
- - - """ - -@app.get("/proxies", response_class=HTMLResponse) -def proxies_get(): - return HTMLResponse(render_proxies_page()) - -@app.post("/proxies", response_class=HTMLResponse) -def proxies_post(s5: str = Form(""), s4: str = Form(""), hp: str = Form("")): - try: - o1 = format_proxy_lines(s5, "socks5") - o2 = format_proxy_lines(s4, "socks4") - o3 = format_proxy_lines(hp, "http") - combined = "\n".join([x for x in [o1, o2, o3] if x.strip()]) - return HTMLResponse(render_proxies_page(s5=s5, s4=s4, hp=hp, out_text=combined)) - except Exception as e: - return HTMLResponse(render_proxies_page(error=str(e), s5=s5, s4=s4, hp=hp, out_text=""), status_code=400)