New GET /api/widget/recent — returns recent unwatched videos from followed channels (title, channel, thumbnail, published_at, duration, direct URL). New GET /api/widget/stats — unwatched count, new this week, channel count. Both endpoints auth via X-Widget-Key header (WIDGET_API_KEY env var) so external services can call without JWT token lifecycle management. Targets the first admin user's data. Also: pass WIDGET_API_KEY through docker-compose environment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
108 lines
3.8 KiB
Python
108 lines
3.8 KiB
Python
"""
|
|
Read-only widget endpoints for external dashboards (e.g. backstage).
|
|
Auth: X-Widget-Key header must match WIDGET_API_KEY env var.
|
|
No user session required — returns data for the first admin user.
|
|
"""
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, Header, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import text
|
|
|
|
from ..config import settings
|
|
from ..database import get_db
|
|
from ..models import User
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _require_widget_key(x_widget_key: Optional[str] = Header(default=None)):
|
|
if not settings.widget_api_key:
|
|
raise HTTPException(status_code=503, detail="widget API not configured")
|
|
if x_widget_key != settings.widget_api_key:
|
|
raise HTTPException(status_code=401, detail="invalid widget key")
|
|
|
|
|
|
def _get_widget_user(db: Session) -> User:
|
|
user = db.query(User).filter_by(is_admin=True).order_by(User.id).first()
|
|
if not user:
|
|
user = db.query(User).order_by(User.id).first()
|
|
if not user:
|
|
raise HTTPException(status_code=503, detail="no users")
|
|
return user
|
|
|
|
|
|
@router.get("/recent")
|
|
def recent_videos(
|
|
limit: int = 12,
|
|
db: Session = Depends(get_db),
|
|
_: None = Depends(_require_widget_key),
|
|
):
|
|
"""Recent unwatched videos from followed channels."""
|
|
user = _get_widget_user(db)
|
|
rows = db.execute(
|
|
text("""
|
|
SELECT v.youtube_video_id, v.title, v.thumbnail_url,
|
|
v.duration_seconds, v.published_at,
|
|
c.name AS channel_name, c.youtube_channel_id AS channel_yt_id,
|
|
COALESCE(uv.watched, 0) AS watched
|
|
FROM videos v
|
|
JOIN channels c ON v.channel_id = c.id
|
|
JOIN user_channels uc ON c.id = uc.channel_id
|
|
AND uc.user_id = :uid AND uc.status = 'followed'
|
|
LEFT JOIN user_videos uv ON v.id = uv.video_id AND uv.user_id = :uid
|
|
WHERE COALESCE(uv.watched, 0) = 0
|
|
AND v.published_at IS NOT NULL
|
|
AND (uc.muted_until IS NULL OR datetime(uc.muted_until) < datetime('now'))
|
|
ORDER BY v.published_at DESC
|
|
LIMIT :limit
|
|
"""),
|
|
{"uid": user.id, "limit": limit},
|
|
).mappings().all()
|
|
|
|
return {
|
|
"videos": [
|
|
{
|
|
"youtube_video_id": r["youtube_video_id"],
|
|
"title": r["title"],
|
|
"channel_name": r["channel_name"],
|
|
"thumbnail_url": r["thumbnail_url"],
|
|
"published_at": r["published_at"],
|
|
"duration_seconds": r["duration_seconds"],
|
|
"url": f"https://yt.nullinput.io/watch/{r['youtube_video_id']}",
|
|
}
|
|
for r in rows
|
|
]
|
|
}
|
|
|
|
|
|
@router.get("/stats")
|
|
def widget_stats(
|
|
db: Session = Depends(get_db),
|
|
_: None = Depends(_require_widget_key),
|
|
):
|
|
"""Quick stats: unwatched count, channel count, recent activity."""
|
|
user = _get_widget_user(db)
|
|
row = db.execute(
|
|
text("""
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE COALESCE(uv.watched, 0) = 0) AS unwatched,
|
|
COUNT(*) FILTER (WHERE v.published_at >= datetime('now', '-7 days')
|
|
AND COALESCE(uv.watched, 0) = 0) AS new_this_week,
|
|
COUNT(DISTINCT uc.channel_id) AS channel_count
|
|
FROM user_channels uc
|
|
JOIN channels c ON c.id = uc.channel_id
|
|
JOIN videos v ON v.channel_id = c.id
|
|
LEFT JOIN user_videos uv ON v.id = uv.video_id AND uv.user_id = :uid
|
|
WHERE uc.user_id = :uid AND uc.status = 'followed'
|
|
"""),
|
|
{"uid": user.id},
|
|
).mappings().first()
|
|
|
|
return {
|
|
"unwatched": row["unwatched"] if row else 0,
|
|
"new_this_week": row["new_this_week"] if row else 0,
|
|
"channel_count": row["channel_count"] if row else 0,
|
|
}
|