feat(player): linkify descriptions and play YouTube links inline
- Narrow the title hover target to the actual text, not the whole row. - Linkify descriptions: timestamps (mm:ss / hh:mm:ss) seek the player; emails become mailto:; hashtags link to YouTube's hashtag feed; other URLs open in a new tab. Blank lines are stripped so the popover isn't mostly whitespace. - YouTube links play in the inline player: a link to the current video seeks (honoring t=), a link to another video navigates the player to it, with a Back button to the original. While on a linked video the title/author come from the player and its views/date/duration + a clickable channel come from the detail endpoint, which falls back to the YouTube API (videos.list, attributed to the user) for videos not in our DB.
This commit is contained in:
parent
2a5660e01e
commit
47d0799407
3 changed files with 335 additions and 63 deletions
|
|
@ -4,9 +4,12 @@ from fastapi import APIRouter, Depends, HTTPException, Query
|
|||
from sqlalchemy import Select, and_, false, func, or_, select
|
||||
from sqlalchemy.orm import Session, aliased
|
||||
|
||||
from app import quota
|
||||
from app.auth import current_user
|
||||
from app.db import get_db
|
||||
from app.models import Channel, ChannelTag, Subscription, Tag, User, Video, VideoState
|
||||
from app.sync.videos import parse_iso8601_duration
|
||||
from app.youtube.client import YouTubeClient, YouTubeError
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["feed"])
|
||||
|
||||
|
|
@ -296,12 +299,48 @@ def get_video_detail(
|
|||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""On-demand detail (description, like count) — kept out of the feed list payload
|
||||
so the feed stays lean; fetched lazily, e.g. for the title hover popover."""
|
||||
so the feed stays lean; fetched lazily, e.g. for the title hover popover.
|
||||
|
||||
Videos we already store are served from the DB for free. A video that isn't in
|
||||
our DB (e.g. a YouTube link inside another video's description that the in-app
|
||||
player navigated to) is resolved via the YouTube API (videos.list, 1 unit,
|
||||
attributed to the requesting user)."""
|
||||
v = db.get(Video, video_id)
|
||||
if v is None:
|
||||
if v is not None:
|
||||
return {
|
||||
"id": v.id,
|
||||
"description": v.description,
|
||||
"like_count": v.like_count,
|
||||
"in_db": True,
|
||||
"channel_id": v.channel_id,
|
||||
"channel_title": v.channel.title if v.channel else None,
|
||||
"published_at": v.published_at.isoformat() if v.published_at else None,
|
||||
"view_count": v.view_count,
|
||||
"duration_seconds": v.duration_seconds,
|
||||
}
|
||||
|
||||
try:
|
||||
with quota.attribute(user.id, "video_lookup"), YouTubeClient(db, user) as yt:
|
||||
items = yt.get_videos([video_id])
|
||||
except YouTubeError as exc:
|
||||
raise HTTPException(status_code=502, detail=f"YouTube lookup failed: {exc}")
|
||||
if not items:
|
||||
raise HTTPException(status_code=404, detail="Unknown video")
|
||||
|
||||
snippet = items[0].get("snippet", {})
|
||||
stats = items[0].get("statistics", {})
|
||||
likes = stats.get("likeCount")
|
||||
views = stats.get("viewCount")
|
||||
return {
|
||||
"id": v.id,
|
||||
"description": v.description,
|
||||
"like_count": v.like_count,
|
||||
"id": video_id,
|
||||
"description": snippet.get("description"),
|
||||
"like_count": int(likes) if likes is not None else None,
|
||||
"in_db": False,
|
||||
"channel_id": snippet.get("channelId"),
|
||||
"channel_title": snippet.get("channelTitle"),
|
||||
"published_at": snippet.get("publishedAt"),
|
||||
"view_count": int(views) if views is not None else None,
|
||||
"duration_seconds": parse_iso8601_duration(
|
||||
items[0].get("contentDetails", {}).get("duration")
|
||||
),
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue