diff --git a/api/video/downloads.py b/api/video/downloads.py index 63971345..0ec4aa83 100644 --- a/api/video/downloads.py +++ b/api/video/downloads.py @@ -195,14 +195,16 @@ def register_routes(bp): want_season, want_episode, season_end = _search_ints(body) if source == "soulseek": - from core.video.slskd_search import build_query, start_search + from core.video.slskd_search import build_query, search_timeout_ms, start_search res = start_search(build_query(scope, title, year=body.get("year"), season=want_season, episode=want_episode)) if not res.get("configured"): return jsonify({"error": "slskd isn't configured — set its URL on Settings → Downloads."}) if res.get("error"): return jsonify({"error": "slskd: " + str(res["error"])}) - return jsonify({"id": res["id"], "live": True, "complete": False}) + # how long the client should keep polling (slskd keeps searching this long). + return jsonify({"id": res["id"], "live": True, "complete": False, + "poll_ms": search_timeout_ms() + 8000}) # mock sources resolve in one shot profile = load_profile(get_video_db()) raw = mock_search(scope, title, year=body.get("year"), season=want_season, @@ -216,16 +218,16 @@ def register_routes(bp): title, season?, episode?. The client polls until it stops growing or times out.""" from . import get_video_db from core.video.quality_profile import load as load_profile - from core.video.slskd_search import poll_responses + from core.video.slskd_search import poll_search sid = request.args.get("id") scope = str(request.args.get("scope") or "movie").lower() want_season, want_episode, _ = _search_ints(request.args) if not sid: - return jsonify({"results": [], "live": True}) + return jsonify({"results": [], "live": True, "total_files": 0}) profile = load_profile(get_video_db()) - raw = poll_responses(sid) - return jsonify({"live": True, - "results": _evaluate_hits(raw, profile, scope, want_season, want_episode)}) + polled = poll_search(sid) + return jsonify({"live": True, "total_files": polled["total_files"], + "results": _evaluate_hits(polled["hits"], profile, scope, want_season, want_episode)}) @bp.route("/downloads/grab", methods=["POST"]) def video_downloads_grab(): diff --git a/core/video/slskd_search.py b/core/video/slskd_search.py index 89376840..d569f97f 100644 --- a/core/video/slskd_search.py +++ b/core/video/slskd_search.py @@ -149,17 +149,29 @@ def start_search(query: str) -> dict: def poll_responses(search_id: str) -> list: """Current grouped video hits for an in-flight search (cheap; call repeatedly).""" + return poll_search(search_id)["hits"] + + +def poll_search(search_id: str) -> dict: + """Poll an in-flight search → {hits (grouped video releases), total_files (every + file slskd returned, incl. non-video)}. total_files lets the UI distinguish + 'nothing back yet' from 'plenty back but it's all audio/junk, no video'.""" base, headers = _conn() if not base or not search_id: - return [] + return {"hits": [], "total_files": 0} try: r = requests.get(base + "/api/v0/searches/%s/responses" % search_id, headers=headers, timeout=15) if not r.ok: - return [] + return {"hits": [], "total_files": 0} data = r.json() except Exception: # noqa: BLE001, S110 - transient error → no new hits this poll - return [] - return group_video_files(data) + return {"hits": [], "total_files": 0} + total = 0 + for u in (data if isinstance(data, list) else []): + if isinstance(u, dict): + for d in (u.get("directories") or []): + total += len(d.get("files") or []) + return {"hits": group_video_files(data), "total_files": total} def slskd_search(query: str, *, max_seconds: int = 8, slskd_timeout_ms: int = 4500) -> dict: @@ -205,4 +217,5 @@ def slskd_search(query: str, *, max_seconds: int = 8, slskd_timeout_ms: int = 45 return {"configured": True, "hits": group_video_files(responses)} -__all__ = ["VIDEO_EXTS", "build_query", "group_video_files", "slskd_search"] +__all__ = ["VIDEO_EXTS", "build_query", "group_video_files", "slskd_search", + "start_search", "poll_responses", "poll_search", "search_timeout_ms"] diff --git a/webui/static/video/video-download-view.js b/webui/static/video/video-download-view.js index d82ca3ad..ac3b0f51 100644 --- a/webui/static/video/video-download-view.js +++ b/webui/static/video/video-download-view.js @@ -208,12 +208,17 @@ } // Render the result cards (shared by the immediate mock path and live polling). - function renderResults(resultsEl, params, rows, live, done) { + function renderResults(resultsEl, params, rows, live, done, totalFiles) { rows = rows || []; if (!rows.length) { - resultsEl.innerHTML = done - ? '