diff --git a/rose/audiotags.py b/rose/audiotags.py index d515cf7..8134949 100644 --- a/rose/audiotags.py +++ b/rose/audiotags.py @@ -87,6 +87,17 @@ class RoseDate: month: int | None = None day: int | None = None + @classmethod + def parse(cls, value: str | None) -> RoseDate | None: + if not value: + return None + if YEAR_REGEX.match(value): + return RoseDate(year=int(value), month=None, day=None) + # There may be a time value after the date... allow that and other crap. + if m := DATE_REGEX.match(value): + return RoseDate(year=int(m[1]), month=int(m[2]), day=int(m[3])) + return None + def __str__(self) -> str: if self.month is None and self.day is None: return f"{self.year:04}" @@ -161,9 +172,11 @@ def _get_paired_frame(x: str) -> str | None: id=_get_tag(m.tags, ["TXXX:ROSEID"], first=True), release_id=_get_tag(m.tags, ["TXXX:ROSERELEASEID"], first=True), tracktitle=_get_tag(m.tags, ["TIT2"]), - releasedate=_parse_date(_get_tag(m.tags, ["TDRC", "TYER", "TDAT"])), - originaldate=_parse_date(_get_tag(m.tags, ["TDOR", "TORY"])), - compositiondate=_parse_date(_get_tag(m.tags, ["TXXX:COMPOSITIONDATE"], first=True)), + releasedate=RoseDate.parse(_get_tag(m.tags, ["TDRC", "TYER", "TDAT"])), + originaldate=RoseDate.parse(_get_tag(m.tags, ["TDOR", "TORY"])), + compositiondate=RoseDate.parse( + _get_tag(m.tags, ["TXXX:COMPOSITIONDATE"], first=True) + ), tracknumber=tracknumber, tracktotal=tracktotal, discnumber=discnumber, @@ -205,8 +218,8 @@ def _get_paired_frame(x: str) -> str | None: id=_get_tag(m.tags, ["----:net.sunsetglow.rose:ID"]), release_id=_get_tag(m.tags, ["----:net.sunsetglow.rose:RELEASEID"]), tracktitle=_get_tag(m.tags, ["\xa9nam"]), - releasedate=_parse_date(_get_tag(m.tags, ["\xa9day"])), - originaldate=_parse_date( + releasedate=RoseDate.parse(_get_tag(m.tags, ["\xa9day"])), + originaldate=RoseDate.parse( _get_tag( m.tags, [ @@ -216,7 +229,7 @@ def _get_paired_frame(x: str) -> str | None: ], ) ), - compositiondate=_parse_date( + compositiondate=RoseDate.parse( _get_tag(m.tags, ["----:net.sunsetglow.rose:COMPOSITIONDATE"]) ), tracknumber=str(tracknumber), @@ -261,9 +274,9 @@ def _get_paired_frame(x: str) -> str | None: id=_get_tag(m.tags, ["roseid"]), release_id=_get_tag(m.tags, ["rosereleaseid"]), tracktitle=_get_tag(m.tags, ["title"]), - releasedate=_parse_date(_get_tag(m.tags, ["date", "year"])), - originaldate=_parse_date(_get_tag(m.tags, ["originaldate", "originalyear"])), - compositiondate=_parse_date(_get_tag(m.tags, ["compositiondate"])), + releasedate=RoseDate.parse(_get_tag(m.tags, ["date", "year"])), + originaldate=RoseDate.parse(_get_tag(m.tags, ["originaldate", "originalyear"])), + compositiondate=RoseDate.parse(_get_tag(m.tags, ["compositiondate"])), tracknumber=_get_tag(m.tags, ["tracknumber"], first=True), tracktotal=_parse_int(_get_tag(m.tags, ["tracktotal"], first=True)), discnumber=_get_tag(m.tags, ["discnumber"], first=True), @@ -516,17 +529,6 @@ def _parse_int(x: str | None) -> int | None: return None -def _parse_date(value: str | None) -> RoseDate | None: - if not value: - return None - if YEAR_REGEX.match(value): - return RoseDate(year=int(value), month=None, day=None) - # There may be a time value after the date... allow that and other crap. - if m := DATE_REGEX.match(value): - return RoseDate(year=int(m[1]), month=int(m[2]), day=int(m[3])) - return None - - TAG_SPLITTER_REGEX = re.compile(r" \\\\ | / |; ?| vs\. ") diff --git a/rose/cache.py b/rose/cache.py index f510fc4..569474e 100644 --- a/rose/cache.py +++ b/rose/cache.py @@ -266,9 +266,9 @@ def dump(self) -> dict[str, Any]: "added_at": self.added_at, "releasetitle": self.releasetitle, "releasetype": self.releasetype, - "releasedate": self.releasedate, - "originaldate": self.originaldate, - "compositiondate": self.compositiondate, + "releasedate": str(self.releasedate), + "originaldate": str(self.originaldate), + "compositiondate": str(self.compositiondate), "catalognumber": self.catalognumber, "edition": self.edition, "new": self.new, @@ -344,9 +344,9 @@ def dump(self, with_release_info: bool = True) -> dict[str, Any]: "releasetitle": self.release.releasetitle, "releasetype": self.release.releasetype, "disctotal": self.release.disctotal, - "releasedate": self.release.releasedate, - "originaldate": self.release.originaldate, - "compositiondate": self.release.compositiondate, + "releasedate": str(self.release.releasedate), + "originaldate": str(self.release.originaldate), + "compositiondate": str(self.release.compositiondate), "catalognumber": self.release.catalognumber, "edition": self.release.edition, "new": self.release.new, @@ -1910,12 +1910,12 @@ def get_release_logtext(c: Config, release_id: str) -> str | None: def calculate_release_logtext( title: str, - releasedate: int | None, + releasedate: RoseDate | None, artists: ArtistMapping, ) -> str: logtext = f"{artistsfmt(artists)} - " if releasedate: - logtext += f"{releasedate}. " + logtext += f"{releasedate.year}. " logtext += title return logtext @@ -2088,10 +2088,14 @@ def get_track_logtext(c: Config, track_id: str) -> str | None: def calculate_track_logtext( title: str, artists: ArtistMapping, - releasedate: int | None, + releasedate: RoseDate | None, suffix: str, ) -> str: - return f"{artistsfmt(artists)} - {title or 'Unknown Title'} [{releasedate}]{suffix}" + rval = f"{artistsfmt(artists)} - {title or 'Unknown Title'}" + if releasedate: + rval += f" [{releasedate.year}]" + rval += suffix + return rval def list_playlists(c: Config) -> list[str]: diff --git a/rose/cache_test.py b/rose/cache_test.py index eb54bbb..2a34860 100644 --- a/rose/cache_test.py +++ b/rose/cache_test.py @@ -8,7 +8,7 @@ import tomllib from conftest import TEST_COLLAGE_1, TEST_PLAYLIST_1, TEST_RELEASE_1, TEST_RELEASE_2, TEST_RELEASE_3 -from rose.audiotags import AudioTags +from rose.audiotags import AudioTags, RoseDate from rose.cache import ( CACHE_SCHEMA_PATH, STORED_DATA_FILE_REGEX, @@ -1091,7 +1091,7 @@ def test_list_releases(config: Config) -> None: releasetype="album", compositiondate=None, catalognumber=None, - releasedate=2023, + releasedate=RoseDate(2023), disctotal=1, new=False, genres=["Techno", "Deep House"], @@ -1124,7 +1124,7 @@ def test_list_releases(config: Config) -> None: added_at="0000-01-01T00:00:00+00:00", releasetitle="Release 2", releasetype="album", - releasedate=2021, + releasedate=RoseDate(2021), compositiondate=None, catalognumber="DG-001", disctotal=1, @@ -1132,7 +1132,7 @@ def test_list_releases(config: Config) -> None: genres=["Classical"], parent_genres=[], labels=["Native State"], - originaldate=2019, + originaldate=RoseDate(2019), edition="Deluxe", secondary_genres=["Orchestral"], parent_secondary_genres=[ @@ -1153,8 +1153,8 @@ def test_list_releases(config: Config) -> None: added_at="0000-01-01T00:00:00+00:00", releasetitle="Release 3", releasetype="album", - releasedate=2021, - compositiondate=1780, + releasedate=RoseDate(2021), + compositiondate=RoseDate(1780), catalognumber="DG-002", disctotal=1, new=True, @@ -1187,7 +1187,7 @@ def test_get_release_and_associated_tracks(config: Config) -> None: added_at="0000-01-01T00:00:00+00:00", releasetitle="Release 1", releasetype="album", - releasedate=2023, + releasedate=RoseDate(2023), compositiondate=None, catalognumber=None, disctotal=1, @@ -1304,7 +1304,7 @@ def test_list_tracks(config: Config) -> None: added_at="0000-01-01T00:00:00+00:00", releasetitle="Release 1", releasetype="album", - releasedate=2023, + releasedate=RoseDate(2023), compositiondate=None, catalognumber=None, disctotal=1, @@ -1351,7 +1351,7 @@ def test_list_tracks(config: Config) -> None: added_at="0000-01-01T00:00:00+00:00", releasetitle="Release 1", releasetype="album", - releasedate=2023, + releasedate=RoseDate(2023), compositiondate=None, catalognumber=None, disctotal=1, @@ -1400,7 +1400,7 @@ def test_list_tracks(config: Config) -> None: datafile_mtime="999", releasetitle="Release 2", releasetype="album", - releasedate=2021, + releasedate=RoseDate(2021), compositiondate=None, catalognumber="DG-001", new=False, @@ -1408,7 +1408,7 @@ def test_list_tracks(config: Config) -> None: genres=["Classical"], parent_genres=[], labels=["Native State"], - originaldate=2019, + originaldate=RoseDate(2019), edition="Deluxe", secondary_genres=["Orchestral"], parent_secondary_genres=[ @@ -1441,8 +1441,8 @@ def test_list_tracks(config: Config) -> None: datafile_mtime="999", releasetitle="Release 3", releasetype="album", - releasedate=2021, - compositiondate=1780, + releasedate=RoseDate(2021), + compositiondate=RoseDate(1780), catalognumber="DG-002", new=True, disctotal=1, @@ -1485,7 +1485,7 @@ def test_get_track(config: Config) -> None: added_at="0000-01-01T00:00:00+00:00", releasetitle="Release 1", releasetype="album", - releasedate=2023, + releasedate=RoseDate(2023), compositiondate=None, catalognumber=None, disctotal=1, @@ -1598,7 +1598,7 @@ def test_get_collage(config: Config) -> None: datafile_mtime="999", releasetitle="Release 1", releasetype="album", - releasedate=2023, + releasedate=RoseDate(2023), compositiondate=None, catalognumber=None, new=False, @@ -1633,7 +1633,7 @@ def test_get_collage(config: Config) -> None: datafile_mtime="999", releasetitle="Release 2", releasetype="album", - releasedate=2021, + releasedate=RoseDate(2021), compositiondate=None, catalognumber="DG-001", new=False, @@ -1641,7 +1641,7 @@ def test_get_collage(config: Config) -> None: genres=["Classical"], parent_genres=[], labels=["Native State"], - originaldate=2019, + originaldate=RoseDate(2019), edition="Deluxe", secondary_genres=["Orchestral"], parent_secondary_genres=[ @@ -1710,7 +1710,7 @@ def test_get_playlist(config: Config) -> None: added_at="0000-01-01T00:00:00+00:00", releasetitle="Release 1", releasetype="album", - releasedate=2023, + releasedate=RoseDate(2023), compositiondate=None, catalognumber=None, disctotal=1, @@ -1759,7 +1759,7 @@ def test_get_playlist(config: Config) -> None: datafile_mtime="999", releasetitle="Release 2", releasetype="album", - releasedate=2021, + releasedate=RoseDate(2021), compositiondate=None, catalognumber="DG-001", new=False, @@ -1767,7 +1767,7 @@ def test_get_playlist(config: Config) -> None: genres=["Classical"], parent_genres=[], labels=["Native State"], - originaldate=2019, + originaldate=RoseDate(2019), edition="Deluxe", secondary_genres=["Orchestral"], parent_secondary_genres=[ diff --git a/rose/releases.py b/rose/releases.py index e460619..61ce8a5 100644 --- a/rose/releases.py +++ b/rose/releases.py @@ -17,7 +17,7 @@ import tomllib from send2trash import send2trash -from rose.audiotags import AudioTags +from rose.audiotags import AudioTags, RoseDate from rose.cache import ( STORED_DATA_FILE_REGEX, CachedRelease, @@ -238,9 +238,9 @@ class MetadataRelease: title: str new: bool releasetype: str - releasedate: int | None - originaldate: int | None - compositiondate: int | None + releasedate: RoseDate | None + originaldate: RoseDate | None + compositiondate: RoseDate | None artists: list[MetadataArtist] labels: list[str] edition: str | None @@ -295,9 +295,9 @@ def from_toml(cls, toml: str) -> MetadataRelease: title=d["title"], new=d["new"], releasetype=d["releasetype"], - originaldate=d["originaldate"] if d["originaldate"] != -9999 else None, - releasedate=d["releasedate"] if d["releasedate"] != -9999 else None, - compositiondate=d["compositiondate"] if d["compositiondate"] != -9999 else None, + originaldate=RoseDate.parse(d["originaldate"]), + releasedate=RoseDate.parse(d["releasedate"]), + compositiondate=RoseDate.parse(d["compositiondate"]), genres=d["genres"], secondary_genres=d["secondary_genres"], descriptors=d["descriptors"], diff --git a/rose/releases_test.py b/rose/releases_test.py index 6488d9b..b1c74eb 100644 --- a/rose/releases_test.py +++ b/rose/releases_test.py @@ -8,7 +8,7 @@ import tomllib from conftest import TEST_RELEASE_1 -from rose.audiotags import AudioTags +from rose.audiotags import AudioTags, RoseDate from rose.cache import ( CachedRelease, CachedTrack, @@ -136,9 +136,9 @@ def test_edit_release(monkeypatch: Any, config: Config, source_dir: Path) -> Non title = "I Really Love Blackpink" new = false releasetype = "single" - releasedate = 2222 - originaldate = 2000 - compositiondate = 1800 + releasedate = "2222" + originaldate = "2000" + compositiondate = "1800" artists = [ {{ name = "BLACKPINK", role = "main" }}, {{ name = "JISOO", role = "main" }}, @@ -189,9 +189,9 @@ def test_edit_release(monkeypatch: Any, config: Config, source_dir: Path) -> Non datafile_mtime=release.datafile_mtime, releasetitle="I Really Love Blackpink", releasetype="single", - releasedate=2222, - originaldate=2000, - compositiondate=1800, + releasedate=RoseDate(2222), + originaldate=RoseDate(2000), + compositiondate=RoseDate(1800), catalognumber="Lalala", edition="Blabla", new=False, @@ -259,9 +259,9 @@ def test_edit_release_failure_and_resume( title = "I Really Love Blackpink" new = false releasetype = "bullshit" - releasedate = 2222 - originaldate = -9999 - compositiondate = -9999 + releasedate = "2222" + originaldate = "" + compositiondate = "" artists = [ {{ name = "BLACKPINK", role = "main" }}, {{ name = "JISOO", role = "main" }}, @@ -307,9 +307,9 @@ def test_edit_release_failure_and_resume( title = "I Really Love Blackpink" new = false releasetype = "single" - releasedate = 2222 - originaldate = -9999 - compositiondate = -9999 + releasedate = "2222" + originaldate = "" + compositiondate = "" artists = [ {{ name = "BLACKPINK", role = "main" }}, {{ name = "JISOO", role = "main" }}, @@ -363,7 +363,7 @@ def editfn(text: str, **_: Any) -> str: datafile_mtime=release.datafile_mtime, releasetitle="I Really Love Blackpink", releasetype="single", - releasedate=2222, + releasedate=RoseDate(2222), originaldate=None, compositiondate=None, catalognumber=None, diff --git a/rose/rules.py b/rose/rules.py index 407948f..1fdef67 100644 --- a/rose/rules.py +++ b/rose/rules.py @@ -22,7 +22,7 @@ import click -from rose.audiotags import AudioTags +from rose.audiotags import AudioTags, RoseDate from rose.cache import ( CachedRelease, CachedTrack, @@ -310,7 +310,9 @@ def filter_track_false_positives_using_tags( return rval -Changes = tuple[str, str | int | None | list[str], str | int | None | list[str]] +Changes = tuple[ + str, str | int | RoseDate | None | list[str], str | int | RoseDate | None | list[str] +] def execute_metadata_actions( @@ -350,28 +352,28 @@ def artists(xs: list[str]) -> list[Artist]: elif field == "releasedate": v = execute_single_action(act, tags.releasedate) try: - tags.releasedate = int(v) if v else None + tags.releasedate = RoseDate.parse(v) except ValueError as e: raise InvalidReplacementValueError( - f"Failed to assign new value {v} to releasedate: value must be integer" + f"Failed to assign new value {v} to releasedate: value must be date string" ) from e potential_changes.append(("releasedate", origtags.releasedate, tags.releasedate)) elif field == "originaldate": v = execute_single_action(act, tags.originaldate) try: - tags.originaldate = int(v) if v else None + tags.originaldate = RoseDate.parse(v) except ValueError as e: raise InvalidReplacementValueError( - f"Failed to assign new value {v} to originaldate: value must be integer" + f"Failed to assign new value {v} to originaldate: value must be date string" ) from e potential_changes.append(("originaldate", origtags.originaldate, tags.originaldate)) elif field == "compositiondate": v = execute_single_action(act, tags.compositiondate) try: - tags.compositiondate = int(v) if v else None + tags.compositiondate = RoseDate.parse(v) except ValueError as e: raise InvalidReplacementValueError( - f"Failed to assign new value {v} to compositiondate: value must be integer" + f"Failed to assign new value {v} to compositiondate: value must be date string" ) from e potential_changes.append(("compositiondate", origtags.compositiondate, tags.compositiondate)) elif field == "edition": @@ -539,7 +541,7 @@ def artists(xs: list[str]) -> list[Artist]: update_cache_for_releases(c, source_paths) -def matches_pattern(pattern: MatcherPattern, value: str | int | None) -> bool: +def matches_pattern(pattern: MatcherPattern, value: str | int | RoseDate | None) -> bool: value = str(value) if value is not None else "" needle = pattern.pattern @@ -571,7 +573,7 @@ def matches_pattern(pattern: MatcherPattern, value: str | int | None) -> bool: # Factor out the logic for executing an action on a single-value tag and a multi-value tag. -def execute_single_action(action: MetadataAction, value: str | int | None) -> str | None: +def execute_single_action(action: MetadataAction, value: str | int | RoseDate | None) -> str | None: if action.pattern and not matches_pattern(action.pattern, value): return str(value) diff --git a/rose/rules_test.py b/rose/rules_test.py index 0061978..24673b2 100644 --- a/rose/rules_test.py +++ b/rose/rules_test.py @@ -5,7 +5,7 @@ import pytest -from rose.audiotags import AudioTags +from rose.audiotags import AudioTags, RoseDate from rose.cache import ( list_releases, list_tracks, @@ -119,7 +119,7 @@ def test_rules_fields_match_releasedate(config: Config, source_dir: Path) -> Non rule = MetadataRule.parse("releasedate:1990", ["replace:8"]) execute_metadata_rule(config, rule, confirm_yes=False) af = AudioTags.from_file(source_dir / "Test Release 1" / "01.m4a") - assert af.releasedate == 8 + assert af.releasedate == RoseDate(8) def test_rules_fields_match_releasetype(config: Config, source_dir: Path) -> None: diff --git a/rose/templates_test.py b/rose/templates_test.py index deba55b..2482ade 100644 --- a/rose/templates_test.py +++ b/rose/templates_test.py @@ -4,6 +4,7 @@ import click from click.testing import CliRunner +from rose.audiotags import RoseDate from rose.cache import CachedRelease, CachedTrack from rose.common import Artist, ArtistMapping from rose.config import Config @@ -61,7 +62,7 @@ def test_default_templates() -> None: release = deepcopy(EMPTY_CACHED_RELEASE) release.releasetitle = "Title" - release.releasedate = 2023 + release.releasedate = RoseDate(2023) release.releaseartists = ArtistMapping( main=[Artist("A1"), Artist("A2"), Artist("A3")], guest=[Artist("BB")],