From 01ca6545fd3626aa0381edf7312a744b50605933 Mon Sep 17 00:00:00 2001 From: Aidan Date: Fri, 28 Mar 2025 15:00:11 -0400 Subject: [PATCH] security/feat: no need for api on lastplayed, greatly improve logging+interface+ status of music widget --- app/api/now-playing/route.ts | 21 +++++ app/globals.css | 136 ++++++++++++++++++++++++++++++ components.json | 21 +++++ components/ui/progress.tsx | 31 +++++++ components/widgets/LastPlayed.tsx | 87 ++++++++++--------- lib/utils.ts | 6 ++ package.json | 9 +- 7 files changed, 268 insertions(+), 43 deletions(-) create mode 100644 app/api/now-playing/route.ts create mode 100644 components.json create mode 100644 components/ui/progress.tsx create mode 100644 lib/utils.ts diff --git a/app/api/now-playing/route.ts b/app/api/now-playing/route.ts new file mode 100644 index 0000000..3ef59cf --- /dev/null +++ b/app/api/now-playing/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server' + +export async function GET() { + try { + const response = await fetch("https://api.listenbrainz.org/1/user/p0ntus/playing-now", { + headers: { + Authorization: `Token ${process.env.LISTENBRAINZ_TOKEN}`, + }, + }) + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + console.error('Error fetching now playing:', error) + return NextResponse.json({ error: 'Failed to fetch now playing data' }, { status: 500 }) + } +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index 78de090..e05ccc7 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,4 +1,8 @@ @import 'tailwindcss'; +/* + ---break--- +*/ +@custom-variant dark (&:is(.dark *)); @theme { --color-background: var(--background); @@ -71,3 +75,135 @@ html { margin-left: 2rem; } +/* + ---break--- +*/ + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +/* + ---break--- +*/ + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +/* + ---break--- +*/ + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +/* + ---break--- +*/ + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + diff --git a/components.json b/components.json new file mode 100644 index 0000000..dea737b --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx new file mode 100644 index 0000000..e7a416c --- /dev/null +++ b/components/ui/progress.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +function Progress({ + className, + value, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { Progress } diff --git a/components/widgets/LastPlayed.tsx b/components/widgets/LastPlayed.tsx index 44fa1a8..b504d69 100644 --- a/components/widgets/LastPlayed.tsx +++ b/components/widgets/LastPlayed.tsx @@ -3,9 +3,9 @@ import type React from "react" import { useEffect, useState, useCallback, useRef } from "react" import Image from "next/image" -import { Music, ExternalLink, Disc, User, Loader2 } from "lucide-react" +import { Music, ExternalLink, Disc, User, Loader2, AlertCircle } from "lucide-react" import Marquee from "react-fast-marquee" - +import { Progress } from "@/components/ui/progress" interface Track { track_name: string artist_name: string @@ -47,110 +47,120 @@ const NowPlaying: React.FC = () => { const [loading, setLoading] = useState(true) const [loadingStatus, setLoadingStatus] = useState("Initializing") const [error, setError] = useState(null) + const [progress, setProgress] = useState(0) + const [steps, setSteps] = useState(0) + + const updateProgress = useCallback((current: number, total: number, status: string) => { + setProgress(current) + setSteps(total) + setLoadingStatus(status) + console.log(`[${current}/${total}] ${status}`) + }, []) const fetchAlbumArt = useCallback(async (artist: string, album?: string) => { if (!album) { - console.log("[i] no album found") + updateProgress(0, 0, "No album found") setCoverArt(null) setLoading(false) return } try { - console.log("[i] fetching album art: ", artist, album) + updateProgress(2, 3, `Searching for album: ${artist} - ${album}`) const response = await fetch( `https://musicbrainz.org/ws/2/release/?query=artist:${encodeURIComponent( artist )}%20AND%20release:${encodeURIComponent(album)}&fmt=json` ) if (!response.ok) { - console.log("[!] album art fetch error:", response.status) - throw new Error(`HTTP error! status: ${response.status}`) + updateProgress(0, 0, `Album art fetch error: ${response.status}`) + setError("Error fetching album art (see console for details)") + setLoading(false) + return } const data = await response.json() if (data.releases && data.releases.length > 0) { const mbid = data.releases[0].id - console.log("✅ mbid found:", mbid) + updateProgress(3, 3, "Fetching cover art...") setTrack(prev => prev ? { ...prev, mbid: `${mbid || null}` } : { track_name: "", artist_name: "", release_name: undefined, mbid: `${mbid || null}` }) const coverArtResponse = await fetch(`https://coverartarchive.org/release/${mbid}/front-250`) if (coverArtResponse.ok) { - console.log("✅ cover art found") setCoverArt(coverArtResponse.url) + setLoading(false) } else { - console.log("[i] cover art not found!") + updateProgress(0, 0, "Cover art not found") setCoverArt(null) + setLoading(false) } } else { - console.log("[i] no releases in data!") + updateProgress(0, 0, "No releases found") setCoverArt(null) + setLoading(false) } } catch (error) { - console.log("[!] album art error", error) + updateProgress(0, 0, `Error: ${error}`) setCoverArt(null) - } finally { setLoading(false) - console.log("[i] album art done") } - }, []) + }, [updateProgress]) const fetchNowPlaying = useCallback(async () => { - setLoadingStatus("Fetching now playing") - console.log("[i] fetching now playing...") + updateProgress(1, 3, "Fetching current listen...") try { - const response = await fetch("https://api.listenbrainz.org/1/user/p0ntus/playing-now", { - headers: { - Authorization: `Token ${process.env.NEXT_PUBLIC_LISTENBRAINZ_TOKEN}`, - }, - }) - if (!response.ok) { - console.log("[!] now playing error:", response.status) - throw new Error(`HTTP error! Status: ${response.status}`) - } + const response = await fetch("https://api.listenbrainz.org/1/user/p0ntus/playing-now") const data = await response.json() + if (data.payload.count > 0 && data.payload.listens[0].track_metadata) { const trackMetadata = data.payload.listens[0].track_metadata - console.log("✅ track found: ", trackMetadata.track_name) - console.log("[i] song details: ", trackMetadata.track_name, "-", trackMetadata.artist_name, "/", trackMetadata.release_name) + console.log("= TRACK METADATA =") + if (trackMetadata.track_name) { console.log("🎵", trackMetadata.track_name) } + if (trackMetadata.artist_name) { console.log("🎤", trackMetadata.artist_name) } + if (trackMetadata.release_name) { console.log("💿", trackMetadata.release_name) } setTrack({ track_name: trackMetadata.track_name, artist_name: trackMetadata.artist_name, release_name: trackMetadata.release_name, mbid: trackMetadata.mbid, }) - setLoadingStatus("Fetching album art") + updateProgress(2, 3, "Finding album art...") await fetchAlbumArt(trackMetadata.artist_name, trackMetadata.release_name) } else { - console.log("[i] no track playing") - setLoadingStatus("No track playing") + updateProgress(0, 0, "No track playing") setLoading(false) } } catch (error) { - console.log("[!] now playing error:", error) + updateProgress(0, 0, `Error: ${error}`) setError("Error fetching now playing data") setLoading(false) } - }, [fetchAlbumArt]) + }, [fetchAlbumArt, updateProgress]) useEffect(() => { fetchNowPlaying() }, [fetchNowPlaying]) if (loading) { + console.log("[LastPlayed] Loading state rendered") return (

