feat: animated covers
This commit is contained in:
parent
700645919c
commit
424ee12d04
4 changed files with 203 additions and 16 deletions
13
js/api.js
13
js/api.js
|
|
@ -1247,6 +1247,19 @@ export class LosslessAPI {
|
||||||
return `https://resources.tidal.com/images/${formattedId}/${size}x${size}.jpg`;
|
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') {
|
getArtistPictureUrl(id, size = '320') {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return `https://picsum.photos/seed/${Math.random()}/${size}`;
|
return `https://picsum.photos/seed/${Math.random()}/${size}`;
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,14 @@ export class MusicAPI {
|
||||||
return this.tidalAPI.getCoverUrl(id, size);
|
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') {
|
getArtistPictureUrl(id, size = '320') {
|
||||||
if (typeof id === 'string' && id.startsWith('q:')) {
|
if (typeof id === 'string' && id.startsWith('q:')) {
|
||||||
return this.qobuzAPI.getArtistPictureUrl(id.slice(2), size);
|
return this.qobuzAPI.getArtistPictureUrl(id.slice(2), size);
|
||||||
|
|
|
||||||
59
js/player.js
59
js/player.js
|
|
@ -173,7 +173,34 @@ export class Player {
|
||||||
const albumEl = document.querySelector('.now-playing-bar .album');
|
const albumEl = document.querySelector('.now-playing-bar .album');
|
||||||
const artistEl = document.querySelector('.now-playing-bar .artist');
|
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) {
|
if (titleEl) {
|
||||||
const qualityBadge = createQualityBadgeHTML(track);
|
const qualityBadge = createQualityBadgeHTML(track);
|
||||||
titleEl.innerHTML = `${escapeHtml(trackTitle)} ${qualityBadge}`;
|
titleEl.innerHTML = `${escapeHtml(trackTitle)} ${qualityBadge}`;
|
||||||
|
|
@ -365,7 +392,35 @@ export class Player {
|
||||||
const trackArtistsHTML = getTrackArtistsHTML(track);
|
const trackArtistsHTML = getTrackArtistsHTML(track);
|
||||||
const yearDisplay = getTrackYearDisplay(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 =
|
document.querySelector('.now-playing-bar .title').innerHTML =
|
||||||
`${escapeHtml(trackTitle)} ${createQualityBadgeHTML(track)}`;
|
`${escapeHtml(trackTitle)} ${createQualityBadgeHTML(track)}`;
|
||||||
const albumEl = document.querySelector('.now-playing-bar .album');
|
const albumEl = document.querySelector('.now-playing-bar .album');
|
||||||
|
|
|
||||||
139
js/ui.js
139
js/ui.js
|
|
@ -334,7 +334,7 @@ export class UIRenderer {
|
||||||
const isUnavailable = track.isUnavailable;
|
const isUnavailable = track.isUnavailable;
|
||||||
const isBlocked = contentBlockingSettings?.shouldHideTrack(track);
|
const isBlocked = contentBlockingSettings?.shouldHideTrack(track);
|
||||||
const trackImageHTML = showCover
|
const trackImageHTML = showCover
|
||||||
? `<img src="${this.api.getCoverUrl(track.album?.cover)}" alt="Track Cover" class="track-item-cover" loading="lazy">`
|
? this.getCoverHTML(track.album?.videoCover, track.album?.cover, 'Track Cover', 'track-item-cover')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
let displayIndex;
|
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 `<video src="${videoUrl}" class="${className}" alt="${alt}" autoplay loop muted playsinline></video>`;
|
||||||
|
}
|
||||||
|
return `<img src="${this.api.getCoverUrl(cover)}" class="${className}" alt="${alt}" loading="${loading}">`;
|
||||||
|
}
|
||||||
|
|
||||||
createBaseCardHTML({
|
createBaseCardHTML({
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
|
|
@ -608,7 +616,7 @@ export class UIRenderer {
|
||||||
href: `/album/${album.id}`,
|
href: `/album/${album.id}`,
|
||||||
title: `${escapeHtml(album.title)} ${explicitBadge} ${qualityBadge}`,
|
title: `${escapeHtml(album.title)} ${explicitBadge} ${qualityBadge}`,
|
||||||
subtitle: `${escapeHtml(artistName)} • ${yearDisplay}${typeLabel}`,
|
subtitle: `${escapeHtml(artistName)} • ${yearDisplay}${typeLabel}`,
|
||||||
imageHTML: `<img src="${this.api.getCoverUrl(album.cover)}" alt="${escapeHtml(album.title)}" class="card-image" loading="lazy">`,
|
imageHTML: this.getCoverHTML(album.videoCover, album.cover, escapeHtml(album.title)),
|
||||||
actionButtonsHTML: `
|
actionButtonsHTML: `
|
||||||
<button class="like-btn card-like-btn" data-action="toggle-like" data-type="album" title="Add to Liked">
|
<button class="like-btn card-like-btn" data-action="toggle-like" data-type="album" title="Add to Liked">
|
||||||
${this.createHeartIcon(false)}
|
${this.createHeartIcon(false)}
|
||||||
|
|
@ -893,19 +901,45 @@ export class UIRenderer {
|
||||||
const artist = document.getElementById('fullscreen-track-artist');
|
const artist = document.getElementById('fullscreen-track-artist');
|
||||||
const nextTrackEl = document.getElementById('fullscreen-next-track');
|
const nextTrackEl = document.getElementById('fullscreen-next-track');
|
||||||
|
|
||||||
const coverUrl = this.api.getCoverUrl(track.album?.cover, '1280');
|
const videoCoverUrl = track.album?.videoCover
|
||||||
|
? this.api.tidalAPI.getVideoCoverUrl(track.album.videoCover, '1280')
|
||||||
|
: null;
|
||||||
|
const coverUrl = videoCoverUrl || this.api.getCoverUrl(track.album?.cover, '1280');
|
||||||
|
|
||||||
const fsLikeBtn = document.getElementById('fs-like-btn');
|
const fsLikeBtn = document.getElementById('fs-like-btn');
|
||||||
if (fsLikeBtn) {
|
if (fsLikeBtn) {
|
||||||
this.updateLikeState(fsLikeBtn.parentElement, 'track', track.id);
|
this.updateLikeState(fsLikeBtn.parentElement, 'track', track.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image.src !== coverUrl) {
|
if (videoCoverUrl) {
|
||||||
image.src = coverUrl;
|
if (image.tagName === 'IMG') {
|
||||||
overlay.style.setProperty('--bg-image', `url('${coverUrl}')`);
|
const video = document.createElement('video');
|
||||||
this.extractAndApplyColor(coverUrl);
|
video.src = videoCoverUrl;
|
||||||
|
video.autoplay = true;
|
||||||
|
video.loop = true;
|
||||||
|
video.muted = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.id = image.id;
|
||||||
|
video.className = image.className;
|
||||||
|
image.replaceWith(video);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (image.tagName === 'VIDEO') {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = coverUrl;
|
||||||
|
img.id = image.id;
|
||||||
|
img.className = image.className;
|
||||||
|
image.replaceWith(img);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentImage = document.getElementById('fullscreen-cover-image');
|
||||||
|
if (currentImage.src !== coverUrl || !videoCoverUrl) {
|
||||||
|
currentImage.src = coverUrl;
|
||||||
|
}
|
||||||
|
overlay.style.setProperty('--bg-image', `url('${coverUrl}')`);
|
||||||
|
this.extractAndApplyColor(this.api.getCoverUrl(track.album?.cover, '80'));
|
||||||
|
|
||||||
const qualityBadge = createQualityBadgeHTML(track);
|
const qualityBadge = createQualityBadgeHTML(track);
|
||||||
title.innerHTML = `${escapeHtml(track.title)} ${qualityBadge}`;
|
title.innerHTML = `${escapeHtml(track.title)} ${qualityBadge}`;
|
||||||
artist.textContent = getTrackArtists(track);
|
artist.textContent = getTrackArtists(track);
|
||||||
|
|
@ -1683,7 +1717,7 @@ export class UIRenderer {
|
||||||
href: `/track/${track.id}`,
|
href: `/track/${track.id}`,
|
||||||
title: `${escapeHtml(getTrackTitle(track))} ${explicitBadge} ${qualityBadge}`,
|
title: `${escapeHtml(getTrackTitle(track))} ${explicitBadge} ${qualityBadge}`,
|
||||||
subtitle: escapeHtml(getTrackArtists(track)),
|
subtitle: escapeHtml(getTrackArtists(track)),
|
||||||
imageHTML: `<img src="${this.api.getCoverUrl(track.album?.cover)}" alt="${escapeHtml(track.title)}" class="card-image" loading="lazy">`,
|
imageHTML: this.getCoverHTML(track.album?.videoCover, track.album?.cover, escapeHtml(track.title)),
|
||||||
actionButtonsHTML: `
|
actionButtonsHTML: `
|
||||||
<button class="like-btn card-like-btn" data-action="toggle-like" data-type="track" title="Add to Liked">
|
<button class="like-btn card-like-btn" data-action="toggle-like" data-type="track" title="Add to Liked">
|
||||||
${this.createHeartIcon(false)}
|
${this.createHeartIcon(false)}
|
||||||
|
|
@ -2245,8 +2279,32 @@ export class UIRenderer {
|
||||||
try {
|
try {
|
||||||
const { album, tracks } = await this.api.getAlbum(albumId, provider);
|
const { album, tracks } = await this.api.getAlbum(albumId, provider);
|
||||||
|
|
||||||
const coverUrl = this.api.getCoverUrl(album.cover);
|
const videoCoverUrl = album.videoCover ? this.api.tidalAPI.getVideoCoverUrl(album.videoCover) : null;
|
||||||
imageEl.src = coverUrl;
|
const coverUrl = videoCoverUrl || this.api.getCoverUrl(album.cover);
|
||||||
|
|
||||||
|
if (videoCoverUrl) {
|
||||||
|
if (imageEl.tagName === 'IMG') {
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.src = videoCoverUrl;
|
||||||
|
video.autoplay = true;
|
||||||
|
video.loop = true;
|
||||||
|
video.muted = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.className = imageEl.className;
|
||||||
|
imageEl.replaceWith(video);
|
||||||
|
} else {
|
||||||
|
imageEl.src = videoCoverUrl;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (imageEl.tagName === 'VIDEO') {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = coverUrl;
|
||||||
|
img.className = imageEl.className;
|
||||||
|
imageEl.replaceWith(img);
|
||||||
|
} else {
|
||||||
|
imageEl.src = coverUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
imageEl.style.backgroundColor = '';
|
imageEl.style.backgroundColor = '';
|
||||||
|
|
||||||
// Set background and vibrant color
|
// Set background and vibrant color
|
||||||
|
|
@ -2970,8 +3028,35 @@ export class UIRenderer {
|
||||||
} else {
|
} else {
|
||||||
// Try to get cover from first track album
|
// Try to get cover from first track album
|
||||||
if (tracks.length > 0 && tracks[0].album?.cover) {
|
if (tracks.length > 0 && tracks[0].album?.cover) {
|
||||||
imageEl.src = this.api.getCoverUrl(tracks[0].album.cover);
|
const videoCoverUrl = tracks[0].album?.videoCover
|
||||||
this.setPageBackground(imageEl.src);
|
? this.api.tidalAPI.getVideoCoverUrl(tracks[0].album.videoCover)
|
||||||
|
: null;
|
||||||
|
const coverUrl = videoCoverUrl || this.api.getCoverUrl(tracks[0].album.cover);
|
||||||
|
|
||||||
|
if (videoCoverUrl) {
|
||||||
|
if (imageEl.tagName === 'IMG') {
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.src = videoCoverUrl;
|
||||||
|
video.autoplay = true;
|
||||||
|
video.loop = true;
|
||||||
|
video.muted = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.className = imageEl.className;
|
||||||
|
imageEl.replaceWith(video);
|
||||||
|
} else {
|
||||||
|
imageEl.src = videoCoverUrl;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (imageEl.tagName === 'VIDEO') {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = coverUrl;
|
||||||
|
img.className = imageEl.className;
|
||||||
|
imageEl.replaceWith(img);
|
||||||
|
} else {
|
||||||
|
imageEl.src = coverUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setPageBackground(coverUrl);
|
||||||
this.extractAndApplyColor(this.api.getCoverUrl(tracks[0].album.cover, '160'));
|
this.extractAndApplyColor(this.api.getCoverUrl(tracks[0].album.cover, '160'));
|
||||||
} else {
|
} else {
|
||||||
imageEl.src = '/assets/appicon.png';
|
imageEl.src = '/assets/appicon.png';
|
||||||
|
|
@ -3986,8 +4071,34 @@ export class UIRenderer {
|
||||||
track = await this.api.getTrackMetadata(trackId, provider);
|
track = await this.api.getTrackMetadata(trackId, provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
const coverUrl = this.api.getCoverUrl(track.album?.cover);
|
const videoCoverUrl = track.album?.videoCover
|
||||||
imageEl.src = coverUrl;
|
? this.api.tidalAPI.getVideoCoverUrl(track.album.videoCover)
|
||||||
|
: null;
|
||||||
|
const coverUrl = videoCoverUrl || this.api.getCoverUrl(track.album?.cover);
|
||||||
|
|
||||||
|
if (videoCoverUrl) {
|
||||||
|
if (imageEl.tagName === 'IMG') {
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.src = videoCoverUrl;
|
||||||
|
video.autoplay = true;
|
||||||
|
video.loop = true;
|
||||||
|
video.muted = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.className = imageEl.className;
|
||||||
|
imageEl.replaceWith(video);
|
||||||
|
} else {
|
||||||
|
imageEl.src = videoCoverUrl;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (imageEl.tagName === 'VIDEO') {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = coverUrl;
|
||||||
|
img.className = imageEl.className;
|
||||||
|
imageEl.replaceWith(img);
|
||||||
|
} else {
|
||||||
|
imageEl.src = coverUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
imageEl.style.backgroundColor = '';
|
imageEl.style.backgroundColor = '';
|
||||||
|
|
||||||
this.setPageBackground(coverUrl);
|
this.setPageBackground(coverUrl);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue