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}`;
|
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');
|
||||||
|
|
@ -505,43 +501,67 @@ async function bulkDownloadToZipStream(
|
||||||
yield { name: `${folderName}/cover.jpg`, lastModified: new Date(), input: coverBlob };
|
yield { name: `${folderName}/cover.jpg`, lastModified: new Date(), input: coverBlob };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate playlist files first
|
|
||||||
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
|
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
|
||||||
const playlistAudioExtension = getPlaylistAudioExtension(quality);
|
|
||||||
const discLayout = await createDiscLayoutContext(tracks, api);
|
const discLayout = await createDiscLayoutContext(tracks, api);
|
||||||
const separateByDisc = discLayout.separateByDisc;
|
const separateByDisc = discLayout.separateByDisc;
|
||||||
const playlistPathResolver = separateByDisc
|
|
||||||
? (_track, filename, index) => `${getDiscFolderName(discLayout.resolveDiscNumber(index))}/${filename}`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (playlistSettings.shouldGenerateM3U()) {
|
// Download tracks, yielding each immediately and collecting actual paths for playlist generation
|
||||||
const m3uContent = generateM3U(
|
const trackPaths = [];
|
||||||
metadata || { title: folderName },
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
tracks,
|
if (signal.aborted) break;
|
||||||
useRelativePaths,
|
const track = tracks[i];
|
||||||
playlistPathResolver,
|
const trackTitle = getTrackTitle(track);
|
||||||
playlistAudioExtension
|
|
||||||
);
|
|
||||||
yield {
|
|
||||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
|
||||||
lastModified: new Date(),
|
|
||||||
input: m3uContent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playlistSettings.shouldGenerateM3U8()) {
|
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
||||||
const m3u8Content = generateM3U8(
|
|
||||||
metadata || { title: folderName },
|
try {
|
||||||
tracks,
|
const { blob, extension } = await downloadTrackBlob(
|
||||||
useRelativePaths,
|
track,
|
||||||
playlistPathResolver,
|
quality,
|
||||||
playlistAudioExtension
|
api,
|
||||||
);
|
null,
|
||||||
yield {
|
signal,
|
||||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
(p) => {
|
||||||
lastModified: new Date(),
|
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle, p);
|
||||||
input: m3u8Content,
|
},
|
||||||
};
|
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()) {
|
if (playlistSettings.shouldGenerateNFO()) {
|
||||||
|
|
@ -573,56 +593,37 @@ async function bulkDownloadToZipStream(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download tracks
|
// Generate m3u/m3u8 last, using actual track paths collected during download
|
||||||
for (let i = 0; i < tracks.length; i++) {
|
if (playlistSettings.shouldGenerateM3U()) {
|
||||||
if (signal.aborted) break;
|
const m3uContent = generateM3U(
|
||||||
const track = tracks[i];
|
metadata || { title: folderName },
|
||||||
const trackTitle = getTrackTitle(track);
|
tracks,
|
||||||
|
useRelativePaths,
|
||||||
|
null,
|
||||||
|
'flac',
|
||||||
|
trackPaths
|
||||||
|
);
|
||||||
|
yield {
|
||||||
|
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
||||||
|
lastModified: new Date(),
|
||||||
|
input: m3uContent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
if (playlistSettings.shouldGenerateM3U8()) {
|
||||||
|
const m3u8Content = generateM3U8(
|
||||||
try {
|
metadata || { title: folderName },
|
||||||
const { blob, extension } = await downloadTrackBlob(
|
tracks,
|
||||||
track,
|
useRelativePaths,
|
||||||
quality,
|
null,
|
||||||
api,
|
'flac',
|
||||||
null,
|
trackPaths
|
||||||
signal,
|
);
|
||||||
(p) => {
|
yield {
|
||||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle, p);
|
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
||||||
},
|
lastModified: new Date(),
|
||||||
coverBlob
|
input: m3u8Content,
|
||||||
);
|
};
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -657,43 +658,67 @@ async function bulkDownloadToZipBlob(
|
||||||
yield { name: `${folderName}/cover.jpg`, lastModified: new Date(), input: coverBlob };
|
yield { name: `${folderName}/cover.jpg`, lastModified: new Date(), input: coverBlob };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate playlist files first
|
|
||||||
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
|
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
|
||||||
const playlistAudioExtension = getPlaylistAudioExtension(quality);
|
|
||||||
const discLayout = await createDiscLayoutContext(tracks, api);
|
const discLayout = await createDiscLayoutContext(tracks, api);
|
||||||
const separateByDisc = discLayout.separateByDisc;
|
const separateByDisc = discLayout.separateByDisc;
|
||||||
const playlistPathResolver = separateByDisc
|
|
||||||
? (_track, filename, index) => `${getDiscFolderName(discLayout.resolveDiscNumber(index))}/${filename}`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (playlistSettings.shouldGenerateM3U()) {
|
// Download tracks, yielding each immediately and collecting actual paths for playlist generation
|
||||||
const m3uContent = generateM3U(
|
const trackPaths = [];
|
||||||
metadata || { title: folderName },
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
tracks,
|
if (signal.aborted) break;
|
||||||
useRelativePaths,
|
const track = tracks[i];
|
||||||
playlistPathResolver,
|
const trackTitle = getTrackTitle(track);
|
||||||
playlistAudioExtension
|
|
||||||
);
|
|
||||||
yield {
|
|
||||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
|
||||||
lastModified: new Date(),
|
|
||||||
input: m3uContent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playlistSettings.shouldGenerateM3U8()) {
|
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
||||||
const m3u8Content = generateM3U8(
|
|
||||||
metadata || { title: folderName },
|
try {
|
||||||
tracks,
|
const { blob, extension } = await downloadTrackBlob(
|
||||||
useRelativePaths,
|
track,
|
||||||
playlistPathResolver,
|
quality,
|
||||||
playlistAudioExtension
|
api,
|
||||||
);
|
null,
|
||||||
yield {
|
signal,
|
||||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
(p) => {
|
||||||
lastModified: new Date(),
|
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle, p);
|
||||||
input: m3u8Content,
|
},
|
||||||
};
|
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()) {
|
if (playlistSettings.shouldGenerateNFO()) {
|
||||||
|
|
@ -725,56 +750,37 @@ async function bulkDownloadToZipBlob(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download tracks
|
// Generate m3u/m3u8 last, using actual track paths collected during download
|
||||||
for (let i = 0; i < tracks.length; i++) {
|
if (playlistSettings.shouldGenerateM3U()) {
|
||||||
if (signal.aborted) break;
|
const m3uContent = generateM3U(
|
||||||
const track = tracks[i];
|
metadata || { title: folderName },
|
||||||
const trackTitle = getTrackTitle(track);
|
tracks,
|
||||||
|
useRelativePaths,
|
||||||
|
null,
|
||||||
|
'flac',
|
||||||
|
trackPaths
|
||||||
|
);
|
||||||
|
yield {
|
||||||
|
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
||||||
|
lastModified: new Date(),
|
||||||
|
input: m3uContent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
if (playlistSettings.shouldGenerateM3U8()) {
|
||||||
|
const m3u8Content = generateM3U8(
|
||||||
try {
|
metadata || { title: folderName },
|
||||||
const { blob, extension } = await downloadTrackBlob(
|
tracks,
|
||||||
track,
|
useRelativePaths,
|
||||||
quality,
|
null,
|
||||||
api,
|
'flac',
|
||||||
null,
|
trackPaths
|
||||||
signal,
|
);
|
||||||
(p) => {
|
yield {
|
||||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle, p);
|
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
||||||
},
|
lastModified: new Date(),
|
||||||
coverBlob
|
input: m3u8Content,
|
||||||
);
|
};
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -810,43 +816,67 @@ async function bulkDownloadToZipNeutralino(
|
||||||
yield { name: `${folderName}/cover.jpg`, lastModified: new Date(), input: coverBlob };
|
yield { name: `${folderName}/cover.jpg`, lastModified: new Date(), input: coverBlob };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate playlist files first
|
|
||||||
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
|
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
|
||||||
const playlistAudioExtension = getPlaylistAudioExtension(quality);
|
|
||||||
const discLayout = await createDiscLayoutContext(tracks, api);
|
const discLayout = await createDiscLayoutContext(tracks, api);
|
||||||
const separateByDisc = discLayout.separateByDisc;
|
const separateByDisc = discLayout.separateByDisc;
|
||||||
const playlistPathResolver = separateByDisc
|
|
||||||
? (_track, filename, index) => `${getDiscFolderName(discLayout.resolveDiscNumber(index))}/${filename}`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (playlistSettings.shouldGenerateM3U()) {
|
// Download tracks, yielding each immediately and collecting actual paths for playlist generation
|
||||||
const m3uContent = generateM3U(
|
const trackPaths = [];
|
||||||
metadata || { title: folderName },
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
tracks,
|
if (signal.aborted) break;
|
||||||
useRelativePaths,
|
const track = tracks[i];
|
||||||
playlistPathResolver,
|
const trackTitle = getTrackTitle(track);
|
||||||
playlistAudioExtension
|
|
||||||
);
|
|
||||||
yield {
|
|
||||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
|
||||||
lastModified: new Date(),
|
|
||||||
input: m3uContent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playlistSettings.shouldGenerateM3U8()) {
|
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
||||||
const m3u8Content = generateM3U8(
|
|
||||||
metadata || { title: folderName },
|
try {
|
||||||
tracks,
|
const { blob, extension } = await downloadTrackBlob(
|
||||||
useRelativePaths,
|
track,
|
||||||
playlistPathResolver,
|
quality,
|
||||||
playlistAudioExtension
|
api,
|
||||||
);
|
null,
|
||||||
yield {
|
signal,
|
||||||
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
(p) => {
|
||||||
lastModified: new Date(),
|
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle, p);
|
||||||
input: m3u8Content,
|
},
|
||||||
};
|
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()) {
|
if (playlistSettings.shouldGenerateNFO()) {
|
||||||
|
|
@ -878,56 +908,37 @@ async function bulkDownloadToZipNeutralino(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download tracks
|
// Generate m3u/m3u8 last, using actual track paths collected during download
|
||||||
for (let i = 0; i < tracks.length; i++) {
|
if (playlistSettings.shouldGenerateM3U()) {
|
||||||
if (signal.aborted) break;
|
const m3uContent = generateM3U(
|
||||||
const track = tracks[i];
|
metadata || { title: folderName },
|
||||||
const trackTitle = getTrackTitle(track);
|
tracks,
|
||||||
|
useRelativePaths,
|
||||||
|
null,
|
||||||
|
'flac',
|
||||||
|
trackPaths
|
||||||
|
);
|
||||||
|
yield {
|
||||||
|
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
|
||||||
|
lastModified: new Date(),
|
||||||
|
input: m3uContent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
if (playlistSettings.shouldGenerateM3U8()) {
|
||||||
|
const m3u8Content = generateM3U8(
|
||||||
try {
|
metadata || { title: folderName },
|
||||||
const { blob, extension } = await downloadTrackBlob(
|
tracks,
|
||||||
track,
|
useRelativePaths,
|
||||||
quality,
|
null,
|
||||||
api,
|
'flac',
|
||||||
null,
|
trackPaths
|
||||||
signal,
|
);
|
||||||
(p) => {
|
yield {
|
||||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle, p);
|
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
|
||||||
},
|
lastModified: new Date(),
|
||||||
coverBlob
|
input: m3u8Content,
|
||||||
);
|
};
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1155,72 +1166,11 @@ 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 discLayout = await createDiscLayoutContext(tracks, api);
|
||||||
const separateByDisc = discLayout.separateByDisc;
|
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++) {
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
const track = tracks[i];
|
const track = tracks[i];
|
||||||
if (signal.aborted) break;
|
if (signal.aborted) break;
|
||||||
|
|
@ -1236,6 +1186,11 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
|
||||||
);
|
);
|
||||||
const filename = buildTrackFilename(track, quality, extension);
|
const filename = buildTrackFilename(track, quality, extension);
|
||||||
const discNumber = discLayout.resolveDiscNumber(i);
|
const discNumber = discLayout.resolveDiscNumber(i);
|
||||||
|
const discPath = separateByDisc ? `${getDiscFolderName(discNumber)}/${filename}` : filename;
|
||||||
|
|
||||||
|
console.log(`[Playlist] Track ${i + 1}: ${discPath}`);
|
||||||
|
trackPaths.push(discPath);
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
name: buildZipTrackPath(fullFolderPath, filename, separateByDisc, discNumber),
|
name: buildZipTrackPath(fullFolderPath, filename, separateByDisc, discNumber),
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
|
|
@ -1268,8 +1223,56 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.name === 'AbortError') throw err;
|
if (err.name === 'AbortError') throw err;
|
||||||
console.error(`Failed to download track ${track.title}:`, 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) {
|
} catch (error) {
|
||||||
if (error.name === 'AbortError') throw error;
|
if (error.name === 'AbortError') throw error;
|
||||||
console.error(`Failed to download album ${album.title}:`, error);
|
console.error(`Failed to download album ${album.title}:`, error);
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,20 @@ import { sanitizeForFilename } from './utils.js';
|
||||||
* Generates M3U playlist content
|
* Generates M3U playlist content
|
||||||
* @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 - Unused; kept for API compatibility
|
||||||
* @param {Function|null} pathResolver - Optional resolver for per-track relative path
|
* @param {Function|null} pathResolver - Optional resolver for per-track relative path (used when trackPaths is null)
|
||||||
* @param {string} audioExtension - Audio file extension used in generated paths
|
* @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
|
* @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';
|
let content = '#EXTM3U\n';
|
||||||
|
|
||||||
if (playlist.title) {
|
if (playlist.title) {
|
||||||
|
|
@ -17,13 +25,16 @@ export function generateM3U(playlist, tracks, useRelativePaths = true, pathResol
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playlist.artist) {
|
if (playlist.artist) {
|
||||||
content += `#ARTIST:${playlist.artist}\n`;
|
content += `#ARTIST:${playlist.artist?.name || playlist.artist}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = new Date().toISOString().split('T')[0];
|
const date = new Date().toISOString().split('T')[0];
|
||||||
content += `#DATE:${date}\n\n`;
|
content += `#DATE:${date}\n\n`;
|
||||||
|
|
||||||
tracks.forEach((track, index) => {
|
tracks.forEach((track, index) => {
|
||||||
|
const resolvedPath = trackPaths ? trackPaths[index] : null;
|
||||||
|
if (trackPaths && !resolvedPath) return;
|
||||||
|
|
||||||
const duration = Math.round(track.duration || 0);
|
const duration = Math.round(track.duration || 0);
|
||||||
const artists = getTrackArtists(track);
|
const artists = getTrackArtists(track);
|
||||||
const title = track.title || 'Unknown Title';
|
const title = track.title || 'Unknown Title';
|
||||||
|
|
@ -31,9 +42,12 @@ export function generateM3U(playlist, tracks, useRelativePaths = true, pathResol
|
||||||
|
|
||||||
content += `#EXTINF:${duration},${displayName}\n`;
|
content += `#EXTINF:${duration},${displayName}\n`;
|
||||||
|
|
||||||
const filename = getTrackFilename(track, index + 1, audioExtension);
|
const path =
|
||||||
const relativePath = typeof pathResolver === 'function' ? pathResolver(track, filename, index) : filename;
|
resolvedPath ??
|
||||||
const path = useRelativePaths ? relativePath : relativePath;
|
(() => {
|
||||||
|
const filename = getTrackFilename(track, index + 1, audioExtension);
|
||||||
|
return typeof pathResolver === 'function' ? pathResolver(track, filename, index) : filename;
|
||||||
|
})();
|
||||||
|
|
||||||
content += `${path}\n\n`;
|
content += `${path}\n\n`;
|
||||||
});
|
});
|
||||||
|
|
@ -45,12 +59,20 @@ export function generateM3U(playlist, tracks, useRelativePaths = true, pathResol
|
||||||
* Generates M3U8 playlist content (UTF-8 extended)
|
* Generates M3U8 playlist content (UTF-8 extended)
|
||||||
* @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 - Unused; kept for API compatibility
|
||||||
* @param {Function|null} pathResolver - Optional resolver for per-track relative path
|
* @param {Function|null} pathResolver - Optional resolver for per-track relative path (used when trackPaths is null)
|
||||||
* @param {string} audioExtension - Audio file extension used in generated paths
|
* @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
|
* @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';
|
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';
|
||||||
|
|
@ -63,13 +85,16 @@ export function generateM3U8(playlist, tracks, useRelativePaths = true, pathReso
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playlist.artist) {
|
if (playlist.artist) {
|
||||||
content += `#ARTIST:${playlist.artist}\n`;
|
content += `#ARTIST:${playlist.artist?.name || playlist.artist}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = new Date().toISOString().split('T')[0];
|
const date = new Date().toISOString().split('T')[0];
|
||||||
content += `#DATE:${date}\n\n`;
|
content += `#DATE:${date}\n\n`;
|
||||||
|
|
||||||
tracks.forEach((track, index) => {
|
tracks.forEach((track, index) => {
|
||||||
|
const resolvedPath = trackPaths ? trackPaths[index] : null;
|
||||||
|
if (trackPaths && !resolvedPath) return;
|
||||||
|
|
||||||
const duration = Math.round(track.duration || 0);
|
const duration = Math.round(track.duration || 0);
|
||||||
const artists = getTrackArtists(track);
|
const artists = getTrackArtists(track);
|
||||||
const title = track.title || 'Unknown Title';
|
const title = track.title || 'Unknown Title';
|
||||||
|
|
@ -77,9 +102,12 @@ export function generateM3U8(playlist, tracks, useRelativePaths = true, pathReso
|
||||||
|
|
||||||
content += `#EXTINF:${duration}.000,${displayName}\n`;
|
content += `#EXTINF:${duration}.000,${displayName}\n`;
|
||||||
|
|
||||||
const filename = getTrackFilename(track, index + 1, audioExtension);
|
const path =
|
||||||
const relativePath = typeof pathResolver === 'function' ? pathResolver(track, filename, index) : filename;
|
resolvedPath ??
|
||||||
const path = useRelativePaths ? relativePath : relativePath;
|
(() => {
|
||||||
|
const filename = getTrackFilename(track, index + 1, audioExtension);
|
||||||
|
return typeof pathResolver === 'function' ? pathResolver(track, filename, index) : filename;
|
||||||
|
})();
|
||||||
|
|
||||||
content += `${path}\n\n`;
|
content += `${path}\n\n`;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue