Add multi-disc ZIP folders and fix playlist extension paths
This commit is contained in:
parent
400197aabc
commit
bf346f756e
5 changed files with 287 additions and 27 deletions
12
index.html
12
index.html
|
|
@ -4501,6 +4501,18 @@
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="info">
|
||||||
|
<span class="label">Separate Discs in ZIP</span>
|
||||||
|
<span class="description"
|
||||||
|
>Put tracks in Disc folders when a release has multiple discs</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="separate-discs-zip-toggle" />
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
258
js/downloads.js
258
js/downloads.js
|
|
@ -31,6 +31,88 @@ async function loadClientZip() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toPositiveInt(value) {
|
||||||
|
const parsed = parseInt(value, 10);
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExplicitTrackDiscNumber(track) {
|
||||||
|
const candidates = [
|
||||||
|
track?.volumeNumber,
|
||||||
|
track?.discNumber,
|
||||||
|
track?.mediaNumber,
|
||||||
|
track?.media_number,
|
||||||
|
track?.volume,
|
||||||
|
track?.disc,
|
||||||
|
track?.volume?.number,
|
||||||
|
track?.disc?.number,
|
||||||
|
track?.media?.number,
|
||||||
|
track?.disc,
|
||||||
|
track?.disc_no,
|
||||||
|
track?.discNo,
|
||||||
|
track?.disc_number,
|
||||||
|
track?.mediaMetadata?.discNumber,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const parsed = toPositiveInt(candidate);
|
||||||
|
if (parsed) return parsed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDiscLayoutContext(tracks, api) {
|
||||||
|
if (!playlistSettings.shouldSeparateDiscsInZip()) {
|
||||||
|
return { separateByDisc: false, resolveDiscNumber: () => 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const explicitDiscNumbers = tracks.map((track) => getExplicitTrackDiscNumber(track));
|
||||||
|
const explicitDistinct = new Set(explicitDiscNumbers.filter(Boolean));
|
||||||
|
|
||||||
|
if (explicitDistinct.size > 1) {
|
||||||
|
return {
|
||||||
|
separateByDisc: true,
|
||||||
|
resolveDiscNumber: (index) => explicitDiscNumbers[index] || 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some providers omit disc fields in album payload but include them in full track metadata.
|
||||||
|
const hydratedDiscNumbers = await Promise.all(
|
||||||
|
tracks.map(async (track, index) => {
|
||||||
|
if (explicitDiscNumbers[index]) return explicitDiscNumbers[index];
|
||||||
|
try {
|
||||||
|
const fullTrack = await api.getTrackMetadata(track.id);
|
||||||
|
return getExplicitTrackDiscNumber(fullTrack);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const hydratedDistinct = new Set(hydratedDiscNumbers.filter(Boolean));
|
||||||
|
if (hydratedDistinct.size > 1) {
|
||||||
|
return {
|
||||||
|
separateByDisc: true,
|
||||||
|
resolveDiscNumber: (index) => hydratedDiscNumbers[index] || explicitDiscNumbers[index] || 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { separateByDisc: false, resolveDiscNumber: () => 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDiscFolderName(discNumber) {
|
||||||
|
return `Disc ${discNumber}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildZipTrackPath(rootFolder, filename, separateByDisc, discNumber = 1) {
|
||||||
|
if (!separateByDisc) return `${rootFolder}/${filename}`;
|
||||||
|
return `${rootFolder}/${getDiscFolderName(discNumber)}/${filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlaylistAudioExtension(quality) {
|
||||||
|
return quality === 'LOW' || quality === 'HIGH' ? 'm4a' : 'flac';
|
||||||
|
}
|
||||||
|
|
||||||
function createDownloadNotification() {
|
function createDownloadNotification() {
|
||||||
if (!downloadNotificationContainer) {
|
if (!downloadNotificationContainer) {
|
||||||
downloadNotificationContainer = document.createElement('div');
|
downloadNotificationContainer = document.createElement('div');
|
||||||
|
|
@ -190,6 +272,26 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null, sign
|
||||||
artist: track.artist || (track.artists && track.artists.length > 0 ? track.artists[0] : null),
|
artist: track.artist || (track.artists && track.artists.length > 0 ? track.artists[0] : null),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fullTrack = await api.getTrackMetadata(track.id);
|
||||||
|
if (fullTrack) {
|
||||||
|
enrichedTrack = {
|
||||||
|
...fullTrack,
|
||||||
|
...enrichedTrack,
|
||||||
|
artist: enrichedTrack.artist || fullTrack.artist,
|
||||||
|
album: {
|
||||||
|
...(fullTrack.album || {}),
|
||||||
|
...(enrichedTrack.album || {}),
|
||||||
|
},
|
||||||
|
// Preserve explicit disc fields from either source
|
||||||
|
discNumber: enrichedTrack.discNumber ?? fullTrack.discNumber,
|
||||||
|
volumeNumber: enrichedTrack.volumeNumber ?? fullTrack.volumeNumber,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Non-fatal: continue with best available track payload
|
||||||
|
}
|
||||||
|
|
||||||
if (enrichedTrack.album && (!enrichedTrack.album.title || !enrichedTrack.album.artist) && enrichedTrack.album.id) {
|
if (enrichedTrack.album && (!enrichedTrack.album.title || !enrichedTrack.album.artist) && enrichedTrack.album.id) {
|
||||||
try {
|
try {
|
||||||
const albumData = await api.getAlbum(enrichedTrack.album.id);
|
const albumData = await api.getAlbum(enrichedTrack.album.id);
|
||||||
|
|
@ -323,9 +425,21 @@ async function bulkDownloadToZipStream(
|
||||||
|
|
||||||
// Generate playlist files first
|
// Generate playlist files first
|
||||||
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
|
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
|
||||||
|
const playlistAudioExtension = getPlaylistAudioExtension(quality);
|
||||||
|
const discLayout = await createDiscLayoutContext(tracks, api);
|
||||||
|
const separateByDisc = discLayout.separateByDisc;
|
||||||
|
const playlistPathResolver = separateByDisc
|
||||||
|
? (_track, filename, index) => `${getDiscFolderName(discLayout.resolveDiscNumber(index))}/${filename}`
|
||||||
|
: null;
|
||||||
|
|
||||||
if (playlistSettings.shouldGenerateM3U()) {
|
if (playlistSettings.shouldGenerateM3U()) {
|
||||||
const m3uContent = generateM3U(metadata || { title: folderName }, tracks, useRelativePaths);
|
const m3uContent = generateM3U(
|
||||||
|
metadata || { title: folderName },
|
||||||
|
tracks,
|
||||||
|
useRelativePaths,
|
||||||
|
playlistPathResolver,
|
||||||
|
playlistAudioExtension
|
||||||
|
);
|
||||||
yield {
|
yield {
|
||||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
|
|
@ -334,7 +448,13 @@ async function bulkDownloadToZipStream(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playlistSettings.shouldGenerateM3U8()) {
|
if (playlistSettings.shouldGenerateM3U8()) {
|
||||||
const m3u8Content = generateM3U8(metadata || { title: folderName }, tracks, useRelativePaths);
|
const m3u8Content = generateM3U8(
|
||||||
|
metadata || { title: folderName },
|
||||||
|
tracks,
|
||||||
|
useRelativePaths,
|
||||||
|
playlistPathResolver,
|
||||||
|
playlistAudioExtension
|
||||||
|
);
|
||||||
yield {
|
yield {
|
||||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
|
|
@ -382,7 +502,12 @@ async function bulkDownloadToZipStream(
|
||||||
try {
|
try {
|
||||||
const { blob, extension } = await downloadTrackBlob(track, quality, api, null, signal);
|
const { blob, extension } = await downloadTrackBlob(track, quality, api, null, signal);
|
||||||
const filename = buildTrackFilename(track, quality, extension);
|
const filename = buildTrackFilename(track, quality, extension);
|
||||||
yield { name: `${folderName}/${filename}`, lastModified: new Date(), input: blob };
|
const discNumber = discLayout.resolveDiscNumber(i);
|
||||||
|
yield {
|
||||||
|
name: buildZipTrackPath(folderName, filename, separateByDisc, discNumber),
|
||||||
|
lastModified: new Date(),
|
||||||
|
input: blob,
|
||||||
|
};
|
||||||
|
|
||||||
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -392,7 +517,7 @@ async function bulkDownloadToZipStream(
|
||||||
if (lrcContent) {
|
if (lrcContent) {
|
||||||
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
|
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
|
||||||
yield {
|
yield {
|
||||||
name: `${folderName}/${lrcFilename}`,
|
name: buildZipTrackPath(folderName, lrcFilename, separateByDisc, discNumber),
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
input: lrcContent,
|
input: lrcContent,
|
||||||
};
|
};
|
||||||
|
|
@ -442,9 +567,21 @@ async function bulkDownloadToZipBlob(
|
||||||
|
|
||||||
// Generate playlist files first
|
// Generate playlist files first
|
||||||
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
|
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
|
||||||
|
const playlistAudioExtension = getPlaylistAudioExtension(quality);
|
||||||
|
const discLayout = await createDiscLayoutContext(tracks, api);
|
||||||
|
const separateByDisc = discLayout.separateByDisc;
|
||||||
|
const playlistPathResolver = separateByDisc
|
||||||
|
? (_track, filename, index) => `${getDiscFolderName(discLayout.resolveDiscNumber(index))}/${filename}`
|
||||||
|
: null;
|
||||||
|
|
||||||
if (playlistSettings.shouldGenerateM3U()) {
|
if (playlistSettings.shouldGenerateM3U()) {
|
||||||
const m3uContent = generateM3U(metadata || { title: folderName }, tracks, useRelativePaths);
|
const m3uContent = generateM3U(
|
||||||
|
metadata || { title: folderName },
|
||||||
|
tracks,
|
||||||
|
useRelativePaths,
|
||||||
|
playlistPathResolver,
|
||||||
|
playlistAudioExtension
|
||||||
|
);
|
||||||
yield {
|
yield {
|
||||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
|
|
@ -453,7 +590,13 @@ async function bulkDownloadToZipBlob(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playlistSettings.shouldGenerateM3U8()) {
|
if (playlistSettings.shouldGenerateM3U8()) {
|
||||||
const m3u8Content = generateM3U8(metadata || { title: folderName }, tracks, useRelativePaths);
|
const m3u8Content = generateM3U8(
|
||||||
|
metadata || { title: folderName },
|
||||||
|
tracks,
|
||||||
|
useRelativePaths,
|
||||||
|
playlistPathResolver,
|
||||||
|
playlistAudioExtension
|
||||||
|
);
|
||||||
yield {
|
yield {
|
||||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
|
|
@ -501,7 +644,12 @@ async function bulkDownloadToZipBlob(
|
||||||
try {
|
try {
|
||||||
const { blob, extension } = await downloadTrackBlob(track, quality, api, null, signal);
|
const { blob, extension } = await downloadTrackBlob(track, quality, api, null, signal);
|
||||||
const filename = buildTrackFilename(track, quality, extension);
|
const filename = buildTrackFilename(track, quality, extension);
|
||||||
yield { name: `${folderName}/${filename}`, lastModified: new Date(), input: blob };
|
const discNumber = discLayout.resolveDiscNumber(i);
|
||||||
|
yield {
|
||||||
|
name: buildZipTrackPath(folderName, filename, separateByDisc, discNumber),
|
||||||
|
lastModified: new Date(),
|
||||||
|
input: blob,
|
||||||
|
};
|
||||||
|
|
||||||
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -511,7 +659,7 @@ async function bulkDownloadToZipBlob(
|
||||||
if (lrcContent) {
|
if (lrcContent) {
|
||||||
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
|
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
|
||||||
yield {
|
yield {
|
||||||
name: `${folderName}/${lrcFilename}`,
|
name: buildZipTrackPath(folderName, lrcFilename, separateByDisc, discNumber),
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
input: lrcContent,
|
input: lrcContent,
|
||||||
};
|
};
|
||||||
|
|
@ -562,9 +710,21 @@ async function bulkDownloadToZipNeutralino(
|
||||||
|
|
||||||
// Generate playlist files first
|
// Generate playlist files first
|
||||||
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
|
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
|
||||||
|
const playlistAudioExtension = getPlaylistAudioExtension(quality);
|
||||||
|
const discLayout = await createDiscLayoutContext(tracks, api);
|
||||||
|
const separateByDisc = discLayout.separateByDisc;
|
||||||
|
const playlistPathResolver = separateByDisc
|
||||||
|
? (_track, filename, index) => `${getDiscFolderName(discLayout.resolveDiscNumber(index))}/${filename}`
|
||||||
|
: null;
|
||||||
|
|
||||||
if (playlistSettings.shouldGenerateM3U()) {
|
if (playlistSettings.shouldGenerateM3U()) {
|
||||||
const m3uContent = generateM3U(metadata || { title: folderName }, tracks, useRelativePaths);
|
const m3uContent = generateM3U(
|
||||||
|
metadata || { title: folderName },
|
||||||
|
tracks,
|
||||||
|
useRelativePaths,
|
||||||
|
playlistPathResolver,
|
||||||
|
playlistAudioExtension
|
||||||
|
);
|
||||||
yield {
|
yield {
|
||||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
|
|
@ -573,7 +733,13 @@ async function bulkDownloadToZipNeutralino(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playlistSettings.shouldGenerateM3U8()) {
|
if (playlistSettings.shouldGenerateM3U8()) {
|
||||||
const m3u8Content = generateM3U8(metadata || { title: folderName }, tracks, useRelativePaths);
|
const m3u8Content = generateM3U8(
|
||||||
|
metadata || { title: folderName },
|
||||||
|
tracks,
|
||||||
|
useRelativePaths,
|
||||||
|
playlistPathResolver,
|
||||||
|
playlistAudioExtension
|
||||||
|
);
|
||||||
yield {
|
yield {
|
||||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
|
|
@ -621,7 +787,12 @@ async function bulkDownloadToZipNeutralino(
|
||||||
try {
|
try {
|
||||||
const { blob, extension } = await downloadTrackBlob(track, quality, api, null, signal);
|
const { blob, extension } = await downloadTrackBlob(track, quality, api, null, signal);
|
||||||
const filename = buildTrackFilename(track, quality, extension);
|
const filename = buildTrackFilename(track, quality, extension);
|
||||||
yield { name: `${folderName}/${filename}`, lastModified: new Date(), input: blob };
|
const discNumber = discLayout.resolveDiscNumber(i);
|
||||||
|
yield {
|
||||||
|
name: buildZipTrackPath(folderName, filename, separateByDisc, discNumber),
|
||||||
|
lastModified: new Date(),
|
||||||
|
input: blob,
|
||||||
|
};
|
||||||
|
|
||||||
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -631,7 +802,7 @@ async function bulkDownloadToZipNeutralino(
|
||||||
if (lrcContent) {
|
if (lrcContent) {
|
||||||
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
|
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
|
||||||
yield {
|
yield {
|
||||||
name: `${folderName}/${lrcFilename}`,
|
name: buildZipTrackPath(folderName, lrcFilename, separateByDisc, discNumber),
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
input: lrcContent,
|
input: lrcContent,
|
||||||
};
|
};
|
||||||
|
|
@ -718,8 +889,9 @@ async function startBulkDownload(
|
||||||
const isNeutralino = window.NL_MODE === true;
|
const isNeutralino = window.NL_MODE === true;
|
||||||
const hasFileSystemAccess =
|
const hasFileSystemAccess =
|
||||||
'showSaveFilePicker' in window && 'createWritable' in FileSystemFileHandle.prototype;
|
'showSaveFilePicker' in window && 'createWritable' in FileSystemFileHandle.prototype;
|
||||||
const useZip = hasFileSystemAccess && !bulkDownloadSettings.shouldForceIndividual();
|
const forceIndividual = bulkDownloadSettings.shouldForceIndividual();
|
||||||
const useZipBlob = !hasFileSystemAccess && !bulkDownloadSettings.shouldForceIndividual();
|
const useZip = hasFileSystemAccess && !forceIndividual;
|
||||||
|
const useZipBlob = !hasFileSystemAccess && !forceIndividual;
|
||||||
|
|
||||||
if (isNeutralino) {
|
if (isNeutralino) {
|
||||||
// Neutralino Native Logic
|
// Neutralino Native Logic
|
||||||
|
|
@ -871,9 +1043,21 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
|
||||||
|
|
||||||
// Generate playlist files for each album
|
// Generate playlist files for each album
|
||||||
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
|
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
|
||||||
|
const playlistAudioExtension = getPlaylistAudioExtension(quality);
|
||||||
|
const discLayout = await createDiscLayoutContext(tracks, api);
|
||||||
|
const separateByDisc = discLayout.separateByDisc;
|
||||||
|
const playlistPathResolver = separateByDisc
|
||||||
|
? (_track, filename, index) => `${getDiscFolderName(discLayout.resolveDiscNumber(index))}/${filename}`
|
||||||
|
: null;
|
||||||
|
|
||||||
if (playlistSettings.shouldGenerateM3U()) {
|
if (playlistSettings.shouldGenerateM3U()) {
|
||||||
const m3uContent = generateM3U(fullAlbum, tracks, useRelativePaths);
|
const m3uContent = generateM3U(
|
||||||
|
fullAlbum,
|
||||||
|
tracks,
|
||||||
|
useRelativePaths,
|
||||||
|
playlistPathResolver,
|
||||||
|
playlistAudioExtension
|
||||||
|
);
|
||||||
yield {
|
yield {
|
||||||
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.m3u`,
|
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.m3u`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
|
|
@ -882,7 +1066,13 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playlistSettings.shouldGenerateM3U8()) {
|
if (playlistSettings.shouldGenerateM3U8()) {
|
||||||
const m3u8Content = generateM3U8(fullAlbum, tracks, useRelativePaths);
|
const m3u8Content = generateM3U8(
|
||||||
|
fullAlbum,
|
||||||
|
tracks,
|
||||||
|
useRelativePaths,
|
||||||
|
playlistPathResolver,
|
||||||
|
playlistAudioExtension
|
||||||
|
);
|
||||||
yield {
|
yield {
|
||||||
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.m3u8`,
|
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.m3u8`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
|
|
@ -918,12 +1108,18 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const track of tracks) {
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
|
const track = tracks[i];
|
||||||
if (signal.aborted) break;
|
if (signal.aborted) break;
|
||||||
try {
|
try {
|
||||||
const { blob, extension } = await downloadTrackBlob(track, quality, api, null, signal);
|
const { blob, extension } = await downloadTrackBlob(track, quality, api, null, signal);
|
||||||
const filename = buildTrackFilename(track, quality, extension);
|
const filename = buildTrackFilename(track, quality, extension);
|
||||||
yield { name: `${fullFolderPath}/${filename}`, lastModified: new Date(), input: blob };
|
const discNumber = discLayout.resolveDiscNumber(i);
|
||||||
|
yield {
|
||||||
|
name: buildZipTrackPath(fullFolderPath, filename, separateByDisc, discNumber),
|
||||||
|
lastModified: new Date(),
|
||||||
|
input: blob,
|
||||||
|
};
|
||||||
|
|
||||||
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -933,7 +1129,12 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
|
||||||
if (lrcContent) {
|
if (lrcContent) {
|
||||||
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
|
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
|
||||||
yield {
|
yield {
|
||||||
name: `${fullFolderPath}/${lrcFilename}`,
|
name: buildZipTrackPath(
|
||||||
|
fullFolderPath,
|
||||||
|
lrcFilename,
|
||||||
|
separateByDisc,
|
||||||
|
discNumber
|
||||||
|
),
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
input: lrcContent,
|
input: lrcContent,
|
||||||
};
|
};
|
||||||
|
|
@ -1097,6 +1298,25 @@ export async function downloadTrackWithMetadata(track, quality, api, lyricsManag
|
||||||
artist: track.artist || (track.artists && track.artists.length > 0 ? track.artists[0] : null),
|
artist: track.artist || (track.artists && track.artists.length > 0 ? track.artists[0] : null),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fullTrack = await api.getTrackMetadata(track.id);
|
||||||
|
if (fullTrack) {
|
||||||
|
enrichedTrack = {
|
||||||
|
...fullTrack,
|
||||||
|
...enrichedTrack,
|
||||||
|
artist: enrichedTrack.artist || fullTrack.artist,
|
||||||
|
album: {
|
||||||
|
...(fullTrack.album || {}),
|
||||||
|
...(enrichedTrack.album || {}),
|
||||||
|
},
|
||||||
|
discNumber: enrichedTrack.discNumber ?? fullTrack.discNumber,
|
||||||
|
volumeNumber: enrichedTrack.volumeNumber ?? fullTrack.volumeNumber,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Continue with available track payload
|
||||||
|
}
|
||||||
|
|
||||||
if (enrichedTrack.album && (!enrichedTrack.album.title || !enrichedTrack.album.artist) && enrichedTrack.album.id) {
|
if (enrichedTrack.album && (!enrichedTrack.album.title || !enrichedTrack.album.artist) && enrichedTrack.album.id) {
|
||||||
try {
|
try {
|
||||||
const albumData = await api.getAlbum(enrichedTrack.album.id);
|
const albumData = await api.getAlbum(enrichedTrack.album.id);
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ import { sanitizeForFilename } from './utils.js';
|
||||||
* @param {Object} playlist - Playlist metadata (title, artist, etc.)
|
* @param {Object} playlist - Playlist metadata (title, artist, etc.)
|
||||||
* @param {Array} tracks - Array of track objects
|
* @param {Array} tracks - Array of track objects
|
||||||
* @param {boolean} useRelativePaths - Whether to use relative paths
|
* @param {boolean} useRelativePaths - Whether to use relative paths
|
||||||
|
* @param {Function|null} pathResolver - Optional resolver for per-track relative path
|
||||||
|
* @param {string} audioExtension - Audio file extension used in generated paths
|
||||||
* @returns {string} M3U content
|
* @returns {string} M3U content
|
||||||
*/
|
*/
|
||||||
export function generateM3U(playlist, tracks, useRelativePaths = true) {
|
export function generateM3U(playlist, tracks, useRelativePaths = true, pathResolver = null, audioExtension = 'flac') {
|
||||||
let content = '#EXTM3U\n';
|
let content = '#EXTM3U\n';
|
||||||
|
|
||||||
if (playlist.title) {
|
if (playlist.title) {
|
||||||
|
|
@ -29,8 +31,9 @@ export function generateM3U(playlist, tracks, useRelativePaths = true) {
|
||||||
|
|
||||||
content += `#EXTINF:${duration},${displayName}\n`;
|
content += `#EXTINF:${duration},${displayName}\n`;
|
||||||
|
|
||||||
const filename = getTrackFilename(track, index + 1);
|
const filename = getTrackFilename(track, index + 1, audioExtension);
|
||||||
const path = useRelativePaths ? filename : filename;
|
const relativePath = typeof pathResolver === 'function' ? pathResolver(track, filename, index) : filename;
|
||||||
|
const path = useRelativePaths ? relativePath : relativePath;
|
||||||
|
|
||||||
content += `${path}\n\n`;
|
content += `${path}\n\n`;
|
||||||
});
|
});
|
||||||
|
|
@ -43,9 +46,11 @@ export function generateM3U(playlist, tracks, useRelativePaths = true) {
|
||||||
* @param {Object} playlist - Playlist metadata
|
* @param {Object} playlist - Playlist metadata
|
||||||
* @param {Array} tracks - Array of track objects
|
* @param {Array} tracks - Array of track objects
|
||||||
* @param {boolean} useRelativePaths - Whether to use relative paths
|
* @param {boolean} useRelativePaths - Whether to use relative paths
|
||||||
|
* @param {Function|null} pathResolver - Optional resolver for per-track relative path
|
||||||
|
* @param {string} audioExtension - Audio file extension used in generated paths
|
||||||
* @returns {string} M3U8 content
|
* @returns {string} M3U8 content
|
||||||
*/
|
*/
|
||||||
export function generateM3U8(playlist, tracks, useRelativePaths = true) {
|
export function generateM3U8(playlist, tracks, useRelativePaths = true, pathResolver = null, audioExtension = 'flac') {
|
||||||
let content = '#EXTM3U\n';
|
let content = '#EXTM3U\n';
|
||||||
content += '#EXT-X-VERSION:3\n';
|
content += '#EXT-X-VERSION:3\n';
|
||||||
content += '#EXT-X-PLAYLIST-TYPE:VOD\n';
|
content += '#EXT-X-PLAYLIST-TYPE:VOD\n';
|
||||||
|
|
@ -72,8 +77,9 @@ export function generateM3U8(playlist, tracks, useRelativePaths = true) {
|
||||||
|
|
||||||
content += `#EXTINF:${duration}.000,${displayName}\n`;
|
content += `#EXTINF:${duration}.000,${displayName}\n`;
|
||||||
|
|
||||||
const filename = getTrackFilename(track, index + 1);
|
const filename = getTrackFilename(track, index + 1, audioExtension);
|
||||||
const path = useRelativePaths ? filename : filename;
|
const relativePath = typeof pathResolver === 'function' ? pathResolver(track, filename, index) : filename;
|
||||||
|
const path = useRelativePaths ? relativePath : relativePath;
|
||||||
|
|
||||||
content += `${path}\n\n`;
|
content += `${path}\n\n`;
|
||||||
});
|
});
|
||||||
|
|
@ -242,7 +248,7 @@ function getTrackArtists(track) {
|
||||||
/**
|
/**
|
||||||
* Helper function to get track filename
|
* Helper function to get track filename
|
||||||
*/
|
*/
|
||||||
function getTrackFilename(track, trackNumber = 1) {
|
function getTrackFilename(track, trackNumber = 1, audioExtension = 'flac') {
|
||||||
const paddedNumber = String(trackNumber).padStart(2, '0');
|
const paddedNumber = String(trackNumber).padStart(2, '0');
|
||||||
const artists = getTrackArtists(track);
|
const artists = getTrackArtists(track);
|
||||||
const title = track.title || 'Unknown Title';
|
const title = track.title || 'Unknown Title';
|
||||||
|
|
@ -250,7 +256,7 @@ function getTrackFilename(track, trackNumber = 1) {
|
||||||
const sanitizedArtists = sanitizeForFilename(artists);
|
const sanitizedArtists = sanitizeForFilename(artists);
|
||||||
const sanitizedTitle = sanitizeForFilename(title);
|
const sanitizedTitle = sanitizeForFilename(title);
|
||||||
|
|
||||||
return `${paddedNumber} - ${sanitizedArtists} - ${sanitizedTitle}.flac`;
|
return `${paddedNumber} - ${sanitizedArtists} - ${sanitizedTitle}.${audioExtension}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2533,6 +2533,14 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const separateDiscsZipToggle = document.getElementById('separate-discs-zip-toggle');
|
||||||
|
if (separateDiscsZipToggle) {
|
||||||
|
separateDiscsZipToggle.checked = playlistSettings.shouldSeparateDiscsInZip();
|
||||||
|
separateDiscsZipToggle.addEventListener('change', (e) => {
|
||||||
|
playlistSettings.setSeparateDiscsInZip(e.target.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// API settings
|
// API settings
|
||||||
document.getElementById('refresh-speed-test-btn')?.addEventListener('click', async () => {
|
document.getElementById('refresh-speed-test-btn')?.addEventListener('click', async () => {
|
||||||
const btn = document.getElementById('refresh-speed-test-btn');
|
const btn = document.getElementById('refresh-speed-test-btn');
|
||||||
|
|
|
||||||
|
|
@ -638,6 +638,7 @@ export const playlistSettings = {
|
||||||
NFO_KEY: 'playlist-generate-nfo',
|
NFO_KEY: 'playlist-generate-nfo',
|
||||||
JSON_KEY: 'playlist-generate-json',
|
JSON_KEY: 'playlist-generate-json',
|
||||||
RELATIVE_PATHS_KEY: 'playlist-relative-paths',
|
RELATIVE_PATHS_KEY: 'playlist-relative-paths',
|
||||||
|
SEPARATE_DISCS_KEY: 'playlist-separate-discs-in-zip',
|
||||||
|
|
||||||
shouldGenerateM3U() {
|
shouldGenerateM3U() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -689,6 +690,15 @@ export const playlistSettings = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
shouldSeparateDiscsInZip() {
|
||||||
|
try {
|
||||||
|
const val = localStorage.getItem(this.SEPARATE_DISCS_KEY);
|
||||||
|
return val === null ? true : val === 'true';
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setGenerateM3U(enabled) {
|
setGenerateM3U(enabled) {
|
||||||
localStorage.setItem(this.M3U_KEY, enabled ? 'true' : 'false');
|
localStorage.setItem(this.M3U_KEY, enabled ? 'true' : 'false');
|
||||||
},
|
},
|
||||||
|
|
@ -712,6 +722,10 @@ export const playlistSettings = {
|
||||||
setUseRelativePaths(enabled) {
|
setUseRelativePaths(enabled) {
|
||||||
localStorage.setItem(this.RELATIVE_PATHS_KEY, enabled ? 'true' : 'false');
|
localStorage.setItem(this.RELATIVE_PATHS_KEY, enabled ? 'true' : 'false');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setSeparateDiscsInZip(enabled) {
|
||||||
|
localStorage.setItem(this.SEPARATE_DISCS_KEY, enabled ? 'true' : 'false');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const visualizerSettings = {
|
export const visualizerSettings = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue