Skip to main content

trackparse

PyPI Python versions python license

Turn messy track names into structured data: who made it, who's featured, who remixed it, and what's just noise.

01. Wilkinson ft. Becky Hill & Tom Cane - Afterglow (Sub Focus VIP Remix) [Official Video]

Most title parsers split that into "Wilkinson ft. Becky Hill & Tom Cane" and "Afterglow (Sub Focus VIP Remix)" and stop. Others "clean" it and throw the feat and the remix away. trackparse keeps all of it. Everything it recognises goes into a field, and anything it doesn't recognise stays in the title, word for word.

This is the Python port of trackparse. Its behaviour is defined by a shared spec and pinned by 11,891 fixtures. It passes all of them, and for every fixture input its output is byte-identical to the JavaScript package.

  • Python 3.9 to 3.14
  • No dependencies
  • Typed (py.typed), immutable results
  • Never raises on string input

Install

pip install trackparse
# or
uv add trackparse

Quick start

from trackparse import format_track, parse

track = parse(
    "01. Wilkinson ft. Becky Hill & Tom Cane - Afterglow (Sub Focus VIP Remix) [Official Video]"
)

[f"{a.name} ({a.role})" for a in track.artists]
# ['Wilkinson (primary)', 'Becky Hill (featured)', 'Tom Cane (featured)']
track.title  # 'Afterglow'
track.full_title  # 'Afterglow (Sub Focus VIP Remix)'
track.versions[0].type  # 'remix'
track.versions[0].artists[0].name  # 'Sub Focus'
track.versions[0].modifiers  # ('vip',)
track.junk  # (Junk(raw='Official Video', kind='video'),)
track.position  # Position(raw='01', number=1)
track.mode  # 'youtube'

format_track(track, joiners="canonical")
# 'Wilkinson feat. Becky Hill & Tom Cane - Afterglow (Sub Focus VIP Remix)'

to_dict() returns the spec's JSON form, with the same camelCase keys the JS package uses:

import json

print(json.dumps(track.to_dict(), indent=2))
Output
{
  "input": "01. Wilkinson ft. Becky Hill & Tom Cane - Afterglow (Sub Focus VIP Remix) [Official Video]",
  "mode": "youtube",
  "position": { "raw": "01", "number": 1 },
  "timestamp": null,
  "artists": [
    { "name": "Wilkinson", "role": "primary", "joiner": null, "source": "artist" },
    { "name": "Becky Hill", "role": "featured", "joiner": "ft.", "source": "artist" },
    { "name": "Tom Cane", "role": "featured", "joiner": "&", "source": "artist" }
  ],
  "title": "Afterglow",
  "fullTitle": "Afterglow (Sub Focus VIP Remix)",
  "versions": [
    {
      "type": "remix",
      "raw": "Sub Focus VIP Remix",
      "artists": [{ "name": "Sub Focus", "role": "remixer", "joiner": null, "source": "version" }],
      "modifiers": ["vip"],
      "descriptor": null,
      "year": null,
      "unknownArtist": false,
      "delimiter": "("
    }
  ],
  "year": null,
  "flags": { "explicit": false, "clean": false, "unknownArtist": false, "unknownTitle": false },
  "junk": [{ "raw": "Official Video", "kind": "video" }],
  "warnings": []
}

(Reformatted for width; the keys and values are exactly what to_dict() returns.)

API

parse(input, *, mode="auto", uploader=None, known_artists=(), split_and="auto", keywords=None) -> ParsedTrack
parse_artists(input, *, <same options>) -> list[Artist]
format_track(track, *, feat="source", feat_marker=None, joiners="original",
             versions="all", producers=True, position=False) -> str
normalize(s) -> str
all_artists(track) -> list[Artist]
create_parser(**options) -> Parser   # Parser.parse(...), Parser.parse_artists(...)
SPEC_VERSION, __version__

parse(input, **options)

Option Default
mode "auto" "clean", "youtube", "filename", or "auto" to detect from the string
uploader None YouTube channel name, used as the artist when there's no separator (- Topic and VEVO are dropped)
known_artists () Names that are never split. There is no built-in list
split_and "auto" "auto" splits on and only after a comma (A, B and C), so Simon and Garfunkel stays whole. Also "always", "never"
keywords None Extra vocabulary, added to the built-in lists (see below)
[a.name for a in parse("Chase & Status - Baddadan").artists]
# ['Chase', 'Status']
[a.name for a in parse("Chase & Status - Baddadan", known_artists=["Chase & Status"]).artists]
# ['Chase & Status']

[a.name for a in parse("Simon and Garfunkel - Mrs. Robinson", split_and="always").artists]
# ['Simon', 'Garfunkel']

keywords is a dict with any of these keys:

Key Type
version_heads dict[str, VersionType] Word that sets a version type ({"rerub": "rework"})
descriptors list[str] Words kept as a version's descriptor
genres list[str] Genre words
junk dict[str, JunkKind] Phrase to junk kind ({"some label rip": "label"})
feat_markers list[str] Extra feat markers
parse("Artist - Song [Some Label Rip]").title
# 'Song [Some Label Rip]'
parse("Artist - Song [Some Label Rip]", keywords={"junk": {"some label rip": "label"}}).junk
# (Junk(raw='Some Label Rip', kind='label'),)

Parsing is total: any str gives a ParsedTrack, including "". Bad option values fall back to the defaults. Passing something that isn't a str raises TypeError.

ParsedTrack

Attribute Type
input str The original string, untouched
mode "clean" | "youtube" | "filename" Resolved mode
position Position | None Track number: Position(raw='01', number=1)
timestamp Timestamp | None Leading cue time: [12:34] gives Timestamp(raw='12:34', seconds=754)
artists tuple[Artist, ...] Primary, featured and producer credits. Remixers live in versions
title str Clean title. Brackets it doesn't recognise stay in
full_title str title with the versions re-attached
versions tuple[Version, ...] Remixes and other versions, left to right
year int | None From (2015) or - 2015
flags Flags explicit, clean, unknown_artist, unknown_title
junk tuple[Junk, ...] What was taken out, each with a kind
warnings tuple[Warning, ...] Heuristics that fired

Artist(name, role, joiner, source):

  • role is "primary", "featured", "remixer" or "producer".
  • joiner is the text before the name as written ("&", ",", "ft.", "prod. by"), or None for the first name in a list.
  • source is where the credit came from: "artist" (left of the dash), "title" (right of it) or "version".

Version(type, raw, artists, modifiers, descriptor, year, unknown_artist, delimiter):

parse("Pendulum - Watercolour (Extended VIP Mix)").versions[0]
# Version(type='vip', raw='Extended VIP Mix', artists=(), modifiers=('extended',),
#         descriptor=None, year=None, unknown_artist=False, delimiter='(')

parse("Queen - Bohemian Rhapsody (Live at Wembley 1986)").versions[0]
# Version(type='live', raw='Live at Wembley 1986', artists=(), modifiers=(),
#         descriptor='at Wembley', year=1986, unknown_artist=False, delimiter='(')

Version types, junk kinds and warnings are listed in the main README and typed as Literal aliases (VersionType, JunkKind, Warning, ArtistRole, ArtistSource, Mode, ModeOption, VersionDelimiter).

to_dict() and from_dict()

Every result class has to_dict(), which returns plain dicts and lists matching parsed-track.schema.json (fullTitle, unknownArtist, ...). With json.dumps(track.to_dict(), separators=(",", ":"), ensure_ascii=False) the bytes are the same as JSON.stringify(parse(s)) in JS. The from_dict() classmethod goes the other way:

ParsedTrack.from_dict(track.to_dict()) == track  # True

parse_artists(input, **options)

Splits a bare artist string. Takes the same options as parse.

from trackparse import parse_artists

[f"{a.name}:{a.role}" for a in parse_artists("Sub Focus, Wilkinson & Dimension feat. Kojo")]
# ['Sub Focus:primary', 'Wilkinson:primary', 'Dimension:primary', 'Kojo:featured']

format_track(track, **options)

Renders a track back to a string.

Option Default
feat "source" Where featured artists go: "source" (where they were), "artist", "title" or "omit"
feat_marker None Marker for featured artists, such as "feat."
joiners "original" "original" keeps the joiners as written, "canonical" uses the standard ones
versions "all" "all" or "none"
producers True Include (prod. ...) credits
position False Include the track number
from trackparse import format_track

format_track(
    parse("Wilkinson ft. Becky Hill - Afterglow (Sub Focus Remix)"),
    feat="title",
    joiners="canonical",
)
# 'Wilkinson - Afterglow (feat. Becky Hill) (Sub Focus Remix)'

