From c090a395dc2dd2ea9101eacf485f95ad511bb78e Mon Sep 17 00:00:00 2001
From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com>
Date: Tue, 30 Dec 2025 19:52:04 -0800
Subject: [PATCH] feat(videos): add video icons to setlists and song versions
- 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
---
backend/models.py | 3 +-
backend/routers/shows.py | 30 ++++++++++++-------
backend/routers/songs.py | 18 ++++++++++-
.../components/songs/performance-list.tsx | 14 ++++++++-
4 files changed, 51 insertions(+), 14 deletions(-)
diff --git a/backend/models.py b/backend/models.py
index afc987f..e5454a4 100644
--- a/backend/models.py
+++ b/backend/models.py
@@ -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):
diff --git a/backend/routers/shows.py b/backend/routers/shows.py
index 40c96a7..ba15933 100644
--- a/backend/routers/shows.py
+++ b/backend/routers/shows.py
@@ -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
diff --git a/backend/routers/songs.py b/backend/routers/songs.py
index 979be79..0156e7b 100644
--- a/backend/routers/songs.py
+++ b/backend/routers/songs.py
@@ -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()
@@ -97,9 +104,18 @@ def read_song(slug: str, session: Session = Depends(get_session)):
venue_state = p.show.venue.state
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,
diff --git a/frontend/components/songs/performance-list.tsx b/frontend/components/songs/performance-list.tsx
index da963c0..b65dd11 100644
--- a/frontend/components/songs/performance-list.tsx
+++ b/frontend/components/songs/performance-list.tsx
@@ -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) {
{perf.set_name || "Set ?"}
+ {perf.youtube_link && (
+
+