trackparse 0.4.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.
Files changed (37) hide show
  1. {trackparse-0.4.0 → trackparse-0.5.1}/.gitignore +0 -1
  2. {trackparse-0.4.0 → trackparse-0.5.1}/PKG-INFO +165 -48
  3. {trackparse-0.4.0 → trackparse-0.5.1}/README.md +164 -47
  4. {trackparse-0.4.0 → trackparse-0.5.1}/pyproject.toml +1 -1
  5. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/__init__.py +1 -1
  6. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_data.py +1 -1
  7. {trackparse-0.4.0 → trackparse-0.5.1}/LICENSE +0 -0
  8. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_artists.py +0 -0
  9. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_assemble.py +0 -0
  10. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_classify.py +0 -0
  11. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_context.py +0 -0
  12. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_credits.py +0 -0
  13. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_digits.py +0 -0
  14. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_format.py +0 -0
  15. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_mode.py +0 -0
  16. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_normalize.py +0 -0
  17. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_parser.py +0 -0
  18. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_prefix.py +0 -0
  19. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_scanner.py +0 -0
  20. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_separator.py +0 -0
  21. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_tables.py +0 -0
  22. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_title.py +0 -0
  23. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_tracklist_edits.py +0 -0
  24. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_tracklist_emphasis.py +0 -0
  25. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_tracklist_kinds.py +0 -0
  26. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_tracklist_options.py +0 -0
  27. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_tracklist_parse.py +0 -0
  28. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_tracklist_positions.py +0 -0
  29. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_tracklist_split.py +0 -0
  30. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_tracklist_times.py +0 -0
  31. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_tracklist_types.py +0 -0
  32. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_tracklist_warnings.py +0 -0
  33. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/_words.py +0 -0
  34. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/options.py +0 -0
  35. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/py.typed +0 -0
  36. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/tracklist.py +0 -0
  37. {trackparse-0.4.0 → trackparse-0.5.1}/src/trackparse/types.py +0 -0
@@ -18,7 +18,6 @@ python/dist/
18
18
 
19
19
  # Rust
20
20
  target/
21
- rust/Cargo.lock
22
21
 
23
22
  # Editors / OS
24
23
  .DS_Store
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: trackparse
3
- Version: 0.4.0
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
- [![PyPI](https://img.shields.io/pypi/v/trackparse?color=3775a9&logo=pypi&logoColor=white)](https://pypi.org/project/trackparse/)
36
+ [![PyPI](https://img.shields.io/pypi/v/trackparse?logo=pypi&logoColor=white)](https://pypi.org/project/trackparse/)
37
37
  [![Python versions](https://img.shields.io/pypi/pyversions/trackparse?logo=python&logoColor=white)](https://pypi.org/project/trackparse/)
38
- [![python](https://img.shields.io/github/actions/workflow/status/4matic/trackparse/python.yml?branch=main&label=tests&logo=github)](https://github.com/4matic/trackparse/actions/workflows/python.yml)
38
+ [![tests](https://img.shields.io/github/actions/workflow/status/4matic/trackparse/python.yml?branch=main&label=tests&logo=github)](https://github.com/4matic/trackparse/actions/workflows/python.yml)
39
39
  [![license](https://img.shields.io/badge/license-MIT-blue)](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 recognises goes into a field, and anything it
53
- doesn't recognise stays in the title, word for word.
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
- by 13,124 fixtures. It passes all of them, and for every fixture input its output is byte-identical
58
- to the JavaScript package.
57
+ by 13,130 fixtures. It passes all of them, and for every fixture input its output is byte-identical
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 string input
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 recognise stay in |
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
- Version types, junk kinds and warnings are listed in the
243
- [API reference](https://4matic.github.io/trackparse/reference/parse/) and typed as `Literal` aliases
244
- (`VersionType`, `JunkKind`, `Warning`, `ArtistRole`, `ArtistSource`, `Mode`, `ModeOption`,
245
- `VersionDelimiter`).
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:** [JSON and Python objects](https://4matic.github.io/trackparse/reference/parse/#json-and-python-objects)
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 `"feat."` |
284
- | `joiners` | `"original"` | `"original"` keeps the joiners as written, `"canonical"` uses the standard ones |
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, invisible characters, and dash, bracket and
310
- quote variants.
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​ – Stigma (VIP)")
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. The PyPI package, the npm package and the spec are
354
- released together and always share one version number.
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 bad edit changes nothing
453
- and warns `edit-invalid`. Neither function raises on `str` input.
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(text, *, mode="tracklist", known_artists=(), keywords=None, split_and="auto",
457
- audio_seconds=None, marker_words=()) -> Tracklist
458
- apply_edits(text, edits, *, <same options>) -> EditResult
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
- The package ships `py.typed` and passes `mypy --strict`. `ParsedTrack`, `Artist`, `Version`,
504
- `Junk`, `Flags`, `Position` and `Timestamp` are frozen dataclasses with snake_case attributes, and
505
- so are the tracklist results in `trackparse.tracklist`.
506
- Sequences are tuples, so results are immutable and hashable and can go in sets or be used as dict
507
- keys. Option dicts have `TypedDict` types (`ParseOptions`, `KeywordOptions`, `FormatOptions`);
508
- tracklist edits are plain dicts. `trackparse.tracklist` also exports the `Literal` aliases
509
- `EntryKind`, `Eol`, `Severity`, `TracklistWarningCode` and `EditWarningCode`.
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 Python and JavaScript, and a
532
- [playground](https://4matic.github.io/trackparse/playground/) that runs the parser in your browser
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
- - [Changelog](https://github.com/4matic/trackparse/blob/main/python/CHANGELOG.md)
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
- [![PyPI](https://img.shields.io/pypi/v/trackparse?color=3775a9&logo=pypi&logoColor=white)](https://pypi.org/project/trackparse/)
3
+ [![PyPI](https://img.shields.io/pypi/v/trackparse?logo=pypi&logoColor=white)](https://pypi.org/project/trackparse/)
4
4
  [![Python versions](https://img.shields.io/pypi/pyversions/trackparse?logo=python&logoColor=white)](https://pypi.org/project/trackparse/)
5
- [![python](https://img.shields.io/github/actions/workflow/status/4matic/trackparse/python.yml?branch=main&label=tests&logo=github)](https://github.com/4matic/trackparse/actions/workflows/python.yml)
5
+ [![tests](https://img.shields.io/github/actions/workflow/status/4matic/trackparse/python.yml?branch=main&label=tests&logo=github)](https://github.com/4matic/trackparse/actions/workflows/python.yml)
6
6
  [![license](https://img.shields.io/badge/license-MIT-blue)](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 recognises goes into a field, and anything it
20
- doesn't recognise stays in the title, word for word.
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
- by 13,124 fixtures. It passes all of them, and for every fixture input its output is byte-identical
25
- to the JavaScript package.
24
+ by 13,130 fixtures. It passes all of them, and for every fixture input its output is byte-identical
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 string input
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 recognise stay in |
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
- Version types, junk kinds and warnings are listed in the
210
- [API reference](https://4matic.github.io/trackparse/reference/parse/) and typed as `Literal` aliases
211
- (`VersionType`, `JunkKind`, `Warning`, `ArtistRole`, `ArtistSource`, `Mode`, `ModeOption`,
212
- `VersionDelimiter`).
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:** [JSON and Python objects](https://4matic.github.io/trackparse/reference/parse/#json-and-python-objects)
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 `"feat."` |
251
- | `joiners` | `"original"` | `"original"` keeps the joiners as written, `"canonical"` uses the standard ones |
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, invisible characters, and dash, bracket and
277
- quote variants.
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​ – Stigma (VIP)")
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. The PyPI package, the npm package and the spec are
321
- released together and always share one version number.
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 bad edit changes nothing
420
- and warns `edit-invalid`. Neither function raises on `str` input.
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(text, *, mode="tracklist", known_artists=(), keywords=None, split_and="auto",
424
- audio_seconds=None, marker_words=()) -> Tracklist
425
- apply_edits(text, edits, *, <same options>) -> EditResult
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
- The package ships `py.typed` and passes `mypy --strict`. `ParsedTrack`, `Artist`, `Version`,
471
- `Junk`, `Flags`, `Position` and `Timestamp` are frozen dataclasses with snake_case attributes, and
472
- so are the tracklist results in `trackparse.tracklist`.
473
- Sequences are tuples, so results are immutable and hashable and can go in sets or be used as dict
474
- keys. Option dicts have `TypedDict` types (`ParseOptions`, `KeywordOptions`, `FormatOptions`);
475
- tracklist edits are plain dicts. `trackparse.tracklist` also exports the `Literal` aliases
476
- `EntryKind`, `Eol`, `Severity`, `TracklistWarningCode` and `EditWarningCode`.
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 Python and JavaScript, and a
499
- [playground](https://4matic.github.io/trackparse/playground/) that runs the parser in your browser
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
- - [Changelog](https://github.com/4matic/trackparse/blob/main/python/CHANGELOG.md)
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.4.0"
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"
@@ -33,7 +33,7 @@ from .types import (
33
33
  Warning,
34
34
  )
35
35
 
36
- __version__ = "0.4.0" # x-release-please-version
36
+ __version__ = "0.5.1" # x-release-please-version
37
37
 
38
38
  __all__ = [
39
39
  "SPEC_VERSION",
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
 
6
6
  from typing import Any
7
7
 
8
- SPEC_VERSION = "0.4.0" # x-release-please-version
8
+ SPEC_VERSION = "0.5.1" # x-release-please-version
9
9
 
10
10
  # spec/data/descriptors.json
11
11
  DESCRIPTORS_DATA: dict[str, Any] = {
File without changes