fix: m3u8 relative path generation/resolution

This commit is contained in:
Markury
2026-03-19 11:37:27 -04:00
parent e5d12c9041
commit cc92640908
5 changed files with 24 additions and 28 deletions

View File

@@ -57,35 +57,32 @@ export async function writeM3U8(
} }
/** /**
* Convert absolute music file path to relative path from playlists folder * Compute a relative path from the playlists folder to a music file.
* Assumes music folder and playlists folder are siblings: * Music and playlists folders are expected to be siblings:
* /path/to/Music/Artist/Album/Track.flac * /path/to/Music/Artist/Album/Track.flac
* /path/to/Playlists/playlist.m3u8 * /path/to/Playlists/playlist.m3u8
* Becomes: ../Music/Artist/Album/Track.flac * Becomes: ../Music/Artist/Album/Track.flac
* *
* @param absoluteMusicPath - Absolute path to music file * @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 * @returns Relative path from playlists folder
*/ */
export function makeRelativePath( export function makeRelativePath(
absoluteMusicPath: string, absoluteMusicPath: string,
musicFolderName: string = 'Music' playlistsFolder: string
): string { ): string {
// Split path into parts const fileParts = absoluteMusicPath.split('/').filter(Boolean);
const parts = absoluteMusicPath.split('/'); const baseParts = playlistsFolder.replace(/\/$/, '').split('/').filter(Boolean);
// Find the music folder index // Find common prefix length
const musicIndex = parts.findIndex(part => part === musicFolderName); let common = 0;
while (common < baseParts.length && common < fileParts.length && baseParts[common] === fileParts[common]) {
if (musicIndex === -1) { common++;
// Fallback: if music folder not found, use the path as-is
console.warn(`[M3U8] Could not find "${musicFolderName}" in path: ${absoluteMusicPath}`);
return absoluteMusicPath;
} }
// Take everything from music folder onwards // Go up from playlists folder to common ancestor, then down to the file
const relativeParts = parts.slice(musicIndex); const ups = baseParts.length - common;
const remaining = fileParts.slice(common);
// Prepend ../ to go up from playlists folder return [...Array(ups).fill('..'), ...remaining].join('/');
return `../${relativeParts.join('/')}`;
} }

View File

@@ -277,23 +277,22 @@ export async function findPlaylistCoverFallback(
*/ */
export async function loadPlaylistTracks( export async function loadPlaylistTracks(
playlistPath: string, playlistPath: string,
playlistName: string, playlistName: string
baseFolder: string
): Promise<PlaylistWithTracks> { ): Promise<PlaylistWithTracks> {
const parsedTracks = await parsePlaylist(playlistPath); 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 // Load tracks with metadata in parallel
const tracks: Track[] = await Promise.all( const tracks: Track[] = await Promise.all(
parsedTracks.map(async (parsedTrack) => { parsedTracks.map(async (parsedTrack) => {
const trackPath = parsedTrack.path; 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(':\\') let fullPath = trackPath.startsWith('/') || trackPath.includes(':\\')
? trackPath // Absolute path ? trackPath
: `${baseFolder}/${trackPath}`; // Relative path : normalizePath(`${playlistDir}/${trackPath}`);
// Normalize path to remove .. and . segments for Tauri security
fullPath = normalizePath(fullPath);
// Try to find the actual file (handles track number mismatches) // Try to find the actual file (handles track number mismatches)
const actualPath = await findActualFilePath(fullPath); const actualPath = await findActualFilePath(fullPath);

View File

@@ -93,7 +93,7 @@ export async function downloadDeezerPlaylist(
const absolutePath = `${paths.filepath}/${paths.filename}`; const absolutePath = `${paths.filepath}/${paths.filename}`;
// Convert to relative path from playlists folder // Convert to relative path from playlists folder
const relativePath = makeRelativePath(absolutePath, 'Music'); const relativePath = makeRelativePath(absolutePath, playlistsFolder);
return { return {
duration: track.duration, duration: track.duration,

View File

@@ -221,7 +221,7 @@ export async function downloadSpotifyPlaylist(
const m3u8Tracks: M3U8Track[] = successfulTracks.map(({ deezerTrack }) => { const m3u8Tracks: M3U8Track[] = successfulTracks.map(({ deezerTrack }) => {
const paths = generateTrackPath(deezerTrack, musicFolder, appSettings.deezerFormat, false); const paths = generateTrackPath(deezerTrack, musicFolder, appSettings.deezerFormat, false);
const absolutePath = `${paths.filepath}/${paths.filename}`; const absolutePath = `${paths.filepath}/${paths.filename}`;
const relativePath = makeRelativePath(absolutePath, 'Music'); const relativePath = makeRelativePath(absolutePath, playlistsFolder);
return { return {
duration: deezerTrack.duration, duration: deezerTrack.duration,

View File

@@ -51,7 +51,7 @@
// Load tracks and cover art in parallel // Load tracks and cover art in parallel
const [tracksData, coverPath] = await Promise.all([ const [tracksData, coverPath] = await Promise.all([
loadPlaylistTracks(playlist.path, playlist.name, $settings.musicFolder), loadPlaylistTracks(playlist.path, playlist.name),
findPlaylistArt(playlist.path) findPlaylistArt(playlist.path)
]); ]);