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])
|
@router.get("/{artist_name}", response_model=List[MusicSchema])
|
||||||
async def get_artist_songs(
|
async def get_artist_songs(
|
||||||
artist_name: str,
|
artist_name: str,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Play, AlertCircle, Info, Search, LayoutList, LayoutGrid, ArrowUpDown, Loader2 } from 'lucide-react'
|
import { Play, AlertCircle, Info, Search, LayoutList, LayoutGrid, ArrowUpDown, Loader2 } from 'lucide-react'
|
||||||
import MusicDetailModal from './music/MusicDetailModal'
|
import MusicDetailModal from './music/MusicDetailModal'
|
||||||
|
import { getMusicThumbnail } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
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"
|
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)}
|
onClick={() => music.file_exists && onPlayMusic(music, filteredMusic)}
|
||||||
>
|
>
|
||||||
{music.thumbnail ? (
|
{getMusicThumbnail(music) ? (
|
||||||
<img
|
<img
|
||||||
src={music.thumbnail.startsWith('http') ? music.thumbnail : `/music/${music.thumbnail}`}
|
src={getMusicThumbnail(music)!}
|
||||||
alt={music.title}
|
alt={music.title}
|
||||||
className="w-12 h-12 rounded object-cover flex-shrink-0"
|
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')
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : null}
|
||||||
<div className="w-12 h-12 rounded bg-secondary flex items-center justify-center flex-shrink-0">
|
<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" />
|
<Play className="h-6 w-6 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<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"
|
className="group relative flex flex-col gap-2 p-3 rounded-lg hover:bg-accent transition-colors"
|
||||||
>
|
>
|
||||||
<div className="relative aspect-square">
|
<div className="relative aspect-square">
|
||||||
{music.thumbnail ? (
|
{getMusicThumbnail(music) ? (
|
||||||
<img
|
<img
|
||||||
src={music.thumbnail.startsWith('http') ? music.thumbnail : `/music/${music.thumbnail}`}
|
src={getMusicThumbnail(music)!}
|
||||||
alt={music.title}
|
alt={music.title}
|
||||||
className="w-full h-full rounded object-cover"
|
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')
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : null}
|
||||||
<div className="w-full h-full rounded bg-secondary flex items-center justify-center">
|
<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" />
|
<Play className="h-12 w-12 text-muted-foreground" />
|
||||||
</div>
|
</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">
|
<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
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Music } from '@/types'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Slider } from '@/components/ui/slider'
|
import { Slider } from '@/components/ui/slider'
|
||||||
import { X, Play, Pause, SkipBack, SkipForward, Heart, ListPlus, Share2, MessageSquareText, Repeat, Repeat1, Shuffle } from 'lucide-react'
|
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 { useQuery } from '@tanstack/react-query'
|
||||||
import { musicApi } from '@/api/client'
|
import { musicApi } from '@/api/client'
|
||||||
import React from 'react'
|
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">
|
<div className="flex-1 flex flex-col items-center justify-center p-8 overflow-y-auto">
|
||||||
{/* Large Album Art */}
|
{/* Large Album Art */}
|
||||||
<div className={`w-full max-w-lg ${showLyrics ? 'mb-4 md:mb-8' : 'mb-8'}`}>
|
<div className={`w-full max-w-lg ${showLyrics ? 'mb-4 md:mb-8' : 'mb-8'}`}>
|
||||||
{currentMusic.thumbnail ? (
|
{getMusicThumbnail(currentMusic) ? (
|
||||||
<img
|
<img
|
||||||
src={currentMusic.thumbnail.startsWith('http') ? currentMusic.thumbnail : `/music/${currentMusic.thumbnail}`}
|
src={getMusicThumbnail(currentMusic)!}
|
||||||
alt={currentMusic.title}
|
alt={currentMusic.title}
|
||||||
className={`w-full aspect-square rounded-2xl object-cover shadow-2xl ${showLyrics ? 'max-w-xs md:max-w-lg mx-auto' : ''}`}
|
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')
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : 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' : ''}`}>
|
<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'} />
|
<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>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Song Info */}
|
{/* Song Info */}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Music } from '@/types'
|
|||||||
import { Slider } from '@/components/ui/slider'
|
import { Slider } from '@/components/ui/slider'
|
||||||
import { Button } from '@/components/ui/button'
|
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 { 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 { playlistApi, musicApi } from '@/api/client'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import PlaylistSelector from './PlaylistSelector'
|
import PlaylistSelector from './PlaylistSelector'
|
||||||
@@ -117,12 +117,13 @@ export default function Player({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentMusic || !('mediaSession' in navigator)) return
|
if (!currentMusic || !('mediaSession' in navigator)) return
|
||||||
|
|
||||||
const getAbsoluteImageUrl = (thumbnail: string | null) => {
|
const thumbnailUrl = getMusicThumbnail(currentMusic)
|
||||||
if (!thumbnail) return undefined
|
const getAbsoluteImageUrl = (url: string | null) => {
|
||||||
if (thumbnail.startsWith('http')) return thumbnail
|
if (!url) return undefined
|
||||||
|
if (url.startsWith('http')) return url
|
||||||
// Convert relative path to absolute URL
|
// Convert relative path to absolute URL
|
||||||
const baseUrl = window.location.origin
|
const baseUrl = window.location.origin
|
||||||
return `${baseUrl}/music/${thumbnail}`
|
return `${baseUrl}${url.startsWith('/') ? '' : '/'}${url}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set metadata for lock screen
|
// Set metadata for lock screen
|
||||||
@@ -130,8 +131,8 @@ export default function Player({
|
|||||||
title: currentMusic.title,
|
title: currentMusic.title,
|
||||||
artist: currentMusic.artist || 'Unknown Artist',
|
artist: currentMusic.artist || 'Unknown Artist',
|
||||||
album: currentMusic.album || '',
|
album: currentMusic.album || '',
|
||||||
artwork: currentMusic.thumbnail ? [
|
artwork: thumbnailUrl ? [
|
||||||
{ src: getAbsoluteImageUrl(currentMusic.thumbnail) || '', sizes: '512x512', type: 'image/jpeg' },
|
{ src: getAbsoluteImageUrl(thumbnailUrl) || '', sizes: '512x512', type: 'image/jpeg' },
|
||||||
] : undefined,
|
] : undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -265,21 +266,25 @@ export default function Player({
|
|||||||
<div className="flex items-center justify-between gap-2 md:gap-4">
|
<div className="flex items-center justify-between gap-2 md:gap-4">
|
||||||
{/* Album art and music info */}
|
{/* Album art and music info */}
|
||||||
<div className="flex items-center gap-2 md:gap-3 flex-1 min-w-0">
|
<div className="flex items-center gap-2 md:gap-3 flex-1 min-w-0">
|
||||||
{currentMusic.thumbnail ? (
|
{getMusicThumbnail(currentMusic) ? (
|
||||||
<img
|
<img
|
||||||
src={currentMusic.thumbnail.startsWith('http') ? currentMusic.thumbnail : `/music/${currentMusic.thumbnail}`}
|
src={getMusicThumbnail(currentMusic)!}
|
||||||
alt={currentMusic.title}
|
alt={currentMusic.title}
|
||||||
className="w-12 h-12 md:w-14 md:h-14 rounded object-cover cursor-pointer hover:opacity-80 transition-opacity"
|
className="w-12 h-12 md:w-14 md:h-14 rounded object-cover cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
onClick={() => setShowFullScreen(true)}
|
onClick={() => setShowFullScreen(true)}
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement
|
||||||
|
target.style.display = 'none'
|
||||||
|
target.nextElementSibling?.classList.remove('hidden')
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : null}
|
||||||
<div
|
<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)}
|
onClick={() => setShowFullScreen(true)}
|
||||||
>
|
>
|
||||||
<Play className="h-6 w-6 md:h-7 md:w-7 text-muted-foreground" />
|
<Play className="h-6 w-6 md:h-7 md:w-7 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3
|
<h3
|
||||||
className="font-semibold truncate cursor-pointer hover:underline"
|
className="font-semibold truncate cursor-pointer hover:underline"
|
||||||
|
|||||||
@@ -47,3 +47,27 @@ export function formatArtist(artist: string | null | undefined, maxLength?: numb
|
|||||||
// Last resort
|
// Last resort
|
||||||
return `+${artists.length}`
|
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