1pub mod eval;
2pub mod functions;
3pub mod parser;
4
5pub use eval::{MetadataProvider, evaluate};
6pub use parser::{FormatError, Token, parse};
7
8pub fn format(template: &str, provider: &dyn MetadataProvider) -> Result<String, FormatError> {
10 let tokens = parse(template)?;
11 Ok(evaluate(&tokens, provider))
12}
13
14#[cfg(test)]
15mod tests {
16 use super::*;
17 use std::collections::HashMap;
18
19 fn provider(data: &[(&str, &str)]) -> HashMap<String, String> {
20 data.iter()
21 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
22 .collect()
23 }
24
25 #[test]
28 fn basic_artist() {
29 let p = provider(&[("album artist", "Radiohead")]);
30 assert_eq!(format("%album artist%", &p).unwrap(), "Radiohead");
31 }
32
33 #[test]
34 fn conditional_artist_album() {
35 let p = provider(&[("album artist", "Radiohead"), ("album", "OK Computer")]);
36 assert_eq!(
37 format("[%album artist% - ]%album%", &p).unwrap(),
38 "Radiohead - OK Computer"
39 );
40 }
41
42 #[test]
43 fn conditional_missing_artist() {
44 let p = provider(&[("album", "OK Computer")]);
45 assert_eq!(
46 format("[%album artist% - ]%album%", &p).unwrap(),
47 "OK Computer"
48 );
49 }
50
51 #[test]
52 fn quoted_literal_with_function() {
53 let p = provider(&[("date", "1997-06-16"), ("album", "OK Computer")]);
54 assert_eq!(
55 format("['('$left(%date%,4)')' ]%album%", &p).unwrap(),
56 "(1997) OK Computer"
57 );
58 }
59
60 #[test]
61 fn quoted_literal_missing_date() {
62 let p = provider(&[("album", "OK Computer")]);
63 assert_eq!(
64 format("['('$left(%date%,4)')' ]%album%", &p).unwrap(),
65 "OK Computer"
66 );
67 }
68
69 #[test]
70 fn complex_display() {
71 let p = provider(&[
72 ("tracknumber", "3"),
73 ("title", "Subterranean Homesick Alien"),
74 ("album artist", "Radiohead"),
75 ("album", "OK Computer"),
76 ("date", "1997-06-16"),
77 ]);
78 assert_eq!(
79 format(
80 "$num(%tracknumber%,2). %title% - [%album artist% - ]%album%[ '('$left(%date%,4)')']",
81 &p
82 )
83 .unwrap(),
84 "03. Subterranean Homesick Alien - Radiohead - OK Computer (1997)"
85 );
86 }
87
88 #[test]
89 fn if_function_integration() {
90 let p = provider(&[("genre", "Rock")]);
91 assert_eq!(format("$if(%genre%,%genre%,Unknown)", &p).unwrap(), "Rock");
92 }
93
94 #[test]
95 fn if_function_missing() {
96 let p = provider(&[]);
97 assert_eq!(
98 format("$if(%genre%,%genre%,Unknown)", &p).unwrap(),
99 "Unknown"
100 );
101 }
102
103 const PATTERN_1: &str = "%album artist%/$if($stricmp(%album artist%,Various Artists),,['('$left(%date%,4)')' ])%album% '['%codec%']'/[$num(%discnumber%,2)][%tracknumber%. ][%artist% - ]%title%";
112
113 #[test]
114 fn pattern1_normal_album_full_metadata() {
115 let p = provider(&[
116 ("album artist", "Aphex Twin"),
117 ("album", "Selected Ambient Works 85-92"),
118 ("date", "1992-11-09"),
119 ("codec", "FLAC"),
120 ("discnumber", "1"),
121 ("tracknumber", "01"),
122 ("artist", "Aphex Twin"),
123 ("title", "Xtal"),
124 ]);
125 assert_eq!(
126 format(PATTERN_1, &p).unwrap(),
127 "Aphex Twin/(1992) Selected Ambient Works 85-92 [FLAC]/0101. Aphex Twin - Xtal"
128 );
129 }
130
131 #[test]
132 fn pattern1_normal_album_no_disc() {
133 let p = provider(&[
134 ("album artist", "Aphex Twin"),
135 ("album", "Selected Ambient Works 85-92"),
136 ("date", "1992-11-09"),
137 ("codec", "FLAC"),
138 ("tracknumber", "01"),
139 ("artist", "Aphex Twin"),
140 ("title", "Xtal"),
141 ]);
142 assert_eq!(
143 format(PATTERN_1, &p).unwrap(),
144 "Aphex Twin/(1992) Selected Ambient Works 85-92 [FLAC]/01. Aphex Twin - Xtal"
145 );
146 }
147
148 #[test]
149 fn pattern1_normal_album_no_date() {
150 let p = provider(&[
151 ("album artist", "Aphex Twin"),
152 ("album", "Selected Ambient Works 85-92"),
153 ("codec", "FLAC"),
154 ("tracknumber", "01"),
155 ("artist", "Aphex Twin"),
156 ("title", "Xtal"),
157 ]);
158 assert_eq!(
160 format(PATTERN_1, &p).unwrap(),
161 "Aphex Twin/Selected Ambient Works 85-92 [FLAC]/01. Aphex Twin - Xtal"
162 );
163 }
164
165 #[test]
166 fn pattern1_va_album_skips_date() {
167 let p = provider(&[
168 ("album artist", "Various Artists"),
169 ("album", "Warp 20 Recreated"),
170 ("date", "2009"),
171 ("codec", "FLAC"),
172 ("tracknumber", "03"),
173 ("artist", "Flying Lotus"),
174 ("title", "Roygbiv"),
175 ]);
176 assert_eq!(
178 format(PATTERN_1, &p).unwrap(),
179 "Various Artists/Warp 20 Recreated [FLAC]/03. Flying Lotus - Roygbiv"
180 );
181 }
182
183 #[test]
184 fn pattern1_va_case_insensitive() {
185 let p = provider(&[
186 ("album artist", "various artists"),
187 ("album", "Compilation"),
188 ("date", "2020"),
189 ("codec", "MP3"),
190 ("tracknumber", "01"),
191 ("artist", "Some Artist"),
192 ("title", "Some Track"),
193 ]);
194 assert_eq!(
195 format(PATTERN_1, &p).unwrap(),
196 "various artists/Compilation [MP3]/01. Some Artist - Some Track"
197 );
198 }
199
200 #[test]
201 fn pattern1_artist_same_as_album_artist() {
202 let p = provider(&[
203 ("album artist", "Radiohead"),
204 ("album", "OK Computer"),
205 ("date", "1997-06-16"),
206 ("codec", "FLAC"),
207 ("tracknumber", "01"),
208 ("artist", "Radiohead"),
209 ("title", "Airbag"),
210 ]);
211 assert_eq!(
213 format(PATTERN_1, &p).unwrap(),
214 "Radiohead/(1997) OK Computer [FLAC]/01. Radiohead - Airbag"
215 );
216 }
217
218 #[test]
219 fn pattern1_no_artist_tag() {
220 let p = provider(&[
221 ("album artist", "Radiohead"),
222 ("album", "OK Computer"),
223 ("date", "1997"),
224 ("codec", "FLAC"),
225 ("tracknumber", "01"),
226 ("title", "Airbag"),
227 ]);
228 assert_eq!(
230 format(PATTERN_1, &p).unwrap(),
231 "Radiohead/(1997) OK Computer [FLAC]/01. Airbag"
232 );
233 }
234
235 #[test]
236 fn pattern1_multi_disc() {
237 let p = provider(&[
238 ("album artist", "Tool"),
239 ("album", "10,000 Days"),
240 ("date", "2006"),
241 ("codec", "FLAC"),
242 ("discnumber", "1"),
243 ("tracknumber", "01"),
244 ("artist", "Tool"),
245 ("title", "Vicarious"),
246 ]);
247 assert_eq!(
248 format(PATTERN_1, &p).unwrap(),
249 "Tool/(2006) 10,000 Days [FLAC]/0101. Tool - Vicarious"
250 );
251 }
252
253 const PATTERN_2: &str = "$if2(%label%,%album artist%)/%album% '['%codec%']'/[$num(%discnumber%,2)][%tracknumber%. ][%artist% - ]%title%";
258
259 #[test]
260 fn pattern2_with_label() {
261 let p = provider(&[
262 ("label", "Warp Records"),
263 ("album artist", "Aphex Twin"),
264 ("album", "Selected Ambient Works 85-92"),
265 ("codec", "FLAC"),
266 ("tracknumber", "01"),
267 ("artist", "Aphex Twin"),
268 ("title", "Xtal"),
269 ]);
270 assert_eq!(
271 format(PATTERN_2, &p).unwrap(),
272 "Warp Records/Selected Ambient Works 85-92 [FLAC]/01. Aphex Twin - Xtal"
273 );
274 }
275
276 #[test]
277 fn pattern2_no_label_fallback_to_artist() {
278 let p = provider(&[
279 ("album artist", "Aphex Twin"),
280 ("album", "Selected Ambient Works 85-92"),
281 ("codec", "FLAC"),
282 ("tracknumber", "01"),
283 ("artist", "Aphex Twin"),
284 ("title", "Xtal"),
285 ]);
286 assert_eq!(
287 format(PATTERN_2, &p).unwrap(),
288 "Aphex Twin/Selected Ambient Works 85-92 [FLAC]/01. Aphex Twin - Xtal"
289 );
290 }
291
292 #[test]
293 fn pattern2_with_disc_number() {
294 let p = provider(&[
295 ("album artist", "Tool"),
296 ("album", "10,000 Days"),
297 ("codec", "FLAC"),
298 ("discnumber", "2"),
299 ("tracknumber", "01"),
300 ("artist", "Tool"),
301 ("title", "Wings for Marie"),
302 ]);
303 assert_eq!(
304 format(PATTERN_2, &p).unwrap(),
305 "Tool/10,000 Days [FLAC]/0201. Tool - Wings for Marie"
306 );
307 }
308
309 #[test]
310 fn pattern2_minimal_metadata() {
311 let p = provider(&[
312 ("album artist", "Unknown"),
313 ("album", "Untitled"),
314 ("codec", "MP3"),
315 ("title", "Track 1"),
316 ]);
317 assert_eq!(
319 format(PATTERN_2, &p).unwrap(),
320 "Unknown/Untitled [MP3]/Track 1"
321 );
322 }
323
324 #[test]
329 fn nested_function_in_function() {
330 let p = provider(&[("artist", "aphex twin")]);
331 assert_eq!(format("$upper($left(%artist%,5))", &p).unwrap(), "APHEX");
332 }
333
334 #[test]
335 fn multiple_conditionals_adjacent() {
336 let p = provider(&[("artist", "Radiohead"), ("album", "OK Computer")]);
337 assert_eq!(
338 format("[%artist%][ - %album%]", &p).unwrap(),
339 "Radiohead - OK Computer"
340 );
341 }
342
343 #[test]
344 fn multiple_conditionals_partial() {
345 let p = provider(&[("artist", "Radiohead")]);
346 assert_eq!(
347 format("[%artist%][ - %album%][ (%date%)]", &p).unwrap(),
348 "Radiohead"
349 );
350 }
351
352 #[test]
353 fn empty_format_string() {
354 let p = provider(&[]);
355 assert_eq!(format("", &p).unwrap(), "");
356 }
357
358 #[test]
359 fn only_literals() {
360 let p = provider(&[]);
361 assert_eq!(format("hello world", &p).unwrap(), "hello world");
362 }
363
364 #[test]
365 fn quoted_brackets_outside_conditional() {
366 let p = provider(&[("codec", "FLAC")]);
367 assert_eq!(format("'['%codec%']'", &p).unwrap(), "[FLAC]");
368 }
369
370 #[test]
371 fn if_with_stricmp_nested_in_conditional() {
372 let p = provider(&[("album artist", "Radiohead"), ("date", "1997")]);
374 assert_eq!(
375 format(
376 "[$if($stricmp(%album artist%,Various Artists),,'('$left(%date%,4)')' )]%album artist%",
377 &p
378 )
379 .unwrap(),
380 "(1997) Radiohead"
381 );
382 }
383
384 #[test]
385 fn stricmp_in_if_empty_then_branch() {
386 let p = provider(&[("album artist", "Radiohead")]);
388 assert_eq!(
389 format("$if($stricmp(%album artist%,Radiohead),,not radiohead)", &p).unwrap(),
390 "" );
392 }
393
394 #[test]
395 fn stricmp_in_if_else_branch() {
396 let p = provider(&[("album artist", "Aphex Twin")]);
397 assert_eq!(
398 format("$if($stricmp(%album artist%,Radiohead),,not radiohead)", &p).unwrap(),
399 "not radiohead" );
401 }
402
403 #[test]
404 fn if2_both_empty() {
405 let p = provider(&[]);
406 assert_eq!(format("$if2(%label%,%album artist%)", &p).unwrap(), "");
408 }
409
410 #[test]
411 fn organize_standard_layout() {
412 let p = provider(&[
414 ("album artist", "Aphex Twin"),
415 ("date", "1999"),
416 ("album", "Windowlicker EP"),
417 ("tracknumber", "01"),
418 ("title", "Windowlicker"),
419 ]);
420 assert_eq!(
421 format("%album artist%/(%date%) %album%/%tracknumber%. %title%", &p).unwrap(),
422 "Aphex Twin/(1999) Windowlicker EP/01. Windowlicker"
423 );
424 }
425
426 #[test]
427 fn organize_multi_disc() {
428 let p = provider(&[
430 ("album artist", "Tool"),
431 ("date", "2006"),
432 ("album", "10,000 Days"),
433 ("discnumber", "1"),
434 ("tracknumber", "01"),
435 ("title", "Vicarious"),
436 ]);
437 assert_eq!(
438 format(
439 "%album artist%/[(%date%) ]%album%/[%discnumber%-]%tracknumber%. %title%",
440 &p
441 )
442 .unwrap(),
443 "Tool/(2006) 10,000 Days/1-01. Vicarious"
444 );
445 }
446
447 #[test]
448 fn organize_multi_disc_no_disc() {
449 let p = provider(&[
450 ("album artist", "Tool"),
451 ("date", "2006"),
452 ("album", "10,000 Days"),
453 ("tracknumber", "01"),
454 ("title", "Vicarious"),
455 ]);
456 assert_eq!(
457 format(
458 "%album artist%/[(%date%) ]%album%/[%discnumber%-]%tracknumber%. %title%",
459 &p
460 )
461 .unwrap(),
462 "Tool/(2006) 10,000 Days/01. Vicarious"
463 );
464 }
465
466 #[test]
467 fn organize_if2_fallback() {
468 let p = provider(&[
470 ("artist", "Flying Lotus"),
471 ("album", "Cosmogramma"),
472 ("tracknumber", "01"),
473 ("title", "Clock Catcher"),
474 ]);
475 assert_eq!(
476 format(
477 "$if2(%album artist%,%artist%)/%album%/%tracknumber%. %title%",
478 &p
479 )
480 .unwrap(),
481 "Flying Lotus/Cosmogramma/01. Clock Catcher"
482 );
483 }
484
485 #[test]
486 fn organize_genre_based() {
487 let p = provider(&[
488 ("genre", "Electronic"),
489 ("album artist", "Aphex Twin"),
490 ("album", "Windowlicker EP"),
491 ("tracknumber", "01"),
492 ("title", "Windowlicker"),
493 ]);
494 assert_eq!(
495 format(
496 "[%genre%/]%album artist%/%album%/%tracknumber%. %title%",
497 &p
498 )
499 .unwrap(),
500 "Electronic/Aphex Twin/Windowlicker EP/01. Windowlicker"
501 );
502 }
503
504 #[test]
505 fn organize_genre_based_no_genre() {
506 let p = provider(&[
507 ("album artist", "Aphex Twin"),
508 ("album", "Windowlicker EP"),
509 ("tracknumber", "01"),
510 ("title", "Windowlicker"),
511 ]);
512 assert_eq!(
513 format(
514 "[%genre%/]%album artist%/%album%/%tracknumber%. %title%",
515 &p
516 )
517 .unwrap(),
518 "Aphex Twin/Windowlicker EP/01. Windowlicker"
519 );
520 }
521
522 fn bicep_chroma_track1() -> HashMap<String, String> {
541 provider(&[
542 ("album artist", "Bicep"),
543 ("album", "CHROMA 000"),
544 ("artist", "Bicep"),
545 ("date", "2025-11-21"),
546 ("discnumber", "1"),
547 ("label", "CHROMA"),
548 ("title", "CHROMA 001 HELIUM"),
549 ("tracknumber", "01"),
550 ("codec", "FLAC"),
551 ])
552 }
553
554 fn bicep_chroma_track3_feat() -> HashMap<String, String> {
555 provider(&[
556 ("album artist", "Bicep"),
557 ("album", "CHROMA 000"),
558 ("artist", "Dove"),
559 ("date", "2025-11-21"),
560 ("discnumber", "1"),
561 ("label", "CHROMA"),
562 ("title", "CHROMA 003 Bi83 feat. Dove"),
563 ("tracknumber", "03"),
564 ("codec", "FLAC"),
565 ])
566 }
567
568 #[test]
569 fn chroma_pattern2_label_present() {
570 let p = bicep_chroma_track1();
572 assert_eq!(
573 format(PATTERN_2, &p).unwrap(),
574 "CHROMA/CHROMA 000 [FLAC]/0101. Bicep - CHROMA 001 HELIUM"
575 );
576 }
577
578 #[test]
579 fn chroma_pattern1_uses_album_artist_and_date() {
580 let p = bicep_chroma_track1();
582 assert_eq!(
583 format(PATTERN_1, &p).unwrap(),
584 "Bicep/(2025) CHROMA 000 [FLAC]/0101. Bicep - CHROMA 001 HELIUM"
585 );
586 }
587
588 #[test]
589 fn chroma_pattern1_feat_artist() {
590 let p = bicep_chroma_track3_feat();
592 assert_eq!(
593 format(PATTERN_1, &p).unwrap(),
594 "Bicep/(2025) CHROMA 000 [FLAC]/0103. Dove - CHROMA 003 Bi83 feat. Dove"
595 );
596 }
597
598 #[test]
599 fn chroma_pattern2_no_label_falls_through() {
600 let mut p = bicep_chroma_track1();
602 p.remove("label");
603 assert_eq!(
604 format(PATTERN_2, &p).unwrap(),
605 "Bicep/CHROMA 000 [FLAC]/0101. Bicep - CHROMA 001 HELIUM"
606 );
607 }
608
609 const PATTERN_2_WITH_DATE: &str = "$if2(%label%,%album artist%)/['('$left(%date%,4)')' ]%album% '['%codec%']'/[$num(%discnumber%,2)][%tracknumber%. ][%artist% - ]%title%";
612
613 #[test]
614 fn chroma_pattern2_with_date_label_present() {
615 let p = bicep_chroma_track1();
616 assert_eq!(
617 format(PATTERN_2_WITH_DATE, &p).unwrap(),
618 "CHROMA/(2025) CHROMA 000 [FLAC]/0101. Bicep - CHROMA 001 HELIUM"
619 );
620 }
621
622 #[test]
623 fn chroma_pattern2_with_date_no_label() {
624 let mut p = bicep_chroma_track1();
625 p.remove("label");
626 assert_eq!(
627 format(PATTERN_2_WITH_DATE, &p).unwrap(),
628 "Bicep/(2025) CHROMA 000 [FLAC]/0101. Bicep - CHROMA 001 HELIUM"
629 );
630 }
631
632 #[test]
633 fn chroma_track11_tangz_ii() {
634 let p = provider(&[
636 ("album artist", "Bicep"),
637 ("album", "CHROMA 000"),
638 ("artist", "Bicep"),
639 ("date", "2025-11-21"),
640 ("discnumber", "1"),
641 ("title", "CHROMA 012 TANGZ II"),
642 ("tracknumber", "11"),
643 ("codec", "FLAC"),
644 ]);
645 assert_eq!(
646 format(PATTERN_1, &p).unwrap(),
647 "Bicep/(2025) CHROMA 000 [FLAC]/0111. Bicep - CHROMA 012 TANGZ II"
648 );
649 }
650
651 #[test]
652 fn chroma_track10_aloe_ii_dots_in_title() {
653 let p = provider(&[
655 ("album artist", "Bicep"),
656 ("album", "CHROMA 000"),
657 ("artist", "Bicep"),
658 ("date", "2025-11-21"),
659 ("discnumber", "1"),
660 ("title", "CHROMA 011 A.L.O.E II"),
661 ("tracknumber", "10"),
662 ("codec", "FLAC"),
663 ]);
664 assert_eq!(
665 format(PATTERN_1, &p).unwrap(),
666 "Bicep/(2025) CHROMA 000 [FLAC]/0110. Bicep - CHROMA 011 A.L.O.E II"
667 );
668 }
669
670 #[test]
671 fn chroma_track2_feat_in_title() {
672 let p = provider(&[
674 ("album artist", "Bicep"),
675 ("album", "CHROMA 000"),
676 ("artist", "BDB, Bicep"),
677 ("date", "2025-11-21"),
678 ("discnumber", "1"),
679 ("title", "CHROMA 002 L.A.V.A feat. Benjamin Damage"),
680 ("tracknumber", "02"),
681 ("codec", "FLAC"),
682 ]);
683 assert_eq!(
684 format(PATTERN_1, &p).unwrap(),
685 "Bicep/(2025) CHROMA 000 [FLAC]/0102. BDB, Bicep - CHROMA 002 L.A.V.A feat. Benjamin Damage"
686 );
687 }
688}