From 8df513b84faf24845045e31e32823a2d46abd017 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Sun, 21 Dec 2025 12:58:32 -0800 Subject: [PATCH] feat: Add YouTube link support for shows, songs, and performances --- backend/migrations/add_youtube_links.py | 24 +++++++++++++ backend/models.py | 3 ++ frontend/app/shows/[id]/page.tsx | 17 ++++++++- frontend/components/ui/youtube-embed.tsx | 45 ++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 backend/migrations/add_youtube_links.py create mode 100644 frontend/components/ui/youtube-embed.tsx diff --git a/backend/migrations/add_youtube_links.py b/backend/migrations/add_youtube_links.py new file mode 100644 index 0000000..11c4dc4 --- /dev/null +++ b/backend/migrations/add_youtube_links.py @@ -0,0 +1,24 @@ +""" +Migration to add youtube_link column to show, song, and performance tables. +""" +from sqlmodel import Session, create_engine, text +from database import DATABASE_URL + +def add_youtube_link_columns(): + engine = create_engine(DATABASE_URL) + + tables = ['show', 'song', 'performance'] + + with Session(engine) as session: + for table in tables: + try: + session.exec(text(f""" + ALTER TABLE "{table}" ADD COLUMN IF NOT EXISTS youtube_link VARCHAR + """)) + session.commit() + print(f"✅ Added youtube_link to {table}") + except Exception as e: + print(f"⚠️ {table}: {e}") + +if __name__ == "__main__": + add_youtube_link_columns() diff --git a/backend/models.py b/backend/models.py index 58391b5..1771791 100644 --- a/backend/models.py +++ b/backend/models.py @@ -13,6 +13,7 @@ class Performance(SQLModel, table=True): segue: bool = Field(default=False, description="Transition to next song >") notes: Optional[str] = Field(default=None) track_url: Optional[str] = Field(default=None, description="Deep link to track audio") + youtube_link: Optional[str] = Field(default=None, description="YouTube video URL") nicknames: List["PerformanceNickname"] = Relationship(back_populates="performance") show: "Show" = Relationship(back_populates="performances") @@ -97,6 +98,7 @@ class Show(SQLModel, table=True): # External Links bandcamp_link: Optional[str] = Field(default=None) nugs_link: Optional[str] = Field(default=None) + youtube_link: Optional[str] = Field(default=None) vertical: Vertical = Relationship(back_populates="shows") venue: Optional[Venue] = Relationship(back_populates="shows") @@ -110,6 +112,7 @@ class Song(SQLModel, table=True): original_artist: Optional[str] = Field(default=None) vertical_id: int = Field(foreign_key="vertical.id") notes: Optional[str] = Field(default=None) + youtube_link: Optional[str] = Field(default=None) vertical: Vertical = Relationship(back_populates="songs") diff --git a/frontend/app/shows/[id]/page.tsx b/frontend/app/shows/[id]/page.tsx index e26b05e..5185bf1 100644 --- a/frontend/app/shows/[id]/page.tsx +++ b/frontend/app/shows/[id]/page.tsx @@ -1,7 +1,7 @@ import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { ArrowLeft, Calendar, MapPin, Music2, Disc, PlayCircle, ExternalLink } from "lucide-react" +import { ArrowLeft, Calendar, MapPin, Music2, Disc, PlayCircle, ExternalLink, Youtube } from "lucide-react" import Link from "next/link" import { CommentSection } from "@/components/social/comment-section" import { EntityRating } from "@/components/social/entity-rating" @@ -13,6 +13,7 @@ import { notFound } from "next/navigation" import { SuggestNicknameDialog } from "@/components/shows/suggest-nickname-dialog" import { EntityReviews } from "@/components/reviews/entity-reviews" import { getApiUrl } from "@/lib/api-config" +import { YouTubeEmbed } from "@/components/ui/youtube-embed" async function getShow(id: string) { try { @@ -136,6 +137,20 @@ export default async function ShowDetailPage({ params }: { params: Promise<{ id: )} + {show.youtube_link && ( + + + + + Video + + + + + + + )} +
diff --git a/frontend/components/ui/youtube-embed.tsx b/frontend/components/ui/youtube-embed.tsx new file mode 100644 index 0000000..9abeb6f --- /dev/null +++ b/frontend/components/ui/youtube-embed.tsx @@ -0,0 +1,45 @@ +"use client" + +import { useMemo } from "react" + +interface YouTubeEmbedProps { + url: string + title?: string +} + +function extractVideoId(url: string): string | null { + // Handle various YouTube URL formats + const patterns = [ + /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&?/]+)/, + /youtube\.com\/v\/([^&?/]+)/, + /youtube\.com\/shorts\/([^&?/]+)/ + ] + + for (const pattern of patterns) { + const match = url.match(pattern) + if (match && match[1]) { + return match[1] + } + } + return null +} + +export function YouTubeEmbed({ url, title = "YouTube video" }: YouTubeEmbedProps) { + const videoId = useMemo(() => extractVideoId(url), [url]) + + if (!videoId) { + return null + } + + return ( +
+