Fix duplicate keys, optimize images, and update Next.js config
This commit is contained in:
parent
d50c2721d2
commit
7a58c2357d
5 changed files with 52 additions and 18 deletions
|
|
@ -317,14 +317,18 @@ async def get_playlist(id: str):
|
||||||
# Safely extract album
|
# Safely extract album
|
||||||
album_name = extract_album_name(track, playlist_data.get('title', 'Single'))
|
album_name = extract_album_name(track, playlist_data.get('title', 'Single'))
|
||||||
|
|
||||||
|
video_id = track.get('videoId')
|
||||||
|
if not video_id:
|
||||||
|
continue
|
||||||
|
|
||||||
formatted_tracks.append({
|
formatted_tracks.append({
|
||||||
"title": track.get('title', 'Unknown Title'),
|
"title": track.get('title', 'Unknown Title'),
|
||||||
"artist": artist_names,
|
"artist": artist_names,
|
||||||
"album": album_name,
|
"album": album_name,
|
||||||
"duration": track.get('duration_seconds', track.get('length_seconds', 0)),
|
"duration": track.get('duration_seconds', track.get('length_seconds', 0)),
|
||||||
"cover_url": cover_url,
|
"cover_url": cover_url,
|
||||||
"id": track.get('videoId'),
|
"id": video_id,
|
||||||
"url": f"https://music.youtube.com/watch?v={track.get('videoId')}"
|
"url": f"https://music.youtube.com/watch?v={video_id}"
|
||||||
})
|
})
|
||||||
|
|
||||||
# Get Playlist Cover (usually highest res)
|
# Get Playlist Cover (usually highest res)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import Image from 'next/image';
|
||||||
import { usePlayer } from "@/context/PlayerContext";
|
import { usePlayer } from "@/context/PlayerContext";
|
||||||
import { Play, Pause, Clock, Heart, MoreHorizontal, Plus } from "lucide-react";
|
import { Play, Pause, Clock, Heart, MoreHorizontal, Plus } from "lucide-react";
|
||||||
import { useEffect, useState, Suspense } from "react";
|
import { useEffect, useState, Suspense } from "react";
|
||||||
|
|
@ -127,7 +129,16 @@ function PlaylistContent() {
|
||||||
<div className="h-full overflow-y-auto no-scrollbar bg-gradient-to-b from-gray-900 via-[#121212] to-[#121212]">
|
<div className="h-full overflow-y-auto no-scrollbar bg-gradient-to-b from-gray-900 via-[#121212] to-[#121212]">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col md:flex-row items-center md:items-end gap-6 p-8 bg-gradient-to-b from-transparent to-black/20 pt-20 text-center md:text-left">
|
<div className="flex flex-col md:flex-row items-center md:items-end gap-6 p-8 bg-gradient-to-b from-transparent to-black/20 pt-20 text-center md:text-left">
|
||||||
<img src={playlist.cover_url} alt={playlist.title} className="w-52 h-52 md:w-60 md:h-60 shadow-2xl rounded-md object-cover" />
|
<div className="relative w-52 h-52 md:w-60 md:h-60 shadow-2xl rounded-md overflow-hidden shrink-0">
|
||||||
|
<Image
|
||||||
|
src={playlist.cover_url}
|
||||||
|
alt={playlist.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
sizes="(max-width: 768px) 208px, 240px"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-2 w-full md:w-auto">
|
<div className="flex flex-col gap-2 w-full md:w-auto">
|
||||||
<span className="text-sm font-bold uppercase hidden md:block">Playlist</span>
|
<span className="text-sm font-bold uppercase hidden md:block">Playlist</span>
|
||||||
<h1 className="text-2xl md:text-6xl font-black tracking-tight text-white mb-2 md:mb-4 line-clamp-2 leading-tight">{playlist.title}</h1>
|
<h1 className="text-2xl md:text-6xl font-black tracking-tight text-white mb-2 md:mb-4 line-clamp-2 leading-tight">{playlist.title}</h1>
|
||||||
|
|
@ -179,7 +190,7 @@ function PlaylistContent() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{playlist.tracks.map((track, i) => {
|
{playlist.tracks.filter(t => t.id).map((track, i) => {
|
||||||
const isCurrent = currentTrack?.id === track.id;
|
const isCurrent = currentTrack?.id === track.id;
|
||||||
const isLiked = likedTracks.has(track.id);
|
const isLiked = likedTracks.has(track.id);
|
||||||
return (
|
return (
|
||||||
|
|
@ -198,7 +209,15 @@ function PlaylistContent() {
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 min-w-0 overflow-hidden">
|
<div className="flex items-center gap-3 min-w-0 overflow-hidden">
|
||||||
<img src={track.cover_url} className="w-10 h-10 rounded shadow-sm object-cover shrink-0" alt="" />
|
<div className="relative w-10 h-10 rounded shadow-sm overflow-hidden shrink-0">
|
||||||
|
<Image
|
||||||
|
src={track.cover_url}
|
||||||
|
alt={track.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
sizes="40px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col min-w-0 pr-2">
|
<div className="flex flex-col min-w-0 pr-2">
|
||||||
{/* Changed from truncate to line-clamp-2 for readability */}
|
{/* Changed from truncate to line-clamp-2 for readability */}
|
||||||
<span className={`font-semibold text-base leading-tight line-clamp-2 break-words ${isCurrent ? 'text-green-500' : 'text-white'}`}>{track.title}</span>
|
<span className={`font-semibold text-base leading-tight line-clamp-2 break-words ${isCurrent ? 'text-green-500' : 'text-white'}`}>{track.title}</span>
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ function getGradient(text: string): string {
|
||||||
return gradients[Math.abs(hash) % gradients.length];
|
return gradients[Math.abs(hash) % gradients.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function CoverImage({ src, alt, className = "", fallbackText }: CoverImageProps) {
|
export default function CoverImage({ src, alt, className = "", fallbackText }: CoverImageProps) {
|
||||||
const [hasError, setHasError] = useState(false);
|
const [hasError, setHasError] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
@ -53,24 +55,26 @@ export default function CoverImage({ src, alt, className = "", fallbackText }: C
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={`relative overflow-hidden ${className} bg-[#282828]`}>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div
|
<div
|
||||||
className={`bg-gradient-to-br ${gradient} flex items-center justify-center text-white animate-pulse ${className}`}
|
className={`absolute inset-0 bg-gradient-to-br ${gradient} flex items-center justify-center text-white animate-pulse z-10`}
|
||||||
>
|
>
|
||||||
<span className="opacity-50">♪</span>
|
<span className="opacity-50">♪</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<img
|
<Image
|
||||||
src={src}
|
src={src}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
className={`${className} ${isLoading ? 'hidden' : ''}`}
|
fill
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||||
|
className={`object-cover ${isLoading ? 'opacity-0' : 'opacity-100'} transition-opacity duration-300`}
|
||||||
onLoad={() => setIsLoading(false)}
|
onLoad={() => setIsLoading(false)}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
setHasError(true);
|
setHasError(true);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -163,13 +163,12 @@ export default function Sidebar() {
|
||||||
{showAlbums && albums.map((album) => (
|
{showAlbums && albums.map((album) => (
|
||||||
<Link href={`/search?q=${encodeURIComponent(album.title)}`} key={album.id}>
|
<Link href={`/search?q=${encodeURIComponent(album.title)}`} key={album.id}>
|
||||||
<div className="flex items-center gap-3 p-2 rounded-md hover:bg-[#1a1a1a] cursor-pointer">
|
<div className="flex items-center gap-3 p-2 rounded-md hover:bg-[#1a1a1a] cursor-pointer">
|
||||||
<div className="w-12 h-12 bg-[#282828] rounded flex items-center justify-center overflow-hidden relative">
|
<CoverImage
|
||||||
{album.cover_url ? (
|
src={album.cover_url}
|
||||||
<img src={album.cover_url} alt="" className="w-full h-full object-cover" onError={(e) => e.currentTarget.style.display = 'none'} />
|
alt={album.title}
|
||||||
) : (
|
className="w-12 h-12 rounded object-cover"
|
||||||
<span className="text-xl">💿</span>
|
fallbackText="💿"
|
||||||
)}
|
/>
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="text-white font-medium truncate">{album.title}</h3>
|
<h3 className="text-white font-medium truncate">{album.title}</h3>
|
||||||
<p className="text-sm text-spotify-text-muted truncate">Album • {album.creator || 'Spotify'}</p>
|
<p className="text-sm text-spotify-text-muted truncate">Album • {album.creator || 'Spotify'}</p>
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,14 @@ const nextConfig = {
|
||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname: 'lh3.googleusercontent.com',
|
hostname: 'lh3.googleusercontent.com',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'yt3.googleusercontent.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'yt3.ggpht.com',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname: 'placehold.co',
|
hostname: 'placehold.co',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue