diff --git a/rose/cache.py b/rose/cache.py index 1a13829..aab9038 100644 --- a/rose/cache.py +++ b/rose/cache.py @@ -1824,7 +1824,7 @@ def list_releases_delete_this( args: list[str | bool] = [] if artist_filter: artists: list[str] = [artist_filter] - for alias in c.artist_aliases_map.get(artist_filter, []): + for alias in _get_all_artist_aliases(c, artist_filter): artists.append(alias) query += f""" AND EXISTS ( @@ -2234,7 +2234,7 @@ def list_artists(c: Config) -> list[str]: def artist_exists(c: Config, artist: str) -> bool: args: list[str] = [artist] - for alias in c.artist_aliases_map.get(artist, []): + for alias in _get_all_artist_aliases(c, artist): args.append(alias) with connect(c) as conn: cursor = conn.execute( @@ -2301,18 +2301,39 @@ def _unpack_artists( aliases: bool = True, ) -> ArtistMapping: mapping = ArtistMapping() + seen: set[tuple[str, str]] = set() for name, role in _unpack(names, roles): role_artists: list[Artist] = getattr(mapping, role) role_artists.append(Artist(name=name, alias=False)) - seen: set[str] = {name} - if aliases: - for alias in c.artist_aliases_parents_map.get(name, []): - if alias not in seen: + seen.add((name, role)) + if not aliases: + continue + + # Get all immediate and transitive artist aliases. + unvisited: set[str] = {name} + while unvisited: + cur = unvisited.pop() + for alias in c.artist_aliases_parents_map.get(cur, []): + if (alias, role) not in seen: role_artists.append(Artist(name=alias, alias=True)) - seen.add(alias) + seen.add((alias, role)) + unvisited.add(alias) return mapping +def _get_all_artist_aliases(c: Config, x: str) -> list[str]: + """Includes transitive aliases.""" + aliases: set[str] = set() + unvisited: set[str] = {x} + while unvisited: + cur = unvisited.pop() + if cur in aliases: + continue + aliases.add(cur) + unvisited.update(c.artist_aliases_map.get(cur, [])) + return list(aliases) + + def _get_parent_genres(genres: list[str]) -> list[str]: rval: set[str] = set() for g in genres: diff --git a/rose/cache_test.py b/rose/cache_test.py index 7a2eb50..1084488 100644 --- a/rose/cache_test.py +++ b/rose/cache_test.py @@ -1252,18 +1252,28 @@ def test_get_release_and_associated_tracks(config: Config) -> None: def test_get_release_applies_artist_aliases(config: Config) -> None: config = dataclasses.replace( config, - artist_aliases_map={"Hype Boy": ["Bass Man"]}, - artist_aliases_parents_map={"Bass Man": ["Hype Boy"]}, + artist_aliases_map={"Hype Boy": ["Bass Man"], "Bubble Gum": ["Hype Boy"]}, + artist_aliases_parents_map={"Bass Man": ["Hype Boy"], "Hype Boy": ["Bubble Gum"]}, ) release = get_release(config, "r1") assert release is not None assert release.releaseartists == ArtistMapping( - main=[Artist("Techno Man"), Artist("Bass Man"), Artist("Hype Boy", True)], + main=[ + Artist("Techno Man"), + Artist("Bass Man"), + Artist("Hype Boy", True), + Artist("Bubble Gum", True), + ], ) tracks = get_tracks_associated_with_release(config, release) for t in tracks: assert t.trackartists == ArtistMapping( - main=[Artist("Techno Man"), Artist("Bass Man"), Artist("Hype Boy", True)], + main=[ + Artist("Techno Man"), + Artist("Bass Man"), + Artist("Hype Boy", True), + Artist("Bubble Gum", True), + ], ) @@ -1805,6 +1815,16 @@ def test_artist_exists_with_alias(config: Config) -> None: assert artist_exists(config, "Hype Boy") +@pytest.mark.usefixtures("seeded_cache") +def test_artist_exists_with_alias_transient(config: Config) -> None: + config = dataclasses.replace( + config, + artist_aliases_map={"Hype Boy": ["Bass Man"], "Bubble Gum": ["Hype Boy"]}, + artist_aliases_parents_map={"Bass Man": ["Hype Boy"], "Hype Boy": ["Bubble Gum"]}, + ) + assert artist_exists(config, "Bubble Gum") + + @pytest.mark.usefixtures("seeded_cache") def test_genre_exists(config: Config) -> None: assert genre_exists(config, "Deep House")