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 && ( + + + + )}
{perf.venue_name} • {perf.venue_city}{perf.venue_state ? `, ${perf.venue_state}` : ""}