Fix download status handling and the blank page after download
- Never let a non-authoritative MeTube 'updated' event downgrade a terminal job (it refilled the progress bar after completion). - Self-heal stale active jobs against MeTube history on status polls, so a missed event no longer leaves a job stuck in 'queued'. - Build YT.Player on an imperatively created child div: React keeps owning the container, so switching to the local copy after a download no longer throws removeChild and blanks the page. - Autoplay on open and seek controls; subtitle experiment reverted.
This commit is contained in:
parent
b1faeb3729
commit
43adec5224
9 changed files with 758 additions and 5 deletions
|
|
@ -1,4 +1,4 @@
|
|||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
|
@ -128,6 +128,149 @@ def test_download_status_not_downloaded(client, db_session):
|
|||
assert resp.json()["status"] == "not_downloaded"
|
||||
|
||||
|
||||
def _seed_job_with_age(db_session, video, status="queued", age_minutes=10):
|
||||
"""Seeds a job whose requested_at/updated_at are both `age_minutes` in the
|
||||
past (aware datetimes -- see AGENTS.md: SQLite stores them naive, the
|
||||
production code normalizes them before comparing)."""
|
||||
from app.models.download_job import DownloadJob
|
||||
|
||||
old = datetime.now(timezone.utc) - timedelta(minutes=age_minutes)
|
||||
job = DownloadJob(video_id=video.id, status=status, requested_at=old, updated_at=old)
|
||||
db_session.add(job)
|
||||
db_session.commit()
|
||||
return job
|
||||
|
||||
|
||||
def test_download_status_self_heals_stale_job_to_completed(client, db_session, monkeypatch):
|
||||
"""Bug 1: a job stuck in 'queued' because the terminal Socket.IO event was
|
||||
missed is reconciled against MeTube history on poll."""
|
||||
video = _seed_single_video(db_session)
|
||||
_seed_job_with_age(db_session, video, status="queued", age_minutes=10)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.metube_client.MeTubeClient.fetch_history",
|
||||
lambda self: {
|
||||
"queue": [],
|
||||
"pending": [],
|
||||
"done": [{"id": "vid1.vid1", "url": video.youtube_url, "status": "finished", "filename": "f.mp4"}],
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.services.metube_client.MeTubeClient.build_media_url",
|
||||
lambda self, filename: f"http://metube.local/download/{filename}",
|
||||
)
|
||||
|
||||
resp = client.get(f"/api/videos/{video.youtube_video_id}/download-status")
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
assert body["media_url"] == "http://metube.local/download/f.mp4"
|
||||
|
||||
|
||||
def test_download_status_self_heals_stale_job_to_failed(client, db_session, monkeypatch):
|
||||
video = _seed_single_video(db_session)
|
||||
_seed_job_with_age(db_session, video, status="queued", age_minutes=10)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.metube_client.MeTubeClient.fetch_history",
|
||||
lambda self: {
|
||||
"queue": [],
|
||||
"pending": [],
|
||||
"done": [{"id": "vid1.vid1", "url": video.youtube_url, "status": "error", "msg": "HTTP 429 bot check"}],
|
||||
},
|
||||
)
|
||||
|
||||
resp = client.get(f"/api/videos/{video.youtube_video_id}/download-status")
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "failed"
|
||||
assert body["error_message"] == "HTTP 429 bot check"
|
||||
assert body["media_url"] is None
|
||||
|
||||
|
||||
def test_download_status_stale_job_still_in_queue_stays_active(client, db_session, monkeypatch):
|
||||
video = _seed_single_video(db_session)
|
||||
_seed_job_with_age(db_session, video, status="queued", age_minutes=10)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.metube_client.MeTubeClient.fetch_history",
|
||||
lambda self: {
|
||||
"queue": [{"id": "vid1.vid1", "url": video.youtube_url, "status": "pending"}],
|
||||
"pending": [],
|
||||
"done": [],
|
||||
},
|
||||
)
|
||||
|
||||
resp = client.get(f"/api/videos/{video.youtube_video_id}/download-status")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "queued"
|
||||
|
||||
|
||||
def test_download_status_stale_job_not_in_history_is_unknown(client, db_session, monkeypatch):
|
||||
video = _seed_single_video(db_session)
|
||||
_seed_job_with_age(db_session, video, status="queued", age_minutes=10)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.metube_client.MeTubeClient.fetch_history",
|
||||
lambda self: {"queue": [], "pending": [], "done": []},
|
||||
)
|
||||
|
||||
resp = client.get(f"/api/videos/{video.youtube_video_id}/download-status")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "unknown"
|
||||
|
||||
|
||||
def test_download_status_fresh_job_does_not_fetch_history(client, db_session, monkeypatch):
|
||||
video = _seed_single_video(db_session)
|
||||
_seed_job_with_age(db_session, video, status="queued", age_minutes=0)
|
||||
|
||||
def fail_if_called(self):
|
||||
raise AssertionError("fetch_history must not be called for a fresh job")
|
||||
|
||||
monkeypatch.setattr("app.services.metube_client.MeTubeClient.fetch_history", fail_if_called)
|
||||
|
||||
resp = client.get(f"/api/videos/{video.youtube_video_id}/download-status")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "queued"
|
||||
|
||||
|
||||
def test_download_status_terminal_job_does_not_fetch_history(client, db_session, monkeypatch):
|
||||
video = _seed_single_video(db_session)
|
||||
_seed_job_with_age(db_session, video, status="completed", age_minutes=10)
|
||||
|
||||
def fail_if_called(self):
|
||||
raise AssertionError("fetch_history must not be called for a terminal job")
|
||||
|
||||
monkeypatch.setattr("app.services.metube_client.MeTubeClient.fetch_history", fail_if_called)
|
||||
|
||||
resp = client.get(f"/api/videos/{video.youtube_video_id}/download-status")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "completed"
|
||||
|
||||
|
||||
def test_download_status_fetch_error_keeps_status(client, db_session, monkeypatch):
|
||||
"""A transient history-fetch failure must not bubble up nor change the
|
||||
job's status -- the poll response keeps the last known state."""
|
||||
video = _seed_single_video(db_session)
|
||||
_seed_job_with_age(db_session, video, status="queued", age_minutes=10)
|
||||
|
||||
def raise_error(self):
|
||||
raise RuntimeError("connection refused")
|
||||
|
||||
monkeypatch.setattr("app.services.metube_client.MeTubeClient.fetch_history", raise_error)
|
||||
|
||||
resp = client.get(f"/api/videos/{video.youtube_video_id}/download-status")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "queued"
|
||||
|
||||
|
||||
def test_recheck_local_downgrades_to_unknown_when_media_missing(client, db_session, monkeypatch):
|
||||
from app.models.download_job import DownloadJob
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue