feat(videos): add video icons to setlists and song versions
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s

- Backend: Added video_links relationship to Performance model
- Backend: Updated shows and songs routers to eager-load videos and populate youtube_link
- Frontend: Added YouTube icon to performance list items if video exists
This commit is contained in:
fullsizemalt 2025-12-30 19:52:04 -08:00
parent 8e7be96991
commit c090a395dc
4 changed files with 51 additions and 14 deletions

View file

@ -22,6 +22,7 @@ class Performance(SQLModel, table=True):
nicknames: List["PerformanceNickname"] = Relationship(back_populates="performance")
show: "Show" = Relationship(back_populates="performances")
song: "Song" = Relationship()
video_links: List["VideoPerformance"] = Relationship(back_populates="performance")
class ShowArtist(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
@ -696,7 +697,7 @@ class VideoPerformance(SQLModel, table=True):
notes: Optional[str] = Field(default=None)
video: Video = Relationship(back_populates="performances")
performance: "Performance" = Relationship()
performance: "Performance" = Relationship(back_populates="video_links")
class VideoSong(SQLModel, table=True):

View file

@ -164,7 +164,17 @@ def read_upcoming_shows(
@router.get("/{slug}", response_model=ShowRead)
def read_show(slug: str, session: Session = Depends(get_session)):
show = session.exec(select(Show).where(Show.slug == slug)).first()
from sqlalchemy.orm import selectinload, joinedload
from models import Performance, VideoPerformance, Video, VideoPlatform
# Eager load relationships clearly
show = session.exec(
select(Show)
.options(
selectinload(Show.performances).selectinload(Performance.video_links).joinedload(VideoPerformance.video)
)
.where(Show.slug == slug)
).first()
if not show:
raise HTTPException(status_code=404, detail="Show not found")
@ -176,15 +186,6 @@ def read_show(slug: str, session: Session = Depends(get_session)):
.where(EntityTag.entity_id == show.id)
).all()
# Manually populate performances to ensure nicknames are filtered if needed
# (Though for now we just return all, or filter approved in schema if we had a custom getter)
# The relationship `show.performances` is already loaded if we access it, but we might want to sort.
# Re-fetch show with relationships if needed, or just rely on lazy loading + validation
# But for nicknames, we only want "approved" ones usually.
# Let's let the frontend filter or do it here.
# Doing it here is safer.
show_data = ShowRead.model_validate(show)
show_data.tags = tags
@ -195,10 +196,17 @@ def read_show(slug: str, session: Session = Depends(get_session)):
# Sort performances by position
sorted_perfs = sorted(show.performances, key=lambda p: p.position)
# Filter nicknames for each performance
# Process performances: Filter nicknames and populate video links
for perf in sorted_perfs:
perf.nicknames = [n for n in perf.nicknames if n.status == "approved"]
# Backfill youtube_link from Video entity if not present
if not perf.youtube_link and perf.video_links:
for link in perf.video_links:
if link.video and link.video.platform == VideoPlatform.YOUTUBE:
perf.youtube_link = link.video.url
break
show_data.performances = sorted_perfs
return show_data

View file

@ -59,10 +59,17 @@ def read_song(slug: str, session: Session = Depends(get_session)):
.where(EntityTag.entity_id == song_id)
).all()
# Fetch performances
# Fetch performances with video links
from sqlalchemy.orm import selectinload, joinedload
from models import VideoPerformance
from models import Video, VideoPlatform
perfs = session.exec(
select(Performance)
.join(Show)
.options(
selectinload(Performance.video_links).joinedload(VideoPerformance.video)
)
.where(Performance.song_id == song_id)
.order_by(Show.date.desc())
).all()
@ -98,8 +105,17 @@ def read_song(slug: str, session: Session = Depends(get_session)):
r_stats = rating_stats.get(p.id, {"avg": 0.0, "count": 0})
# Backfill youtube_link
youtube_link = p.youtube_link
if not youtube_link and p.video_links:
for link in p.video_links:
if link.video and link.video.platform == VideoPlatform.YOUTUBE:
youtube_link = link.video.url
break
perf_dtos.append(PerformanceReadWithShow(
**p.model_dump(),
youtube_link=youtube_link,
show_date=show_date,
show_slug=show_slug,
venue_name=venue_name,

View file

@ -5,7 +5,7 @@ import Link from "next/link"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { RatePerformanceDialog } from "@/components/songs/rate-performance-dialog"
import { Star, Music } from "lucide-react"
import { Star, Music, Youtube } from "lucide-react"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
export interface Performance {
@ -23,6 +23,7 @@ export interface Performance {
venue_state: string | null
avg_rating: number
total_reviews: number
youtube_link?: string | null
}
interface PerformanceListProps {
@ -98,6 +99,17 @@ export function PerformanceList({ performances }: PerformanceListProps) {
<span className="text-xs text-muted-foreground uppercase tracking-wider font-mono">
{perf.set_name || "Set ?"}
</span>
{perf.youtube_link && (
<a
href={perf.youtube_link}
target="_blank"
rel="noopener noreferrer"
className="text-red-500 hover:text-red-600 transition-colors"
title="Watch Video"
>
<Youtube className="h-4 w-4" />
</a>
)}
</div>
<div className="text-sm text-muted-foreground truncate">
{perf.venue_name} {perf.venue_city}{perf.venue_state ? `, ${perf.venue_state}` : ""}