fix(downloads): fix m3u generation artist [object Object] bug and mismatched file extensions

This commit is contained in:
Daniel 2026-03-11 04:50:45 +00:00 committed by GitHub
parent 57a72ac5d7
commit 3ef50cb6ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 357 additions and 326 deletions

View file

@ -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);

View file

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