Skip to content

Commit

Permalink
support dates (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
azuline authored Apr 26, 2024
1 parent 519a647 commit fb2225e
Show file tree
Hide file tree
Showing 21 changed files with 385 additions and 358 deletions.
20 changes: 10 additions & 10 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,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, originalyear, compositionyear, catalognumber, edition , disctotal, new , metahash)
VALUES ('r1', '{dirpaths[0]}', null , '0000-01-01T00:00:00+00:00', '999' , 'Release 1', 'album' , 2023 , null , null , null , null , 1 , false, '1')
, ('r2', '{dirpaths[1]}', '{imagepaths[0]}', '0000-01-01T00:00:00+00:00', '999' , 'Release 2', 'album' , 2021 , 2019 , null , 'DG-001' , 'Deluxe', 1 , false, '2')
, ('r3', '{dirpaths[2]}', null , '0000-01-01T00:00:00+00:00', '999' , 'Release 3', 'album' , 2021 , null , 1780 , 'DG-002' , null , 1 , true , '3');
(id , source_path , cover_image_path , added_at , datafile_mtime, title , releasetype, releasedate , originaldate, compositiondate, catalognumber, edition , disctotal, new , metahash)
VALUES ('r1', '{dirpaths[0]}', null , '0000-01-01T00:00:00+00:00', '999' , 'Release 1', 'album' , '2023' , null , null , null , null , 1 , false, '1')
, ('r2', '{dirpaths[1]}', '{imagepaths[0]}', '0000-01-01T00:00:00+00:00', '999' , 'Release 2', 'album' , '2021' , '2019' , null , 'DG-001' , 'Deluxe', 1 , false, '2')
, ('r3', '{dirpaths[2]}', null , '0000-01-01T00:00:00+00:00', '999' , 'Release 3', 'album' , '2021-04-20', null , '1780' , 'DG-002' , null , 1 , true , '3');
INSERT INTO releases_genres
(release_id, genre , position)
Expand Down Expand Up @@ -196,9 +196,9 @@ def seeded_cache(config: Config) -> None:
, tracknumber
, discnumber
, releasetitle
, releaseyear
, originalyear
, compositionyear
, releasedate
, originaldate
, compositiondate
, catalognumber
, edition
, releasetype
Expand All @@ -215,9 +215,9 @@ def seeded_cache(config: Config) -> None:
, process_string_for_fts(t.tracknumber) AS tracknumber
, 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.originalyear) AS originalyear
, process_string_for_fts(r.compositionyear) AS compositionyear
, process_string_for_fts(r.releasedate) AS releasedate
, process_string_for_fts(r.originaldate) AS originaldate
, process_string_for_fts(r.compositiondate) AS compositiondate
, process_string_for_fts(r.catalognumber) AS catalognumber
, process_string_for_fts(r.edition) AS edition
, process_string_for_fts(r.releasetype) AS releasetype
Expand Down
2 changes: 1 addition & 1 deletion docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ max_proc = 4
[path_templates]
default.release = """
{{ artists | artistsfmt }} -
{% if releaseyear %}{{ releaseyear }}.{% endif %}
{% if releasedate %}{{ releasedate }}.{% endif %}
{{ title }}
{% if releasetype == "single" %}- {{ releasetype | releasetypefmt }}{% endif %}
"""
Expand Down
12 changes: 6 additions & 6 deletions docs/METADATA_TOOLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ An example of the release's TOML representation:
title = "Mix & Match"
new = false
releasetype = "ep"
releaseyear = 2017
originalyear = 2017
compositionyear = -9999
releasedate = 2017
originaldate = 2017
compositiondate = -9999
genres = [
"K-Pop",
"Dance-Pop",
Expand Down Expand Up @@ -316,9 +316,9 @@ The rules engine supports matching and acting on the following tags:
- `releaseartist[conductor]`
- `releaseartist[djmixer]`
- `releasetype`
- `releaseyear`
- `originalyear`
- `compositionyear`
- `releasedate`
- `originaldate`
- `compositiondate`
- `genre`
- `parentgenre`
- `secondarygenre`
Expand Down
16 changes: 8 additions & 8 deletions docs/TEMPLATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ If set, the `default.xxx` templates are used as the default values for all other
{# "Default Default" Release Template #}
{{ releaseartists | artistsfmt }} -
{% if releaseyear %}{{ releaseyear }}.{% endif %}
{% if releasedate %}{{ releasedate }}.{% endif %}
{{ releasetitle }}
{% if releasetype == "single" %}- {{ releasetype | releasetypefmt }}{% endif %}
{% if new %}[NEW]{% endif %}
Expand All @@ -66,7 +66,7 @@ comments when defining your templates, like so:
[path_templates]
default.release = """
{{ releaseartists | artistsfmt }} -
{% if releaseyear %}{{ releaseyear }}.{% endif %} {# Hi! This is a comment! #}
{% if releasedate %}{{ releasedate }}.{% endif %} {# Hi! This is a comment! #}
{{ releasetitle }}
{% if new %}[NEW]{% endif %}
"""
Expand All @@ -78,9 +78,9 @@ Rosé provides the following template variables for releases:
added_at: str # ISO8601 timestamp of when the release was added to the library.
releasetitle: str
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 # The year of this edition of the release.
originalyear: int | None # The year of the first edition of the release.
compositionyear: int | None # The year that the release was composed. Mainly of interest in classical music.
releasedate: int | None # The year of this edition of the release.
originaldate: int | None # The year of the first edition of the release.
compositiondate: 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]
Expand Down Expand Up @@ -126,9 +126,9 @@ trackartists.conductor: list[Artist]
trackartists.djmixer: list[Artist]
releasetitle: str
releasetype: str # The type of the track's release (e.g. single, ep, etc).
releaseyear: int | None
originalyear: int | None # The year of the first edition of the release.
compositionyear: int | None # The year that the release was composed. Mainly of interest in classical music.
releasedate: int | None
originaldate: int | None # The year of the first edition of the release.
compositiondate: 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]
parent_genres: list[str] # The parent genres of `genres`, excluding `genres`.
Expand Down
84 changes: 47 additions & 37 deletions rose/audiotags.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

