Channel features: subscribers, new-videos badges, activity-driven sync
- 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
This commit is contained in:
parent
fde9a439df
commit
e10df8dcbd
27 changed files with 1420 additions and 107 deletions
|
|
@ -1,15 +1,18 @@
|
|||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import settings
|
||||
from app.core.auth_dependency import require_session
|
||||
from app.db import get_db
|
||||
from app.models.category import Category
|
||||
from app.models.channel import Channel
|
||||
from app.models.channel_category import channel_categories
|
||||
from app.models.video import Video
|
||||
from app.services import sync
|
||||
from app.services.google_oauth import OAuthNotConnected
|
||||
from app.services.youtube_client import YouTubeAPIError, YouTubeInsufficientScope, YouTubeQuotaExceeded
|
||||
|
|
@ -37,7 +40,22 @@ def _category_ids_by_channel(db: Session, channel_ids: list[int]) -> dict[int, l
|
|||
return result
|
||||
|
||||
|
||||
def _serialize(channel: Channel, category_ids: list[int]) -> dict:
|
||||
def _new_videos_counts(db: Session, channel_ids: list[int]) -> dict[int, int]:
|
||||
"""Videos published within the new-videos window, per channel, in one
|
||||
aggregate query."""
|
||||
if not channel_ids:
|
||||
return {}
|
||||
since = datetime.now(timezone.utc) - timedelta(days=settings.new_videos_window_days)
|
||||
rows = (
|
||||
db.query(Video.channel_id, func.count(Video.id))
|
||||
.filter(Video.channel_id.in_(channel_ids), Video.published_at >= since)
|
||||
.group_by(Video.channel_id)
|
||||
.all()
|
||||
)
|
||||
return {channel_id: count for channel_id, count in rows}
|
||||
|
||||
|
||||
def _serialize(channel: Channel, category_ids: list[int], new_videos_count: int = 0) -> dict:
|
||||
return {
|
||||
"id": channel.id,
|
||||
"youtube_channel_id": channel.youtube_channel_id,
|
||||
|
|
@ -45,9 +63,11 @@ def _serialize(channel: Channel, category_ids: list[int]) -> dict:
|
|||
"description": channel.description,
|
||||
"thumbnail_url": channel.thumbnail_url,
|
||||
"uploads_playlist_id": channel.uploads_playlist_id,
|
||||
"subscriber_count": channel.subscriber_count,
|
||||
"subscribed": channel.subscribed,
|
||||
"last_synced_at": channel.last_synced_at,
|
||||
"category_ids": category_ids,
|
||||
"new_videos_count": new_videos_count,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -75,8 +95,10 @@ def list_channels(
|
|||
query = query.filter(Channel.id.in_(channel_ids_in_category))
|
||||
|
||||
channels = query.order_by(Channel.title.asc()).all()
|
||||
category_map = _category_ids_by_channel(db, [c.id for c in channels])
|
||||
return [_serialize(c, category_map.get(c.id, [])) for c in channels]
|
||||
channel_ids = [c.id for c in channels]
|
||||
category_map = _category_ids_by_channel(db, channel_ids)
|
||||
new_videos_map = _new_videos_counts(db, channel_ids)
|
||||
return [_serialize(c, category_map.get(c.id, []), new_videos_map.get(c.id, 0)) for c in channels]
|
||||
|
||||
|
||||
@router.get("/channels/{channel_id}")
|
||||
|
|
@ -85,7 +107,8 @@ def get_channel(channel_id: int, db: Session = Depends(get_db)) -> dict:
|
|||
if channel is None:
|
||||
raise HTTPException(status_code=404, detail="Channel not found")
|
||||
category_map = _category_ids_by_channel(db, [channel_id])
|
||||
return _serialize(channel, category_map.get(channel_id, []))
|
||||
new_videos_map = _new_videos_counts(db, [channel_id])
|
||||
return _serialize(channel, category_map.get(channel_id, []), new_videos_map.get(channel_id, 0))
|
||||
|
||||
|
||||
@router.put("/channels/{channel_id}/categories")
|
||||
|
|
@ -110,7 +133,8 @@ def set_channel_categories(channel_id: int, payload: ChannelCategoriesUpdate, db
|
|||
)
|
||||
db.commit()
|
||||
|
||||
return _serialize(channel, sorted(unique_ids))
|
||||
new_videos_map = _new_videos_counts(db, [channel_id])
|
||||
return _serialize(channel, sorted(unique_ids), new_videos_map.get(channel_id, 0))
|
||||
|
||||
|
||||
@router.post("/channels/{channel_id}/unsubscribe")
|
||||
|
|
@ -139,4 +163,5 @@ def unsubscribe_channel(channel_id: int, db: Session = Depends(get_db)) -> dict:
|
|||
raise HTTPException(status_code=502, detail="YouTube is unavailable")
|
||||
|
||||
category_map = _category_ids_by_channel(db, [channel_id])
|
||||
return _serialize(channel, category_map.get(channel_id, []))
|
||||
new_videos_map = _new_videos_counts(db, [channel_id])
|
||||
return _serialize(channel, category_map.get(channel_id, []), new_videos_map.get(channel_id, 0))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue