Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support dates #112

Merged
merged 4 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading