Hide unsubscribed channels from the channels list, add a Saved tab

- 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>
This commit is contained in:
vrubelroman 2026-09-16 20:23:33 +00:00
parent 62f8fb746b
commit b29bcfb8d7
7 changed files with 127 additions and 3 deletions

View file

@ -9,6 +9,7 @@ from app.main import app
from app.models.category import Category
from app.models.channel import Channel
from app.models.channel_category import channel_categories
from app.models.download_job import DownloadJob
from app.models.video import Video
@ -109,6 +110,35 @@ def test_feed_filters_uncategorized(client, db_session):
assert ids == {"vid1", "vid3"}
def test_feed_filters_downloaded(client, db_session):
_, _, _, videos = _seed(db_session)
completed = DownloadJob(video_id=videos[0].id, status="completed", media_url="http://x/f0.mp4")
failed = DownloadJob(video_id=videos[1].id, status="failed")
db_session.add_all([completed, failed])
db_session.commit()
resp = client.get("/api/feed?downloaded=true").json()
ids = {i["youtube_video_id"] for i in resp["items"]}
assert ids == {"vid0"}
def test_feed_filters_downloaded_uses_latest_job_per_video(client, db_session):
_, _, _, videos = _seed(db_session)
older = DownloadJob(video_id=videos[0].id, status="completed", media_url="http://x/f0.mp4")
db_session.add(older)
db_session.commit()
newer = DownloadJob(video_id=videos[0].id, status="deleted")
db_session.add(newer)
db_session.commit()
resp = client.get("/api/feed?downloaded=true").json()
assert resp["items"] == []
def test_feed_item_shape(client, db_session):
channel_a, _, category, _ = _seed(db_session)