From cc92640908988bce44a4947f5b6d220e39035042 Mon Sep 17 00:00:00 2001 From: Markury Date: Thu, 19 Mar 2026 11:37:27 -0400 Subject: [PATCH] fix: m3u8 relative path generation/resolution --- src/lib/library/m3u8.ts | 31 +++++++++---------- src/lib/library/playlist.ts | 15 +++++---- src/lib/services/deezer/playlistDownloader.ts | 2 +- .../services/spotify/playlistDownloader.ts | 2 +- src/routes/playlists/[name]/+page.svelte | 2 +- 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/lib/library/m3u8.ts b/src/lib/library/m3u8.ts index 55f98ee..9cb5ec7 100644 --- a/src/lib/library/m3u8.ts +++ b/src/lib/library/m3u8.ts @@ -57,35 +57,32 @@ export async function writeM3U8( } /** - * Convert absolute music file path to relative path from playlists folder - * Assumes music folder and playlists folder are siblings: + * Compute a relative path from the playlists folder to a music file. + * Music and playlists folders are expected to be siblings: * /path/to/Music/Artist/Album/Track.flac * /path/to/Playlists/playlist.m3u8 * Becomes: ../Music/Artist/Album/Track.flac * * @param absoluteMusicPath - Absolute path to music file - * @param musicFolderName - Name of music folder (default: 'Music') + * @param playlistsFolder - Absolute path to playlists folder * @returns Relative path from playlists folder */ export function makeRelativePath( absoluteMusicPath: string, - musicFolderName: string = 'Music' + playlistsFolder: string ): string { - // Split path into parts - const parts = absoluteMusicPath.split('/'); + const fileParts = absoluteMusicPath.split('/').filter(Boolean); + const baseParts = playlistsFolder.replace(/\/$/, '').split('/').filter(Boolean); - // Find the music folder index - const musicIndex = parts.findIndex(part => part === musicFolderName); - - if (musicIndex === -1) { - // Fallback: if music folder not found, use the path as-is - console.warn(`[M3U8] Could not find "${musicFolderName}" in path: ${absoluteMusicPath}`); - return absoluteMusicPath; + // Find common prefix length + let common = 0; + while (common < baseParts.length && common < fileParts.length && baseParts[common] === fileParts[common]) { + common++; } - // Take everything from music folder onwards - const relativeParts = parts.slice(musicIndex); + // Go up from playlists folder to common ancestor, then down to the file + const ups = baseParts.length - common; + const remaining = fileParts.slice(common); - // Prepend ../ to go up from playlists folder - return `../${relativeParts.join('/')}`; + return [...Array(ups).fill('..'), ...remaining].join('/'); } diff --git a/src/lib/library/playlist.ts b/src/lib/library/playlist.ts index ceafcbe..c0ce5c0 100644 --- a/src/lib/library/playlist.ts +++ b/src/lib/library/playlist.ts @@ -277,23 +277,22 @@ export async function findPlaylistCoverFallback( */ export async function loadPlaylistTracks( playlistPath: string, - playlistName: string, - baseFolder: string + playlistName: string ): Promise { const parsedTracks = await parsePlaylist(playlistPath); + // Resolve relative paths against the playlist file's directory + const playlistDir = playlistPath.split('/').slice(0, -1).join('/'); + // Load tracks with metadata in parallel const tracks: Track[] = await Promise.all( parsedTracks.map(async (parsedTrack) => { const trackPath = parsedTrack.path; - // Handle relative paths - resolve relative to playlist location or music folder + // Resolve path: absolute paths used as-is, relative paths resolved from playlist dir let fullPath = trackPath.startsWith('/') || trackPath.includes(':\\') - ? trackPath // Absolute path - : `${baseFolder}/${trackPath}`; // Relative path - - // Normalize path to remove .. and . segments for Tauri security - fullPath = normalizePath(fullPath); + ? trackPath + : normalizePath(`${playlistDir}/${trackPath}`); // Try to find the actual file (handles track number mismatches) const actualPath = await findActualFilePath(fullPath); diff --git a/src/lib/services/deezer/playlistDownloader.ts b/src/lib/services/deezer/playlistDownloader.ts index cccafee..1b16674 100644 --- a/src/lib/services/deezer/playlistDownloader.ts +++ b/src/lib/services/deezer/playlistDownloader.ts @@ -93,7 +93,7 @@ export async function downloadDeezerPlaylist( const absolutePath = `${paths.filepath}/${paths.filename}`; // Convert to relative path from playlists folder - const relativePath = makeRelativePath(absolutePath, 'Music'); + const relativePath = makeRelativePath(absolutePath, playlistsFolder); return { duration: track.duration, diff --git a/src/lib/services/spotify/playlistDownloader.ts b/src/lib/services/spotify/playlistDownloader.ts index f0468dd..4b96a82 100644 --- a/src/lib/services/spotify/playlistDownloader.ts +++ b/src/lib/services/spotify/playlistDownloader.ts @@ -221,7 +221,7 @@ export async function downloadSpotifyPlaylist( const m3u8Tracks: M3U8Track[] = successfulTracks.map(({ deezerTrack }) => { const paths = generateTrackPath(deezerTrack, musicFolder, appSettings.deezerFormat, false); const absolutePath = `${paths.filepath}/${paths.filename}`; - const relativePath = makeRelativePath(absolutePath, 'Music'); + const relativePath = makeRelativePath(absolutePath, playlistsFolder); return { duration: deezerTrack.duration, diff --git a/src/routes/playlists/[name]/+page.svelte b/src/routes/playlists/[name]/+page.svelte index 2882ace..78f1cab 100644 --- a/src/routes/playlists/[name]/+page.svelte +++ b/src/routes/playlists/[name]/+page.svelte @@ -51,7 +51,7 @@ // Load tracks and cover art in parallel const [tracksData, coverPath] = await Promise.all([ - loadPlaylistTracks(playlist.path, playlist.name, $settings.musicFolder), + loadPlaylistTracks(playlist.path, playlist.name), findPlaylistArt(playlist.path) ]);