Now Playing

+ 0 ? (progress * 100) / steps : 0} className="mb-4" />
-

{loadingStatus}

+

+ {loadingStatus} {steps > 0 && `(${progress}/${steps})`} +

) } if (error) { + console.log("[LastPlayed] Error state rendered") return (

Now Playing

+

{error}

@@ -158,16 +168,11 @@ const NowPlaying: React.FC = () => { } if (!track) { - return ( -
-

Now Playing

-
-

No track playing

-
-
- ) + console.log("[LastPlayed] Hidden due to no track data") + return null } + console.log("[LastPlayed] Rendered track:", track.track_name) return (

Now Playing

diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/package.json b/package.json index 77bd707..41a3935 100644 --- a/package.json +++ b/package.json @@ -13,17 +13,22 @@ "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", + "@radix-ui/react-progress": "^1.1.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "geist": "^1.3.1", "i18next": "^24.2.3", "i18next-browser-languagedetector": "^8.0.4", - "lucide-react": "^0.469.0", + "lucide-react": "^0.485.0", "next": "^15.2.4", "react": "^19.0.0", "react-dom": "^19.0.0", "react-fast-marquee": "^1.6.5", "react-i18next": "^15.4.1", "react-icons": "^5.5.0", - "tailwindcss-animate": "^1.0.7" + "tailwind-merge": "^3.0.2", + "tailwindcss-animate": "^1.0.7", + "tw-animate-css": "^1.2.5" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1",