mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
hooks: introduce Info.name property
This commit is contained in:
parent
77e25db1b5
commit
02f3cb7821
3 changed files with 117 additions and 109 deletions
|
|
@ -18,10 +18,13 @@ from __future__ import annotations
|
|||
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING, Any, TypeVar
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from beets.util import cached_classproperty
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from beets.library import Item
|
||||
|
||||
|
|
@ -55,6 +58,10 @@ class AttrDict(dict[str, V]):
|
|||
class Info(AttrDict[Any]):
|
||||
"""Container for metadata about a musical entity."""
|
||||
|
||||
@cached_property
|
||||
def name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
album: str | None = None,
|
||||
|
|
@ -96,6 +103,10 @@ class AlbumInfo(Info):
|
|||
user items, and later to drive tagging decisions once selected.
|
||||
"""
|
||||
|
||||
@cached_property
|
||||
def name(self) -> str:
|
||||
return self.album or ""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tracks: list[TrackInfo],
|
||||
|
|
@ -168,6 +179,10 @@ class TrackInfo(Info):
|
|||
stand alone for singleton matching.
|
||||
"""
|
||||
|
||||
@cached_property
|
||||
def name(self) -> str:
|
||||
return self.title or ""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
|
|
@ -220,6 +235,10 @@ class Match:
|
|||
distance: Distance
|
||||
info: Info
|
||||
|
||||
@cached_classproperty
|
||||
def type(cls) -> str:
|
||||
return cls.__name__.removesuffix("Match") # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlbumMatch(Match):
|
||||
|
|
|
|||
|
|
@ -1,15 +1,36 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING, TypedDict
|
||||
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
from beets import autotag, config, ui
|
||||
from beets.autotag import hooks
|
||||
from beets.util import displayable_path
|
||||
from beets.util.units import human_seconds_short
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
import confuse
|
||||
|
||||
from beets.library.models import Item
|
||||
from beets.ui import ColorName
|
||||
|
||||
VARIOUS_ARTISTS = "Various Artists"
|
||||
|
||||
|
||||
class Line(TypedDict):
|
||||
prefix: str
|
||||
contents: str
|
||||
suffix: str
|
||||
width: NotRequired[int]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChangeRepresentation:
|
||||
"""Keeps track of all information needed to generate a (colored) text
|
||||
representation of the changes that will be made if an album or singleton's
|
||||
|
|
@ -17,46 +38,46 @@ class ChangeRepresentation:
|
|||
TrackMatch object, accordingly.
|
||||
"""
|
||||
|
||||
cur_artist: str
|
||||
cur_name: str
|
||||
match: autotag.hooks.Match
|
||||
|
||||
@cached_property
|
||||
def changed_prefix(self) -> str:
|
||||
return ui.colorize("changed", "\u2260")
|
||||
|
||||
cur_artist = None
|
||||
# cur_album set if album, cur_title set if singleton
|
||||
cur_album = None
|
||||
cur_title = None
|
||||
match = None
|
||||
indent_header = ""
|
||||
indent_detail = ""
|
||||
@cached_property
|
||||
def _indentation_config(self) -> confuse.ConfigView:
|
||||
return config["ui"]["import"]["indentation"]
|
||||
|
||||
def __init__(self):
|
||||
# Read match header indentation width from config.
|
||||
match_header_indent_width = config["ui"]["import"]["indentation"][
|
||||
"match_header"
|
||||
].as_number()
|
||||
self.indent_header = ui.indent(match_header_indent_width)
|
||||
@cached_property
|
||||
def indent_header(self) -> str:
|
||||
return ui.indent(self._indentation_config["match_header"].as_number())
|
||||
|
||||
# Read match detail indentation width from config.
|
||||
match_detail_indent_width = config["ui"]["import"]["indentation"][
|
||||
"match_details"
|
||||
].as_number()
|
||||
self.indent_detail = ui.indent(match_detail_indent_width)
|
||||
@cached_property
|
||||
def indent_detail(self) -> str:
|
||||
return ui.indent(self._indentation_config["match_details"].as_number())
|
||||
|
||||
# Read match tracklist indentation width from config
|
||||
match_tracklist_indent_width = config["ui"]["import"]["indentation"][
|
||||
"match_tracklist"
|
||||
].as_number()
|
||||
self.indent_tracklist = ui.indent(match_tracklist_indent_width)
|
||||
self.layout = config["ui"]["import"]["layout"].as_choice(
|
||||
{
|
||||
"column": 0,
|
||||
"newline": 1,
|
||||
}
|
||||
@cached_property
|
||||
def indent_tracklist(self) -> str:
|
||||
return ui.indent(
|
||||
self._indentation_config["match_tracklist"].as_number()
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def layout(self) -> int:
|
||||
return config["ui"]["import"]["layout"].as_choice(
|
||||
{"column": 0, "newline": 1}
|
||||
)
|
||||
|
||||
def print_layout(
|
||||
self, indent, left, right, separator=" -> ", max_width=None
|
||||
):
|
||||
self,
|
||||
indent: str,
|
||||
left: Line,
|
||||
right: Line,
|
||||
separator: str = " -> ",
|
||||
max_width: int | None = None,
|
||||
) -> None:
|
||||
if not max_width:
|
||||
# If no max_width provided, use terminal width
|
||||
max_width = ui.term_width()
|
||||
|
|
@ -65,7 +86,7 @@ class ChangeRepresentation:
|
|||
else:
|
||||
ui.print_newline_layout(indent, left, right, separator, max_width)
|
||||
|
||||
def show_match_header(self):
|
||||
def show_match_header(self) -> None:
|
||||
"""Print out a 'header' identifying the suggested match (album name,
|
||||
artist name,...) and summarizing the changes that would be made should
|
||||
the user accept the match.
|
||||
|
|
@ -78,19 +99,10 @@ class ChangeRepresentation:
|
|||
f"{self.indent_header}Match ({dist_string(self.match.distance)}):"
|
||||
)
|
||||
|
||||
if isinstance(self.match.info, autotag.hooks.AlbumInfo):
|
||||
# Matching an album - print that
|
||||
artist_album_str = (
|
||||
f"{self.match.info.artist} - {self.match.info.album}"
|
||||
)
|
||||
else:
|
||||
# Matching a single track
|
||||
artist_album_str = (
|
||||
f"{self.match.info.artist} - {self.match.info.title}"
|
||||
)
|
||||
artist_name_str = f"{self.match.info.artist} - {self.match.info.name}"
|
||||
ui.print_(
|
||||
self.indent_header
|
||||
+ dist_colorize(artist_album_str, self.match.distance)
|
||||
+ dist_colorize(artist_name_str, self.match.distance)
|
||||
)
|
||||
|
||||
# Penalties.
|
||||
|
|
@ -108,7 +120,7 @@ class ChangeRepresentation:
|
|||
url = ui.colorize("text_faint", f"{self.match.info.data_url}")
|
||||
ui.print_(f"{self.indent_header}{url}")
|
||||
|
||||
def show_match_details(self):
|
||||
def show_match_details(self) -> None:
|
||||
"""Print out the details of the match, including changes in album name
|
||||
and artist name.
|
||||
"""
|
||||
|
|
@ -117,6 +129,8 @@ class ChangeRepresentation:
|
|||
if artist_r == VARIOUS_ARTISTS:
|
||||
# Hide artists for VA releases.
|
||||
artist_l, artist_r = "", ""
|
||||
left: Line
|
||||
right: Line
|
||||
if artist_l != artist_r:
|
||||
artist_l, artist_r = ui.colordiff(artist_l, artist_r)
|
||||
left = {
|
||||
|
|
@ -130,39 +144,22 @@ class ChangeRepresentation:
|
|||
else:
|
||||
ui.print_(f"{self.indent_detail}*", "Artist:", artist_r)
|
||||
|
||||
if self.cur_album:
|
||||
# Album
|
||||
album_l, album_r = self.cur_album or "", self.match.info.album
|
||||
if (
|
||||
self.cur_album != self.match.info.album
|
||||
and self.match.info.album != VARIOUS_ARTISTS
|
||||
):
|
||||
album_l, album_r = ui.colordiff(album_l, album_r)
|
||||
if self.cur_name:
|
||||
type_ = self.match.type
|
||||
name_l, name_r = self.cur_name or "", self.match.info.name
|
||||
if self.cur_name != self.match.info.name != VARIOUS_ARTISTS:
|
||||
name_l, name_r = ui.colordiff(name_l, name_r)
|
||||
left = {
|
||||
"prefix": f"{self.changed_prefix} Album: ",
|
||||
"contents": album_l,
|
||||
"prefix": f"{self.changed_prefix} {type_}: ",
|
||||
"contents": name_l,
|
||||
"suffix": "",
|
||||
}
|
||||
right = {"prefix": "", "contents": album_r, "suffix": ""}
|
||||
right = {"prefix": "", "contents": name_r, "suffix": ""}
|
||||
self.print_layout(self.indent_detail, left, right)
|
||||
else:
|
||||
ui.print_(f"{self.indent_detail}*", "Album:", album_r)
|
||||
elif self.cur_title:
|
||||
# Title - for singletons
|
||||
title_l, title_r = self.cur_title or "", self.match.info.title
|
||||
if self.cur_title != self.match.info.title:
|
||||
title_l, title_r = ui.colordiff(title_l, title_r)
|
||||
left = {
|
||||
"prefix": f"{self.changed_prefix} Title: ",
|
||||
"contents": title_l,
|
||||
"suffix": "",
|
||||
}
|
||||
right = {"prefix": "", "contents": title_r, "suffix": ""}
|
||||
self.print_layout(self.indent_detail, left, right)
|
||||
else:
|
||||
ui.print_(f"{self.indent_detail}*", "Title:", title_r)
|
||||
ui.print_(f"{self.indent_detail}*", f"{type_}:", name_r)
|
||||
|
||||
def make_medium_info_line(self, track_info):
|
||||
def make_medium_info_line(self, track_info: hooks.TrackInfo) -> str:
|
||||
"""Construct a line with the current medium's info."""
|
||||
track_media = track_info.get("media", "Media")
|
||||
# Build output string.
|
||||
|
|
@ -177,7 +174,7 @@ class ChangeRepresentation:
|
|||
else:
|
||||
return ""
|
||||
|
||||
def format_index(self, track_info):
|
||||
def format_index(self, track_info: hooks.TrackInfo | Item) -> str:
|
||||
"""Return a string representing the track index of the given
|
||||
TrackInfo or Item object.
|
||||
"""
|
||||
|
|
@ -198,12 +195,15 @@ class ChangeRepresentation:
|
|||
else:
|
||||
return str(index)
|
||||
|
||||
def make_track_numbers(self, item, track_info):
|
||||
def make_track_numbers(
|
||||
self, item, track_info: hooks.TrackInfo
|
||||
) -> tuple[str, str, bool]:
|
||||
"""Format colored track indices."""
|
||||
cur_track = self.format_index(item)
|
||||
new_track = self.format_index(track_info)
|
||||
changed = False
|
||||
# Choose color based on change.
|
||||
highlight_color: ColorName
|
||||
if cur_track != new_track:
|
||||
changed = True
|
||||
if item.track in (track_info.index, track_info.medium_index):
|
||||
|
|
@ -218,9 +218,11 @@ class ChangeRepresentation:
|
|||
return lhs_track, rhs_track, changed
|
||||
|
||||
@staticmethod
|
||||
def make_track_titles(item, track_info):
|
||||
def make_track_titles(
|
||||
item: Item, track_info: hooks.TrackInfo
|
||||
) -> tuple[str, str, bool]:
|
||||
"""Format colored track titles."""
|
||||
new_title = track_info.title
|
||||
new_title = track_info.name
|
||||
if not item.title.strip():
|
||||
# If there's no title, we use the filename. Don't colordiff.
|
||||
cur_title = displayable_path(os.path.basename(item.path))
|
||||
|
|
@ -232,9 +234,12 @@ class ChangeRepresentation:
|
|||
return cur_col, new_col, cur_title != new_title
|
||||
|
||||
@staticmethod
|
||||
def make_track_lengths(item, track_info):
|
||||
def make_track_lengths(
|
||||
item: Item, track_info: hooks.TrackInfo
|
||||
) -> tuple[str, str, bool]:
|
||||
"""Format colored track lengths."""
|
||||
changed = False
|
||||
highlight_color: ColorName
|
||||
if (
|
||||
item.length
|
||||
and track_info.length
|
||||
|
|
@ -258,7 +263,9 @@ class ChangeRepresentation:
|
|||
|
||||
return lhs_length, rhs_length, changed
|
||||
|
||||
def make_line(self, item, track_info):
|
||||
def make_line(
|
||||
self, item: Item, track_info: hooks.TrackInfo
|
||||
) -> tuple[Line, Line]:
|
||||
"""Extract changes from item -> new TrackInfo object, and colorize
|
||||
appropriately. Returns (lhs, rhs) for column printing.
|
||||
"""
|
||||
|
|
@ -282,12 +289,12 @@ class ChangeRepresentation:
|
|||
# the case, thus the 'info' dictionary is unneeded.
|
||||
# penalties = penalty_string(self.match.distance.tracks[track_info])
|
||||
|
||||
lhs = {
|
||||
lhs: Line = {
|
||||
"prefix": f"{self.changed_prefix if changed else '*'} {lhs_track} ",
|
||||
"contents": lhs_title,
|
||||
"suffix": f" {lhs_length}",
|
||||
}
|
||||
rhs = {"prefix": "", "contents": "", "suffix": ""}
|
||||
rhs: Line = {"prefix": "", "contents": "", "suffix": ""}
|
||||
if not changed:
|
||||
# Only return the left side, as nothing changed.
|
||||
return (lhs, rhs)
|
||||
|
|
@ -358,27 +365,18 @@ class ChangeRepresentation:
|
|||
|
||||
|
||||
class AlbumChange(ChangeRepresentation):
|
||||
"""Album change representation, setting cur_album"""
|
||||
match: autotag.hooks.AlbumMatch
|
||||
|
||||
def __init__(self, cur_artist, cur_album, match):
|
||||
super().__init__()
|
||||
self.cur_artist = cur_artist
|
||||
self.cur_album = cur_album
|
||||
self.match = match
|
||||
|
||||
def show_match_tracks(self):
|
||||
def show_match_tracks(self) -> None:
|
||||
"""Print out the tracks of the match, summarizing changes the match
|
||||
suggests for them.
|
||||
"""
|
||||
# Tracks.
|
||||
# match is an AlbumMatch NamedTuple, mapping is a dict
|
||||
# Sort the pairs by the track_info index (at index 1 of the NamedTuple)
|
||||
pairs = sorted(
|
||||
self.match.item_info_pairs, key=lambda pair: pair[1].index
|
||||
self.match.item_info_pairs, key=lambda pair: pair[1].index or 0
|
||||
)
|
||||
# Build up LHS and RHS for track difference display. The `lines` list
|
||||
# contains `(left, right)` tuples.
|
||||
lines = []
|
||||
lines: list[tuple[Line, Line]] = []
|
||||
medium = disctitle = None
|
||||
for item, track_info in pairs:
|
||||
# If the track is the first on a new medium, show medium
|
||||
|
|
@ -427,21 +425,17 @@ class AlbumChange(ChangeRepresentation):
|
|||
class TrackChange(ChangeRepresentation):
|
||||
"""Track change representation, comparing item with match."""
|
||||
|
||||
def __init__(self, cur_artist, cur_title, match):
|
||||
super().__init__()
|
||||
self.cur_artist = cur_artist
|
||||
self.cur_title = cur_title
|
||||
self.match = match
|
||||
match: autotag.hooks.TrackMatch
|
||||
|
||||
|
||||
def show_change(cur_artist, cur_album, match):
|
||||
def show_change(
|
||||
cur_artist: str, cur_album: str, match: hooks.AlbumMatch
|
||||
) -> None:
|
||||
"""Print out a representation of the changes that will be made if an
|
||||
album's tags are changed according to `match`, which must be an AlbumMatch
|
||||
object.
|
||||
"""
|
||||
change = AlbumChange(
|
||||
cur_artist=cur_artist, cur_album=cur_album, match=match
|
||||
)
|
||||
change = AlbumChange(cur_artist, cur_album, match)
|
||||
|
||||
# Print the match header.
|
||||
change.show_match_header()
|
||||
|
|
@ -453,13 +447,11 @@ def show_change(cur_artist, cur_album, match):
|
|||
change.show_match_tracks()
|
||||
|
||||
|
||||
def show_item_change(item, match):
|
||||
def show_item_change(item: Item, match: hooks.TrackMatch) -> None:
|
||||
"""Print out the change that would occur by tagging `item` with the
|
||||
metadata from `match`, a TrackMatch object.
|
||||
"""
|
||||
change = TrackChange(
|
||||
cur_artist=item.artist, cur_title=item.title, match=match
|
||||
)
|
||||
change = TrackChange(item.artist, item.title, match)
|
||||
# Print the match header.
|
||||
change.show_match_header()
|
||||
# Print the match details.
|
||||
|
|
|
|||
|
|
@ -451,10 +451,7 @@ def choose_candidate(
|
|||
index = dist_colorize(index0, match.distance)
|
||||
dist = f"({(1 - match.distance) * 100:.1f}%)"
|
||||
distance = dist_colorize(dist, match.distance)
|
||||
metadata = (
|
||||
f"{match.info.artist} -"
|
||||
f" {match.info.title if singleton else match.info.album}"
|
||||
)
|
||||
metadata = f"{match.info.artist} - {match.info.name}"
|
||||
if i == 0:
|
||||
metadata = dist_colorize(metadata, match.distance)
|
||||
else:
|
||||
|
|
|
|||
Loading…
Reference in a new issue