diff --git a/docs/METADATA_TOOLS.md b/docs/METADATA_TOOLS.md index b9e6515..e8561cd 100644 --- a/docs/METADATA_TOOLS.md +++ b/docs/METADATA_TOOLS.md @@ -44,7 +44,7 @@ genres = [ "Dance-Pop", "Future Bass", ] -secondary_genres = [ +secondarygenres = [ "Electropop", "Alternative R&B", "Synthpop", @@ -320,15 +320,18 @@ The rules engine supports matching and acting on the following tags: - `originalyear` - `compositionyear` - `genre` -- `secondary_genre` +- `parentgenre` +- `secondarygenre` +- `parentsecondarygenre` - `descriptor` - `label` - `catalognumber` - `edition` -The `trackartist[*]`, `releaseartist[*]`, `genre`, `secondary_genre`, `descriptor`, and `label` tags -are _multi-value_ tags, which have a slightly different behavior from single-value tags for some of -the actions. We'll explore this difference in the [Actions](#actions) section. +The `trackartist[*]`, `releaseartist[*]`, `genre` (& parents), `secondarygenre` (& parents), +`descriptor`, and `label` tags are _multi-value_ tags, which have a slightly different behavior from +single-value tags for some of the actions. We'll explore this difference in the [Actions](#actions) +section. For convenience, the rules parser also allows you to specify _tag aliases_ in place of the above tags, which expand to multiple tags when matching. The diff --git a/docs/TAGGING_CONVENTIONS.md b/docs/TAGGING_CONVENTIONS.md index c582711..814f893 100644 --- a/docs/TAGGING_CONVENTIONS.md +++ b/docs/TAGGING_CONVENTIONS.md @@ -135,7 +135,7 @@ world, Rosé will support reading from additional fields. | Release Artists | `aART` | | | Release Type | `----:com.apple.iTunes:RELEASETYPE` | `----:com.apple.iTunes:MusicBrainz Album Type` | | Release Year | `\xa9day` | | -| Original Year | `----:net.sunsetglow.rose:ORIGINALDATE` | `----:com.apple.iTunes:ORIGINALDATE`, `----:com.apple.iTunes:ORIGINALYEAR` | +| Original Year | `----:net.sunsetglow.rose:ORIGINALDATE` | `----:com.apple.iTunes:ORIGINALDATE`, `----:com.apple.iTunes:ORIGINALYEAR` | | Composition Year | `----:net.sunsetglow.rose:COMPOSITIONDATE` | | | Genre | `\xa9gen` | | | Secondary Genre | `----:net.sunsetglow.rose:SECONDARYGENRE` | | diff --git a/rose/rule_parser.py b/rose/rule_parser.py index 67ce340..f88cbcc 100644 --- a/rose/rule_parser.py +++ b/rose/rule_parser.py @@ -66,9 +66,13 @@ def __str__(self) -> str: "releaseartist[djmixer]", "releasetype", "releaseyear", + "originalyear", "compositionyear", "catalognumber", + "edition", "genre", + "secondarygenre", + "descriptor", "label", ] @@ -115,9 +119,13 @@ def __str__(self) -> str: "releaseartist[djmixer]": ["releaseartist[djmixer]"], "releasetype": ["releasetype"], "releaseyear": ["releaseyear"], + "originalyear": ["originalyear"], "compositionyear": ["compositionyear"], + "edition": ["edition"], "catalognumber": ["catalognumber"], "genre": ["genre"], + "secondarygenre": ["secondarygenre"], + "descriptor": ["descriptor"], "label": ["label"], "artist": [ "trackartist[main]", @@ -158,9 +166,13 @@ def __str__(self) -> str: "releaseartist[djmixer]", "releasetype", "releaseyear", + "originalyear", "compositionyear", + "edition", "catalognumber", "genre", + "secondarygenre", + "descriptor", "label", ] @@ -173,7 +185,9 @@ def __str__(self) -> str: "releasetitle", "releasetype", "releaseyear", + "originalyear", "compositionyear", + "edition", "catalognumber", ] @@ -189,9 +203,13 @@ def __str__(self) -> str: "releasetype", "releasetype", "releaseyear", + "originalyear", "compositionyear", + "edition", "catalognumber", "genre", + "secondarygenre", + "descriptor", "label", "disctotal", ] diff --git a/rose/rule_parser_test.py b/rose/rule_parser_test.py index 547cc22..997c603 100644 --- a/rose/rule_parser_test.py +++ b/rose/rule_parser_test.py @@ -77,7 +77,7 @@ def test_err(rule: str, err: str) -> None: tracknumber^Track$ ^ - Invalid tag: must be one of {tracktitle, trackartist, trackartist[main], trackartist[guest], trackartist[remixer], trackartist[producer], trackartist[composer], trackartist[conductor], trackartist[djmixer], tracknumber, tracktotal, discnumber, disctotal, releasetitle, releaseartist, releaseartist[main], releaseartist[guest], releaseartist[remixer], releaseartist[producer], releaseartist[composer], releaseartist[conductor], releaseartist[djmixer], releasetype, releaseyear, compositionyear, catalognumber, genre, label, artist}. The next character after a tag must be ':' or ','. + Invalid tag: must be one of {tracktitle, trackartist, trackartist[main], trackartist[guest], trackartist[remixer], trackartist[producer], trackartist[composer], trackartist[conductor], trackartist[djmixer], tracknumber, tracktotal, discnumber, disctotal, releasetitle, releaseartist, releaseartist[main], releaseartist[guest], releaseartist[remixer], releaseartist[producer], releaseartist[composer], releaseartist[conductor], releaseartist[djmixer], releasetype, releaseyear, originalyear, compositionyear, edition, catalognumber, genre, secondarygenre, descriptor, label, artist}. The next character after a tag must be ':' or ','. """, ) @@ -250,7 +250,7 @@ def test_err(rule: str, err: str, matcher: MetadataMatcher | None = None) -> Non haha/delete ^ - Invalid tag: must be one of {tracktitle, trackartist, trackartist[main], trackartist[guest], trackartist[remixer], trackartist[producer], trackartist[composer], trackartist[conductor], trackartist[djmixer], tracknumber, discnumber, releasetitle, releaseartist, releaseartist[main], releaseartist[guest], releaseartist[remixer], releaseartist[producer], releaseartist[composer], releaseartist[conductor], releaseartist[djmixer], releasetype, releaseyear, compositionyear, catalognumber, genre, label, artist}. The next character after a tag must be ':' or ','. + Invalid tag: must be one of {tracktitle, trackartist, trackartist[main], trackartist[guest], trackartist[remixer], trackartist[producer], trackartist[composer], trackartist[conductor], trackartist[djmixer], tracknumber, discnumber, releasetitle, releaseartist, releaseartist[main], releaseartist[guest], releaseartist[remixer], releaseartist[producer], releaseartist[composer], releaseartist[conductor], releaseartist[djmixer], releasetype, releaseyear, originalyear, compositionyear, edition, catalognumber, genre, secondarygenre, descriptor, label, artist}. The next character after a tag must be ':' or ','. """, ) @@ -261,7 +261,7 @@ def test_err(rule: str, err: str, matcher: MetadataMatcher | None = None) -> Non tracktitler/delete ^ - Invalid tag: must be one of {tracktitle, trackartist, trackartist[main], trackartist[guest], trackartist[remixer], trackartist[producer], trackartist[composer], trackartist[conductor], trackartist[djmixer], tracknumber, discnumber, releasetitle, releaseartist, releaseartist[main], releaseartist[guest], releaseartist[remixer], releaseartist[producer], releaseartist[composer], releaseartist[conductor], releaseartist[djmixer], releasetype, releaseyear, compositionyear, catalognumber, genre, label, artist}. The next character after a tag must be ':' or ','. + Invalid tag: must be one of {tracktitle, trackartist, trackartist[main], trackartist[guest], trackartist[remixer], trackartist[producer], trackartist[composer], trackartist[conductor], trackartist[djmixer], tracknumber, discnumber, releasetitle, releaseartist, releaseartist[main], releaseartist[guest], releaseartist[remixer], releaseartist[producer], releaseartist[composer], releaseartist[conductor], releaseartist[djmixer], releasetype, releaseyear, originalyear, compositionyear, edition, catalognumber, genre, secondarygenre, descriptor, label, artist}. The next character after a tag must be ':' or ','. """, ) diff --git a/rose/rules.py b/rose/rules.py index 8b769b6..f276faf 100644 --- a/rose/rules.py +++ b/rose/rules.py @@ -228,7 +228,9 @@ def filter_track_false_positives_using_tags( # fmt: off match = match or (field == "tracktitle" and matches_pattern(matcher.pattern, tags.tracktitle)) match = match or (field == "releaseyear" and matches_pattern(matcher.pattern, tags.releaseyear)) + match = match or (field == "originalyear" and matches_pattern(matcher.pattern, tags.originalyear)) match = match or (field == "compositionyear" and matches_pattern(matcher.pattern, tags.compositionyear)) + match = match or (field == "edition" and matches_pattern(matcher.pattern, tags.edition)) match = match or (field == "catalognumber" and matches_pattern(matcher.pattern, tags.catalognumber)) match = match or (field == "tracknumber" and matches_pattern(matcher.pattern, tags.tracknumber)) match = match or (field == "tracktotal" and matches_pattern(matcher.pattern, tags.tracktotal)) @@ -237,6 +239,8 @@ def filter_track_false_positives_using_tags( match = match or (field == "releasetitle" and matches_pattern(matcher.pattern, tags.releasetitle)) match = match or (field == "releasetype" and matches_pattern(matcher.pattern, tags.releasetype)) match = match or (field == "genre" and any(matches_pattern(matcher.pattern, x) for x in tags.genre)) + match = match or (field == "secondarygenre" and any(matches_pattern(matcher.pattern, x) for x in tags.secondarygenre)) + match = match or (field == "descriptor" and any(matches_pattern(matcher.pattern, x) for x in tags.descriptor)) match = match or (field == "label" and any(matches_pattern(matcher.pattern, x) for x in tags.label)) match = match or (field == "trackartist[main]" and any(matches_pattern(matcher.pattern, x.name) for x in tags.releaseartists.main)) match = match or (field == "trackartist[guest]" and any(matches_pattern(matcher.pattern, x.name) for x in tags.releaseartists.guest)) @@ -262,7 +266,9 @@ def filter_track_false_positives_using_tags( # fmt: off skip = skip or (field == "tracktitle" and matches_pattern(i.pattern, tags.tracktitle)) skip = skip or (field == "releaseyear" and matches_pattern(i.pattern, tags.releaseyear)) + skip = skip or (field == "originalyear" and matches_pattern(i.pattern, tags.originalyear)) skip = skip or (field == "compositionyear" and matches_pattern(i.pattern, tags.compositionyear)) + skip = skip or (field == "edition" and matches_pattern(i.pattern, tags.edition)) skip = skip or (field == "catalognumber" and matches_pattern(i.pattern, tags.catalognumber)) skip = skip or (field == "tracknumber" and matches_pattern(i.pattern, tags.tracknumber)) skip = skip or (field == "tracktotal" and matches_pattern(i.pattern, tags.tracktotal)) @@ -271,6 +277,8 @@ def filter_track_false_positives_using_tags( skip = skip or (field == "releasetitle" and matches_pattern(i.pattern, tags.releasetitle)) skip = skip or (field == "releasetype" and matches_pattern(i.pattern, tags.releasetype)) skip = skip or (field == "genre" and any(matches_pattern(i.pattern, x) for x in tags.genre)) + skip = skip or (field == "secondarygenre" and any(matches_pattern(i.pattern, x) for x in tags.secondarygenre)) + skip = skip or (field == "descriptor" and any(matches_pattern(i.pattern, x) for x in tags.descriptor)) skip = skip or (field == "label" and any(matches_pattern(i.pattern, x) for x in tags.label)) skip = skip or (field == "trackartist[main]" and any(matches_pattern(i.pattern, x.name) for x in tags.releaseartists.main)) skip = skip or (field == "trackartist[guest]" and any(matches_pattern(i.pattern, x.name) for x in tags.releaseartists.guest)) @@ -348,6 +356,15 @@ def artists(xs: list[str]) -> list[Artist]: f"Failed to assign new value {v} to releaseyear: value must be integer" ) from e potential_changes.append(("releaseyear", origtags.releaseyear, tags.releaseyear)) + elif field == "originalyear": + v = execute_single_action(act, tags.originalyear) + try: + tags.originalyear = int(v) if v else None + except ValueError as e: + raise InvalidReplacementValueError( + f"Failed to assign new value {v} to originalyear: value must be integer" + ) from e + potential_changes.append(("originalyear", origtags.originalyear, tags.originalyear)) elif field == "compositionyear": v = execute_single_action(act, tags.compositionyear) try: @@ -357,6 +374,9 @@ def artists(xs: list[str]) -> list[Artist]: f"Failed to assign new value {v} to compositionyear: value must be integer" ) from e potential_changes.append(("compositionyear", origtags.compositionyear, tags.compositionyear)) + elif field == "edition": + tags.edition = execute_single_action(act, tags.edition) + potential_changes.append(("edition", origtags.edition, tags.edition)) elif field == "catalognumber": tags.catalognumber = execute_single_action(act, tags.catalognumber) potential_changes.append(("catalognumber", origtags.catalognumber, tags.catalognumber)) @@ -375,6 +395,12 @@ def artists(xs: list[str]) -> list[Artist]: elif field == "genre": tags.genre = execute_multi_value_action(act, tags.genre) potential_changes.append(("genre", origtags.genre, tags.genre)) + elif field == "secondarygenre": + tags.secondarygenre = execute_multi_value_action(act, tags.secondarygenre) + potential_changes.append(("secondarygenre", origtags.secondarygenre, tags.secondarygenre)) + elif field == "descriptor": + tags.descriptor = execute_multi_value_action(act, tags.descriptor) + potential_changes.append(("descriptor", origtags.descriptor, tags.descriptor)) elif field == "label": tags.label = execute_multi_value_action(act, tags.label) potential_changes.append(("label", origtags.label, tags.label)) @@ -659,7 +685,9 @@ def filter_track_false_positives_using_read_cache( # fmt: off match = match or (field == "tracktitle" and matches_pattern(matcher.pattern, t.tracktitle)) match = match or (field == "releaseyear" and matches_pattern(matcher.pattern, t.release.releaseyear)) + match = match or (field == "originalyear" and matches_pattern(matcher.pattern, t.release.originalyear)) match = match or (field == "compositionyear" and matches_pattern(matcher.pattern, t.release.compositionyear)) + match = match or (field == "edition" and matches_pattern(matcher.pattern, t.release.edition)) match = match or (field == "catalognumber" and matches_pattern(matcher.pattern, t.release.catalognumber)) match = match or (field == "tracknumber" and matches_pattern(matcher.pattern, t.tracknumber)) match = match or (field == "tracktotal" and matches_pattern(matcher.pattern, t.tracktotal)) @@ -668,6 +696,8 @@ def filter_track_false_positives_using_read_cache( match = match or (field == "releasetitle" and matches_pattern(matcher.pattern, t.release.releasetitle)) match = match or (field == "releasetype" and matches_pattern(matcher.pattern, t.release.releasetype)) match = match or (field == "genre" and any(matches_pattern(matcher.pattern, x) for x in t.release.genres)) + match = match or (field == "secondarygenre" and any(matches_pattern(matcher.pattern, x) for x in t.release.secondary_genres)) + match = match or (field == "descriptor" and any(matches_pattern(matcher.pattern, x) for x in t.release.descriptors)) match = match or (field == "label" and any(matches_pattern(matcher.pattern, x) for x in t.release.labels)) match = match or (field == "trackartist[main]" and any(matches_pattern(matcher.pattern, x.name) for x in t.trackartists.main)) match = match or (field == "trackartist[guest]" and any(matches_pattern(matcher.pattern, x.name) for x in t.trackartists.guest)) @@ -705,11 +735,15 @@ def filter_release_false_positives_using_read_cache( # Only attempt to match the release tags; ignore track tags. # fmt: off match = match or (field == "releaseyear" and matches_pattern(matcher.pattern, r.releaseyear)) + match = match or (field == "originalyear" and matches_pattern(matcher.pattern, r.originalyear)) match = match or (field == "compositionyear" and matches_pattern(matcher.pattern, r.compositionyear)) + match = match or (field == "edition" and matches_pattern(matcher.pattern, r.edition)) match = match or (field == "catalognumber" and matches_pattern(matcher.pattern, r.catalognumber)) match = match or (field == "releasetitle" and matches_pattern(matcher.pattern, r.releasetitle)) match = match or (field == "releasetype" and matches_pattern(matcher.pattern, r.releasetype)) match = match or (field == "genre" and any(matches_pattern(matcher.pattern, x) for x in r.genres)) + match = match or (field == "secondarygenre" and any(matches_pattern(matcher.pattern, x) for x in r.secondary_genres)) + match = match or (field == "descriptor" and any(matches_pattern(matcher.pattern, x) for x in r.descriptors)) match = match or (field == "label" and any(matches_pattern(matcher.pattern, x) for x in r.labels)) match = match or (field == "releaseartist[main]" and any(matches_pattern(matcher.pattern, x.name) for x in r.releaseartists.main)) match = match or (field == "releaseartist[guest]" and any(matches_pattern(matcher.pattern, x.name) for x in r.releaseartists.guest)) diff --git a/rose/templates.py b/rose/templates.py index 2b68140..7fd4c74 100644 --- a/rose/templates.py +++ b/rose/templates.py @@ -276,12 +276,17 @@ def _calc_release_variables(release: CachedRelease, position: str | None) -> dic "releasetitle": release.releasetitle, "releasetype": release.releasetype, "releaseyear": release.releaseyear, + "originalyear": release.originalyear, "compositionyear": release.compositionyear, + "edition": release.edition, "catalognumber": release.catalognumber, "new": release.new, "disctotal": release.disctotal, "genres": release.genres, - "parent_genres": release.parent_genres, + "parentgenres": release.parent_genres, + "secondarygenres": release.secondary_genres, + "parentsecondarygenres": release.parent_secondary_genres, + "descriptors": release.descriptors, "labels": release.labels, "releaseartists": release.releaseartists, "position": position, @@ -301,11 +306,16 @@ def _calc_track_variables(track: CachedTrack, position: str | None) -> dict[str, "releasetitle": track.release.releasetitle, "releasetype": track.release.releasetype, "releaseyear": track.release.releaseyear, + "originalyear": track.release.originalyear, "compositionyear": track.release.compositionyear, + "edition": track.release.edition, "catalognumber": track.release.catalognumber, "new": track.release.new, "genres": track.release.genres, - "parent_genres": track.release.parent_genres, + "parentgenres": track.release.parent_genres, + "secondarygenres": track.release.secondary_genres, + "parentsecondarygenres": track.release.parent_secondary_genres, + "descriptors": track.release.descriptors, "labels": track.release.labels, "releaseartists": track.release.releaseartists, "position": position, @@ -362,12 +372,30 @@ def _get_preview_releases(c: Config) -> tuple[CachedRelease, CachedRelease, Cach releasetitle="Kim Lip", releasetype="single", releaseyear=2017, + originalyear=2017, compositionyear=None, + edition=None, catalognumber="CMCC11088", new=True, disctotal=1, genres=["K-Pop", "Dance-Pop", "Contemporary R&B"], parent_genres=["Pop", "R&B"], + secondary_genres=["Synth Funk", "Synthpop", "Future Bass"], + parent_secondary_genres=["Funk", "Pop"], + descriptors=[ + "Female Vocalist", + "Mellow", + "Sensual", + "Ethereal", + "Love", + "Lush", + "Romantic", + "Warm", + "Melodic", + "Passionate", + "Nocturnal", + "Summer", + ], labels=["BlockBerryCreative"], releaseartists=ArtistMapping(main=[Artist("Kim Lip")]), metahash="0", @@ -382,12 +410,33 @@ def _get_preview_releases(c: Config) -> tuple[CachedRelease, CachedRelease, Cach releasetitle="Young Forever (花樣年華)", releasetype="album", releaseyear=2016, + originalyear=2016, compositionyear=None, + edition="Deluxe", catalognumber="L200001238", new=False, disctotal=2, genres=["K-Pop"], parent_genres=["Pop"], + secondary_genres=["Pop Rap", "Electropop"], + parent_secondary_genres=["Hip Hop", "Electronic"], + descriptors=[ + "Autumn", + "Passionate", + "Melodic", + "Romantic", + "Eclectic", + "Melancholic", + "Male Vocalist", + "Sentimental", + "Uplifting", + "Breakup", + "Love", + "Anthemic", + "Lush", + "Bittersweet", + "Spring", + ], labels=["BIGHIT"], releaseartists=ArtistMapping(main=[Artist("BTS")]), metahash="0", @@ -403,12 +452,17 @@ def _get_preview_releases(c: Config) -> tuple[CachedRelease, CachedRelease, Cach releasetitle="Images", releasetype="album", releaseyear=1992, + originalyear=1991, compositionyear=1907, + edition=None, catalognumber="435-766 2", new=False, disctotal=2, genres=["Impressionism, Orchestral"], parent_genres=["Classical"], + secondary_genres=["Tone Poem"], + parent_secondary_genres=["Orchestral"], + descriptors=["Orchestral"], labels=["Deustche Grammophon"], releaseartists=ArtistMapping( main=[Artist("Cleveland Orchestra")], diff --git a/rose/templates_test.py b/rose/templates_test.py index f647df7..9388b1d 100644 --- a/rose/templates_test.py +++ b/rose/templates_test.py @@ -25,12 +25,17 @@ releasetitle="", releasetype="unknown", releaseyear=None, + originalyear=None, compositionyear=None, + edition=None, catalognumber=None, new=False, disctotal=1, genres=[], parent_genres=[], + secondary_genres=[], + parent_secondary_genres=[], + descriptors=[], labels=[], releaseartists=ArtistMapping(), metahash="0",