trackparse 0.1.0

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 ADDED
@@ -0,0 +1,211 @@
1
+ # trackparse
2
+
3
+ Parse music track names into structured metadata: artists, featured artists, remixers, version
4
+ (remix, extended, live, remaster), producers and a clean title.
5
+
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)
9
+
10
+ This is the JavaScript/TypeScript reference implementation of the
11
+ [trackparse spec](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md). It passes all
12
+ 15,000+ shared fixtures. Zero dependencies, ESM and CommonJS, TypeScript types included, never
13
+ throws.
14
+
15
+ The principle is **extract, don't delete**: everything recognised lands in a field (artists with
16
+ roles, versions with a type and their own credits, junk with a kind, flags, position), and
17
+ everything else stays in the title verbatim.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install trackparse
23
+ # or: pnpm add trackparse / yarn add trackparse / bun add trackparse
24
+ ```
25
+
26
+ Node 18 or later. No Node-specific APIs are used, so it also runs in browsers and other runtimes.
27
+
28
+ ## Quick start
29
+
30
+ ```js
31
+ import { parse } from "trackparse";
32
+ // const { parse } = require("trackparse");
33
+
34
+ const track = parse(
35
+ "01. Wilkinson ft. Becky Hill & Tom Cane - Afterglow (Sub Focus VIP Remix) [Official Video]",
36
+ );
37
+
38
+ track.artists.map((a) => `${a.name} (${a.role})`);
39
+ // [ 'Wilkinson (primary)', 'Becky Hill (featured)', 'Tom Cane (featured)' ]
40
+ track.title; // 'Afterglow'
41
+ track.fullTitle; // 'Afterglow (Sub Focus VIP Remix)'
42
+ track.versions[0].type; // 'remix'
43
+ track.versions[0].artists[0].name; // 'Sub Focus'
44
+ track.versions[0].modifiers; // [ 'vip' ]
45
+ track.junk; // [ { raw: 'Official Video', kind: 'video' } ]
46
+ track.position; // { raw: '01', number: 1 }
47
+ track.mode; // 'youtube' (auto-detected from the junk group)
48
+ ```
49
+
50
+ ## API
51
+
52
+ ### `parse(input, options?) → ParsedTrack`
53
+
54
+ Parses one track string. Every key of the result is always present.
55
+
56
+ ### `parseArtists(input, options?) → Artist[]`
57
+
58
+ Splits an artist string (no title) into credits.
59
+
60
+ ```js
61
+ parseArtists("Sub Focus, Wilkinson & Dimension feat. Kojo");
62
+ // [
63
+ // { name: 'Sub Focus', role: 'primary', joiner: null, source: 'artist' },
64
+ // { name: 'Wilkinson', role: 'primary', joiner: ',', source: 'artist' },
65
+ // { name: 'Dimension', role: 'primary', joiner: '&', source: 'artist' },
66
+ // { name: 'Kojo', role: 'featured', joiner: 'feat.', source: 'artist' }
67
+ // ]
68
+ ```
69
+
70
+ ### `format(track, options?) → string`
71
+
72
+ Renders a `ParsedTrack` back to `Artists - Title (versions) (prod. ...)`.
73
+
74
+ ```js
75
+ const t = parse("Wilkinson ft. Becky Hill - Afterglow (Sub Focus Remix)");
76
+
77
+ format(t); // 'Wilkinson ft. Becky Hill - Afterglow (Sub Focus Remix)'
78
+ format(t, { feat: "title", joiners: "canonical" }); // 'Wilkinson - Afterglow (feat. Becky Hill) (Sub Focus Remix)'
79
+ format(t, { feat: "omit", versions: "none" }); // 'Wilkinson - Afterglow'
80
+ ```
81
+
82
+ | Option | Default | Effect |
83
+ |---|---|---|
84
+ | `feat` | `"source"` | `"source"` (where they were found), `"artist"`, `"title"` (`(feat. X)` after the title) or `"omit"`. |
85
+ | `featMarker` | – | Override the feat marker (`"featuring"`). |
86
+ | `joiners` | `"original"` | `"canonical"`: `and` → `&`, `vs` → `vs.`, feat markers → `feat.`. |
87
+ | `versions` | `"all"` | `"none"` drops versions. |
88
+ | `producers` | `true` | Append `(prod. by X)`. |
89
+ | `position` | `false` | Prefix the track number (`01. `). |
90
+
91
+ ### `normalize(input) → string`
92
+
93
+ The Unicode cleanup applied before parsing: NFC, zero-width spaces, BOM and soft hyphens removed, fullwidth
94
+ brackets, smart quotes and dash variants mapped to ASCII, whitespace collapsed. Idempotent.
95
+
96
+ ```js
97
+ normalize(" Artist​ — Title (Remix) “Quoted” ");
98
+ // 'Artist - Title (Remix) "Quoted"'
99
+ ```
100
+
101
+ ### `allArtists(track) → Artist[]`
102
+
103
+ Track credits plus every version's remixers, deduplicated.
104
+
105
+ ```js
106
+ allArtists(parse("Camo & Krooked - Kallisto (Noisia Remix)", { knownArtists: ["Camo & Krooked"] }))
107
+ .map((a) => `${a.name} (${a.role})`);
108
+ // [ 'Camo & Krooked (primary)', 'Noisia (remixer)' ]
109
+ ```
110
+
111
+ ### `createParser(options) → { parse, parseArtists }`
112
+
113
+ Compiles options, including extra vocabulary, once. Per-call options are merged over the base.
114
+
115
+ ```js
116
+ const parser = createParser({
117
+ knownArtists: ["Chase & Status", "Camo & Krooked"],
118
+ keywords: { versionHeads: { rerub: "rework" }, junk: { "full album": "other" } },
119
+ });
120
+
121
+ const r = parser.parse("Chase & Status x Camo & Krooked - Track (Alix Perez Rerub) [Full Album]");
122
+ r.artists.map((a) => a.name); // [ 'Chase & Status', 'Camo & Krooked' ]
123
+ r.versions.map((v) => `${v.type} by ${v.artists.map((a) => a.name).join(", ")}`);
124
+ // [ 'rework by Alix Perez' ]
125
+ r.junk; // [ { raw: 'Full Album', kind: 'other' } ]
126
+ ```
127
+
128
+ ### `SPEC_VERSION`
129
+
130
+ The spec version this build implements (`"0.1.0"`).
131
+
132
+ ## Options
133
+
134
+ | Option | Default | Effect |
135
+ |---|---|---|
136
+ | `mode` | `"auto"` | `"clean"` (tags, store metadata), `"youtube"` (video titles), `"filename"`, or `"auto"` to detect. |
137
+ | `uploader` | – | YouTube channel, used as the artist when no separator is found. ` - Topic` and `VEVO` are stripped. |
138
+ | `knownArtists` | `[]` | Names never split (`Chase & Status`). There is no built-in list. |
139
+ | `splitAnd` | `"auto"` | `and` splits only after a comma in the same list (`A, B and C`), so `Simon and Garfunkel` stays one. `"always"` / `"never"`. |
140
+ | `keywords` | – | Extra vocabulary: `versionHeads`, `descriptors`, `genres`, `junk`, `featMarkers`. |
141
+
142
+ ```js
143
+ parse("Chase & Status - Blind Faith (Loadstar Remix)").artists.map((a) => a.name);
144
+ // [ 'Chase', 'Status' ]
145
+ parse("Chase & Status - Blind Faith (Loadstar Remix)", { knownArtists: ["Chase & Status"] })
146
+ .artists.map((a) => a.name);
147
+ // [ 'Chase & Status' ]
148
+
149
+ parse("Jay-Z-Numb").title; // 'Jay-Z-Numb' (clean: no split)
150
+ parse("Jay-Z-Numb", { mode: "youtube" }).artists.map((a) => a.name); // [ 'Jay-Z' ]
151
+ ```
152
+
153
+ ## Output types
154
+
155
+ All types are exported (`import type { ParsedTrack } from "trackparse"`).
156
+
157
+ ```ts
158
+ interface ParsedTrack {
159
+ input: string; // original, untouched
160
+ mode: "clean" | "youtube" | "filename"; // resolved mode
161
+ position: { raw: string; number: number } | null;
162
+ timestamp: { raw: string; seconds: number } | null;
163
+ artists: Artist[]; // primary, featured, producer (not remixers)
164
+ title: string; // clean title
165
+ fullTitle: string; // title + versions with their brackets
166
+ versions: Version[];
167
+ year: number | null;
168
+ flags: { explicit: boolean; clean: boolean; unknownArtist: boolean; unknownTitle: boolean };
169
+ junk: { raw: string; kind: JunkKind }[];
170
+ warnings: Warning[];
171
+ }
172
+
173
+ interface Artist {
174
+ name: string;
175
+ role: "primary" | "featured" | "remixer" | "producer";
176
+ joiner: string | null; // "&", ",", "x", "ft.", "prod. by"... as written
177
+ source: "artist" | "title" | "version";
178
+ }
179
+
180
+ interface Version {
181
+ type: VersionType; // remix, vip, bootleg, extended, radio, live, remaster...
182
+ raw: string; // "Sub Focus VIP Remix"
183
+ artists: Artist[]; // remixers
184
+ modifiers: string[]; // ["vip"]
185
+ descriptor: string | null; // "Taylor's", "at Wembley", genres
186
+ year: number | null;
187
+ unknownArtist: boolean; // "ID Remix"
188
+ delimiter: "(" | "[" | "{" | "-";
189
+ }
190
+ ```
191
+
192
+ - `VersionType`: `remix`, `bootleg`, `vip`, `edit`, `flip`, `refix`, `rework`, `mashup`, `blend`,
193
+ `dub`, `mix`, `extended`, `radio`, `club`, `original`, `instrumental`, `acapella`, `live`,
194
+ `acoustic`, `remaster`, `demo`, `reprise`, `cover`, `version`, `spedUp`, `slowed`, `nightcore`.
195
+ - `JunkKind`: `video`, `audio`, `lyrics`, `quality`, `promo`, `platform`, `label`, `genre`, `other`.
196
+ - `Warning`: `noSeparator`, `ambiguousSeparator`, `unspacedDashSplit`, `asymmetricDashSplit`,
197
+ `bySplit`, `quotedTitleSplit`, `ambiguousMixCredit`, `unbalancedBrackets`.
198
+
199
+ The full field reference, a gallery of examples and the comparison with other libraries are in the
200
+ [main README](https://github.com/4matic/trackparse#readme). Exact behaviour is defined by the
201
+ [spec](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md) and its
202
+ [fixtures](https://github.com/4matic/trackparse/tree/main/spec/fixtures).
203
+
204
+ ## Limitations
205
+
206
+ One track string per call (no multi-line tracklists yet), no label/catalog, key or BPM fields, and
207
+ no built-in artist database: pass `knownArtists` for names that contain a joiner.
208
+
209
+ ## License
210
+
211
+ MIT © Maksim Maksimov