diff --git a/README.md b/README.md index ff38e20..4e87d28 100644 --- a/README.md +++ b/README.md @@ -351,9 +351,6 @@ Rosé's CLI is also designed to make scripting against your library easy. Operat release" and "jump to artist" can be expressed as a bash one-liner and integrated into your file manager. -See [Shell Scripting](./docs/SHELL_SCRIPTING.md) for additional documentation on scripting with -Rosé. - # Learn More For additional documentation, please refer to the following files: diff --git a/conftest.py b/conftest.py index 79046ba..c9d1a40 100644 --- a/conftest.py +++ b/conftest.py @@ -112,10 +112,10 @@ def seeded_cache(config: Config) -> None: conn.executescript( f"""\ INSERT INTO releases - (id , source_path , cover_image_path , added_at , datafile_mtime, title , releasetype, releaseyear, disctotal, new , metahash) -VALUES ('r1', '{dirpaths[0]}', null , '0000-01-01T00:00:00+00:00', '999' , 'Release 1', 'album' , 2023 , 1 , false, '1') - , ('r2', '{dirpaths[1]}', '{imagepaths[0]}', '0000-01-01T00:00:00+00:00', '999' , 'Release 2', 'album' , 2021 , 1 , false, '2') - , ('r3', '{dirpaths[2]}', null , '0000-01-01T00:00:00+00:00', '999' , 'Release 3', 'album' , 2021 , 1 , true , '3'); + (id , source_path , cover_image_path , added_at , datafile_mtime, title , releasetype, releaseyear, compositionyear, catalognumber, disctotal, new , metahash) +VALUES ('r1', '{dirpaths[0]}', null , '0000-01-01T00:00:00+00:00', '999' , 'Release 1', 'album' , 2023 , null , null , 1 , false, '1') + , ('r2', '{dirpaths[1]}', '{imagepaths[0]}', '0000-01-01T00:00:00+00:00', '999' , 'Release 2', 'album' , 2021 , null , 'DG-001' , 1 , false, '2') + , ('r3', '{dirpaths[2]}', null , '0000-01-01T00:00:00+00:00', '999' , 'Release 3', 'album' , 2021 , 1780 , 'DG-002' , 1 , true , '3'); INSERT INTO releases_genres (release_id, genre , genre_sanitized, position) @@ -184,6 +184,8 @@ def seeded_cache(config: Config) -> None: , discnumber , releasetitle , releaseyear + , compositionyear + , catalognumber , releasetype , genre , label @@ -197,6 +199,8 @@ def seeded_cache(config: Config) -> None: , process_string_for_fts(t.discnumber) AS discnumber , process_string_for_fts(r.title) AS releasetitle , process_string_for_fts(r.releaseyear) AS releaseyear + , process_string_for_fts(r.compositionyear) AS compositionyear + , process_string_for_fts(r.catalognumber) AS catalognumber , process_string_for_fts(r.releasetype) AS releasetype , process_string_for_fts(COALESCE(GROUP_CONCAT(rg.genre, ' '), '')) AS genre , process_string_for_fts(COALESCE(GROUP_CONCAT(rl.label, ' '), '')) AS label diff --git a/docs/METADATA_TOOLS.md b/docs/METADATA_TOOLS.md index 13da1c6..f7498bc 100644 --- a/docs/METADATA_TOOLS.md +++ b/docs/METADATA_TOOLS.md @@ -40,6 +40,7 @@ title = "Mix & Match" new = false releasetype = "ep" releaseyear = 2017 +compositionyear = -9999 genres = [ "Dance-Pop", "Future Bass", @@ -48,6 +49,7 @@ genres = [ labels = [ "BlockBerry Creative", ] +catalognumber = "WMED0709" artists = [ { name = "LOOΠΔ ODD EYE CIRCLE", role = "main" }, ] @@ -295,8 +297,10 @@ The rules engine supports matching and acting on the following tags: - `releaseartist[djmixer]` - `releasetype` - `releaseyear` +- `compositionyear` - `genre` - `label` +- `catalognumber` The `trackartist[*]`, `releaseartist[*]`, `genre`, 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 diff --git a/docs/README.md b/docs/README.md index 82e0044..44dfeaf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,7 +10,6 @@ For more detailed documentation, please visit the following files: - [Improving Your Music Metadata](./METADATA_TOOLS.md) - [Maintaining the Cache](./CACHE_MAINTENANCE.md) - [Directory & Filename Templates](./TEMPLATES.md) -- [Shell Scripting](./SHELL_SCRIPTING.md) - [Tagging Conventions](./docs/TAGGING_CONVENTIONS.md) - [Architecture](./ARCHITECTURE.md) diff --git a/docs/SHELL_SCRIPTING.md b/docs/SHELL_SCRIPTING.md deleted file mode 100644 index a614093..0000000 --- a/docs/SHELL_SCRIPTING.md +++ /dev/null @@ -1,3 +0,0 @@ -# Shell Scripting - -_TODO_ diff --git a/docs/TEMPLATES.md b/docs/TEMPLATES.md index dc79afb..7fa3468 100644 --- a/docs/TEMPLATES.md +++ b/docs/TEMPLATES.md @@ -77,12 +77,14 @@ Rosé provides the following template variables for releases: ```python added_at: str # ISO8601 timestamp of when the release was added to the library. releasetitle: str -releasetype: str # Type of the release (e.g. single, ep, etc). One of the enums as defined in TAGGING_CONVENTIONS.md. +releasetype: str # The type of the release (e.g. single, ep, etc). One of the enums as defined in TAGGING_CONVENTIONS.md. releaseyear: int | None +compositionyear: int | None # The year that the release was composed. Mainly of interest in classical music. new: bool # The "new"-ness of the release. See RELEASES.md for documentation on this feature. disctotal: int # The number of discs in the release. genres: list[str] labels: list[str] +catalognumber: str | None releaseartists: ArtistMapping # All release artists: an object with 6 properties, each corresponding to one role. releaseartists.main: list[Artist] # The Artist object has a `name` property with the artist name. releaseartists.guest: list[Artist] @@ -113,11 +115,13 @@ trackartists.composer: list[Artist] trackartists.conductor: list[Artist] trackartists.djmixer: list[Artist] releasetitle: str -releasetype: str # Type of the track's release (e.g. single, ep, etc). +releasetype: str # The type of the track's release (e.g. single, ep, etc). releaseyear: int | None +compositionyear: int | None # The year that the release was composed. Mainly of interest in classical music. new: bool # The "new"-ness of the track's release. genres: list[str] labels: list[str] +catalognumber: str | None releaseartists: ArtistMapping # All release artists: an object with 6 properties, each corresponding to one role. releaseartists.main: list[Artist] # The Artist object has a `name` property with the artist name. releaseartists.guest: list[Artist] diff --git a/rose/audiotags.py b/rose/audiotags.py index 37c9c57..81e2002 100644 --- a/rose/audiotags.py +++ b/rose/audiotags.py @@ -87,6 +87,7 @@ class AudioTags: release_id: str | None title: str | None releaseyear: int | None + compositionyear: int | None tracknumber: str | None tracktotal: int | None discnumber: str | None @@ -94,6 +95,7 @@ class AudioTags: release: str | None genre: list[str] label: list[str] + catalognumber: str | None releasetype: str releaseartists: ArtistMapping @@ -140,10 +142,11 @@ def _get_paired_frame(x: str) -> str | None: return None return AudioTags( - id=_get_tag(m.tags, ["TXXX:ROSEID"]), - release_id=_get_tag(m.tags, ["TXXX:ROSERELEASEID"]), + id=_get_tag(m.tags, ["TXXX:ROSEID"], first=True), + release_id=_get_tag(m.tags, ["TXXX:ROSERELEASEID"], first=True), title=_get_tag(m.tags, ["TIT2"]), releaseyear=_parse_year(_get_tag(m.tags, ["TDRC", "TYER"])), + compositionyear=_parse_year(_get_tag(m.tags, ["TXXX:COMPOSITIONDATE"], first=True)), tracknumber=tracknumber, tracktotal=tracktotal, discnumber=discnumber, @@ -151,6 +154,7 @@ def _get_paired_frame(x: str) -> str | None: release=_get_tag(m.tags, ["TALB"]), genre=_split_tag(_get_tag(m.tags, ["TCON"], split=True)), label=_split_tag(_get_tag(m.tags, ["TPUB"], split=True)), + catalognumber=_get_tag(m.tags, ["TXXX:CATALOGNUMBER"], first=True), releasetype=_normalize_rtype(_get_tag(m.tags, ["TXXX:RELEASETYPE"], first=True)), releaseartists=parse_artist_string(main=_get_tag(m.tags, ["TPE2"], split=True)), trackartists=parse_artist_string( @@ -178,6 +182,9 @@ def _get_paired_frame(x: str) -> str | None: release_id=_get_tag(m.tags, ["----:net.sunsetglow.rose:RELEASEID"]), title=_get_tag(m.tags, ["\xa9nam"]), releaseyear=_parse_year(_get_tag(m.tags, ["\xa9day"])), + compositionyear=_parse_year( + _get_tag(m.tags, ["----:net.sunsetglow.rose:COMPOSITIONDATE"]) + ), tracknumber=str(tracknumber), tracktotal=tracktotal, discnumber=str(discnumber), @@ -185,6 +192,7 @@ def _get_paired_frame(x: str) -> str | None: release=_get_tag(m.tags, ["\xa9alb"]), genre=_split_tag(_get_tag(m.tags, ["\xa9gen"], split=True)), label=_split_tag(_get_tag(m.tags, ["----:com.apple.iTunes:LABEL"], split=True)), + catalognumber=_get_tag(m.tags, ["----:com.apple.iTunes:CATALOGNUMBER"]), releasetype=_normalize_rtype( _get_tag(m.tags, ["----:com.apple.iTunes:RELEASETYPE"], first=True) ), @@ -206,6 +214,7 @@ def _get_paired_frame(x: str) -> str | None: release_id=_get_tag(m.tags, ["rosereleaseid"]), title=_get_tag(m.tags, ["title"]), releaseyear=_parse_year(_get_tag(m.tags, ["date", "year"])), + compositionyear=_parse_year(_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), @@ -213,8 +222,9 @@ def _get_paired_frame(x: str) -> str | None: release=_get_tag(m.tags, ["album"]), genre=_split_tag(_get_tag(m.tags, ["genre"], split=True)), label=_split_tag( - _get_tag(m.tags, ["organization", "label", "recordlabel"], split=True) + _get_tag(m.tags, ["label", "organization", "recordlabel"], split=True) ), + catalognumber=_get_tag(m.tags, ["catalognumber"]), releasetype=_normalize_rtype(_get_tag(m.tags, ["releasetype"], first=True)), releaseartists=parse_artist_string( main=_get_tag(m.tags, ["albumartist"], split=True) @@ -263,7 +273,7 @@ def _write_tag_with_description(name: str, value: str | None) -> None: keep_fields = [f for f in m.tags.getall(key) if getattr(f, "desc", None) != desc] m.tags.delall(key) if value: - frame = getattr(mutagen.id3, key)(desc=desc, text=value) + frame = getattr(mutagen.id3, key)(desc=desc, text=[value]) m.tags.add(frame) for f in keep_fields: m.tags.add(f) @@ -272,11 +282,13 @@ def _write_tag_with_description(name: str, value: str | None) -> None: _write_tag_with_description("TXXX:ROSERELEASEID", self.release_id) _write_standard_tag("TIT2", self.title) _write_standard_tag("TDRC", str(self.releaseyear).zfill(4)) + _write_tag_with_description("TXXX:COMPOSITIONDATE", self.compositionyear) _write_standard_tag("TRCK", self.tracknumber) _write_standard_tag("TPOS", self.discnumber) _write_standard_tag("TALB", self.release) _write_standard_tag("TCON", ";".join(self.genre)) _write_standard_tag("TPUB", ";".join(self.label)) + _write_tag_with_description("TXXX:CATALOGNUMBER", self.catalognumber) _write_tag_with_description("TXXX:RELEASETYPE", self.releasetype) _write_standard_tag("TPE2", format_artist_string(self.releaseartists)) _write_standard_tag("TPE1", format_artist_string(self.trackartists)) @@ -297,9 +309,13 @@ def _write_tag_with_description(name: str, value: str | None) -> None: m.tags["----:net.sunsetglow.rose:RELEASEID"] = (self.release_id or "").encode() m.tags["\xa9nam"] = self.title or "" m.tags["\xa9day"] = str(self.releaseyear).zfill(4) + m.tags["----:net.sunsetglow.rose:COMPOSITIONDATE"] = ( + str(self.compositionyear).zfill(4).encode() + ) m.tags["\xa9alb"] = self.release or "" m.tags["\xa9gen"] = ";".join(self.genre) m.tags["----:com.apple.iTunes:LABEL"] = ";".join(self.label).encode() + m.tags["----:com.apple.iTunes:CATALOGNUMBER"] = (self.catalognumber or "").encode() m.tags["----:com.apple.iTunes:RELEASETYPE"] = self.releasetype.encode() m.tags["aART"] = format_artist_string(self.releaseartists) m.tags["\xa9ART"] = format_artist_string(self.trackartists) @@ -351,11 +367,13 @@ def _write_tag_with_description(name: str, value: str | None) -> None: m.tags["rosereleaseid"] = self.release_id or "" m.tags["title"] = self.title or "" m.tags["date"] = str(self.releaseyear).zfill(4) + m.tags["compositiondate"] = str(self.compositionyear).zfill(4) m.tags["tracknumber"] = self.tracknumber or "" m.tags["discnumber"] = self.discnumber or "" m.tags["album"] = self.release or "" m.tags["genre"] = ";".join(self.genre) - m.tags["organization"] = ";".join(self.label) + m.tags["label"] = ";".join(self.label) + m.tags["catalognumber"] = self.catalognumber or "" m.tags["releasetype"] = self.releasetype m.tags["albumartist"] = format_artist_string(self.releaseartists) m.tags["artist"] = format_artist_string(self.trackartists) @@ -399,8 +417,8 @@ def _get_tag(t: Any, keys: list[str], *, split: bool = False, first: bool = Fals f"Encountered a tag value of type {type(val)}" ) if first: - return values[0] if values else None - return r" \\ ".join(values) + return (values[0] or None) if values else None + return r" \\ ".join(values) or None except (KeyError, ValueError): pass return None diff --git a/rose/audiotags_test.py b/rose/audiotags_test.py index 77ea615..db077cd 100644 --- a/rose/audiotags_test.py +++ b/rose/audiotags_test.py @@ -26,20 +26,21 @@ ) def test_getters(filename: str, track_num: str, duration: int) -> None: af = AudioTags.from_file(TEST_TAGGER / filename) - assert af.title == f"Track {track_num}" - - assert af.tracknumber == track_num - assert af.tracktotal == 5 - assert af.discnumber == "1" - assert af.disctotal == 1 - assert af.release == "A Cool Album" assert af.releasetype == "album" assert af.releaseyear == 1990 + assert af.compositionyear == 1984 assert af.genre == ["Electronic", "House"] assert af.label == ["A Cool Label"] - + assert af.catalognumber == "DN-420" assert af.releaseartists.main == [Artist("Artist A"), Artist("Artist B")] + + assert af.tracknumber == track_num + assert af.tracktotal == 5 + assert af.discnumber == "1" + assert af.disctotal == 1 + + assert af.title == f"Track {track_num}" assert af.trackartists == ArtistMapping( main=[Artist("Artist A"), Artist("Artist B")], guest=[Artist("Artist C"), Artist("Artist D")], @@ -73,17 +74,19 @@ def test_flush(isolated_dir: Path, filename: str, track_num: str, duration: int) af.flush() af = AudioTags.from_file(fpath) - assert af.tracknumber == track_num - assert af.title == f"Track {track_num}" - assert af.release == "A Cool Album" assert af.releasetype == "album" assert af.releaseyear == 1990 - assert af.discnumber == "1" + assert af.compositionyear == 1984 assert af.genre == ["Electronic", "House"] assert af.label == ["A Cool Label"] - + assert af.catalognumber == "DN-420" assert af.releaseartists.main == [Artist("Artist A"), Artist("Artist B")] + + assert af.tracknumber == track_num + assert af.discnumber == "1" + + assert af.title == f"Track {track_num}" assert af.trackartists == ArtistMapping( main=[Artist("Artist A"), Artist("Artist B")], guest=[Artist("Artist C"), Artist("Artist D")], diff --git a/rose/cache.py b/rose/cache.py index 95212df..b48ae31 100644 --- a/rose/cache.py +++ b/rose/cache.py @@ -209,6 +209,8 @@ class CachedRelease: releasetitle: str releasetype: str releaseyear: int | None + compositionyear: int | None + catalognumber: str | None new: bool disctotal: int genres: list[str] @@ -227,6 +229,8 @@ def from_view(cls, c: Config, row: dict[str, Any], aliases: bool = True) -> Cach releasetitle=row["releasetitle"], releasetype=row["releasetype"], releaseyear=row["releaseyear"], + compositionyear=row["compositionyear"], + catalognumber=row["catalognumber"], disctotal=row["disctotal"], new=bool(row["new"]), genres=_split(row["genres"]) if row["genres"] else [], @@ -248,6 +252,8 @@ def dump(self) -> dict[str, Any]: "releasetitle": self.releasetitle, "releasetype": self.releasetype, "releaseyear": self.releaseyear, + "compositionyear": self.compositionyear, + "catalognumber": self.catalognumber, "new": self.new, "disctotal": self.disctotal, "genres": self.genres, @@ -318,6 +324,8 @@ def dump(self, with_release_info: bool = True) -> dict[str, Any]: "releasetype": self.release.releasetype, "disctotal": self.release.disctotal, "releaseyear": self.release.releaseyear, + "compositionyear": self.release.compositionyear, + "catalognumber": self.release.catalognumber, "new": self.release.new, "genres": self.release.genres, "labels": self.release.labels, @@ -613,6 +621,8 @@ def _update_cache_for_releases_executor( releasetitle="", releasetype="", releaseyear=None, + compositionyear=None, + catalognumber=None, new=True, disctotal=0, genres=[], @@ -782,6 +792,20 @@ def _update_cache_for_releases_executor( release.releaseyear = tags.releaseyear release_dirty = True + if tags.compositionyear != release.compositionyear: + logger.debug( + f"Release composition year change detected for {source_path}, updating" + ) + release.compositionyear = tags.compositionyear + release_dirty = True + + if tags.catalognumber != release.catalognumber: + logger.debug( + f"Release catalog number change detected for {source_path}, updating" + ) + release.catalognumber = tags.catalognumber + release_dirty = True + if set(tags.genre) != set(release.genres): logger.debug(f"Release genre change detected for {source_path}, updating") release.genres = uniq(tags.genre) @@ -950,6 +974,8 @@ def _update_cache_for_releases_executor( release.releasetitle, release.releasetype, release.releaseyear, + release.compositionyear, + release.catalognumber, release.disctotal, release.new, sha256_dataclass(release), @@ -1031,10 +1057,12 @@ def _update_cache_for_releases_executor( , title , releasetype , releaseyear + , compositionyear + , catalognumber , disctotal , new , metahash - ) VALUES {",".join(["(?,?,?,?,?,?,?,?,?,?,?)"] * len(upd_release_args))} + ) VALUES {",".join(["(?,?,?,?,?,?,?,?,?,?,?,?,?)"] * len(upd_release_args))} """, _flatten(upd_release_args), ) @@ -1149,6 +1177,8 @@ def _update_cache_for_releases_executor( , disctotal , releasetitle , releaseyear + , compositionyear + , catalognumber , releasetype , genre , label @@ -1164,6 +1194,8 @@ def _update_cache_for_releases_executor( , process_string_for_fts(r.disctotal) AS discnumber , process_string_for_fts(r.title) AS releasetitle , process_string_for_fts(r.releaseyear) AS releaseyear + , process_string_for_fts(r.compositionyear) AS compositionyear + , process_string_for_fts(r.catalognumber) AS catalognumber , process_string_for_fts(r.releasetype) AS releasetype , process_string_for_fts(COALESCE(GROUP_CONCAT(rg.genre, ' '), '')) AS genre , process_string_for_fts(COALESCE(GROUP_CONCAT(rl.label, ' '), '')) AS label diff --git a/rose/cache.sql b/rose/cache.sql index e628e4c..976767b 100644 --- a/rose/cache.sql +++ b/rose/cache.sql @@ -15,6 +15,8 @@ CREATE TABLE releases ( title TEXT NOT NULL, releasetype TEXT NOT NULL, releaseyear INTEGER, + compositionyear INTEGER, + catalognumber TEXT, disctotal INTEGER NOT NULL, -- A sha256() of the release object, which can be used as a performant cache -- key. @@ -156,8 +158,10 @@ CREATE VIRTUAL TABLE rules_engine_fts USING fts5 ( , discnumber , disctotal , releasetitle - , releaseyear , releasetype + , releaseyear + , compositionyear + , catalognumber , genre , label , releaseartist @@ -201,6 +205,8 @@ CREATE VIEW releases_view AS , r.title AS releasetitle , r.releasetype , r.releaseyear + , r.compositionyear + , r.catalognumber , r.disctotal , r.new , r.metahash diff --git a/rose/cache_test.py b/rose/cache_test.py index 384322b..6cd9b96 100644 --- a/rose/cache_test.py +++ b/rose/cache_test.py @@ -174,7 +174,7 @@ def test_update_cache_releases(config: Config) -> None: with connect(config) as conn: cursor = conn.execute( """ - SELECT id, source_path, title, releasetype, releaseyear, new + SELECT id, source_path, title, releasetype, releaseyear, compositionyear, catalognumber, new FROM releases WHERE id = ? """, (release_id,), @@ -184,6 +184,8 @@ def test_update_cache_releases(config: Config) -> None: assert row["title"] == "I Love Blackpink" assert row["releasetype"] == "album" assert row["releaseyear"] == 1990 + assert row["compositionyear"] is None + assert row["catalognumber"] is None assert row["new"] cursor = conn.execute( @@ -1042,6 +1044,8 @@ def test_list_releases(config: Config) -> None: added_at="0000-01-01T00:00:00+00:00", releasetitle="Release 1", releasetype="album", + compositionyear=None, + catalognumber=None, releaseyear=2023, disctotal=1, new=False, @@ -1059,6 +1063,8 @@ def test_list_releases(config: Config) -> None: releasetitle="Release 2", releasetype="album", releaseyear=2021, + compositionyear=None, + catalognumber="DG-001", disctotal=1, new=False, genres=["Classical"], @@ -1077,6 +1083,8 @@ def test_list_releases(config: Config) -> None: releasetitle="Release 3", releasetype="album", releaseyear=2021, + compositionyear=1780, + catalognumber="DG-002", disctotal=1, new=True, genres=[], @@ -1103,6 +1111,8 @@ def test_get_release_and_associated_tracks(config: Config) -> None: releasetitle="Release 1", releasetype="album", releaseyear=2023, + compositionyear=None, + catalognumber=None, disctotal=1, new=False, genres=["Techno", "Deep House"], @@ -1191,6 +1201,8 @@ def test_list_tracks(config: Config) -> None: releasetitle="Release 1", releasetype="album", releaseyear=2023, + compositionyear=None, + catalognumber=None, disctotal=1, new=False, genres=["Techno", "Deep House"], @@ -1219,6 +1231,8 @@ def test_list_tracks(config: Config) -> None: releasetitle="Release 1", releasetype="album", releaseyear=2023, + compositionyear=None, + catalognumber=None, disctotal=1, new=False, genres=["Techno", "Deep House"], @@ -1249,6 +1263,8 @@ def test_list_tracks(config: Config) -> None: releasetitle="Release 2", releasetype="album", releaseyear=2021, + compositionyear=None, + catalognumber="DG-001", new=False, disctotal=1, genres=["Classical"], @@ -1279,6 +1295,8 @@ def test_list_tracks(config: Config) -> None: releasetitle="Release 3", releasetype="album", releaseyear=2021, + compositionyear=1780, + catalognumber="DG-002", new=True, disctotal=1, genres=[], @@ -1315,6 +1333,8 @@ def test_get_track(config: Config) -> None: releasetitle="Release 1", releasetype="album", releaseyear=2023, + compositionyear=None, + catalognumber=None, disctotal=1, new=False, genres=["Techno", "Deep House"], @@ -1405,6 +1425,8 @@ def test_get_collage(config: Config) -> None: releasetitle="Release 1", releasetype="album", releaseyear=2023, + compositionyear=None, + catalognumber=None, new=False, disctotal=1, genres=["Techno", "Deep House"], @@ -1421,6 +1443,8 @@ def test_get_collage(config: Config) -> None: releasetitle="Release 2", releasetype="album", releaseyear=2021, + compositionyear=None, + catalognumber="DG-001", new=False, disctotal=1, genres=["Classical"], @@ -1487,6 +1511,8 @@ def test_get_playlist(config: Config) -> None: releasetitle="Release 1", releasetype="album", releaseyear=2023, + compositionyear=None, + catalognumber=None, disctotal=1, new=False, genres=["Techno", "Deep House"], @@ -1517,6 +1543,8 @@ def test_get_playlist(config: Config) -> None: releasetitle="Release 2", releasetype="album", releaseyear=2021, + compositionyear=None, + catalognumber="DG-001", new=False, disctotal=1, genres=["Classical"], diff --git a/rose/collages_test.py b/rose/collages_test.py index 674a45e..8d6f2de 100644 --- a/rose/collages_test.py +++ b/rose/collages_test.py @@ -133,6 +133,8 @@ def test_dump_collage(config: Config) -> None: "releasetitle": "Release 1", "releasetype": "album", "releaseyear": 2023, + "compositionyear": None, + "catalognumber": None, "new": False, "disctotal": 1, "genres": ["Techno", "Deep House"], @@ -159,6 +161,8 @@ def test_dump_collage(config: Config) -> None: "releasetitle": "Release 2", "releasetype": "album", "releaseyear": 2021, + "compositionyear": None, + "catalognumber": "DG-001", "new": False, "disctotal": 1, "genres": ["Classical"], @@ -193,6 +197,8 @@ def test_dump_collages(config: Config) -> None: "releasetitle": "Release 1", "releasetype": "album", "releaseyear": 2023, + "compositionyear": None, + "catalognumber": None, "new": False, "disctotal": 1, "genres": ["Techno", "Deep House"], @@ -219,6 +225,8 @@ def test_dump_collages(config: Config) -> None: "releasetitle": "Release 2", "releasetype": "album", "releaseyear": 2021, + "compositionyear": None, + "catalognumber": "DG-001", "new": False, "disctotal": 1, "genres": ["Classical"], diff --git a/rose/playlists_test.py b/rose/playlists_test.py index f8d2d80..9b82562 100644 --- a/rose/playlists_test.py +++ b/rose/playlists_test.py @@ -158,6 +158,8 @@ def test_dump_playlist(config: Config) -> None: "releasetitle": "Release 1", "releasetype": "album", "releaseyear": 2023, + "compositionyear": None, + "catalognumber": None, "new": False, "genres": ["Techno", "Deep House"], "labels": ["Silk Music"], @@ -198,6 +200,8 @@ def test_dump_playlist(config: Config) -> None: "releasetitle": "Release 2", "releasetype": "album", "releaseyear": 2021, + "compositionyear": None, + "catalognumber": "DG-001", "new": False, "genres": ["Classical"], "labels": ["Native State"], @@ -250,6 +254,8 @@ def test_dump_playlists(config: Config) -> None: "releasetitle": "Release 1", "releasetype": "album", "releaseyear": 2023, + "compositionyear": None, + "catalognumber": None, "new": False, "genres": ["Techno", "Deep House"], "labels": ["Silk Music"], @@ -290,6 +296,8 @@ def test_dump_playlists(config: Config) -> None: "releasetitle": "Release 2", "releasetype": "album", "releaseyear": 2021, + "compositionyear": None, + "catalognumber": "DG-001", "new": False, "genres": ["Classical"], "labels": ["Native State"], diff --git a/rose/releases.py b/rose/releases.py index 912e186..9410e5c 100644 --- a/rose/releases.py +++ b/rose/releases.py @@ -239,8 +239,10 @@ class MetadataRelease: new: bool releasetype: str releaseyear: int | None + compositionyear: int | None genres: list[str] labels: list[str] + catalognumber: str | None artists: list[MetadataArtist] tracks: dict[str, MetadataTrack] @@ -251,6 +253,8 @@ def from_cache(cls, release: CachedRelease, tracks: list[CachedTrack]) -> Metada new=release.new, releasetype=release.releasetype, releaseyear=release.releaseyear, + compositionyear=release.compositionyear, + catalognumber=release.catalognumber, genres=release.genres, labels=release.labels, artists=MetadataArtist.from_mapping(release.releaseartists), @@ -270,6 +274,8 @@ def serialize(self) -> str: # released in -9999, you should probably lay off the shrooms. data = asdict(self) data["releaseyear"] = self.releaseyear or -9999 + data["compositionyear"] = self.compositionyear or -9999 + data["catalognumber"] = self.releaseyear or "" return tomli_w.dumps(data) @classmethod @@ -280,8 +286,10 @@ def from_toml(cls, toml: str) -> MetadataRelease: new=d["new"], releasetype=d["releasetype"], releaseyear=d["releaseyear"] if d["releaseyear"] != -9999 else None, + compositionyear=d["compositionyear"] if d["compositionyear"] != -9999 else None, genres=d["genres"], labels=d["labels"], + catalognumber=d["catalognumber"] or None, artists=[MetadataArtist(name=a["name"], role=a["role"]) for a in d["artists"]], tracks={ tid: MetadataTrack( @@ -382,6 +390,14 @@ def edit_release( tags.releaseyear = release_meta.releaseyear dirty = True logger.debug(f"Modified tag detected for {t.source_path}: releaseyear") + if tags.compositionyear != release_meta.compositionyear: + tags.compositionyear = release_meta.compositionyear + dirty = True + logger.debug(f"Modified tag detected for {t.source_path}: compositionyear") + if tags.catalognumber != release_meta.catalognumber: + tags.catalognumber = release_meta.catalognumber + dirty = True + logger.debug(f"Modified tag detected for {t.source_path}: catalognumber") if tags.genre != release_meta.genres: tags.genre = release_meta.genres dirty = True diff --git a/rose/releases_test.py b/rose/releases_test.py index 099298d..f6450ff 100644 --- a/rose/releases_test.py +++ b/rose/releases_test.py @@ -137,6 +137,7 @@ def test_edit_release(monkeypatch: Any, config: Config, source_dir: Path) -> Non new = false releasetype = "single" releaseyear = 2222 + compositionyear = 1800 genres = [ "J-Pop", "Pop-Rap", @@ -144,6 +145,7 @@ def test_edit_release(monkeypatch: Any, config: Config, source_dir: Path) -> Non labels = [ "YG Entertainment", ] + catalognumber = "Lalala" artists = [ {{ name = "BLACKPINK", role = "main" }}, {{ name = "JISOO", role = "main" }}, @@ -179,6 +181,8 @@ def test_edit_release(monkeypatch: Any, config: Config, source_dir: Path) -> Non releasetitle="I Really Love Blackpink", releasetype="single", releaseyear=2222, + compositionyear=1800, + catalognumber="Lalala", new=False, disctotal=1, genres=["J-Pop", "Pop-Rap"], @@ -236,6 +240,7 @@ def test_edit_release_failure_and_resume( new = false releasetype = "bullshit" releaseyear = 2222 + compositionyear = -9999 genres = [ "J-Pop", "Pop-Rap", @@ -243,6 +248,7 @@ def test_edit_release_failure_and_resume( labels = [ "YG Entertainment", ] + catalognumber = "" artists = [ {{ name = "BLACKPINK", role = "main" }}, {{ name = "JISOO", role = "main" }}, @@ -278,6 +284,7 @@ def test_edit_release_failure_and_resume( new = false releasetype = "single" releaseyear = 2222 + compositionyear = -9999 genres = [ "J-Pop", "Pop-Rap", @@ -285,6 +292,7 @@ def test_edit_release_failure_and_resume( labels = [ "YG Entertainment", ] + catalognumber = "" artists = [ {{ name = "BLACKPINK", role = "main" }}, {{ name = "JISOO", role = "main" }}, @@ -328,6 +336,8 @@ def editfn(text: str, **_: Any) -> str: releasetitle="I Really Love Blackpink", releasetype="single", releaseyear=2222, + compositionyear=None, + catalognumber=None, new=False, disctotal=1, genres=["J-Pop", "Pop-Rap"], @@ -412,6 +422,8 @@ def test_dump_release(config: Config) -> None: "releasetitle": "Release 1", "releasetype": "album", "releaseyear": 2023, + "compositionyear": None, + "catalognumber": None, "new": False, "disctotal": 1, "genres": ["Techno", "Deep House"], @@ -486,6 +498,8 @@ def test_dump_releases(config: Config) -> None: "releasetitle": "Release 1", "releasetype": "album", "releaseyear": 2023, + "compositionyear": None, + "catalognumber": None, "new": False, "disctotal": 1, "genres": ["Techno", "Deep House"], @@ -555,6 +569,8 @@ def test_dump_releases(config: Config) -> None: "releasetitle": "Release 2", "releasetype": "album", "releaseyear": 2021, + "compositionyear": None, + "catalognumber": "DG-001", "new": False, "disctotal": 1, "genres": ["Classical"], @@ -597,6 +613,8 @@ def test_dump_releases(config: Config) -> None: "releasetitle": "Release 3", "releasetype": "album", "releaseyear": 2021, + "compositionyear": 1780, + "catalognumber": "DG-002", "new": True, "disctotal": 1, "genres": [], @@ -646,6 +664,8 @@ def test_dump_releases_matcher(config: Config) -> None: "releasetitle": "Release 2", "releasetype": "album", "releaseyear": 2021, + "compositionyear": None, + "catalognumber": "DG-001", "new": False, "disctotal": 1, "genres": ["Classical"], diff --git a/rose/rule_parser.py b/rose/rule_parser.py index 9964275..67ce340 100644 --- a/rose/rule_parser.py +++ b/rose/rule_parser.py @@ -66,6 +66,8 @@ def __str__(self) -> str: "releaseartist[djmixer]", "releasetype", "releaseyear", + "compositionyear", + "catalognumber", "genre", "label", ] @@ -113,6 +115,8 @@ def __str__(self) -> str: "releaseartist[djmixer]": ["releaseartist[djmixer]"], "releasetype": ["releasetype"], "releaseyear": ["releaseyear"], + "compositionyear": ["compositionyear"], + "catalognumber": ["catalognumber"], "genre": ["genre"], "label": ["label"], "artist": [ @@ -154,6 +158,8 @@ def __str__(self) -> str: "releaseartist[djmixer]", "releasetype", "releaseyear", + "compositionyear", + "catalognumber", "genre", "label", ] @@ -167,6 +173,8 @@ def __str__(self) -> str: "releasetitle", "releasetype", "releaseyear", + "compositionyear", + "catalognumber", ] RELEASE_TAGS: list[Tag] = [ @@ -181,6 +189,8 @@ def __str__(self) -> str: "releasetype", "releasetype", "releaseyear", + "compositionyear", + "catalognumber", "genre", "label", "disctotal", diff --git a/rose/rule_parser_test.py b/rose/rule_parser_test.py index 73ed63a..547cc22 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, 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, compositionyear, catalognumber, genre, 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, 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, compositionyear, catalognumber, genre, 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, 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, compositionyear, catalognumber, genre, label, artist}. The next character after a tag must be ':' or ','. """, ) diff --git a/rose/rules.py b/rose/rules.py index 3dfd015..384e0aa 100644 --- a/rose/rules.py +++ b/rose/rules.py @@ -228,6 +228,8 @@ def filter_track_false_positives_using_tags( # fmt: off match = match or (field == "tracktitle" and matches_pattern(matcher.pattern, tags.title)) match = match or (field == "releaseyear" and matches_pattern(matcher.pattern, tags.releaseyear)) + match = match or (field == "compositionyear" and matches_pattern(matcher.pattern, tags.compositionyear)) + 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)) match = match or (field == "discnumber" and matches_pattern(matcher.pattern, tags.discnumber)) @@ -260,6 +262,8 @@ def filter_track_false_positives_using_tags( # fmt: off skip = skip or (field == "tracktitle" and matches_pattern(i.pattern, tags.title)) skip = skip or (field == "releaseyear" and matches_pattern(i.pattern, tags.releaseyear)) + skip = skip or (field == "compositionyear" and matches_pattern(i.pattern, tags.compositionyear)) + 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)) skip = skip or (field == "discnumber" and matches_pattern(i.pattern, tags.discnumber)) @@ -344,6 +348,18 @@ 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 == "compositionyear": + v = execute_single_action(act, tags.compositionyear) + try: + tags.compositionyear = int(v) if v else None + except ValueError as e: + raise InvalidReplacementValueError( + 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 == "catalognumber": + tags.catalognumber = execute_single_action(act, tags.catalognumber) + potential_changes.append(("catalognumber", origtags.catalognumber, tags.catalognumber)) elif field == "tracknumber": tags.tracknumber = execute_single_action(act, tags.tracknumber) potential_changes.append(("tracknumber", origtags.tracknumber, tags.tracknumber)) @@ -643,6 +659,8 @@ 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 == "compositionyear" and matches_pattern(matcher.pattern, t.release.compositionyear)) + 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)) match = match or (field == "discnumber" and matches_pattern(matcher.pattern, t.discnumber)) @@ -687,6 +705,8 @@ 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 == "compositionyear" and matches_pattern(matcher.pattern, r.compositionyear)) + 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)) diff --git a/rose/rules_test.py b/rose/rules_test.py index 6311234..b9202b4 100644 --- a/rose/rules_test.py +++ b/rose/rules_test.py @@ -115,7 +115,7 @@ def test_rules_fields_match_tracktitle(config: Config, source_dir: Path) -> None assert af.title == "8" -def test_rules_fields_match_year(config: Config, source_dir: Path) -> None: +def test_rules_fields_match_releaseyear(config: Config, source_dir: Path) -> None: rule = MetadataRule.parse("releaseyear:1990", ["replace:8"]) execute_metadata_rule(config, rule, confirm_yes=False) af = AudioTags.from_file(source_dir / "Test Release 1" / "01.m4a") diff --git a/rose/templates.py b/rose/templates.py index a082fbd..3acf25d 100644 --- a/rose/templates.py +++ b/rose/templates.py @@ -255,6 +255,8 @@ def _calc_release_variables(release: CachedRelease, position: str | None) -> dic "releasetitle": release.releasetitle, "releasetype": release.releasetype, "releaseyear": release.releaseyear, + "compositionyear": release.compositionyear, + "catalognumber": release.catalognumber, "new": release.new, "disctotal": release.disctotal, "genres": release.genres, @@ -277,6 +279,8 @@ def _calc_track_variables(track: CachedTrack, position: str | None) -> dict[str, "releasetitle": track.release.releasetitle, "releasetype": track.release.releasetype, "releaseyear": track.release.releaseyear, + "compositionyear": track.release.compositionyear, + "catalognumber": track.release.catalognumber, "new": track.release.new, "genres": track.release.genres, "labels": track.release.labels, @@ -323,7 +327,7 @@ def preview_path_templates(c: Config) -> None: # fmt: on -def _get_preview_releases(c: Config) -> tuple[CachedRelease, CachedRelease]: +def _get_preview_releases(c: Config) -> tuple[CachedRelease, CachedRelease, CachedRelease]: from rose.cache import CachedRelease kimlip = CachedRelease( @@ -335,6 +339,8 @@ def _get_preview_releases(c: Config) -> tuple[CachedRelease, CachedRelease]: releasetitle="Kim Lip", releasetype="single", releaseyear=2017, + compositionyear=None, + catalognumber="CMCC11088", new=True, disctotal=1, genres=["K-Pop", "Dance-Pop", "Contemporary R&B"], @@ -352,6 +358,8 @@ def _get_preview_releases(c: Config) -> tuple[CachedRelease, CachedRelease]: releasetitle="Young Forever (花樣年華)", releasetype="album", releaseyear=2016, + compositionyear=None, + catalognumber="L200001238", new=False, disctotal=2, genres=["K-Pop"], @@ -360,24 +368,50 @@ def _get_preview_releases(c: Config) -> tuple[CachedRelease, CachedRelease]: metahash="0", ) - return kimlip, youngforever + debussy = CachedRelease( + id="018b268e-de0c-7cb2-8ffa-bcc2083c94e6", + source_path=c.music_source_dir + / "Debussy - 1907. Images performed by Cleveland Orchestra under Pierre Boulez (1992)", + cover_image_path=None, + added_at="2023-09-06:23:45Z", + datafile_mtime="999", + releasetitle="Images", + releasetype="album", + releaseyear=1992, + compositionyear=1907, + catalognumber="435-766 2", + new=False, + disctotal=2, + genres=["Impressionism, Orchestral"], + labels=["Deustche Grammophon"], + releaseartists=ArtistMapping( + main=[Artist("Cleveland Orchestra")], + composer=[Artist("Claude Debussy")], + conductor=[Artist("Pierre Boulez")], + ), + metahash="0", + ) + + return kimlip, youngforever, debussy def _preview_release_template(c: Config, label: str, template: PathTemplate) -> None: # Import cycle trick :) - kimlip, youngforever = _get_preview_releases(c) + kimlip, youngforever, debussy = _get_preview_releases(c) click.secho(f"{label}:", dim=True, underline=True) click.secho(" Sample 1: ", dim=True, nl=False) click.secho(eval_release_template(template, kimlip, "1")) click.secho(" Sample 2: ", dim=True, nl=False) click.secho(eval_release_template(template, youngforever, "2")) + click.secho(" Sample 3: ", dim=True, nl=False) + click.secho(eval_release_template(template, debussy, "3")) def _preview_track_template(c: Config, label: str, template: PathTemplate) -> None: # Import cycle trick :) from rose.cache import CachedTrack - kimlip, youngforever = _get_preview_releases(c) + kimlip, youngforever, debussy = _get_preview_releases(c) click.secho(f"{label}:", dim=True, underline=True) @@ -402,7 +436,7 @@ def _preview_track_template(c: Config, label: str, template: PathTemplate) -> No id="018b6021-f1e5-7d4b-b796-440fbbea3b15", source_path=c.music_source_dir / "BTS - 2016. Young Forever (花樣年華)" - / "House of Cards.opus", + / "02-05. House of Cards.opus", source_mtime="999", tracktitle="House of Cards", tracknumber="5", @@ -414,3 +448,25 @@ def _preview_track_template(c: Config, label: str, template: PathTemplate) -> No release=youngforever, ) click.secho(eval_track_template(template, track, "2")) + + click.secho(" Sample 3: ", dim=True, nl=False) + track = CachedTrack( + id="018b6514-6e65-78cc-94a5-fdb17418f090", + source_path=c.music_source_dir + / "Debussy - 1907. Images performed by Cleveland Orchestra under Pierre Boulez (1992)" + / "01. Gigues: Modéré.opus", + source_mtime="999", + tracktitle="Gigues: Modéré.opus", + tracknumber="1", + tracktotal=6, + discnumber="1", + duration_seconds=444, + trackartists=ArtistMapping( + main=[Artist("Cleveland Orchestra")], + composer=[Artist("Claude Debussy")], + conductor=[Artist("Pierre Boulez")], + ), + metahash="0", + release=debussy, + ) + click.secho(eval_track_template(template, track, "3")) diff --git a/rose/templates_test.py b/rose/templates_test.py index 6a16f1e..c782421 100644 --- a/rose/templates_test.py +++ b/rose/templates_test.py @@ -23,6 +23,8 @@ releasetitle="", releasetype="unknown", releaseyear=None, + compositionyear=None, + catalognumber=None, new=False, disctotal=1, genres=[], @@ -113,61 +115,78 @@ def test_preview_templates(config: Config) -> None: Source Directory - Release: Sample 1: Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: BTS - 2016. Young Forever (花樣年華) + Sample 3: Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images Source Directory - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus + Sample 3: 01-01. Gigues: Modéré.opus.opus 1. All Releases - Release: Sample 1: Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: BTS - 2016. Young Forever (花樣年華) + Sample 3: Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images 1. All Releases - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus + Sample 3: 01-01. Gigues: Modéré.opus.opus 2. New Releases - Release: Sample 1: Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: BTS - 2016. Young Forever (花樣年華) + Sample 3: Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images 2. New Releases - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus + Sample 3: 01-01. Gigues: Modéré.opus.opus 3. Recently Added Releases - Release: Sample 1: [2023-04-20] Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: [2023-06-09] BTS - 2016. Young Forever (花樣年華) + Sample 3: [2023-09-06] Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images 3. Recently Added Releases - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus + Sample 3: 01-01. Gigues: Modéré.opus.opus 4. Artists - Release: Sample 1: Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: BTS - 2016. Young Forever (花樣年華) + Sample 3: Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images 4. Artists - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus + Sample 3: 01-01. Gigues: Modéré.opus.opus 5. Genres - Release: Sample 1: Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: BTS - 2016. Young Forever (花樣年華) + Sample 3: Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images 5. Genres - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus + Sample 3: 01-01. Gigues: Modéré.opus.opus 6. Labels - Release: Sample 1: Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: BTS - 2016. Young Forever (花樣年華) + Sample 3: Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images 6. Labels - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus + Sample 3: 01-01. Gigues: Modéré.opus.opus 7. Collages - Release: Sample 1: 1. Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: 2. BTS - 2016. Young Forever (花樣年華) + Sample 3: 3. Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images 7. Collages - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus + Sample 3: 01-01. Gigues: Modéré.opus.opus 8. Playlists - Track: Sample 1: 1. Kim Lip - Eclipse.opus Sample 2: 2. BTS - House of Cards.opus + Sample 3: 3. Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - Gigues: Modéré.opus.opus """ ) diff --git a/rose/tracks_test.py b/rose/tracks_test.py index d6468e8..157da8e 100644 --- a/rose/tracks_test.py +++ b/rose/tracks_test.py @@ -47,6 +47,8 @@ def test_dump_tracks(config: Config) -> None: "releasetitle": "Release 1", "releasetype": "album", "releaseyear": 2023, + "compositionyear": None, + "catalognumber": None, "new": False, "genres": ["Techno", "Deep House"], "labels": ["Silk Music"], @@ -89,6 +91,8 @@ def test_dump_tracks(config: Config) -> None: "releasetitle": "Release 1", "releasetype": "album", "releaseyear": 2023, + "compositionyear": None, + "catalognumber": None, "new": False, "genres": ["Techno", "Deep House"], "labels": ["Silk Music"], @@ -128,6 +132,8 @@ def test_dump_tracks(config: Config) -> None: "releasetitle": "Release 2", "releasetype": "album", "releaseyear": 2021, + "compositionyear": None, + "catalognumber": "DG-001", "new": False, "genres": ["Classical"], "labels": ["Native State"], @@ -164,6 +170,8 @@ def test_dump_tracks(config: Config) -> None: "releasetitle": "Release 3", "releasetype": "album", "releaseyear": 2021, + "compositionyear": 1780, + "catalognumber": "DG-002", "new": True, "genres": [], "labels": [], @@ -210,6 +218,8 @@ def test_dump_tracks_with_matcher(config: Config) -> None: "releasetitle": "Release 1", "releasetype": "album", "releaseyear": 2023, + "compositionyear": None, + "catalognumber": None, "new": False, "genres": ["Techno", "Deep House"], "labels": ["Silk Music"], @@ -252,6 +262,8 @@ def test_dump_tracks_with_matcher(config: Config) -> None: "releasetitle": "Release 1", "releasetype": "album", "releaseyear": 2023, + "compositionyear": None, + "catalognumber": None, "new": False, "genres": ["Techno", "Deep House"], "labels": ["Silk Music"], @@ -299,6 +311,8 @@ def test_dump_track(config: Config) -> None: "releasetitle": "Release 1", "releasetype": "album", "releaseyear": 2023, + "compositionyear": None, + "catalognumber": None, "new": False, "genres": ["Techno", "Deep House"], "labels": ["Silk Music"], diff --git a/testdata/Tagger/track1.flac b/testdata/Tagger/track1.flac index 0054e72..dca4ba7 100644 Binary files a/testdata/Tagger/track1.flac and b/testdata/Tagger/track1.flac differ diff --git a/testdata/Tagger/track2.m4a b/testdata/Tagger/track2.m4a index 7e675ef..96af674 100644 Binary files a/testdata/Tagger/track2.m4a and b/testdata/Tagger/track2.m4a differ diff --git a/testdata/Tagger/track3.mp3 b/testdata/Tagger/track3.mp3 index ae9cac0..a53c096 100644 Binary files a/testdata/Tagger/track3.mp3 and b/testdata/Tagger/track3.mp3 differ diff --git a/testdata/Tagger/track4.vorbis.ogg b/testdata/Tagger/track4.vorbis.ogg index 83bea21..3a769d2 100644 Binary files a/testdata/Tagger/track4.vorbis.ogg and b/testdata/Tagger/track4.vorbis.ogg differ diff --git a/testdata/Tagger/track5.opus.ogg b/testdata/Tagger/track5.opus.ogg index d390bde..42653dd 100644 Binary files a/testdata/Tagger/track5.opus.ogg and b/testdata/Tagger/track5.opus.ogg differ diff --git a/testdata/Tagger/update.py b/testdata/Tagger/update.py new file mode 100755 index 0000000..b921b87 --- /dev/null +++ b/testdata/Tagger/update.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +from typing import Any + +import mutagen +import mutagen.id3 + + +def write_tag_with_description(f: Any, name: str, value: str | None) -> None: + key, desc = name.split(":", 1) + # Since the ID3 tags work with the shared prefix key before `:`, manually preserve + # the other tags with the shared prefix key. + keep_fields = [f for f in f.tags.getall(key) if getattr(f, "desc", None) != desc] + f.tags.delall(key) + if value: + frame = getattr(mutagen.id3, key)(desc=desc, text=[value]) + f.tags.add(frame) + for x in keep_fields: + f.tags.add(x) + + +f = mutagen.File("track1.flac") # type: ignore +f.tags["compositiondate"] = "1984" +f.tags["catalognumber"] = "DN-420" +f.save() + +f = mutagen.File("track2.m4a") # type: ignore +f.tags["----:net.sunsetglow.rose:COMPOSITIONDATE"] = b"1984" +f.tags["----:com.apple.iTunes:CATALOGNUMBER"] = b"DN-420" +f.save() + +f = mutagen.File("track3.mp3") # type: ignore +write_tag_with_description(f, "TXXX:COMPOSITIONDATE", "1984") +write_tag_with_description(f, "TXXX:CATALOGNUMBER", "DN-420") +f.save() + +f = mutagen.File("track4.vorbis.ogg") # type: ignore +f.tags["compositiondate"] = "1984" +f.tags["catalognumber"] = "DN-420" +f.save() + +f = mutagen.File("track5.opus.ogg") # type: ignore +f.tags["compositiondate"] = "1984" +f.tags["catalognumber"] = "DN-420" +f.save()