feat(maintenance): scheduled job to retire unplayable videos
Add a daily maintenance/validation job that detects videos which can't be played anywhere and retires them safely. Two phases: re-check already-flagged videos (recover if available again, else hard-delete once the grace period elapses, cascading to states/playlist items), and a rolling re-validation of the least-recently-checked currently-available videos that flags newly-unplayable ones (hidden from the feed immediately via unavailable_since). Detection is ~free: a video missing from the videos.list response is deleted-or-private; an `upcoming` premiere >2 days past its scheduled start that never went live is abandoned. A still-live broadcast is kept (legit 24/7 stream). Enrichment now also fetches part=status to populate the status columns. Grace is 7 days for removed videos, none for abandoned. Before deleting, affected users get one batched notification (never per-video). Interval is admin-tunable via the Scheduler dashboard; batch size and grace are config. Quota-attributed to the system and bounded by the same backfill reserve as the other jobs.
This commit is contained in:
parent
b9a3a9012d
commit
c4aecf9f77
6 changed files with 280 additions and 1 deletions
|
|
@ -13,6 +13,7 @@ from app.db import SessionLocal
|
|||
from app.models import SchedulerSetting
|
||||
from app.state import is_sync_paused
|
||||
from app.sync.autotag import run_autotag_all
|
||||
from app.sync.maintenance import run_maintenance
|
||||
from app.sync.playlists import sync_all_playlists
|
||||
from app.sync.runner import (
|
||||
run_deep_backfill,
|
||||
|
|
@ -44,6 +45,7 @@ JOB_INTERVALS: dict[str, int] = {
|
|||
"shorts": settings.shorts_probe_interval_minutes,
|
||||
"subscriptions": settings.subscriptions_resync_minutes,
|
||||
"playlist_sync": settings.playlist_sync_minutes,
|
||||
"maintenance": settings.maintenance_interval_minutes,
|
||||
}
|
||||
|
||||
# Sane bounds for an admin-set interval (minutes).
|
||||
|
|
@ -201,6 +203,14 @@ def _playlist_sync_job() -> None:
|
|||
_job("playlist_sync", sync_all_playlists)
|
||||
|
||||
|
||||
def _maintenance_job() -> None:
|
||||
def work(db):
|
||||
with quota.attribute(None, "maintenance"):
|
||||
return run_maintenance(db)
|
||||
|
||||
_job("maintenance", work)
|
||||
|
||||
|
||||
def start_scheduler() -> None:
|
||||
global _scheduler
|
||||
if not settings.scheduler_enabled or _scheduler is not None:
|
||||
|
|
@ -219,6 +229,7 @@ def start_scheduler() -> None:
|
|||
"shorts": _shorts_job,
|
||||
"subscriptions": _subscriptions_job,
|
||||
"playlist_sync": _playlist_sync_job,
|
||||
"maintenance": _maintenance_job,
|
||||
}
|
||||
scheduler = BackgroundScheduler(timezone="UTC")
|
||||
for job_id, fn in fns.items():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue