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 - 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', trackPaths = null ) { let content = '#EXTM3U\n'; if (playlist.title) { content += `#PLAYLIST:${sanitizeForFilename(playlist.title)}\n`; } if (playlist.artist) { 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'; const displayName = `${artists} - ${title}`; content += `#EXTINF:${duration},${displayName}\n`; const path = resolvedPath ?? (() => { const filename = getTrackFilename(track, index + 1, audioExtension); return typeof pathResolver === 'function' ? pathResolver(track, filename, index) : filename; })(); content += `${path}\n\n`; }); return content; } /** * Generates M3U8 playlist content (UTF-8 extended) * @param {Object} playlist - Playlist metadata * @param {Array} tracks - Array of track objects * @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', trackPaths = null ) { let content = '#EXTM3U\n'; content += '#EXT-X-VERSION:3\n'; content += '#EXT-X-PLAYLIST-TYPE:VOD\n'; const maxDuration = Math.max(...tracks.map((track) => Math.round(track.duration || 0))); content += `#EXT-X-TARGETDURATION:${maxDuration}\n`; if (playlist.title) { content += `#PLAYLIST:${sanitizeForFilename(playlist.title)}\n`; } if (playlist.artist) { 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'; const displayName = `${artists} - ${title}`; content += `#EXTINF:${duration}.000,${displayName}\n`; const path = resolvedPath ?? (() => { const filename = getTrackFilename(track, index + 1, audioExtension); return typeof pathResolver === 'function' ? pathResolver(track, filename, index) : filename; })(); content += `${path}\n\n`; }); content += '#EXT-X-ENDLIST\n'; return content; } /** * Generates CUE sheet content for albums * @param {Object} album - Album metadata * @param {Array} tracks - Array of track objects * @param {string} _audioFilenameBase - Unused; kept for API compatibility * @param {Array|null} trackPaths - Actual per-track resolved paths; when provided, each track gets its own FILE entry * @param {string} audioExtension - Audio file extension for generated paths (used when trackPaths is null) * @returns {string} CUE content */ export function generateCUE(album, tracks, _audioFilenameBase, trackPaths = null, audioExtension = 'flac') { const performer = album.artist?.name || album.artist || 'Unknown Artist'; const title = album.title || 'Unknown Album'; let content = `PERFORMER "${performer}"\n`; content += `TITLE "${title}"\n`; tracks.forEach((track, index) => { const resolvedPath = trackPaths ? trackPaths[index] : null; if (trackPaths && !resolvedPath) return; const trackNumber = String(track.trackNumber || index + 1).padStart(2, '0'); const trackTitle = track.title || 'Unknown Track'; const trackPerformer = track.artist?.name || getTrackArtists(track) || performer; const path = resolvedPath ?? (() => { const filename = getTrackFilename(track, index + 1, audioExtension); return filename; })(); const fileExtension = path.split('.').pop().toUpperCase(); content += `FILE "${path}" ${fileExtension}\n`; content += ` TRACK ${trackNumber} AUDIO\n`; content += ` TITLE "${trackTitle}"\n`; content += ` PERFORMER "${trackPerformer}"\n`; content += ` INDEX 01 00:00:00\n`; }); return content; } /** * Generates NFO file content for Kodi/media center compatibility * @param {Object} playlist - Playlist metadata * @param {Array} tracks - Array of track objects * @param {string} type - 'playlist' or 'album' * @returns {string} NFO XML content */ export function generateNFO(playlist, tracks, type = 'playlist') { const date = new Date().toISOString(); let xml = '\n'; if (type === 'album') { xml += '\n'; xml += ` ${escapeXml(playlist.title || 'Unknown Album')}\n`; xml += ` ${escapeXml(playlist.artist?.name || playlist.artist || 'Unknown Artist')}\n`; if (playlist.releaseDate) { xml += ` ${new Date(playlist.releaseDate).getFullYear()}\n`; } xml += ` ${playlist.id || ''}\n`; xml += ` ${date}\n`; tracks.forEach((track, index) => { xml += ' \n'; xml += ` ${index + 1}\n`; xml += ` ${escapeXml(track.title || '')}\n`; xml += ` ${escapeXml(getTrackArtists(track) || '')}\n`; xml += ` ${Math.round(track.duration || 0)}\n`; if (track.trackNumber) { xml += ` ${track.trackNumber}\n`; } xml += ` ${track.id || ''}\n`; xml += ' \n'; }); xml += '\n'; } else { xml += '\n'; xml += ` ${escapeXml(playlist.title || 'Unknown Playlist')}\n`; xml += ` ${escapeXml(playlist.artist || 'Various Artists')}\n`; xml += ` ${date}\n`; tracks.forEach((track, index) => { xml += ' \n'; xml += ` ${index + 1}\n`; xml += ` ${escapeXml(track.title || '')}\n`; xml += ` ${escapeXml(getTrackArtists(track) || '')}\n`; xml += ` ${escapeXml(track.album?.title || '')}\n`; xml += ` ${Math.round(track.duration || 0)}\n`; xml += ` ${track.id || ''}\n`; xml += ' \n'; }); xml += '\n'; } return xml; } /** * Generates JSON playlist with rich metadata * @param {Object} playlist - Playlist metadata * @param {Array} tracks - Array of track objects * @param {string} type - 'playlist' or 'album' * @returns {string} JSON content */ export function generateJSON(playlist, tracks, type = 'playlist') { const data = { format: 'monochrome-playlist', version: '1.0', type: type, generated: new Date().toISOString(), playlist: { title: playlist.title || 'Unknown', artist: playlist.artist || 'Various Artists', id: playlist.id || playlist.uuid || null, }, tracks: tracks.map((track, index) => ({ position: index + 1, id: track.id || null, title: track.title || 'Unknown Title', artist: getTrackArtists(track) || 'Unknown Artist', album: track.album?.title || null, albumArtist: track.album?.artist?.name || null, trackNumber: track.trackNumber || null, duration: Math.round(track.duration || 0), isrc: track.isrc || null, releaseDate: track.album?.releaseDate || null, })), }; if (type === 'album') { data.playlist.releaseDate = playlist.releaseDate || null; data.playlist.numberOfTracks = tracks.length; data.playlist.cover = playlist.cover || null; } return JSON.stringify(data, null, 2); } /** * Helper function to get track artists string */ function getTrackArtists(track) { if (track.artists && track.artists.length > 0) { return track.artists.map((artist) => artist.name).join(', '); } return track.artist?.name || 'Unknown Artist'; } /** * Helper function to get track filename */ function getTrackFilename(track, trackNumber = 1, audioExtension = 'flac') { const paddedNumber = String(trackNumber).padStart(2, '0'); const artists = getTrackArtists(track); const title = track.title || 'Unknown Title'; const sanitizedArtists = sanitizeForFilename(artists); const sanitizedTitle = sanitizeForFilename(title); return `${paddedNumber} - ${sanitizedArtists} - ${sanitizedTitle}.${audioExtension}`; } /** * Helper function to escape XML special characters */ function escapeXml(text) { if (!text) return ''; return text .toString() .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }