diff --git a/bun.lock b/bun.lock index 2e5156f..1e31d77 100644 --- a/bun.lock +++ b/bun.lock @@ -19,7 +19,7 @@ "@svta/common-media-library": "^0.18.1", "@types/wicg-file-system-access": "^2023.10.7", "@typescript-eslint/eslint-plugin": "^8.57.2", - "@uimaxbai/am-lyrics": "^1.2.1", + "@uimaxbai/am-lyrics": "^1.2.8", "@vitest/web-worker": "^4.1.2", "appwrite": "^23.0.0", "butterchurn": "^2.6.7", @@ -678,7 +678,7 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="], - "@uimaxbai/am-lyrics": ["@uimaxbai/am-lyrics@1.2.1", "", { "dependencies": { "@babel/runtime": "^7.27.6", "lit": "^3.1.4" }, "peerDependencies": { "@lit/react": "^1.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@lit/react", "react"] }, "sha512-DbzIeQS3bNAiQ+T35EYnFgCSEn6caTefyhtycb4DJr2+iLf8bi8DRP8Dd2cwY5bxAl3ZG6MKdM+Vma3fnN2ruw=="], + "@uimaxbai/am-lyrics": ["@uimaxbai/am-lyrics@1.2.8", "", { "dependencies": { "@babel/runtime": "^7.27.6", "lit": "^3.1.4" }, "peerDependencies": { "@lit/react": "^1.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@lit/react", "react"] }, "sha512-aR8kxqIYcVlsMCH6bbH8ANG+bN/2OAw66ZFjYD1a25hkMTyxtULWgWwAZlUfreP9V47bFvNgXIKvOqhO5JFpeg=="], "@vitest/browser": ["@vitest/browser@4.1.2", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.2", "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.1.0", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.2" } }, "sha512-CwdIf90LNf1Zitgqy63ciMAzmyb4oIGs8WZ40VGYrWkssQKeEKr32EzO8MKUrDPPcPVHFI9oQ5ni2Hp24NaNRQ=="], diff --git a/js/music-api.js b/js/music-api.js index 588fd0a..d5cac2f 100644 --- a/js/music-api.js +++ b/js/music-api.js @@ -247,10 +247,6 @@ export class MusicAPI { } try { - /* - Maintainer of artwork.boidu.dev has asked for his API to be removed for the time being due to spam - */ - /* const url = `https://artwork.boidu.dev/?s=${encodeURIComponent(title)}&a=${encodeURIComponent(artist)}`; const response = await fetch(url); if (!response.ok) return null; @@ -261,8 +257,6 @@ export class MusicAPI { }; this.videoArtworkCache.set(cacheKey, result); return result; - */ - throw new Error('Video artwork is disabled for now.'); } catch (error) { console.warn('Failed to fetch video artwork:', error); return null; diff --git a/js/player.js b/js/player.js index 159d48d..f24caad 100644 --- a/js/player.js +++ b/js/player.js @@ -189,6 +189,29 @@ export class Player { }); this._setupVideoSync(); + this._setupAnimatedCoverSync(); + } + + _setupAnimatedCoverSync() { + const syncPlayPause = () => { + const isPaused = this.activeElement.paused; + document.querySelectorAll('.cover, #fullscreen-cover-image').forEach((el) => { + if (el.tagName === 'VIDEO' && el !== this.video) { + if (isPaused) { + el.pause(); + } else { + el.play().catch(() => {}); + } + } + }); + }; + + this.audio.addEventListener('play', syncPlayPause); + this.audio.addEventListener('pause', syncPlayPause); + if (this.video) { + this.video.addEventListener('play', syncPlayPause); + this.video.addEventListener('pause', syncPlayPause); + } } _setupVideoSync() { @@ -779,6 +802,46 @@ export class Player { await this.playTrackFromQueue(); } + async updateVideoCovers(videoUrl) { + if (!videoUrl) return; + + const syncCover = async (el) => { + if (!el) return; + const isPaused = this.activeElement.paused; + let videoEl; + if (el.tagName === 'IMG') { + videoEl = document.createElement('video'); + videoEl.autoplay = !isPaused; + videoEl.loop = true; + videoEl.muted = true; + videoEl.playsInline = true; + videoEl.className = el.className; + videoEl.id = el.id; + videoEl.style.objectFit = 'cover'; + el.replaceWith(videoEl); + } else if (el.tagName === 'VIDEO') { + videoEl = el; + } else { + return; + } + + if (UIRenderer.instance) { + await UIRenderer.instance.setupHlsVideo(videoEl, videoUrl, null); + if (isPaused) { + videoEl.pause(); + } else { + videoEl.play().catch(() => {}); + } + } + }; + + const playerBarCover = document.querySelector('.now-playing-bar .cover'); + if (playerBarCover) await syncCover(playerBarCover); + + const fullscreenCover = document.getElementById('fullscreen-cover-image'); + if (fullscreenCover) await syncCover(fullscreenCover); + } + async playTrackFromQueue(startTime = 0, recursiveCount = 0, isRetry = false) { if (!isRetry) { this.isFallbackRetry = false; @@ -837,9 +900,26 @@ export class Player { this.currentTrack = track; const trackTitle = getTrackTitle(track); + const artistName = getTrackArtists(track); const trackArtistsHTML = getTrackArtistsHTML(track); const yearDisplay = getTrackYearDisplay(track); + if (!track.videoUrl && !track.videoCoverUrl && !track.album?.videoCoverUrl) { + this.api.getVideoArtwork(trackTitle, artistName).then((result) => { + if (this.currentTrack?.id === track.id && result && (result.videoUrl || result.hlsUrl)) { + track.videoCoverUrl = result.videoUrl || result.hlsUrl; + this.updateVideoCovers(track.videoCoverUrl); + + if ( + UIRenderer.instance && + document.getElementById('fullscreen-cover-overlay')?.style.display === 'flex' + ) { + UIRenderer.instance.updateFullscreenMetadata(track, this.getNextTrack()); + } + } + }); + } + const trackInfo = document.querySelector('.now-playing-bar .track-info'); const coverEl = trackInfo?.querySelector('.cover:not(#audio-player):not(#video-player)'); @@ -904,17 +984,31 @@ export class Player { } else { if (coverEl) { coverEl.style.display = 'block'; + const videoCoverUrl = track.videoUrl || track.videoCoverUrl || track.album?.videoCoverUrl || null; const coverId = track.image || track.cover || track.album?.cover; - const coverUrl = this.api.getCoverUrl(coverId); - const coverSrcset = this.api.getCoverSrcset(coverId); - if (coverEl.getAttribute('src') !== coverUrl) { - coverEl.src = coverUrl; - if (coverSrcset) { - coverEl.setAttribute('srcset', coverSrcset); - coverEl.setAttribute('sizes', '(max-width: 640px) 160px, (max-width: 1024px) 320px, 640px'); - } else { - coverEl.removeAttribute('srcset'); - coverEl.removeAttribute('sizes'); + const coverUrl = videoCoverUrl || this.api.getCoverUrl(coverId); + const coverSrcset = videoCoverUrl ? null : this.api.getCoverSrcset(coverId); + + if (videoCoverUrl) { + this.updateVideoCovers(videoCoverUrl); + } else { + let imgEl = coverEl; + if (coverEl.tagName === 'VIDEO') { + imgEl = document.createElement('img'); + imgEl.className = coverEl.className; + imgEl.id = coverEl.id; + coverEl.replaceWith(imgEl); + } + + if (imgEl.getAttribute('src') !== coverUrl) { + imgEl.src = coverUrl; + if (coverSrcset) { + imgEl.setAttribute('srcset', coverSrcset); + imgEl.setAttribute('sizes', '(max-width: 640px) 160px, (max-width: 1024px) 320px, 640px'); + } else { + imgEl.removeAttribute('srcset'); + imgEl.removeAttribute('sizes'); + } } } } @@ -2090,33 +2184,32 @@ export class Player { } updateMediaSession(track) { - const coverId = track.album?.cover; const trackTitle = getTrackTitle(track); // Force a refresh for picky Bluetooth systems by clearing metadata first MediaSession.setMetadata({}) - .finally(() => - MediaSession.setMetadata({ - title: trackTitle || 'Unknown Title', - artist: getTrackArtists(track) || 'Unknown Artist', - album: track.album?.title || 'Unknown Album', - artwork: coverId - ? [ - { - src: this.api.getCoverUrl(coverId, '1280'), - sizes: '1280x1280', - type: 'image/jpeg', - }, - ] - : undefined, - }) - ) - .catch(() => {}) - .finally(() => { - this.updateMediaSessionPlaybackState(); - this.updateMediaSessionPositionState(); - }); + .finally(() => + MediaSession.setMetadata({ + title: trackTitle || 'Unknown Title', + artist: getTrackArtists(track) || 'Unknown Artist', + album: track.album?.title || 'Unknown Album', + artwork: coverId + ? [ + { + src: this.api.getCoverUrl(coverId, '1280'), + sizes: '1280x1280', + type: 'image/jpeg', + }, + ] + : undefined, + }) + ) + .catch(() => {}) + .finally(() => { + this.updateMediaSessionPlaybackState(); + this.updateMediaSessionPositionState(); + }); } updateMediaSessionPlaybackState() { @@ -2169,9 +2262,8 @@ export class Player { duration: duration, playbackRate: el.playbackRate || 1, position: Math.min(el.currentTime, duration), - }) - .catch((error) => { - console.log('Failed to update Media Session position:', error); + }).catch((error) => { + console.log('Failed to update Media Session position:', error); }); } diff --git a/js/ui.js b/js/ui.js index eb86c72..02c7f7a 100644 --- a/js/ui.js +++ b/js/ui.js @@ -1279,18 +1279,35 @@ export class UIRenderer { const currentImage = document.getElementById('fullscreen-cover-image'); if (videoCoverUrl) { + const isPaused = this.player?.activeElement?.paused ?? true; if (currentImage.tagName === 'IMG') { const video = document.createElement('video'); video.src = videoCoverUrl; - video.autoplay = true; + video.autoplay = !isPaused; video.loop = true; video.muted = true; video.playsInline = true; video.preload = 'auto'; video.className = currentImage.className; + video.id = currentImage.id; + video.style.objectFit = 'cover'; currentImage.replaceWith(video); + if (!isPaused) { + video.play().catch(() => {}); + } } else if (currentImage.src !== videoCoverUrl) { currentImage.src = videoCoverUrl; + if (!isPaused) { + currentImage.play().catch(() => {}); + } else { + currentImage.pause(); + } + } else { + if (!isPaused) { + currentImage.play().catch(() => {}); + } else { + currentImage.pause(); + } } } else { if (currentImage.tagName === 'VIDEO') {