Fix download status flapping: only the 'completed' event is terminal
Production logs showed a job oscillating downloading -> completed -> downloading -> completed -> postprocessing -> completed. Cause: yt-dlp reports status='finished' via progress-hook ticks once per stream when downloading separate video+audio for muxing (video lands, audio is still in flight), and MeTube forwards that through 'updated' Socket.IO events too -- we were mapping any status='finished' to our "completed", regardless of which event carried it. Only the dedicated 'completed' event (and the 'done' bucket of MeTube's /history, for startup reconciliation) is now treated as authoritative for terminal status; a transient 'finished' arriving via 'added'/'updated' (or history's queue/pending) is a no-op for status, matching the progress_percent update it also carries. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
fe16c08daa
commit
e333296170
2 changed files with 80 additions and 11 deletions
|
|
@ -106,18 +106,27 @@ async def handle_metube_event(db: Session, event_name: str, raw_payload) -> None
|
|||
if job is None:
|
||||
return
|
||||
|
||||
_apply_metube_info(job, payload)
|
||||
# Only the dedicated 'completed' event is authoritative for terminal
|
||||
# status. yt-dlp fires per-stream progress hooks with status=='finished'
|
||||
# when downloading video+audio separately for merging (one stream lands,
|
||||
# the other is still in flight) -- MeTube forwards that transient state
|
||||
# through 'updated' events too, so treating it as our own "completed"
|
||||
# there flaps the job back and forth: downloading -> completed ->
|
||||
# downloading -> ... -> postprocessing -> completed. Verified against a
|
||||
# real deploy's logs before this fix.
|
||||
_apply_metube_info(job, payload, authoritative=event_name == "completed")
|
||||
db.commit()
|
||||
logger.info("download_job %s updated to status=%s via '%s' event", job.id, job.status, event_name)
|
||||
|
||||
|
||||
def _apply_metube_info(job: DownloadJob, info: dict) -> None:
|
||||
def _apply_metube_info(job: DownloadJob, info: dict, *, authoritative: bool) -> None:
|
||||
if info.get("id"):
|
||||
job.metube_job_id = info["id"]
|
||||
|
||||
our_status = METUBE_STATUS_MAP.get(info.get("status"), "unknown")
|
||||
metube_status = info.get("status")
|
||||
our_status = METUBE_STATUS_MAP.get(metube_status, "unknown")
|
||||
|
||||
if job.started_at is None and our_status in ("downloading", "postprocessing", "completed"):
|
||||
if job.started_at is None and metube_status in ("downloading", "postprocessing"):
|
||||
job.started_at = datetime.now(timezone.utc)
|
||||
|
||||
percent = info.get("percent")
|
||||
|
|
@ -128,6 +137,10 @@ def _apply_metube_info(job: DownloadJob, info: dict) -> None:
|
|||
job.status = "failed"
|
||||
job.error_message = info.get("msg") or info.get("error") or "Ошибка загрузки"
|
||||
elif our_status == "completed":
|
||||
if not authoritative:
|
||||
# A stream finished, not necessarily the whole job (see comment
|
||||
# above) -- keep the current status, just take the progress tick.
|
||||
return
|
||||
filename = info.get("filename")
|
||||
if filename:
|
||||
job.metube_filename = filename
|
||||
|
|
@ -135,7 +148,7 @@ def _apply_metube_info(job: DownloadJob, info: dict) -> None:
|
|||
job.status = "completed"
|
||||
job.progress_percent = 100
|
||||
job.completed_at = job.completed_at or datetime.now(timezone.utc)
|
||||
else:
|
||||
elif our_status != "unknown":
|
||||
job.status = our_status
|
||||
|
||||
|
||||
|
|
@ -158,22 +171,25 @@ def reconcile_on_startup(db: Session) -> None:
|
|||
db.commit()
|
||||
return
|
||||
|
||||
by_url: dict[str, dict] = {}
|
||||
for bucket in ("queue", "pending", "done"):
|
||||
# (item, authoritative) -- only the 'done' bucket is a terminal, trustworthy
|
||||
# outcome; 'queue'/'pending' are still in flight (see handle_metube_event).
|
||||
by_url: dict[str, tuple[dict, bool]] = {}
|
||||
for bucket, authoritative in (("queue", False), ("pending", False), ("done", True)):
|
||||
for item in history.get(bucket, []) or []:
|
||||
url = item.get("url")
|
||||
if url:
|
||||
by_url[url] = item
|
||||
by_url[url] = (item, authoritative)
|
||||
|
||||
videos = {v.id: v for v in db.query(Video).filter(Video.id.in_([j.video_id for j in jobs])).all()}
|
||||
|
||||
for job in jobs:
|
||||
video = videos.get(job.video_id)
|
||||
info = by_url.get(video.youtube_url) if video else None
|
||||
if info is None:
|
||||
match = by_url.get(video.youtube_url) if video else None
|
||||
if match is None:
|
||||
job.status = "unknown"
|
||||
continue
|
||||
_apply_metube_info(job, info)
|
||||
info, authoritative = match
|
||||
_apply_metube_info(job, info, authoritative=authoritative)
|
||||
|
||||
db.commit()
|
||||
logger.info("Reconciled %d download job(s) against MeTube history", len(jobs))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue