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:
vrubelroman 2026-09-17 21:07:33 +00:00
parent fde9a439df
commit e10df8dcbd
27 changed files with 1420 additions and 107 deletions

View file

@ -1,3 +1,5 @@
from datetime import datetime, timedelta, timezone
import pytest
from fastapi.testclient import TestClient
@ -5,6 +7,7 @@ from app.core.auth_dependency import require_session
from app.db import get_db
from app.main import app
from app.models.channel import Channel
from app.models.video import Video
from app.services import sync
from app.services.youtube_client import YouTubeInsufficientScope
@ -86,3 +89,54 @@ def test_unsubscribe_insufficient_scope_returns_403(client, db_session, monkeypa
def test_unsubscribe_channel_not_found(client):
resp = client.post("/api/channels/9999/unsubscribe")
assert resp.status_code == 404
def _seed_video(db_session, channel, video_id, published_at):
video = Video(
youtube_video_id=video_id,
channel_id=channel.id,
title=f"Video {video_id}",
published_at=published_at,
youtube_url=f"https://www.youtube.com/watch?v={video_id}",
)
db_session.add(video)
db_session.commit()
return video
def test_channel_responses_include_subscriber_and_new_videos_counts(client, db_session):
channel = _seed_channel(db_session)
channel.subscriber_count = 1200000
db_session.commit()
now = datetime.now(timezone.utc)
_seed_video(db_session, channel, "vidRecent", now - timedelta(days=1))
_seed_video(db_session, channel, "vidOld", now - timedelta(days=30))
resp = client.get("/api/channels")
assert resp.status_code == 200
payload = resp.json()
assert len(payload) == 1
assert payload[0]["subscriber_count"] == 1200000
# Only the video from 1 day ago is inside the 7-day window.
assert payload[0]["new_videos_count"] == 1
resp_single = client.get(f"/api/channels/{channel.id}")
assert resp_single.status_code == 200
single = resp_single.json()
assert single["subscriber_count"] == 1200000
assert single["new_videos_count"] == 1
def test_channel_new_videos_count_zero_without_recent_videos(client, db_session):
channel = _seed_channel(db_session)
now = datetime.now(timezone.utc)
_seed_video(db_session, channel, "vidOld", now - timedelta(days=30))
resp = client.get("/api/channels")
assert resp.status_code == 200
payload = resp.json()
assert len(payload) == 1
assert payload[0]["subscriber_count"] is None
assert payload[0]["new_videos_count"] == 0