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 +211 -0
- package/dist/index.cjs +2269 -0
- package/dist/index.d.cts +104 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +2261 -0
- package/package.json +73 -0
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
|
+
[](https://www.npmjs.com/package/trackparse)
|
|
7
|
+
[](https://github.com/4matic/trackparse/actions/workflows/js.yml)
|
|
8
|
+
[](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
|