bring back animated album covers + improvements
This commit is contained in:
parent
5f8cb6e947
commit
4497863667
4 changed files with 147 additions and 44 deletions
4
bun.lock
4
bun.lock
|
|
@ -19,7 +19,7 @@
|
||||||
"@svta/common-media-library": "^0.18.1",
|
"@svta/common-media-library": "^0.18.1",
|
||||||
"@types/wicg-file-system-access": "^2023.10.7",
|
"@types/wicg-file-system-access": "^2023.10.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
"@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",
|
"@vitest/web-worker": "^4.1.2",
|
||||||
"appwrite": "^23.0.0",
|
"appwrite": "^23.0.0",
|
||||||
"butterchurn": "^2.6.7",
|
"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=="],
|
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -247,10 +247,6 @@ export class MusicAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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 url = `https://artwork.boidu.dev/?s=${encodeURIComponent(title)}&a=${encodeURIComponent(artist)}`;
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) return null;
|
if (!response.ok) return null;
|
||||||
|
|
@ -261,8 +257,6 @@ export class MusicAPI {
|
||||||
};
|
};
|
||||||
this.videoArtworkCache.set(cacheKey, result);
|
this.videoArtworkCache.set(cacheKey, result);
|
||||||
return result;
|
return result;
|
||||||
*/
|
|
||||||
throw new Error('Video artwork is disabled for now.');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to fetch video artwork:', error);
|
console.warn('Failed to fetch video artwork:', error);
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
162
js/player.js
162
js/player.js
|
|
@ -189,6 +189,29 @@ export class Player {
|
||||||
});
|
});
|
||||||
|
|
||||||
this._setupVideoSync();
|
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() {
|
_setupVideoSync() {
|
||||||
|
|
@ -779,6 +802,46 @@ export class Player {
|
||||||
await this.playTrackFromQueue();
|
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) {
|
async playTrackFromQueue(startTime = 0, recursiveCount = 0, isRetry = false) {
|
||||||
if (!isRetry) {
|
if (!isRetry) {
|
||||||
this.isFallbackRetry = false;
|
this.isFallbackRetry = false;
|
||||||
|
|
@ -837,9 +900,26 @@ export class Player {
|
||||||
this.currentTrack = track;
|
this.currentTrack = track;
|
||||||
|
|
||||||
const trackTitle = getTrackTitle(track);
|
const trackTitle = getTrackTitle(track);
|
||||||
|
const artistName = getTrackArtists(track);
|
||||||
const trackArtistsHTML = getTrackArtistsHTML(track);
|
const trackArtistsHTML = getTrackArtistsHTML(track);
|
||||||
const yearDisplay = getTrackYearDisplay(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 trackInfo = document.querySelector('.now-playing-bar .track-info');
|
||||||
const coverEl = trackInfo?.querySelector('.cover:not(#audio-player):not(#video-player)');
|
const coverEl = trackInfo?.querySelector('.cover:not(#audio-player):not(#video-player)');
|
||||||
|
|
||||||
|
|
@ -904,17 +984,31 @@ export class Player {
|
||||||
} else {
|
} else {
|
||||||
if (coverEl) {
|
if (coverEl) {
|
||||||
coverEl.style.display = 'block';
|
coverEl.style.display = 'block';
|
||||||
|
const videoCoverUrl = track.videoUrl || track.videoCoverUrl || track.album?.videoCoverUrl || null;
|
||||||
const coverId = track.image || track.cover || track.album?.cover;
|
const coverId = track.image || track.cover || track.album?.cover;
|
||||||
const coverUrl = this.api.getCoverUrl(coverId);
|
const coverUrl = videoCoverUrl || this.api.getCoverUrl(coverId);
|
||||||
const coverSrcset = this.api.getCoverSrcset(coverId);
|
const coverSrcset = videoCoverUrl ? null : this.api.getCoverSrcset(coverId);
|
||||||
if (coverEl.getAttribute('src') !== coverUrl) {
|
|
||||||
coverEl.src = coverUrl;
|
if (videoCoverUrl) {
|
||||||
if (coverSrcset) {
|
this.updateVideoCovers(videoCoverUrl);
|
||||||
coverEl.setAttribute('srcset', coverSrcset);
|
} else {
|
||||||
coverEl.setAttribute('sizes', '(max-width: 640px) 160px, (max-width: 1024px) 320px, 640px');
|
let imgEl = coverEl;
|
||||||
} else {
|
if (coverEl.tagName === 'VIDEO') {
|
||||||
coverEl.removeAttribute('srcset');
|
imgEl = document.createElement('img');
|
||||||
coverEl.removeAttribute('sizes');
|
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) {
|
updateMediaSession(track) {
|
||||||
|
|
||||||
const coverId = track.album?.cover;
|
const coverId = track.album?.cover;
|
||||||
const trackTitle = getTrackTitle(track);
|
const trackTitle = getTrackTitle(track);
|
||||||
|
|
||||||
// Force a refresh for picky Bluetooth systems by clearing metadata first
|
// Force a refresh for picky Bluetooth systems by clearing metadata first
|
||||||
MediaSession.setMetadata({})
|
MediaSession.setMetadata({})
|
||||||
.finally(() =>
|
.finally(() =>
|
||||||
MediaSession.setMetadata({
|
MediaSession.setMetadata({
|
||||||
title: trackTitle || 'Unknown Title',
|
title: trackTitle || 'Unknown Title',
|
||||||
artist: getTrackArtists(track) || 'Unknown Artist',
|
artist: getTrackArtists(track) || 'Unknown Artist',
|
||||||
album: track.album?.title || 'Unknown Album',
|
album: track.album?.title || 'Unknown Album',
|
||||||
artwork: coverId
|
artwork: coverId
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
src: this.api.getCoverUrl(coverId, '1280'),
|
src: this.api.getCoverUrl(coverId, '1280'),
|
||||||
sizes: '1280x1280',
|
sizes: '1280x1280',
|
||||||
type: 'image/jpeg',
|
type: 'image/jpeg',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: undefined,
|
: undefined,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.updateMediaSessionPlaybackState();
|
this.updateMediaSessionPlaybackState();
|
||||||
this.updateMediaSessionPositionState();
|
this.updateMediaSessionPositionState();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMediaSessionPlaybackState() {
|
updateMediaSessionPlaybackState() {
|
||||||
|
|
@ -2169,9 +2262,8 @@ export class Player {
|
||||||
duration: duration,
|
duration: duration,
|
||||||
playbackRate: el.playbackRate || 1,
|
playbackRate: el.playbackRate || 1,
|
||||||
position: Math.min(el.currentTime, duration),
|
position: Math.min(el.currentTime, duration),
|
||||||
})
|
}).catch((error) => {
|
||||||
.catch((error) => {
|
console.log('Failed to update Media Session position:', error);
|
||||||
console.log('Failed to update Media Session position:', error);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
19
js/ui.js
19
js/ui.js
|
|
@ -1279,18 +1279,35 @@ export class UIRenderer {
|
||||||
const currentImage = document.getElementById('fullscreen-cover-image');
|
const currentImage = document.getElementById('fullscreen-cover-image');
|
||||||
|
|
||||||
if (videoCoverUrl) {
|
if (videoCoverUrl) {
|
||||||
|
const isPaused = this.player?.activeElement?.paused ?? true;
|
||||||
if (currentImage.tagName === 'IMG') {
|
if (currentImage.tagName === 'IMG') {
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
video.src = videoCoverUrl;
|
video.src = videoCoverUrl;
|
||||||
video.autoplay = true;
|
video.autoplay = !isPaused;
|
||||||
video.loop = true;
|
video.loop = true;
|
||||||
video.muted = true;
|
video.muted = true;
|
||||||
video.playsInline = true;
|
video.playsInline = true;
|
||||||
video.preload = 'auto';
|
video.preload = 'auto';
|
||||||
video.className = currentImage.className;
|
video.className = currentImage.className;
|
||||||
|
video.id = currentImage.id;
|
||||||
|
video.style.objectFit = 'cover';
|
||||||
currentImage.replaceWith(video);
|
currentImage.replaceWith(video);
|
||||||
|
if (!isPaused) {
|
||||||
|
video.play().catch(() => {});
|
||||||
|
}
|
||||||
} else if (currentImage.src !== videoCoverUrl) {
|
} else if (currentImage.src !== videoCoverUrl) {
|
||||||
currentImage.src = videoCoverUrl;
|
currentImage.src = videoCoverUrl;
|
||||||
|
if (!isPaused) {
|
||||||
|
currentImage.play().catch(() => {});
|
||||||
|
} else {
|
||||||
|
currentImage.pause();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!isPaused) {
|
||||||
|
currentImage.play().catch(() => {});
|
||||||
|
} else {
|
||||||
|
currentImage.pause();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (currentImage.tagName === 'VIDEO') {
|
if (currentImage.tagName === 'VIDEO') {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue