1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
//! Turn messy track names into structured data: artists with roles, remixes and versions,
//! junk, and whole tracklists.
//!
//! trackparse is driven by a written spec. Behaviour is defined by [`SPEC.md`] and its shared
//! fixtures. This crate passes every shared fixture, and for every fixture input its output is
//! byte-identical to the JavaScript (npm `trackparse`) and Python (PyPI `trackparse`) packages.
//! Rule IDs such as `R6.2` in these docs refer to the spec.
//!
//! - User documentation: <https://4matic.github.io/trackparse/>
//! - The spec: <https://4matic.github.io/trackparse/reference/spec/>
//!
//! [`SPEC.md`]: https://github.com/4matic/trackparse/blob/main/spec/SPEC.md
//!
//! # Single tracks
//!
//! [`parse`] takes any string and returns a [`ParsedTrack`]. It never panics and never returns
//! an error: what it recognises goes into a field, what it does not stays in the title.
//!
//! ```
//! use trackparse::{parse, ArtistRole, JunkKind, VersionType};
//!
//! let track = parse("Noisia feat. Foreign Beggars - Shellshock (Rockwell Remix) [Official Video]");
//!
//! assert_eq!(track.artists[0].name, "Noisia");
//! assert_eq!(track.artists[1].name, "Foreign Beggars");
//! assert_eq!(track.artists[1].role, ArtistRole::Featured);
//! assert_eq!(track.title, "Shellshock");
//! assert_eq!(track.full_title, "Shellshock (Rockwell Remix)");
//! assert_eq!(track.versions[0].version_type, VersionType::Remix);
//! assert_eq!(track.versions[0].artists[0].name, "Rockwell");
//! assert_eq!(track.junk[0].kind, JunkKind::Video);
//! ```
//!
//! [`parse_with`] takes [`ParseOptions`]; a [`Parser`] keeps resolved options for many calls.
//! [`parse_artists`] splits a credit string, [`format_track`] writes a track back as a string,
//! [`normalize()`] cleans a string without parsing it, and [`all_artists`] lists every credited
//! name, remixers included.
//!
//! ```
//! use trackparse::{format_track_with, parse_with, FeatPlacement, FormatOptions, ParseOptions};
//!
//! let options = ParseOptions::new().known_artists(["Chase & Status"]);
//! let track = parse_with("Chase & Status - Blind Faith ft. Liam Bailey", &options);
//! assert_eq!(track.artists.len(), 2);
//!
//! let format = FormatOptions::new().feat(FeatPlacement::Artist);
//! assert_eq!(format_track_with(&track, &format), "Chase & Status ft. Liam Bailey - Blind Faith");
//! ```
//!
//! # Tracklists
//!
//! The [`tracklist`] module reads a whole pasted tracklist: one entry per line, with its time,
//! its kind and the parsed track, plus warnings about the times. [`tracklist::apply_edits`]
//! rewrites times and lines without touching anything else.
//!
//! ```
//! use trackparse::tracklist::{apply_edits, parse_tracklist, Edit, WarningCode};
//!
//! let text = "00:00 Noisia - Stigma\n03:45 Phace & Misanthrop - Desert Orgy\n02:10 Mefjus - Suicide Bassline";
//! let list = parse_tracklist(text);
//! assert_eq!(list.stats.tracks, 3);
//! assert_eq!(list.entries[1].time.as_ref().unwrap().seconds, 225.0);
//! assert_eq!(list.entries[1].track.as_ref().unwrap().title, "Desert Orgy");
//! assert_eq!(list.warnings[0].code, WarningCode::TimeOutOfOrder);
//!
//! let fixed = apply_edits(text, &[Edit::sort_by_time()]);
//! assert_eq!(
//! fixed.text,
//! "00:00 Noisia - Stigma\n02:10 Mefjus - Suicide Bassline\n03:45 Phace & Misanthrop - Desert Orgy"
//! );
//! ```
//!
//! # JSON
//!
//! Fields are snake_case in Rust. `to_json()` on every output type writes the spec's JSON form:
//! camelCase keys (`fullTitle`, `unknownArtist`), every key present, in the schema's order. It is
//! the same text the JavaScript and Python packages produce, and needs no dependency.
//!
//! ```
//! let artists = trackparse::parse_artists("Noisia & Phace");
//! assert_eq!(
//! artists[1].to_json(),
//! r#"{"name":"Phace","role":"primary","joiner":"&","source":"artist"}"#
//! );
//! ```
//!
//! With the `serde` feature the output types implement `Serialize` and `Deserialize` with the
//! same keys, so `serde_json::to_string(&track)` gives the text of `track.to_json()`. The
//! options and [`tracklist::Edit`] implement both traits too. Numbers are written as
//! `to_json()` writes them. The exceptions are numbers no parse result contains: seconds from
//! 0.000001 up to 0.00001 (and, with older `serde_json` releases, of 1e21 and more) in an
//! `Edit` or an option built by hand, which `serde_json` writes in its own float notation
//! (`1e-6` for `0.000001`). Non-finite seconds are written as `null`, as by `to_json()`, and
//! `null` reads back as NaN. Keyword maps are written without repeated keys (the last value
//! wins, as in a JS object).
//!
//! # Guarantees
//!
//! - Total: any `&str` gives a result. No panics, no `Result` (R0.8).
//! - No regular expressions and no backtracking: the scanners make a bounded number of passes
//! over the input. A 10,000-code-point string parses in a few milliseconds, and the test suite
//! checks that repeated hostile fragments scale far below quadratic.
//! - No `unsafe`. One dependency (`unicode-normalization`); `serde` only with the feature.
//! - The Unicode tables the rules depend on (letters, uppercase letters, lowercase mapping) are
//! compiled in, so the output does not change with the compiler version.
//! - NFC and NFD (R1, R0.4) come from `unicode-normalization` and follow that crate's Unicode
//! version: 0.1.26, the minimum this crate accepts, has the Unicode 18 tables. The JavaScript
//! reference normalizes with its runtime's tables (Unicode 17 on Node 24), so a string with
//! one of the 32 combining marks added in Unicode 18 can come out in another mark order than
//! it does there. Text without those marks is not affected.
pub use SPEC_VERSION;
pub use ;
pub use ;
use ;
use Engine;
/// The examples of the README, run as doc tests (they use the `serde` feature).
;
/// Parse one track string with the default options (mode `auto`).
///
/// Never panics: any string gives a [`ParsedTrack`] (SPEC R0.8).
///
/// ```
/// let track = trackparse::parse("Sub Focus - Out the Blue ft. Alice Gold (2015)");
/// assert_eq!(track.title, "Out the Blue");
/// assert_eq!(track.year, Some(2015));
/// assert_eq!(track.artists.len(), 2);
/// ```
/// Parse one track string with options (R12).
///
/// To parse many strings with the same options, build a [`Parser`] once instead.
///
/// ```
/// use trackparse::{parse_with, Mode, ModeOption, ParseOptions};
///
/// let options = ParseOptions::new().mode(ModeOption::Youtube);
/// let track = parse_with("Noisia - Stigma (Official Video)", &options);
/// assert_eq!(track.mode, Mode::Youtube);
/// assert_eq!(track.title, "Stigma");
/// assert_eq!(track.junk[0].raw, "Official Video");
/// ```
/// Split a credit string into artists (R10).
///
/// ```
/// let artists = trackparse::parse_artists("Noisia & The Upbeats feat. Foreign Beggars");
/// let names: Vec<&str> = artists.iter().map(|a| a.name.as_str()).collect();
/// assert_eq!(names, ["Noisia", "The Upbeats", "Foreign Beggars"]);
/// ```
/// [`parse_artists`] with options (`known_artists`, `split_and`, `keywords`).
///
/// ```
/// use trackparse::{parse_artists_with, ParseOptions};
///
/// let options = ParseOptions::new().known_artists(["Above & Beyond"]);
/// let artists = parse_artists_with("Above & Beyond & Zoë Johnston", &options);
/// let names: Vec<&str> = artists.iter().map(|a| a.name.as_str()).collect();
/// assert_eq!(names, ["Above & Beyond", "Zoë Johnston"]);
/// ```
/// Render a parsed track back to a string (R11) with the default format options.
///
/// ```
/// use trackparse::{format_track, parse};
///
/// let track = parse("Sub Focus - Out the Blue ft. Alice Gold");
/// assert_eq!(format_track(&track), "Sub Focus - Out the Blue (ft. Alice Gold)");
/// ```
/// Render a parsed track back to a string (R11).
///
/// ```
/// use trackparse::{format_track_with, parse, FeatPlacement, FormatOptions};
///
/// let track = parse("Sub Focus - Out the Blue ft. Alice Gold");
/// let options = FormatOptions::new().feat(FeatPlacement::Artist);
/// assert_eq!(format_track_with(&track, &options), "Sub Focus ft. Alice Gold - Out the Blue");
/// ```
/// Normalize a track string (R1): NFC, ASCII brackets, quotes and dashes, invisible characters
/// removed, whitespace collapsed. Idempotent.
///
/// ```
/// assert_eq!(trackparse::normalize(" Noisia – Stigma\u{200b} "), "Noisia - Stigma");
/// ```
/// All credited names: the track's artists plus every version's remixers, deduped (R0.4 key).
///
/// ```
/// let track = trackparse::parse("Fox Stevenson - Bruises (Magnetude Remix)");
/// let names: Vec<String> = trackparse::all_artists(&track).into_iter().map(|a| a.name).collect();
/// assert_eq!(names, ["Fox Stevenson", "Magnetude"]);
/// ```
/// A parser with its options resolved and its vocabulary tables built once (`createParser` in
/// JavaScript, `create_parser` in Python). Use it to parse many strings with the same options.
/// It is `Send + Sync`, and cloning it is cheap (the tables are shared).
///
/// ```
/// use trackparse::{ParseOptions, Parser};
///
/// let parser = Parser::new(&ParseOptions::new().known_artists(["Chase & Status"]));
/// let track = parser.parse("Chase & Status - Blind Faith");
/// assert_eq!(track.artists.len(), 1);
/// assert_eq!(track.artists[0].name, "Chase & Status");
/// ```