Fix duplicate keys, optimize images, and update Next.js config

This commit is contained in:
Khoa Vo 2026-01-01 16:02:07 +07:00
parent d50c2721d2
commit 7a58c2357d
5 changed files with 52 additions and 18 deletions

View file

@ -317,14 +317,18 @@ async def get_playlist(id: str):
# Safely extract album
album_name = extract_album_name(track, playlist_data.get('title', 'Single'))
video_id = track.get('videoId')
if not video_id:
continue
formatted_tracks.append({
"title": track.get('title', 'Unknown Title'),
"artist": artist_names,
"album": album_name,
"duration": track.get('duration_seconds', track.get('length_seconds', 0)),
"cover_url": cover_url,
"id": track.get('videoId'),
"url": f"https://music.youtube.com/watch?v={track.get('videoId')}"
"id": video_id,
"url": f"https://music.youtube.com/watch?v={video_id}"
})
# Get Playlist Cover (usually highest res)

View file

@ -1,5 +1,7 @@
"use client";
import Link from 'next/link';
import Image from 'next/image';
import { usePlayer } from "@/context/PlayerContext";
import { Play, Pause, Clock, Heart, MoreHorizontal, Plus } from "lucide-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]">
{/* 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">
<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">
<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>
@ -179,7 +190,7 @@ function PlaylistContent() {
</div>
<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 isLiked = likedTracks.has(track.id);
return (
@ -198,7 +209,15 @@ function PlaylistContent() {
</span>
<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">
{/* 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>

View file

@ -32,6 +32,8 @@ function getGradient(text: string): string {
return gradients[Math.abs(hash) % gradients.length];
}
import Image from "next/image";
export default function CoverImage({ src, alt, className = "", fallbackText }: CoverImageProps) {
const [hasError, setHasError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
@ -53,24 +55,26 @@ export default function CoverImage({ src, alt, className = "", fallbackText }: C
}
return (
<>
<div className={`relative overflow-hidden ${className} bg-[#282828]`}>
{isLoading && (
<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>
</div>
)}
<img
<Image
src={src}
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)}
onError={() => {
setHasError(true);
setIsLoading(false);
}}
/>
</>
</div>
);
}

View file

@ -161,15 +161,14 @@ export default function Sidebar() {
{/* Albums */}
{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="w-12 h-12 bg-[#282828] rounded flex items-center justify-center overflow-hidden relative">
{album.cover_url ? (
<img src={album.cover_url} alt="" className="w-full h-full object-cover" onError={(e) => e.currentTarget.style.display = 'none'} />
) : (
<span className="text-xl">💿</span>
)}
</div>
<CoverImage
src={album.cover_url}
alt={album.title}
className="w-12 h-12 rounded object-cover"
fallbackText="💿"
/>
<div className="flex-1 min-w-0">
<h3 className="text-white font-medium truncate">{album.title}</h3>
<p className="text-sm text-spotify-text-muted truncate">Album {album.creator || 'Spotify'}</p>

View file

@ -36,6 +36,14 @@ const nextConfig = {
protocol: 'https',
hostname: 'lh3.googleusercontent.com',
},
{
protocol: 'https',
hostname: 'yt3.googleusercontent.com',
},
{
protocol: 'https',
hostname: 'yt3.ggpht.com',
},
{
protocol: 'https',
hostname: 'placehold.co',