diff --git a/rose/cache/read.py b/rose/cache/read.py index f80f619..0b5809c 100644 --- a/rose/cache/read.py +++ b/rose/cache/read.py @@ -60,7 +60,28 @@ def list_albums(c: Config) -> Iterator[CachedRelease]: release_type=row["release_type"], release_year=row["release_year"], new=bool(row["new"]), - genres=sorted(row["genres"].split(r" \\ ")), - labels=sorted(row["labels"].split(r" \\ ")), + genres=row["genres"].split(r" \\ "), + labels=row["labels"].split(r" \\ "), artists=artists, ) + + +def list_artists(c: Config) -> Iterator[str]: + with connect(c) as conn: + cursor = conn.execute("SELECT DISTINCT artist FROM releases_artists") + for row in cursor: + yield row["artist"] + + +def list_genres(c: Config) -> Iterator[str]: + with connect(c) as conn: + cursor = conn.execute("SELECT DISTINCT genre FROM releases_genres") + for row in cursor: + yield row["genre"] + + +def list_labels(c: Config) -> Iterator[str]: + with connect(c) as conn: + cursor = conn.execute("SELECT DISTINCT label FROM releases_labels") + for row in cursor: + yield row["label"] diff --git a/rose/cache/read_test.py b/rose/cache/read_test.py index 29f9438..52904f5 100644 --- a/rose/cache/read_test.py +++ b/rose/cache/read_test.py @@ -2,7 +2,7 @@ from rose.cache.database import connect from rose.cache.dataclasses import CachedArtist, CachedRelease -from rose.cache.read import list_albums +from rose.cache.read import list_albums, list_artists, list_genres, list_labels from rose.foundation.conf import Config @@ -10,9 +10,9 @@ def seed_data(c: Config) -> None: with connect(c) as conn: conn.executescript( """\ -INSERT INTO releases (id, source_path, title, release_type, release_year, new) -VALUES ('r1', '/tmp/r1', 'Release 1', 'album', 2023, true) - , ('r2', '/tmp/r2', 'Release 2', 'album', 2021, false); +INSERT INTO releases (id, source_path, virtual_dirname, title, release_type, release_year, new) +VALUES ('r1', '/tmp/r1', 'r1', 'Release 1', 'album', 2023, true) + , ('r2', '/tmp/r2', 'r2', 'Release 2', 'album', 2021, false); INSERT INTO releases_genres (release_id, genre) VALUES ('r1', 'Techno') @@ -23,10 +23,11 @@ def seed_data(c: Config) -> None: VALUES ('r1', 'Silk Music') , ('r2', 'Native State'); -INSERT INTO tracks (id, source_path, title, release_id, track_number, disc_number, duration_seconds) -VALUES ('t1', '/tmp/r1/01.m4a', 'Track 1', 'r1', '01', '01', 120) - , ('t2', '/tmp/r1/02.m4a', 'Track 2', 'r1', '02', '01', 240) - , ('t3', '/tmp/r2/01.m4a', 'Track 1', 'r2', '01', '01', 120); +INSERT INTO tracks +(id, source_path, virtual_filename, title, release_id, track_number, disc_number, duration_seconds) +VALUES ('t1', '/tmp/r1/01.m4a', '01.m4a', 'Track 1', 'r1', '01', '01', 120) + , ('t2', '/tmp/r1/02.m4a', '02.m4a', 'Track 2', 'r1', '02', '01', 240) + , ('t3', '/tmp/r2/01.m4a', '01.m4a', 'Track 1', 'r2', '01', '01', 120); INSERT INTO releases_artists (release_id, artist, role) VALUES ('r1', 'Techno Man', 'main') @@ -52,6 +53,7 @@ def test_list_albums(config: Config) -> None: CachedRelease( id="r1", source_path=Path("/tmp/r1"), + virtual_dirname="r1", title="Release 1", release_type="album", release_year=2023, @@ -66,6 +68,7 @@ def test_list_albums(config: Config) -> None: CachedRelease( id="r2", source_path=Path("/tmp/r2"), + virtual_dirname="r2", title="Release 2", release_type="album", release_year=2021, @@ -78,3 +81,21 @@ def test_list_albums(config: Config) -> None: ], ), ] + + +def test_list_artists(config: Config) -> None: + seed_data(config) + artists = list(list_artists(config)) + assert set(artists) == {"Techno Man", "Bass Man", "Violin Woman", "Conductor Woman"} + + +def test_list_genres(config: Config) -> None: + seed_data(config) + genres = list(list_genres(config)) + assert set(genres) == {"Techno", "Deep House", "Classical"} + + +def test_list_labels(config: Config) -> None: + seed_data(config) + labels = list(list_labels(config)) + assert set(labels) == {"Silk Music", "Native State"} diff --git a/rose/cache/update.py b/rose/cache/update.py index 089bb76..a949e45 100644 --- a/rose/cache/update.py +++ b/rose/cache/update.py @@ -10,6 +10,7 @@ from rose.cache.dataclasses import CachedArtist, CachedRelease, CachedTrack from rose.foundation.conf import Config from rose.tagger import ArtistTags, AudioFile +from rose.virtualfs.sanitize import sanitize_filename logger = logging.getLogger(__name__) @@ -37,8 +38,6 @@ ID_REGEX = re.compile(r"\{id=([^\}]+)\}$") -ILLEGAL_FS_CHARS_REGEX = re.compile(r'[:\?<>\\*\|"\/]') - def update_cache_for_all_releases(c: Config) -> None: """ @@ -102,7 +101,7 @@ def update_cache_for_release(c: Config, release_dir: Path) -> Path: virtual_dirname += " [" + ";".join(tags.genre) + "]" if tags.label: virtual_dirname += " {" + ";".join(tags.label) + "}" - virtual_dirname = ILLEGAL_FS_CHARS_REGEX.sub("_", virtual_dirname) + virtual_dirname = sanitize_filename(virtual_dirname) release = CachedRelease( id=release_id, @@ -198,7 +197,7 @@ def update_cache_for_release(c: Config, release_dir: Path) -> Path: virtual_filename += f" [{tags.duration_sec // 60}:{tags.duration_sec % 60:02d}]" if tags.artists != tags.album_artists: virtual_filename += " (by " + _format_artists(tags.artists) + ")" - virtual_filename = ILLEGAL_FS_CHARS_REGEX.sub("_", virtual_filename) + virtual_filename = sanitize_filename(virtual_filename) track = CachedTrack( id=track_id, diff --git a/rose/virtualfs/__init__.py b/rose/virtualfs/__init__.py index 8cd5395..050fa04 100644 --- a/rose/virtualfs/__init__.py +++ b/rose/virtualfs/__init__.py @@ -8,8 +8,9 @@ import fuse -from rose.cache.read import list_albums +from rose.cache.read import list_albums, list_artists, list_genres, list_labels from rose.foundation.conf import Config +from rose.virtualfs.sanitize import sanitize_filename logger = logging.getLogger(__name__) @@ -85,16 +86,25 @@ def readdir(self, path: str, _: Any) -> Iterator[fuse.Direntry]: if path.startswith("/artists"): if path == "/artists": yield from [fuse.Direntry("."), fuse.Direntry("..")] + for artist in list_artists(self.config): + yield fuse.Direntry(sanitize_filename(artist)) + return return if path.startswith("/genres"): if path == "/genres": yield from [fuse.Direntry("."), fuse.Direntry("..")] + for genre in list_genres(self.config): + yield fuse.Direntry(sanitize_filename(genre)) + return return if path.startswith("/labels"): if path == "/labels": yield from [fuse.Direntry("."), fuse.Direntry("..")] + for label in list_labels(self.config): + yield fuse.Direntry(sanitize_filename(label)) + return return diff --git a/rose/virtualfs/sanitize.py b/rose/virtualfs/sanitize.py new file mode 100644 index 0000000..a0b61b0 --- /dev/null +++ b/rose/virtualfs/sanitize.py @@ -0,0 +1,7 @@ +import re + +ILLEGAL_FS_CHARS_REGEX = re.compile(r'[:\?<>\\*\|"\/]') + + +def sanitize_filename(x: str) -> str: + return ILLEGAL_FS_CHARS_REGEX.sub("_", x)