From 57f7130268e8cb362daaf039bd9b816a059afded Mon Sep 17 00:00:00 2001 From: blissful Date: Sun, 5 May 2024 22:08:41 -0400 Subject: [PATCH] clean up api --- rose/__init__.py | 36 ++++++------ rose/cache.py | 125 ++++++++++++++++------------------------- rose/cache_test.py | 60 +++++--------------- rose/collages.py | 15 +++-- rose/playlists.py | 19 ++++--- rose/releases.py | 22 ++++---- rose/releases_test.py | 6 +- rose/templates.py | 16 +++--- rose/templates_test.py | 22 ++++---- rose_cli/cli.py | 4 +- rose_vfs/virtualfs.py | 70 +++++++++++------------ 11 files changed, 168 insertions(+), 227 deletions(-) diff --git a/rose/__init__.py b/rose/__init__.py index c078611..9fa22e6 100644 --- a/rose/__init__.py +++ b/rose/__init__.py @@ -4,7 +4,6 @@ UnsupportedFiletypeError, ) from rose.cache import ( - STORED_DATA_FILE_REGEX, Collage, DescriptorEntry, GenreEntry, @@ -13,18 +12,16 @@ Release, Track, artist_exists, - calculate_release_logtext, - calculate_track_logtext, - collage_exists, descriptor_exists, genre_exists, get_collage, + get_collage_releases, get_path_of_track_in_playlist, get_playlist, - get_playlist_cover_path, + get_playlist_tracks, get_release, get_track, - get_tracks_associated_with_release, + get_tracks_of_release, label_exists, list_artists, list_collages, @@ -32,8 +29,9 @@ list_genres, list_labels, list_playlists, + make_release_logtext, + make_track_logtext, maybe_invalidate_cache_database, - playlist_exists, update_cache, update_cache_evict_nonexistent_collages, update_cache_evict_nonexistent_playlists, @@ -91,8 +89,8 @@ from rose.templates import ( PathContext, PathTemplate, - eval_release_template, - eval_track_template, + evaluate_release_template, + evaluate_track_template, preview_path_templates, ) from rose.tracks import dump_all_tracks, dump_track, run_actions_on_track @@ -100,7 +98,7 @@ __all__ = [ # Plumbing "initialize_logging", - "VERSION", # TODO: get_version() + "VERSION", # Errors "RoseError", "RoseExpectedError", @@ -108,10 +106,9 @@ # Utilities "sanitize_dirname", "sanitize_filename", - "calculate_release_logtext", # TODO: Rename. - "calculate_track_logtext", # TODO: Rename. - "STORED_DATA_FILE_REGEX", # TODO: Revise: is_release_directory() / is_track_file() - "SUPPORTED_AUDIO_EXTENSIONS", # TODO: is_supported_audio_file() + "make_release_logtext", + "make_track_logtext", + "SUPPORTED_AUDIO_EXTENSIONS", # Configuration "Config", # Cache @@ -136,8 +133,8 @@ # Path Templates "PathContext", "PathTemplate", - "eval_release_template", # TODO: Rename. - "eval_track_template", # TODO: Rename. + "evaluate_release_template", + "evaluate_track_template", "preview_path_templates", # Releases "Release", @@ -155,7 +152,7 @@ "dump_all_tracks", "dump_track", "get_track", - "get_tracks_associated_with_release", # TODO: Rename: `get_tracks_of_release` / `dump_release(with_tracks=tracks)` + "get_tracks_of_release", # Artists "Artist", "ArtistMapping", @@ -176,13 +173,13 @@ # Collages "Collage", "add_release_to_collage", - "collage_exists", "create_collage", "delete_collage", "dump_all_collages", "dump_collage", "edit_collage_in_editor", # TODO: Move editor part to CLI, make this file-submissions. "get_collage", + "get_collage_releases", "list_collages", "remove_release_from_collage", "rename_collage", @@ -190,16 +187,15 @@ "Playlist", "add_track_to_playlist", "list_playlists", - "playlist_exists", "create_playlist", "delete_playlist", "delete_playlist_cover_art", "get_playlist", + "get_playlist_tracks", "dump_all_playlists", "dump_playlist", "edit_playlist_in_editor", # TODO: Move editor part to CLI, make this file-submissions. "get_path_of_track_in_playlist", # TODO: Redesign. - "get_playlist_cover_path", # TODO: Remove. "remove_track_from_playlist", "rename_playlist", "set_playlist_cover_art", diff --git a/rose/cache.py b/rose/cache.py index f688267..2ecae60 100644 --- a/rose/cache.py +++ b/rose/cache.py @@ -62,7 +62,7 @@ ) from rose.config import Config from rose.genre_hierarchy import TRANSIENT_CHILD_GENRES, TRANSIENT_PARENT_GENRES -from rose.templates import artistsfmt, eval_release_template, eval_track_template +from rose.templates import artistsfmt, evaluate_release_template, evaluate_track_template logger = logging.getLogger(__name__) @@ -371,7 +371,6 @@ def cached_track_from_view( class Collage: name: str source_mtime: str - release_ids: list[str] @dataclass(slots=True) @@ -379,7 +378,6 @@ class Playlist: name: str source_mtime: str cover_path: Path | None - track_ids: list[str] @dataclass(slots=True) @@ -953,7 +951,7 @@ def _update_cache_for_releases_executor( # And now perform directory/file renames if configured. if c.rename_source_files: if release_dirty: - wanted_dirname = eval_release_template(c.path_templates.source.release, release) + wanted_dirname = evaluate_release_template(c.path_templates.source.release, release) wanted_dirname = sanitize_dirname(c, wanted_dirname, True) # Iterate until we've either: # 1. Realized that the name of the source path matches the desired dirname (which we @@ -989,7 +987,7 @@ def _update_cache_for_releases_executor( track.source_mtime = str(os.stat(track.source_path).st_mtime) track_ids_to_insert.add(track.id) for track in [t for t in tracks if t.id in track_ids_to_insert]: - wanted_filename = eval_track_template(c.path_templates.source.track, track) + wanted_filename = evaluate_track_template(c.path_templates.source.track, track) wanted_filename = sanitize_filename(c, wanted_filename, True) # And repeat a similar process to the release rename handling. Except: we can have # arbitrarily nested files here, so we need to compare more than the name. @@ -1423,7 +1421,7 @@ def update_cache_for_collages( files.append((path.resolve(), path.stem, f)) logger.debug(f"Refreshing the read cache for {len(files)} collages") - cached_collages: dict[str, Collage] = {} + cached_collages: dict[str, tuple[Collage, list[str]]] = {} with connect(c) as conn: cursor = conn.execute( """ @@ -1437,10 +1435,12 @@ def update_cache_for_collages( """, ) for row in cursor: - cached_collages[row["name"]] = Collage( - name=row["name"], - source_mtime=row["source_mtime"], - release_ids=_split(row["release_ids"]) if row["release_ids"] else [], + cached_collages[row["name"]] = ( + Collage( + name=row["name"], + source_mtime=row["source_mtime"], + ), + _split(row["release_ids"]) if row["release_ids"] else [], ) # We want to validate that all release IDs exist before we write them. In order to do that, @@ -1452,14 +1452,14 @@ def update_cache_for_collages( with connect(c) as conn: for source_path, name, f in files: try: - cached_collage = cached_collages[name] + cached_collage, release_ids = cached_collages[name] except KeyError: logger.debug(f"First-time unidentified collage found at {source_path}") cached_collage = Collage( name=name, source_mtime="", - release_ids=[], ) + release_ids = [] try: source_mtime = str(f.stat().st_mtime) @@ -1495,9 +1495,9 @@ def update_cache_for_collages( ) del rls["missing"] - cached_collage.release_ids = [r["uuid"] for r in releases] + release_ids = [r["uuid"] for r in releases] logger.debug( - f"Found {len(cached_collage.release_ids)} release(s) (including missing) in {source_path}" + f"Found {len(release_ids)} release(s) (including missing) in {source_path}" ) # Update the description_metas. @@ -1507,10 +1507,10 @@ def update_cache_for_collages( SELECT id, releasetitle, releasedate, releaseartist_names, releaseartist_roles FROM releases_view WHERE id IN ({','.join(['?']*len(releases))}) """, - cached_collage.release_ids, + release_ids, ) for row in cursor: - desc_map[row["id"]] = calculate_release_logtext( + desc_map[row["id"]] = make_release_logtext( title=row["releasetitle"], releasedate=RoseDate.parse(row["releasedate"]), artists=_unpack_artists( @@ -1618,7 +1618,7 @@ def update_cache_for_playlists( files.append((path.resolve(), path.stem, f)) logger.debug(f"Refreshing the read cache for {len(files)} playlists") - cached_playlists: dict[str, Playlist] = {} + cached_playlists: dict[str, tuple[Playlist, list[str]]] = {} with connect(c) as conn: cursor = conn.execute( """ @@ -1633,11 +1633,13 @@ def update_cache_for_playlists( """, ) for row in cursor: - cached_playlists[row["name"]] = Playlist( - name=row["name"], - source_mtime=row["source_mtime"], - cover_path=Path(row["cover_path"]) if row["cover_path"] else None, - track_ids=_split(row["track_ids"]) if row["track_ids"] else [], + cached_playlists[row["name"]] = ( + Playlist( + name=row["name"], + source_mtime=row["source_mtime"], + cover_path=Path(row["cover_path"]) if row["cover_path"] else None, + ), + _split(row["track_ids"]) if row["track_ids"] else [], ) # We want to validate that all track IDs exist before we write them. In order to do that, @@ -1649,15 +1651,15 @@ def update_cache_for_playlists( with connect(c) as conn: for source_path, name, f in files: try: - cached_playlist = cached_playlists[name] + cached_playlist, track_ids = cached_playlists[name] except KeyError: logger.debug(f"First-time unidentified playlist found at {source_path}") cached_playlist = Playlist( name=name, source_mtime="", cover_path=None, - track_ids=[], ) + track_ids = [] # We do a quick scan for the playlist's cover art here. We always do this check, as it # amounts to ~4 getattrs. If a change is detected, we ignore the mtime optimization and @@ -1712,9 +1714,9 @@ def update_cache_for_playlists( ) del trk["missing"] - cached_playlist.track_ids = [t["uuid"] for t in tracks] + track_ids = [t["uuid"] for t in tracks] logger.debug( - f"Found {len(cached_playlist.track_ids)} track(s) (including missing) in {source_path}" + f"Found {len(track_ids)} track(s) (including missing) in {source_path}" ) # Update the description_metas. @@ -1732,10 +1734,10 @@ def update_cache_for_playlists( JOIN releases_view r ON r.id = t.release_id WHERE t.id IN ({','.join(['?']*len(tracks))}) """, - cached_playlist.track_ids, + track_ids, ) for row in cursor: - desc_map[row["id"]] = calculate_track_logtext( + desc_map[row["id"]] = make_track_logtext( title=row["tracktitle"], artists=_unpack_artists( c, row["trackartist_names"], row["trackartist_roles"] @@ -1920,14 +1922,14 @@ def get_release_logtext(c: Config, release_id: str) -> str | None: row = cursor.fetchone() if not row: return None - return calculate_release_logtext( + return make_release_logtext( title=row["releasetitle"], releasedate=RoseDate.parse(row["releasedate"]), artists=_unpack_artists(c, row["releaseartist_names"], row["releaseartist_roles"]), ) -def calculate_release_logtext( +def make_release_logtext( title: str, releasedate: RoseDate | None, artists: ArtistMapping, @@ -1981,7 +1983,7 @@ def get_track(c: Config, uuid: str) -> Track | None: return cached_track_from_view(c, trackrow, release) -def get_tracks_associated_with_release( +def get_tracks_of_release( c: Config, release: Release, ) -> list[Track]: @@ -2001,7 +2003,7 @@ def get_tracks_associated_with_release( return rval -def get_tracks_associated_with_releases( +def get_tracks_of_releases( c: Config, releases: list[Release], ) -> list[tuple[Release, list[Track]]]: @@ -2096,7 +2098,7 @@ def get_track_logtext(c: Config, track_id: str) -> str | None: row = cursor.fetchone() if not row: return None - return calculate_track_logtext( + return make_track_logtext( title=row["tracktitle"], artists=_unpack_artists(c, row["trackartist_names"], row["trackartist_roles"]), releasedate=RoseDate.parse(row["releasedate"]), @@ -2104,7 +2106,7 @@ def get_track_logtext(c: Config, track_id: str) -> str | None: ) -def calculate_track_logtext( +def make_track_logtext( title: str, artists: ArtistMapping, releasedate: RoseDate | None, @@ -2123,7 +2125,7 @@ def list_playlists(c: Config) -> list[str]: return [r["name"] for r in cursor] -def get_playlist(c: Config, playlist_name: str) -> tuple[Playlist, list[Track]] | None: +def get_playlist(c: Config, playlist_name: str) -> Playlist | None: with connect(c) as conn: cursor = conn.execute( """ @@ -2139,14 +2141,15 @@ def get_playlist(c: Config, playlist_name: str) -> tuple[Playlist, list[Track]] row = cursor.fetchone() if not row: return None - playlist = Playlist( + return Playlist( name=row["name"], source_mtime=row["source_mtime"], cover_path=Path(row["cover_path"]) if row["cover_path"] else None, - # Accumulated below when we query the tracks. - track_ids=[], ) + +def get_playlist_tracks(c: Config, playlist_name: str) -> list[Track]: + with connect(c) as conn: cursor = conn.execute( """ SELECT t.* @@ -2174,31 +2177,9 @@ def get_playlist(c: Config, playlist_name: str) -> tuple[Playlist, list[Track]] tracks: list[Track] = [] for row in trackrows: - playlist.track_ids.append(row["id"]) tracks.append(cached_track_from_view(c, row, releases_map[row["release_id"]])) - return playlist, tracks - - -def playlist_exists(c: Config, playlist_name: str) -> bool: - with connect(c) as conn: - cursor = conn.execute( - "SELECT EXISTS(SELECT * FROM playlists WHERE name = ?)", - (playlist_name,), - ) - return bool(cursor.fetchone()[0]) - - -def get_playlist_cover_path(c: Config, playlist_name: str) -> Path | None: - with connect(c) as conn: - cursor = conn.execute( - "SELECT cover_path FROM playlists WHERE name = ?", - (playlist_name,), - ) - row = cursor.fetchone() - if row and row["cover_path"]: - return Path(row["cover_path"]) - return None + return tracks def list_collages(c: Config) -> list[str]: @@ -2207,7 +2188,7 @@ def list_collages(c: Config) -> list[str]: return [r["name"] for r in cursor] -def get_collage(c: Config, collage_name: str) -> tuple[Collage, list[Release]] | None: +def get_collage(c: Config, collage_name: str) -> Collage | None: with connect(c) as conn: cursor = conn.execute( "SELECT name, source_mtime FROM collages WHERE name = ?", @@ -2216,12 +2197,14 @@ def get_collage(c: Config, collage_name: str) -> tuple[Collage, list[Release]] | row = cursor.fetchone() if not row: return None - collage = Collage( + return Collage( name=row["name"], source_mtime=row["source_mtime"], - # Accumulated below when we query the releases. - release_ids=[], ) + + +def get_collage_releases(c: Config, collage_name: str) -> list[Release]: + with connect(c) as conn: cursor = conn.execute( """ SELECT r.* @@ -2234,19 +2217,9 @@ def get_collage(c: Config, collage_name: str) -> tuple[Collage, list[Release]] | ) releases: list[Release] = [] for row in cursor: - collage.release_ids.append(row["id"]) releases.append(cached_release_from_view(c, row)) - return (collage, releases) - - -def collage_exists(c: Config, collage_name: str) -> bool: - with connect(c) as conn: - cursor = conn.execute( - "SELECT EXISTS(SELECT * FROM collages WHERE name = ?)", - (collage_name,), - ) - return bool(cursor.fetchone()[0]) + return releases def list_artists(c: Config) -> list[str]: diff --git a/rose/cache_test.py b/rose/cache_test.py index 8d65efa..495ac80 100644 --- a/rose/cache_test.py +++ b/rose/cache_test.py @@ -21,21 +21,21 @@ Track, _unpack, artist_exists, - collage_exists, connect, descriptor_exists, genre_exists, get_collage, + get_collage_releases, get_path_of_track_in_playlist, get_path_of_track_in_release, get_playlist, - get_playlist_cover_path, + get_playlist_tracks, get_release, get_release_logtext, get_track, get_track_logtext, - get_tracks_associated_with_release, - get_tracks_associated_with_releases, + get_tracks_of_release, + get_tracks_of_releases, label_exists, list_artists, list_collages, @@ -47,7 +47,6 @@ list_tracks, lock, maybe_invalidate_cache_database, - playlist_exists, update_cache, update_cache_evict_nonexistent_releases, update_cache_for_releases, @@ -1270,8 +1269,8 @@ def test_get_release_and_associated_tracks(config: Config) -> None: ), ] - assert get_tracks_associated_with_release(config, release) == expected_tracks - assert get_tracks_associated_with_releases(config, [release]) == [(release, expected_tracks)] + assert get_tracks_of_release(config, release) == expected_tracks + assert get_tracks_of_releases(config, [release]) == [(release, expected_tracks)] @pytest.mark.usefixtures("seeded_cache") @@ -1291,7 +1290,7 @@ def test_get_release_applies_artist_aliases(config: Config) -> None: Artist("Bubble Gum", True), ], ) - tracks = get_tracks_associated_with_release(config, release) + tracks = get_tracks_of_release(config, release) for t in tracks: assert t.trackartists == ArtistMapping( main=[ @@ -1627,15 +1626,11 @@ def test_list_collages(config: Config) -> None: @pytest.mark.usefixtures("seeded_cache") def test_get_collage(config: Config) -> None: - cdata = get_collage(config, "Rose Gold") - assert cdata is not None - collage, releases = cdata - assert collage == Collage( + assert get_collage(config, "Rose Gold") == Collage( name="Rose Gold", source_mtime="999", - release_ids=["r1", "r2"], ) - assert releases == [ + assert get_collage_releases(config, "Rose Gold") == [ Release( id="r1", source_path=config.music_source_dir / "r1", @@ -1702,21 +1697,11 @@ def test_get_collage(config: Config) -> None: ), ] - cdata = get_collage(config, "Ruby Red") - assert cdata is not None - collage, releases = cdata - assert collage == Collage( + assert get_collage(config, "Ruby Red") == Collage( name="Ruby Red", source_mtime="999", - release_ids=[], ) - assert releases == [] - - -@pytest.mark.usefixtures("seeded_cache") -def test_collage_exists(config: Config) -> None: - assert collage_exists(config, "Rose Gold") - assert not collage_exists(config, "lalala") + assert get_collage_releases(config, "Ruby Red") == [] @pytest.mark.usefixtures("seeded_cache") @@ -1727,16 +1712,12 @@ def test_list_playlists(config: Config) -> None: @pytest.mark.usefixtures("seeded_cache") def test_get_playlist(config: Config) -> None: - pdata = get_playlist(config, "Lala Lisa") - assert pdata is not None - playlist, tracks = pdata - assert playlist == Playlist( + assert get_playlist(config, "Lala Lisa") == Playlist( name="Lala Lisa", source_mtime="999", cover_path=config.music_source_dir / "!playlists" / "Lala Lisa.jpg", - track_ids=["t1", "t3"], ) - assert tracks == [ + assert get_playlist_tracks(config, "Lala Lisa") == [ Track( id="t1", source_path=config.music_source_dir / "r1" / "01.m4a", @@ -1830,21 +1811,6 @@ def test_get_playlist(config: Config) -> None: ] -@pytest.mark.usefixtures("seeded_cache") -def test_playlist_exists(config: Config) -> None: - assert playlist_exists(config, "Lala Lisa") - assert not playlist_exists(config, "lalala") - - -@pytest.mark.usefixtures("seeded_cache") -def test_get_playlist_cover_path(config: Config) -> None: - assert ( - get_playlist_cover_path(config, "Lala Lisa") - == config.music_source_dir / "!playlists" / "Lala Lisa.jpg" - ) - assert get_playlist_cover_path(config, "lalala") is None - - @pytest.mark.usefixtures("seeded_cache") def test_artist_exists(config: Config) -> None: assert artist_exists(config, "Bass Man") diff --git a/rose/collages.py b/rose/collages.py index d77a019..2b71fdb 100644 --- a/rose/collages.py +++ b/rose/collages.py @@ -15,6 +15,7 @@ from rose.cache import ( collage_lock_name, get_collage, + get_collage_releases, get_release_logtext, list_collages, lock, @@ -145,11 +146,12 @@ def add_release_to_collage( def dump_collage(c: Config, collage_name: str) -> str: - cdata = get_collage(c, collage_name) - if cdata is None: + collage = get_collage(c, collage_name) + if collage is None: raise CollageDoesNotExistError(f"Collage {collage_name} does not exist") + collage_releases = get_collage_releases(c, collage_name) releases: list[dict[str, Any]] = [] - for idx, rls in enumerate(cdata[1]): + for idx, rls in enumerate(collage_releases): releases.append({"position": idx + 1, **rls.dump()}) return json.dumps({"name": collage_name, "releases": releases}) @@ -157,10 +159,11 @@ def dump_collage(c: Config, collage_name: str) -> str: def dump_all_collages(c: Config) -> str: out: list[dict[str, Any]] = [] for name in list_collages(c): - cdata = get_collage(c, name) - assert cdata is not None + collage = get_collage(c, name) + assert collage is not None + collage_releases = get_collage_releases(c, name) releases: list[dict[str, Any]] = [] - for idx, rls in enumerate(cdata[1]): + for idx, rls in enumerate(collage_releases): releases.append({"position": idx + 1, **rls.dump()}) out.append({"name": name, "releases": releases}) return json.dumps(out) diff --git a/rose/playlists.py b/rose/playlists.py index 4fce649..80fa336 100644 --- a/rose/playlists.py +++ b/rose/playlists.py @@ -16,6 +16,7 @@ from rose.cache import ( get_playlist, + get_playlist_tracks, get_track_logtext, list_playlists, lock, @@ -152,16 +153,17 @@ def add_track_to_playlist( def dump_playlist(c: Config, playlist_name: str) -> str: - pdata = get_playlist(c, playlist_name) - if pdata is None: + playlist = get_playlist(c, playlist_name) + if playlist is None: raise PlaylistDoesNotExistError(f"Playlist {playlist_name} does not exist") + playlist_tracks = get_playlist_tracks(c, playlist_name) tracks: list[dict[str, Any]] = [] - for idx, trk in enumerate(pdata[1]): + for idx, trk in enumerate(playlist_tracks): tracks.append({"position": idx + 1, **trk.dump()}) return json.dumps( { "name": playlist_name, - "cover_image_path": str(pdata[0].cover_path) if pdata[0].cover_path else None, + "cover_image_path": str(playlist.cover_path) if playlist.cover_path else None, "tracks": tracks, } ) @@ -170,15 +172,16 @@ def dump_playlist(c: Config, playlist_name: str) -> str: def dump_all_playlists(c: Config) -> str: out: list[dict[str, Any]] = [] for name in list_playlists(c): - pdata = get_playlist(c, name) - assert pdata is not None + playlist = get_playlist(c, name) + assert playlist is not None + playlist_tracks = get_playlist_tracks(c, name) tracks: list[dict[str, Any]] = [] - for idx, trk in enumerate(pdata[1]): + for idx, trk in enumerate(playlist_tracks): tracks.append({"position": idx + 1, **trk.dump()}) out.append( { "name": name, - "cover_image_path": str(pdata[0].cover_path) if pdata[0].cover_path else None, + "cover_image_path": str(playlist.cover_path) if playlist.cover_path else None, "tracks": tracks, } ) diff --git a/rose/releases.py b/rose/releases.py index fccb8f3..338a5f2 100644 --- a/rose/releases.py +++ b/rose/releases.py @@ -23,12 +23,12 @@ STORED_DATA_FILE_REGEX, Release, Track, - calculate_release_logtext, get_release, - get_tracks_associated_with_release, - get_tracks_associated_with_releases, + get_tracks_of_release, + get_tracks_of_releases, list_releases, lock, + make_release_logtext, release_lock_name, update_cache_evict_nonexistent_releases, update_cache_for_collages, @@ -72,7 +72,7 @@ def dump_release(c: Config, release_id: str) -> str: release = get_release(c, release_id) if not release: raise ReleaseDoesNotExistError(f"Release {release_id} does not exist") - tracks = get_tracks_associated_with_release(c, release) + tracks = get_tracks_of_release(c, release) return json.dumps( {**release.dump(), "tracks": [t.dump(with_release_info=False) for t in tracks]} ) @@ -85,7 +85,7 @@ def dump_all_releases(c: Config, matcher: MetadataMatcher | None = None) -> str: releases = list_releases(c, release_ids) if matcher: releases = filter_release_false_positives_using_read_cache(matcher, releases) - rt_pairs = get_tracks_associated_with_releases(c, releases) + rt_pairs = get_tracks_of_releases(c, releases) return json.dumps( [ {**release.dump(), "tracks": [t.dump(with_release_info=False) for t in tracks]} @@ -100,7 +100,7 @@ def delete_release(c: Config, release_id: str) -> None: raise ReleaseDoesNotExistError(f"Release {release_id} does not exist") with lock(c, release_lock_name(release_id)): send2trash(release.source_path) - release_logtext = calculate_release_logtext( + release_logtext = make_release_logtext( title=release.releasetitle, releasedate=release.releasedate, artists=release.releaseartists, @@ -118,7 +118,7 @@ def toggle_release_new(c: Config, release_id: str) -> None: if not release: raise ReleaseDoesNotExistError(f"Release {release_id} does not exist") - release_logtext = calculate_release_logtext( + release_logtext = make_release_logtext( title=release.releasetitle, releasedate=release.releasedate, artists=release.releaseartists, @@ -160,7 +160,7 @@ def set_release_cover_art( if not release: raise ReleaseDoesNotExistError(f"Release {release_id} does not exist") - release_logtext = calculate_release_logtext( + release_logtext = make_release_logtext( title=release.releasetitle, releasedate=release.releasedate, artists=release.releaseartists, @@ -181,7 +181,7 @@ def delete_release_cover_art(c: Config, release_id: str) -> None: if not release: raise ReleaseDoesNotExistError(f"Release {release_id} does not exist") - release_logtext = calculate_release_logtext( + release_logtext = make_release_logtext( title=release.releasetitle, releasedate=release.releasedate, artists=release.releaseartists, @@ -338,7 +338,7 @@ def edit_release( # TODO: Read from tags directly to ensure that we are not writing stale data. with lock(c, release_lock_name(release_id)): assert release is not None - tracks = get_tracks_associated_with_release(c, release) + tracks = get_tracks_of_release(c, release) if resume_file is not None: m = FAILED_RELEASE_EDIT_FILENAME_REGEX.match(resume_file.name) @@ -490,7 +490,7 @@ def run_actions_on_release( release = get_release(c, release_id) if release is None: raise ReleaseDoesNotExistError(f"Release {release_id} does not exist") - tracks = get_tracks_associated_with_release(c, release) + tracks = get_tracks_of_release(c, release) audiotags = [AudioTags.from_file(t.source_path) for t in tracks] execute_metadata_actions(c, actions, audiotags, dry_run=dry_run, confirm_yes=confirm_yes) diff --git a/rose/releases_test.py b/rose/releases_test.py index 046cc82..b90c619 100644 --- a/rose/releases_test.py +++ b/rose/releases_test.py @@ -14,7 +14,7 @@ Track, connect, get_release, - get_tracks_associated_with_release, + get_tracks_of_release, update_cache, ) from rose.common import Artist, ArtistMapping @@ -210,7 +210,7 @@ def test_edit_release(monkeypatch: Any, config: Config, source_dir: Path) -> Non releaseartists=ArtistMapping(main=[Artist("BLACKPINK"), Artist("JISOO")]), metahash=release.metahash, ) - tracks = get_tracks_associated_with_release(config, release) + tracks = get_tracks_of_release(config, release) assert tracks == [ Track( id=track_ids[0], @@ -379,7 +379,7 @@ def editfn(text: str, **_: Any) -> str: releaseartists=ArtistMapping(main=[Artist("BLACKPINK"), Artist("JISOO")]), metahash=release.metahash, ) - tracks = get_tracks_associated_with_release(config, release) + tracks = get_tracks_of_release(config, release) assert tracks == [ Track( id=track_ids[0], diff --git a/rose/templates.py b/rose/templates.py index 80439a6..758135e 100644 --- a/rose/templates.py +++ b/rose/templates.py @@ -281,7 +281,7 @@ class PathContext: playlist: str | None -def eval_release_template( +def evaluate_release_template( template: PathTemplate, release: Release, context: PathContext | None = None, @@ -292,7 +292,7 @@ def eval_release_template( ) -def eval_track_template( +def evaluate_track_template( template: PathTemplate, track: Track, context: PathContext | None = None, @@ -522,11 +522,11 @@ def _preview_release_template(c: Config, label: str, template: PathTemplate) -> kimlip, youngforever, debussy = _get_preview_releases(c) click.secho(f"{label}:", dim=True, underline=True) click.secho(" Sample 1: ", dim=True, nl=False) - click.secho(eval_release_template(template, kimlip, position="1")) + click.secho(evaluate_release_template(template, kimlip, position="1")) click.secho(" Sample 2: ", dim=True, nl=False) - click.secho(eval_release_template(template, youngforever, position="2")) + click.secho(evaluate_release_template(template, youngforever, position="2")) click.secho(" Sample 3: ", dim=True, nl=False) - click.secho(eval_release_template(template, debussy, position="3")) + click.secho(evaluate_release_template(template, debussy, position="3")) def _preview_track_template(c: Config, label: str, template: PathTemplate) -> None: @@ -551,7 +551,7 @@ def _preview_track_template(c: Config, label: str, template: PathTemplate) -> No metahash="0", release=kimlip, ) - click.secho(eval_track_template(template, track, position="1")) + click.secho(evaluate_track_template(template, track, position="1")) click.secho(" Sample 2: ", dim=True, nl=False) track = Track( @@ -569,7 +569,7 @@ def _preview_track_template(c: Config, label: str, template: PathTemplate) -> No metahash="0", release=youngforever, ) - click.secho(eval_track_template(template, track, position="2")) + click.secho(evaluate_track_template(template, track, position="2")) click.secho(" Sample 3: ", dim=True, nl=False) track = Track( @@ -591,4 +591,4 @@ def _preview_track_template(c: Config, label: str, template: PathTemplate) -> No metahash="0", release=debussy, ) - click.secho(eval_track_template(template, track, position="3")) + click.secho(evaluate_track_template(template, track, position="3")) diff --git a/rose/templates_test.py b/rose/templates_test.py index f716c06..d2dd243 100644 --- a/rose/templates_test.py +++ b/rose/templates_test.py @@ -12,8 +12,8 @@ PathTemplate, PathTemplateConfig, _get_preview_releases, - eval_release_template, - eval_track_template, + evaluate_release_template, + evaluate_track_template, preview_path_templates, ) @@ -70,28 +70,28 @@ def test_default_templates() -> None: ) release.releasetype = "single" assert ( - eval_release_template(templates.source.release, release) + evaluate_release_template(templates.source.release, release) == "A1, A2 & A3 (feat. BB) (prod. PP) - 2023. Title - Single" ) assert ( - eval_release_template(templates.collages.release, release, position="4") + evaluate_release_template(templates.collages.release, release, position="4") == "4. A1, A2 & A3 (feat. BB) (prod. PP) - 2023. Title - Single" ) release = deepcopy(EMPTY_CACHED_RELEASE) release.releasetitle = "Title" - assert eval_release_template(templates.source.release, release) == "Unknown Artists - Title" + assert evaluate_release_template(templates.source.release, release) == "Unknown Artists - Title" assert ( - eval_release_template(templates.collages.release, release, position="4") + evaluate_release_template(templates.collages.release, release, position="4") == "4. Unknown Artists - Title" ) track = deepcopy(EMPTY_CACHED_TRACK) track.tracknumber = "2" track.tracktitle = "Trick" - assert eval_track_template(templates.source.track, track) == "02. Trick.m4a" + assert evaluate_track_template(templates.source.track, track) == "02. Trick.m4a" assert ( - eval_track_template(templates.playlists, track, position="4") + evaluate_track_template(templates.playlists, track, position="4") == "4. Unknown Artists - Trick.m4a" ) @@ -105,11 +105,11 @@ def test_default_templates() -> None: guest=[Artist("Hi"), Artist("High"), Artist("Hye")], ) assert ( - eval_track_template(templates.source.track, track) + evaluate_track_template(templates.source.track, track) == "04-02. Trick (feat. Hi, High & Hye).m4a" ) assert ( - eval_track_template(templates.playlists, track, position="4") + evaluate_track_template(templates.playlists, track, position="4") == "4. Main (feat. Hi, High & Hye) - Trick.m4a" ) @@ -238,6 +238,6 @@ def test_classical(config: Config) -> None: _, _, debussy = _get_preview_releases(config) assert ( - eval_release_template(template, debussy) + evaluate_release_template(template, debussy) == "Debussy, Claude - 1907. Images performed by Cleveland Orchestra under Pierre Boulez (1992)" ) diff --git a/rose_cli/cli.py b/rose_cli/cli.py index 8b75f28..a7fc640 100644 --- a/rose_cli/cli.py +++ b/rose_cli/cli.py @@ -6,6 +6,7 @@ import contextlib import logging import os +import re import signal import subprocess import uuid @@ -16,7 +17,6 @@ import click from rose import ( - STORED_DATA_FILE_REGEX, VERSION, AudioTags, Config, @@ -65,6 +65,8 @@ logger = logging.getLogger(__name__) +STORED_DATA_FILE_REGEX = re.compile(r"\.rose\.([^.]+)\.toml") + class CliExpectedError(Exception): pass diff --git a/rose_vfs/virtualfs.py b/rose_vfs/virtualfs.py index 8e701cf..ebb8860 100644 --- a/rose_vfs/virtualfs.py +++ b/rose_vfs/virtualfs.py @@ -43,6 +43,7 @@ import logging import os import random +import re import stat import subprocess import tempfile @@ -55,7 +56,6 @@ import llfuse from rose import ( - STORED_DATA_FILE_REGEX, SUPPORTED_AUDIO_EXTENSIONS, AudioTags, Config, @@ -67,9 +67,6 @@ add_release_to_collage, add_track_to_playlist, artist_exists, - calculate_release_logtext, - calculate_track_logtext, - collage_exists, create_collage, create_playlist, delete_collage, @@ -77,16 +74,15 @@ delete_playlist_cover_art, delete_release, descriptor_exists, - eval_release_template, - eval_track_template, + evaluate_release_template, + evaluate_track_template, genre_exists, get_collage, get_path_of_track_in_playlist, get_playlist, - get_playlist_cover_path, get_release, get_track, - get_tracks_associated_with_release, + get_tracks_of_release, label_exists, list_artists, list_collages, @@ -94,7 +90,8 @@ list_genres, list_labels, list_playlists, - playlist_exists, + make_release_logtext, + make_track_logtext, remove_release_from_collage, remove_track_from_playlist, rename_collage, @@ -105,10 +102,12 @@ set_release_cover_art, update_cache_for_releases, ) -from rose.cache import list_releases_delete_this +from rose.cache import get_collage_releases, get_playlist_tracks, list_releases_delete_this logger = logging.getLogger(__name__) +STORED_DATA_FILE_REGEX = re.compile(r"\.rose\.([^.]+)\.toml") + K = TypeVar("K") V = TypeVar("V") T = TypeVar("T") @@ -443,7 +442,7 @@ def list_release_paths( else: raise RoseError(f"VNAMES: No release template found for {release_parent=}.") - logtext = calculate_release_logtext( + logtext = make_release_logtext( title=release.releasetitle, releasedate=release.releasedate, artists=release.releaseartists, @@ -491,7 +490,7 @@ def list_release_paths( collage=release_parent.collage, playlist=None, ) - vname = eval_release_template(template, release, context, position) + vname = evaluate_release_template(template, release, context, position) vname = sanitize_dirname(self._config, vname, False) self._release_template_eval_cache[cachekey] = vname logger.debug( @@ -556,7 +555,7 @@ def list_track_paths( else: raise RoseError(f"VNAMES: No track template found for {track_parent=}.") - logtext = calculate_track_logtext( + logtext = make_track_logtext( title=track.tracktitle, artists=track.trackartists, releasedate=track.release.releasedate, @@ -601,7 +600,7 @@ def list_track_paths( collage=track_parent.collage, playlist=track_parent.playlist, ) - vname = eval_track_template(template, track, context, position) + vname = evaluate_track_template(template, track, context, position) vname = sanitize_filename(self._config, vname, False) logger.debug( f"VNAMES: Generated virtual filename {vname} for track {logtext} in {time.time() - time_start} seconds" @@ -916,7 +915,7 @@ def _getattr_release(self, p: VirtualPath) -> dict[str, Any]: if p.file == f".rose.{release.id}.toml": return self.stat("file") track_id = self._get_track_id(p) - tracks = get_tracks_associated_with_release(self.config, release) + tracks = get_tracks_of_release(self.config, release) for t in tracks: if t.id == track_id: return self.stat("file", t.source_path) @@ -928,12 +927,12 @@ def getattr(self, p: VirtualPath) -> dict[str, Any]: # 7. Playlists if p.playlist: - if not playlist_exists(self.config, p.playlist): + playlist = get_playlist(self.config, p.playlist) + if not playlist: raise llfuse.FUSEError(errno.ENOENT) if p.file: - cover_path = get_playlist_cover_path(self.config, p.playlist) - if cover_path and f"cover{cover_path.suffix}" == p.file: - return self.stat("file", cover_path) + if playlist.cover_path and f"cover{playlist.cover_path.suffix}" == p.file: + return self.stat("file", playlist.cover_path) track_id = self._get_track_id(p) if source_path := get_path_of_track_in_playlist(self.config, track_id, p.playlist): return self.stat("file", source_path) @@ -942,7 +941,7 @@ def getattr(self, p: VirtualPath) -> dict[str, Any]: # 6. Collages if p.collage: - if not collage_exists(self.config, p.collage): + if not get_collage(self.config, p.collage): raise llfuse.FUSEError(errno.ENOENT) if p.release: return self._getattr_release(p) @@ -1028,7 +1027,7 @@ def readdir(self, p: VirtualPath) -> Iterator[tuple[str, dict[str, Any]]]: if (release_id := self.vnames.lookup_release(p)) and ( release := get_release(self.config, release_id) ): - tracks = get_tracks_associated_with_release(self.config, release) + tracks = get_tracks_of_release(self.config, release) for trk, vname in self.vnames.list_track_paths(p, tracks): yield vname, self.stat("file", trk.source_path) if release.cover_image_path: @@ -1093,7 +1092,7 @@ def readdir(self, p: VirtualPath) -> Iterator[tuple[str, dict[str, Any]]]: return if p.view == "Collages" and p.collage: - _, releases = get_collage(self.config, p.collage) # type: ignore + releases = get_collage_releases(self.config, p.collage) for rls, vname in self.vnames.list_release_paths(p, releases): yield vname, self.stat("dir", rls.source_path) return @@ -1105,15 +1104,15 @@ def readdir(self, p: VirtualPath) -> Iterator[tuple[str, dict[str, Any]]]: return if p.view == "Playlists" and p.playlist: - pdata = get_playlist(self.config, p.playlist) - if pdata is None: + playlist = get_playlist(self.config, p.playlist) + if playlist is None: raise llfuse.FUSEError(errno.ENOENT) - playlist, tracks = pdata - for trk, vname in self.vnames.list_track_paths(p, tracks): - yield vname, self.stat("file", trk.source_path) if playlist.cover_path: v = f"cover{playlist.cover_path.suffix}" yield v, self.stat("file", playlist.cover_path) + tracks = get_playlist_tracks(self.config, p.playlist) + for trk, vname in self.vnames.list_track_paths(p, tracks): + yield vname, self.stat("file", trk.source_path) return if p.view == "Playlists": @@ -1140,15 +1139,15 @@ def unlink(self, p: VirtualPath) -> None: and p.playlist and p.file and p.file.lower() in self.config.valid_cover_arts - and (pdata := get_playlist(self.config, p.playlist)) + and (playlist := get_playlist(self.config, p.playlist)) ): - delete_playlist_cover_art(self.config, pdata[0].name) + delete_playlist_cover_art(self.config, playlist.name) return if ( p.view == "Playlists" and p.playlist and p.file - and (pdata := get_playlist(self.config, p.playlist)) + and get_playlist(self.config, p.playlist) is not None and (track_id := self.vnames.lookup_track(p)) ): remove_track_from_playlist(self.config, p.playlist, track_id) @@ -1260,7 +1259,7 @@ def open(self, p: VirtualPath, flags: int) -> int: ): # If the file is a music file, handle it as a music file. if track_id := self.vnames.lookup_track(p): - tracks = get_tracks_associated_with_release(self.config, release) + tracks = get_tracks_of_release(self.config, release) for t in tracks: if t.id == track_id: fh = self.fhandler.wrap_host(os.open(str(t.source_path), flags)) @@ -1277,7 +1276,7 @@ def open(self, p: VirtualPath, flags: int) -> int: # sequence. if p.file.lower() in self.config.valid_cover_arts and flags & os.O_CREAT == os.O_CREAT: fh = self.fhandler.next() - logtext = calculate_release_logtext( + logtext = make_release_logtext( title=release.releasetitle, releasedate=release.releasedate, artists=release.releaseartists, @@ -1295,10 +1294,9 @@ def open(self, p: VirtualPath, flags: int) -> int: return fh raise llfuse.FUSEError(err) if p.playlist and p.file: - try: - playlist, tracks = get_playlist(self.config, p.playlist) # type: ignore - except TypeError as e: - raise llfuse.FUSEError(errno.ENOENT) from e + playlist = get_playlist(self.config, p.playlist) + if not playlist: + raise llfuse.FUSEError(errno.ENOENT) # If we are trying to create an audio file in the playlist, enter the # "add-track-to-playlist" operation sequence. See the __init__ for more details. pf = Path(p.file)