show album release date next to tracks

This commit is contained in:
BlackSigkill 2026-02-03 11:57:46 +01:00
parent 18b82e793e
commit 9780263cf7
4 changed files with 89 additions and 32 deletions

View file

@ -173,6 +173,34 @@ export class LosslessAPI {
return artist; return artist;
} }
async enrichTracksWithAlbumDates(tracks) {
const albumIdsToFetch = [];
for (const track of tracks) {
if (!track.album?.releaseDate && track.album?.id && !albumIdsToFetch.includes(track.album.id)) {
albumIdsToFetch.push(track.album.id);
}
}
if (albumIdsToFetch.length === 0) return tracks;
const albumDateMap = new Map();
const results = await Promise.allSettled(albumIdsToFetch.map((id) => this.getAlbum(id)));
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result.status === 'fulfilled' && result.value.album?.releaseDate) {
albumDateMap.set(albumIdsToFetch[i], result.value.album.releaseDate);
}
}
return tracks.map((track) => {
if (!track.album?.releaseDate && track.album?.id && albumDateMap.has(track.album.id)) {
return { ...track, album: { ...track.album, releaseDate: albumDateMap.get(track.album.id) } };
}
return track;
});
}
parseTrackLookup(data) { parseTrackLookup(data) {
const entries = Array.isArray(data) ? data : [data]; const entries = Array.isArray(data) ? data : [data];
let track, info, originalTrackUrl; let track, info, originalTrackUrl;
@ -272,9 +300,11 @@ export class LosslessAPI {
const response = await this.fetchWithRetry(`/search/?s=${encodeURIComponent(query)}`, options); const response = await this.fetchWithRetry(`/search/?s=${encodeURIComponent(query)}`, options);
const data = await response.json(); const data = await response.json();
const normalized = this.normalizeSearchResponse(data, 'tracks'); const normalized = this.normalizeSearchResponse(data, 'tracks');
const preparedTracks = normalized.items.map((t) => this.prepareTrack(t));
const enrichedTracks = await this.enrichTracksWithAlbumDates(preparedTracks);
const result = { const result = {
...normalized, ...normalized,
items: normalized.items.map((t) => this.prepareTrack(t)), items: enrichedTracks,
}; };
await this.cache.set('search_tracks', query, result); await this.cache.set('search_tracks', query, result);
@ -466,6 +496,16 @@ export class LosslessAPI {
} }
} }
// Enrich tracks with album releaseDate if available
if (album?.releaseDate) {
tracks = tracks.map((track) => {
if (track.album && !track.album.releaseDate) {
return { ...track, album: { ...track.album, releaseDate: album.releaseDate } };
}
return track;
});
}
const result = { album, tracks }; const result = { album, tracks };
await this.cache.set('album', id, result); await this.cache.set('album', id, result);
@ -572,6 +612,9 @@ export class LosslessAPI {
} }
} }
// Enrich tracks with album release dates
tracks = await this.enrichTracksWithAlbumDates(tracks);
const result = { playlist, tracks }; const result = { playlist, tracks };
await this.cache.set('playlist', id, result); await this.cache.set('playlist', id, result);
@ -592,7 +635,10 @@ export class LosslessAPI {
throw new Error('Mix metadata not found'); throw new Error('Mix metadata not found');
} }
const tracks = items.map((i) => this.prepareTrack(i.item || i)); let tracks = items.map((i) => this.prepareTrack(i.item || i));
// Enrich tracks with album release dates
tracks = await this.enrichTracksWithAlbumDates(tracks);
const mix = { const mix = {
id: mixData.id, id: mixData.id,
@ -689,10 +735,13 @@ export class LosslessAPI {
const eps = allReleases.filter((a) => a.type === 'EP' || a.type === 'SINGLE'); const eps = allReleases.filter((a) => a.type === 'EP' || a.type === 'SINGLE');
const albums = allReleases.filter((a) => !eps.includes(a)); const albums = allReleases.filter((a) => !eps.includes(a));
const tracks = Array.from(trackMap.values()) const topTracks = Array.from(trackMap.values())
.sort((a, b) => (b.popularity || 0) - (a.popularity || 0)) .sort((a, b) => (b.popularity || 0) - (a.popularity || 0))
.slice(0, 15); .slice(0, 15);
// Enrich tracks with album release dates
const tracks = await this.enrichTracksWithAlbumDates(topTracks);
const result = { ...artist, albums, eps, tracks }; const result = { ...artist, albums, eps, tracks };
await this.cache.set('artist', artistId, result); await this.cache.set('artist', artistId, result);

View file

@ -6,6 +6,7 @@ import {
getTrackArtists, getTrackArtists,
getTrackTitle, getTrackTitle,
getTrackArtistsHTML, getTrackArtistsHTML,
getTrackYearDisplay,
createQualityBadgeHTML, createQualityBadgeHTML,
} from './utils.js'; } from './utils.js';
import { queueManager, replayGainSettings } from './storage.js'; import { queueManager, replayGainSettings } from './storage.js';
@ -112,15 +113,7 @@ export class Player {
const track = this.currentTrack; const track = this.currentTrack;
const trackTitle = getTrackTitle(track); const trackTitle = getTrackTitle(track);
const trackArtistsHTML = getTrackArtistsHTML(track); const trackArtistsHTML = getTrackArtistsHTML(track);
const yearDisplay = getTrackYearDisplay(track);
let yearDisplay = '';
const releaseDate = track.album?.releaseDate || track.streamStartDate;
if (releaseDate) {
const date = new Date(releaseDate);
if (!isNaN(date.getTime())) {
yearDisplay = `${date.getFullYear()}`;
}
}
const coverEl = document.querySelector('.now-playing-bar .cover'); const coverEl = document.querySelector('.now-playing-bar .cover');
const titleEl = document.querySelector('.now-playing-bar .title'); const titleEl = document.querySelector('.now-playing-bar .title');
@ -144,6 +137,11 @@ export class Player {
} }
if (artistEl) artistEl.innerHTML = trackArtistsHTML + yearDisplay; if (artistEl) artistEl.innerHTML = trackArtistsHTML + yearDisplay;
// Fetch album release date in background if missing
if (!yearDisplay && track.album?.id) {
this.loadAlbumYear(track, trackArtistsHTML, artistEl);
}
const mixBtn = document.getElementById('now-playing-mix-btn'); const mixBtn = document.getElementById('now-playing-mix-btn');
if (mixBtn) { if (mixBtn) {
mixBtn.style.display = track.mixes && track.mixes.TRACK_MIX ? 'flex' : 'none'; mixBtn.style.display = track.mixes && track.mixes.TRACK_MIX ? 'flex' : 'none';
@ -278,19 +276,10 @@ export class Player {
const trackTitle = getTrackTitle(track); const trackTitle = getTrackTitle(track);
const trackArtistsHTML = getTrackArtistsHTML(track); const trackArtistsHTML = getTrackArtistsHTML(track);
const yearDisplay = getTrackYearDisplay(track);
let yearDisplay = '';
const releaseDate = track.album?.releaseDate || track.streamStartDate;
if (releaseDate) {
const date = new Date(releaseDate);
if (!isNaN(date.getTime())) {
yearDisplay = `${date.getFullYear()}`;
}
}
document.querySelector('.now-playing-bar .cover').src = this.api.getCoverUrl(track.album?.cover); document.querySelector('.now-playing-bar .cover').src = this.api.getCoverUrl(track.album?.cover);
const qualityBadge = createQualityBadgeHTML(track); document.querySelector('.now-playing-bar .title').innerHTML = `${trackTitle} ${createQualityBadgeHTML(track)}`;
document.querySelector('.now-playing-bar .title').innerHTML = `${trackTitle} ${qualityBadge}`;
const albumEl = document.querySelector('.now-playing-bar .album'); const albumEl = document.querySelector('.now-playing-bar .album');
if (albumEl) { if (albumEl) {
const albumTitle = track.album?.title || ''; const albumTitle = track.album?.title || '';
@ -302,7 +291,13 @@ export class Player {
albumEl.style.display = 'none'; albumEl.style.display = 'none';
} }
} }
document.querySelector('.now-playing-bar .artist').innerHTML = trackArtistsHTML + yearDisplay; const artistEl = document.querySelector('.now-playing-bar .artist');
artistEl.innerHTML = trackArtistsHTML + yearDisplay;
// Fetch album release date in background if missing
if (!yearDisplay && track.album?.id) {
this.loadAlbumYear(track, trackArtistsHTML, artistEl);
}
const mixBtn = document.getElementById('now-playing-mix-btn'); const mixBtn = document.getElementById('now-playing-mix-btn');
if (mixBtn) { if (mixBtn) {
@ -721,6 +716,18 @@ export class Player {
return null; return null;
} }
loadAlbumYear(track, trackArtistsHTML, artistEl) {
this.api.getAlbum(track.album.id).then(({ album }) => {
if (album?.releaseDate && this.currentTrack?.id === track.id) {
track.album.releaseDate = album.releaseDate;
const year = new Date(album.releaseDate).getFullYear();
if (!isNaN(year) && artistEl) {
artistEl.innerHTML = `${trackArtistsHTML}${year}`;
}
}
}).catch(() => {});
}
updatePlayingTrackIndicator() { updatePlayingTrackIndicator() {
const currentTrack = this.getCurrentQueue()[this.currentQueueIndex]; const currentTrack = this.getCurrentQueue()[this.currentQueueIndex];
document.querySelectorAll('.track-item').forEach((item) => { document.querySelectorAll('.track-item').forEach((item) => {

View file

@ -13,6 +13,7 @@ import {
hasExplicitContent, hasExplicitContent,
getTrackArtists, getTrackArtists,
getTrackTitle, getTrackTitle,
getTrackYearDisplay,
createQualityBadgeHTML, createQualityBadgeHTML,
calculateTotalDuration, calculateTotalDuration,
formatDuration, formatDuration,
@ -234,14 +235,7 @@ export class UIRenderer {
showCover = false; showCover = false;
} }
let yearDisplay = ''; const yearDisplay = getTrackYearDisplay(track);
const releaseDate = track.album?.releaseDate || track.streamStartDate;
if (releaseDate) {
const date = new Date(releaseDate);
if (!isNaN(date.getTime())) {
yearDisplay = `${date.getFullYear()}`;
}
}
const actionsHTML = isUnavailable const actionsHTML = isUnavailable
? '' ? ''

View file

@ -66,6 +66,13 @@ export const formatTime = (seconds) => {
return `${m}:${String(s).padStart(2, '0')}`; return `${m}:${String(s).padStart(2, '0')}`;
}; };
export const getTrackYearDisplay = (track) => {
const releaseDate = track?.album?.releaseDate || track?.streamStartDate;
if (!releaseDate) return '';
const date = new Date(releaseDate);
return isNaN(date.getTime()) ? '' : `${date.getFullYear()}`;
};
export const createPlaceholder = (text, isLoading = false) => { export const createPlaceholder = (text, isLoading = false) => {
return `<div class="placeholder-text ${isLoading ? 'loading' : ''}">${text}</div>`; return `<div class="placeholder-text ${isLoading ? 'loading' : ''}">${text}</div>`;
}; };