diff --git a/index.html b/index.html
index 927947a..ab7ff43 100644
--- a/index.html
+++ b/index.html
@@ -2603,6 +2603,7 @@
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;