feat(channels): channel-explore backend — about metadata, ephemeral provenance, cleanup

Adds the backend for a dedicated channel page + ephemeral browsing of un-subscribed
channels:
- migration 0033: channels.{total_view_count,published_at,banner_url,external_links,
  from_explore}, videos.via_explore, explored_channels (per-user, grace-clocked).
- enrich channels with part=brandingSettings (banner/links) + statistics.viewCount +
  snippet.publishedAt — no extra quota.
- GET /api/channels/{id}: About detail + this user's relationship; lazy-enriches the new
  About fields (published_at sentinel).
- POST /api/channels/{id}/explore (require_human, quota-reserve guarded): records the
  exploration, flags the channel ephemeral unless followed, ingests one page of uploads
  (via_explore) + enriches; returns next_page_token for load-more.
- feed: per-explorer gate — a via_explore video is visible only to users who explored or
  subscribe to its channel, so exploration never leaks into everyone's catalog.
- subscribe = keep: clears from_explore + via_explore on the channel's videos.
- scheduler explore_cleanup job + explore_grace_days config: hard-delete explored-but-
  unkept channels and their untouched ephemeral videos after a grace period.
This commit is contained in:
npeter83 2026-06-30 02:52:44 +02:00
parent bb7b9972fd
commit bc4c362423
11 changed files with 481 additions and 4 deletions

View file

@ -16,6 +16,7 @@ from app.models import SchedulerSetting
from app.notifications import create_notification
from app.state import is_sync_paused
from app.sync.autotag import run_autotag_all
from app.sync.explore import purge_unkept_explored
from app.sync.maintenance import run_maintenance
from app.sync.playlists import sync_all_playlists
from app.sync.runner import (
@ -57,6 +58,7 @@ JOB_INTERVALS: dict[str, int] = {
"playlist_sync": settings.playlist_sync_minutes,
"maintenance": settings.maintenance_interval_minutes,
"demo_reset": settings.demo_reset_minutes,
"explore_cleanup": settings.explore_cleanup_minutes,
}
# Sane bounds for an admin-set interval (minutes).
@ -268,6 +270,12 @@ def _demo_reset_job() -> None:
_job("demo_reset", work)
def _explore_cleanup_job() -> None:
# Hard-delete explored-but-never-kept channels and their untouched ephemeral videos after
# the grace period. 0 quota (pure DB), so no quota attribution.
_job("explore_cleanup", purge_unkept_explored)
# job_id -> wrapper. The single source of truth for which jobs exist and how to run one,
# shared by start_scheduler (recurring registration) and trigger_job (manual "run now").
JOB_FUNCS: dict[str, Callable[[], None]] = {
@ -280,6 +288,7 @@ JOB_FUNCS: dict[str, Callable[[], None]] = {
"playlist_sync": _playlist_sync_job,
"maintenance": _maintenance_job,
"demo_reset": _demo_reset_job,
"explore_cleanup": _explore_cleanup_job,
}