Skip to content

Tracklists

Mixes and podcasts come with a tracklist: a block of text, one track per line, usually with a start time in front. trackparse/tracklist reads the whole block. Every line gets its kind, its time and a full ParsedTrack. The list as a whole gets checks with line numbers.

import { parseTracklist } from "trackparse/tracklist";

It's a separate entry point, about 7.6 KiB gzipped, so code that only parses single tracks never loads it.

const text = [
"Hour 2",
"48:20 Noisia - Stigma",
"52:47 Mefjus & Kasra - Bandwagon (Phace Remix)",
"56:05 Black Sun Empire - ID",
"59:31 Alix Perez - Fade Away",
"01:03:10 Interview with Hybris",
"01:07:44 Imanu - Sacrifice",
"00:11:02 Current Value feat. Teddy Killerz - Payload", // typo: should be 01:11:02
"01:15:36 Hybris - Spinning Top",
].join("\r\n");
const list = parseTracklist(text);
list.entries.map((e) => e.kind);
// [ 'header', 'track', 'track', 'track', 'track', 'marker', 'track', 'track', 'track' ]
const bandwagon = list.entries[2];
bandwagon.time; // { raw: '52:47', seconds: 3167 }
bandwagon.parts; // { lead: '', time: '52:47', gap: ' ', rest: 'Mefjus & Kasra - Bandwagon (Phace Remix)' }
bandwagon.track.artists.map((a) => a.name); // [ 'Mefjus', 'Kasra' ]
bandwagon.track.versions.map((v) => `${v.type} by ${v.artists[0].name}`); // [ 'remix by Phace' ]
list.entries[3].track.flags.unknownTitle; // true (the ID)
list.stats;
// { lines: 9, tracks: 7, markers: 1, timed: 8, firstTime: 2900, lastTime: 4536 }

Paste it into the playground to see the table, the warnings and the fix button.

Every entry keeps its line exactly as written in raw, and its own line ending in eol. Joining raw + eol over the entries gives you back the input, byte for byte. CRLF, LF and CR all work, even mixed in one text, and trailing spaces survive. A last line without a newline has eol: "".

parts cuts raw into four strings that join back into it:

Part Holds
lead spaces or an opening bracket before the time
time the time as written, 52:47
gap whatever sits between the time and the track: ] , -, |, =>
rest the track text, which is what gets parsed

That split is what lets an editor rewrite a time without touching anything else on the line.

All of these are read as start times:

Written Seconds
12:34, [12:34], (12:34) 754
1:02:03, 01:02:03 3723
125:30 (a long mix with no hours) 7530
00:00:07.500, 0:07,5 7.5

Something that looks like a time but can't be one, like 1:75 or 0:61:00, stays in rest and raises an invalid-time warning.

Kind When
track a timed line, or an untimed line with an artist/title separator
marker no separator and a word like intro, outro, break or interview
header an untimed line ending in :, or something like Part 2 or Hour 2
link starts with http://, https:// or www.
note any other untimed line
blank empty or only whitespace

Only track and marker entries have a track. No line is ever dropped. Extra marker words go in the markerWords option (marker_words in Python): with ["drop"], 03:00 Drop becomes a marker instead of a track.

list.warnings;
// [
// { code: 'no-start-at-zero', line: 2, severity: 'info', suggestion: null },
// { code: 'time-out-of-order', line: 8, severity: 'warning',
// suggestion: { line: 8, raw: '01:11:02', seconds: 4262 } }
// ]

00:11:02 sits between 01:07:44 and 01:15:36. Changing one digit gives exactly one time that fits, 01:11:02, so the warning carries it as a suggestion. When several fixes would fit, or none does, suggestion is null and you only get the warning.

Sometimes the typo is on the line before. In 01:22:21 followed by 01:03:55, the second line is flagged, but the suggestion is to change the first one to 01:02:21. All the codes are listed in Warnings.

Some lists number their lines without a dot: 1 Artist - Title, 2 Artist - Title. A single parse() call would read 1 Artist as the artist's name, and it should, because 2 Unlimited exists. In a list, trackparse looks at the neighbours: a bare number counts as a position when the nearest line before or after it carries the next or previous number.

Inside a tracklist, the first - is the separator on practically every line, so each line is parsed with mode: "tracklist". These lines all go wrong with a plain parse() and right in a list:

Line parse(line) In a tracklist
Electrosoul System - Liquid Metal title Electrosoul System, Liquid Metal as genre junk Electrosoul System / Liquid Metal
Effector - 2012 title Effector, year 2012 Effector / 2012
Paperclip - Anticipation VIP title Paperclip, a vip version Paperclip / Anticipation VIP
8 Linkin Park_-_Points Of Authority (Gydra Bootleg) no separator position 8, Linkin Park / Points Of Authority, bootleg by Gydra
10 Optiv & BTK-Inception (Gydra remix) no separator position 10, Optiv, BTK / Inception, remix by Gydra

The last two need neighbours: the position needs a 7 or 9 next to it, and the unspaced BTK-Inception dash only splits on a timed line when at least half the list uses spaced dashes.

Next: editing tracklists.