trackparse 0.5.0__tar.gz → 0.5.1__tar.gz
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.
- {trackparse-0.5.0 → trackparse-0.5.1}/PKG-INFO +164 -47
- {trackparse-0.5.0 → trackparse-0.5.1}/README.md +163 -46
- {trackparse-0.5.0 → trackparse-0.5.1}/pyproject.toml +1 -1
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/__init__.py +1 -1
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_data.py +1 -1
- {trackparse-0.5.0 → trackparse-0.5.1}/.gitignore +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/LICENSE +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_artists.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_assemble.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_classify.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_context.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_credits.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_digits.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_format.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_mode.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_normalize.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_parser.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_prefix.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_scanner.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_separator.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_tables.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_title.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_tracklist_edits.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_tracklist_emphasis.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_tracklist_kinds.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_tracklist_options.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_tracklist_parse.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_tracklist_positions.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_tracklist_split.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_tracklist_times.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_tracklist_types.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_tracklist_warnings.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/_words.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/options.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/py.typed +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/tracklist.py +0 -0
- {trackparse-0.5.0 → trackparse-0.5.1}/src/trackparse/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: trackparse
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Spec-driven parser for music track titles and tracklists: artists, feat, remixes, versions, junk, positions, times.
|
|
5
5
|
Project-URL: Homepage, https://4matic.github.io/trackparse/
|
|
6
6
|
Project-URL: Documentation, https://4matic.github.io/trackparse/start/getting-started/
|
|
@@ -33,9 +33,9 @@ Description-Content-Type: text/markdown
|
|
|
33
33
|
|
|
34
34
|
# trackparse
|
|
35
35
|
|
|
36
|
-
[](https://pypi.org/project/trackparse/)
|
|
37
37
|
[](https://pypi.org/project/trackparse/)
|
|
38
|
-
[](https://github.com/4matic/trackparse/actions/workflows/python.yml)
|
|
39
39
|
[](https://github.com/4matic/trackparse/blob/main/LICENSE)
|
|
40
40
|
|
|
41
41
|
**[Documentation](https://4matic.github.io/trackparse/)** · **[Getting started](https://4matic.github.io/trackparse/start/getting-started/)** · **[Playground](https://4matic.github.io/trackparse/playground/)**
|
|
@@ -49,18 +49,18 @@ what's just noise.**
|
|
|
49
49
|
|
|
50
50
|
Most title parsers split that into `"Wilkinson ft. Becky Hill & Tom Cane"` and
|
|
51
51
|
`"Afterglow (Sub Focus VIP Remix)"` and stop. Others "clean" it and throw the feat and the remix
|
|
52
|
-
away. trackparse keeps all of it. Everything it
|
|
53
|
-
doesn't
|
|
52
|
+
away. trackparse keeps all of it. Everything it recognizes goes into a field, and anything it
|
|
53
|
+
doesn't recognize stays in the title, word for word.
|
|
54
54
|
|
|
55
55
|
This is the Python port of [trackparse](https://github.com/4matic/trackparse). Its behaviour is
|
|
56
56
|
defined by a [shared spec](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md) and pinned
|
|
57
57
|
by 13,130 fixtures. It passes all of them, and for every fixture input its output is byte-identical
|
|
58
|
-
to the JavaScript
|
|
58
|
+
to the [JavaScript and Rust packages](#other-languages).
|
|
59
59
|
|
|
60
60
|
- Python 3.9 to 3.14
|
|
61
61
|
- No dependencies
|
|
62
62
|
- Typed (`py.typed`), immutable results
|
|
63
|
-
- Never raises on
|
|
63
|
+
- Never raises on `str` input
|
|
64
64
|
|
|
65
65
|
## Install
|
|
66
66
|
|
|
@@ -156,6 +156,11 @@ normalize(s) -> str
|
|
|
156
156
|
all_artists(track) -> list[Artist]
|
|
157
157
|
create_parser(**options) -> Parser # Parser.parse(...), Parser.parse_artists(...)
|
|
158
158
|
SPEC_VERSION, __version__
|
|
159
|
+
|
|
160
|
+
# from trackparse.tracklist
|
|
161
|
+
parse_tracklist(text, *, mode="tracklist", known_artists=(), keywords=None, split_and="auto",
|
|
162
|
+
audio_seconds=None, marker_words=()) -> Tracklist
|
|
163
|
+
apply_edits(text, edits, *, <same options>) -> EditResult
|
|
159
164
|
```
|
|
160
165
|
|
|
161
166
|
### `parse(input, **options)`
|
|
@@ -164,7 +169,7 @@ SPEC_VERSION, __version__
|
|
|
164
169
|
|
|
165
170
|
| Option | Default | |
|
|
166
171
|
|---|---|---|
|
|
167
|
-
| `mode` | `"auto"` | `"clean"`, `"youtube"`, `"filename"`, `"tracklist"`, or `"auto"` to detect from the string |
|
|
172
|
+
| `mode` | `"auto"` | `"clean"`, `"youtube"`, `"filename"`, `"tracklist"`, or `"auto"` to detect from the string. See [Modes](#modes) |
|
|
168
173
|
| `uploader` | `None` | YouTube channel name, used as the artist when there's no separator (`- Topic` and `VEVO` are dropped) |
|
|
169
174
|
| `known_artists` | `()` | Names that are never split. There is no built-in list |
|
|
170
175
|
| `split_and` | `"auto"` | `"auto"` splits on `and` only after a comma (`A, B and C`), so `Simon and Garfunkel` stays whole. Also `"always"`, `"never"` |
|
|
@@ -197,9 +202,6 @@ parse("Artist - Song [Some Label Rip]", keywords={"junk": {"some label rip": "la
|
|
|
197
202
|
# (Junk(raw='Some Label Rip', kind='label'),)
|
|
198
203
|
```
|
|
199
204
|
|
|
200
|
-
Parsing is total: any `str` gives a `ParsedTrack`, including `""`. Bad option values fall back to
|
|
201
|
-
the defaults. Passing something that isn't a `str` raises `TypeError`.
|
|
202
|
-
|
|
203
205
|
### `ParsedTrack`
|
|
204
206
|
|
|
205
207
|
**Docs:** [ParsedTrack fields](https://4matic.github.io/trackparse/reference/parse/#parsedtrack)
|
|
@@ -211,7 +213,7 @@ the defaults. Passing something that isn't a `str` raises `TypeError`.
|
|
|
211
213
|
| `position` | `Position \| None` | Track number: `Position(raw='01', number=1)` |
|
|
212
214
|
| `timestamp` | `Timestamp \| None` | Leading cue time: `[12:34]` gives `Timestamp(raw='12:34', seconds=754)` |
|
|
213
215
|
| `artists` | `tuple[Artist, ...]` | Primary, featured and producer credits. Remixers live in `versions` |
|
|
214
|
-
| `title` | `str` | Clean title. Brackets it doesn't
|
|
216
|
+
| `title` | `str` | Clean title. Brackets it doesn't recognize stay in |
|
|
215
217
|
| `full_title` | `str` | `title` with the versions re-attached |
|
|
216
218
|
| `versions` | `tuple[Version, ...]` | Remixes and other versions, left to right |
|
|
217
219
|
| `year` | `int \| None` | From `(2015)` or `- 2015` |
|
|
@@ -239,14 +241,17 @@ parse("Queen - Bohemian Rhapsody (Live at Wembley 1986)").versions[0]
|
|
|
239
241
|
# descriptor='at Wembley', year=1986, unknown_artist=False, delimiter='(')
|
|
240
242
|
```
|
|
241
243
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
`
|
|
244
|
+
- `VersionType`: `remix`, `bootleg`, `vip`, `edit`, `flip`, `refix`, `rework`, `mashup`, `blend`,
|
|
245
|
+
`dub`, `mix`, `extended`, `radio`, `club`, `original`, `instrumental`, `acapella`, `live`,
|
|
246
|
+
`acoustic`, `remaster`, `demo`, `reprise`, `cover`, `version`, `spedUp`, `slowed`, `nightcore`.
|
|
247
|
+
- `JunkKind`: `video`, `audio`, `lyrics`, `quality`, `promo`, `platform`, `label`, `genre`, `other`.
|
|
248
|
+
- `Warning`: see [Warnings](#warnings).
|
|
249
|
+
|
|
250
|
+
These are plain strings, typed as `Literal` aliases (see [Typing](#typing)).
|
|
246
251
|
|
|
247
252
|
### `to_dict()` and `from_dict()`
|
|
248
253
|
|
|
249
|
-
**Docs:** [
|
|
254
|
+
**Docs:** [Serializing to JSON](https://4matic.github.io/trackparse/reference/parse/#serializing-to-json)
|
|
250
255
|
|
|
251
256
|
Every result class has `to_dict()`, which returns plain dicts and lists matching
|
|
252
257
|
[`parsed-track.schema.json`](https://github.com/4matic/trackparse/blob/main/spec/schema/parsed-track.schema.json)
|
|
@@ -255,14 +260,16 @@ ensure_ascii=False)` the bytes are the same as `JSON.stringify(parse(s))` in JS.
|
|
|
255
260
|
(`ParsedTrack`, `Artist`, ...) also have a `from_dict()` classmethod that goes the other way:
|
|
256
261
|
|
|
257
262
|
```python
|
|
263
|
+
from trackparse import ParsedTrack
|
|
264
|
+
|
|
258
265
|
ParsedTrack.from_dict(track.to_dict()) == track # True
|
|
259
266
|
```
|
|
260
267
|
|
|
261
268
|
### `parse_artists(input, **options)`
|
|
262
269
|
|
|
263
|
-
**Docs:** [parseArtists](https://4matic.github.io/trackparse/reference/functions/#parseartistsinput-options)
|
|
270
|
+
**Docs:** [parseArtists](https://4matic.github.io/trackparse/reference/functions/#parseartistsinput-options) · [Artists and credits](https://4matic.github.io/trackparse/guides/artists/)
|
|
264
271
|
|
|
265
|
-
Splits a bare artist string. Takes the same options as `parse`.
|
|
272
|
+
Splits a bare artist string (no title). Takes the same options as `parse`.
|
|
266
273
|
|
|
267
274
|
```python
|
|
268
275
|
from trackparse import parse_artists
|
|
@@ -279,12 +286,12 @@ Renders a track back to a string.
|
|
|
279
286
|
|
|
280
287
|
| Option | Default | |
|
|
281
288
|
|---|---|---|
|
|
282
|
-
| `feat` | `"source"` | Where featured artists go: `"source"` (where they were), `"artist"`, `"title"` or `"omit"` |
|
|
283
|
-
| `feat_marker` | `None` | Marker for featured artists, such as `"
|
|
284
|
-
| `joiners` | `"original"` | `"original"` keeps the joiners as written
|
|
289
|
+
| `feat` | `"source"` | Where featured artists go: `"source"` (where they were), `"artist"`, `"title"` (`(feat. X)` after the title) or `"omit"` |
|
|
290
|
+
| `feat_marker` | `None` | Marker for featured artists, such as `"featuring"` |
|
|
291
|
+
| `joiners` | `"original"` | `"original"` keeps the joiners as written. `"canonical"` writes `and` as `&`, `vs` as `vs.` and every feat marker as `feat.` |
|
|
285
292
|
| `versions` | `"all"` | `"all"` or `"none"` |
|
|
286
293
|
| `producers` | `True` | Include `(prod. ...)` credits |
|
|
287
|
-
| `position` | `False` | Include the track number |
|
|
294
|
+
| `position` | `False` | Include the track number (`01. `) |
|
|
288
295
|
|
|
289
296
|
```python
|
|
290
297
|
from trackparse import format_track
|
|
@@ -306,13 +313,14 @@ format_track(track, position=True)
|
|
|
306
313
|
|
|
307
314
|
**Docs:** [normalize](https://4matic.github.io/trackparse/reference/functions/#normalizeinput)
|
|
308
315
|
|
|
309
|
-
The Unicode cleanup every function runs first: NFC,
|
|
310
|
-
|
|
316
|
+
The Unicode cleanup every function runs first: NFC, zero-width spaces, BOM and soft hyphens
|
|
317
|
+
removed, fullwidth brackets, smart quotes and dash variants mapped to ASCII, whitespace collapsed.
|
|
318
|
+
Running it twice changes nothing.
|
|
311
319
|
|
|
312
320
|
```python
|
|
313
321
|
from trackparse import normalize
|
|
314
322
|
|
|
315
|
-
normalize("Noisia
|
|
323
|
+
normalize("Noisia\u200b – Stigma (VIP)")
|
|
316
324
|
# 'Noisia - Stigma (VIP)'
|
|
317
325
|
```
|
|
318
326
|
|
|
@@ -350,8 +358,9 @@ parser.parse("Chase & Status x Camo & Krooked - Track (Alix Perez Rerub)").versi
|
|
|
350
358
|
|
|
351
359
|
### `SPEC_VERSION`
|
|
352
360
|
|
|
353
|
-
The spec version this build implements.
|
|
354
|
-
released together and always share
|
|
361
|
+
The spec version this build implements, `"0.5.0"` in this release. It equals `__version__`: the
|
|
362
|
+
PyPI package, the npm package, the Rust crate and the spec are released together and always share
|
|
363
|
+
one version number.
|
|
355
364
|
|
|
356
365
|
## Modes
|
|
357
366
|
|
|
@@ -378,9 +387,39 @@ parse("Shelter", mode="youtube", uploader="Porter Robinson").artists
|
|
|
378
387
|
# (Artist(name='Porter Robinson', role='primary', joiner=None, source='artist'),)
|
|
379
388
|
```
|
|
380
389
|
|
|
390
|
+
## Warnings
|
|
391
|
+
|
|
392
|
+
**Docs:** [Warnings](https://4matic.github.io/trackparse/reference/warnings/)
|
|
393
|
+
|
|
394
|
+
The parser never fails, but it says when it had to guess. `track.warnings` is a tuple of strings,
|
|
395
|
+
empty when every decision was unambiguous.
|
|
396
|
+
|
|
397
|
+
| Warning | Means |
|
|
398
|
+
|---|---|
|
|
399
|
+
| `noSeparator` | No artist/title separator was found. The whole string is the title |
|
|
400
|
+
| `ambiguousSeparator` | A two-part pipe title with no dash was read as artist and title |
|
|
401
|
+
| `unspacedDashSplit` | Split at a dash without spaces around it |
|
|
402
|
+
| `asymmetricDashSplit` | Split at a dash with a space on one side only |
|
|
403
|
+
| `bySplit` | Read `"Title" by Artist` |
|
|
404
|
+
| `quotedTitleSplit` | Took a quoted span as the title and the text before it as the artist |
|
|
405
|
+
| `ambiguousMixCredit` | `<Name> Mix` was read as a remix, but the name is long enough to be a style |
|
|
406
|
+
| `unbalancedBrackets` | A bracket was opened and never closed |
|
|
407
|
+
|
|
408
|
+
```python
|
|
409
|
+
t = parse("Around the World")
|
|
410
|
+
t.artists, t.title, t.warnings
|
|
411
|
+
# ((), 'Around the World', ('noSeparator',))
|
|
412
|
+
|
|
413
|
+
parse("Noisia - Stigma (VIP").warnings
|
|
414
|
+
# ('unbalancedBrackets',)
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Tracklists have their own warnings, with line numbers. They are listed under
|
|
418
|
+
[Tracklists](#tracklists).
|
|
419
|
+
|
|
381
420
|
## Tracklists
|
|
382
421
|
|
|
383
|
-
**Docs:** [Tracklists](https://4matic.github.io/trackparse/guides/tracklists/) · [Editing tracklists](https://4matic.github.io/trackparse/guides/editing-tracklists/)
|
|
422
|
+
**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)
|
|
384
423
|
|
|
385
424
|
`trackparse.tracklist` parses a whole tracklist, one entry per line, and checks it. Each entry
|
|
386
425
|
keeps its line exactly as written plus its own line ending, so `"".join(e.raw + e.eol for e in
|
|
@@ -449,13 +488,19 @@ apply_edits(text, [{"op": "shift", "seconds": -3000, "lines": [2, 3]}]).warnings
|
|
|
449
488
|
```
|
|
450
489
|
|
|
451
490
|
A rewritten time keeps its line's style: `52:47` stays `MM:SS`, and a time that crosses the hour
|
|
452
|
-
gets hours like the rest of the list, with a `style-upgraded` warning. A
|
|
453
|
-
and warns `
|
|
491
|
+
gets hours like the rest of the list, with a `style-upgraded` warning. A time pushed below zero
|
|
492
|
+
stops at zero and warns `time-clamped`. A bad edit changes nothing and warns `edit-invalid`.
|
|
493
|
+
|
|
494
|
+
Both functions take the keyword options `mode` (default `"tracklist"`), `known_artists`,
|
|
495
|
+
`split_and` and `keywords`, as `parse` does, plus `audio_seconds` (the length of the audio, which
|
|
496
|
+
turns on the `time-after-audio-end` check) and `marker_words` (extra words that make a line a
|
|
497
|
+
marker):
|
|
454
498
|
|
|
455
499
|
```python
|
|
456
|
-
parse_tracklist(
|
|
457
|
-
|
|
458
|
-
|
|
500
|
+
parse_tracklist(
|
|
501
|
+
"00:00 Noisia - Stigma\n1:02:03 Mefjus - Suicide Bassline", audio_seconds=3600
|
|
502
|
+
).warnings
|
|
503
|
+
# (TracklistWarning(code='time-after-audio-end', line=2, severity='warning', suggestion=None),)
|
|
459
504
|
```
|
|
460
505
|
|
|
461
506
|
| Result | Fields |
|
|
@@ -481,7 +526,9 @@ Warning codes: `time-missing`, `time-out-of-order`, `time-duplicate`, `no-start-
|
|
|
481
526
|
`position-duplicate`, `line-unparsed`. Edits: `set-time {line, seconds}`, `shift {seconds, lines? | from?}`
|
|
482
527
|
(all timed lines when neither is given), `sort-by-time {}`, `set-rest {line, rest}`,
|
|
483
528
|
`insert {after, text}` (`after: 0` inserts at the top), `delete {line}`. The
|
|
484
|
-
[Tracklist API reference](https://4matic.github.io/trackparse/reference/tracklist/) explains each one
|
|
529
|
+
[Tracklist API reference](https://4matic.github.io/trackparse/reference/tracklist/) explains each one,
|
|
530
|
+
and the rules are in
|
|
531
|
+
[SPEC R13](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md#r13-tracklists).
|
|
485
532
|
`to_dict()` gives the camelCase JSON from
|
|
486
533
|
[`tracklist.schema.json`](https://github.com/4matic/trackparse/blob/main/spec/schema/tracklist.schema.json),
|
|
487
534
|
byte-identical to the JS package's output.
|
|
@@ -496,17 +543,86 @@ parse("Electrosoul System - Liquid Metal", mode="tracklist").title
|
|
|
496
543
|
# 'Liquid Metal'
|
|
497
544
|
```
|
|
498
545
|
|
|
546
|
+
## Errors
|
|
547
|
+
|
|
548
|
+
Parsing is total: any `str` gives a complete `ParsedTrack`, `""` included, and `parse_tracklist`
|
|
549
|
+
and `apply_edits` return for any text. An option of the wrong type or with an unknown value is
|
|
550
|
+
ignored and the default is used. `apply_edits` reports a bad edit as an `edit-invalid` warning and
|
|
551
|
+
leaves the text alone.
|
|
552
|
+
|
|
553
|
+
The one exception is the input itself: passing something that isn't a `str` raises `TypeError`.
|
|
554
|
+
|
|
555
|
+
```python
|
|
556
|
+
parse(None)
|
|
557
|
+
# TypeError: parse() expects str input, got NoneType
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
Recognition is done by hand-written scanners, so there is no regex backtracking. A
|
|
561
|
+
10,000-code-point input parses in well under a second, and the test suite enforces a 1 s ceiling.
|
|
562
|
+
|
|
499
563
|
## Typing
|
|
500
564
|
|
|
501
|
-
**Docs:** [Types](https://4matic.github.io/trackparse/reference/parse/#types)
|
|
565
|
+
**Docs:** [Types](https://4matic.github.io/trackparse/reference/parse/#types) · [Tracklist types](https://4matic.github.io/trackparse/reference/tracklist/#types)
|
|
566
|
+
|
|
567
|
+
The package ships `py.typed` and passes `mypy --strict`.
|
|
568
|
+
|
|
569
|
+
- `ParsedTrack`, `Artist`, `Version`, `Junk`, `Flags`, `Position` and `Timestamp` are frozen
|
|
570
|
+
dataclasses with snake_case attributes, and so are the tracklist results in
|
|
571
|
+
`trackparse.tracklist`. Sequences are tuples, so results are immutable and hashable: they can go
|
|
572
|
+
in sets or be used as dict keys.
|
|
573
|
+
- Version types, junk kinds, warnings, roles and modes are plain strings, typed as `Literal`
|
|
574
|
+
aliases: `VersionType`, `JunkKind`, `Warning`, `ArtistRole`, `ArtistSource`, `Mode`,
|
|
575
|
+
`ModeOption`, `VersionDelimiter`. `trackparse.tracklist` adds `EntryKind`, `Eol`, `Severity`,
|
|
576
|
+
`TracklistWarningCode` and `EditWarningCode`.
|
|
577
|
+
- Option dicts have `TypedDict` types (`ParseOptions`, `KeywordOptions`, `FormatOptions`).
|
|
578
|
+
Tracklist edits are plain dicts.
|
|
579
|
+
|
|
580
|
+
## How it is tested
|
|
581
|
+
|
|
582
|
+
**Docs:** [How it's tested](https://4matic.github.io/trackparse/project/testing/)
|
|
583
|
+
|
|
584
|
+
- 13,130 shared fixtures: 1,203 written by hand and 11,927 generated from the rules. Every output
|
|
585
|
+
is also validated against the JSON Schema.
|
|
586
|
+
- Property tests with [Hypothesis](https://hypothesis.works) for the spec's invariants: no
|
|
587
|
+
exception on any string, `normalize` is idempotent, whitespace and dash noise doesn't change
|
|
588
|
+
the result, and `parse(format_track(parse(s)))` round-trips.
|
|
589
|
+
- CI runs the suite on Python 3.9 to 3.14, and runs the fixtures again against the built wheel.
|
|
590
|
+
- [`compare-ports.mjs`](https://github.com/4matic/trackparse/blob/main/scripts/compare-ports.mjs)
|
|
591
|
+
runs every fixture input through the JavaScript package, this package and the Rust crate and
|
|
592
|
+
diffs the complete JSON output. The difference count is zero.
|
|
593
|
+
|
|
594
|
+
## Versioning
|
|
502
595
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
596
|
+
**Changelog:** [python/CHANGELOG.md](https://github.com/4matic/trackparse/blob/main/python/CHANGELOG.md)
|
|
597
|
+
|
|
598
|
+
The package version is the spec version (`SPEC_VERSION`). While it is 0.x, a release that changes
|
|
599
|
+
the output for some input bumps the minor version, and a patch release only fixes bugs. If you
|
|
600
|
+
store parsed results, pin the minor version (`trackparse~=0.5.0`) and read the changelog before
|
|
601
|
+
moving up.
|
|
602
|
+
|
|
603
|
+
## Other languages
|
|
604
|
+
|
|
605
|
+
**Docs:** [Ports](https://4matic.github.io/trackparse/project/ports/)
|
|
606
|
+
|
|
607
|
+
The same parser exists for JavaScript and Rust. All three implement one spec, pass the same
|
|
608
|
+
fixtures, are released together under one version number, and return identical JSON for every
|
|
609
|
+
fixture input. You can parse in one language and read the result in another.
|
|
610
|
+
|
|
611
|
+
| Language | Package | Install |
|
|
612
|
+
|---|---|---|
|
|
613
|
+
| JavaScript / TypeScript | [trackparse on npm](https://www.npmjs.com/package/trackparse) | `npm install trackparse` |
|
|
614
|
+
| Python | [trackparse on PyPI](https://pypi.org/project/trackparse/) | `pip install trackparse` |
|
|
615
|
+
| Rust | [trackparse on crates.io](https://crates.io/crates/trackparse) | `cargo add trackparse` |
|
|
616
|
+
|
|
617
|
+
## Limitations
|
|
618
|
+
|
|
619
|
+
**Docs:** [Limitations and roadmap](https://4matic.github.io/trackparse/project/roadmap/)
|
|
620
|
+
|
|
621
|
+
- `parse()` takes one track string. Whole tracklists go through `trackparse.tracklist`, which reads
|
|
622
|
+
start times only: no durations, trailing times or time ranges.
|
|
623
|
+
- Catalog numbers, key and BPM are not fields. They stay in the title.
|
|
624
|
+
- There is no artist database. Pass `known_artists` for names that contain a joiner
|
|
625
|
+
(`Earth, Wind & Fire`).
|
|
510
626
|
|
|
511
627
|
## Development
|
|
512
628
|
|
|
@@ -528,13 +644,14 @@ same spec rule IDs. Behaviour changes start in the spec, not here: see
|
|
|
528
644
|
|
|
529
645
|
## Links
|
|
530
646
|
|
|
531
|
-
- [Documentation](https://4matic.github.io/trackparse/), with every example in
|
|
532
|
-
[playground](https://4matic.github.io/trackparse/playground/) that runs the
|
|
647
|
+
- [Documentation](https://4matic.github.io/trackparse/), with every example in JavaScript, Python
|
|
648
|
+
and Rust, and a [playground](https://4matic.github.io/trackparse/playground/) that runs the
|
|
649
|
+
parser in your browser
|
|
533
650
|
- [Spec](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md)
|
|
651
|
+
- [Changelog](https://github.com/4matic/trackparse/blob/main/python/CHANGELOG.md)
|
|
534
652
|
- [Project README](https://github.com/4matic/trackparse#readme)
|
|
535
653
|
- [GitHub repository](https://github.com/4matic/trackparse)
|
|
536
|
-
- [
|
|
537
|
-
- [npm package](https://www.npmjs.com/package/trackparse)
|
|
654
|
+
- [npm package](https://www.npmjs.com/package/trackparse) · [Rust crate](https://crates.io/crates/trackparse)
|
|
538
655
|
|
|
539
656
|
## License
|
|
540
657
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# trackparse
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/trackparse/)
|
|
4
4
|
[](https://pypi.org/project/trackparse/)
|
|
5
|
-
[](https://github.com/4matic/trackparse/actions/workflows/python.yml)
|
|
6
6
|
[](https://github.com/4matic/trackparse/blob/main/LICENSE)
|
|
7
7
|
|
|
8
8
|
**[Documentation](https://4matic.github.io/trackparse/)** · **[Getting started](https://4matic.github.io/trackparse/start/getting-started/)** · **[Playground](https://4matic.github.io/trackparse/playground/)**
|
|
@@ -16,18 +16,18 @@ what's just noise.**
|
|
|
16
16
|
|
|
17
17
|
Most title parsers split that into `"Wilkinson ft. Becky Hill & Tom Cane"` and
|
|
18
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
|
|
20
|
-
doesn't
|
|
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
21
|
|
|
22
22
|
This is the Python port of [trackparse](https://github.com/4matic/trackparse). Its behaviour is
|
|
23
23
|
defined by a [shared spec](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md) and pinned
|
|
24
24
|
by 13,130 fixtures. It passes all of them, and for every fixture input its output is byte-identical
|
|
25
|
-
to the JavaScript
|
|
25
|
+
to the [JavaScript and Rust packages](#other-languages).
|
|
26
26
|
|
|
27
27
|
- Python 3.9 to 3.14
|
|
28
28
|
- No dependencies
|
|
29
29
|
- Typed (`py.typed`), immutable results
|
|
30
|
-
- Never raises on
|
|
30
|
+
- Never raises on `str` input
|
|
31
31
|
|
|
32
32
|
## Install
|
|
33
33
|
|
|
@@ -123,6 +123,11 @@ normalize(s) -> str
|
|
|
123
123
|
all_artists(track) -> list[Artist]
|
|
124
124
|
create_parser(**options) -> Parser # Parser.parse(...), Parser.parse_artists(...)
|
|
125
125
|
SPEC_VERSION, __version__
|
|
126
|
+
|
|
127
|
+
# from trackparse.tracklist
|
|
128
|
+
parse_tracklist(text, *, mode="tracklist", known_artists=(), keywords=None, split_and="auto",
|
|
129
|
+
audio_seconds=None, marker_words=()) -> Tracklist
|
|
130
|
+
apply_edits(text, edits, *, <same options>) -> EditResult
|
|
126
131
|
```
|
|
127
132
|
|
|
128
133
|
### `parse(input, **options)`
|
|
@@ -131,7 +136,7 @@ SPEC_VERSION, __version__
|
|
|
131
136
|
|
|
132
137
|
| Option | Default | |
|
|
133
138
|
|---|---|---|
|
|
134
|
-
| `mode` | `"auto"` | `"clean"`, `"youtube"`, `"filename"`, `"tracklist"`, or `"auto"` to detect from the string |
|
|
139
|
+
| `mode` | `"auto"` | `"clean"`, `"youtube"`, `"filename"`, `"tracklist"`, or `"auto"` to detect from the string. See [Modes](#modes) |
|
|
135
140
|
| `uploader` | `None` | YouTube channel name, used as the artist when there's no separator (`- Topic` and `VEVO` are dropped) |
|
|
136
141
|
| `known_artists` | `()` | Names that are never split. There is no built-in list |
|
|
137
142
|
| `split_and` | `"auto"` | `"auto"` splits on `and` only after a comma (`A, B and C`), so `Simon and Garfunkel` stays whole. Also `"always"`, `"never"` |
|
|
@@ -164,9 +169,6 @@ parse("Artist - Song [Some Label Rip]", keywords={"junk": {"some label rip": "la
|
|
|
164
169
|
# (Junk(raw='Some Label Rip', kind='label'),)
|
|
165
170
|
```
|
|
166
171
|
|
|
167
|
-
Parsing is total: any `str` gives a `ParsedTrack`, including `""`. Bad option values fall back to
|
|
168
|
-
the defaults. Passing something that isn't a `str` raises `TypeError`.
|
|
169
|
-
|
|
170
172
|
### `ParsedTrack`
|
|
171
173
|
|
|
172
174
|
**Docs:** [ParsedTrack fields](https://4matic.github.io/trackparse/reference/parse/#parsedtrack)
|
|
@@ -178,7 +180,7 @@ the defaults. Passing something that isn't a `str` raises `TypeError`.
|
|
|
178
180
|
| `position` | `Position \| None` | Track number: `Position(raw='01', number=1)` |
|
|
179
181
|
| `timestamp` | `Timestamp \| None` | Leading cue time: `[12:34]` gives `Timestamp(raw='12:34', seconds=754)` |
|
|
180
182
|
| `artists` | `tuple[Artist, ...]` | Primary, featured and producer credits. Remixers live in `versions` |
|
|
181
|
-
| `title` | `str` | Clean title. Brackets it doesn't
|
|
183
|
+
| `title` | `str` | Clean title. Brackets it doesn't recognize stay in |
|
|
182
184
|
| `full_title` | `str` | `title` with the versions re-attached |
|
|
183
185
|
| `versions` | `tuple[Version, ...]` | Remixes and other versions, left to right |
|
|
184
186
|
| `year` | `int \| None` | From `(2015)` or `- 2015` |
|
|
@@ -206,14 +208,17 @@ parse("Queen - Bohemian Rhapsody (Live at Wembley 1986)").versions[0]
|
|
|
206
208
|
# descriptor='at Wembley', year=1986, unknown_artist=False, delimiter='(')
|
|
207
209
|
```
|
|
208
210
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
`
|
|
211
|
+
- `VersionType`: `remix`, `bootleg`, `vip`, `edit`, `flip`, `refix`, `rework`, `mashup`, `blend`,
|
|
212
|
+
`dub`, `mix`, `extended`, `radio`, `club`, `original`, `instrumental`, `acapella`, `live`,
|
|
213
|
+
`acoustic`, `remaster`, `demo`, `reprise`, `cover`, `version`, `spedUp`, `slowed`, `nightcore`.
|
|
214
|
+
- `JunkKind`: `video`, `audio`, `lyrics`, `quality`, `promo`, `platform`, `label`, `genre`, `other`.
|
|
215
|
+
- `Warning`: see [Warnings](#warnings).
|
|
216
|
+
|
|
217
|
+
These are plain strings, typed as `Literal` aliases (see [Typing](#typing)).
|
|
213
218
|
|
|
214
219
|
### `to_dict()` and `from_dict()`
|
|
215
220
|
|
|
216
|
-
**Docs:** [
|
|
221
|
+
**Docs:** [Serializing to JSON](https://4matic.github.io/trackparse/reference/parse/#serializing-to-json)
|
|
217
222
|
|
|
218
223
|
Every result class has `to_dict()`, which returns plain dicts and lists matching
|
|
219
224
|
[`parsed-track.schema.json`](https://github.com/4matic/trackparse/blob/main/spec/schema/parsed-track.schema.json)
|
|
@@ -222,14 +227,16 @@ ensure_ascii=False)` the bytes are the same as `JSON.stringify(parse(s))` in JS.
|
|
|
222
227
|
(`ParsedTrack`, `Artist`, ...) also have a `from_dict()` classmethod that goes the other way:
|
|
223
228
|
|
|
224
229
|
```python
|
|
230
|
+
from trackparse import ParsedTrack
|
|
231
|
+
|
|
225
232
|
ParsedTrack.from_dict(track.to_dict()) == track # True
|
|
226
233
|
```
|
|
227
234
|
|
|
228
235
|
### `parse_artists(input, **options)`
|
|
229
236
|
|
|
230
|
-
**Docs:** [parseArtists](https://4matic.github.io/trackparse/reference/functions/#parseartistsinput-options)
|
|
237
|
+
**Docs:** [parseArtists](https://4matic.github.io/trackparse/reference/functions/#parseartistsinput-options) · [Artists and credits](https://4matic.github.io/trackparse/guides/artists/)
|
|
231
238
|
|
|
232
|
-
Splits a bare artist string. Takes the same options as `parse`.
|
|
239
|
+
Splits a bare artist string (no title). Takes the same options as `parse`.
|
|
233
240
|
|
|
234
241
|
```python
|
|
235
242
|
from trackparse import parse_artists
|
|
@@ -246,12 +253,12 @@ Renders a track back to a string.
|
|
|
246
253
|
|
|
247
254
|
| Option | Default | |
|
|
248
255
|
|---|---|---|
|
|
249
|
-
| `feat` | `"source"` | Where featured artists go: `"source"` (where they were), `"artist"`, `"title"` or `"omit"` |
|
|
250
|
-
| `feat_marker` | `None` | Marker for featured artists, such as `"
|
|
251
|
-
| `joiners` | `"original"` | `"original"` keeps the joiners as written
|
|
256
|
+
| `feat` | `"source"` | Where featured artists go: `"source"` (where they were), `"artist"`, `"title"` (`(feat. X)` after the title) or `"omit"` |
|
|
257
|
+
| `feat_marker` | `None` | 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.` |
|
|
252
259
|
| `versions` | `"all"` | `"all"` or `"none"` |
|
|
253
260
|
| `producers` | `True` | Include `(prod. ...)` credits |
|
|
254
|
-
| `position` | `False` | Include the track number |
|
|
261
|
+
| `position` | `False` | Include the track number (`01. `) |
|
|
255
262
|
|
|
256
263
|
```python
|
|
257
264
|
from trackparse import format_track
|
|
@@ -273,13 +280,14 @@ format_track(track, position=True)
|
|
|
273
280
|
|
|
274
281
|
**Docs:** [normalize](https://4matic.github.io/trackparse/reference/functions/#normalizeinput)
|
|
275
282
|
|
|
276
|
-
The Unicode cleanup every function runs first: NFC,
|
|
277
|
-
|
|
283
|
+
The Unicode cleanup every function runs first: NFC, zero-width spaces, BOM and soft hyphens
|
|
284
|
+
removed, fullwidth brackets, smart quotes and dash variants mapped to ASCII, whitespace collapsed.
|
|
285
|
+
Running it twice changes nothing.
|
|
278
286
|
|
|
279
287
|
```python
|
|
280
288
|
from trackparse import normalize
|
|
281
289
|
|
|
282
|
-
normalize("Noisia
|
|
290
|
+
normalize("Noisia\u200b – Stigma (VIP)")
|
|
283
291
|
# 'Noisia - Stigma (VIP)'
|
|
284
292
|
```
|
|
285
293
|
|
|
@@ -317,8 +325,9 @@ parser.parse("Chase & Status x Camo & Krooked - Track (Alix Perez Rerub)").versi
|
|
|
317
325
|
|
|
318
326
|
### `SPEC_VERSION`
|
|
319
327
|
|
|
320
|
-
The spec version this build implements.
|
|
321
|
-
released together and always share
|
|
328
|
+
The spec version this build implements, `"0.5.0"` in this release. It equals `__version__`: the
|
|
329
|
+
PyPI package, the npm package, the Rust crate and the spec are released together and always share
|
|
330
|
+
one version number.
|
|
322
331
|
|
|
323
332
|
## Modes
|
|
324
333
|
|
|
@@ -345,9 +354,39 @@ parse("Shelter", mode="youtube", uploader="Porter Robinson").artists
|
|
|
345
354
|
# (Artist(name='Porter Robinson', role='primary', joiner=None, source='artist'),)
|
|
346
355
|
```
|
|
347
356
|
|
|
357
|
+
## Warnings
|
|
358
|
+
|
|
359
|
+
**Docs:** [Warnings](https://4matic.github.io/trackparse/reference/warnings/)
|
|
360
|
+
|
|
361
|
+
The parser never fails, but it says when it had to guess. `track.warnings` is a tuple of strings,
|
|
362
|
+
empty when every decision was unambiguous.
|
|
363
|
+
|
|
364
|
+
| Warning | Means |
|
|
365
|
+
|---|---|
|
|
366
|
+
| `noSeparator` | No artist/title separator was found. The whole string is the title |
|
|
367
|
+
| `ambiguousSeparator` | A two-part pipe title with no dash was read as artist and title |
|
|
368
|
+
| `unspacedDashSplit` | Split at a dash without spaces around it |
|
|
369
|
+
| `asymmetricDashSplit` | Split at a dash with a space on one side only |
|
|
370
|
+
| `bySplit` | Read `"Title" by Artist` |
|
|
371
|
+
| `quotedTitleSplit` | Took a quoted span as the title and the text before it as the artist |
|
|
372
|
+
| `ambiguousMixCredit` | `<Name> Mix` was read as a remix, but the name is long enough to be a style |
|
|
373
|
+
| `unbalancedBrackets` | A bracket was opened and never closed |
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
t = parse("Around the World")
|
|
377
|
+
t.artists, t.title, t.warnings
|
|
378
|
+
# ((), 'Around the World', ('noSeparator',))
|
|
379
|
+
|
|
380
|
+
parse("Noisia - Stigma (VIP").warnings
|
|
381
|
+
# ('unbalancedBrackets',)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Tracklists have their own warnings, with line numbers. They are listed under
|
|
385
|
+
[Tracklists](#tracklists).
|
|
386
|
+
|
|
348
387
|
## Tracklists
|
|
349
388
|
|
|
350
|
-
**Docs:** [Tracklists](https://4matic.github.io/trackparse/guides/tracklists/) · [Editing tracklists](https://4matic.github.io/trackparse/guides/editing-tracklists/)
|
|
389
|
+
**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)
|
|
351
390
|
|
|
352
391
|
`trackparse.tracklist` parses a whole tracklist, one entry per line, and checks it. Each entry
|
|
353
392
|
keeps its line exactly as written plus its own line ending, so `"".join(e.raw + e.eol for e in
|
|
@@ -416,13 +455,19 @@ apply_edits(text, [{"op": "shift", "seconds": -3000, "lines": [2, 3]}]).warnings
|
|
|
416
455
|
```
|
|
417
456
|
|
|
418
457
|
A rewritten time keeps its line's style: `52:47` stays `MM:SS`, and a time that crosses the hour
|
|
419
|
-
gets hours like the rest of the list, with a `style-upgraded` warning. A
|
|
420
|
-
and warns `
|
|
458
|
+
gets hours like the rest of the list, with a `style-upgraded` warning. A time pushed below zero
|
|
459
|
+
stops at zero and warns `time-clamped`. A bad edit changes nothing and warns `edit-invalid`.
|
|
460
|
+
|
|
461
|
+
Both functions take the keyword options `mode` (default `"tracklist"`), `known_artists`,
|
|
462
|
+
`split_and` and `keywords`, as `parse` does, plus `audio_seconds` (the length of the audio, which
|
|
463
|
+
turns on the `time-after-audio-end` check) and `marker_words` (extra words that make a line a
|
|
464
|
+
marker):
|
|
421
465
|
|
|
422
466
|
```python
|
|
423
|
-
parse_tracklist(
|
|
424
|
-
|
|
425
|
-
|
|
467
|
+
parse_tracklist(
|
|
468
|
+
"00:00 Noisia - Stigma\n1:02:03 Mefjus - Suicide Bassline", audio_seconds=3600
|
|
469
|
+
).warnings
|
|
470
|
+
# (TracklistWarning(code='time-after-audio-end', line=2, severity='warning', suggestion=None),)
|
|
426
471
|
```
|
|
427
472
|
|
|
428
473
|
| Result | Fields |
|
|
@@ -448,7 +493,9 @@ Warning codes: `time-missing`, `time-out-of-order`, `time-duplicate`, `no-start-
|
|
|
448
493
|
`position-duplicate`, `line-unparsed`. Edits: `set-time {line, seconds}`, `shift {seconds, lines? | from?}`
|
|
449
494
|
(all timed lines when neither is given), `sort-by-time {}`, `set-rest {line, rest}`,
|
|
450
495
|
`insert {after, text}` (`after: 0` inserts at the top), `delete {line}`. The
|
|
451
|
-
[Tracklist API reference](https://4matic.github.io/trackparse/reference/tracklist/) explains each one
|
|
496
|
+
[Tracklist API reference](https://4matic.github.io/trackparse/reference/tracklist/) explains each one,
|
|
497
|
+
and the rules are in
|
|
498
|
+
[SPEC R13](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md#r13-tracklists).
|
|
452
499
|
`to_dict()` gives the camelCase JSON from
|
|
453
500
|
[`tracklist.schema.json`](https://github.com/4matic/trackparse/blob/main/spec/schema/tracklist.schema.json),
|
|
454
501
|
byte-identical to the JS package's output.
|
|
@@ -463,17 +510,86 @@ parse("Electrosoul System - Liquid Metal", mode="tracklist").title
|
|
|
463
510
|
# 'Liquid Metal'
|
|
464
511
|
```
|
|
465
512
|
|
|
513
|
+
## Errors
|
|
514
|
+
|
|
515
|
+
Parsing is total: any `str` gives a complete `ParsedTrack`, `""` included, and `parse_tracklist`
|
|
516
|
+
and `apply_edits` return for any text. An option of the wrong type or with an unknown value is
|
|
517
|
+
ignored and the default is used. `apply_edits` reports a bad edit as an `edit-invalid` warning and
|
|
518
|
+
leaves the text alone.
|
|
519
|
+
|
|
520
|
+
The one exception is the input itself: passing something that isn't a `str` raises `TypeError`.
|
|
521
|
+
|
|
522
|
+
```python
|
|
523
|
+
parse(None)
|
|
524
|
+
# TypeError: parse() expects str input, got NoneType
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Recognition is done by hand-written scanners, so there is no regex backtracking. A
|
|
528
|
+
10,000-code-point input parses in well under a second, and the test suite enforces a 1 s ceiling.
|
|
529
|
+
|
|
466
530
|
## Typing
|
|
467
531
|
|
|
468
|
-
**Docs:** [Types](https://4matic.github.io/trackparse/reference/parse/#types)
|
|
532
|
+
**Docs:** [Types](https://4matic.github.io/trackparse/reference/parse/#types) · [Tracklist types](https://4matic.github.io/trackparse/reference/tracklist/#types)
|
|
533
|
+
|
|
534
|
+
The package ships `py.typed` and passes `mypy --strict`.
|
|
535
|
+
|
|
536
|
+
- `ParsedTrack`, `Artist`, `Version`, `Junk`, `Flags`, `Position` and `Timestamp` are frozen
|
|
537
|
+
dataclasses with snake_case attributes, and so are the tracklist results in
|
|
538
|
+
`trackparse.tracklist`. Sequences are tuples, so results are immutable and hashable: they can go
|
|
539
|
+
in sets or be used as dict keys.
|
|
540
|
+
- Version types, junk kinds, warnings, roles and modes are plain strings, typed as `Literal`
|
|
541
|
+
aliases: `VersionType`, `JunkKind`, `Warning`, `ArtistRole`, `ArtistSource`, `Mode`,
|
|
542
|
+
`ModeOption`, `VersionDelimiter`. `trackparse.tracklist` adds `EntryKind`, `Eol`, `Severity`,
|
|
543
|
+
`TracklistWarningCode` and `EditWarningCode`.
|
|
544
|
+
- Option dicts have `TypedDict` types (`ParseOptions`, `KeywordOptions`, `FormatOptions`).
|
|
545
|
+
Tracklist edits are plain dicts.
|
|
546
|
+
|
|
547
|
+
## How it is tested
|
|
548
|
+
|
|
549
|
+
**Docs:** [How it's tested](https://4matic.github.io/trackparse/project/testing/)
|
|
550
|
+
|
|
551
|
+
- 13,130 shared fixtures: 1,203 written by hand and 11,927 generated from the rules. Every output
|
|
552
|
+
is also validated against the JSON Schema.
|
|
553
|
+
- Property tests with [Hypothesis](https://hypothesis.works) for the spec's invariants: no
|
|
554
|
+
exception on any string, `normalize` is idempotent, whitespace and dash noise doesn't change
|
|
555
|
+
the result, and `parse(format_track(parse(s)))` round-trips.
|
|
556
|
+
- CI runs the suite on Python 3.9 to 3.14, and runs the fixtures again against the built wheel.
|
|
557
|
+
- [`compare-ports.mjs`](https://github.com/4matic/trackparse/blob/main/scripts/compare-ports.mjs)
|
|
558
|
+
runs every fixture input through the JavaScript package, this package and the Rust crate and
|
|
559
|
+
diffs the complete JSON output. The difference count is zero.
|
|
560
|
+
|
|
561
|
+
## Versioning
|
|
469
562
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
563
|
+
**Changelog:** [python/CHANGELOG.md](https://github.com/4matic/trackparse/blob/main/python/CHANGELOG.md)
|
|
564
|
+
|
|
565
|
+
The package version is the spec version (`SPEC_VERSION`). While it is 0.x, a release that changes
|
|
566
|
+
the output for some input bumps the minor version, and a patch release only fixes bugs. If you
|
|
567
|
+
store parsed results, pin the minor version (`trackparse~=0.5.0`) and read the changelog before
|
|
568
|
+
moving up.
|
|
569
|
+
|
|
570
|
+
## Other languages
|
|
571
|
+
|
|
572
|
+
**Docs:** [Ports](https://4matic.github.io/trackparse/project/ports/)
|
|
573
|
+
|
|
574
|
+
The same parser exists for JavaScript and Rust. All three implement one spec, pass the same
|
|
575
|
+
fixtures, are released together under one version number, and return identical JSON for every
|
|
576
|
+
fixture input. You can parse in one language and read the result in another.
|
|
577
|
+
|
|
578
|
+
| Language | Package | Install |
|
|
579
|
+
|---|---|---|
|
|
580
|
+
| JavaScript / TypeScript | [trackparse on npm](https://www.npmjs.com/package/trackparse) | `npm install trackparse` |
|
|
581
|
+
| Python | [trackparse on PyPI](https://pypi.org/project/trackparse/) | `pip install trackparse` |
|
|
582
|
+
| Rust | [trackparse on crates.io](https://crates.io/crates/trackparse) | `cargo add trackparse` |
|
|
583
|
+
|
|
584
|
+
## Limitations
|
|
585
|
+
|
|
586
|
+
**Docs:** [Limitations and roadmap](https://4matic.github.io/trackparse/project/roadmap/)
|
|
587
|
+
|
|
588
|
+
- `parse()` takes one track string. Whole tracklists go through `trackparse.tracklist`, which reads
|
|
589
|
+
start times only: no durations, trailing times or time ranges.
|
|
590
|
+
- Catalog numbers, key and BPM are not fields. They stay in the title.
|
|
591
|
+
- There is no artist database. Pass `known_artists` for names that contain a joiner
|
|
592
|
+
(`Earth, Wind & Fire`).
|
|
477
593
|
|
|
478
594
|
## Development
|
|
479
595
|
|
|
@@ -495,13 +611,14 @@ same spec rule IDs. Behaviour changes start in the spec, not here: see
|
|
|
495
611
|
|
|
496
612
|
## Links
|
|
497
613
|
|
|
498
|
-
- [Documentation](https://4matic.github.io/trackparse/), with every example in
|
|
499
|
-
[playground](https://4matic.github.io/trackparse/playground/) that runs the
|
|
614
|
+
- [Documentation](https://4matic.github.io/trackparse/), with every example in JavaScript, Python
|
|
615
|
+
and Rust, and a [playground](https://4matic.github.io/trackparse/playground/) that runs the
|
|
616
|
+
parser in your browser
|
|
500
617
|
- [Spec](https://github.com/4matic/trackparse/blob/main/spec/SPEC.md)
|
|
618
|
+
- [Changelog](https://github.com/4matic/trackparse/blob/main/python/CHANGELOG.md)
|
|
501
619
|
- [Project README](https://github.com/4matic/trackparse#readme)
|
|
502
620
|
- [GitHub repository](https://github.com/4matic/trackparse)
|
|
503
|
-
- [
|
|
504
|
-
- [npm package](https://www.npmjs.com/package/trackparse)
|
|
621
|
+
- [npm package](https://www.npmjs.com/package/trackparse) · [Rust crate](https://crates.io/crates/trackparse)
|
|
505
622
|
|
|
506
623
|
## License
|
|
507
624
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trackparse"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.1"
|
|
8
8
|
description = "Spec-driven parser for music track titles and tracklists: artists, feat, remixes, versions, junk, positions, times."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|