diff --git a/backend/app/api/artist.py b/backend/app/api/artist.py
index c0da306..d1a1d59 100644
--- a/backend/app/api/artist.py
+++ b/backend/app/api/artist.py
@@ -482,6 +482,57 @@ async def get_artist_all_songs(
)
+@router.get("/{artist_name}/image")
+async def get_artist_image(artist_name: str, db: AsyncSession = Depends(get_db)):
+ """Get artist image (returns image file or 404)
+
+ Priority:
+ 1. Cached artist image from online APIs
+ 2. Thumbnail from artist's first song
+ 3. 404 (frontend will show default icon)
+ """
+ from fastapi.responses import FileResponse, Response
+ from fastapi import HTTPException
+
+ # 1. Try to get from artist info cache
+ cached_info = get_cached_artist_info(artist_name)
+ if cached_info and cached_info.image:
+ # Check if it's a local cache path
+ if not cached_info.image.startswith('http'):
+ # It's a relative path like "cache/artist_images/artist_name.jpg"
+ image_path = os.path.join(settings.BASE_DIR, "data", cached_info.image)
+ if os.path.exists(image_path):
+ return FileResponse(image_path, media_type="image/jpeg")
+
+ # If it's an HTTP URL (song thumbnail), redirect to it
+ if cached_info.image.startswith('http'):
+ return Response(status_code=307, headers={"Location": cached_info.image})
+
+ # 2. If not in cache, try to get from artist's songs
+ result = await db.execute(
+ select(Music)
+ .where(Music.artist == artist_name)
+ .where(Music.thumbnail.isnot(None))
+ .where(Music.thumbnail != "")
+ .limit(1)
+ )
+ song_with_thumbnail = result.scalar_one_or_none()
+
+ if song_with_thumbnail and song_with_thumbnail.thumbnail:
+ # Check if it's a local thumbnail
+ if not song_with_thumbnail.thumbnail.startswith('http'):
+ thumbnail_path = os.path.join(settings.MUSIC_DIR, song_with_thumbnail.thumbnail)
+ if os.path.exists(thumbnail_path):
+ return FileResponse(thumbnail_path, media_type="image/jpeg")
+
+ # If it's an HTTP URL, redirect
+ if song_with_thumbnail.thumbnail.startswith('http'):
+ return Response(status_code=307, headers={"Location": song_with_thumbnail.thumbnail})
+
+ # 3. No image found - return 404
+ raise HTTPException(status_code=404, detail="Artist image not found")
+
+
@router.get("/{artist_name}", response_model=List[MusicSchema])
async def get_artist_songs(
artist_name: str,
diff --git a/frontend/src/components/MusicLibrary.tsx b/frontend/src/components/MusicLibrary.tsx
index de98263..4d19b74 100644
--- a/frontend/src/components/MusicLibrary.tsx
+++ b/frontend/src/components/MusicLibrary.tsx
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Play, AlertCircle, Info, Search, LayoutList, LayoutGrid, ArrowUpDown, Loader2 } from 'lucide-react'
import MusicDetailModal from './music/MusicDetailModal'
+import { getMusicThumbnail } from '@/lib/utils'
import {
Select,
SelectContent,
@@ -187,17 +188,22 @@ export default function MusicLibrary({ onPlayMusic, currentMusic, onPlayNext }:
className="flex items-center gap-2 md:gap-4 p-2 md:p-3 rounded-lg hover:bg-accent transition-colors cursor-pointer"
onClick={() => music.file_exists && onPlayMusic(music, filteredMusic)}
>
- {music.thumbnail ? (
+ {getMusicThumbnail(music) ? (
{
+ // Fallback to default icon if image fails to load
+ const target = e.target as HTMLImageElement
+ target.style.display = 'none'
+ target.nextElementSibling?.classList.remove('hidden')
+ }}
/>
- ) : (
-