diff --git a/index.html b/index.html index cf70332..7173669 100644 --- a/index.html +++ b/index.html @@ -2370,6 +2370,18 @@ +
+
+ Album release year + Show original album year instead of track/remaster date +
+ +
Gapless Playback diff --git a/js/api.js b/js/api.js index c775c97..23cca36 100644 --- a/js/api.js +++ b/js/api.js @@ -6,6 +6,7 @@ import { isTrackUnavailable, getExtensionFromBlob, } from './utils.js'; +import { trackDateSettings } from './storage.js'; import { APICache } from './cache.js'; import { addMetadataToAudio } from './metadata.js'; import { DashDownloader } from './dash-downloader.js'; @@ -173,6 +174,36 @@ export class LosslessAPI { return artist; } + async enrichTracksWithAlbumDates(tracks) { + if (!trackDateSettings.useAlbumYear()) return 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) { const entries = Array.isArray(data) ? data : [data]; let track, info, originalTrackUrl; @@ -272,9 +303,11 @@ export class LosslessAPI { const response = await this.fetchWithRetry(`/search/?s=${encodeURIComponent(query)}`, options); const data = await response.json(); const normalized = this.normalizeSearchResponse(data, 'tracks'); + const preparedTracks = normalized.items.map((t) => this.prepareTrack(t)); + const enrichedTracks = await this.enrichTracksWithAlbumDates(preparedTracks); const result = { ...normalized, - items: normalized.items.map((t) => this.prepareTrack(t)), + items: enrichedTracks, }; await this.cache.set('search_tracks', query, result); @@ -466,6 +499,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 }; await this.cache.set('album', id, result); @@ -572,6 +615,9 @@ export class LosslessAPI { } } + // Enrich tracks with album release dates + tracks = await this.enrichTracksWithAlbumDates(tracks); + const result = { playlist, tracks }; await this.cache.set('playlist', id, result); @@ -592,7 +638,10 @@ export class LosslessAPI { 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 = { id: mixData.id, @@ -689,10 +738,13 @@ export class LosslessAPI { const eps = allReleases.filter((a) => a.type === 'EP' || a.type === 'SINGLE'); 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)) .slice(0, 15); + // Enrich tracks with album release dates + const tracks = await this.enrichTracksWithAlbumDates(topTracks); + const result = { ...artist, albums, eps, tracks }; await this.cache.set('artist', artistId, result); diff --git a/js/player.js b/js/player.js index bea2daf..74258b8 100644 --- a/js/player.js +++ b/js/player.js @@ -6,9 +6,10 @@ import { getTrackArtists, getTrackTitle, getTrackArtistsHTML, + getTrackYearDisplay, createQualityBadgeHTML, } from './utils.js'; -import { queueManager, replayGainSettings } from './storage.js'; +import { queueManager, replayGainSettings, trackDateSettings } from './storage.js'; import { audioContextManager } from './audio-context.js'; export class Player { @@ -124,15 +125,7 @@ export class Player { const track = this.currentTrack; const trackTitle = getTrackTitle(track); const trackArtistsHTML = getTrackArtistsHTML(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 yearDisplay = getTrackYearDisplay(track); const coverEl = document.querySelector('.now-playing-bar .cover'); const titleEl = document.querySelector('.now-playing-bar .title'); @@ -156,6 +149,11 @@ export class Player { } 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'); if (mixBtn) { mixBtn.style.display = track.mixes && track.mixes.TRACK_MIX ? 'flex' : 'none'; @@ -313,19 +311,10 @@ export class Player { const trackTitle = getTrackTitle(track); const trackArtistsHTML = getTrackArtistsHTML(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 yearDisplay = getTrackYearDisplay(track); 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} ${qualityBadge}`; + document.querySelector('.now-playing-bar .title').innerHTML = `${trackTitle} ${createQualityBadgeHTML(track)}`; const albumEl = document.querySelector('.now-playing-bar .album'); if (albumEl) { const albumTitle = track.album?.title || ''; @@ -337,7 +326,13 @@ export class Player { 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'); if (mixBtn) { @@ -756,6 +751,23 @@ export class Player { return null; } + loadAlbumYear(track, trackArtistsHTML, artistEl) { + if (!trackDateSettings.useAlbumYear()) return; + + 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() { const currentTrack = this.getCurrentQueue()[this.currentQueueIndex]; document.querySelectorAll('.track-item').forEach((item) => { diff --git a/js/settings.js b/js/settings.js index 6099edc..e7bf331 100644 --- a/js/settings.js +++ b/js/settings.js @@ -12,6 +12,7 @@ import { downloadQualitySettings, coverArtSizeSettings, qualityBadgeSettings, + trackDateSettings, visualizerSettings, bulkDownloadSettings, playlistSettings, @@ -355,6 +356,15 @@ export function initializeSettings(scrobbler, player, api, ui) { }); } + // Track Date Settings + const useAlbumReleaseYearToggle = document.getElementById('use-album-release-year-toggle'); + if (useAlbumReleaseYearToggle) { + useAlbumReleaseYearToggle.checked = trackDateSettings.useAlbumYear(); + useAlbumReleaseYearToggle.addEventListener('change', (e) => { + trackDateSettings.setUseAlbumYear(e.target.checked); + }); + } + const zippedBulkDownloadsToggle = document.getElementById('zipped-bulk-downloads-toggle'); if (zippedBulkDownloadsToggle) { zippedBulkDownloadsToggle.checked = !bulkDownloadSettings.shouldForceIndividual(); diff --git a/js/storage.js b/js/storage.js index 6d6ffe7..dea1b6a 100644 --- a/js/storage.js +++ b/js/storage.js @@ -586,6 +586,23 @@ export const qualityBadgeSettings = { }, }; +export const trackDateSettings = { + STORAGE_KEY: 'use-album-release-year', + + useAlbumYear() { + try { + const val = localStorage.getItem(this.STORAGE_KEY); + return val === null ? true : val === 'true'; + } catch { + return true; + } + }, + + setUseAlbumYear(enabled) { + localStorage.setItem(this.STORAGE_KEY, enabled ? 'true' : 'false'); + }, +}; + export const bulkDownloadSettings = { STORAGE_KEY: 'force-individual-downloads', diff --git a/js/ui.js b/js/ui.js index 4ff016b..75aeafd 100644 --- a/js/ui.js +++ b/js/ui.js @@ -13,6 +13,7 @@ import { hasExplicitContent, getTrackArtists, getTrackTitle, + getTrackYearDisplay, createQualityBadgeHTML, calculateTotalDuration, formatDuration, @@ -241,14 +242,7 @@ export class UIRenderer { showCover = false; } - let yearDisplay = ''; - const releaseDate = track.album?.releaseDate || track.streamStartDate; - if (releaseDate) { - const date = new Date(releaseDate); - if (!isNaN(date.getTime())) { - yearDisplay = ` • ${date.getFullYear()}`; - } - } + const yearDisplay = getTrackYearDisplay(track); const actionsHTML = isUnavailable ? '' diff --git a/js/utils.js b/js/utils.js index 6ec9fc6..c53c376 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,5 +1,5 @@ //js/utils.js -import { qualityBadgeSettings, coverArtSizeSettings } from './storage.js'; +import { qualityBadgeSettings, coverArtSizeSettings, trackDateSettings } from './storage.js'; export const QUALITY = 'HI_RES_LOSSLESS'; @@ -66,6 +66,16 @@ export const formatTime = (seconds) => { return `${m}:${String(s).padStart(2, '0')}`; }; +export const getTrackYearDisplay = (track) => { + const useAlbumYear = trackDateSettings.useAlbumYear(); + const releaseDate = useAlbumYear + ? track?.album?.releaseDate || track?.streamStartDate + : track?.streamStartDate || track?.album?.releaseDate; + if (!releaseDate) return ''; + const date = new Date(releaseDate); + return isNaN(date.getTime()) ? '' : ` • ${date.getFullYear()}`; +}; + export const createPlaceholder = (text, isLoading = false) => { return `
${text}
`; };