feat(playlists): YouTube playlist read-sync (backend)

Mirror each user's own YouTube playlists into local source='youtube' playlists,
one-way (YT -> local). New client methods iter_my_playlists / iter_my_playlist_video_ids
(OAuth, so private playlists work); sync/playlists.py reconciles the mirror (matching YT
order) and ingests any playlist videos not in the shared catalog yet (with stub channels).
POST /api/playlists/sync-youtube for manual sync (read-scope gated, per-user quota) plus a
scheduler job (playlist_sync_minutes, default 6h) that syncs all read-scope users. YouTube's
Watch Later / History are not API-accessible and are never synced.
This commit is contained in:
npeter83 2026-06-15 19:37:03 +02:00
parent 9c28a75787
commit a661d079ee
5 changed files with 228 additions and 1 deletions

View file

@ -9,6 +9,7 @@ from app.config import settings
from app.db import SessionLocal
from app.state import is_sync_paused
from app.sync.autotag import run_autotag_all
from app.sync.playlists import sync_all_playlists
from app.sync.runner import (
run_deep_backfill,
run_enrich,
@ -79,6 +80,12 @@ def _subscriptions_job() -> None:
_job("subscriptions", work)
def _playlist_sync_job() -> None:
# Mirror each read-scope user's YouTube playlists; per-user quota attribution is
# handled inside sync_all_playlists.
_job("playlist_sync", sync_all_playlists)
def start_scheduler() -> None:
global _scheduler
if not settings.scheduler_enabled or _scheduler is not None:
@ -114,6 +121,12 @@ def start_scheduler() -> None:
minutes=settings.subscriptions_resync_minutes,
id="subscriptions",
)
scheduler.add_job(
_playlist_sync_job,
"interval",
minutes=settings.playlist_sync_minutes,
id="playlist_sync",
)
scheduler.start()
_scheduler = scheduler
logger.info("scheduler started")