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}
`;
};