diff --git a/jd-webgui/app.py b/jd-webgui/app.py index 00dd9fe..3ea154b 100644 --- a/jd-webgui/app.py +++ b/jd-webgui/app.py @@ -18,7 +18,7 @@ from typing import Any, Dict, List, Optional, Tuple from myjdapi import Myjdapi import paramiko from fastapi import FastAPI, Form, Request -from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse from fastapi.staticfiles import StaticFiles # ============================================================ @@ -57,9 +57,12 @@ POLL_SECONDS = float(os.environ.get("POLL_SECONDS", "5")) # JDownloader writes here inside container JD_OUTPUT_PATH = "/output" PROXY_EXPORT_PATH = os.environ.get("PROXY_EXPORT_PATH", "/output/jd-proxies.jdproxies") +LOG_BUFFER_LIMIT = int(os.environ.get("LOG_BUFFER_LIMIT", "500")) URL_RE = re.compile(r"^https?://", re.I) +NO_PROXY_OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({})) + VIDEO_EXTS = { ".mkv", ".mp4", ".m4v", ".avi", ".mov", ".wmv", ".flv", ".webm", ".ts", ".m2ts", ".mts", ".mpg", ".mpeg", ".vob", ".ogv", @@ -123,6 +126,21 @@ class Job: jobs: Dict[str, Job] = {} lock = threading.Lock() +log_lock = threading.Lock() +connection_logs: List[str] = [] + +def log_connection(message: str) -> None: + timestamp = time.strftime("%Y-%m-%d %H:%M:%S") + line = f"[{timestamp}] {message}" + with log_lock: + connection_logs.append(line) + if len(connection_logs) > LOG_BUFFER_LIMIT: + excess = len(connection_logs) - LOG_BUFFER_LIMIT + del connection_logs[:excess] + +def get_connection_logs() -> str: + with log_lock: + return "\n".join(connection_logs) # ============================================================ # Core helpers @@ -149,6 +167,7 @@ def ensure_env(): def get_device(): jd = Myjdapi() + log_connection(f"MyJDownloader connect as {MYJD_EMAIL or 'unknown'}") jd.connect(MYJD_EMAIL, MYJD_PASSWORD) wanted = (MYJD_DEVICE or "").strip() @@ -246,6 +265,7 @@ def ffprobe_ok(path: str) -> bool: def ssh_connect() -> paramiko.SSHClient: ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + log_connection(f"SSH connect {JELLYFIN_USER}@{JELLYFIN_HOST}:{JELLYFIN_PORT}") ssh.connect( hostname=JELLYFIN_HOST, port=JELLYFIN_PORT, @@ -268,6 +288,7 @@ def sftp_mkdirs(sftp: paramiko.SFTPClient, remote_dir: str): def sftp_upload(ssh: paramiko.SSHClient, local_path: str, remote_path: str): sftp = ssh.open_sftp() try: + log_connection(f"SFTP upload {local_path} -> {remote_path}") sftp_mkdirs(sftp, os.path.dirname(remote_path)) sftp.put(local_path, remote_path) finally: @@ -276,6 +297,7 @@ def sftp_upload(ssh: paramiko.SSHClient, local_path: str, remote_path: str): def remote_md5sum(ssh: paramiko.SSHClient, remote_path: str) -> str: quoted = shlex.quote(remote_path) cmd = f"md5sum {quoted}" + log_connection(f"SSH exec {cmd}") stdin, stdout, stderr = ssh.exec_command(cmd, timeout=120) out = stdout.read().decode("utf-8", "replace").strip() err = stderr.read().decode("utf-8", "replace").strip() @@ -290,7 +312,8 @@ def remote_md5sum(ssh: paramiko.SSHClient, remote_path: str) -> str: # ============================================================ def _http_get_json(url: str, headers: Optional[Dict[str, str]] = None) -> Any: req = urllib.request.Request(url, headers=headers or {}) - with urllib.request.urlopen(req, timeout=20) as r: + log_connection(f"HTTP GET {url} (no-proxy)") + with NO_PROXY_OPENER.open(req, timeout=20) as r: return json.loads(r.read().decode("utf-8", "replace")) def tmdb_search_movie(query: str) -> Optional[Dict[str, Any]]: @@ -363,7 +386,8 @@ def format_proxy_lines(raw: str, scheme: str) -> str: def fetch_proxy_list(url: str) -> str: req = urllib.request.Request(url) - with urllib.request.urlopen(req, timeout=20) as resp: + log_connection(f"HTTP GET {url} (no-proxy)") + with NO_PROXY_OPENER.open(req, timeout=20) as resp: return resp.read().decode("utf-8", "replace") def build_jdproxies_payload(text: str) -> Dict[str, Any]: @@ -386,6 +410,8 @@ def build_jdproxies_payload(text: str) -> Dict[str, Any]: "api.jdownloader.org", "", "*.jdownloader.org", + "", + "*.your-server.de", ], "type": "BLACKLIST", } @@ -527,7 +553,8 @@ def jellyfin_refresh_library(): try: url = JELLYFIN_API_BASE + path req = urllib.request.Request(url, headers=headers, method="POST") - with urllib.request.urlopen(req, timeout=20) as r: + log_connection(f"HTTP POST {url} (no-proxy)") + with NO_PROXY_OPENER.open(req, timeout=20) as r: _ = r.read() return except Exception: @@ -825,6 +852,14 @@ def favicon(): def jobs_get(): return HTMLResponse(render_job_rows()) +@app.get("/logs", response_class=HTMLResponse) +def logs_get(): + return HTMLResponse(render_logs_page()) + +@app.get("/logs/data", response_class=PlainTextResponse) +def logs_data(): + return PlainTextResponse(get_connection_logs()) + def render_job_rows() -> str: rows = "" with lock: @@ -936,9 +971,45 @@ def render_nav(active: str) -> str: "
" + link("Downloads", "/", "downloads") + link("Proxies", "/proxies", "proxies") + + link("Logs", "/logs", "logs") + "
" ) +def render_logs_page() -> str: + return f""" + + + + + JD → Jellyfin (Logs) + + + +

JD → Jellyfin

+ {render_nav("logs")} +

Verbindungs-Debugger (Echtzeit). Letzte {LOG_BUFFER_LIMIT} Einträge.

+ + + + """ + def render_proxies_page( error: str = "", message: str = "", diff --git a/jd-webgui/static/style.css b/jd-webgui/static/style.css index ab042f6..b473e61 100644 --- a/jd-webgui/static/style.css +++ b/jd-webgui/static/style.css @@ -19,3 +19,4 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; fo .progress-row { display:flex; align-items:center; gap:8px; margin-top:6px; } .progress-text { font-size:12px; color:#333; min-width:48px; } .inline-form { margin-top:6px; } +.log-area { width:100%; max-width: 920px; padding:10px; border:1px solid #ccc; border-radius:8px; background:#fff; }