mirror of
https://github.com/wahyd4/you-music.git
synced 2026-08-08 20:59:47 +10:00
Update logic of artist avatar fetching
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) ? (
|
||||
<img
|
||||
src={music.thumbnail.startsWith('http') ? music.thumbnail : `/music/${music.thumbnail}`}
|
||||
src={getMusicThumbnail(music)!}
|
||||
alt={music.title}
|
||||
className="w-12 h-12 rounded object-cover flex-shrink-0"
|
||||
onError={(e) => {
|
||||
// Fallback to default icon if image fails to load
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = 'none'
|
||||
target.nextElementSibling?.classList.remove('hidden')
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded bg-secondary flex items-center justify-center flex-shrink-0">
|
||||
) : null}
|
||||
<div className={`w-12 h-12 rounded bg-secondary flex items-center justify-center flex-shrink-0 ${getMusicThumbnail(music) ? 'hidden' : ''}`}>
|
||||
<Play className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -243,17 +249,21 @@ export default function MusicLibrary({ onPlayMusic, currentMusic, onPlayNext }:
|
||||
className="group relative flex flex-col gap-2 p-3 rounded-lg hover:bg-accent transition-colors"
|
||||
>
|
||||
<div className="relative aspect-square">
|
||||
{music.thumbnail ? (
|
||||
{getMusicThumbnail(music) ? (
|
||||
<img
|
||||
src={music.thumbnail.startsWith('http') ? music.thumbnail : `/music/${music.thumbnail}`}
|
||||
src={getMusicThumbnail(music)!}
|
||||
alt={music.title}
|
||||
className="w-full h-full rounded object-cover"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = 'none'
|
||||
target.nextElementSibling?.classList.remove('hidden')
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full rounded bg-secondary flex items-center justify-center">
|
||||
) : null}
|
||||
<div className={`w-full h-full rounded bg-secondary flex items-center justify-center ${getMusicThumbnail(music) ? 'hidden' : ''}`}>
|
||||
<Play className="h-12 w-12 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity rounded flex items-center justify-center gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Music } from '@/types'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { X, Play, Pause, SkipBack, SkipForward, Heart, ListPlus, Share2, MessageSquareText, Repeat, Repeat1, Shuffle } from 'lucide-react'
|
||||
import { formatDuration } from '@/lib/utils'
|
||||
import { formatDuration, getMusicThumbnail } from '@/lib/utils'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { musicApi } from '@/api/client'
|
||||
import React from 'react'
|
||||
@@ -113,17 +113,21 @@ export default function FullScreenPlayer({
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-8 overflow-y-auto">
|
||||
{/* Large Album Art */}
|
||||
<div className={`w-full max-w-lg ${showLyrics ? 'mb-4 md:mb-8' : 'mb-8'}`}>
|
||||
{currentMusic.thumbnail ? (
|
||||
{getMusicThumbnail(currentMusic) ? (
|
||||
<img
|
||||
src={currentMusic.thumbnail.startsWith('http') ? currentMusic.thumbnail : `/music/${currentMusic.thumbnail}`}
|
||||
src={getMusicThumbnail(currentMusic)!}
|
||||
alt={currentMusic.title}
|
||||
className={`w-full aspect-square rounded-2xl object-cover shadow-2xl ${showLyrics ? 'max-w-xs md:max-w-lg mx-auto' : ''}`}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = 'none'
|
||||
target.nextElementSibling?.classList.remove('hidden')
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className={`w-full aspect-square rounded-2xl bg-secondary flex items-center justify-center shadow-2xl ${showLyrics ? 'max-w-xs md:max-w-lg mx-auto' : ''}`}>
|
||||
) : null}
|
||||
<div className={`w-full aspect-square rounded-2xl bg-secondary flex items-center justify-center shadow-2xl ${showLyrics ? 'max-w-xs md:max-w-lg mx-auto' : ''} ${getMusicThumbnail(currentMusic) ? 'hidden' : ''}`}>
|
||||
<Play className={showLyrics ? 'h-24 w-24 md:h-32 md:w-32 text-muted-foreground' : 'h-32 w-32 text-muted-foreground'} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Song Info */}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Music } from '@/types'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Play, Pause, SkipBack, SkipForward, Volume2, VolumeX, Heart, ListPlus, Repeat, Repeat1, Shuffle, Share2, Maximize2, MessageSquareText, Edit } from 'lucide-react'
|
||||
import { formatDuration } from '@/lib/utils'
|
||||
import { formatDuration, getMusicThumbnail } from '@/lib/utils'
|
||||
import { playlistApi, musicApi } from '@/api/client'
|
||||
import { toast } from 'sonner'
|
||||
import PlaylistSelector from './PlaylistSelector'
|
||||
@@ -117,12 +117,13 @@ export default function Player({
|
||||
useEffect(() => {
|
||||
if (!currentMusic || !('mediaSession' in navigator)) return
|
||||
|
||||
const getAbsoluteImageUrl = (thumbnail: string | null) => {
|
||||
if (!thumbnail) return undefined
|
||||
if (thumbnail.startsWith('http')) return thumbnail
|
||||
const thumbnailUrl = getMusicThumbnail(currentMusic)
|
||||
const getAbsoluteImageUrl = (url: string | null) => {
|
||||
if (!url) return undefined
|
||||
if (url.startsWith('http')) return url
|
||||
// Convert relative path to absolute URL
|
||||
const baseUrl = window.location.origin
|
||||
return `${baseUrl}/music/${thumbnail}`
|
||||
return `${baseUrl}${url.startsWith('/') ? '' : '/'}${url}`
|
||||
}
|
||||
|
||||
// Set metadata for lock screen
|
||||
@@ -130,8 +131,8 @@ export default function Player({
|
||||
title: currentMusic.title,
|
||||
artist: currentMusic.artist || 'Unknown Artist',
|
||||
album: currentMusic.album || '',
|
||||
artwork: currentMusic.thumbnail ? [
|
||||
{ src: getAbsoluteImageUrl(currentMusic.thumbnail) || '', sizes: '512x512', type: 'image/jpeg' },
|
||||
artwork: thumbnailUrl ? [
|
||||
{ src: getAbsoluteImageUrl(thumbnailUrl) || '', sizes: '512x512', type: 'image/jpeg' },
|
||||
] : undefined,
|
||||
})
|
||||
|
||||
@@ -265,21 +266,25 @@ export default function Player({
|
||||
<div className="flex items-center justify-between gap-2 md:gap-4">
|
||||
{/* Album art and music info */}
|
||||
<div className="flex items-center gap-2 md:gap-3 flex-1 min-w-0">
|
||||
{currentMusic.thumbnail ? (
|
||||
{getMusicThumbnail(currentMusic) ? (
|
||||
<img
|
||||
src={currentMusic.thumbnail.startsWith('http') ? currentMusic.thumbnail : `/music/${currentMusic.thumbnail}`}
|
||||
src={getMusicThumbnail(currentMusic)!}
|
||||
alt={currentMusic.title}
|
||||
className="w-12 h-12 md:w-14 md:h-14 rounded object-cover cursor-pointer hover:opacity-80 transition-opacity"
|
||||
onClick={() => setShowFullScreen(true)}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = 'none'
|
||||
target.nextElementSibling?.classList.remove('hidden')
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
) : null}
|
||||
<div
|
||||
className="w-12 h-12 md:w-14 md:h-14 rounded bg-secondary flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity"
|
||||
className={`w-12 h-12 md:w-14 md:h-14 rounded bg-secondary flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity ${getMusicThumbnail(currentMusic) ? 'hidden' : ''}`}
|
||||
onClick={() => setShowFullScreen(true)}
|
||||
>
|
||||
<Play className="h-6 w-6 md:h-7 md:w-7 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3
|
||||
className="font-semibold truncate cursor-pointer hover:underline"
|
||||
|
||||
@@ -47,3 +47,27 @@ export function formatArtist(artist: string | null | undefined, maxLength?: numb
|
||||
// Last resort
|
||||
return `+${artists.length}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the best available thumbnail for a music item
|
||||
* Priority: Music thumbnail → Artist image → null (for default icon)
|
||||
*/
|
||||
export function getMusicThumbnail(music: { thumbnail?: string | null, artist?: string | null }): string | null {
|
||||
// If music has a thumbnail, use it
|
||||
if (music.thumbnail) {
|
||||
return music.thumbnail.startsWith('http')
|
||||
? music.thumbnail
|
||||
: `/music/${music.thumbnail}`
|
||||
}
|
||||
|
||||
// If no thumbnail but has artist, try artist image
|
||||
if (music.artist && music.artist !== 'Unknown') {
|
||||
// Get first artist if multiple
|
||||
const firstArtist = music.artist.split(/[,/]/)[0].trim()
|
||||
// Return artist image path (backend will handle the lookup)
|
||||
return `/api/artists/${encodeURIComponent(firstArtist)}/image`
|
||||
}
|
||||
|
||||
// No thumbnail or artist available
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user