mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
match.current_metadata -> util.get_most_common_tags
This commit is contained in:
parent
509cbdcbe4
commit
1c9aebd36c
6 changed files with 127 additions and 130 deletions
|
|
@ -24,13 +24,7 @@ from beets.library import Album, Item, LibModel
|
||||||
from beets.util import unique_list
|
from beets.util import unique_list
|
||||||
|
|
||||||
from .hooks import AlbumInfo, AlbumMatch, Distance, TrackInfo, TrackMatch
|
from .hooks import AlbumInfo, AlbumMatch, Distance, TrackInfo, TrackMatch
|
||||||
from .match import (
|
from .match import Proposal, Recommendation, tag_album, tag_item
|
||||||
Proposal,
|
|
||||||
Recommendation,
|
|
||||||
current_metadata,
|
|
||||||
tag_album,
|
|
||||||
tag_item,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AlbumInfo",
|
"AlbumInfo",
|
||||||
|
|
@ -43,7 +37,6 @@ __all__ = [
|
||||||
"apply_album_metadata",
|
"apply_album_metadata",
|
||||||
"apply_item_metadata",
|
"apply_item_metadata",
|
||||||
"apply_metadata",
|
"apply_metadata",
|
||||||
"current_metadata",
|
|
||||||
"tag_album",
|
"tag_album",
|
||||||
"tag_item",
|
"tag_item",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ from beets.autotag import (
|
||||||
TrackMatch,
|
TrackMatch,
|
||||||
hooks,
|
hooks,
|
||||||
)
|
)
|
||||||
from beets.util import plurality
|
from beets.util import get_most_common_tags
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Iterable, Sequence
|
from collections.abc import Iterable, Sequence
|
||||||
|
|
@ -80,44 +80,6 @@ class Proposal(NamedTuple):
|
||||||
# Primary matching functionality.
|
# Primary matching functionality.
|
||||||
|
|
||||||
|
|
||||||
def current_metadata(
|
|
||||||
items: Iterable[Item],
|
|
||||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
||||||
"""Extract the likely current metadata for an album given a list of its
|
|
||||||
items. Return two dictionaries:
|
|
||||||
- The most common value for each field.
|
|
||||||
- Whether each field's value was unanimous (values are booleans).
|
|
||||||
"""
|
|
||||||
assert items # Must be nonempty.
|
|
||||||
|
|
||||||
likelies = {}
|
|
||||||
consensus = {}
|
|
||||||
fields = [
|
|
||||||
"artist",
|
|
||||||
"album",
|
|
||||||
"albumartist",
|
|
||||||
"year",
|
|
||||||
"disctotal",
|
|
||||||
"mb_albumid",
|
|
||||||
"label",
|
|
||||||
"barcode",
|
|
||||||
"catalognum",
|
|
||||||
"country",
|
|
||||||
"media",
|
|
||||||
"albumdisambig",
|
|
||||||
]
|
|
||||||
for field in fields:
|
|
||||||
values = [item[field] for item in items if item]
|
|
||||||
likelies[field], freq = plurality(values)
|
|
||||||
consensus[field] = freq == len(values)
|
|
||||||
|
|
||||||
# If there's an album artist consensus, use this for the artist.
|
|
||||||
if consensus["albumartist"] and likelies["albumartist"]:
|
|
||||||
likelies["artist"] = likelies["albumartist"]
|
|
||||||
|
|
||||||
return likelies, consensus
|
|
||||||
|
|
||||||
|
|
||||||
def assign_items(
|
def assign_items(
|
||||||
items: Sequence[Item],
|
items: Sequence[Item],
|
||||||
tracks: Sequence[TrackInfo],
|
tracks: Sequence[TrackInfo],
|
||||||
|
|
@ -231,7 +193,7 @@ def distance(
|
||||||
keys are a subset of `items` and the values are a subset of
|
keys are a subset of `items` and the values are a subset of
|
||||||
`album_info.tracks`.
|
`album_info.tracks`.
|
||||||
"""
|
"""
|
||||||
likelies, _ = current_metadata(items)
|
likelies, _ = get_most_common_tags(items)
|
||||||
|
|
||||||
dist = hooks.Distance()
|
dist = hooks.Distance()
|
||||||
|
|
||||||
|
|
@ -499,7 +461,7 @@ def tag_album(
|
||||||
candidates.
|
candidates.
|
||||||
"""
|
"""
|
||||||
# Get current metadata.
|
# Get current metadata.
|
||||||
likelies, consensus = current_metadata(items)
|
likelies, consensus = get_most_common_tags(items)
|
||||||
cur_artist: str = likelies["artist"]
|
cur_artist: str = likelies["artist"]
|
||||||
cur_album: str = likelies["album"]
|
cur_album: str = likelies["album"]
|
||||||
log.debug("Tagging {0} - {1}", cur_artist, cur_album)
|
log.debug("Tagging {0} - {1}", cur_artist, cur_album)
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ class ImportTask(BaseImportTask):
|
||||||
or APPLY (in which case the data comes from the choice).
|
or APPLY (in which case the data comes from the choice).
|
||||||
"""
|
"""
|
||||||
if self.choice_flag in (Action.ASIS, Action.RETAG):
|
if self.choice_flag in (Action.ASIS, Action.RETAG):
|
||||||
likelies, consensus = autotag.current_metadata(self.items)
|
likelies, consensus = util.get_most_common_tags(self.items)
|
||||||
return likelies
|
return likelies
|
||||||
elif self.choice_flag is Action.APPLY and self.match:
|
elif self.choice_flag is Action.APPLY and self.match:
|
||||||
return self.match.info.copy()
|
return self.match.info.copy()
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ if TYPE_CHECKING:
|
||||||
from collections.abc import Iterator, Sequence
|
from collections.abc import Iterator, Sequence
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
|
|
||||||
|
from beets.library import Item
|
||||||
|
|
||||||
if sys.version_info >= (3, 10):
|
if sys.version_info >= (3, 10):
|
||||||
from typing import TypeAlias
|
from typing import TypeAlias
|
||||||
else:
|
else:
|
||||||
|
|
@ -814,6 +816,44 @@ def plurality(objs: Iterable[T]) -> tuple[T, int]:
|
||||||
return c.most_common(1)[0]
|
return c.most_common(1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def get_most_common_tags(
|
||||||
|
items: Sequence[Item],
|
||||||
|
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||||
|
"""Extract the likely current metadata for an album given a list of its
|
||||||
|
items. Return two dictionaries:
|
||||||
|
- The most common value for each field.
|
||||||
|
- Whether each field's value was unanimous (values are booleans).
|
||||||
|
"""
|
||||||
|
assert items # Must be nonempty.
|
||||||
|
|
||||||
|
likelies = {}
|
||||||
|
consensus = {}
|
||||||
|
fields = [
|
||||||
|
"artist",
|
||||||
|
"album",
|
||||||
|
"albumartist",
|
||||||
|
"year",
|
||||||
|
"disctotal",
|
||||||
|
"mb_albumid",
|
||||||
|
"label",
|
||||||
|
"barcode",
|
||||||
|
"catalognum",
|
||||||
|
"country",
|
||||||
|
"media",
|
||||||
|
"albumdisambig",
|
||||||
|
]
|
||||||
|
for field in fields:
|
||||||
|
values = [item[field] for item in items if item]
|
||||||
|
likelies[field], freq = plurality(values)
|
||||||
|
consensus[field] = freq == len(values)
|
||||||
|
|
||||||
|
# If there's an album artist consensus, use this for the artist.
|
||||||
|
if consensus["albumartist"] and likelies["albumartist"]:
|
||||||
|
likelies["artist"] = likelies["albumartist"]
|
||||||
|
|
||||||
|
return likelies, consensus
|
||||||
|
|
||||||
|
|
||||||
# stdout and stderr as bytes
|
# stdout and stderr as bytes
|
||||||
class CommandOutput(NamedTuple):
|
class CommandOutput(NamedTuple):
|
||||||
stdout: bytes
|
stdout: bytes
|
||||||
|
|
|
||||||
|
|
@ -24,86 +24,6 @@ from beets.autotag import AlbumInfo, TrackInfo, correct_list_fields, match
|
||||||
from beets.autotag.hooks import Distance, string_dist
|
from beets.autotag.hooks import Distance, string_dist
|
||||||
from beets.library import Item
|
from beets.library import Item
|
||||||
from beets.test.helper import BeetsTestCase, ConfigMixin
|
from beets.test.helper import BeetsTestCase, ConfigMixin
|
||||||
from beets.util import plurality
|
|
||||||
|
|
||||||
|
|
||||||
class PluralityTest(BeetsTestCase):
|
|
||||||
def test_plurality_consensus(self):
|
|
||||||
objs = [1, 1, 1, 1]
|
|
||||||
obj, freq = plurality(objs)
|
|
||||||
assert obj == 1
|
|
||||||
assert freq == 4
|
|
||||||
|
|
||||||
def test_plurality_near_consensus(self):
|
|
||||||
objs = [1, 1, 2, 1]
|
|
||||||
obj, freq = plurality(objs)
|
|
||||||
assert obj == 1
|
|
||||||
assert freq == 3
|
|
||||||
|
|
||||||
def test_plurality_conflict(self):
|
|
||||||
objs = [1, 1, 2, 2, 3]
|
|
||||||
obj, freq = plurality(objs)
|
|
||||||
assert obj in (1, 2)
|
|
||||||
assert freq == 2
|
|
||||||
|
|
||||||
def test_plurality_empty_sequence_raises_error(self):
|
|
||||||
with pytest.raises(ValueError, match="must be non-empty"):
|
|
||||||
plurality([])
|
|
||||||
|
|
||||||
def test_current_metadata_finds_pluralities(self):
|
|
||||||
items = [
|
|
||||||
Item(artist="The Beetles", album="The White Album"),
|
|
||||||
Item(artist="The Beatles", album="The White Album"),
|
|
||||||
Item(artist="The Beatles", album="Teh White Album"),
|
|
||||||
]
|
|
||||||
likelies, consensus = match.current_metadata(items)
|
|
||||||
assert likelies["artist"] == "The Beatles"
|
|
||||||
assert likelies["album"] == "The White Album"
|
|
||||||
assert not consensus["artist"]
|
|
||||||
|
|
||||||
def test_current_metadata_artist_consensus(self):
|
|
||||||
items = [
|
|
||||||
Item(artist="The Beatles", album="The White Album"),
|
|
||||||
Item(artist="The Beatles", album="The White Album"),
|
|
||||||
Item(artist="The Beatles", album="Teh White Album"),
|
|
||||||
]
|
|
||||||
likelies, consensus = match.current_metadata(items)
|
|
||||||
assert likelies["artist"] == "The Beatles"
|
|
||||||
assert likelies["album"] == "The White Album"
|
|
||||||
assert consensus["artist"]
|
|
||||||
|
|
||||||
def test_albumartist_consensus(self):
|
|
||||||
items = [
|
|
||||||
Item(artist="tartist1", album="album", albumartist="aartist"),
|
|
||||||
Item(artist="tartist2", album="album", albumartist="aartist"),
|
|
||||||
Item(artist="tartist3", album="album", albumartist="aartist"),
|
|
||||||
]
|
|
||||||
likelies, consensus = match.current_metadata(items)
|
|
||||||
assert likelies["artist"] == "aartist"
|
|
||||||
assert not consensus["artist"]
|
|
||||||
|
|
||||||
def test_current_metadata_likelies(self):
|
|
||||||
fields = [
|
|
||||||
"artist",
|
|
||||||
"album",
|
|
||||||
"albumartist",
|
|
||||||
"year",
|
|
||||||
"disctotal",
|
|
||||||
"mb_albumid",
|
|
||||||
"label",
|
|
||||||
"barcode",
|
|
||||||
"catalognum",
|
|
||||||
"country",
|
|
||||||
"media",
|
|
||||||
"albumdisambig",
|
|
||||||
]
|
|
||||||
items = [Item(**{f: f"{f}_{i or 1}" for f in fields}) for i in range(5)]
|
|
||||||
likelies, _ = match.current_metadata(items)
|
|
||||||
for f in fields:
|
|
||||||
if isinstance(likelies[f], int):
|
|
||||||
assert likelies[f] == 0
|
|
||||||
else:
|
|
||||||
assert likelies[f] == f"{f}_1"
|
|
||||||
|
|
||||||
|
|
||||||
def _make_item(title, track, artist="some artist"):
|
def _make_item(title, track, artist="some artist"):
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,10 @@ from unittest.mock import Mock, patch
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from beets import util
|
from beets import util
|
||||||
|
from beets.library import Item
|
||||||
from beets.test import _common
|
from beets.test import _common
|
||||||
|
from beets.test.helper import BeetsTestCase
|
||||||
|
from beets.util import plurality
|
||||||
|
|
||||||
|
|
||||||
class UtilTest(unittest.TestCase):
|
class UtilTest(unittest.TestCase):
|
||||||
|
|
@ -217,3 +220,82 @@ class TestPathLegalization:
|
||||||
expected_path,
|
expected_path,
|
||||||
expected_truncated,
|
expected_truncated,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PluralityTest(BeetsTestCase):
|
||||||
|
def test_plurality_consensus(self):
|
||||||
|
objs = [1, 1, 1, 1]
|
||||||
|
obj, freq = plurality(objs)
|
||||||
|
assert obj == 1
|
||||||
|
assert freq == 4
|
||||||
|
|
||||||
|
def test_plurality_near_consensus(self):
|
||||||
|
objs = [1, 1, 2, 1]
|
||||||
|
obj, freq = plurality(objs)
|
||||||
|
assert obj == 1
|
||||||
|
assert freq == 3
|
||||||
|
|
||||||
|
def test_plurality_conflict(self):
|
||||||
|
objs = [1, 1, 2, 2, 3]
|
||||||
|
obj, freq = plurality(objs)
|
||||||
|
assert obj in (1, 2)
|
||||||
|
assert freq == 2
|
||||||
|
|
||||||
|
def test_plurality_empty_sequence_raises_error(self):
|
||||||
|
with pytest.raises(ValueError, match="must be non-empty"):
|
||||||
|
plurality([])
|
||||||
|
|
||||||
|
def test_current_metadata_finds_pluralities(self):
|
||||||
|
items = [
|
||||||
|
Item(artist="The Beetles", album="The White Album"),
|
||||||
|
Item(artist="The Beatles", album="The White Album"),
|
||||||
|
Item(artist="The Beatles", album="Teh White Album"),
|
||||||
|
]
|
||||||
|
likelies, consensus = util.get_most_common_tags(items)
|
||||||
|
assert likelies["artist"] == "The Beatles"
|
||||||
|
assert likelies["album"] == "The White Album"
|
||||||
|
assert not consensus["artist"]
|
||||||
|
|
||||||
|
def test_current_metadata_artist_consensus(self):
|
||||||
|
items = [
|
||||||
|
Item(artist="The Beatles", album="The White Album"),
|
||||||
|
Item(artist="The Beatles", album="The White Album"),
|
||||||
|
Item(artist="The Beatles", album="Teh White Album"),
|
||||||
|
]
|
||||||
|
likelies, consensus = util.get_most_common_tags(items)
|
||||||
|
assert likelies["artist"] == "The Beatles"
|
||||||
|
assert likelies["album"] == "The White Album"
|
||||||
|
assert consensus["artist"]
|
||||||
|
|
||||||
|
def test_albumartist_consensus(self):
|
||||||
|
items = [
|
||||||
|
Item(artist="tartist1", album="album", albumartist="aartist"),
|
||||||
|
Item(artist="tartist2", album="album", albumartist="aartist"),
|
||||||
|
Item(artist="tartist3", album="album", albumartist="aartist"),
|
||||||
|
]
|
||||||
|
likelies, consensus = util.get_most_common_tags(items)
|
||||||
|
assert likelies["artist"] == "aartist"
|
||||||
|
assert not consensus["artist"]
|
||||||
|
|
||||||
|
def test_current_metadata_likelies(self):
|
||||||
|
fields = [
|
||||||
|
"artist",
|
||||||
|
"album",
|
||||||
|
"albumartist",
|
||||||
|
"year",
|
||||||
|
"disctotal",
|
||||||
|
"mb_albumid",
|
||||||
|
"label",
|
||||||
|
"barcode",
|
||||||
|
"catalognum",
|
||||||
|
"country",
|
||||||
|
"media",
|
||||||
|
"albumdisambig",
|
||||||
|
]
|
||||||
|
items = [Item(**{f: f"{f}_{i or 1}" for f in fields}) for i in range(5)]
|
||||||
|
likelies, _ = util.get_most_common_tags(items)
|
||||||
|
for f in fields:
|
||||||
|
if isinstance(likelies[f], int):
|
||||||
|
assert likelies[f] == 0
|
||||||
|
else:
|
||||||
|
assert likelies[f] == f"{f}_1"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue