- Detect shorts by duration (SHORTS_MAX_DURATION_SECONDS, default 180)
and expose is_short in feed, video and channel-activity DTOs.
- Feed gains type=all|long|short and anchor; lists show a two-mode
'Обычные | Shorts' filter (no 'Все') persisted in the URL.
- Clicking a short opens /shorts/🆔 a vertical scroll-snap feed with
autoplay for the active slide, context-aware endpoints and infinite
loading.
- Keep the sound choice across swipes, syncing with the player's own
mute control and guarding against the widget's stale isMuted() reads.
- 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 /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>
- 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>