Skip to content

Commit

Permalink
add subartists / artist aliases
Browse files Browse the repository at this point in the history
  • Loading branch information
azuline committed Oct 24, 2023
1 parent 133c196 commit ce32b1b
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 19 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ fuse_mount_dir = "~/music"
# The directory to write the cache to. Defaults to
# `${XDG_CACHE_HOME:-$HOME/.cache}/rose`.
cache_dir = "~/.cache/rose"
# Artist aliases: Releases belonging to an alias will also appear in the main
# artists' releases.
artist_aliases = [
["main_artist", ["alias1", "alias2"]],
["Abakus", ["Cinnamon Chasers"]],
["tripleS", ["EVOLution", "LOVElution", "+(KR)ystal Eyes", "Acid Angel From Asia", "Acid Eyes"]],
]
# Artists, genres, and labels to hide from the virtual filesystem navigation.
fuse_hide_artists = [ "xxx" ]
fuse_hide_genres = [ "xxx" ]
Expand Down Expand Up @@ -218,6 +225,10 @@ name ::= string ';' name | string

TODO

## Artist Aliases

TODO

## The Read Cache

For performance, Rosé stores a copy of every source file's metadata in a SQLite
Expand Down
26 changes: 14 additions & 12 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ def config(isolated_dir: Path) -> Config:
fuse_mount_dir=mount_dir,
cache_dir=cache_dir,
cache_database_path=cache_database_path,
artist_aliases_map={},
artist_aliases_parents_map={},
fuse_hide_artists=[],
fuse_hide_genres=[],
fuse_hide_labels=[],
Expand Down Expand Up @@ -109,20 +111,20 @@ def seeded_cache(config: Config) -> None:
, ('t4', '{musicpaths[3]}', '999' , '01.m4a' , 'Track 1', 'r3' , '01' , '01' , 120 , '');
INSERT INTO releases_artists
(release_id, artist , artist_sanitized , role)
VALUES ('r1' , 'Techno Man' , 'Techno Man' , 'main')
, ('r1' , 'Bass Man' , 'Bass Man' , 'main')
, ('r2' , 'Violin Woman' , 'Violin Woman' , 'main')
, ('r2' , 'Conductor Woman', 'Conductor Woman', 'guest');
(release_id, artist , artist_sanitized , role , alias)
VALUES ('r1' , 'Techno Man' , 'Techno Man' , 'main' , false)
, ('r1' , 'Bass Man' , 'Bass Man' , 'main' , false)
, ('r2' , 'Violin Woman' , 'Violin Woman' , 'main' , false)
, ('r2' , 'Conductor Woman', 'Conductor Woman', 'guest', false);
INSERT INTO tracks_artists
(track_id, artist , artist_sanitized , role)
VALUES ('t1' , 'Techno Man' , 'Techno Man' , 'main')
, ('t1' , 'Bass Man' , 'Bass Man' , 'main')
, ('t2' , 'Techno Man' , 'Techno Man' , 'main')
, ('t2' , 'Bass Man' , 'Bass Man' , 'main')
, ('t3' , 'Violin Woman' , 'Violin Woman' , 'main')
, ('t3' , 'Conductor Woman', 'Conductor Woman', 'guest');
(track_id, artist , artist_sanitized , role , alias)
VALUES ('t1' , 'Techno Man' , 'Techno Man' , 'main' , false)
, ('t1' , 'Bass Man' , 'Bass Man' , 'main' , false)
, ('t2' , 'Techno Man' , 'Techno Man' , 'main' , false)
, ('t2' , 'Bass Man' , 'Bass Man' , 'main' , false)
, ('t3' , 'Violin Woman' , 'Violin Woman' , 'main' , false)
, ('t3' , 'Conductor Woman', 'Conductor Woman', 'guest', false);
INSERT INTO collages
(name , source_mtime)
Expand Down
41 changes: 35 additions & 6 deletions rose/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ def migrate_database(c: Config) -> None:
class CachedArtist:
name: str
role: str
# Whether this artist is an aliased name of the artist that the release was actually released
# under. We include aliased artists in the cached state because it lets us relate releases from
# the aliased name to the main artist. However, we ignore these artists when computing the
# virtual dirname and tags.
alias: bool = False


