feat: pre-download filter for videos >= MIN_VIDEO_SIZE_MB (default 200 MB)
This commit is contained in:
120
jd-webgui/app.py
120
jd-webgui/app.py
@@ -57,6 +57,8 @@ BASIC_AUTH_USER = os.environ.get("BASIC_AUTH_USER", "")
|
|||||||
BASIC_AUTH_PASS = os.environ.get("BASIC_AUTH_PASS", "")
|
BASIC_AUTH_PASS = os.environ.get("BASIC_AUTH_PASS", "")
|
||||||
|
|
||||||
POLL_SECONDS = float(os.environ.get("POLL_SECONDS", "5"))
|
POLL_SECONDS = float(os.environ.get("POLL_SECONDS", "5"))
|
||||||
|
MIN_VIDEO_SIZE_MB = int(os.environ.get("MIN_VIDEO_SIZE_MB", "200"))
|
||||||
|
MIN_VIDEO_BYTES = MIN_VIDEO_SIZE_MB * 1024 * 1024
|
||||||
|
|
||||||
# JDownloader writes here inside container
|
# JDownloader writes here inside container
|
||||||
JD_OUTPUT_PATH = "/output"
|
JD_OUTPUT_PATH = "/output"
|
||||||
@@ -830,6 +832,70 @@ def calculate_progress(links: List[Dict[str, Any]]) -> float:
|
|||||||
return 0.0
|
return 0.0
|
||||||
return max(0.0, min(100.0, (loaded / total) * 100.0))
|
return max(0.0, min(100.0, (loaded / total) * 100.0))
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Linkgrabber filter (pre-download)
|
||||||
|
# ============================================================
|
||||||
|
def _filter_linkgrabber(dev, jobid: str) -> Tuple[int, int]:
|
||||||
|
"""Wait for link crawler, then remove non-video and sub-minimum-size links.
|
||||||
|
Returns (accepted, rejected) counts."""
|
||||||
|
deadline = time.time() + 120
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
crawlers = dev.linkgrabber.query_link_crawlers([{"collectorInfo": True}]) or []
|
||||||
|
if not any(c.get("crawling") for c in crawlers):
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
links = []
|
||||||
|
try:
|
||||||
|
links = dev.linkgrabber.query_links([{
|
||||||
|
"jobUUIDs": [int(jobid)] if str(jobid).isdigit() else [jobid],
|
||||||
|
"maxResults": -1,
|
||||||
|
"startAt": 0,
|
||||||
|
"name": True,
|
||||||
|
"size": True,
|
||||||
|
"uuid": True,
|
||||||
|
"packageUUID": True,
|
||||||
|
}]) or []
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
to_remove_ids = []
|
||||||
|
keep_ids = []
|
||||||
|
keep_pkg_ids = set()
|
||||||
|
for link in links:
|
||||||
|
name = link.get("name", "")
|
||||||
|
size = link.get("size", -1)
|
||||||
|
_, ext = os.path.splitext(name.lower())
|
||||||
|
is_video = ext in VIDEO_EXTS
|
||||||
|
big_enough = size < 0 or size >= MIN_VIDEO_BYTES
|
||||||
|
if is_video and big_enough:
|
||||||
|
keep_ids.append(link.get("uuid"))
|
||||||
|
if link.get("packageUUID") is not None:
|
||||||
|
keep_pkg_ids.add(link.get("packageUUID"))
|
||||||
|
else:
|
||||||
|
to_remove_ids.append(link.get("uuid"))
|
||||||
|
|
||||||
|
if to_remove_ids:
|
||||||
|
try:
|
||||||
|
dev.linkgrabber.remove_links(link_ids=to_remove_ids, package_ids=[])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if keep_ids:
|
||||||
|
try:
|
||||||
|
dev.linkgrabber.move_to_downloadlist(
|
||||||
|
link_ids=keep_ids,
|
||||||
|
package_ids=list(keep_pkg_ids),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return len(keep_ids), len(to_remove_ids)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Worker
|
# Worker
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -838,6 +904,26 @@ def worker(jobid: str):
|
|||||||
ensure_env()
|
ensure_env()
|
||||||
dev = get_device()
|
dev = get_device()
|
||||||
|
|
||||||
|
# Filter linkgrabber: keep only video files >= MIN_VIDEO_SIZE_MB
|
||||||
|
with lock:
|
||||||
|
job = jobs.get(jobid)
|
||||||
|
if job:
|
||||||
|
job.status = "collecting"
|
||||||
|
job.message = f"Filtere Links (nur Videos \u2265 {MIN_VIDEO_SIZE_MB} MB)\u2026"
|
||||||
|
accepted, rejected = _filter_linkgrabber(dev, jobid)
|
||||||
|
with lock:
|
||||||
|
job = jobs.get(jobid)
|
||||||
|
if job:
|
||||||
|
if accepted == 0:
|
||||||
|
job.status = "failed"
|
||||||
|
job.message = (
|
||||||
|
f"Keine Video-Dateien \u2265 {MIN_VIDEO_SIZE_MB} MB gefunden "
|
||||||
|
f"({rejected} Link(s) verworfen)."
|
||||||
|
)
|
||||||
|
job.progress = 0.0
|
||||||
|
return
|
||||||
|
job.message = f"{accepted} Video(s) akzeptiert, {rejected} verworfen."
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
with lock:
|
with lock:
|
||||||
job = jobs.get(jobid)
|
job = jobs.get(jobid)
|
||||||
@@ -856,7 +942,7 @@ def worker(jobid: str):
|
|||||||
if not links:
|
if not links:
|
||||||
with lock:
|
with lock:
|
||||||
job.status = "collecting"
|
job.status = "collecting"
|
||||||
job.message = "Warte auf Link-Crawler…"
|
job.message = "Warte auf Link-Crawler\u2026"
|
||||||
job.progress = 0.0
|
job.progress = 0.0
|
||||||
time.sleep(POLL_SECONDS)
|
time.sleep(POLL_SECONDS)
|
||||||
continue
|
continue
|
||||||
@@ -866,7 +952,7 @@ def worker(jobid: str):
|
|||||||
cancel_msg = cancel_job(dev, jobid)
|
cancel_msg = cancel_job(dev, jobid)
|
||||||
with lock:
|
with lock:
|
||||||
job.status = "failed"
|
job.status = "failed"
|
||||||
base_msg = "JDownloader lieferte das Demo-Video Big Buck Bunny statt des gewünschten Links."
|
base_msg = "JDownloader lieferte das Demo-Video Big Buck Bunny statt des gew\u00fcnschten Links."
|
||||||
job.message = f"{base_msg} {cancel_msg}" if cancel_msg else base_msg
|
job.message = f"{base_msg} {cancel_msg}" if cancel_msg else base_msg
|
||||||
job.progress = 0.0
|
job.progress = 0.0
|
||||||
return
|
return
|
||||||
@@ -877,7 +963,7 @@ def worker(jobid: str):
|
|||||||
with lock:
|
with lock:
|
||||||
job.status = "downloading"
|
job.status = "downloading"
|
||||||
done = sum(1 for l in links if l.get("finished"))
|
done = sum(1 for l in links if l.get("finished"))
|
||||||
job.message = f"Download läuft… ({done}/{len(links)} fertig)"
|
job.message = f"Download l\u00e4uft\u2026 ({done}/{len(links)} fertig)"
|
||||||
job.progress = progress
|
job.progress = progress
|
||||||
time.sleep(POLL_SECONDS)
|
time.sleep(POLL_SECONDS)
|
||||||
continue
|
continue
|
||||||
@@ -896,13 +982,13 @@ def worker(jobid: str):
|
|||||||
if not valid_videos:
|
if not valid_videos:
|
||||||
with lock:
|
with lock:
|
||||||
job.status = "failed"
|
job.status = "failed"
|
||||||
job.message = "ffprobe: keine gültige Video-Datei."
|
job.message = "ffprobe: keine g\u00fcltige Video-Datei."
|
||||||
job.progress = 0.0
|
job.progress = 0.0
|
||||||
return
|
return
|
||||||
|
|
||||||
with lock:
|
with lock:
|
||||||
job.status = "upload"
|
job.status = "upload"
|
||||||
job.message = f"Download fertig. MD5/Upload/Verify für {len(valid_videos)} Datei(en)…"
|
job.message = f"Download fertig. MD5/Upload/Verify f\u00fcr {len(valid_videos)} Datei(en)\u2026"
|
||||||
job.progress = 100.0
|
job.progress = 100.0
|
||||||
|
|
||||||
ssh = ssh_connect()
|
ssh = ssh_connect()
|
||||||
@@ -1018,7 +1104,7 @@ def render_page(error: str = "") -> str:
|
|||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>JD → Jellyfin</title>
|
<title>JD \u2192 Jellyfin</title>
|
||||||
<script>
|
<script>
|
||||||
async function refreshJobs() {{
|
async function refreshJobs() {{
|
||||||
if (document.hidden) return;
|
if (document.hidden) return;
|
||||||
@@ -1035,7 +1121,7 @@ def render_page(error: str = "") -> str:
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>JD → Jellyfin</h1>
|
<h1>JD \u2192 Jellyfin</h1>
|
||||||
{render_nav("downloads")}
|
{render_nav("downloads")}
|
||||||
{err_html}
|
{err_html}
|
||||||
|
|
||||||
@@ -1099,7 +1185,7 @@ def render_logs_page() -> str:
|
|||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>JD → Jellyfin (Logs)</title>
|
<title>JD \u2192 Jellyfin (Logs)</title>
|
||||||
<script>
|
<script>
|
||||||
async function refreshLogs() {{
|
async function refreshLogs() {{
|
||||||
if (document.hidden) return;
|
if (document.hidden) return;
|
||||||
@@ -1120,9 +1206,9 @@ def render_logs_page() -> str:
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>JD → Jellyfin</h1>
|
<h1>JD \u2192 Jellyfin</h1>
|
||||||
{render_nav("logs")}
|
{render_nav("logs")}
|
||||||
<p class="hint">Verbindungs-Debugger (Echtzeit). Letzte {LOG_BUFFER_LIMIT} Einträge.</p>
|
<p class="hint">Verbindungs-Debugger (Echtzeit). Letzte {LOG_BUFFER_LIMIT} Eintr\u00e4ge.</p>
|
||||||
<textarea id="log-body" class="log-area" rows="20" readonly></textarea>
|
<textarea id="log-body" class="log-area" rows="20" readonly></textarea>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1143,10 +1229,10 @@ def render_proxies_page(
|
|||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>JD → Jellyfin (Proxies)</title>
|
<title>JD \u2192 Jellyfin (Proxies)</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>JD → Jellyfin</h1>
|
<h1>JD \u2192 Jellyfin</h1>
|
||||||
{render_nav("proxies")}
|
{render_nav("proxies")}
|
||||||
{err_html}
|
{err_html}
|
||||||
{msg_html}
|
{msg_html}
|
||||||
@@ -1166,7 +1252,7 @@ def render_proxies_page(
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h2 style="margin-top:18px;">JDownloader Import-Liste</h2>
|
<h2 style="margin-top:18px;">JDownloader Import-Liste</h2>
|
||||||
<p class="hint">Format: <code>socks5://IP:PORT</code>, <code>socks4://IP:PORT</code>. Keine Prüfung/Validierung.</p>
|
<p class="hint">Format: <code>socks5://IP:PORT</code>, <code>socks4://IP:PORT</code>. Keine Pr\u00fcfung/Validierung.</p>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<textarea id="out" rows="12" readonly style="width:100%; max-width:860px; padding:10px; border:1px solid #ccc; border-radius:8px;">{esc(out_text)}</textarea>
|
<textarea id="out" rows="12" readonly style="width:100%; max-width:860px; padding:10px; border:1px solid #ccc; border-radius:8px;">{esc(out_text)}</textarea>
|
||||||
@@ -1174,8 +1260,8 @@ def render_proxies_page(
|
|||||||
|
|
||||||
<button type="button" onclick="navigator.clipboard.writeText(document.getElementById('out').value)">Kopieren</button>
|
<button type="button" onclick="navigator.clipboard.writeText(document.getElementById('out').value)">Kopieren</button>
|
||||||
|
|
||||||
<h2 style="margin-top:18px;">Datei für Connection Manager</h2>
|
<h2 style="margin-top:18px;">Datei f\u00fcr Connection Manager</h2>
|
||||||
<p class="hint">Speichert die Liste als <code>.jdproxies</code> im Container, z. B. zum Import in JDownloader → Verbindungsmanager → Importieren.</p>
|
<p class="hint">Speichert die Liste als <code>.jdproxies</code> im Container, z. B. zum Import in JDownloader \u2192 Verbindungsmanager \u2192 Importieren.</p>
|
||||||
|
|
||||||
<form method="post" action="/proxies/save">
|
<form method="post" action="/proxies/save">
|
||||||
<textarea name="socks5_in" style="display:none;">{esc(socks5_in)}</textarea>
|
<textarea name="socks5_in" style="display:none;">{esc(socks5_in)}</textarea>
|
||||||
@@ -1217,7 +1303,7 @@ def submit(url: str = Form(...), package_name: str = Form(""), library: str = Fo
|
|||||||
return HTMLResponse(render_page(f"JDownloader nicht erreichbar: {e}"), status_code=503)
|
return HTMLResponse(render_page(f"JDownloader nicht erreichbar: {e}"), status_code=503)
|
||||||
resp = dev.linkgrabber.add_links([{
|
resp = dev.linkgrabber.add_links([{
|
||||||
"links": url,
|
"links": url,
|
||||||
"autostart": True,
|
"autostart": False,
|
||||||
"assignJobID": True,
|
"assignJobID": True,
|
||||||
"packageName": package_name,
|
"packageName": package_name,
|
||||||
}])
|
}])
|
||||||
@@ -1251,7 +1337,7 @@ def cancel(jobid: str):
|
|||||||
if job.status in {"finished", "failed", "canceled"}:
|
if job.status in {"finished", "failed", "canceled"}:
|
||||||
return RedirectResponse(url="/", status_code=303)
|
return RedirectResponse(url="/", status_code=303)
|
||||||
job.cancel_requested = True
|
job.cancel_requested = True
|
||||||
job.message = "Abbruch angefordert…"
|
job.message = "Abbruch angefordert\u2026"
|
||||||
return RedirectResponse(url="/", status_code=303)
|
return RedirectResponse(url="/", status_code=303)
|
||||||
|
|
||||||
@app.post("/clear-finished")
|
@app.post("/clear-finished")
|
||||||
|
|||||||
Reference in New Issue
Block a user