diff --git a/migrations/20231009_01_qlEHa-bootstrap.sql b/migrations/20231009_01_qlEHa-bootstrap.sql index 7de36f8..0fa0789 100644 --- a/migrations/20231009_01_qlEHa-bootstrap.sql +++ b/migrations/20231009_01_qlEHa-bootstrap.sql @@ -30,16 +30,20 @@ CREATE INDEX releases_release_year ON releases(release_year); CREATE TABLE releases_genres ( release_id TEXT, genre TEXT, + genre_sanitized TEXT NOT NULL, PRIMARY KEY (release_id, genre) ); CREATE INDEX releases_genres_genre ON releases_genres(genre); +CREATE INDEX releases_genres_genre_sanitized ON releases_genres(genre_sanitized); CREATE TABLE releases_labels ( release_id TEXT, label TEXT, + label_sanitized TEXT NOT NULL, PRIMARY KEY (release_id, label) ); CREATE INDEX releases_labels_label ON releases_labels(label); +CREATE INDEX releases_labels_label_sanitized ON releases_labels(label_sanitized); CREATE TABLE tracks ( id TEXT PRIMARY KEY, @@ -68,20 +72,24 @@ INSERT INTO artist_role_enum (value) VALUES CREATE TABLE releases_artists ( release_id TEXT REFERENCES releases(id) ON DELETE CASCADE, artist TEXT, - role TEXT REFERENCES artist_role_enum(value), + artist_sanitized TEXT NOT NULL, + role TEXT REFERENCES artist_role_enum(value) NOT NULL, PRIMARY KEY (release_id, artist) ); CREATE INDEX releases_artists_release_id ON releases_artists(release_id); CREATE INDEX releases_artists_artist ON releases_artists(artist); +CREATE INDEX releases_artists_artist_sanitized ON releases_artists(artist_sanitized); CREATE TABLE tracks_artists ( track_id TEXT REFERENCES tracks(id) ON DELETE CASCADE, artist TEXT, - role TEXT REFERENCES artist_role_enum(value), + artist_sanitized TEXT NOT NULL, + role TEXT REFERENCES artist_role_enum(value) NOT NULL, PRIMARY KEY (track_id, artist) ); CREATE INDEX tracks_artists_track_id ON tracks_artists(track_id); CREATE INDEX tracks_artists_artist ON tracks_artists(artist); +CREATE INDEX tracks_artists_artist_sanitized ON tracks_artists(artist_sanitized); CREATE TABLE collections ( id TEXT PRIMARY KEY, diff --git a/rose/cache/read.py b/rose/cache/read.py index 0b5809c..9060570 100644 --- a/rose/cache/read.py +++ b/rose/cache/read.py @@ -85,3 +85,94 @@ def list_labels(c: Config) -> Iterator[str]: cursor = conn.execute("SELECT DISTINCT label FROM releases_labels") for row in cursor: yield row["label"] + + +def get_release(c: Config, virtual_dirname: str) -> CachedRelease | None: + with connect(c) as conn: + cursor = conn.execute( + r""" + WITH genres AS ( + SELECT + release_id, + GROUP_CONCAT(genre, ' \\ ') AS genres + FROM releases_genres + GROUP BY release_id + ), labels AS ( + SELECT + release_id, + GROUP_CONCAT(label, ' \\ ') AS labels + FROM releases_labels + GROUP BY release_id + ), artists AS ( + SELECT + release_id, + GROUP_CONCAT(artist, ' \\ ') AS names, + GROUP_CONCAT(role, ' \\ ') AS roles + FROM releases_artists + GROUP BY release_id + ) + SELECT + r.id + , r.source_path + , r.virtual_dirname + , r.title + , r.release_type + , r.release_year + , r.new + , COALESCE(g.genres, '') AS genres + , COALESCE(l.labels, '') AS labels + , COALESCE(a.names, '') AS artist_names + , COALESCE(a.roles, '') AS artist_roles + FROM releases r + LEFT JOIN genres g ON g.release_id = r.id + LEFT JOIN labels l ON l.release_id = r.id + LEFT JOIN artists a ON a.release_id = r.id + WHERE r.virtual_dirname = ? + """, + (virtual_dirname,), + ) + row = cursor.fetchone() + if not row: + return None + artists: list[CachedArtist] = [] + for n, r in zip(row["artist_names"].split(r" \\ "), row["artist_roles"].split(r" \\ ")): + artists.append(CachedArtist(name=n, role=r)) + return CachedRelease( + id=row["id"], + source_path=Path(row["source_path"]), + virtual_dirname=row["virtual_dirname"], + title=row["title"], + release_type=row["release_type"], + release_year=row["release_year"], + new=bool(row["new"]), + genres=row["genres"].split(r" \\ "), + labels=row["labels"].split(r" \\ "), + artists=artists, + ) + + +def artist_exists(c: Config, artist_sanitized: str) -> bool: + with connect(c) as conn: + cursor = conn.execute( + "SELECT EXISTS(SELECT * FROM releases_artists WHERE artist_sanitized = ?)", + (artist_sanitized,), + ) + return bool(cursor.fetchone()[0]) + + +def genre_exists(c: Config, genre_sanitized: str) -> bool: + with connect(c) as conn: + cursor = conn.execute( + "SELECT EXISTS(SELECT * FROM releases_genres WHERE genre_sanitized = ?)", + (genre_sanitized,), + ) + return bool(cursor.fetchone()[0]) + + +def label_exists(c: Config, label_sanitized: str) -> bool: + with connect(c) as conn: + cursor = conn.execute( + "SELECT EXISTS(SELECT * FROM releases_labels WHERE label_sanitized = ?)", + (label_sanitized,), + ) + return bool(cursor.fetchone()[0]) diff --git a/rose/cache/update.py b/rose/cache/update.py index a949e45..05390a7 100644 --- a/rose/cache/update.py +++ b/rose/cache/update.py @@ -156,27 +156,29 @@ def update_cache_for_release(c: Config, release_dir: Path) -> Path: for genre in release.genres: conn.execute( """ - INSERT INTO releases_genres (release_id, genre) VALUES (?, ?) + INSERT INTO releases_genres (release_id, genre, genre_sanitized) + VALUES (?, ?, ?) ON CONFLICT (release_id, genre) DO NOTHING """, - (release.id, genre), + (release.id, genre, sanitize_filename(genre)), ) for label in release.labels: conn.execute( """ - INSERT INTO releases_labels (release_id, label) VALUES (?, ?) + INSERT INTO releases_labels (release_id, label, label_sanitized) + VALUES (?, ?, ?) ON CONFLICT (release_id, label) DO NOTHING """, - (release.id, label), + (release.id, label, sanitize_filename(label)), ) for art in release.artists: conn.execute( """ - INSERT INTO releases_artists (release_id, artist, role) - VALUES (?, ?, ?) + INSERT INTO releases_artists (release_id, artist, artist_sanitized, role) + VALUES (?, ?, ?, ?) ON CONFLICT (release_id, artist) DO UPDATE SET role = ? """, - (release.id, art.name, art.role, art.role), + (release.id, art.name, sanitize_filename(art.name), art.role, art.role), ) # Now process the track. Release is guaranteed to exist here. @@ -249,10 +251,11 @@ def update_cache_for_release(c: Config, release_dir: Path) -> Path: for art in track.artists: conn.execute( """ - INSERT INTO tracks_artists (track_id, artist, role) VALUES (?, ?, ?) + INSERT INTO tracks_artists (track_id, artist, artist_sanitized, role) + VALUES (?, ?, ?, ?) ON CONFLICT (track_id, artist) DO UPDATE SET role = ? """, - (track.id, art.name, art.role, art.role), + (track.id, art.name, sanitize_filename(art.name), art.role, art.role), ) return release_dir diff --git a/rose/virtualfs/__init__.py b/rose/virtualfs/__init__.py index 050fa04..8386b93 100644 --- a/rose/virtualfs/__init__.py +++ b/rose/virtualfs/__init__.py @@ -8,7 +8,16 @@ import fuse -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_albums, + list_artists, + list_genres, + list_labels, +) from rose.foundation.conf import Config from rose.virtualfs.sanitize import sanitize_filename @@ -30,23 +39,35 @@ def get_mode_type(path: str) -> Literal["dir", "file", "missing"]: return "dir" if path.startswith("/albums"): - if path == "/albums": + if path.count("/") == 1: return "dir" - return "dir" + if path.count("/") == 2: + release = get_release(self.config, path.split("/")[2]) + return "dir" if release else "missing" + return "missing" if path.startswith("/artists"): - if path == "/artists": + if path.count("/") == 1: return "dir" + if path.count("/") == 2: + exists = artist_exists(self.config, path.split("/")[2]) + return "dir" if exists else "missing" return "missing" if path.startswith("/genres"): - if path == "/genres": + if path.count("/") == 1: return "dir" + if path.count("/") == 2: + exists = genre_exists(self.config, path.split("/")[2]) + return "dir" if exists else "missing" return "missing" if path.startswith("/labels"): - if path == "/labels": + if path.count("/") == 1: return "dir" + if path.count("/") == 2: + exists = label_exists(self.config, path.split("/")[2]) + return "dir" if exists else "missing" return "missing" return "missing"