From 325be8487ea3feb8bca185b8866e5b18cc90e403 Mon Sep 17 00:00:00 2001 From: Junwei Zhao Date: Fri, 31 Oct 2025 22:58:24 +1100 Subject: [PATCH] UI update --- API_V1_README.md | 12 ++- backend/app/api/music.py | 42 +++++++++ backend/pyproject.toml | 1 + backend/uv.lock | 39 ++++++++ frontend/src/App.tsx | 16 ---- frontend/src/api/client.ts | 1 + .../components/player/FullScreenPlayer.tsx | 90 +++++++++---------- frontend/src/components/player/Player.tsx | 1 + frontend/src/index.css | 45 ++++++++++ 9 files changed, 175 insertions(+), 72 deletions(-) diff --git a/API_V1_README.md b/API_V1_README.md index a597cc3..8383b72 100644 --- a/API_V1_README.md +++ b/API_V1_README.md @@ -165,10 +165,10 @@ while True: ) job = response.json() print(f"Status: {job['status']}") - + if job['status'] in ['completed', 'failed']: break - + time.sleep(5) ``` @@ -207,16 +207,14 @@ async function getJobStatus(jobId) { // Usage (async () => { const job = await createJob('Bohemian Rhapsody - Queen'); - console.log('Job created:', job.id); - + while (true) { const status = await getJobStatus(job.id); - console.log('Status:', status.status); - + if (['completed', 'failed'].includes(status.status)) { break; } - + await new Promise(resolve => setTimeout(resolve, 5000)); } })(); diff --git a/backend/app/api/music.py b/backend/app/api/music.py index bb52c93..2ae903f 100644 --- a/backend/app/api/music.py +++ b/backend/app/api/music.py @@ -102,6 +102,48 @@ async def get_music(music_id: int, db: AsyncSession = Depends(get_db)): return music +@router.get("/{music_id}/lyrics") +async def get_music_lyrics(music_id: int, db: AsyncSession = Depends(get_db)): + """Get lyrics for a music track""" + import httpx + + result = await db.execute(select(Music).where(Music.id == music_id)) + music = result.scalar_one_or_none() + + if not music: + raise HTTPException(status_code=404, detail="Music not found") + + # Try to fetch from lrclib.net + try: + async with httpx.AsyncClient() as client: + params = { + "track_name": music.title, + "artist_name": music.artist or "", + "album_name": music.album or "", + "duration": int(music.duration) if music.duration else 0 + } + response = await client.get("https://lrclib.net/api/get", params=params, timeout=10.0) + + if response.status_code == 200: + data = response.json() + lyrics = data.get("plainLyrics") or data.get("syncedLyrics", "") + + # Save lyrics to database + if lyrics: + music.lyrics = lyrics + await db.commit() + + return {"lyrics": lyrics, "synced": bool(data.get("syncedLyrics"))} + except Exception as e: + logger.error(f"Error fetching lyrics: {e}") + + # Return stored lyrics if available + if music.lyrics: + return {"lyrics": music.lyrics, "synced": False} + + return {"lyrics": "", "synced": False} + + @router.get("/{music_id}/info", response_model=MusicDetailInfo) async def get_music_detail_info(music_id: int, db: AsyncSession = Depends(get_db)): """Get detailed information about a music file""" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 306640c..7988f0a 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "aiosqlite>=0.20.0", "alembic>=1.17.1", "apscheduler>=3.10.4", + "httpx>=0.28.1", ] [tool.uv] diff --git a/backend/uv.lock b/backend/uv.lock index dc09463..b7b8165 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -143,6 +143,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + [[package]] name = "cffi" version = "2.0.0" @@ -374,6 +383,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + [[package]] name = "httptools" version = "0.7.1" @@ -396,6 +418,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, ] +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1136,6 +1173,7 @@ dependencies = [ { name = "alembic" }, { name = "apscheduler" }, { name = "fastapi" }, + { name = "httpx" }, { name = "mutagen" }, { name = "pillow" }, { name = "pydantic" }, @@ -1155,6 +1193,7 @@ requires-dist = [ { name = "alembic", specifier = ">=1.17.1" }, { name = "apscheduler", specifier = ">=3.10.4" }, { name = "fastapi", specifier = ">=0.115.0" }, + { name = "httpx", specifier = ">=0.28.1" }, { name = "mutagen", specifier = ">=1.47.0" }, { name = "pillow", specifier = ">=10.4.0" }, { name = "pydantic", specifier = ">=2.9.2" }, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6c4309c..1363dde 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -30,19 +30,6 @@ function App() { const audioRef = useRef(null) const location = useLocation() - // Load theme from localStorage on mount - NO LONGER NEEDED, handled in initial state - // useEffect(() => { - // const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null - // console.log('Loading theme from localStorage:', savedTheme) - // if (savedTheme) { - // setTheme(savedTheme) - // } else { - // // Default to system preference - // const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches - // setTheme(prefersDark ? 'dark' : 'light') - // } - // }, []) - // Load play mode from localStorage useEffect(() => { const savedMode = localStorage.getItem('playMode') as 'loop' | 'shuffle' | 'repeat-one' | null @@ -53,14 +40,12 @@ function App() { // Apply theme to document useEffect(() => { - console.log('Applying theme:', theme) if (theme === 'dark') { document.documentElement.classList.add('dark') } else { document.documentElement.classList.remove('dark') } localStorage.setItem('theme', theme) - console.log('Theme saved to localStorage:', theme) }, [theme]) // Save play mode to localStorage @@ -69,7 +54,6 @@ function App() { }, [playMode]) const handleThemeChange = (isDark: boolean) => { - console.log('handleThemeChange called with:', isDark) setTheme(isDark ? 'dark' : 'light') } diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 68f2b27..eebff59 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -17,6 +17,7 @@ export const musicApi = { search: (query: string) => api.get('/music/search', { params: { q: query } }), getById: (id: number) => api.get(`/music/${id}`), getDetailInfo: (id: number) => api.get(`/music/${id}/info`), + getLyrics: (id: number) => api.get(`/music/${id}/lyrics`), update: (id: number, data: any) => api.put(`/music/${id}`, data), delete: (id: number) => api.delete(`/music/${id}`), upload: (file: File) => { diff --git a/frontend/src/components/player/FullScreenPlayer.tsx b/frontend/src/components/player/FullScreenPlayer.tsx index 033eb55..d9553b4 100644 --- a/frontend/src/components/player/FullScreenPlayer.tsx +++ b/frontend/src/components/player/FullScreenPlayer.tsx @@ -1,5 +1,6 @@ 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 } from 'lucide-react' import { formatDuration } from '@/lib/utils' import { useQuery } from '@tanstack/react-query' @@ -21,6 +22,7 @@ interface FullScreenPlayerProps { onNavigateToArtist: () => void showLyrics?: boolean onToggleLyrics?: () => void + onSeek?: (value: number[]) => void } export default function FullScreenPlayer({ @@ -39,31 +41,20 @@ export default function FullScreenPlayer({ onNavigateToArtist, showLyrics = false, onToggleLyrics, + onSeek, }: FullScreenPlayerProps) { // Fetch lyrics when enabled const { data: lyrics, isLoading: lyricsLoading } = useQuery({ queryKey: ['lyrics', currentMusic.id], queryFn: async () => { - console.log('Fetching lyrics for:', currentMusic.title) const response = await musicApi.getLyrics(currentMusic.id) - console.log('Lyrics received:', response.data) return response.data.lyrics }, enabled: showLyrics && !!currentMusic.id, }) - console.log('FullScreenPlayer - showLyrics:', showLyrics, 'lyrics:', lyrics) - return (
- {/* Header */} -
-

Now Playing

- -
- {/* Main Content */}
{/* Large Album Art */} @@ -102,45 +93,33 @@ export default function FullScreenPlayer({
{/* Lyrics Display */} -
-
- showLyrics: {String(showLyrics)} | - loading: {String(lyricsLoading)} | - lyrics: {lyrics ? `${lyrics.substring(0, 50)}...` : 'null'} -
-
- {showLyrics && ( -
-
- DEBUG: showLyrics={String(showLyrics)}, loading={String(lyricsLoading)}, - lyrics={lyrics ? `YES (${lyrics.length} chars)` : 'NO'}, - type={typeof lyrics} -
+
{lyricsLoading ? ( -

Loading lyrics...

+

Loading lyrics...

) : lyrics && lyrics.length > 0 ? ( -
-
-
-
- {lyrics} -
+
+
+
+ {lyrics} +
+ {/* Duplicate for seamless loop */} +
+ {lyrics}
{/* Gradient overlays for fade effect */} -
-
+
+
) : ( -

- {lyrics === '' ? 'No lyrics found for this song' : 'No lyrics available'} -

+

No lyrics available

)}
)} @@ -151,12 +130,17 @@ export default function FullScreenPlayer({ {formatDuration(currentTime)} {formatDuration(duration)}
-
-
0 ? (currentTime / duration) * 100 : 0}%` }} - /> -
+ { + if (onSeek) { + onSeek(value) + } + }} + className="w-full" + />
{/* Controls */} @@ -230,6 +214,14 @@ export default function FullScreenPlayer({ > + +
)}
diff --git a/frontend/src/components/player/Player.tsx b/frontend/src/components/player/Player.tsx index e8dd998..f795ed8 100644 --- a/frontend/src/components/player/Player.tsx +++ b/frontend/src/components/player/Player.tsx @@ -449,6 +449,7 @@ export default function Player({ }} showLyrics={showLyrics} onToggleLyrics={() => setShowLyrics(!showLyrics)} + onSeek={handleSeek} /> )} diff --git a/frontend/src/index.css b/frontend/src/index.css index 47bc064..052e619 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -83,3 +83,48 @@ } } } + +@keyframes scroll { + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(-50%); + } +} + +.scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.scrollbar-hide::-webkit-scrollbar { + display: none; +} + +.lyrics-scroll { + animation: lyricsScroll linear infinite; + will-change: transform; +} + +@keyframes lyricsScroll { + 0% { + transform: translateY(100%); + } + 100% { + transform: translateY(-100%); + } +} + +.lyrics-scroll-content { + animation: lyricsAutoScroll linear infinite; +} + +@keyframes lyricsAutoScroll { + from { + transform: translateY(0); + } + to { + transform: translateY(-50%); + } +}