Merge branch 'main' into feature/tabbed-settings
This commit is contained in:
commit
1e61091df6
7 changed files with 150 additions and 34 deletions
21
index.html
21
index.html
|
|
@ -2404,6 +2404,27 @@
|
|||
<input type="checkbox" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="show-quality-badges-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Album release year</span>
|
||||
<span class="description"
|
||||
>Show original album year instead of track/remaster date</span
|
||||
>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="use-album-release-year-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Gapless Playback</span>
|
||||
<span class="description">Play audio without interruption between tracks</span>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
|
|
|
|||
58
js/api.js
58
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);
|
||||
|
|
|
|||
56
js/player.js
56
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) => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
||||
|
|
|
|||
10
js/ui.js
10
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
|
||||
? ''
|
||||
|
|
|
|||
12
js/utils.js
12
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 `<div class="placeholder-text ${isLoading ? 'loading' : ''}">${text}</div>`;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue