From 424ee12d04c9a3b5b1e6d47d094fc9c2f385b618 Mon Sep 17 00:00:00 2001 From: Eduard Prigoana Date: Tue, 24 Feb 2026 12:43:36 +0000 Subject: [PATCH] feat: animated covers --- js/api.js | 13 +++++ js/music-api.js | 8 +++ js/player.js | 59 +++++++++++++++++++- js/ui.js | 139 +++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 203 insertions(+), 16 deletions(-) diff --git a/js/api.js b/js/api.js index 02d8557..d980e8c 100644 --- a/js/api.js +++ b/js/api.js @@ -1247,6 +1247,19 @@ export class LosslessAPI { return `https://resources.tidal.com/images/${formattedId}/${size}x${size}.jpg`; } + getVideoCoverUrl(id, size = '1280') { + if (!id) { + return null; + } + + const parts = id.split('-'); + if (parts.length !== 5) { + return null; + } + + return `https://resources.tidal.com/videos/${parts[0]}/${parts[1]}/${parts[2]}/${parts[3]}/${parts[4]}/${size}x${size}.mp4`; + } + getArtistPictureUrl(id, size = '320') { if (!id) { return `https://picsum.photos/seed/${Math.random()}/${size}`; diff --git a/js/music-api.js b/js/music-api.js index 106974a..adb8ee2 100644 --- a/js/music-api.js +++ b/js/music-api.js @@ -131,6 +131,14 @@ export class MusicAPI { return this.tidalAPI.getCoverUrl(id, size); } + getVideoCoverUrl(videoCoverId, fallbackCoverId, size = '1280') { + if (videoCoverId) { + const videoUrl = this.tidalAPI.getVideoCoverUrl(videoCoverId, size); + if (videoUrl) return videoUrl; + } + return this.getCoverUrl(fallbackCoverId, size); + } + getArtistPictureUrl(id, size = '320') { if (typeof id === 'string' && id.startsWith('q:')) { return this.qobuzAPI.getArtistPictureUrl(id.slice(2), size); diff --git a/js/player.js b/js/player.js index 7fdcab5..e7d7d48 100644 --- a/js/player.js +++ b/js/player.js @@ -173,7 +173,34 @@ export class Player { const albumEl = document.querySelector('.now-playing-bar .album'); const artistEl = document.querySelector('.now-playing-bar .artist'); - if (coverEl) coverEl.src = this.api.getCoverUrl(track.album?.cover); + if (coverEl) { + const videoCoverUrl = track.album?.videoCover + ? this.api.tidalAPI.getVideoCoverUrl(track.album.videoCover) + : null; + const coverUrl = videoCoverUrl || this.api.getCoverUrl(track.album?.cover); + + if (videoCoverUrl) { + if (coverEl.tagName === 'IMG') { + const video = document.createElement('video'); + video.src = videoCoverUrl; + video.autoplay = true; + video.loop = true; + video.muted = true; + video.playsInline = true; + video.className = coverEl.className; + coverEl.replaceWith(video); + } + } else { + if (coverEl.tagName === 'VIDEO') { + const img = document.createElement('img'); + img.src = coverUrl; + img.className = coverEl.className; + coverEl.replaceWith(img); + } else { + coverEl.src = coverUrl; + } + } + } if (titleEl) { const qualityBadge = createQualityBadgeHTML(track); titleEl.innerHTML = `${escapeHtml(trackTitle)} ${qualityBadge}`; @@ -365,7 +392,35 @@ export class Player { const trackArtistsHTML = getTrackArtistsHTML(track); const yearDisplay = getTrackYearDisplay(track); - document.querySelector('.now-playing-bar .cover').src = this.api.getCoverUrl(track.album?.cover); + const coverEl = document.querySelector('.now-playing-bar .cover'); + if (coverEl) { + const videoCoverUrl = track.album?.videoCover + ? this.api.tidalAPI.getVideoCoverUrl(track.album.videoCover) + : null; + const coverUrl = videoCoverUrl || this.api.getCoverUrl(track.album?.cover); + + if (videoCoverUrl) { + if (coverEl.tagName === 'IMG') { + const video = document.createElement('video'); + video.src = videoCoverUrl; + video.autoplay = true; + video.loop = true; + video.muted = true; + video.playsInline = true; + video.className = coverEl.className; + coverEl.replaceWith(video); + } + } else { + if (coverEl.tagName === 'VIDEO') { + const img = document.createElement('img'); + img.src = coverUrl; + img.className = coverEl.className; + coverEl.replaceWith(img); + } else { + coverEl.src = coverUrl; + } + } + } document.querySelector('.now-playing-bar .title').innerHTML = `${escapeHtml(trackTitle)} ${createQualityBadgeHTML(track)}`; const albumEl = document.querySelector('.now-playing-bar .album'); diff --git a/js/ui.js b/js/ui.js index 30ca4d4..ffe0bb6 100644 --- a/js/ui.js +++ b/js/ui.js @@ -334,7 +334,7 @@ export class UIRenderer { const isUnavailable = track.isUnavailable; const isBlocked = contentBlockingSettings?.shouldHideTrack(track); const trackImageHTML = showCover - ? `Track Cover` + ? this.getCoverHTML(track.album?.videoCover, track.album?.cover, 'Track Cover', 'track-item-cover') : ''; let displayIndex; @@ -405,6 +405,14 @@ export class UIRenderer { `; } + getCoverHTML(videoCover, cover, alt, className = 'card-image', loading = 'lazy') { + const videoUrl = videoCover ? this.api.tidalAPI.getVideoCoverUrl(videoCover) : null; + if (videoUrl) { + return ``; + } + return `${alt}`; + } + createBaseCardHTML({ type, id, @@ -608,7 +616,7 @@ export class UIRenderer { href: `/album/${album.id}`, title: `${escapeHtml(album.title)} ${explicitBadge} ${qualityBadge}`, subtitle: `${escapeHtml(artistName)} • ${yearDisplay}${typeLabel}`, - imageHTML: `${escapeHtml(album.title)}`, + imageHTML: this.getCoverHTML(album.videoCover, album.cover, escapeHtml(album.title)), actionButtonsHTML: `