Skip to content

Commit

Permalink
refactor!: rename album_obj reference to album in tracks and extras
Browse files Browse the repository at this point in the history
`track.album` now refers to the actual album object (renamed from `track.album_obj`) and `track.albumartist` has been removed. Similarly, `extra.album_obj` has been renamed to `extra.album`.

The original idea was that `track.album` was a string that referred to an album's title, while `track.album_obj` was the actual album object itself. `track.album` and `track.albumartist` were "mapped" attributes of an album directly exposed in the track API due to convention. However, these mapped attributes are not first-class attributes as far as sqlalchemy is concerned, and thus have additional issues and considerations compared to normal attributes. Ultimately, I decided these mapped attributes are not worth the headache.
  • Loading branch information
jtpavlock committed Dec 20, 2022
1 parent 66a93eb commit 51ff9a9
Show file tree
Hide file tree
Showing 23 changed files with 95 additions and 147 deletions.
5 changes: 0 additions & 5 deletions docs/fields.rst
Expand Up @@ -4,14 +4,9 @@ Fields

These are the fields Moe keeps track of for tracks, albums, and extras in your library.

.. note::
Some track fields are actually on a per-album basis, and are just a mapping to the related album field e.g. ``albumartist`` or ``album``. These fields are exposed as track fields for convenience and due to convention.

************
Track Fields
************
* ``album`` - album title
* ``albumartist`` - album artist
* ``artist`` - track artist
* ``artists`` - track artists [#f1]_
* ``audio_format`` - aac, aiff, alac, ape, asf, dsf, flac, ogg, opus, mp3, mpc, wav, or wv [#f3]_ [#f4]_
Expand Down
8 changes: 0 additions & 8 deletions docs/plugins/edit.rst
Expand Up @@ -30,14 +30,6 @@ Positional Arguments
.. note::
If the specified field supports multiple values, you can separate those values with a semicolon e.g. ``genre=hip hop;pop``.

.. note::
Editing a track's album-related field is equivalent to editing the field directly through the album. For example, the following commands will both edit an album's artist.

.. code-block:: bash
moe edit [query] albumartist=2Pac
moe edit -a [query] artist=2Pac
Optional Arguments
==================
``-h, --help``
Expand Down
6 changes: 3 additions & 3 deletions moe/library/album.py
Expand Up @@ -358,13 +358,13 @@ class Album(LibItem, SABase, MetaAlbum):

tracks: list["Track"] = relationship(
"Track",
back_populates="album_obj",
back_populates="album",
cascade="all, delete-orphan",
collection_class=list,
)
extras: list["Extra"] = relationship(
"Extra",
back_populates="album_obj",
back_populates="album",
cascade="all, delete-orphan",
collection_class=list,
)
Expand Down Expand Up @@ -435,7 +435,7 @@ def from_dir(cls: type[A], album_path: Path) -> A:
extra_paths.append(file_path)
else:
if not album:
album = track.album_obj
album = track.album

if not album:
raise AlbumError(f"No tracks found in album directory. [dir={album_path}]")
Expand Down
20 changes: 10 additions & 10 deletions moe/library/extra.py
Expand Up @@ -69,7 +69,7 @@ class Extra(LibItem, SABase):
"""An Album can have any number of extra files such as logs, cues, etc.
Attributes:
album_obj (Album): Album the extra file belongs to.
album (Album): Album the extra file belongs to.
path (pathlib.Path): Filesystem path of the extra file.
"""

Expand All @@ -85,7 +85,7 @@ class Extra(LibItem, SABase):
)

_album_id: int = cast(int, Column(Integer, ForeignKey("album._id")))
album_obj: Album = relationship("Album", back_populates="extras")
album: Album = relationship("Album", back_populates="extras")

def __init__(self, album: Album, path: Path, **kwargs):
"""Creates an Extra.
Expand All @@ -109,12 +109,12 @@ def __init__(self, album: Album, path: Path, **kwargs):
@property
def fields(self) -> set[str]:
"""Returns any editable, extra fields."""
return {"album_obj", "path"}.union(self._custom_fields)
return {"album", "path"}.union(self._custom_fields)

@property
def rel_path(self) -> PurePath:
"""Returns the extra's path relative to its album's path."""
return self.path.relative_to(self.album_obj.path)
return self.path.relative_to(self.album.path)

def is_unique(self, other: "Extra") -> bool:
"""Returns whether an extra is unique in the library from ``other``."""
Expand All @@ -140,7 +140,7 @@ def merge(self, other: "Extra", overwrite: bool = False):
f"Merging extras. [extra_a={self!r}, extra_b={other!r}, {overwrite=!r}]"
)

omit_fields = {"album_obj"}
omit_fields = {"album"}
for field in self.fields - omit_fields:
other_value = getattr(other, field)
self_value = getattr(self, field)
Expand All @@ -166,19 +166,19 @@ def __eq__(self, other):

def __lt__(self, other: "Extra") -> bool:
"""Sort based on album then path."""
if self.album_obj == other.album_obj:
if self.album == other.album:
return self.path < other.path

return self.album_obj < other.album_obj
return self.album < other.album

def __repr__(self):
"""Represents an Extra using its path and album."""
field_reprs = []
omit_fields = {"album_obj"}
omit_fields = {"album"}
for field in self.fields - omit_fields:
if hasattr(self, field):
field_reprs.append(f"{field}={getattr(self, field)!r}")
repr_str = "Extra(" + ", ".join(field_reprs) + f", album='{self.album_obj}'"
repr_str = "Extra(" + ", ".join(field_reprs) + f", album='{self.album}'"

custom_field_reprs = []
for custom_field, value in self._custom_fields.items():
Expand All @@ -191,7 +191,7 @@ def __repr__(self):

def __str__(self):
"""String representation of an Extra."""
return f"{self.album_obj}: {self.rel_path}"
return f"{self.album}: {self.rel_path}"

def _get_default_custom_fields(self) -> dict[str, Any]:
"""Returns the default custom extra fields."""
Expand Down
37 changes: 16 additions & 21 deletions moe/library/track.py
Expand Up @@ -7,7 +7,6 @@
import mediafile
import pluggy
from sqlalchemy import JSON, Column, Integer, String
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.mutable import MutableDict, MutableSet
from sqlalchemy.orm import relationship
from sqlalchemy.schema import ForeignKey, UniqueConstraint
Expand Down Expand Up @@ -149,7 +148,7 @@ class MetaTrack(MetaLibItem):
instance.
Attributes:
album_obj (Optional[Album]): Corresponding Album object.
album (Optional[Album]): Corresponding Album object.
artist (Optional[str])
artists (Optional[set[str]]): Set of all artists.
disc (Optional[int]): Disc number the track is on.
Expand All @@ -173,11 +172,11 @@ def __init__(
self._custom_fields = self._get_default_custom_fields()
self._custom_fields_set = set(self._custom_fields)

self.album_obj = album
self.album = album
album.tracks.append(self)

self.track_num = track_num
self.artist = artist or self.album_obj.artist
self.artist = artist or self.album.artist
self.artists = artists
self.disc = disc
self.genres = genres
Expand Down Expand Up @@ -212,7 +211,7 @@ def genre(self, genre_str: Optional[str]):
def fields(self) -> set[str]:
"""Returns any editable, track-specific fields."""
return {
"album_obj",
"album",
"artist",
"artists",
"disc",
Expand All @@ -232,7 +231,7 @@ def merge(self, other: "MetaTrack", overwrite: bool = False):
f"Merging tracks. [track_a={self!r}, track_b={other!r}, {overwrite=!r}]"
)

omit_fields = {"album_obj"}
omit_fields = {"album"}
for field in self.fields - omit_fields:
other_value = getattr(other, field, None)
self_value = getattr(self, field, None)
Expand All @@ -258,25 +257,25 @@ def __eq__(self, other) -> bool:

def __lt__(self, other) -> bool:
"""Sort based on album, then disc, then track number."""
if self.album_obj == other.album_obj:
if self.album == other.album:
if self.disc == other.disc:
return self.track_num < other.track_num

return self.disc < other.disc

return self.album_obj < other.album_obj
return self.album < other.album

def __repr__(self):
"""Represents a Track using track-specific and relevant album fields."""
field_reprs = []
omit_fields = {"album_obj"}
omit_fields = {"album"}
for field in self.fields - omit_fields:
if hasattr(self, field):
field_reprs.append(f"{field}={getattr(self, field)!r}")
repr_str = (
f"{type(self).__name__}("
+ ", ".join(field_reprs)
+ f", album='{self.album_obj}'"
+ f", album='{self.album}'"
)

custom_field_reprs = []
Expand Down Expand Up @@ -305,9 +304,7 @@ class Track(LibItem, SABase, MetaTrack):
"""A single track in the library.
Attributes:
album (str)
albumartist (str)
album_obj (Album): Corresponding Album object.
album (Album): Corresponding Album object.
artist (str)
artists (Optional[set[str]]): Set of all artists.
disc (int): Disc number the track is on.
Expand Down Expand Up @@ -343,9 +340,7 @@ class Track(LibItem, SABase, MetaTrack):
)

_album_id: int = cast(int, Column(Integer, ForeignKey("album._id")))
album_obj: Album = relationship("Album", back_populates="tracks")
album: str = association_proxy("album_obj", "title")
albumartist: str = association_proxy("album_obj", "artist")
album: Album = relationship("Album", back_populates="tracks")

__table_args__ = (UniqueConstraint("disc", "track_num", "_album_id"),)

Expand Down Expand Up @@ -378,7 +373,7 @@ def __init__(
setattr(self, key, value)

if self.artist is None:
self.artist = self.albumartist
self.artist = self.album.artist

if not self.disc:
self.disc = self._guess_disc()
Expand All @@ -389,17 +384,17 @@ def _guess_disc(self) -> int:
"""Attempts to guess the disc of a track based on it's path."""
log.debug(f"Guessing track disc number. [track={self!r}]")

if self.path.parent == self.album_obj.path:
if self.path.parent == self.album.path:
return 1

# The track is in a subdirectory of the album - most likely disc directories.
disc_dirs: list[Path] = []
for path in self.album_obj.path.iterdir():
for path in self.album.path.iterdir():
if not path.is_dir():
continue

contains_tracks = False
for album_track in self.album_obj.tracks:
for album_track in self.album.tracks:
if album_track.path.is_relative_to(path):
contains_tracks = True

Expand Down Expand Up @@ -527,7 +522,7 @@ def is_unique(self, other: "Track") -> bool:
if (
self.track_num == other.track_num
and self.disc == other.disc
and self.album_obj == other.album_obj
and self.album == other.album
):
return False

Expand Down
8 changes: 3 additions & 5 deletions moe/plugins/duplicate/dup_cli.py
Expand Up @@ -113,14 +113,12 @@ def _fmt_item_text(
header = Text(f"{item}", justify="center")
omit_fields = {"extras", "tracks", "year"}
elif isinstance(item, Extra):
header = Text(f"{item.rel_path}\n{item.album_obj}", justify="center")
omit_fields = {"album_obj", "rel_path", "path"}
header = Text(f"{item.rel_path}\n{item.album}", justify="center")
omit_fields = {"album", "rel_path", "path"}
else:
header = Text(f"{item.title}\n{item.album_obj}", justify="center")
header = Text(f"{item.title}\n{item.album}", justify="center")
omit_fields = {
"album",
"albumartist",
"album_obj",
"date",
"disc_total",
"genre",
Expand Down
2 changes: 1 addition & 1 deletion moe/plugins/list.py
Expand Up @@ -131,7 +131,7 @@ def _fmt_extra_info(extra: Extra) -> str:
def _fmt_track_info(track: Track) -> str:
"""Formats a track's information for display."""
base_dict = OrderedDict(sorted(_get_base_dict(track).items()))
base_dict.pop("album_obj", None)
base_dict.pop("album", None)

return "\n".join(
f"{field}: {value}"
Expand Down
2 changes: 1 addition & 1 deletion moe/plugins/moe_import/import_core.py
Expand Up @@ -102,7 +102,7 @@ def pre_add(item: LibItem):
if isinstance(item, Album):
album = item
elif isinstance(item, Track):
album = item.album_obj
album = item.album
else:
return

Expand Down
10 changes: 5 additions & 5 deletions moe/plugins/move/move_core.py
Expand Up @@ -79,7 +79,7 @@ def create_path_template_func() -> list[Callable]:

def e_unique(extra: Extra) -> str:
"""Returns a unique filename for an extra within its album."""
extra_names = [album_extra.path.name for album_extra in extra.album_obj.extras]
extra_names = [album_extra.path.name for album_extra in extra.album.extras]

if (name_count := extra_names.count(extra.path.name)) > 1:
return extra.path.stem + f" ({name_count - 1})" + extra.path.suffix
Expand Down Expand Up @@ -130,15 +130,15 @@ def _fmt_album_path(album: Album, lib_path: Path) -> Path:

def _fmt_extra_path(extra: Extra, lib_path: Path) -> Path:
"""Returns a formatted extra path according to the user configuration."""
album_path = _fmt_album_path(extra.album_obj, lib_path)
album_path = _fmt_album_path(extra.album, lib_path)
extra_path = _eval_path_template(config.CONFIG.settings.move.extra_path, extra)

return album_path / extra_path


def _fmt_track_path(track: Track, lib_path: Path) -> Path:
"""Returns a formatted track path according to the user configuration."""
album_path = _fmt_album_path(track.album_obj, lib_path)
album_path = _fmt_album_path(track.album, lib_path)
track_path = _eval_path_template(config.CONFIG.settings.move.track_path, track)

return album_path / track_path
Expand Down Expand Up @@ -194,10 +194,10 @@ def _lazy_fstr_item(template: str, lib_item: LibItem) -> str:
album = lib_item # noqa: F841
elif isinstance(lib_item, Track):
track = lib_item # noqa: F841
album = track.album_obj
album = track.album
elif isinstance(lib_item, Extra):
extra = lib_item # noqa: F841
album = extra.album_obj # noqa: F841
album = extra.album # noqa: F841
else:
raise NotImplementedError

Expand Down
2 changes: 1 addition & 1 deletion moe/plugins/musicbrainz/mb_cli.py
Expand Up @@ -64,7 +64,7 @@ def _parse_args(args: argparse.Namespace):
for item in items:
release_id: Optional[str] = None
if isinstance(item, (Extra, Track)):
release_id = item.album_obj.mb_album_id
release_id = item.album.mb_album_id
elif isinstance(item, Album):
release_id = item.mb_album_id

Expand Down
4 changes: 2 additions & 2 deletions moe/plugins/musicbrainz/mb_core.py
Expand Up @@ -215,7 +215,7 @@ def sync_metadata(item: LibItem):
elif isinstance(item, Track) and hasattr(item, "mb_track_id"):
item = cast(Track, item)
item.merge(
get_track_by_id(item.mb_track_id, item.album_obj.mb_album_id),
get_track_by_id(item.mb_track_id, item.album.mb_album_id),
overwrite=True,
)

Expand All @@ -225,7 +225,7 @@ def write_custom_tags(track: Track):
"""Write musicbrainz ID fields as tags."""
audio_file = mediafile.MediaFile(track.path)

audio_file.mb_albumid = track.album_obj.mb_album_id
audio_file.mb_albumid = track.album.mb_album_id
audio_file.mb_releasetrackid = track.mb_track_id

audio_file.save()
Expand Down
2 changes: 1 addition & 1 deletion moe/plugins/remove/rm_core.py
Expand Up @@ -25,7 +25,7 @@ def remove_item(item: LibItem):
elif insp.pending:
session.expunge(item)
if isinstance(item, (Track, Extra)):
item.album_obj = None # type: ignore
item.album = None # type: ignore

try:
session.flush()
Expand Down

0 comments on commit 51ff9a9

Please sign in to comment.