Skip to content
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
11 changes: 10 additions & 1 deletion beetsplug/deezer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from beets.autotag import AlbumInfo, TrackInfo
from beets.dbcore import types
from beets.exceptions import UserError
from beets.importer.tasks import SINGLE_ARTIST_THRESH
from beets.metadata_plugins import IDResponse, SearchApiMetadataSourcePlugin

VARIOUS_ARTISTS_ID = 5080
Expand Down Expand Up @@ -112,7 +113,15 @@ def album_for_id(self, album_id: str) -> AlbumInfo | None:
for track in tracks:
track.medium_total = medium_totals[track.medium]

is_va = str(album_data["artist"]["id"]) == str(VARIOUS_ARTISTS_ID)
album_artist_id = str(album_data["artist"]["id"])
# Deezer assigns some compilations a single "main" artist instead of
# the Various Artists entity, leaving them undetected. Treat a release
# as VA when its album artist performs on fewer tracks than the
# importer's own single-artist plurality threshold.
is_va = album_artist_id == str(VARIOUS_ARTISTS_ID) or (
sum(t.artist_id == album_artist_id for t in tracks)
< len(tracks) * SINGLE_ARTIST_THRESH
)
if is_va:
va_name = config["va_name"].as_str()
artist = va_name
Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ Bug fixes
- :doc:`plugins/lyrics`: ``beet lyrics`` no longer crashes with an
``AttributeError`` on tracks that have no stored lyrics when ``force`` is
enabled; a missing lyrics body is now treated as empty text. :bug:`6860`
- :doc:`plugins/deezer`: Detect compilations that Deezer credits to a single
"main" artist instead of "Various Artists", so they are tagged as such rather
than getting every track artist in the album artist field. :bug:`4057`
- Flexible attributes whose names contain uppercase characters (for example
``beet import --set Tag_With_Uppercase=true``) can now be found by queries.
Field names are lowercased when a query is parsed, so such attributes could
Expand Down
114 changes: 114 additions & 0 deletions test/plugins/test_deezer.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from typing import Any

import pytest

from beets.library import Item
from beetsplug.deezer import DeezerPlugin

JSONDict = dict[str, Any]


@pytest.fixture
def plugin():
Expand Down Expand Up @@ -41,6 +45,19 @@ def test_album_query_omits_artist_for_various_artists(self, plugin):
assert query == 'album:"Album"'


def make_track(artist_id: int, position: int) -> JSONDict:
Comment thread
snejus marked this conversation as resolved.
Resolved
"""Build a minimal Deezer track payload credited to ``artist_id``."""
return {
"title": f"Track {position}",
"id": 1000 + position,
"link": f"https://www.deezer.com/track/{1000 + position}",
"duration": 200,
"track_position": position,
"disk_number": 1,
"artist": {"id": artist_id, "name": f"Artist {artist_id}"},
}


class TestGetTrack:
def track_data(self, **fields):
return {
Expand Down Expand Up @@ -72,3 +89,100 @@ def test_tolerates_missing_artist_and_contributors(self, plugin):

assert track.artist is None
assert track.artist_id is None


class TestVariousArtistsDetection:
def make_album(
self, album_artist_id: int, contributor_ids: list[int]
) -> JSONDict:
"""Build a minimal Deezer album payload."""
return {
"title": "Some Album",
"link": "https://www.deezer.com/album/1",
"record_type": "album",
"label": "Some Label",
"release_date": "2017-01-01",
"artist": {
"id": album_artist_id,
"name": f"Artist {album_artist_id}",
},
"contributors": [
{"id": aid, "name": f"Artist {aid}"} for aid in contributor_ids
],
"cover_xl": None,
}

def mock_album(
self,
requests_mock,
album_artist_id,
track_artist_ids,
contributor_ids=None,
):
"""Mock the album and album-tracks endpoints for album id 1.

``contributor_ids`` overrides the album-level contributors, which
otherwise cover every artist involved.
"""
if contributor_ids is None:
contributor_ids = list(
dict.fromkeys([album_artist_id, *track_artist_ids])
)
requests_mock.get(
f"{DeezerPlugin.album_url}1",
json=self.make_album(album_artist_id, contributor_ids),
)
requests_mock.get(
f"{DeezerPlugin.album_url}1/tracks",
json={
"data": [
make_track(aid, i)
for i, aid in enumerate(track_artist_ids, start=1)
]
},
)

def test_compilation_with_non_va_album_artist(self, plugin, requests_mock):
# Album credited to a single "main" artist that performs on only one
# of many tracks: this is a compilation and should be flagged as VA.
self.mock_album(
requests_mock,
album_artist_id=100,
track_artist_ids=[100, 200, 300, 400, 500, 600],
)

album_info = plugin.album_for_id("1")

assert album_info is not None
assert album_info.va is True
assert album_info.artist == "Various Artists"

def test_plurality_artist_album_not_va(self, plugin, requests_mock):
# Album whose main artist performs on 2 of 5 tracks (40%) is above
# the importer's single-artist threshold and stays single-artist.
self.mock_album(
requests_mock,
album_artist_id=100,
track_artist_ids=[100, 100, 200, 300, 400],
contributor_ids=[100],
)

album_info = plugin.album_for_id("1")

assert album_info is not None
assert album_info.va is False
assert album_info.artist == "Artist 100"

def test_single_artist_album_not_va(self, plugin, requests_mock):
# Album whose main artist performs on every track is not a compilation.
self.mock_album(
requests_mock,
album_artist_id=100,
track_artist_ids=[100, 100, 100, 100],
)

album_info = plugin.album_for_id("1")

assert album_info is not None
assert album_info.va is False
assert album_info.artist == "Artist 100"
Loading