From 112f87e7648618cfcb787aa22e61f68a1df6ac4b Mon Sep 17 00:00:00 2001 From: Mattias Thall Date: Tue, 26 May 2026 22:38:53 +0200 Subject: [PATCH] Popular tab now shows only flagged popular videos in rank order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add channel_popular_videos table (channel_id, video_id, rank). _fetch_popular_task clears and rewrites this table after each fetch. GET /channels/{id}/videos?sort=popular now JOINs this table and orders by rank instead of view_count, so the tab shows exactly the videos YouTube returned in popularity order — nothing more. Co-Authored-By: Claude Sonnet 4.6 --- backend/main.py | 8 ++++ backend/routers/channels.py | 82 ++++++++++++++++++++++++++----------- 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/backend/main.py b/backend/main.py index cc1330f..8c98b64 100644 --- a/backend/main.py +++ b/backend/main.py @@ -87,6 +87,14 @@ def on_startup(): crawled_at DATETIME DEFAULT CURRENT_TIMESTAMP )""", "ALTER TABLE playlists ADD COLUMN video_ids TEXT", + """CREATE TABLE IF NOT EXISTS channel_popular_videos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE, + video_id INTEGER NOT NULL REFERENCES videos(id) ON DELETE CASCADE, + rank INTEGER NOT NULL, + fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(channel_id, video_id) + )""", """CREATE TABLE IF NOT EXISTS search_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, diff --git a/backend/routers/channels.py b/backend/routers/channels.py index 1fc1475..e6fd76f 100644 --- a/backend/routers/channels.py +++ b/backend/routers/channels.py @@ -612,32 +612,49 @@ def get_channel_videos( current_user: User = Depends(get_current_user), ): _get_channel_or_404(db, channel_id) - order = { - "newest": "v.published_at DESC NULLS LAST", - "oldest": "v.published_at ASC NULLS LAST", - "title": "v.title ASC", - "unwatched":"COALESCE(uv.watched, 0) ASC, v.published_at DESC NULLS LAST", - "popular": "v.view_count DESC NULLS LAST", - }.get(sort, "v.published_at DESC NULLS LAST") params: dict = {"user_id": current_user.id, "channel_id": channel_id, "limit": limit, "offset": offset} q_clause = "" if q.strip(): q_clause = "AND (v.title LIKE :q OR v.description LIKE :q)" params["q"] = f"%{q.strip()}%" - rows = db.execute( - text(f""" - SELECT v.id, v.youtube_video_id, v.title, v.thumbnail_url, - v.duration_seconds, v.published_at, v.view_count, - COALESCE(uv.downloaded, 0) AS is_downloaded, - COALESCE(uv.watched, 0) AS is_watched - FROM videos v - LEFT JOIN user_videos uv ON v.id = uv.video_id AND uv.user_id = :user_id - WHERE v.channel_id = :channel_id {q_clause} - ORDER BY {order} - LIMIT :limit OFFSET :offset - """), - params, - ).mappings().all() + + if sort == "popular": + rows = db.execute( + text(f""" + SELECT v.id, v.youtube_video_id, v.title, v.thumbnail_url, + v.duration_seconds, v.published_at, v.view_count, + COALESCE(uv.downloaded, 0) AS is_downloaded, + COALESCE(uv.watched, 0) AS is_watched + FROM channel_popular_videos cpv + JOIN videos v ON cpv.video_id = v.id + LEFT JOIN user_videos uv ON v.id = uv.video_id AND uv.user_id = :user_id + WHERE cpv.channel_id = :channel_id {q_clause} + ORDER BY cpv.rank ASC + LIMIT :limit OFFSET :offset + """), + params, + ).mappings().all() + else: + order = { + "newest": "v.published_at DESC NULLS LAST", + "oldest": "v.published_at ASC NULLS LAST", + "title": "v.title ASC", + "unwatched":"COALESCE(uv.watched, 0) ASC, v.published_at DESC NULLS LAST", + }.get(sort, "v.published_at DESC NULLS LAST") + rows = db.execute( + text(f""" + SELECT v.id, v.youtube_video_id, v.title, v.thumbnail_url, + v.duration_seconds, v.published_at, v.view_count, + COALESCE(uv.downloaded, 0) AS is_downloaded, + COALESCE(uv.watched, 0) AS is_watched + FROM videos v + LEFT JOIN user_videos uv ON v.id = uv.video_id AND uv.user_id = :user_id + WHERE v.channel_id = :channel_id {q_clause} + ORDER BY {order} + LIMIT :limit OFFSET :offset + """), + params, + ).mappings().all() return [VideoOut(**dict(r)) for r in rows] @@ -705,7 +722,12 @@ def _fetch_popular_task(channel_id: int, youtube_channel_id: str): channel = db.query(Channel).filter_by(id=channel_id).first() if not channel: return - for yt_id in video_ids: + + # Clear previous popular list for this channel + db.execute(text("DELETE FROM channel_popular_videos WHERE channel_id = :cid"), {"cid": channel_id}) + db.commit() + + for rank, yt_id in enumerate(video_ids, start=1): meta = results.get(yt_id) if not meta: continue @@ -716,8 +738,9 @@ def _fetch_popular_task(channel_id: int, youtube_channel_id: str): existing.view_count = meta["view_count"] if meta.get("published_at") and not existing.published_at: existing.published_at = meta["published_at"] + video_id = existing.id else: - db.add(Video( + v = Video( youtube_video_id=yt_id, channel_id=channel.id, title=meta.get("title", ""), @@ -726,7 +749,18 @@ def _fetch_popular_task(channel_id: int, youtube_channel_id: str): published_at=meta.get("published_at"), tags=meta.get("tags") or "[]", view_count=meta.get("view_count"), - )) + ) + db.add(v) + db.flush() + video_id = v.id + db.execute( + text(""" + INSERT INTO channel_popular_videos (channel_id, video_id, rank) + VALUES (:cid, :vid, :rank) + ON CONFLICT(channel_id, video_id) DO UPDATE SET rank = :rank + """), + {"cid": channel_id, "vid": video_id, "rank": rank}, + ) db.commit() except Exception: db.rollback()