diff --git a/rose/cache/read.py b/rose/cache/read.py index 9060570..94e4879 100644 --- a/rose/cache/read.py +++ b/rose/cache/read.py @@ -6,7 +6,7 @@ from rose.foundation.conf import Config -def list_albums(c: Config) -> Iterator[CachedRelease]: +def list_releases(c: Config) -> Iterator[CachedRelease]: with connect(c) as conn: cursor = conn.execute( r""" diff --git a/rose/cache/read_test.py b/rose/cache/read_test.py index 52904f5..08a5799 100644 --- a/rose/cache/read_test.py +++ b/rose/cache/read_test.py @@ -2,7 +2,16 @@ from rose.cache.database import connect from rose.cache.dataclasses import CachedArtist, CachedRelease -from rose.cache.read import list_albums, list_artists, list_genres, list_labels +from rose.cache.read import ( + artist_exists, + genre_exists, + get_release, + label_exists, + list_artists, + list_genres, + list_labels, + list_releases, +) from rose.foundation.conf import Config @@ -14,14 +23,14 @@ def seed_data(c: Config) -> None: 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') - , ('r1', 'Deep House') - , ('r2', 'Classical'); +INSERT INTO releases_genres (release_id, genre, genre_sanitized) +VALUES ('r1', 'Techno', 'Techno') + , ('r1', 'Deep House', 'Deep House') + , ('r2', 'Classical', 'Classical'); -INSERT INTO releases_labels (release_id, label) -VALUES ('r1', 'Silk Music') - , ('r2', 'Native State'); +INSERT INTO releases_labels (release_id, label, label_sanitized) +VALUES ('r1', 'Silk Music', 'Silk Music') + , ('r2', 'Native State', 'Native State'); INSERT INTO tracks (id, source_path, virtual_filename, title, release_id, track_number, disc_number, duration_seconds) @@ -29,26 +38,26 @@ def seed_data(c: Config) -> None: , ('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') - , ('r1', 'Bass Man', 'main') - , ('r2', 'Violin Woman', 'main') - , ('r2', 'Conductor Woman', 'guest'); - -INSERT INTO tracks_artists (track_id, artist, role) -VALUES ('t1', 'Techno Man', 'main') - , ('t1', 'Bass Man', 'main') - , ('t2', 'Techno Man', 'main') - , ('t2', 'Bass Man', 'main') - , ('t3', 'Violin Woman', 'main') - , ('t3', 'Conductor Woman', 'guest'); +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'); + +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'); """ ) -def test_list_albums(config: Config) -> None: +def test_list_releases(config: Config) -> None: seed_data(config) - albums = list(list_albums(config)) + albums = list(list_releases(config)) assert albums == [ CachedRelease( id="r1", @@ -99,3 +108,41 @@ def test_list_labels(config: Config) -> None: seed_data(config) labels = list(list_labels(config)) assert set(labels) == {"Silk Music", "Native State"} + + +def test_get_release(config: Config) -> None: + seed_data(config) + release = get_release(config, "r1") + assert release == CachedRelease( + id="r1", + source_path=Path("/tmp/r1"), + virtual_dirname="r1", + title="Release 1", + release_type="album", + release_year=2023, + new=True, + genres=["Deep House", "Techno"], + labels=["Silk Music"], + artists=[ + CachedArtist(name="Techno Man", role="main"), + CachedArtist(name="Bass Man", role="main"), + ], + ) + + +def test_artist_exists(config: Config) -> None: + seed_data(config) + assert artist_exists(config, "Bass Man") + assert not artist_exists(config, "lalala") + + +def test_genre_exists(config: Config) -> None: + seed_data(config) + assert genre_exists(config, "Deep House") + assert not genre_exists(config, "lalala") + + +def test_label_exists(config: Config) -> None: + seed_data(config) + assert label_exists(config, "Silk Music") + assert not label_exists(config, "Cotton Music") diff --git a/rose/cache/update.py b/rose/cache/update.py index 05390a7..2ba11ba 100644 --- a/rose/cache/update.py +++ b/rose/cache/update.py @@ -87,6 +87,7 @@ def update_cache_for_release(c: Config, release_dir: Path) -> Path: if release is None: logger.debug("Upserting release from first track's tags") + # Compute the album's visual directory name. virtual_dirname = _format_artists(tags.album_artists) + " - " if tags.year: virtual_dirname += str(tags.year) + ". " @@ -102,6 +103,23 @@ def update_cache_for_release(c: Config, release_dir: Path) -> Path: if tags.label: virtual_dirname += " {" + ";".join(tags.label) + "}" virtual_dirname = sanitize_filename(virtual_dirname) + # And in case of a name collision, add an extra number at the end. Iterate to find + # the first unused number. + original_virtual_dirname = virtual_dirname + collision_no = 1 + while True: + collision_no += 1 + cursor = conn.execute( + """ + SELECT EXISTS( + SELECT * FROM releases WHERE virtual_dirname = ? AND id <> ? + ) + """, + (virtual_dirname, release_id), + ) + if not cursor.fetchone()[0]: + break + virtual_dirname = f"{original_virtual_dirname} [{collision_no}]" release = CachedRelease( id=release_id, @@ -200,6 +218,23 @@ def update_cache_for_release(c: Config, release_dir: Path) -> Path: if tags.artists != tags.album_artists: virtual_filename += " (by " + _format_artists(tags.artists) + ")" virtual_filename = sanitize_filename(virtual_filename) + # And in case of a name collision, add an extra number at the end. Iterate to find + # the first unused number. + original_virtual_filename = virtual_filename + collision_no = 1 + while True: + collision_no += 1 + cursor = conn.execute( + """ + SELECT EXISTS( + SELECT * FROM tracks WHERE virtual_filename = ? AND id <> ? + ) + """, + (virtual_filename, track_id), + ) + if not cursor.fetchone()[0]: + break + virtual_filename = f"{original_virtual_filename} [{collision_no}]" track = CachedTrack( id=track_id, diff --git a/rose/cache/update_test.py b/rose/cache/update_test.py index b0edde7..3c2cef3 100644 --- a/rose/cache/update_test.py +++ b/rose/cache/update_test.py @@ -141,8 +141,8 @@ def test_update_cache_for_all_releases(config: Config) -> None: with connect(config) as conn: conn.execute( """ - INSERT INTO releases (id, source_path, title, release_type) - VALUES ('aaaaaa', '/nonexistent', 'aa', 'unknown') + INSERT INTO releases (id, source_path, virtual_dirname, title, release_type) + VALUES ('aaaaaa', '/nonexistent', 'nonexistent', 'aa', 'unknown') """ ) diff --git a/rose/virtualfs/__init__.py b/rose/virtualfs/__init__.py index 8386b93..5feccba 100644 --- a/rose/virtualfs/__init__.py +++ b/rose/virtualfs/__init__.py @@ -13,10 +13,10 @@ genre_exists, get_release, label_exists, - list_albums, list_artists, list_genres, list_labels, + list_releases, ) from rose.foundation.conf import Config from rose.virtualfs.sanitize import sanitize_filename @@ -99,7 +99,7 @@ def readdir(self, path: str, _: Any) -> Iterator[fuse.Direntry]: if path.startswith("/albums"): if path == "/albums": yield from [fuse.Direntry("."), fuse.Direntry("..")] - for album in list_albums(self.config): + for album in list_releases(self.config): yield fuse.Direntry(album.virtual_dirname) return return