format_track(track, feat="omit", versions="none")
# 'Wilkinson - Afterglow'
format_track(track, position=True)
# '01. Wilkinson ft. Becky Hill & Tom Cane - Afterglow (Sub Focus VIP Remix)'

normalize(s)

The Unicode cleanup every function runs first: NFC, invisible characters, and dash, bracket and quote variants.

from trackparse import normalize

normalize("Noisia​ – Stigma (VIP)")
# 'Noisia - Stigma (VIP)'

all_artists(track)

Credits plus every remixer, deduplicated.

from trackparse import all_artists

[(a.name, a.role) for a in all_artists(track)]
# [('Wilkinson', 'primary'), ('Becky Hill', 'featured'), ('Tom Cane', 'featured'), ('Sub Focus', 'remixer')]

create_parser(**options)

Compiles the options once, for many calls. Per-call options are merged over the parser's: a value passed per call replaces the base one (known_artists included), None leaves it alone, and keywords dicts are merged.

from trackparse import create_parser

parser = create_parser(
    known_artists=["Chase & Status", "Camo & Krooked"],
    keywords={"version_heads": {"rerub": "rework"}},
)
parser.parse("Chase & Status x Camo & Krooked - Track (Alix Perez Rerub)").versions[0].type
# 'rework'

SPEC_VERSION

The spec version this build implements. The PyPI package, the npm package and the spec are released together and always share one version number.

Modes

Mode For Adds
clean Store metadata, tags Only a spaced dash separates artist and title
youtube Video titles, scrobbles Pipe segments, "Title" by Artist, Artist: "Title", unspaced dashes, trailing junk, uploader fallback
filename Files Strips the extension, _ becomes a space, 01-Track numbering
auto Anything filename for known extensions, youtube when it sees junk, pipes or platform suffixes, clean otherwise
t = parse('"Numb" by Linkin Park', mode="youtube")
[a.name for a in t.artists], t.title, t.warnings
# (['Linkin Park'], 'Numb', ('bySplit',))

t = parse("07_Burial_-_Archangel.mp3")
t.mode, [a.name for a in t.artists], t.title, t.position
# ('filename', ['Burial'], 'Archangel', Position(raw='07', number=7))

parse("Shelter", mode="youtube", uploader="Porter Robinson").artists
# (Artist(name='Porter Robinson', role='primary', joiner=None, source='artist'),)

Typing

The package ships py.typed and passes mypy --strict. ParsedTrack, Artist, Version, Junk, Flags, Position and Timestamp are frozen dataclasses with snake_case attributes. Sequences are tuples, so results are immutable and hashable and can go in sets or be used as dict keys. Option dicts have TypedDict types (ParseOptions, KeywordOptions, FormatOptions).

Development

From python/ in the repository, with uv:

uv sync
uv run python scripts/gen_data.py --check  # spec/data codegen is up to date
uv run ruff check . && uv run ruff format --check .
uv run mypy --strict src
uv run pytest                              # every shared fixture, properties, API
uv run python scripts/conformance.py       # pass rate per fixture file and per rule

Modules mirror the JS reference one to one (_scanner.py is scanner.ts, and so on) and cite the same spec rule IDs. Behaviour changes start in the spec, not here: see AGENTS.md.

License

MIT

Metadata

Release files for trackparse 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for trackparse 0.2.0
File Size Uploaded
trackparse-0.2.0.tar.gz 41.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for trackparse 0.2.0
File Interpreter ABI Platform
trackparse-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 91.1 kB

Release files / trackparse-0.2.0.tar.gz

Download URL trackparse-0.2.0.tar.gz
Size 41.1 kB
Tags Source
SHA-256 checksum
How to use checksums
269f59c3457d5e752a32daae964c7075dd7bb3e5844e63db9ea772ae5092cd32
BLAKE2b-256 checksum
How to use checksums
75583ee73796dba45ab5e1d319a4ee5870d2084fb0dc25fe09971c95d3a3036a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / trackparse-0.2.0-py3-none-any.whl

Download URL trackparse-0.2.0-py3-none-any.whl
Size 50.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
066e2aac0a527b399affda18f6cf3ad441241d828f880d665be2c9ab4ad2b702
BLAKE2b-256 checksum
How to use checksums
996eddcb7d08a21b24ba32fb656883604ca1bc6d7fb7e0a31cd6b6f9bb80d897
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page