TAG_SPLITTER_REGEX = re.compile(r" \\\\ | / |; ?| vs\. ")
YEAR_REGEX = re.compile(r"\d{4}$")
DATE_REGEX = re.compile(r"(\d{4})-\d{2}-\d{2}")
DATE_REGEX = re.compile(r"(\d{4})-(\d{2})-(\d{2})")

SUPPORTED_AUDIO_EXTENSIONS = [
".mp3",
Expand Down Expand Up @@ -81,6 +81,29 @@ class UnsupportedTagValueTypeError(RoseExpectedError):
pass


@dataclass(frozen=True)
class RoseDate:
year: int
month: int | None = None
day: int | None = None

@classmethod
def parse(cls, value: str | None) -> RoseDate | None:
if not value:
return None
with contextlib.suppress(ValueError):
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}"
return f"{self.year:04}-{self.month or 1:02}-{self.day or 1:02}"


@dataclass
class AudioTags:
id: str | None
Expand All @@ -95,9 +118,9 @@ class AudioTags:

releasetitle: str | None
releasetype: str
releaseyear: int | None
originalyear: int | None
compositionyear: int | None
releasedate: RoseDate | None
originaldate: RoseDate | None
compositiondate: RoseDate | None
genre: list[str]
secondarygenre: list[str]
descriptor: list[str]
Expand Down Expand Up @@ -149,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"]),
releaseyear=_parse_year(_get_tag(m.tags, ["TDRC", "TYER", "TDAT"])),
originalyear=_parse_year(_get_tag(m.tags, ["TDOR", "TORY"])),
compositionyear=_parse_year(_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,
Expand Down Expand Up @@ -193,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"]),
releaseyear=_parse_year(_get_tag(m.tags, ["\xa9day"])),
originalyear=_parse_year(
releasedate=RoseDate.parse(_get_tag(m.tags, ["\xa9day"])),
originaldate=RoseDate.parse(
_get_tag(
m.tags,
[
Expand All @@ -204,7 +229,7 @@ def _get_paired_frame(x: str) -> str | None:
],
)
),
compositionyear=_parse_year(
compositiondate=RoseDate.parse(
_get_tag(m.tags, ["----:net.sunsetglow.rose:COMPOSITIONDATE"])
),
tracknumber=str(tracknumber),
Expand Down Expand Up @@ -249,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"]),
releaseyear=_parse_year(_get_tag(m.tags, ["date", "year"])),
originalyear=_parse_year(_get_tag(m.tags, ["originaldate", "originalyear"])),
compositionyear=_parse_year(_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),
Expand Down Expand Up @@ -321,9 +346,9 @@ def _write_tag_with_description(name: str, value: str | None) -> None:
_write_tag_with_description("TXXX:ROSEID", self.id)
_write_tag_with_description("TXXX:ROSERELEASEID", self.release_id)
_write_standard_tag("TIT2", self.tracktitle)
_write_standard_tag("TDRC", str(self.releaseyear).zfill(4))
_write_standard_tag("TDOR", str(self.originalyear).zfill(4))
_write_tag_with_description("TXXX:COMPOSITIONDATE", self.compositionyear)
_write_standard_tag("TDRC", str(self.releasedate))
_write_standard_tag("TDOR", str(self.originaldate))
_write_tag_with_description("TXXX:COMPOSITIONDATE", str(self.compositiondate))
_write_standard_tag("TRCK", self.tracknumber)
_write_standard_tag("TPOS", self.discnumber)
_write_standard_tag("TALB", self.releasetitle)
Expand Down Expand Up @@ -352,13 +377,9 @@ def _write_tag_with_description(name: str, value: str | None) -> None:
m.tags["----:net.sunsetglow.rose:ID"] = (self.id or "").encode()
m.tags["----:net.sunsetglow.rose:RELEASEID"] = (self.release_id or "").encode()
m.tags["\xa9nam"] = self.tracktitle or ""
m.tags["\xa9day"] = str(self.releaseyear).zfill(4)
m.tags["----:net.sunsetglow.rose:ORIGINALDATE"] = (
str(self.originalyear).zfill(4).encode()
)
m.tags["----:net.sunsetglow.rose:COMPOSITIONDATE"] = (
str(self.compositionyear).zfill(4).encode()
)
m.tags["\xa9day"] = str(self.releasedate)
m.tags["----:net.sunsetglow.rose:ORIGINALDATE"] = str(self.originaldate).encode()
m.tags["----:net.sunsetglow.rose:COMPOSITIONDATE"] = str(self.compositiondate).encode()
m.tags["\xa9alb"] = self.releasetitle or ""
m.tags["\xa9gen"] = ";".join(self.genre)
m.tags["----:net.sunsetglow.rose:SECONDARYGENRE"] = ";".join(
Expand Down Expand Up @@ -419,9 +440,9 @@ def _write_tag_with_description(name: str, value: str | None) -> None:
m.tags["roseid"] = self.id or ""
m.tags["rosereleaseid"] = self.release_id or ""
m.tags["title"] = self.tracktitle or ""
m.tags["date"] = str(self.releaseyear).zfill(4)
m.tags["originaldate"] = str(self.originalyear).zfill(4)
m.tags["compositiondate"] = str(self.compositionyear).zfill(4)
m.tags["date"] = str(self.releasedate)
m.tags["originaldate"] = str(self.originaldate)
m.tags["compositiondate"] = str(self.compositiondate)
m.tags["tracknumber"] = self.tracknumber or ""
m.tags["discnumber"] = self.discnumber or ""
m.tags["album"] = self.releasetitle or ""
Expand Down Expand Up @@ -508,17 +529,6 @@ def _parse_int(x: str | None) -> int | None:
return None


def _parse_year(value: str | None) -> int | None:
if not value:
return None
if YEAR_REGEX.match(value):
return int(value)
# There may be a time value after the date... allow that and other crap.
if m := DATE_REGEX.match(value):
return int(m[1])
return None


TAG_SPLITTER_REGEX = re.compile(r" \\\\ | / |; ?| vs\. ")


Expand Down
15 changes: 9 additions & 6 deletions rose/audiotags_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from conftest import TEST_TAGGER
from rose.audiotags import (
AudioTags,
RoseDate,
UnsupportedTagValueTypeError,
_split_tag,
format_artist_string,
Expand All @@ -28,9 +29,9 @@ def test_getters(filename: str, track_num: str, duration: int) -> None:
af = AudioTags.from_file(TEST_TAGGER / filename)
assert af.releasetitle == "A Cool Album"
assert af.releasetype == "album"
assert af.releaseyear == 1990
assert af.originalyear == 1990
assert af.compositionyear == 1984
assert af.releasedate == RoseDate(1990, 2, 5)
assert af.originaldate == RoseDate(1990)
assert af.compositiondate == RoseDate(1984)
assert af.genre == ["Electronic", "House"]
assert af.secondarygenre == ["Minimal", "Ambient"]
assert af.descriptor == ["Lush", "Warm"]
Expand Down Expand Up @@ -75,14 +76,16 @@ def test_flush(isolated_dir: Path, filename: str, track_num: str, duration: int)
# Inject one special case into here: modify the djmixer artist. This checks that we also clear
# the original djmixer tag, so that the next read does not contain Artist EF and Artist FG.
af.trackartists.djmixer = [Artist("New")]
# Also test date writing.
af.originaldate = RoseDate(1990, 4, 20)
af.flush()
af = AudioTags.from_file(fpath)

assert af.releasetitle == "A Cool Album"
assert af.releasetype == "album"
assert af.releaseyear == 1990
assert af.originalyear == 1990
assert af.compositionyear == 1984
assert af.releasedate == RoseDate(1990, 2, 5)
assert af.originaldate == RoseDate(1990, 4, 20)
assert af.compositiondate == RoseDate(1984)
assert af.genre == ["Electronic", "House"]
assert af.secondarygenre == ["Minimal", "Ambient"]
assert af.descriptor == ["Lush", "Warm"]
Expand Down
Loading

0 comments on commit fb2225e

Please sign in to comment.