- 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.
New GET /api/channels/activity returns subscribed channels sorted by
their latest video (no-video channels last) with the 3 newest videos
per channel and cursor pagination. The feed page gains a 'Лента |
Каналы' toggle (?view=channels) with compact video cards per channel
row; category filters and the sidebar preserve the view.
- Store subscriber count from YouTube statistics and show it on channel page
- Sync 50 videos per channel with playlistItems pagination support
- Show per-channel and per-category new-videos counters (2-day window)
- Replace hourly videos sync with activity trigger (2h idle) and
incremental backfill (hard cap 200 per channel)
- Clicking the sidebar new-videos count filters the category feed to
recent videos only (new_only)
- Update agent-team docs: deploy after green checks
Backend: cache Google access tokens (drop dead access_token_expires_at,
migration 0007), handle MeTube cleared/canceled events by URL, return
email from /auth/status only when authenticated, move Google base URLs
into settings, run container as non-root.
Frontend: include local feed filters in the query key, remove dead
Saved page and unused assets, drop stale CategoryNav props and classes,
send Content-Type only with a body, remove Uncategorized from sidebar.
GET /api/feed/saved-counts aggregates, per category, how many videos have
a completed download (using each video's latest job only, same rule as
the existing ?downloaded=true feed filter). CategoryNav gains an optional
categoryCounts override so Feed/Channels keep showing channel_count while
Saved shows this instead.
Query key ['feed', 'saved-counts'] deliberately nests under 'feed' so
DownloadButton's existing invalidateQueries({queryKey: ['feed']}) on
download/delete refreshes these counts too, with no extra wiring.
1 new backend test (77 total).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- GET /api/channels called with subscribed=true from the Channels page, so
a channel disappears from the list as soon as it's unsubscribed instead
of lingering with a badge
- GET /api/feed gains a `downloaded` filter (this was already anticipated
in the original TZ's feed query params but left unimplemented until
download_jobs existed) -- matches on each video's *latest* job only, so
a redownload/delete history doesn't leave stale matches
- New /saved route: same category filtering, video grid and download
actions as the main feed, scoped to downloaded videos only
2 new backend tests (76 total).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MeTube's queue/pending/done stores are keyed by the download's URL
(PersistentQueue.put: key = value.info.url), not by the id field a Download
reports over Socket.IO (which is what we stored as metube_job_id and were
sending). Sending the wrong key made MeTube's clear()/cancel() silently
no-op ("requested delete for non-existent download" in its own logs) while
still returning {"status": "ok"} regardless -- confirmed live by curling
/history on the real instance and finding the "deleted" entry still
present, unrelated to the DELETE_FILE_ON_TRASHCAN config fix that came
right before this.
delete_download() now takes the video's canonical youtube_url instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MeTube's /delete only unlinks the file when it's configured with
DELETE_FILE_ON_TRASHCAN=true; otherwise it just drops the entry from its
own "done" list and returns {"status": "ok"} regardless -- we were trusting
that response and marking the job "deleted" (offering a re-download) while
the file was still sitting on mediaVM's disk the whole time.
Now HEAD-check the media_url right after the delete call. If the file is
still reachable, leave the job's status untouched (still "completed", still
playable) and surface a clear 409 explaining MeTube's own config is why,
rather than lying about local state.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Delete local copy:
- MeTubeClient.delete_download() -> POST /delete {ids, where: "done"}
- Only ever acts on a job our own app tracked (metube_job_id we stored from
its own 'completed' event), never a pre-existing MeTube file
- New "deleted" terminal status; DELETE /api/videos/{id}/download
- Frontend: delete button next to "На сервере" badge, confirm dialog
Unsubscribe (deliberate deviation from the original TZ's MVP exclusion of
subscription management, per explicit user request after being shown the
tradeoff):
- OAuth scope widened from youtube.readonly to full youtube (read/write) --
existing stored tokens only cover the old scope, so unsubscribing needs a
fresh reconnect; reads keep working unchanged on the old token meanwhile
- channels.youtube_subscription_id (distinct from the channel id; that's
what subscriptions.delete actually keys on) captured during subscriptions
sync
- YouTubeInsufficientScope raised on 401/403 "insufficient authentication
scopes" and surfaced as a clear 403 asking the user to reconnect, rather
than a generic API error
- POST /api/channels/{id}/unsubscribe calls subscriptions.delete and marks
the channel unsubscribed locally on success
- Frontend: "Отписаться" button on ChannelCard with confirm dialog
10 new backend tests (73 total).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
- MeTubeClient encapsulates all MeTube HTTP/Socket.IO calls (verified against
the real MeTube source: /add returns no job id, GET /history gives a queue
snapshot for reconciliation, filenames arrive already relative, percent is
a 0-100 float)
- download_jobs table + service: request/dedup active downloads, apply live
Socket.IO events (added/updated/completed/canceled/cleared) matched by
canonical YouTube URL, safe relative-path -> public media URL construction
- Reconciliation on startup against MeTube's live queue/done state (section 19):
non-terminal jobs recovered where possible, else marked "unknown"; already
completed jobs are left untouched
- POST/GET /api/videos/{id}/download(-status), recheck-local; feed/video
detail now report real local availability instead of a stub
- Frontend: download button with live status polling (queued/downloading %/
postprocessing/completed/failed+retry), local <video> playback with
YouTube fallback on playback error
- health.py now delegates to MeTubeClient (single place for MeTube calls)
26 new backend tests (63 total). Verified live: Socket.IO connects
successfully to the real MeTube instance on deploy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>