From 26a968797c61c937cc3064913424c413a605d965 Mon Sep 17 00:00:00 2001 From: Stefano Fiorini Date: Sun, 12 Apr 2026 10:03:43 -0500 Subject: [PATCH] Fix spotify m3u Windows path parsing --- skills/spotify/src/importers/importer-utils.ts | 18 +++++++++++++++++- skills/spotify/src/importers/m3u.ts | 7 +++++-- skills/spotify/tests/importer-utils.test.ts | 12 ++++++++++++ skills/spotify/tests/m3u.test.ts | 12 ++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/skills/spotify/src/importers/importer-utils.ts b/skills/spotify/src/importers/importer-utils.ts index 4dc2d70..75bac62 100644 --- a/skills/spotify/src/importers/importer-utils.ts +++ b/skills/spotify/src/importers/importer-utils.ts @@ -24,6 +24,22 @@ export function stripTrackNumberPrefix(value: string): string { .replace(/^\d{1,3}\s+/u, ""); } +function stripTransientSuffixes(value: string): string { + return value.replace(/(?:[.\s_-]+temp)$/iu, ""); +} + +function replaceDotWordSeparators(value: string): string { + const dotParts = value.split(".").filter(Boolean); + if (dotParts.length > 1 && dotParts.every((part) => /^\p{L}$/u.test(part))) { + return value; + } + return value.replace(/\.(?=\S)/gu, " "); +} + +function cleanTrackSource(value: string): string { + return normalizeText(replaceDotWordSeparators(stripTrackNumberPrefix(stripTransientSuffixes(stripAudioExtension(value))))); +} + function ref(source: string, artist: string | undefined, title: string | undefined, query?: string): ParsedTrackRef { const cleanedArtist = artist ? normalizeText(artist) : undefined; const cleanedTitle = title ? normalizeText(title) : undefined; @@ -36,7 +52,7 @@ function ref(source: string, artist: string | undefined, title: string | undefin } export function parseArtistTitle(value: string): ParsedTrackRef[] { - const source = normalizeText(stripTrackNumberPrefix(stripAudioExtension(value))); + const source = cleanTrackSource(value); if (!source) { return []; } diff --git a/skills/spotify/src/importers/m3u.ts b/skills/spotify/src/importers/m3u.ts index a6d5665..0914054 100644 --- a/skills/spotify/src/importers/m3u.ts +++ b/skills/spotify/src/importers/m3u.ts @@ -1,5 +1,4 @@ import { readFile } from "node:fs/promises"; -import { basename } from "node:path"; import type { ParsedTrackRef } from "../types.js"; import { dedupeTrackRefs, parseArtistTitle } from "./importer-utils.js"; @@ -12,6 +11,10 @@ function parseExtInf(line: string): string | undefined { return line.slice(comma + 1).trim(); } +function filenameFromPlaylistPath(path: string): string { + return path.split(/[\\/]/u).at(-1) ?? path; +} + export function parseM3u(content: string): ParsedTrackRef[] { const refs: ParsedTrackRef[] = []; let pendingExtInf: string | undefined; @@ -27,7 +30,7 @@ export function parseM3u(content: string): ParsedTrackRef[] { if (line.startsWith("#")) { continue; } - refs.push(...parseArtistTitle(pendingExtInf ?? basename(line))); + refs.push(...parseArtistTitle(pendingExtInf ?? filenameFromPlaylistPath(line))); pendingExtInf = undefined; } return dedupeTrackRefs(refs); diff --git a/skills/spotify/tests/importer-utils.test.ts b/skills/spotify/tests/importer-utils.test.ts index 24098bd..b0a3eba 100644 --- a/skills/spotify/tests/importer-utils.test.ts +++ b/skills/spotify/tests/importer-utils.test.ts @@ -35,6 +35,18 @@ test("parses artist title patterns", () => { ]); }); +test("cleans filename-only track names from dotted and temp media filenames", () => { + assert.deepEqual(parseArtistTitle("The.Hills.flac"), [ + { source: "The Hills", query: "The Hills", title: "The Hills" } + ]); + assert.deepEqual(parseArtistTitle("15.I Feel It Coming.temp.mp3"), [ + { source: "I Feel It Coming", query: "I Feel It Coming", title: "I Feel It Coming" } + ]); + assert.deepEqual(parseArtistTitle("Y.M.C.A..mp3"), [ + { source: "Y.M.C.A.", query: "Y.M.C.A.", title: "Y.M.C.A." } + ]); +}); + test("dedupes normalized artist title refs", () => { const refs = dedupeTrackRefs([ { source: "a", query: "Radiohead Karma Police", artist: "Radiohead", title: "Karma Police" }, diff --git a/skills/spotify/tests/m3u.test.ts b/skills/spotify/tests/m3u.test.ts index 1d4883a..0bd18be 100644 --- a/skills/spotify/tests/m3u.test.ts +++ b/skills/spotify/tests/m3u.test.ts @@ -26,6 +26,18 @@ test("falls back to filename when EXTINF is absent", () => { assert.equal(refs[0].title, "Karma Police"); }); +test("falls back to the filename from Windows paths", () => { + const refs = parseM3u(String.raw`C:\Users\fiori\iCloudDrive\Music\Classic Hits\Owner of a Lonely Heart.mp3`); + + assert.deepEqual(refs, [ + { + source: "Owner of a Lonely Heart", + query: "Owner of a Lonely Heart", + title: "Owner of a Lonely Heart" + } + ]); +}); + test("reads m3u from file", async () => { const root = await mkdtemp(join(tmpdir(), "spotify-m3u-")); const path = join(root, "playlist.m3u8");