diff --git a/README.md b/README.md index fc8ea5f..cc9a028 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,17 @@ This app runs perfectly on Synology NAS using **Container Manager** (formerly Do - ./data:/app/backend/data ``` 3. In Container Manager, go to **Project** -> **Create**. -4. Select the folder path, give it a name, and it will detect the compose file. -5. Click **Build** / **Run**. +4. Select the folder path, give it a name, and it will detect the compose file. +5. Click **Build** / **Run**. + +#### ✨ Auto-Update Enabled +When using the `docker-compose.yml` above, a **Watchtower** container is included. It will automatically: +- Check for updates to `vndangkhoa/spotify-clone:latest` every hour. +- Download the new image if available. +- Restart the application with the new version. +- Remove old image versions to save space. + +You don't need to do anything manually to keep it updated! 🚀 --- diff --git a/deploy_commands.sh b/deploy_commands.sh index bd9814a..55afcb0 100755 --- a/deploy_commands.sh +++ b/deploy_commands.sh @@ -14,7 +14,7 @@ fi echo "Staging and Committing changes..." git add . -git commit -m "Update: Fix empty album and iOS playback issues, add diverse home content" +git commit -m "Update: Add Media Session API for native mobile controls" echo "Pushing code..." # This might fail if the repo doesn't exist on GitHub yet. diff --git a/docker-compose.yml b/docker-compose.yml index 1b6ef1b..a252be6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,3 +9,13 @@ services: volumes: - ./data:/app/backend/data + + watchtower: + image: containrrr/watchtower + container_name: spotify-watchtower + restart: always + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: --interval 3600 --cleanup + environment: + - WATCHTOWER_INCLUDE_RESTARTING=true diff --git a/frontend/components/PlayerBar.tsx b/frontend/components/PlayerBar.tsx index 2b05993..cf12be1 100644 --- a/frontend/components/PlayerBar.tsx +++ b/frontend/components/PlayerBar.tsx @@ -38,10 +38,53 @@ export default function PlayerBar() { } }, [currentTrack?.url]); + // Media Session API (Lock Screen Controls) + useEffect(() => { + if (!currentTrack || !('mediaSession' in navigator)) return; + + navigator.mediaSession.metadata = new MediaMetadata({ + title: currentTrack.title, + artist: currentTrack.artist, + album: currentTrack.album, + artwork: [ + { src: currentTrack.cover_url, sizes: '96x96', type: 'image/jpeg' }, + { src: currentTrack.cover_url, sizes: '128x128', type: 'image/jpeg' }, + { src: currentTrack.cover_url, sizes: '192x192', type: 'image/jpeg' }, + { src: currentTrack.cover_url, sizes: '256x256', type: 'image/jpeg' }, + { src: currentTrack.cover_url, sizes: '384x384', type: 'image/jpeg' }, + { src: currentTrack.cover_url, sizes: '512x512', type: 'image/jpeg' }, + ] + }); + + // Action Handlers + navigator.mediaSession.setActionHandler('play', () => { + togglePlay(); + navigator.mediaSession.playbackState = "playing"; + }); + navigator.mediaSession.setActionHandler('pause', () => { + togglePlay(); + navigator.mediaSession.playbackState = "paused"; + }); + navigator.mediaSession.setActionHandler('previoustrack', () => prevTrack()); + navigator.mediaSession.setActionHandler('nexttrack', () => nextTrack()); + navigator.mediaSession.setActionHandler('seekto', (details) => { + if (details.seekTime !== undefined && audioRef.current) { + audioRef.current.currentTime = details.seekTime; + setProgress(details.seekTime); + } + }); + + }, [currentTrack]); // access to togglePlay etc. via closure is safe from context + useEffect(() => { if (audioRef.current) { - if (isPlaying) audioRef.current.play().catch(e => console.error("Play error:", e)); - else audioRef.current.pause(); + if (isPlaying) { + audioRef.current.play().catch(e => console.error("Play error:", e)); + if ('mediaSession' in navigator) navigator.mediaSession.playbackState = "playing"; + } else { + audioRef.current.pause(); + if ('mediaSession' in navigator) navigator.mediaSession.playbackState = "paused"; + } } }, [isPlaying]); @@ -58,6 +101,20 @@ export default function PlayerBar() { if (!isNaN(audioRef.current.duration)) { setDuration(audioRef.current.duration); } + + // Update Position State for standard progress bar on lock screen + // Throttle this in real apps, but for simplicity: + if ('mediaSession' in navigator && !isNaN(audioRef.current.duration)) { + try { + navigator.mediaSession.setPositionState({ + duration: audioRef.current.duration, + playbackRate: audioRef.current.playbackRate, + position: audioRef.current.currentTime + }); + } catch (e) { + // Ignore errors (often due to duration being infinite/NaN at start) + } + } } };