@dataclass
Expand Down Expand Up @@ -576,6 +581,11 @@ def update_cache_for_releases(
for role, names in asdict(tags.album_artists).items():
for name in names:
release_artists.append(CachedArtist(name=name, role=role))
# And also make sure we attach any parent aliases for this artist.
for alias in c.artist_aliases_parents_map.get(name, []):
release_artists.append(
CachedArtist(name=alias, role=role, alias=True)
)
if release_artists != release.artists:
logger.debug(f"Release artists change detected for {source_path}, updating")
release.artists = release_artists
Expand Down Expand Up @@ -644,6 +654,9 @@ def update_cache_for_releases(
for role, names in asdict(tags.artists).items():
for name in names:
track.artists.append(CachedArtist(name=name, role=role))
# And also make sure we attach any parent aliases for this artist.
for alias in c.artist_aliases_parents_map.get(name, []):
track.artists.append(CachedArtist(name=alias, role=role, alias=True))
track_ids_to_insert.add(track.id)

# Now calculate whether this release is multidisc, and then assign virtual_filenames for
Expand Down Expand Up @@ -774,10 +787,19 @@ def update_cache_for_releases(
for art in release.artists:
conn.execute(
"""
INSERT INTO releases_artists (release_id, artist, artist_sanitized, role)
VALUES (?, ?, ?, ?) ON CONFLICT (release_id, artist) DO UPDATE SET role = ?
INSERT INTO releases_artists
(release_id, artist, artist_sanitized, role, alias)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (release_id, artist) DO UPDATE SET role = ?
""",
(release.id, art.name, sanitize_filename(art.name), art.role, art.role),
(
release.id,
art.name,
sanitize_filename(art.name),
art.role,
art.alias,
art.role,
),
)

for track in tracks:
Expand Down Expand Up @@ -836,10 +858,17 @@ def update_cache_for_releases(
for art in track.artists:
conn.execute(
"""
INSERT INTO tracks_artists (track_id, artist, artist_sanitized, role)
VALUES (?, ?, ?, ?) ON CONFLICT (track_id, artist) DO UPDATE SET role = ?
INSERT INTO tracks_artists (track_id, artist, artist_sanitized, role, alias)
VALUES (?, ?, ?, ?, ?) ON CONFLICT (track_id, artist) DO UPDATE SET role = ?
""",
(track.id, art.name, sanitize_filename(art.name), art.role, art.role),
(
track.id,
art.name,
sanitize_filename(art.name),
art.role,
art.alias,
art.role,
),
)

logger.debug(f"Release update loop time {time.time() - loop_start=}")
Expand Down
2 changes: 2 additions & 0 deletions rose/cache.sql
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ CREATE TABLE releases_artists (
artist TEXT,
artist_sanitized TEXT NOT NULL,
role TEXT REFERENCES artist_role_enum(value) NOT NULL,
alias BOOL NOT NULL,
PRIMARY KEY (release_id, artist)
);
CREATE INDEX releases_artists_release_id ON releases_artists(release_id);
Expand All @@ -96,6 +97,7 @@ CREATE TABLE tracks_artists (
artist TEXT,
artist_sanitized TEXT NOT NULL,
role TEXT REFERENCES artist_role_enum(value) NOT NULL,
alias BOOL NOT NULL,
PRIMARY KEY (track_id, artist)
);
CREATE INDEX tracks_artists_track_id ON tracks_artists(track_id);
Expand Down
45 changes: 45 additions & 0 deletions rose/cache_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import hashlib
import shutil
from dataclasses import asdict
from pathlib import Path

import pytest
Expand Down Expand Up @@ -327,6 +328,50 @@ def test_update_cache_releases_uncaches_empty_directory(config: Config) -> None:
assert cursor.fetchone()[0] == 0


def test_update_cache_releases_adds_aliased_artist(config: Config) -> None:
"""Test that an artist alias is properly recorded in the read cache."""
config = Config(
**{
**asdict(config),
"artist_aliases_parents_map": {"BLACKPINK": ["HAHA"]},
"artist_aliases_map": {"HAHA": ["BLACKPINK"]},
}
)
release_dir = config.music_source_dir / TEST_RELEASE_1.name
shutil.copytree(TEST_RELEASE_1, release_dir)
update_cache_for_releases(config, [release_dir])

with connect(config) as conn:
cursor = conn.execute(
"SELECT artist, role, alias FROM releases_artists",
)
artists = {(r["artist"], r["role"], bool(r["alias"])) for r in cursor.fetchall()}
assert artists == {
("BLACKPINK", "main", False),
("HAHA", "main", True),
}

for f in release_dir.iterdir():
if f.suffix != ".m4a":
continue

cursor = conn.execute(
"""
SELECT ta.artist, ta.role, ta.alias
FROM tracks_artists ta
JOIN tracks t ON t.id = ta.track_id
WHERE t.source_path = ?
""",
(str(f),),
)
artists = {(r["artist"], r["role"], bool(r["alias"])) for r in cursor.fetchall()}
assert artists == {
("BLACKPINK", "main", False),
("HAHA", "main", True),
("Teddy", "composer", False),
}


def test_update_cache_collages(config: Config) -> None:
shutil.copytree(TEST_RELEASE_2, config.music_source_dir / TEST_RELEASE_2.name)
shutil.copytree(TEST_COLLAGE_1, config.music_source_dir / "!collages")
Expand Down
15 changes: 15 additions & 0 deletions rose/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path

Expand Down Expand Up @@ -29,6 +30,11 @@ class Config:
cache_dir: Path
cache_database_path: Path

# A map from parent artist -> subartists.
artist_aliases_map: dict[str, list[str]]
# A map from subartist -> parent artists.
artist_aliases_parents_map: dict[str, list[str]]

fuse_hide_artists: list[str]
fuse_hide_genres: list[str]
fuse_hide_labels: list[str]
Expand All @@ -47,12 +53,21 @@ def read(cls, config_path_override: Path | None = None) -> Config:
cache_dir = Path(data["cache_dir"]).expanduser()
cache_dir.mkdir(exist_ok=True)

artist_aliases_map: dict[str, list[str]] = defaultdict(list)
artist_aliases_parents_map: dict[str, list[str]] = defaultdict(list)
for parent, subs in data.get("artist_aliases", []):
artist_aliases_map[parent] = subs
for s in subs:
artist_aliases_parents_map[s].append(parent)

try:
return cls(
music_source_dir=Path(data["music_source_dir"]).expanduser(),
fuse_mount_dir=Path(data["fuse_mount_dir"]).expanduser(),
cache_dir=cache_dir,
cache_database_path=cache_dir / "cache.sqlite3",
artist_aliases_map=artist_aliases_map,
artist_aliases_parents_map=artist_aliases_parents_map,
fuse_hide_artists=data.get("fuse_hide_artists", []),
fuse_hide_genres=data.get("fuse_hide_genres", []),
fuse_hide_labels=data.get("fuse_hide_labels", []),
Expand Down
46 changes: 45 additions & 1 deletion rose/config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from rose.config import Config, ConfigNotFoundError, MissingConfigKeyError


def test_config() -> None:
def test_config_minimal() -> None:
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "config.toml"
with path.open("w") as fp:
Expand All @@ -23,6 +23,50 @@ def test_config() -> None:
assert str(c.fuse_mount_dir) == os.environ["HOME"] + "/music"


def test_config_full() -> None:
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "config.toml"
with path.open("w") as fp:
fp.write(
"""\
music_source_dir = "~/.music-src"
fuse_mount_dir = "~/music"
artist_aliases = [
["Abakus", ["Cinnamon Chasers"]],
["tripleS", ["EVOLution", "LOVElution", "+(KR)ystal Eyes", "Acid Angel From Asia", "Acid Eyes"]],
]
fuse_hide_artists = [ "xxx" ]
fuse_hide_genres = [ "yyy" ]
fuse_hide_labels = [ "zzz" ]
""" # noqa: E501
)

c = Config.read(config_path_override=path)
assert str(c.music_source_dir) == os.environ["HOME"] + "/.music-src"
assert str(c.fuse_mount_dir) == os.environ["HOME"] + "/music"
assert c.artist_aliases_map == {
"Abakus": ["Cinnamon Chasers"],
"tripleS": [
"EVOLution",
"LOVElution",
"+(KR)ystal Eyes",
"Acid Angel From Asia",
"Acid Eyes",
],
}
assert c.artist_aliases_parents_map == {
"Cinnamon Chasers": ["Abakus"],
"EVOLution": ["tripleS"],
"LOVElution": ["tripleS"],
"+(KR)ystal Eyes": ["tripleS"],
"Acid Angel From Asia": ["tripleS"],
"Acid Eyes": ["tripleS"],
}
assert c.fuse_hide_artists == ["xxx"]
assert c.fuse_hide_genres == ["yyy"]
assert c.fuse_hide_labels == ["zzz"]


def test_config_not_found() -> None:
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "config.toml"
Expand Down

0 comments on commit ce32b1b

Please sign in to comment.