Skip to content

Commit

Permalink
feat: new hook to allow plugins to read/set custom track tags
Browse files Browse the repository at this point in the history
  • Loading branch information
jtpavlock committed Sep 19, 2022
1 parent e536d31 commit b5069ba
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 21 deletions.
3 changes: 2 additions & 1 deletion moe/library/album.py
Expand Up @@ -94,7 +94,8 @@ class Album(LibItem, SABase):
path (Path): Filesystem path of the album directory.
title (str)
tracks (list[Track]): Album's corresponding tracks.
year (int): Album release year.
year (int): Album release year. Note, this field is read-only. Set ``date``
instead.
"""

__tablename__ = "album"
Expand Down
85 changes: 71 additions & 14 deletions moe/library/track.py
Expand Up @@ -60,6 +60,40 @@ def is_unique_track(track: "Track", other: "Track") -> bool: # type: ignore
track's :meth:`is_unique` method.
"""

@staticmethod
@moe.hookspec
def read_custom_tags(
track_path: Path, album_fields: dict[str, Any], track_fields: dict[str, Any]
) -> None:
"""Read and set any fields from a track_path.
How you read the file and assign tags is up to each individual plugin.
Internally, Moe uses the `mediafile <https://github.com/beetbox/mediafile>`_
library to read tags.
Args:
track_path: Path of the track file to read.
album_fields: Dictionary of album fields to read from the given track file's
tags. The dictionary may contain existing fields and values, and you
can choose to either override the existing fields, or to provide new
fields.
track_fields: Dictionary of track fields to read from the given track file's
tags. The dictionary may contain existing fields and values, and you
can choose to either override the existing fields, or to provide new
fields.
Example:
.. code:: python
audio_file = mediafile.MediaFile(track_path)
album_fields["title"] = audio_file.album
track_fields["title"] = audio_file.title
See Also:
* :ref:`Album and track fields <fields:Fields>`
* `Mediafile docs <https://mediafile.readthedocs.io/en/latest/>`_
"""


@moe.hookimpl
def add_hooks(pm: pluggy.manager.PluginManager):
Expand All @@ -69,6 +103,27 @@ def add_hooks(pm: pluggy.manager.PluginManager):
pm.add_hookspecs(Hooks)


@moe.hookimpl(tryfirst=True)
def read_custom_tags(
track_path: Path, album_fields: dict[str, Any], track_fields: dict[str, Any]
) -> None:
"""Read and set internally tracked fields."""
audio_file = mediafile.MediaFile(track_path)

album_fields["path"] = track_path.parent
album_fields["artist"] = audio_file.albumartist or audio_file.artist
album_fields["title"] = audio_file.album
album_fields["date"] = audio_file.date
album_fields["disc_total"] = audio_file.disctotal

track_fields["path"] = track_path
track_fields["title"] = audio_file.title
track_fields["track_num"] = audio_file.track
track_fields["artist"] = audio_file.artist
track_fields["disc"] = audio_file.disc
track_fields["genres"] = set(audio_file.genres)


class TrackError(LibraryError):
"""Error performing some operation on a Track."""

Expand Down Expand Up @@ -248,32 +303,34 @@ def from_file(cls: type[T], track_path: Path, album: Optional[Album] = None) ->
log.debug(f"Creating track from path. [path={track_path}, {album=}]")

try:
audio_file = mediafile.MediaFile(track_path)
mediafile.MediaFile(track_path)
except mediafile.UnreadableFileError as err:
raise TrackError(
"Unable to create track; given path is not a track file. "
f"[path={track_path}]"
) from err

album_fields: dict[str, Any] = {}
track_fields: dict[str, Any] = {}
config.CONFIG.pm.hook.read_custom_tags(
track_path=track_path, album_fields=album_fields, track_fields=track_fields
)
if not album:
albumartist = audio_file.albumartist or audio_file.artist

album = Album(
artist=albumartist,
title=audio_file.album,
date=audio_file.date,
disc_total=audio_file.disctotal,
path=track_path.parent,
path=album_fields.pop("path"),
artist=album_fields.pop("artist"),
title=album_fields.pop("title"),
date=album_fields.pop("date"),
disc_total=album_fields.pop("disc_total"),
**album_fields,
)

return cls(
album=album,
path=track_path,
track_num=audio_file.track,
artist=audio_file.artist,
disc=audio_file.disc,
genres=audio_file.genres,
title=audio_file.title,
path=track_fields.pop("path"),
title=track_fields.pop("title"),
track_num=track_fields.pop("track_num"),
**track_fields,
)

@property
Expand Down
9 changes: 6 additions & 3 deletions tests/library/test_album.py
Expand Up @@ -52,13 +52,15 @@ def test_is_unique_album(self, tmp_config):
class TestFromDir:
"""Test a creating an album from a directory."""

def test_dir_album(self):
def test_dir_album(self, tmp_config):
"""If a directory given, add to library as an album."""
tmp_config()
album = album_factory(exists=True)
assert Album.from_dir(album.path) == album

def test_extras(self):
def test_extras(self, tmp_config):
"""Add any extras that are within the album directory."""
tmp_config()
album = album_factory(exists=True)
new_album = Album.from_dir(album.path)

Expand All @@ -73,8 +75,9 @@ def test_no_valid_tracks(self, tmp_path):
with pytest.raises(AlbumError):
Album.from_dir(empty_path)

def test_add_multi_disc(self):
def test_add_multi_disc(self, tmp_config):
"""We can add a multi-disc album."""
tmp_config()
album = album_factory(exists=True)
track1 = album.tracks[0]
track2 = album.tracks[1]
Expand Down
30 changes: 28 additions & 2 deletions tests/library/test_track.py
Expand Up @@ -28,6 +28,13 @@ def is_unique_track(track, other):
if track.title == other.title:
return False

@staticmethod
@moe.hookimpl
def read_custom_tags(track_path, album_fields, track_fields):
"""Override a new fields for the track and album."""
album_fields["title"] = "custom album title"
track_fields["title"] = "custom track title"


class TestHooks:
"""Test track hooks."""
Expand Down Expand Up @@ -104,8 +111,9 @@ def test_album_set(self, tmp_path):
class TestFromFile:
"""Test initialization from given file path."""

def test_read_tags(self):
def test_read_tags(self, tmp_config):
"""We can initialize a track with tags from a file if present."""
tmp_config()
track = track_factory(exists=True)
track.album = "The Lost Album"
track.albumartist = "Wu-Tang Clan"
Expand Down Expand Up @@ -135,8 +143,9 @@ def test_non_track_file(self):
with pytest.raises(TrackError):
Track.from_file(extra_factory().path)

def test_albumartist_backup(self):
def test_albumartist_backup(self, tmp_config):
"""Use artist as a backup for albumartist if missing."""
tmp_config()
track = track_factory(exists=True)
track.albumartist = ""
track.artist = "Backup"
Expand All @@ -145,6 +154,23 @@ def test_albumartist_backup(self):
track = Track.from_file(track.path)
assert track.albumartist

def test_read_custom_tags(self, tmp_config):
"""Plugins can add additional track and album fields via `read_custom_tags`."""
tmp_config(extra_plugins=[ExtraPlugin(MyTrackPlugin, "track_plugin")])
track = track_factory(exists=True)
new_track = Track.from_file(track.path)

assert new_track.album == "custom album title"
assert new_track.title == "custom track title"

def test_read_track_fields(self, tmp_config):
"""Plugins can add additional album fields via the `read_album_fields` hook."""
tmp_config(extra_plugins=[ExtraPlugin(MyTrackPlugin, "track_plugin")])
track = track_factory(exists=True)
new_track = Track.from_file(track.path)

assert new_track.title == "custom track title"


class TestEquality:
"""Test equality of tracks."""
Expand Down
3 changes: 2 additions & 1 deletion tests/plugins/test_write.py
Expand Up @@ -27,8 +27,9 @@ def _tmp_write_config(tmp_config):
class TestWriteTags:
"""Tests `write_tags()`."""

def test_write_tags(self):
def test_write_tags(self, tmp_config):
"""We can write track changes to the file."""
tmp_config()
track = track_factory(exists=True)
album = "Bigger, Better, Faster, More!"
albumartist = "4 Non Blondes"
Expand Down

0 comments on commit b5069ba

Please sign in to comment.