Fix spotify m3u Windows path parsing
This commit is contained in:
@@ -24,6 +24,22 @@ export function stripTrackNumberPrefix(value: string): string {
|
|||||||
.replace(/^\d{1,3}\s+/u, "");
|
.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 {
|
function ref(source: string, artist: string | undefined, title: string | undefined, query?: string): ParsedTrackRef {
|
||||||
const cleanedArtist = artist ? normalizeText(artist) : undefined;
|
const cleanedArtist = artist ? normalizeText(artist) : undefined;
|
||||||
const cleanedTitle = title ? normalizeText(title) : 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[] {
|
export function parseArtistTitle(value: string): ParsedTrackRef[] {
|
||||||
const source = normalizeText(stripTrackNumberPrefix(stripAudioExtension(value)));
|
const source = cleanTrackSource(value);
|
||||||
if (!source) {
|
if (!source) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { basename } from "node:path";
|
|
||||||
|
|
||||||
import type { ParsedTrackRef } from "../types.js";
|
import type { ParsedTrackRef } from "../types.js";
|
||||||
import { dedupeTrackRefs, parseArtistTitle } from "./importer-utils.js";
|
import { dedupeTrackRefs, parseArtistTitle } from "./importer-utils.js";
|
||||||
@@ -12,6 +11,10 @@ function parseExtInf(line: string): string | undefined {
|
|||||||
return line.slice(comma + 1).trim();
|
return line.slice(comma + 1).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filenameFromPlaylistPath(path: string): string {
|
||||||
|
return path.split(/[\\/]/u).at(-1) ?? path;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseM3u(content: string): ParsedTrackRef[] {
|
export function parseM3u(content: string): ParsedTrackRef[] {
|
||||||
const refs: ParsedTrackRef[] = [];
|
const refs: ParsedTrackRef[] = [];
|
||||||
let pendingExtInf: string | undefined;
|
let pendingExtInf: string | undefined;
|
||||||
@@ -27,7 +30,7 @@ export function parseM3u(content: string): ParsedTrackRef[] {
|
|||||||
if (line.startsWith("#")) {
|
if (line.startsWith("#")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
refs.push(...parseArtistTitle(pendingExtInf ?? basename(line)));
|
refs.push(...parseArtistTitle(pendingExtInf ?? filenameFromPlaylistPath(line)));
|
||||||
pendingExtInf = undefined;
|
pendingExtInf = undefined;
|
||||||
}
|
}
|
||||||
return dedupeTrackRefs(refs);
|
return dedupeTrackRefs(refs);
|
||||||
|
|||||||
@@ -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", () => {
|
test("dedupes normalized artist title refs", () => {
|
||||||
const refs = dedupeTrackRefs([
|
const refs = dedupeTrackRefs([
|
||||||
{ source: "a", query: "Radiohead Karma Police", artist: "Radiohead", title: "Karma Police" },
|
{ source: "a", query: "Radiohead Karma Police", artist: "Radiohead", title: "Karma Police" },
|
||||||
|
|||||||
@@ -26,6 +26,18 @@ test("falls back to filename when EXTINF is absent", () => {
|
|||||||
assert.equal(refs[0].title, "Karma Police");
|
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 () => {
|
test("reads m3u from file", async () => {
|
||||||
const root = await mkdtemp(join(tmpdir(), "spotify-m3u-"));
|
const root = await mkdtemp(join(tmpdir(), "spotify-m3u-"));
|
||||||
const path = join(root, "playlist.m3u8");
|
const path = join(root, "playlist.m3u8");
|
||||||
|
|||||||
Reference in New Issue
Block a user