trackparse 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,22 +1,34 @@
1
1
  # trackparse
2
2
 
3
- Parse music track names into structured metadata: artists, featured artists, remixers, version
4
- (remix, extended, live, remaster), producers and a clean title.
3
+ [![npm](https://img.shields.io/npm/v/trackparse?logo=npm&logoColor=white)](https://www.npmjs.com/package/trackparse)
4
+ [![types](https://img.shields.io/npm/types/trackparse?logo=typescript&logoColor=white)](https://www.npmjs.com/package/trackparse)
5
+ [![tests](https://img.shields.io/github/actions/workflow/status/4matic/trackparse/js.yml?branch=main&label=tests&logo=github)](https://github.com/4matic/trackparse/actions/workflows/js.yml)
6
+ [![license](https://img.shields.io/badge/license-MIT-blue)](https://github.com/4matic/trackparse/blob/main/LICENSE)
5
7
 
6
- [![npm](https://img.shields.io/npm/v/trackparse.svg)](https://www.npmjs.com/package/trackparse)
7
- [![js](https://img.shields.io/github/actions/workflow/status/4matic/trackparse/js.yml?branch=main&label=js)](https://github.com/4matic/trackparse/actions/workflows/js.yml)
8
- [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/4matic/trackparse/blob/main/LICENSE)
8
+ **[Documentation](https://4matic.github.io/trackparse/)** · **[Getting started](https://4matic.github.io/trackparse/start/getting-started/)** · **[Playground](https://4matic.github.io/trackparse/playground/)**
9
9
 
10
- **[Documentation](https://4matic.github.io/trackparse/)** · **[Playground](https://4matic.github.io/trackparse/playground/)**
10
+ **Turn messy track names into structured data: who made it, who's featured, who remixed it, and
11
+ what's just noise.**
11
12
 
12
- This is the JavaScript/TypeScript reference implementation of the
13
- [trackparse spec](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md). It passes all
14
- 13,130 shared fixtures. Zero dependencies, ESM and CommonJS, TypeScript types included, never
15
- throws.
13
+ ```text
14
+ 01. Wilkinson ft. Becky Hill & Tom Cane - Afterglow (Sub Focus VIP Remix) [Official Video]
15
+ ```
16
+
17
+ Most title parsers split that into `"Wilkinson ft. Becky Hill & Tom Cane"` and
18
+ `"Afterglow (Sub Focus VIP Remix)"` and stop. Others "clean" it and throw the feat and the remix
19
+ away. trackparse keeps all of it. Everything it recognizes goes into a field, and anything it
20
+ doesn't recognize stays in the title, word for word.
21
+
22
+ This is the JavaScript and TypeScript package of
23
+ [trackparse](https://github.com/4matic/trackparse), and the reference implementation. Its behaviour
24
+ is defined by a [shared spec](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md) and
25
+ pinned by 13,130 fixtures. It passes all of them, and the
26
+ [Python and Rust packages](#other-languages) return the same output for every one.
16
27
 
17
- The principle is **extract, don't delete**: everything recognised lands in a field (artists with
18
- roles, versions with a type and their own credits, junk with a kind, flags, position), and
19
- everything else stays in the title verbatim.
28
+ - Node 18 or later, browsers and other runtimes (no Node-specific APIs)
29
+ - No dependencies
30
+ - ESM and CommonJS, TypeScript types included
31
+ - Never throws
20
32
 
21
33
  ## Install
22
34
 
@@ -25,15 +37,13 @@ npm install trackparse
25
37
  # or: pnpm add trackparse / yarn add trackparse / bun add trackparse
26
38
  ```
27
39
 
28
- Node 18 or later. No Node-specific APIs are used, so it also runs in browsers and other runtimes.
29
-
30
40
  ## Quick start
31
41
 
32
42
  **Docs:** [Getting started](https://4matic.github.io/trackparse/start/getting-started/)
33
43
 
34
44
  ```js
35
- import { parse } from "trackparse";
36
- // const { parse } = require("trackparse");
45
+ import { format, parse } from "trackparse";
46
+ // const { format, parse } = require("trackparse");
37
47
 
38
48
  const track = parse(
39
49
  "01. Wilkinson ft. Becky Hill & Tom Cane - Afterglow (Sub Focus VIP Remix) [Official Video]",
@@ -48,24 +58,184 @@ track.versions[0].artists[0].name; // 'Sub Focus'
48
58
  track.versions[0].modifiers; // [ 'vip' ]
49
59
  track.junk; // [ { raw: 'Official Video', kind: 'video' } ]
50
60
  track.position; // { raw: '01', number: 1 }
51
- track.mode; // 'youtube' (auto-detected from the junk group)
61
+ track.mode; // 'youtube' (detected from the junk group)
62
+
63
+ format(track, { joiners: "canonical" });
64
+ // 'Wilkinson feat. Becky Hill & Tom Cane - Afterglow (Sub Focus VIP Remix)'
65
+ ```
66
+
67
+ The result is a plain object, so `JSON.stringify(track)` gives the spec's JSON form. Every key is
68
+ always present:
69
+
70
+ ```js
71
+ console.log(JSON.stringify(track, null, 2));
52
72
  ```
53
73
 
74
+ <details>
75
+ <summary>Output</summary>
76
+
77
+ ```json
78
+ {
79
+ "input": "01. Wilkinson ft. Becky Hill & Tom Cane - Afterglow (Sub Focus VIP Remix) [Official Video]",
80
+ "mode": "youtube",
81
+ "position": { "raw": "01", "number": 1 },
82
+ "timestamp": null,
83
+ "artists": [
84
+ { "name": "Wilkinson", "role": "primary", "joiner": null, "source": "artist" },
85
+ { "name": "Becky Hill", "role": "featured", "joiner": "ft.", "source": "artist" },
86
+ { "name": "Tom Cane", "role": "featured", "joiner": "&", "source": "artist" }
87
+ ],
88
+ "title": "Afterglow",
89
+ "fullTitle": "Afterglow (Sub Focus VIP Remix)",
90
+ "versions": [
91
+ {
92
+ "type": "remix",
93
+ "raw": "Sub Focus VIP Remix",
94
+ "artists": [{ "name": "Sub Focus", "role": "remixer", "joiner": null, "source": "version" }],
95
+ "modifiers": ["vip"],
96
+ "descriptor": null,
97
+ "year": null,
98
+ "unknownArtist": false,
99
+ "delimiter": "("
100
+ }
101
+ ],
102
+ "year": null,
103
+ "flags": { "explicit": false, "clean": false, "unknownArtist": false, "unknownTitle": false },
104
+ "junk": [{ "raw": "Official Video", "kind": "video" }],
105
+ "warnings": []
106
+ }
107
+ ```
108
+
109
+ (Reformatted for width; the keys and values are exactly what `JSON.stringify` prints.)
110
+
111
+ </details>
112
+
54
113
  ## API
55
114
 
56
115
  **Docs:** [parse and ParsedTrack](https://4matic.github.io/trackparse/reference/parse/) · [Other functions](https://4matic.github.io/trackparse/reference/functions/)
57
116
 
58
- ### `parse(input, options?) → ParsedTrack`
117
+ ```ts
118
+ parse(input: string, options?: ParseOptions): ParsedTrack
119
+ parseArtists(input: string, options?: ParseOptions): Artist[]
120
+ format(track: ParsedTrack, options?: FormatOptions): string
121
+ normalize(input: string): string
122
+ allArtists(track: ParsedTrack): Artist[]
123
+ createParser(options?: ParseOptions): Parser // parser.parse(...), parser.parseArtists(...)
124
+ SPEC_VERSION: string
125
+
126
+ // from "trackparse/tracklist"
127
+ parseTracklist(text: string, options?: TracklistOptions): Tracklist
128
+ applyEdits(text: string, edits: Edit[], options?: TracklistOptions): EditResult
129
+ ```
59
130
 
60
- Parses one track string. Every key of the result is always present.
131
+ ### `parse(input, options?)`
61
132
 
62
- ### `parseArtists(input, options?) → Artist[]`
133
+ **Docs:** [parse and ParsedTrack](https://4matic.github.io/trackparse/reference/parse/) · [Options](https://4matic.github.io/trackparse/reference/options/)
63
134
 
64
- **Docs:** [parseArtists](https://4matic.github.io/trackparse/reference/functions/#parseartistsinput-options)
135
+ Parses one track string.
65
136
 
66
- Splits an artist string (no title) into credits.
137
+ | Option | Default | |
138
+ |---|---|---|
139
+ | `mode` | `"auto"` | `"clean"`, `"youtube"`, `"filename"`, `"tracklist"`, or `"auto"` to detect from the string. See [Modes](#modes) |
140
+ | `uploader` | – | YouTube channel name, used as the artist when there's no separator (`- Topic` and `VEVO` are dropped) |
141
+ | `knownArtists` | `[]` | Names that are never split. There is no built-in list |
142
+ | `splitAnd` | `"auto"` | `"auto"` splits on `and` only after a comma (`A, B and C`), so `Simon and Garfunkel` stays whole. Also `"always"`, `"never"` |
143
+ | `keywords` | – | Extra vocabulary, added to the built-in lists (see below) |
67
144
 
68
145
  ```js
146
+ parse("Chase & Status - Baddadan").artists.map((a) => a.name);
147
+ // [ 'Chase', 'Status' ]
148
+ parse("Chase & Status - Baddadan", { knownArtists: ["Chase & Status"] }).artists.map((a) => a.name);
149
+ // [ 'Chase & Status' ]
150
+
151
+ parse("Simon and Garfunkel - Mrs. Robinson", { splitAnd: "always" }).artists.map((a) => a.name);
152
+ // [ 'Simon', 'Garfunkel' ]
153
+ ```
154
+
155
+ `keywords` is an object with any of these keys:
156
+
157
+ | Key | Type | |
158
+ |---|---|---|
159
+ | `versionHeads` | `Record<string, VersionType>` | Word that sets a version type (`{ rerub: "rework" }`) |
160
+ | `descriptors` | `string[]` | Words kept as a version's `descriptor` |
161
+ | `genres` | `string[]` | Genre words |
162
+ | `junk` | `Record<string, JunkKind>` | Phrase to junk kind (`{ "some label rip": "label" }`) |
163
+ | `featMarkers` | `string[]` | Extra feat markers |
164
+
165
+ ```js
166
+ parse("Artist - Song [Some Label Rip]").title;
167
+ // 'Song [Some Label Rip]'
168
+ parse("Artist - Song [Some Label Rip]", { keywords: { junk: { "some label rip": "label" } } }).junk;
169
+ // [ { raw: 'Some Label Rip', kind: 'label' } ]
170
+ ```
171
+
172
+ ### `ParsedTrack`
173
+
174
+ **Docs:** [ParsedTrack fields](https://4matic.github.io/trackparse/reference/parse/#parsedtrack)
175
+
176
+ ```ts
177
+ interface ParsedTrack {
178
+ input: string; // the original string, untouched
179
+ mode: "clean" | "youtube" | "filename" | "tracklist"; // resolved mode
180
+ position: { raw: string; number: number } | null; // track number: "01." gives 1
181
+ timestamp: { raw: string; seconds: number } | null; // leading cue time: "[12:34]" gives 754
182
+ artists: Artist[]; // primary, featured and producer credits (not remixers)
183
+ title: string; // clean title; brackets it doesn't recognize stay in
184
+ fullTitle: string; // title with the versions re-attached
185
+ versions: Version[]; // remixes and other versions, left to right
186
+ year: number | null; // from "(2015)" or "- 2015"
187
+ flags: { explicit: boolean; clean: boolean; unknownArtist: boolean; unknownTitle: boolean };
188
+ junk: { raw: string; kind: JunkKind }[]; // what was taken out, each with a kind
189
+ warnings: Warning[]; // heuristics that fired
190
+ }
191
+
192
+ interface Artist {
193
+ name: string;
194
+ role: "primary" | "featured" | "remixer" | "producer";
195
+ joiner: string | null; // "&", ",", "x", "ft.", "prod. by" as written; null for the first name
196
+ source: "artist" | "title" | "version"; // left of the dash, right of it, or inside a version
197
+ }
198
+
199
+ interface Version {
200
+ type: VersionType; // remix, vip, bootleg, extended, radio, live, remaster...
201
+ raw: string; // "Sub Focus VIP Remix"
202
+ artists: Artist[]; // remixers
203
+ modifiers: string[]; // type words that didn't become the type: ["vip"]
204
+ descriptor: string | null; // leftover words: "Taylor's", "at Wembley"
205
+ year: number | null;
206
+ unknownArtist: boolean; // "ID Remix"
207
+ delimiter: "(" | "[" | "{" | "-"; // the bracket it was in, or "-" for a dash suffix
208
+ }
209
+ ```
210
+
211
+ ```js
212
+ parse("Pendulum - Watercolour (Extended VIP Mix)").versions[0];
213
+ // { type: 'vip', raw: 'Extended VIP Mix', artists: [], modifiers: [ 'extended' ],
214
+ // descriptor: null, year: null, unknownArtist: false, delimiter: '(' }
215
+
216
+ parse("Queen - Bohemian Rhapsody (Live at Wembley 1986)").versions[0];
217
+ // { type: 'live', raw: 'Live at Wembley 1986', artists: [], modifiers: [],
218
+ // descriptor: 'at Wembley', year: 1986, unknownArtist: false, delimiter: '(' }
219
+ ```
220
+
221
+ - `VersionType`: `remix`, `bootleg`, `vip`, `edit`, `flip`, `refix`, `rework`, `mashup`, `blend`,
222
+ `dub`, `mix`, `extended`, `radio`, `club`, `original`, `instrumental`, `acapella`, `live`,
223
+ `acoustic`, `remaster`, `demo`, `reprise`, `cover`, `version`, `spedUp`, `slowed`, `nightcore`.
224
+ - `JunkKind`: `video`, `audio`, `lyrics`, `quality`, `promo`, `platform`, `label`, `genre`, `other`.
225
+ - `Warning`: see [Warnings](#warnings).
226
+
227
+ The same shape is published as a JSON Schema,
228
+ [`parsed-track.schema.json`](https://github.com/4matic/trackparse/blob/main/spec/schema/parsed-track.schema.json).
229
+
230
+ ### `parseArtists(input, options?)`
231
+
232
+ **Docs:** [parseArtists](https://4matic.github.io/trackparse/reference/functions/#parseartistsinput-options) · [Artists and credits](https://4matic.github.io/trackparse/guides/artists/)
233
+
234
+ Splits a bare artist string (no title). Takes the same options as `parse`.
235
+
236
+ ```js
237
+ import { parseArtists } from "trackparse";
238
+
69
239
  parseArtists("Sub Focus, Wilkinson & Dimension feat. Kojo");
70
240
  // [
71
241
  // { name: 'Sub Focus', role: 'primary', joiner: null, source: 'artist' },
@@ -75,60 +245,72 @@ parseArtists("Sub Focus, Wilkinson & Dimension feat. Kojo");
75
245
  // ]
76
246
  ```
77
247
 
78
- ### `format(track, options?) → string`
248
+ ### `format(track, options?)`
79
249
 
80
250
  **Docs:** [format](https://4matic.github.io/trackparse/reference/functions/#formattrack-options) · [Formatting](https://4matic.github.io/trackparse/guides/formatting/)
81
251
 
82
- Renders a `ParsedTrack` back to `Artists - Title (versions) (prod. ...)`.
252
+ Renders a `ParsedTrack` back to a string: `Artists - Title (versions) (prod. ...)`.
253
+
254
+ | Option | Default | |
255
+ |---|---|---|
256
+ | `feat` | `"source"` | Where featured artists go: `"source"` (where they were), `"artist"`, `"title"` (`(feat. X)` after the title) or `"omit"` |
257
+ | `featMarker` | – | Marker for featured artists, such as `"featuring"` |
258
+ | `joiners` | `"original"` | `"original"` keeps the joiners as written. `"canonical"` writes `and` as `&`, `vs` as `vs.` and every feat marker as `feat.` |
259
+ | `versions` | `"all"` | `"all"` or `"none"` |
260
+ | `producers` | `true` | Include `(prod. ...)` credits |
261
+ | `position` | `false` | Include the track number (`01. `) |
83
262
 
84
263
  ```js
264
+ import { format, parse } from "trackparse";
265
+
85
266
  const t = parse("Wilkinson ft. Becky Hill - Afterglow (Sub Focus Remix)");
86
267
 
87
268
  format(t); // 'Wilkinson ft. Becky Hill - Afterglow (Sub Focus Remix)'
88
269
  format(t, { feat: "title", joiners: "canonical" }); // 'Wilkinson - Afterglow (feat. Becky Hill) (Sub Focus Remix)'
89
270
  format(t, { feat: "omit", versions: "none" }); // 'Wilkinson - Afterglow'
271
+ format(t, { featMarker: "featuring" }); // 'Wilkinson featuring Becky Hill - Afterglow (Sub Focus Remix)'
90
272
  ```
91
273
 
92
- | Option | Default | Effect |
93
- |---|---|---|
94
- | `feat` | `"source"` | `"source"` (where they were found), `"artist"`, `"title"` (`(feat. X)` after the title) or `"omit"`. |
95
- | `featMarker` | – | Override the feat marker (`"featuring"`). |
96
- | `joiners` | `"original"` | `"canonical"`: `and` → `&`, `vs` → `vs.`, feat markers → `feat.`. |
97
- | `versions` | `"all"` | `"none"` drops versions. |
98
- | `producers` | `true` | Append `(prod. by X)`. |
99
- | `position` | `false` | Prefix the track number (`01. `). |
100
-
101
- ### `normalize(input) → string`
274
+ ### `normalize(input)`
102
275
 
103
276
  **Docs:** [normalize](https://4matic.github.io/trackparse/reference/functions/#normalizeinput)
104
277
 
105
- The Unicode cleanup applied before parsing: NFC, zero-width spaces, BOM and soft hyphens removed, fullwidth
106
- brackets, smart quotes and dash variants mapped to ASCII, whitespace collapsed. Idempotent.
278
+ The Unicode cleanup every function runs first: NFC, zero-width spaces, BOM and soft hyphens
279
+ removed, fullwidth brackets, smart quotes and dash variants mapped to ASCII, whitespace collapsed.
280
+ Running it twice changes nothing.
107
281
 
108
282
  ```js
109
- normalize(" Artist​ — Title (Remix) “Quoted” ");
283
+ import { normalize } from "trackparse";
284
+
285
+ normalize(" Artist\u200b — Title (Remix) “Quoted” ");
110
286
  // 'Artist - Title (Remix) "Quoted"'
111
287
  ```
112
288
 
113
- ### `allArtists(track) → Artist[]`
289
+ ### `allArtists(track)`
114
290
 
115
291
  **Docs:** [allArtists](https://4matic.github.io/trackparse/reference/functions/#allartiststrack)
116
292
 
117
- Track credits plus every version's remixers, deduplicated.
293
+ Credits plus every remixer, deduplicated.
118
294
 
119
295
  ```js
120
- allArtists(parse("Camo & Krooked - Kallisto (Noisia Remix)", { knownArtists: ["Camo & Krooked"] }))
296
+ import { allArtists, parse } from "trackparse";
297
+
298
+ allArtists(parse("Wilkinson ft. Becky Hill - Afterglow (Sub Focus Remix)"))
121
299
  .map((a) => `${a.name} (${a.role})`);
122
- // [ 'Camo & Krooked (primary)', 'Noisia (remixer)' ]
300
+ // [ 'Wilkinson (primary)', 'Becky Hill (featured)', 'Sub Focus (remixer)' ]
123
301
  ```
124
302
 
125
- ### `createParser(options) → { parse, parseArtists }`
303
+ ### `createParser(options?)`
126
304
 
127
305
  **Docs:** [createParser](https://4matic.github.io/trackparse/reference/functions/#createparseroptions)
128
306
 
129
- Compiles options, including extra vocabulary, once. Per-call options are merged over the base.
307
+ Compiles the options once, for many calls. Per-call options are merged over the parser's: a value
308
+ passed per call replaces the base one (`knownArtists` included), `undefined` leaves it alone, and
309
+ `keywords` objects are merged.
130
310
 
131
311
  ```js
312
+ import { createParser } from "trackparse";
313
+
132
314
  const parser = createParser({
133
315
  knownArtists: ["Chase & Status", "Camo & Krooked"],
134
316
  keywords: { versionHeads: { rerub: "rework" }, junk: { "full album": "other" } },
@@ -143,92 +325,75 @@ r.junk; // [ { raw: 'Full Album', kind: 'other' } ]
143
325
 
144
326
  ### `SPEC_VERSION`
145
327
 
146
- The spec version this build implements, e.g. `"0.3.0"`. Always equal to the package version.
328
+ The spec version this build implements, `"0.5.0"` in this release. The npm package, the PyPI
329
+ package, the Rust crate and the spec are released together and always share one version number.
147
330
 
148
- ## Options
331
+ ## Modes
149
332
 
150
- **Docs:** [Options](https://4matic.github.io/trackparse/reference/options/)
333
+ **Docs:** [Modes](https://4matic.github.io/trackparse/guides/modes/)
151
334
 
152
- | Option | Default | Effect |
335
+ | Mode | For | Adds |
153
336
  |---|---|---|
154
- | `mode` | `"auto"` | `"clean"` (tags, store metadata), `"youtube"` (video titles), `"filename"`, `"tracklist"` (one line of a tracklist), or `"auto"` to detect. |
155
- | `uploader` | – | YouTube channel, used as the artist when no separator is found. ` - Topic` and `VEVO` are stripped. |
156
- | `knownArtists` | `[]` | Names never split (`Chase & Status`). There is no built-in list. |
157
- | `splitAnd` | `"auto"` | `and` splits only after a comma in the same list (`A, B and C`), so `Simon and Garfunkel` stays one. `"always"` / `"never"`. |
158
- | `keywords` | – | Extra vocabulary: `versionHeads`, `descriptors`, `genres`, `junk`, `featMarkers`. |
337
+ | `clean` | Store metadata, tags | Only a spaced dash separates artist and title |
338
+ | `youtube` | Video titles, scrobbles | Pipe segments, `"Title" by Artist`, `Artist: "Title"`, unspaced dashes, trailing junk, uploader fallback |
339
+ | `filename` | Files | Strips the extension, `_` becomes a space, `01-Track` numbering |
340
+ | `auto` | Anything | `filename` for known extensions, `youtube` when it sees junk, pipes or platform suffixes, `clean` otherwise |
341
+ | `tracklist` | One line of a tracklist | The first spaced dash always separates; `_-_` too. Never picked by `auto` |
159
342
 
160
343
  ```js
161
- parse("Chase & Status - Blind Faith (Loadstar Remix)").artists.map((a) => a.name);
162
- // [ 'Chase', 'Status' ]
163
- parse("Chase & Status - Blind Faith (Loadstar Remix)", { knownArtists: ["Chase & Status"] })
164
- .artists.map((a) => a.name);
165
- // [ 'Chase & Status' ]
344
+ let t = parse('"Numb" by Linkin Park', { mode: "youtube" });
345
+ [t.artists.map((a) => a.name), t.title, t.warnings];
346
+ // [ [ 'Linkin Park' ], 'Numb', [ 'bySplit' ] ]
347
+
348
+ t = parse("07_Burial_-_Archangel.mp3");
349
+ [t.mode, t.artists.map((a) => a.name), t.title, t.position];
350
+ // [ 'filename', [ 'Burial' ], 'Archangel', { raw: '07', number: 7 } ]
166
351
 
167
- parse("Jay-Z-Numb").title; // 'Jay-Z-Numb' (clean: no split)
352
+ parse("Shelter", { mode: "youtube", uploader: "Porter Robinson" }).artists;
353
+ // [ { name: 'Porter Robinson', role: 'primary', joiner: null, source: 'artist' } ]
354
+
355
+ parse("Jay-Z-Numb").title; // 'Jay-Z-Numb' (clean: no split)
168
356
  parse("Jay-Z-Numb", { mode: "youtube" }).artists.map((a) => a.name); // [ 'Jay-Z' ]
169
357
  ```
170
358
 
171
- ## Output types
359
+ ## Warnings
172
360
 
173
- All types are exported (`import type { ParsedTrack } from "trackparse"`).
361
+ **Docs:** [Warnings](https://4matic.github.io/trackparse/reference/warnings/)
174
362
 
175
- ```ts
176
- interface ParsedTrack {
177
- input: string; // original, untouched
178
- mode: "clean" | "youtube" | "filename" | "tracklist"; // resolved mode
179
- position: { raw: string; number: number } | null;
180
- timestamp: { raw: string; seconds: number } | null;
181
- artists: Artist[]; // primary, featured, producer (not remixers)
182
- title: string; // clean title
183
- fullTitle: string; // title + versions with their brackets
184
- versions: Version[];
185
- year: number | null;
186
- flags: { explicit: boolean; clean: boolean; unknownArtist: boolean; unknownTitle: boolean };
187
- junk: { raw: string; kind: JunkKind }[];
188
- warnings: Warning[];
189
- }
363
+ The parser never fails, but it says when it had to guess. `track.warnings` is an array of strings,
364
+ empty when every decision was unambiguous.
190
365
 
191
- interface Artist {
192
- name: string;
193
- role: "primary" | "featured" | "remixer" | "producer";
194
- joiner: string | null; // "&", ",", "x", "ft.", "prod. by"... as written
195
- source: "artist" | "title" | "version";
196
- }
366
+ | Warning | Means |
367
+ |---|---|
368
+ | `noSeparator` | No artist/title separator was found. The whole string is the title |
369
+ | `ambiguousSeparator` | A two-part pipe title with no dash was read as artist and title |
370
+ | `unspacedDashSplit` | Split at a dash without spaces around it |
371
+ | `asymmetricDashSplit` | Split at a dash with a space on one side only |
372
+ | `bySplit` | Read `"Title" by Artist` |
373
+ | `quotedTitleSplit` | Took a quoted span as the title and the text before it as the artist |
374
+ | `ambiguousMixCredit` | `<Name> Mix` was read as a remix, but the name is long enough to be a style |
375
+ | `unbalancedBrackets` | A bracket was opened and never closed |
197
376
 
198
- interface Version {
199
- type: VersionType; // remix, vip, bootleg, extended, radio, live, remaster...
200
- raw: string; // "Sub Focus VIP Remix"
201
- artists: Artist[]; // remixers
202
- modifiers: string[]; // ["vip"]
203
- descriptor: string | null; // "Taylor's", "at Wembley", genres
204
- year: number | null;
205
- unknownArtist: boolean; // "ID Remix"
206
- delimiter: "(" | "[" | "{" | "-";
207
- }
208
- ```
377
+ ```js
378
+ const t = parse("Around the World");
379
+ [t.artists, t.title, t.warnings];
380
+ // [ [], 'Around the World', [ 'noSeparator' ] ]
209
381
 
210
- - `VersionType`: `remix`, `bootleg`, `vip`, `edit`, `flip`, `refix`, `rework`, `mashup`, `blend`,
211
- `dub`, `mix`, `extended`, `radio`, `club`, `original`, `instrumental`, `acapella`, `live`,
212
- `acoustic`, `remaster`, `demo`, `reprise`, `cover`, `version`, `spedUp`, `slowed`, `nightcore`.
213
- - `JunkKind`: `video`, `audio`, `lyrics`, `quality`, `promo`, `platform`, `label`, `genre`, `other`.
214
- - `Warning`: `noSeparator`, `ambiguousSeparator`, `unspacedDashSplit`, `asymmetricDashSplit`,
215
- `bySplit`, `quotedTitleSplit`, `ambiguousMixCredit`, `unbalancedBrackets`.
382
+ parse("Noisia - Stigma (VIP").warnings;
383
+ // [ 'unbalancedBrackets' ]
384
+ ```
216
385
 
217
- The full field reference, a gallery of examples and the comparison with other libraries are in the
218
- [documentation](https://4matic.github.io/trackparse/): [`parse` and ParsedTrack](https://4matic.github.io/trackparse/reference/parse/),
219
- [options](https://4matic.github.io/trackparse/reference/options/), [warnings](https://4matic.github.io/trackparse/reference/warnings/),
220
- [what it understands](https://4matic.github.io/trackparse/guides/what-it-understands/). Exact behaviour is defined by the
221
- [spec](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md) and its
222
- [fixtures](https://github.com/4matic/trackparse/tree/main/spec/fixtures).
386
+ Tracklists have their own warnings, with line numbers. They are listed under
387
+ [Tracklists](#tracklists).
223
388
 
224
389
  ## Tracklists
225
390
 
226
- **Docs:** [Tracklists](https://4matic.github.io/trackparse/guides/tracklists/) · [Editing tracklists](https://4matic.github.io/trackparse/guides/editing-tracklists/)
391
+ **Docs:** [Tracklists](https://4matic.github.io/trackparse/guides/tracklists/) · [Editing tracklists](https://4matic.github.io/trackparse/guides/editing-tracklists/) · [Tracklist API](https://4matic.github.io/trackparse/reference/tracklist/) · [Warnings](https://4matic.github.io/trackparse/reference/warnings/#tracklist-warnings)
227
392
 
228
- `trackparse/tracklist` parses a whole tracklist: one entry per line with its kind, start time and
229
- a full `ParsedTrack`, plus checks with line numbers. Joining `raw + eol` over the entries gives the
230
- input back byte for byte, whatever the line endings. It's a separate entry point (about 8.3 KiB
231
- gzipped), so you only pay for it if you import it.
393
+ `trackparse/tracklist` parses a whole tracklist, one entry per line, and checks it. Each entry
394
+ keeps its line exactly as written plus its own line ending, so
395
+ `list.entries.map((e) => e.raw + e.eol).join("")` is always the input, CRLF or not. It is a
396
+ separate entry point (about 8.4 KiB gzipped), so you only load it if you import it.
232
397
 
233
398
  ```js
234
399
  import { applyEdits, parseTracklist } from "trackparse/tracklist";
@@ -250,9 +415,14 @@ const list = parseTracklist(text);
250
415
 
251
416
  list.entries.map((e) => e.kind);
252
417
  // [ 'header', 'track', 'track', 'track', 'track', 'marker', 'track', 'track', 'track' ]
253
- list.entries[2].time; // { raw: '52:47', seconds: 3167 }
254
- list.entries[2].track.versions[0].type; // 'remix'
255
- list.entries[3].track.flags.unknownTitle; // true
418
+
419
+ const e = list.entries[2];
420
+ e.time; // { raw: '52:47', seconds: 3167 }
421
+ e.parts;
422
+ // { lead: '', time: '52:47', gap: ' ', rest: 'Mefjus & Kasra - Bandwagon (Phace Remix)' }
423
+ [e.track.artists.map((a) => a.name), e.track.title, e.track.versions[0].type];
424
+ // [ [ 'Mefjus', 'Kasra' ], 'Bandwagon', 'remix' ]
425
+ list.entries[3].track.flags.unknownTitle; // true (the ID)
256
426
 
257
427
  list.warnings;
258
428
  // [
@@ -260,12 +430,15 @@ list.warnings;
260
430
  // { code: 'time-out-of-order', line: 8, severity: 'warning',
261
431
  // suggestion: { line: 8, raw: '01:11:02', seconds: 4262 } }
262
432
  // ]
433
+
263
434
  list.stats;
264
435
  // { lines: 9, tracks: 7, markers: 1, timed: 8, firstTime: 2900, lastTime: 4536 }
265
436
  ```
266
437
 
267
- `applyEdits` rewrites only the lines you name. Times keep their line's style, and the rest of the
268
- text, CRLF included, stays as it was:
438
+ The suggestion exists because exactly one time, one digit away from `00:11:02`, fits between its
439
+ neighbours. With several candidates or none, `suggestion` is `null`.
440
+
441
+ `applyEdits` takes an array of edit objects and only rewrites the lines they name:
269
442
 
270
443
  ```js
271
444
  const fix = list.warnings[1].suggestion;
@@ -274,20 +447,38 @@ const result = applyEdits(text, [
274
447
  { op: "shift", seconds: 45, from: 5 },
275
448
  ]);
276
449
 
277
- result.changed; // [ 5, 6, 7, 8, 9 ]
278
- result.warnings; // [ { code: 'style-upgraded', edit: 1, line: 5 } ]
279
- result.text.split("\r\n")[4]; // '01:00:16 Alix Perez - Fade Away' (was 59:31)
450
+ result.changed; // [ 5, 6, 7, 8, 9 ]
451
+ result.warnings; // [ { code: 'style-upgraded', edit: 1, line: 5 } ]
452
+ result.text.split("\r\n")[4]; // '01:00:16 Alix Perez - Fade Away' (was 59:31)
453
+
454
+ applyEdits(text, [{ op: "shift", seconds: -3000, lines: [2, 3] }]).warnings;
455
+ // [ { code: 'time-clamped', edit: 0, line: 2 } ]
280
456
  ```
281
457
 
282
- Edits: `set-time {line, seconds}`, `shift {seconds, lines? | from?}`
283
- (all timed lines when neither is given), `sort-by-time {}`, `set-rest {line, rest}`,
284
- `insert {after, text}` (`after: 0` inserts at the top), `delete {line}`.
285
- A bad edit changes nothing and warns `edit-invalid`; a time pushed below zero stops at zero and
286
- warns `time-clamped`.
458
+ A rewritten time keeps its line's style: `52:47` stays `MM:SS`, and a time that crosses the hour
459
+ gets hours like the rest of the list, with a `style-upgraded` warning. A time pushed below zero
460
+ stops at zero and warns `time-clamped`. A bad edit changes nothing and warns `edit-invalid`.
461
+
462
+ Both functions take the options `mode` (default `"tracklist"`), `knownArtists`, `splitAnd` and
463
+ `keywords`, as `parse` does, plus `audioSeconds` (the length of the audio, which turns on the
464
+ `time-after-audio-end` check) and `markerWords` (extra words that make a line a marker):
465
+
466
+ ```js
467
+ parseTracklist("00:00 Noisia - Stigma\n1:02:03 Mefjus - Suicide Bassline", { audioSeconds: 3600 })
468
+ .warnings;
469
+ // [ { code: 'time-after-audio-end', line: 2, severity: 'warning', suggestion: null } ]
470
+ ```
287
471
 
288
- Warning codes: `time-missing`, `time-out-of-order` (with a one- or two-digit `suggestion` when
289
- exactly one fix fits), `time-duplicate`, `no-start-at-zero`, `time-after-audio-end` (pass
290
- `audioSeconds`), `invalid-time`, `position-gap`, `position-duplicate`, `line-unparsed`.
472
+ | Result | Fields |
473
+ |---|---|
474
+ | `Tracklist` | `specVersion`, `entries`, `warnings`, `stats` |
475
+ | `TracklistEntry` | `line` (1-based), `kind` (`track`, `marker`, `header`, `link`, `note`, `blank`), `eol`, `raw`, `parts`, `time`, `track` (`ParsedTrack` or `null`), `layered` (the track text after any time starts with `w/`) |
476
+ | `EntryParts` | `lead`, `time`, `gap`, `rest`, joined they give `raw` |
477
+ | `TracklistWarning` | `code`, `line`, `severity` (`info` or `warning`), `suggestion` |
478
+ | `TracklistTime` | `raw`, `seconds` (`entry.time`, or `null` on untimed lines) |
479
+ | `TimeSuggestion` | `line`, `raw`, `seconds` (`warning.suggestion`, or `null`) |
480
+ | `TracklistStats` | `lines`, `tracks`, `markers`, `timed`, `firstTime`, `lastTime` |
481
+ | `EditResult` | `text`, `changed`, `warnings` (`EditWarning`: `code`, `edit`, `line`) |
291
482
 
292
483
  Lists pasted with list bullets or Markdown emphasis (`* 14:21 Artist - Title`,
293
484
  `**[**14:21**] Artist - Title**`) read like plain lines; the decoration stays in `raw` and `parts`.
@@ -296,29 +487,133 @@ previous one
296
487
  ([Markdown](https://4matic.github.io/trackparse/guides/tracklists/#markdown),
297
488
  [layered tracks](https://4matic.github.io/trackparse/guides/tracklists/#layered-tracks)).
298
489
 
299
- Options: `mode` (default `"tracklist"`), `knownArtists`, `splitAnd`, `keywords`, `audioSeconds`,
300
- `markerWords`. Every type is exported (`import type { Tracklist, Edit } from
301
- "trackparse/tracklist"`).
490
+ Warning codes: `time-missing`, `time-out-of-order`, `time-duplicate`, `no-start-at-zero`,
491
+ `time-after-audio-end` (needs `audioSeconds`), `invalid-time`, `position-gap`,
492
+ `position-duplicate`, `line-unparsed`. Edits: `set-time {line, seconds}`,
493
+ `shift {seconds, lines? | from?}` (all timed lines when neither is given), `sort-by-time {}`,
494
+ `set-rest {line, rest}`, `insert {after, text}` (`after: 0` inserts at the top), `delete {line}`.
495
+ The [Tracklist API reference](https://4matic.github.io/trackparse/reference/tracklist/) explains
496
+ each one, and the rules are in
497
+ [SPEC R13](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md#r13-tracklists).
498
+ `JSON.stringify(list)` matches
499
+ [`tracklist.schema.json`](https://github.com/4matic/trackparse/blob/main/spec/schema/tracklist.schema.json).
302
500
 
303
- Each line is parsed with `mode: "tracklist"`, where the first spaced dash always separates artist
304
- and title. `parse()` takes it too:
501
+ Every line is parsed with `mode: "tracklist"`, where the first spaced dash always separates artist
502
+ and title. `parse()` accepts it too:
305
503
 
306
504
  ```js
307
- parse("Electrosoul System - Liquid Metal").title; // 'Electrosoul System'
308
- parse("Electrosoul System - Liquid Metal", { mode: "tracklist" }).title; // 'Liquid Metal'
505
+ parse("Electrosoul System - Liquid Metal").junk;
506
+ // [ { raw: 'Liquid Metal', kind: 'genre' } ]
507
+ parse("Electrosoul System - Liquid Metal", { mode: "tracklist" }).title;
508
+ // 'Liquid Metal'
309
509
  ```
310
510
 
311
- Field tables and the full list of rules: the [tracklists guide](https://4matic.github.io/trackparse/guides/tracklists/), the
312
- [Tracklist API](https://4matic.github.io/trackparse/reference/tracklist/) and [SPEC R13](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md#r13-tracklists).
511
+ ## Errors
512
+
513
+ No function throws. `parse` returns a complete `ParsedTrack` for any string, `""` included, and a
514
+ value that isn't a string is read as `""`. An option of the wrong type or with an unknown value is
515
+ ignored and the default is used. `applyEdits` reports a bad edit as an `edit-invalid` warning and
516
+ leaves the text alone.
517
+
518
+ Recognition is done by hand-written scanners, so there is no regex backtracking. A
519
+ 10,000-code-point input finishes in under 100 ms.
520
+
521
+ ## TypeScript
522
+
523
+ **Docs:** [Types](https://4matic.github.io/trackparse/reference/parse/#types) · [Tracklist types](https://4matic.github.io/trackparse/reference/tracklist/#types)
524
+
525
+ The types ship with the package for both ESM and CommonJS. Nothing else needs installing.
526
+
527
+ ```ts
528
+ import type { Artist, ParsedTrack, ParseOptions, VersionType } from "trackparse";
529
+ import type { Edit, Tracklist, TracklistWarning } from "trackparse/tracklist";
530
+ ```
531
+
532
+ - `trackparse` exports `ParsedTrack`, `Artist`, `ArtistRole`, `ArtistSource`, `Version`,
533
+ `VersionType`, `VersionDelimiter`, `Junk`, `JunkKind`, `Flags`, `Position`, `Timestamp`,
534
+ `Warning`, `Mode`, `ModeOption`, `ParseOptions`, `KeywordOptions`, `FormatOptions` and `Parser`.
535
+ - `trackparse/tracklist` exports `Tracklist`, `TracklistEntry`, `EntryKind`, `EntryParts`, `Eol`,
536
+ `TracklistTime`, `TracklistWarning`, `TracklistWarningCode`, `TimeSuggestion`, `TracklistStats`,
537
+ `TracklistOptions`, `Edit`, `EditResult`, `EditWarning` and `EditWarningCode`.
538
+
539
+ Version types, junk kinds, warnings and edit operations are string literal unions, so the compiler
540
+ catches a misspelt name.
541
+
542
+ ## How it is tested
543
+
544
+ **Docs:** [How it's tested](https://4matic.github.io/trackparse/project/testing/)
545
+
546
+ - 13,130 shared fixtures: 1,203 written by hand and 11,927 generated from the rules. Every output
547
+ is also validated against the JSON Schema.
548
+ - Property tests with [fast-check](https://github.com/dubzzz/fast-check) for the spec's invariants:
549
+ no throw on any input, `normalize` is idempotent, whitespace and dash noise doesn't change the
550
+ result, and `parse(format(parse(s)))` round-trips.
551
+ - The published build is run against every fixture on Node 18 and 20, as ESM and as CommonJS.
552
+ - [`compare-ports.mjs`](https://github.com/4matic/trackparse/blob/main/scripts/compare-ports.mjs)
553
+ runs every fixture input through this package, the Python package and the Rust crate and diffs
554
+ the complete JSON output. The difference count is zero.
555
+
556
+ ## Versioning
557
+
558
+ **Changelog:** [js/CHANGELOG.md](https://github.com/4matic/trackparse/blob/main/js/CHANGELOG.md)
559
+
560
+ The package version is the spec version (`SPEC_VERSION`). While it is 0.x, a release that changes
561
+ the output for some input bumps the minor version, and a patch release only fixes bugs. If you
562
+ store parsed results, pin the minor version (`"trackparse": "~0.5.0"`) and read the changelog
563
+ before moving up.
564
+
565
+ ## Other languages
566
+
567
+ **Docs:** [Ports](https://4matic.github.io/trackparse/project/ports/)
568
+
569
+ The same parser exists for Python and Rust. All three implement one spec, pass the same fixtures,
570
+ are released together under one version number, and return identical JSON for every fixture input.
571
+ You can parse in one language and read the result in another.
572
+
573
+ | Language | Package | Install |
574
+ |---|---|---|
575
+ | JavaScript / TypeScript | [trackparse on npm](https://www.npmjs.com/package/trackparse) | `npm install trackparse` |
576
+ | Python | [trackparse on PyPI](https://pypi.org/project/trackparse/) | `pip install trackparse` |
577
+ | Rust | [trackparse on crates.io](https://crates.io/crates/trackparse) | `cargo add trackparse` |
313
578
 
314
579
  ## Limitations
315
580
 
316
581
  **Docs:** [Limitations and roadmap](https://4matic.github.io/trackparse/project/roadmap/)
317
582
 
318
- One track string per `parse()` call; tracklists go through `trackparse/tracklist`, which reads
319
- start times only (no durations, trailing times or ranges). No label/catalog, key or BPM fields, and
320
- no built-in artist database: pass `knownArtists` for names that contain a joiner.
583
+ - `parse()` takes one track string. Whole tracklists go through `trackparse/tracklist`, which reads
584
+ start times only: no durations, trailing times or time ranges.
585
+ - Catalog numbers, key and BPM are not fields. They stay in the title.
586
+ - There is no artist database. Pass `knownArtists` for names that contain a joiner
587
+ (`Earth, Wind & Fire`).
588
+
589
+ ## Development
590
+
591
+ From the root of the [repository](https://github.com/4matic/trackparse), with Node 22 or later and
592
+ [pnpm](https://pnpm.io/):
593
+
594
+ ```sh
595
+ pnpm install
596
+ pnpm --filter trackparse test # every shared fixture, properties, API
597
+ pnpm --filter trackparse typecheck
598
+ pnpm lint # Biome
599
+ pnpm --filter trackparse conformance # pass rate per fixture file and per rule
600
+ pnpm --filter trackparse build # js/dist, ESM and CommonJS
601
+ ```
602
+
603
+ The Python and Rust ports mirror this package's modules one to one. Behaviour changes start in the
604
+ spec, not here: see [AGENTS.md](https://github.com/4matic/trackparse/blob/main/AGENTS.md).
605
+
606
+ ## Links
607
+
608
+ - [Documentation](https://4matic.github.io/trackparse/), with every example in JavaScript, Python
609
+ and Rust, and a [playground](https://4matic.github.io/trackparse/playground/) that runs the
610
+ parser in your browser
611
+ - [Spec](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md)
612
+ - [Changelog](https://github.com/4matic/trackparse/blob/main/js/CHANGELOG.md)
613
+ - [Project README](https://github.com/4matic/trackparse#readme)
614
+ - [GitHub repository](https://github.com/4matic/trackparse)
615
+ - [PyPI package](https://pypi.org/project/trackparse/) · [Rust crate](https://crates.io/crates/trackparse)
321
616
 
322
617
  ## License
323
618
 
324
- MIT © Maksim Maksimov
619
+ [MIT](https://github.com/4matic/trackparse/blob/main/LICENSE)
@@ -1,5 +1,5 @@
1
1
  // src/data.generated.ts
2
- var SPEC_VERSION = "0.5.0";
2
+ var SPEC_VERSION = "0.5.1";
3
3
  var descriptorsData = {
4
4
  "descriptors": [
5
5
  "radio",
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  // src/data.generated.ts
4
- var SPEC_VERSION = "0.5.0";
4
+ var SPEC_VERSION = "0.5.1";
5
5
  var descriptorsData = {
6
6
  "descriptors": [
7
7
  "radio",
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { P as ParsedTrack, A as Artist, F as FormatOptions, a as ParseOptions } from './types-DqJSn18b.cjs';
2
2
  export { b as ArtistRole, c as ArtistSource, d as Flags, J as Junk, e as JunkKind, K as KeywordOptions, M as Mode, f as ModeOption, g as Position, T as Timestamp, V as Version, h as VersionDelimiter, i as VersionType, W as Warning } from './types-DqJSn18b.cjs';
3
3
 
4
- declare const SPEC_VERSION = "0.5.0";
4
+ declare const SPEC_VERSION = "0.5.1";
5
5
 
6
6
  declare function format(track: ParsedTrack, options?: FormatOptions): string;
7
7
  /** All credited names: track artists plus every version's remixers, deduped by R0.4 key. */
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { P as ParsedTrack, A as Artist, F as FormatOptions, a as ParseOptions } from './types-DqJSn18b.js';
2
2
  export { b as ArtistRole, c as ArtistSource, d as Flags, J as Junk, e as JunkKind, K as KeywordOptions, M as Mode, f as ModeOption, g as Position, T as Timestamp, V as Version, h as VersionDelimiter, i as VersionType, W as Warning } from './types-DqJSn18b.js';
3
3
 
4
- declare const SPEC_VERSION = "0.5.0";
4
+ declare const SPEC_VERSION = "0.5.1";
5
5
 
6
6
  declare function format(track: ParsedTrack, options?: FormatOptions): string;
7
7
  /** All credited names: track artists plus every version's remixers, deduped by R0.4 key. */
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { getDefaultTables, renderVersion, dedupKey, wordMatches } from './chunk-JHK5DWK5.js';
2
- export { SPEC_VERSION, createParser, normalize, parse, parseArtists } from './chunk-JHK5DWK5.js';
1
+ import { getDefaultTables, renderVersion, dedupKey, wordMatches } from './chunk-F7PBCPFT.js';
2
+ export { SPEC_VERSION, createParser, normalize, parse, parseArtists } from './chunk-F7PBCPFT.js';
3
3
 
4
4
  // src/format.ts
5
5
  function resolve(o) {
@@ -709,7 +709,7 @@ function applyEdits(text, edits, options) {
709
709
  }
710
710
 
711
711
  // src/data.generated.ts
712
- var SPEC_VERSION = "0.5.0";
712
+ var SPEC_VERSION = "0.5.1";
713
713
  var descriptorsData = {
714
714
  "descriptors": [
715
715
  "radio",
package/dist/tracklist.js CHANGED
@@ -1,4 +1,4 @@
1
- import { headerWordsData, layeredWordsData, createListParser, SPEC_VERSION, stringList, PhraseTable, markerWordsData, isWhitespace, isDigit, digitRun, inVocab, asciiLower, splitWords } from './chunk-JHK5DWK5.js';
1
+ import { headerWordsData, layeredWordsData, createListParser, SPEC_VERSION, stringList, PhraseTable, markerWordsData, isWhitespace, isDigit, digitRun, inVocab, asciiLower, splitWords } from './chunk-F7PBCPFT.js';
2
2
 
3
3
  // src/tracklist/split.ts
4
4
  function splitLines(text) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackparse",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Spec-driven parser for music track strings: artists, feat credits, remixes/versions, junk and more from single track strings (store titles, YouTube titles, filenames) and multi-line tracklists.",
5
5
  "license": "MIT",
6
6
  "author": "Maksim Maksimov",