fix(downloads): fix m3u generation artist [object Object] bug and mismatched file extensions
This commit is contained in:
parent
57a72ac5d7
commit
3ef50cb6ce
2 changed files with 357 additions and 326 deletions
623
js/downloads.js
623
js/downloads.js
|
|
@ -112,10 +112,6 @@ function buildZipTrackPath(rootFolder, filename, separateByDisc, discNumber = 1)
|
|||
return `${rootFolder}/${getDiscFolderName(discNumber)}/${filename}`;
|
||||
}
|
||||
|
||||
function getPlaylistAudioExtension(quality) {
|
||||
return quality === 'LOW' || quality === 'HIGH' ? 'm4a' : 'flac';
|
||||
}
|
||||
|
||||
function createDownloadNotification() {
|
||||
if (!downloadNotificationContainer) {
|
||||
downloadNotificationContainer = document.createElement('div');
|
||||
|
|
@ -505,43 +501,67 @@ async function bulkDownloadToZipStream(
|
|||
yield { name: `${folderName}/cover.jpg`, lastModified: new Date(), input: coverBlob };
|
||||
}
|
||||
|
||||
// Generate playlist files first
|
||||
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()) {
|
||||
const m3uContent = generateM3U(
|
||||
metadata || { title: folderName },
|
||||
tracks,
|
||||
useRelativePaths,
|
||||
playlistPathResolver,
|
||||
playlistAudioExtension
|
||||
);
|
||||
yield {
|
||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
||||
lastModified: new Date(),
|
||||
input: m3uContent,
|
||||
};
|
||||
}
|
||||
// Download tracks, yielding each immediately and collecting actual paths for playlist generation
|
||||
const trackPaths = [];
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
if (signal.aborted) break;
|
||||
const track = tracks[i];
|
||||
const trackTitle = getTrackTitle(track);
|
||||
|
||||
if (playlistSettings.shouldGenerateM3U8()) {
|
||||
const m3u8Content = generateM3U8(
|
||||
metadata || { title: folderName },
|
||||
tracks,
|
||||
useRelativePaths,
|
||||
playlistPathResolver,
|
||||
playlistAudioExtension
|
||||
);
|
||||
yield {
|
||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
||||
lastModified: new Date(),
|
||||
input: m3u8Content,
|
||||
};
|
||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
||||
|
||||
try {
|
||||
const { blob, extension } = await downloadTrackBlob(
|
||||
track,
|
||||
quality,
|
||||
api,
|
||||
null,
|
||||
signal,
|
||||
(p) => {
|
||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle, p);
|
||||
},
|
||||
coverBlob
|
||||
);
|
||||
const filename = buildTrackFilename(track, quality, extension);
|
||||
const discNumber = discLayout.resolveDiscNumber(i);
|
||||
const discPath = separateByDisc ? `${getDiscFolderName(discNumber)}/${filename}` : filename;
|
||||
|
||||
console.log(`[Playlist] Track ${i + 1}: ${discPath}`);
|
||||
trackPaths.push(discPath);
|
||||
|
||||
yield {
|
||||
name: buildZipTrackPath(folderName, filename, separateByDisc, discNumber),
|
||||
lastModified: new Date(),
|
||||
input: blob,
|
||||
};
|
||||
|
||||
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
||||
try {
|
||||
const lyricsData = await lyricsManager.fetchLyrics(track.id, track);
|
||||
if (lyricsData) {
|
||||
const lrcContent = lyricsManager.generateLRCContent(lyricsData, track);
|
||||
if (lrcContent) {
|
||||
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
|
||||
yield {
|
||||
name: buildZipTrackPath(folderName, lrcFilename, separateByDisc, discNumber),
|
||||
lastModified: new Date(),
|
||||
input: lrcContent,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') throw err;
|
||||
console.error(`Failed to download track ${trackTitle}:`, err);
|
||||
trackPaths.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (playlistSettings.shouldGenerateNFO()) {
|
||||
|
|
@ -573,56 +593,37 @@ async function bulkDownloadToZipStream(
|
|||
};
|
||||
}
|
||||
|
||||
// Download tracks
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
if (signal.aborted) break;
|
||||
const track = tracks[i];
|
||||
const trackTitle = getTrackTitle(track);
|
||||
// Generate m3u/m3u8 last, using actual track paths collected during download
|
||||
if (playlistSettings.shouldGenerateM3U()) {
|
||||
const m3uContent = generateM3U(
|
||||
metadata || { title: folderName },
|
||||
tracks,
|
||||
useRelativePaths,
|
||||
null,
|
||||
'flac',
|
||||
trackPaths
|
||||
);
|
||||
yield {
|
||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
||||
lastModified: new Date(),
|
||||
input: m3uContent,
|
||||
};
|
||||
}
|
||||
|
||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
||||
|
||||
try {
|
||||
const { blob, extension } = await downloadTrackBlob(
|
||||
track,
|
||||
quality,
|
||||
api,
|
||||
null,
|
||||
signal,
|
||||
(p) => {
|
||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle, p);
|
||||
},
|
||||
coverBlob
|
||||
);
|
||||
const filename = buildTrackFilename(track, quality, extension);
|
||||
const discNumber = discLayout.resolveDiscNumber(i);
|
||||
yield {
|
||||
name: buildZipTrackPath(folderName, filename, separateByDisc, discNumber),
|
||||
lastModified: new Date(),
|
||||
input: blob,
|
||||
};
|
||||
|
||||
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
||||
try {
|
||||
const lyricsData = await lyricsManager.fetchLyrics(track.id, track);
|
||||
if (lyricsData) {
|
||||
const lrcContent = lyricsManager.generateLRCContent(lyricsData, track);
|
||||
if (lrcContent) {
|
||||
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
|
||||
yield {
|
||||
name: buildZipTrackPath(folderName, lrcFilename, separateByDisc, discNumber),
|
||||
lastModified: new Date(),
|
||||
input: lrcContent,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') throw err;
|
||||
console.error(`Failed to download track ${trackTitle}:`, err);
|
||||
}
|
||||
if (playlistSettings.shouldGenerateM3U8()) {
|
||||
const m3u8Content = generateM3U8(
|
||||
metadata || { title: folderName },
|
||||
tracks,
|
||||
useRelativePaths,
|
||||
null,
|
||||
'flac',
|
||||
trackPaths
|
||||
);
|
||||
yield {
|
||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
||||
lastModified: new Date(),
|
||||
input: m3u8Content,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -657,43 +658,67 @@ async function bulkDownloadToZipBlob(
|
|||
yield { name: `${folderName}/cover.jpg`, lastModified: new Date(), input: coverBlob };
|
||||
}
|
||||
|
||||
// Generate playlist files first
|
||||
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()) {
|
||||
const m3uContent = generateM3U(
|
||||
metadata || { title: folderName },
|
||||
tracks,
|
||||
useRelativePaths,
|
||||
playlistPathResolver,
|
||||
playlistAudioExtension
|
||||
);
|
||||
yield {
|
||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
||||
lastModified: new Date(),
|
||||
input: m3uContent,
|
||||
};
|
||||
}
|
||||
// Download tracks, yielding each immediately and collecting actual paths for playlist generation
|
||||
const trackPaths = [];
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
if (signal.aborted) break;
|
||||
const track = tracks[i];
|
||||
const trackTitle = getTrackTitle(track);
|
||||
|
||||
if (playlistSettings.shouldGenerateM3U8()) {
|
||||
const m3u8Content = generateM3U8(
|
||||
metadata || { title: folderName },
|
||||
tracks,
|
||||
useRelativePaths,
|
||||
playlistPathResolver,
|
||||
playlistAudioExtension
|
||||
);
|
||||
yield {
|
||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
||||
lastModified: new Date(),
|
||||
input: m3u8Content,
|
||||
};
|
||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
||||
|
||||
try {
|
||||
const { blob, extension } = await downloadTrackBlob(
|
||||
track,
|
||||
quality,
|
||||
api,
|
||||
null,
|
||||
signal,
|
||||
(p) => {
|
||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle, p);
|
||||
},
|
||||
coverBlob
|
||||
);
|
||||
const filename = buildTrackFilename(track, quality, extension);
|
||||
const discNumber = discLayout.resolveDiscNumber(i);
|
||||
const discPath = separateByDisc ? `${getDiscFolderName(discNumber)}/${filename}` : filename;
|
||||
|
||||
console.log(`[Playlist] Track ${i + 1}: ${discPath}`);
|
||||
trackPaths.push(discPath);
|
||||
|
||||
yield {
|
||||
name: buildZipTrackPath(folderName, filename, separateByDisc, discNumber),
|
||||
lastModified: new Date(),
|
||||
input: blob,
|
||||
};
|
||||
|
||||
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
||||
try {
|
||||
const lyricsData = await lyricsManager.fetchLyrics(track.id, track);
|
||||
if (lyricsData) {
|
||||
const lrcContent = lyricsManager.generateLRCContent(lyricsData, track);
|
||||
if (lrcContent) {
|
||||
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
|
||||
yield {
|
||||
name: buildZipTrackPath(folderName, lrcFilename, separateByDisc, discNumber),
|
||||
lastModified: new Date(),
|
||||
input: lrcContent,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') throw err;
|
||||
console.error(`Failed to download track ${trackTitle}:`, err);
|
||||
trackPaths.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (playlistSettings.shouldGenerateNFO()) {
|
||||
|
|
@ -725,56 +750,37 @@ async function bulkDownloadToZipBlob(
|
|||
};
|
||||
}
|
||||
|
||||
// Download tracks
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
if (signal.aborted) break;
|
||||
const track = tracks[i];
|
||||
const trackTitle = getTrackTitle(track);
|
||||
// Generate m3u/m3u8 last, using actual track paths collected during download
|
||||
if (playlistSettings.shouldGenerateM3U()) {
|
||||
const m3uContent = generateM3U(
|
||||
metadata || { title: folderName },
|
||||
tracks,
|
||||
useRelativePaths,
|
||||
null,
|
||||
'flac',
|
||||
trackPaths
|
||||
);
|
||||
yield {
|
||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
||||
lastModified: new Date(),
|
||||
input: m3uContent,
|
||||
};
|
||||
}
|
||||
|
||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
||||
|
||||
try {
|
||||
const { blob, extension } = await downloadTrackBlob(
|
||||
track,
|
||||
quality,
|
||||
api,
|
||||
null,
|
||||
signal,
|
||||
(p) => {
|
||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle, p);
|
||||
},
|
||||
coverBlob
|
||||
);
|
||||
const filename = buildTrackFilename(track, quality, extension);
|
||||
const discNumber = discLayout.resolveDiscNumber(i);
|
||||
yield {
|
||||
name: buildZipTrackPath(folderName, filename, separateByDisc, discNumber),
|
||||
lastModified: new Date(),
|
||||
input: blob,
|
||||
};
|
||||
|
||||
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
||||
try {
|
||||
const lyricsData = await lyricsManager.fetchLyrics(track.id, track);
|
||||
if (lyricsData) {
|
||||
const lrcContent = lyricsManager.generateLRCContent(lyricsData, track);
|
||||
if (lrcContent) {
|
||||
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
|
||||
yield {
|
||||
name: buildZipTrackPath(folderName, lrcFilename, separateByDisc, discNumber),
|
||||
lastModified: new Date(),
|
||||
input: lrcContent,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') throw err;
|
||||
console.error(`Failed to download track ${trackTitle}:`, err);
|
||||
}
|
||||
if (playlistSettings.shouldGenerateM3U8()) {
|
||||
const m3u8Content = generateM3U8(
|
||||
metadata || { title: folderName },
|
||||
tracks,
|
||||
useRelativePaths,
|
||||
null,
|
||||
'flac',
|
||||
trackPaths
|
||||
);
|
||||
yield {
|
||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
||||
lastModified: new Date(),
|
||||
input: m3u8Content,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -810,43 +816,67 @@ async function bulkDownloadToZipNeutralino(
|
|||
yield { name: `${folderName}/cover.jpg`, lastModified: new Date(), input: coverBlob };
|
||||
}
|
||||
|
||||
// Generate playlist files first
|
||||
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()) {
|
||||
const m3uContent = generateM3U(
|
||||
metadata || { title: folderName },
|
||||
tracks,
|
||||
useRelativePaths,
|
||||
playlistPathResolver,
|
||||
playlistAudioExtension
|
||||
);
|
||||
yield {
|
||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
||||
lastModified: new Date(),
|
||||
input: m3uContent,
|
||||
};
|
||||
}
|
||||
// Download tracks, yielding each immediately and collecting actual paths for playlist generation
|
||||
const trackPaths = [];
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
if (signal.aborted) break;
|
||||
const track = tracks[i];
|
||||
const trackTitle = getTrackTitle(track);
|
||||
|
||||
if (playlistSettings.shouldGenerateM3U8()) {
|
||||
const m3u8Content = generateM3U8(
|
||||
metadata || { title: folderName },
|
||||
tracks,
|
||||
useRelativePaths,
|
||||
playlistPathResolver,
|
||||
playlistAudioExtension
|
||||
);
|
||||
yield {
|
||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
||||
lastModified: new Date(),
|
||||
input: m3u8Content,
|
||||
};
|
||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
||||
|
||||
try {
|
||||
const { blob, extension } = await downloadTrackBlob(
|
||||
track,
|
||||
quality,
|
||||
api,
|
||||
null,
|
||||
signal,
|
||||
(p) => {
|
||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle, p);
|
||||
},
|
||||
coverBlob
|
||||
);
|
||||
const filename = buildTrackFilename(track, quality, extension);
|
||||
const discNumber = discLayout.resolveDiscNumber(i);
|
||||
const discPath = separateByDisc ? `${getDiscFolderName(discNumber)}/${filename}` : filename;
|
||||
|
||||
console.log(`[Playlist] Track ${i + 1}: ${discPath}`);
|
||||
trackPaths.push(discPath);
|
||||
|
||||
yield {
|
||||
name: buildZipTrackPath(folderName, filename, separateByDisc, discNumber),
|
||||
lastModified: new Date(),
|
||||
input: blob,
|
||||
};
|
||||
|
||||
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
||||
try {
|
||||
const lyricsData = await lyricsManager.fetchLyrics(track.id, track);
|
||||
if (lyricsData) {
|
||||
const lrcContent = lyricsManager.generateLRCContent(lyricsData, track);
|
||||
if (lrcContent) {
|
||||
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
|
||||
yield {
|
||||
name: buildZipTrackPath(folderName, lrcFilename, separateByDisc, discNumber),
|
||||
lastModified: new Date(),
|
||||
input: lrcContent,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') throw err;
|
||||
console.error(`Failed to download track ${trackTitle}:`, err);
|
||||
trackPaths.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (playlistSettings.shouldGenerateNFO()) {
|
||||
|
|
@ -878,56 +908,37 @@ async function bulkDownloadToZipNeutralino(
|
|||
};
|
||||
}
|
||||
|
||||
// Download tracks
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
if (signal.aborted) break;
|
||||
const track = tracks[i];
|
||||
const trackTitle = getTrackTitle(track);
|
||||
// Generate m3u/m3u8 last, using actual track paths collected during download
|
||||
if (playlistSettings.shouldGenerateM3U()) {
|
||||
const m3uContent = generateM3U(
|
||||
metadata || { title: folderName },
|
||||
tracks,
|
||||
useRelativePaths,
|
||||
null,
|
||||
'flac',
|
||||
trackPaths
|
||||
);
|
||||
yield {
|
||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
||||
lastModified: new Date(),
|
||||
input: m3uContent,
|
||||
};
|
||||
}
|
||||
|
||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
||||
|
||||
try {
|
||||
const { blob, extension } = await downloadTrackBlob(
|
||||
track,
|
||||
quality,
|
||||
api,
|
||||
null,
|
||||
signal,
|
||||
(p) => {
|
||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle, p);
|
||||
},
|
||||
coverBlob
|
||||
);
|
||||
const filename = buildTrackFilename(track, quality, extension);
|
||||
const discNumber = discLayout.resolveDiscNumber(i);
|
||||
yield {
|
||||
name: buildZipTrackPath(folderName, filename, separateByDisc, discNumber),
|
||||
lastModified: new Date(),
|
||||
input: blob,
|
||||
};
|
||||
|
||||
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
||||
try {
|
||||
const lyricsData = await lyricsManager.fetchLyrics(track.id, track);
|
||||
if (lyricsData) {
|
||||
const lrcContent = lyricsManager.generateLRCContent(lyricsData, track);
|
||||
if (lrcContent) {
|
||||
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
|
||||
yield {
|
||||
name: buildZipTrackPath(folderName, lrcFilename, separateByDisc, discNumber),
|
||||
lastModified: new Date(),
|
||||
input: lrcContent,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') throw err;
|
||||
console.error(`Failed to download track ${trackTitle}:`, err);
|
||||
}
|
||||
if (playlistSettings.shouldGenerateM3U8()) {
|
||||
const m3u8Content = generateM3U8(
|
||||
metadata || { title: folderName },
|
||||
tracks,
|
||||
useRelativePaths,
|
||||
null,
|
||||
'flac',
|
||||
trackPaths
|
||||
);
|
||||
yield {
|
||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
||||
lastModified: new Date(),
|
||||
input: m3u8Content,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1155,72 +1166,11 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
|
|||
|
||||
// Generate playlist files for each album
|
||||
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()) {
|
||||
const m3uContent = generateM3U(
|
||||
fullAlbum,
|
||||
tracks,
|
||||
useRelativePaths,
|
||||
playlistPathResolver,
|
||||
playlistAudioExtension
|
||||
);
|
||||
yield {
|
||||
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.m3u`,
|
||||
lastModified: new Date(),
|
||||
input: m3uContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (playlistSettings.shouldGenerateM3U8()) {
|
||||
const m3u8Content = generateM3U8(
|
||||
fullAlbum,
|
||||
tracks,
|
||||
useRelativePaths,
|
||||
playlistPathResolver,
|
||||
playlistAudioExtension
|
||||
);
|
||||
yield {
|
||||
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.m3u8`,
|
||||
lastModified: new Date(),
|
||||
input: m3u8Content,
|
||||
};
|
||||
}
|
||||
|
||||
if (playlistSettings.shouldGenerateNFO()) {
|
||||
const nfoContent = generateNFO(fullAlbum, tracks, 'album');
|
||||
yield {
|
||||
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.nfo`,
|
||||
lastModified: new Date(),
|
||||
input: nfoContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (playlistSettings.shouldGenerateJSON()) {
|
||||
const jsonContent = generateJSON(fullAlbum, tracks, 'album');
|
||||
yield {
|
||||
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.json`,
|
||||
lastModified: new Date(),
|
||||
input: jsonContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (playlistSettings.shouldGenerateCUE()) {
|
||||
const audioFilename = `${sanitizeForFilename(fullAlbum.title)}.flac`;
|
||||
const cueContent = generateCUE(fullAlbum, tracks, audioFilename);
|
||||
yield {
|
||||
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.cue`,
|
||||
lastModified: new Date(),
|
||||
input: cueContent,
|
||||
};
|
||||
}
|
||||
|
||||
// Download tracks, yielding each immediately and collecting actual paths for playlist generation
|
||||
const trackPaths = [];
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
const track = tracks[i];
|
||||
if (signal.aborted) break;
|
||||
|
|
@ -1236,6 +1186,11 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
|
|||
);
|
||||
const filename = buildTrackFilename(track, quality, extension);
|
||||
const discNumber = discLayout.resolveDiscNumber(i);
|
||||
const discPath = separateByDisc ? `${getDiscFolderName(discNumber)}/${filename}` : filename;
|
||||
|
||||
console.log(`[Playlist] Track ${i + 1}: ${discPath}`);
|
||||
trackPaths.push(discPath);
|
||||
|
||||
yield {
|
||||
name: buildZipTrackPath(fullFolderPath, filename, separateByDisc, discNumber),
|
||||
lastModified: new Date(),
|
||||
|
|
@ -1268,8 +1223,56 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
|
|||
} catch (err) {
|
||||
if (err.name === 'AbortError') throw err;
|
||||
console.error(`Failed to download track ${track.title}:`, err);
|
||||
trackPaths.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (playlistSettings.shouldGenerateNFO()) {
|
||||
const nfoContent = generateNFO(fullAlbum, tracks, 'album');
|
||||
yield {
|
||||
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.nfo`,
|
||||
lastModified: new Date(),
|
||||
input: nfoContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (playlistSettings.shouldGenerateJSON()) {
|
||||
const jsonContent = generateJSON(fullAlbum, tracks, 'album');
|
||||
yield {
|
||||
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.json`,
|
||||
lastModified: new Date(),
|
||||
input: jsonContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (playlistSettings.shouldGenerateCUE()) {
|
||||
const audioFilename = `${sanitizeForFilename(fullAlbum.title)}.flac`;
|
||||
const cueContent = generateCUE(fullAlbum, tracks, audioFilename);
|
||||
yield {
|
||||
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.cue`,
|
||||
lastModified: new Date(),
|
||||
input: cueContent,
|
||||
};
|
||||
}
|
||||
|
||||
// Generate m3u/m3u8 last, using actual track paths collected during download
|
||||
if (playlistSettings.shouldGenerateM3U()) {
|
||||
const m3uContent = generateM3U(fullAlbum, tracks, useRelativePaths, null, 'flac', trackPaths);
|
||||
yield {
|
||||
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.m3u`,
|
||||
lastModified: new Date(),
|
||||
input: m3uContent,
|
||||
};
|
||||
}
|
||||
|
||||
if (playlistSettings.shouldGenerateM3U8()) {
|
||||
const m3u8Content = generateM3U8(fullAlbum, tracks, useRelativePaths, null, 'flac', trackPaths);
|
||||
yield {
|
||||
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.m3u8`,
|
||||
lastModified: new Date(),
|
||||
input: m3u8Content,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') throw error;
|
||||
console.error(`Failed to download album ${album.title}:`, error);
|
||||
|
|
|
|||
|
|
@ -4,12 +4,20 @@ import { sanitizeForFilename } from './utils.js';
|
|||
* Generates M3U playlist content
|
||||
* @param {Object} playlist - Playlist metadata (title, artist, etc.)
|
||||
* @param {Array} tracks - Array of track objects
|
||||
* @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
|
||||
* @param {boolean} _useRelativePaths - Unused; kept for API compatibility
|
||||
* @param {Function|null} pathResolver - Optional resolver for per-track relative path (used when trackPaths is null)
|
||||
* @param {string} audioExtension - Audio file extension for generated paths (used when trackPaths is null)
|
||||
* @param {Array|null} trackPaths - Actual per-track resolved paths; when provided, overrides pathResolver/audioExtension
|
||||
* @returns {string} M3U content
|
||||
*/
|
||||
export function generateM3U(playlist, tracks, useRelativePaths = true, pathResolver = null, audioExtension = 'flac') {
|
||||
export function generateM3U(
|
||||
playlist,
|
||||
tracks,
|
||||
_useRelativePaths = true,
|
||||
pathResolver = null,
|
||||
audioExtension = 'flac',
|
||||
trackPaths = null
|
||||
) {
|
||||
let content = '#EXTM3U\n';
|
||||
|
||||
if (playlist.title) {
|
||||
|
|
@ -17,13 +25,16 @@ export function generateM3U(playlist, tracks, useRelativePaths = true, pathResol
|
|||
}
|
||||
|
||||
if (playlist.artist) {
|
||||
content += `#ARTIST:${playlist.artist}\n`;
|
||||
content += `#ARTIST:${playlist.artist?.name || playlist.artist}\n`;
|
||||
}
|
||||
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
content += `#DATE:${date}\n\n`;
|
||||
|
||||
tracks.forEach((track, index) => {
|
||||
const resolvedPath = trackPaths ? trackPaths[index] : null;
|
||||
if (trackPaths && !resolvedPath) return;
|
||||
|
||||
const duration = Math.round(track.duration || 0);
|
||||
const artists = getTrackArtists(track);
|
||||
const title = track.title || 'Unknown Title';
|
||||
|
|
@ -31,9 +42,12 @@ export function generateM3U(playlist, tracks, useRelativePaths = true, pathResol
|
|||
|
||||
content += `#EXTINF:${duration},${displayName}\n`;
|
||||
|
||||
const filename = getTrackFilename(track, index + 1, audioExtension);
|
||||
const relativePath = typeof pathResolver === 'function' ? pathResolver(track, filename, index) : filename;
|
||||
const path = useRelativePaths ? relativePath : relativePath;
|
||||
const path =
|
||||
resolvedPath ??
|
||||
(() => {
|
||||
const filename = getTrackFilename(track, index + 1, audioExtension);
|
||||
return typeof pathResolver === 'function' ? pathResolver(track, filename, index) : filename;
|
||||
})();
|
||||
|
||||
content += `${path}\n\n`;
|
||||
});
|
||||
|
|
@ -45,12 +59,20 @@ export function generateM3U(playlist, tracks, useRelativePaths = true, pathResol
|
|||
* Generates M3U8 playlist content (UTF-8 extended)
|
||||
* @param {Object} playlist - Playlist metadata
|
||||
* @param {Array} tracks - Array of track objects
|
||||
* @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
|
||||
* @param {boolean} _useRelativePaths - Unused; kept for API compatibility
|
||||
* @param {Function|null} pathResolver - Optional resolver for per-track relative path (used when trackPaths is null)
|
||||
* @param {string} audioExtension - Audio file extension for generated paths (used when trackPaths is null)
|
||||
* @param {Array|null} trackPaths - Actual per-track resolved paths; when provided, overrides pathResolver/audioExtension
|
||||
* @returns {string} M3U8 content
|
||||
*/
|
||||
export function generateM3U8(playlist, tracks, useRelativePaths = true, pathResolver = null, audioExtension = 'flac') {
|
||||
export function generateM3U8(
|
||||
playlist,
|
||||
tracks,
|
||||
_useRelativePaths = true,
|
||||
pathResolver = null,
|
||||
audioExtension = 'flac',
|
||||
trackPaths = null
|
||||
) {
|
||||
let content = '#EXTM3U\n';
|
||||
content += '#EXT-X-VERSION:3\n';
|
||||
content += '#EXT-X-PLAYLIST-TYPE:VOD\n';
|
||||
|
|
@ -63,13 +85,16 @@ export function generateM3U8(playlist, tracks, useRelativePaths = true, pathReso
|
|||
}
|
||||
|
||||
if (playlist.artist) {
|
||||
content += `#ARTIST:${playlist.artist}\n`;
|
||||
content += `#ARTIST:${playlist.artist?.name || playlist.artist}\n`;
|
||||
}
|
||||
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
content += `#DATE:${date}\n\n`;
|
||||
|
||||
tracks.forEach((track, index) => {
|
||||
const resolvedPath = trackPaths ? trackPaths[index] : null;
|
||||
if (trackPaths && !resolvedPath) return;
|
||||
|
||||
const duration = Math.round(track.duration || 0);
|
||||
const artists = getTrackArtists(track);
|
||||
const title = track.title || 'Unknown Title';
|
||||
|
|
@ -77,9 +102,12 @@ export function generateM3U8(playlist, tracks, useRelativePaths = true, pathReso
|
|||
|
||||
content += `#EXTINF:${duration}.000,${displayName}\n`;
|
||||
|
||||
const filename = getTrackFilename(track, index + 1, audioExtension);
|
||||
const relativePath = typeof pathResolver === 'function' ? pathResolver(track, filename, index) : filename;
|
||||
const path = useRelativePaths ? relativePath : relativePath;
|
||||
const path =
|
||||
resolvedPath ??
|
||||
(() => {
|
||||
const filename = getTrackFilename(track, index + 1, audioExtension);
|
||||
return typeof pathResolver === 'function' ? pathResolver(track, filename, index) : filename;
|
||||
})();
|
||||
|
||||
content += `${path}\n\n`;
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue