From 7dd38bac807f09634b21fca1fc845764ac6b9d37 Mon Sep 17 00:00:00 2001 From: Samidy Date: Mon, 13 Apr 2026 14:55:13 +0300 Subject: [PATCH] feat(artists): Artist Banners --- index.html | 11 ++++++ js/music-api.js | 41 +++++++++++++++++++++ js/settings.js | 10 ++++++ js/storage.js | 17 +++++++++ js/ui.js | 44 +++++++++++++++++++++++ styles.css | 94 +++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 217 insertions(+) diff --git a/index.html b/index.html index 927947a..ab7ff43 100644 --- a/index.html +++ b/index.html @@ -2603,6 +2603,7 @@
+
+
+
+ Artist Banners + Display video banners on artist pages +
+ +
Compact Albums diff --git a/js/music-api.js b/js/music-api.js index fe7384b..588fd0a 100644 --- a/js/music-api.js +++ b/js/music-api.js @@ -277,6 +277,47 @@ export class MusicAPI { return this.tidalAPI.getArtistPictureSrcset(this.stripProviderPrefix(id)); } + async getArtistBanner(artistName) { + const cacheKey = `banner-${artistName}`.toLowerCase(); + if (this.videoArtworkCache.has(cacheKey)) { + return this.videoArtworkCache.get(cacheKey); + } + + try { + const url = `https://artwork-boidu-dev.samidy.workers.dev/artist?a=${encodeURIComponent(artistName)}`; + const response = await fetch(url); + if (!response.ok) return null; + const data = await response.json(); + + let hlsUrl = null; + if (data.animated) { + if (typeof data.animated === 'string') { + hlsUrl = data.animated; + } else if (typeof data.animated === 'object') { + hlsUrl = data.animated.hls || data.animated.url || data.animated.hlsUrl || data.animated.videoUrl; + + if (!hlsUrl) { + for (const key in data.animated) { + if (typeof data.animated[key] === 'string' && data.animated[key].includes('.m3u8')) { + hlsUrl = data.animated[key]; + break; + } + } + } + } + } + + const result = { + hlsUrl: hlsUrl, + }; + this.videoArtworkCache.set(cacheKey, result); + return result; + } catch (error) { + console.warn('Failed to fetch artist banner:', error); + return null; + } + } + extractStreamUrlFromManifest(manifest) { return this.tidalAPI.extractStreamUrlFromManifest(manifest); } diff --git a/js/settings.js b/js/settings.js index 5fcdf4d..a691e21 100644 --- a/js/settings.js +++ b/js/settings.js @@ -8,6 +8,7 @@ import { backgroundSettings, dynamicColorSettings, cardSettings, + artistBannerSettings, waveformSettings, replayGainSettings, downloadQualitySettings, @@ -5675,6 +5676,15 @@ export async function initializeSettings(scrobbler, player, api, ui) { }); } + // Artist Banners Toggle + const artistBannersToggle = document.getElementById('artist-banners-toggle'); + if (artistBannersToggle) { + artistBannersToggle.checked = artistBannerSettings.isEnabled(); + artistBannersToggle.addEventListener('change', (e) => { + artistBannerSettings.setEnabled(e.target.checked); + }); + } + // Compact Album Toggle const compactAlbumToggle = document.getElementById('compact-album-toggle'); if (compactAlbumToggle) { diff --git a/js/storage.js b/js/storage.js index 2dab6ee..6e41f69 100644 --- a/js/storage.js +++ b/js/storage.js @@ -687,6 +687,23 @@ export const cardSettings = { }, }; +export const artistBannerSettings = { + STORAGE_KEY: 'artist-banners-enabled', + + isEnabled() { + try { + const val = localStorage.getItem(this.STORAGE_KEY); + return val === null ? true : val === 'true'; + } catch { + return true; + } + }, + + setEnabled(enabled) { + localStorage.setItem(this.STORAGE_KEY, enabled ? 'true' : 'false'); + }, +}; + export const replayGainSettings = { STORAGE_KEY_MODE: 'replay-gain-mode', // 'off', 'track', 'album' STORAGE_KEY_PREAMP: 'replay-gain-preamp', diff --git a/js/ui.js b/js/ui.js index f29712a..eb86c72 100644 --- a/js/ui.js +++ b/js/ui.js @@ -29,6 +29,7 @@ import { contentBlockingSettings, settingsUiState, fullscreenCoverNoRoundSettings, + artistBannerSettings, } from './storage.js'; import { db } from './db.js'; import { getVibrantColorFromImage } from './vibrant-color.js'; @@ -4775,6 +4776,16 @@ export class UIRenderer { await this.showPage('artist'); this.currentArtistId = artistId; + const bannerContainer = document.getElementById('artist-detail-banner-container'); + if (bannerContainer) { + const oldVideo = bannerContainer.querySelector('video'); + if (oldVideo && oldVideo._hls) { + oldVideo._hls.destroy(); + } + bannerContainer.innerHTML = ''; + bannerContainer.style.opacity = '0'; + } + const imageEl = document.getElementById('artist-detail-image'); const nameEl = document.getElementById('artist-detail-name'); const metaEl = document.getElementById('artist-detail-meta'); @@ -4823,6 +4834,39 @@ export class UIRenderer { try { const artist = await this.api.getArtist(artistId, provider); + const currentId = this.currentArtistId; + this.api + .getArtistBanner(artist.name) + .then(async (banner) => { + if (this.currentArtistId !== currentId) return; + + if (banner && banner.hlsUrl && bannerContainer) { + const video = document.createElement('video'); + video.autoplay = true; + video.loop = true; + video.muted = true; + video.playsInline = true; + video.setAttribute('muted', ''); + video.setAttribute('autoplay', ''); + video.setAttribute('playsinline', ''); + video.style.opacity = '1'; + + try { + await this.setupHlsVideo(video, banner, null); + if (this.currentArtistId === currentId) { + bannerContainer.appendChild(video); + bannerContainer.style.opacity = '1'; + video.play().catch(() => {}); + } + } catch (e) { + console.warn('Failed to setup artist banner video:', e); + } + } + }) + .catch((e) => { + console.warn('Failed to fetch artist banner:', e); + }); + // Handle Biography if (bioEl) { // Pre-define regex patterns for better performance diff --git a/styles.css b/styles.css index 9e0722f..c6960fd 100644 --- a/styles.css +++ b/styles.css @@ -2542,6 +2542,100 @@ body.multi-select-mode .track-item:hover { border-radius: var(--radius-full); } +.detail-header-banner { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; + overflow: hidden; + opacity: 0; + transition: opacity 1s ease-in-out; +} + +.detail-header-banner video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + filter: brightness(0.6); + display: block; +} + +#page-artist .detail-header { + position: relative; + padding: 12rem 3rem 4rem 3rem; + border-radius: 0; + overflow: hidden; + margin-top: -8rem; + margin-left: calc(var(--spacing-xl) * -1); + margin-right: calc(var(--spacing-xl) * -1); + margin-bottom: var(--spacing-xl); + min-height: 550px; + display: flex; + align-items: flex-end; + background-color: var(--card); +} + +@media (max-width: 1024px) { + #page-artist .detail-header { + margin-top: -7rem; + margin-left: calc(var(--spacing-lg) * -1); + margin-right: calc(var(--spacing-lg) * -1); + padding: 10rem 2rem 3rem 2rem; + min-height: 450px; + } +} + +@media (max-width: 768px) { + #page-artist .detail-header { + margin-top: -6rem; + margin-left: calc(var(--spacing-md) * -1); + margin-right: calc(var(--spacing-md) * -1); + padding: 8rem 1rem 2rem 1rem; + min-height: 400px; + } +} + +@media (max-width: 480px) { + #page-artist .detail-header { + margin-top: calc((var(--spacing-sm) + var(--spacing-xl)) * -1); + margin-left: calc(var(--spacing-sm) * -1); + margin-right: calc(var(--spacing-sm) * -1); + } +} + +.detail-header-banner::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + to bottom, + rgba(0, 0, 0, 0.4) 0%, + rgba(0, 0, 0, 0) 40%, + rgba(0, 0, 0, 0.2) 70%, + var(--background) 100% + ); + z-index: 1; +} + +#page-artist .detail-header-image { + width: 200px; + height: 200px; + border: 4px solid var(--background); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6); + z-index: 2; +} + +#page-artist .detail-header-info { + z-index: 2; + text-shadow: 0 2px 15px rgba(0, 0, 0, 0.7); +} + + .detail-header-info .type { font-weight: 600; margin-bottom: 0.5rem;