Update: Add Media Session API for native mobile controls

This commit is contained in:
Khoa.vo 2025-12-17 14:03:46 +07:00
parent e23714bbd6
commit c8c232afe9
4 changed files with 81 additions and 5 deletions

View file

@ -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! 🚀
---

View file

@ -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.

View file

@ -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

View file

@ -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)
}
}
}
};