From 8cb4d4f7371ba824961bd6b715a099b2fe98453b Mon Sep 17 00:00:00 2001 From: azuline Date: Sun, 5 May 2024 21:26:31 -0500 Subject: [PATCH] clean up api surface (#116) --- rose/__init__.py | 187 +++++++----- rose/cache.py | 374 +++++++++++------------- rose/cache_test.py | 151 ++++------ rose/collages.py | 15 +- rose/playlists.py | 19 +- rose/releases.py | 28 +- rose/releases_test.py | 22 +- rose/rules.py | 12 +- rose/templates.py | 44 +-- rose/templates_test.py | 28 +- rose_cli/cli.py | 6 +- rose_vfs/virtualfs.py | 121 ++++---- rose_watchdog/__init__.py | 8 + {rose => rose_watchdog}/watcher.py | 0 {rose => rose_watchdog}/watcher_test.py | 2 +- 15 files changed, 515 insertions(+), 502 deletions(-) create mode 100644 rose_watchdog/__init__.py rename {rose => rose_watchdog}/watcher.py (100%) rename {rose => rose_watchdog}/watcher_test.py (99%) diff --git a/rose/__init__.py b/rose/__init__.py index f6c90f7..d88f20c 100644 --- a/rose/__init__.py +++ b/rose/__init__.py @@ -4,25 +4,24 @@ UnsupportedFiletypeError, ) from rose.cache import ( - STORED_DATA_FILE_REGEX, - CachedRelease, - CachedTrack, + Collage, DescriptorEntry, GenreEntry, LabelEntry, + Playlist, + Release, + Track, artist_exists, - calculate_release_logtext, - calculate_track_logtext, - collage_exists, + collage_lock_name, descriptor_exists, genre_exists, get_collage, - get_path_of_track_in_playlist, + get_collage_releases, 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, @@ -30,9 +29,21 @@ list_genres, list_labels, list_playlists, + lock, + make_release_logtext, + make_track_logtext, maybe_invalidate_cache_database, - playlist_exists, + playlist_lock_name, + release_lock_name, + release_within_collage, + track_within_playlist, + track_within_release, update_cache, + update_cache_evict_nonexistent_collages, + update_cache_evict_nonexistent_playlists, + update_cache_evict_nonexistent_releases, + update_cache_for_collages, + update_cache_for_playlists, update_cache_for_releases, ) from rose.collages import ( @@ -47,6 +58,8 @@ ) from rose.common import ( VERSION, + Artist, + ArtistMapping, RoseError, RoseExpectedError, initialize_logging, @@ -82,95 +95,123 @@ 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 -from rose.watcher import start_watchdog __all__ = [ - "AudioTags", - "CachedRelease", - "CachedTrack", + # Plumbing + "initialize_logging", + "VERSION", + # Errors + "RoseError", + "RoseExpectedError", + "UnsupportedFiletypeError", + # Utilities + "sanitize_dirname", + "sanitize_filename", + "make_release_logtext", + "make_track_logtext", + "SUPPORTED_AUDIO_EXTENSIONS", + # Configuration "Config", - "DescriptorEntry", - "GenreEntry", - "LabelEntry", + # Cache + "maybe_invalidate_cache_database", + "update_cache", + "update_cache_evict_nonexistent_collages", + "update_cache_evict_nonexistent_playlists", + "update_cache_evict_nonexistent_releases", + "update_cache_for_collages", + "update_cache_for_playlists", + "update_cache_for_releases", + # Locks + "lock", + "release_lock_name", + "collage_lock_name", + "playlist_lock_name", + # Tagging + "AudioTags", + # Rule Engine "MetadataAction", "MetadataMatcher", "MetadataRule", + "execute_metadata_rule", + "execute_stored_metadata_rules", + "run_actions_on_release", + "run_actions_on_track", + # Path Templates "PathContext", "PathTemplate", - "RoseError", - "RoseExpectedError", - "STORED_DATA_FILE_REGEX", # TODO: Revise: is_release_directory / is_track_file - "SUPPORTED_AUDIO_EXTENSIONS", - "UnsupportedFiletypeError", - "VERSION", - "add_release_to_collage", - "add_track_to_playlist", - "artist_exists", - "calculate_release_logtext", # TODO: Rename. - "calculate_track_logtext", # TODO: Rename. - "collage_exists", - "create_collage", - "create_playlist", + "evaluate_release_template", + "evaluate_track_template", + "preview_path_templates", + # Releases + "Release", "create_single_release", - "delete_collage", - "delete_playlist", - "delete_playlist_cover_art", "delete_release", "delete_release_cover_art", - "descriptor_exists", - "dump_all_collages", - "dump_all_playlists", "dump_all_releases", - "dump_all_tracks", - "dump_collage", - "dump_playlist", "dump_release", - "dump_track", - "edit_collage_in_editor", # TODO: Move editor part to CLI, make this file-submissions. - "edit_playlist_in_editor", # TODO: Move editor part to CLI, make this file-submissions. "edit_release", - "eval_release_template", # TODO: Rename. - "eval_track_template", # TODO: Rename. - "execute_metadata_rule", - "execute_stored_metadata_rules", - "genre_exists", - "get_collage", - "get_path_of_track_in_playlist", # TODO: Redesign. - "get_playlist", - "get_playlist_cover_path", # TODO: Remove. "get_release", + "set_release_cover_art", + "toggle_release_new", + # Tracks + "Track", + "dump_all_tracks", + "dump_track", "get_track", - "get_tracks_associated_with_release", # TODO: Rename: `get_tracks_of_release` / `dump_release(with_tracks=tracks)` - "label_exists", + "get_tracks_of_release", + "track_within_release", + # Artists + "Artist", + "ArtistMapping", + "artist_exists", "list_artists", - "list_collages", - "list_descriptors", + # Genres + "GenreEntry", "list_genres", + "genre_exists", + # Descriptors + "DescriptorEntry", + "list_descriptors", + "descriptor_exists", + # Labels + "LabelEntry", "list_labels", - "list_playlists", - "maybe_invalidate_cache_database", - "playlist_exists", - "preview_path_templates", + "label_exists", + # Collages + "Collage", + "add_release_to_collage", + "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", - "remove_track_from_playlist", + "release_within_collage", "rename_collage", + # Playlists + "Playlist", + "add_track_to_playlist", + "list_playlists", + "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. + "track_within_playlist", + "remove_track_from_playlist", "rename_playlist", - "run_actions_on_release", - "run_actions_on_track", - "sanitize_dirname", - "sanitize_filename", "set_playlist_cover_art", - "set_release_cover_art", - "start_watchdog", - "toggle_release_new", - "update_cache", - "update_cache_for_releases", - "initialize_logging", ] initialize_logging(__name__) diff --git a/rose/cache.py b/rose/cache.py index e21b7b2..3daa417 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__) @@ -201,7 +201,7 @@ def playlist_lock_name(playlist_name: str) -> str: @dataclass(slots=True) -class CachedRelease: +class Release: id: str source_path: Path cover_image_path: Path | None @@ -225,37 +225,6 @@ class CachedRelease: releaseartists: ArtistMapping metahash: str - @classmethod - def from_view(cls, c: Config, row: dict[str, Any], aliases: bool = True) -> CachedRelease: - secondary_genres = _split(row["secondary_genres"]) if row["secondary_genres"] else [] - genres = _split(row["genres"]) if row["genres"] else [] - return CachedRelease( - id=row["id"], - source_path=Path(row["source_path"]), - cover_image_path=Path(row["cover_image_path"]) if row["cover_image_path"] else None, - added_at=row["added_at"], - datafile_mtime=row["datafile_mtime"], - releasetitle=row["releasetitle"], - releasetype=row["releasetype"], - releasedate=RoseDate.parse(row["releasedate"]), - originaldate=RoseDate.parse(row["originaldate"]), - compositiondate=RoseDate.parse(row["compositiondate"]), - catalognumber=row["catalognumber"], - edition=row["edition"], - disctotal=row["disctotal"], - new=bool(row["new"]), - genres=genres, - secondary_genres=secondary_genres, - parent_genres=_get_parent_genres(genres), - parent_secondary_genres=_get_parent_genres(secondary_genres), - descriptors=_split(row["descriptors"]) if row["descriptors"] else [], - labels=_split(row["labels"]) if row["labels"] else [], - releaseartists=_unpack_artists( - c, row["releaseartist_names"], row["releaseartist_roles"], aliases=aliases - ), - metahash=row["metahash"], - ) - def dump(self) -> dict[str, Any]: return { "id": self.id, @@ -283,8 +252,39 @@ def dump(self) -> dict[str, Any]: } +def cached_release_from_view(c: Config, row: dict[str, Any], aliases: bool = True) -> Release: + secondary_genres = _split(row["secondary_genres"]) if row["secondary_genres"] else [] + genres = _split(row["genres"]) if row["genres"] else [] + return Release( + id=row["id"], + source_path=Path(row["source_path"]), + cover_image_path=Path(row["cover_image_path"]) if row["cover_image_path"] else None, + added_at=row["added_at"], + datafile_mtime=row["datafile_mtime"], + releasetitle=row["releasetitle"], + releasetype=row["releasetype"], + releasedate=RoseDate.parse(row["releasedate"]), + originaldate=RoseDate.parse(row["originaldate"]), + compositiondate=RoseDate.parse(row["compositiondate"]), + catalognumber=row["catalognumber"], + edition=row["edition"], + disctotal=row["disctotal"], + new=bool(row["new"]), + genres=genres, + secondary_genres=secondary_genres, + parent_genres=_get_parent_genres(genres), + parent_secondary_genres=_get_parent_genres(secondary_genres), + descriptors=_split(row["descriptors"]) if row["descriptors"] else [], + labels=_split(row["labels"]) if row["labels"] else [], + releaseartists=_unpack_artists( + c, row["releaseartist_names"], row["releaseartist_roles"], aliases=aliases + ), + metahash=row["metahash"], + ) + + @dataclass(slots=True) -class CachedTrack: +class Track: id: str source_path: Path source_mtime: str @@ -296,34 +296,7 @@ class CachedTrack: trackartists: ArtistMapping metahash: str - release: CachedRelease - - @classmethod - def from_view( - cls, - c: Config, - row: dict[str, Any], - release: CachedRelease, - aliases: bool = True, - ) -> CachedTrack: - return CachedTrack( - id=row["id"], - source_path=Path(row["source_path"]), - source_mtime=row["source_mtime"], - tracktitle=row["tracktitle"], - tracknumber=row["tracknumber"], - tracktotal=row["tracktotal"], - discnumber=row["discnumber"], - duration_seconds=row["duration_seconds"], - trackartists=_unpack_artists( - c, - row["trackartist_names"], - row["trackartist_roles"], - aliases=aliases, - ), - metahash=row["metahash"], - release=release, - ) + release: Release def dump(self, with_release_info: bool = True) -> dict[str, Any]: r = { @@ -368,19 +341,43 @@ def dump(self, with_release_info: bool = True) -> dict[str, Any]: return r +def cached_track_from_view( + c: Config, + row: dict[str, Any], + release: Release, + aliases: bool = True, +) -> Track: + return Track( + id=row["id"], + source_path=Path(row["source_path"]), + source_mtime=row["source_mtime"], + tracktitle=row["tracktitle"], + tracknumber=row["tracknumber"], + tracktotal=row["tracktotal"], + discnumber=row["discnumber"], + duration_seconds=row["duration_seconds"], + trackartists=_unpack_artists( + c, + row["trackartist_names"], + row["trackartist_roles"], + aliases=aliases, + ), + metahash=row["metahash"], + release=release, + ) + + @dataclass(slots=True) -class CachedCollage: +class Collage: name: str source_mtime: str - release_ids: list[str] @dataclass(slots=True) -class CachedPlaylist: +class Playlist: name: str source_mtime: str cover_path: Path | None - track_ids: list[str] @dataclass(slots=True) @@ -565,7 +562,7 @@ def _update_cache_for_releases_executor( # 1. Fetch all releases. # 2. Fetch all tracks in a single query, and then associates each track with a release. # The tracks are stored as a dict of source_path -> Track. - cached_releases: dict[str, tuple[CachedRelease, dict[str, CachedTrack]]] = {} + cached_releases: dict[str, tuple[Release, dict[str, Track]]] = {} with connect(c) as conn: cursor = conn.execute( rf""" @@ -576,7 +573,7 @@ def _update_cache_for_releases_executor( release_uuids, ) for row in cursor: - cached_releases[row["id"]] = (CachedRelease.from_view(c, row, aliases=False), {}) + cached_releases[row["id"]] = (cached_release_from_view(c, row, aliases=False), {}) logger.debug(f"Found {len(cached_releases)}/{len(release_dirs)} releases in cache") @@ -590,7 +587,7 @@ def _update_cache_for_releases_executor( ) num_tracks_found = 0 for row in cursor: - cached_releases[row["release_id"]][1][row["source_path"]] = CachedTrack.from_view( + cached_releases[row["release_id"]][1][row["source_path"]] = cached_track_from_view( c, row, cached_releases[row["release_id"]][0], @@ -647,7 +644,7 @@ def _update_cache_for_releases_executor( f"First-time unidentified release found at release {source_path}, writing UUID and new" ) release_dirty = True - release = CachedRelease( + release = Release( id=preexisting_release_id or "", source_path=source_path, datafile_mtime="", @@ -767,7 +764,7 @@ def _update_cache_for_releases_executor( # Now we'll switch over to processing some of the tracks. We need track metadata in # order to calculate some fields of the release, so we'll first compute the valid set of - # CachedTracks, and then we will finalize the release and execute any required database + # Tracks, and then we will finalize the release and execute any required database # operations for the release and tracks. # We want to know which cached tracks are no longer on disk. By the end of the following @@ -778,7 +775,7 @@ def _update_cache_for_releases_executor( # leverage mtimes and such to avoid unnecessary recomputations. If a release has changed # and should be updated in the database, we add its ID to track_ids_to_insert, which # will be used in the database execution step. - tracks: list[CachedTrack] = [] + tracks: list[Track] = [] track_ids_to_insert: set[str] = set() # This value is set to true if we read an AudioTags and used it to confirm the release # tags. @@ -916,7 +913,7 @@ def _update_cache_for_releases_executor( continue # And now create the cached track. - track = CachedTrack( + track = Track( id=track_id, source_path=Path(f), source_mtime=track_mtime, @@ -954,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 @@ -990,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. @@ -1424,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, CachedCollage] = {} + cached_collages: dict[str, tuple[Collage, list[str]]] = {} with connect(c) as conn: cursor = conn.execute( """ @@ -1438,10 +1435,12 @@ def update_cache_for_collages( """, ) for row in cursor: - cached_collages[row["name"]] = CachedCollage( - 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, @@ -1453,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 = CachedCollage( + cached_collage = Collage( name=name, source_mtime="", - release_ids=[], ) + release_ids = [] try: source_mtime = str(f.stat().st_mtime) @@ -1496,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. @@ -1508,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( @@ -1619,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, CachedPlaylist] = {} + cached_playlists: dict[str, tuple[Playlist, list[str]]] = {} with connect(c) as conn: cursor = conn.execute( """ @@ -1634,11 +1633,13 @@ def update_cache_for_playlists( """, ) for row in cursor: - cached_playlists[row["name"]] = CachedPlaylist( - 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, @@ -1650,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 = CachedPlaylist( + 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 @@ -1713,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. @@ -1733,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"] @@ -1823,7 +1824,7 @@ def list_releases_delete_this( descriptor_filter: str | None = None, label_filter: str | None = None, new: bool | None = None, -) -> list[CachedRelease]: +) -> list[Release]: with connect(c) as conn: query = "SELECT * FROM releases_view WHERE 1=1" args: list[str | bool] = [] @@ -1877,13 +1878,13 @@ def list_releases_delete_this( query += " ORDER BY source_path" cursor = conn.execute(query, args) - releases: list[CachedRelease] = [] + releases: list[Release] = [] for row in cursor: - releases.append(CachedRelease.from_view(c, row)) + releases.append(cached_release_from_view(c, row)) return releases -def list_releases(c: Config, release_ids: list[str] | None = None) -> list[CachedRelease]: +def list_releases(c: Config, release_ids: list[str] | None = None) -> list[Release]: """Fetch data associated with given release IDs. Pass None to fetch all.""" query = "SELECT * FROM releases_view" args = [] @@ -1893,13 +1894,13 @@ def list_releases(c: Config, release_ids: list[str] | None = None) -> list[Cache query += " ORDER BY source_path" with connect(c) as conn: cursor = conn.execute(query, args) - releases: list[CachedRelease] = [] + releases: list[Release] = [] for row in cursor: - releases.append(CachedRelease.from_view(c, row)) + releases.append(cached_release_from_view(c, row)) return releases -def get_release(c: Config, release_id: str) -> CachedRelease | None: +def get_release(c: Config, release_id: str) -> Release | None: with connect(c) as conn: cursor = conn.execute( "SELECT * FROM releases_view WHERE id = ?", @@ -1908,7 +1909,7 @@ def get_release(c: Config, release_id: str) -> CachedRelease | None: row = cursor.fetchone() if not row: return None - return CachedRelease.from_view(c, row) + return cached_release_from_view(c, row) def get_release_logtext(c: Config, release_id: str) -> str | None: @@ -1921,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, @@ -1940,7 +1941,7 @@ def calculate_release_logtext( return logtext -def list_tracks(c: Config, track_ids: list[str] | None = None) -> list[CachedTrack]: +def list_tracks(c: Config, track_ids: list[str] | None = None) -> list[Track]: """Fetch data associated with given track IDs. Pass None to fetch all.""" query = "SELECT * FROM tracks_view" args = [] @@ -1961,31 +1962,31 @@ def list_tracks(c: Config, track_ids: list[str] | None = None) -> list[CachedTra """, release_ids, ) - releases_map: dict[str, CachedRelease] = {} + releases_map: dict[str, Release] = {} for row in cursor: - releases_map[row["id"]] = CachedRelease.from_view(c, row) + releases_map[row["id"]] = cached_release_from_view(c, row) rval = [] for row in trackrows: - rval.append(CachedTrack.from_view(c, row, releases_map[row["release_id"]])) + rval.append(cached_track_from_view(c, row, releases_map[row["release_id"]])) return rval -def get_track(c: Config, uuid: str) -> CachedTrack | None: +def get_track(c: Config, uuid: str) -> Track | None: with connect(c) as conn: cursor = conn.execute("SELECT * FROM tracks_view WHERE id = ?", (uuid,)) trackrow = cursor.fetchone() if not trackrow: return None cursor = conn.execute("SELECT * FROM releases_view WHERE id = ?", (trackrow["release_id"],)) - release = CachedRelease.from_view(c, cursor.fetchone()) - return CachedTrack.from_view(c, trackrow, release) + release = cached_release_from_view(c, cursor.fetchone()) + return cached_track_from_view(c, trackrow, release) -def get_tracks_associated_with_release( +def get_tracks_of_release( c: Config, - release: CachedRelease, -) -> list[CachedTrack]: + release: Release, +) -> list[Track]: with connect(c) as conn: cursor = conn.execute( """ @@ -1998,16 +1999,16 @@ def get_tracks_associated_with_release( ) rval = [] for row in cursor: - rval.append(CachedTrack.from_view(c, row, release)) + rval.append(cached_track_from_view(c, row, release)) return rval -def get_tracks_associated_with_releases( +def get_tracks_of_releases( c: Config, - releases: list[CachedRelease], -) -> list[tuple[CachedRelease, list[CachedTrack]]]: + releases: list[Release], +) -> list[tuple[Release, list[Track]]]: releases_map = {r.id: r for r in releases} - tracks_map: dict[str, list[CachedTrack]] = defaultdict(list) + tracks_map: dict[str, list[Track]] = defaultdict(list) with connect(c) as conn: cursor = conn.execute( f""" @@ -2020,7 +2021,7 @@ def get_tracks_associated_with_releases( ) for row in cursor: tracks_map[row["release_id"]].append( - CachedTrack.from_view(c, row, releases_map[row["release_id"]]) + cached_track_from_view(c, row, releases_map[row["release_id"]]) ) rval = [] @@ -2030,51 +2031,57 @@ def get_tracks_associated_with_releases( return rval -def get_path_of_track_in_release( +def track_within_release( c: Config, track_id: str, release_id: str, -) -> Path | None: +) -> bool | None: with connect(c) as conn: cursor = conn.execute( """ - SELECT source_path + SELECT 1 FROM tracks WHERE id = ? AND release_id = ? """, - ( - track_id, - release_id, - ), + (track_id, release_id), ) - row = cursor.fetchone() - if row: - return Path(row["source_path"]) - return None + return bool(cursor.fetchone()) -def get_path_of_track_in_playlist( +def track_within_playlist( c: Config, track_id: str, playlist_name: str, -) -> Path | None: +) -> bool: with connect(c) as conn: cursor = conn.execute( """ - SELECT t.source_path + SELECT 1 FROM tracks t JOIN playlists_tracks pt ON pt.track_id = t.id AND pt.playlist_name = ? WHERE t.id = ? """, - ( - playlist_name, - track_id, - ), + (playlist_name, track_id), ) - row = cursor.fetchone() - if row: - return Path(row["source_path"]) - return None + return bool(cursor.fetchone()) + + +def release_within_collage( + c: Config, + release_id: str, + collage_name: str, +) -> bool: + with connect(c) as conn: + cursor = conn.execute( + """ + SELECT 1 + FROM releases t + JOIN collages_releases pt ON pt.release_id = t.id AND pt.collage_name = ? + WHERE t.id = ? + """, + (collage_name, release_id), + ) + return bool(cursor.fetchone()) def get_track_logtext(c: Config, track_id: str) -> str | None: @@ -2097,7 +2104,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"]), @@ -2105,7 +2112,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, @@ -2124,7 +2131,7 @@ def list_playlists(c: Config) -> list[str]: return [r["name"] for r in cursor] -def get_playlist(c: Config, playlist_name: str) -> tuple[CachedPlaylist, list[CachedTrack]] | None: +def get_playlist(c: Config, playlist_name: str) -> Playlist | None: with connect(c) as conn: cursor = conn.execute( """ @@ -2140,14 +2147,15 @@ def get_playlist(c: Config, playlist_name: str) -> tuple[CachedPlaylist, list[Ca row = cursor.fetchone() if not row: return None - playlist = CachedPlaylist( + 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.* @@ -2169,37 +2177,15 @@ def get_playlist(c: Config, playlist_name: str) -> tuple[CachedPlaylist, list[Ca """, release_ids, ) - releases_map: dict[str, CachedRelease] = {} + releases_map: dict[str, Release] = {} for row in cursor: - releases_map[row["id"]] = CachedRelease.from_view(c, row) + releases_map[row["id"]] = cached_release_from_view(c, row) - tracks: list[CachedTrack] = [] + tracks: list[Track] = [] for row in trackrows: - playlist.track_ids.append(row["id"]) - tracks.append(CachedTrack.from_view(c, row, releases_map[row["release_id"]])) - - return playlist, tracks + tracks.append(cached_track_from_view(c, row, releases_map[row["release_id"]])) - -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]: @@ -2208,7 +2194,7 @@ def list_collages(c: Config) -> list[str]: return [r["name"] for r in cursor] -def get_collage(c: Config, collage_name: str) -> tuple[CachedCollage, list[CachedRelease]] | 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 = ?", @@ -2217,12 +2203,14 @@ def get_collage(c: Config, collage_name: str) -> tuple[CachedCollage, list[Cache row = cursor.fetchone() if not row: return None - collage = CachedCollage( + 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.* @@ -2233,21 +2221,11 @@ def get_collage(c: Config, collage_name: str) -> tuple[CachedCollage, list[Cache """, (collage_name,), ) - releases: list[CachedRelease] = [] + releases: list[Release] = [] for row in cursor: - collage.release_ids.append(row["id"]) - releases.append(CachedRelease.from_view(c, row)) - - return (collage, releases) + releases.append(cached_release_from_view(c, row)) - -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 1a54ad0..6e33ae8 100644 --- a/rose/cache_test.py +++ b/rose/cache_test.py @@ -12,30 +12,28 @@ from rose.cache import ( CACHE_SCHEMA_PATH, STORED_DATA_FILE_REGEX, - CachedCollage, - CachedPlaylist, - CachedRelease, - CachedTrack, + Collage, DescriptorEntry, GenreEntry, LabelEntry, + Playlist, + Release, + Track, _unpack, artist_exists, - collage_exists, connect, descriptor_exists, genre_exists, get_collage, - get_path_of_track_in_playlist, - get_path_of_track_in_release, + get_collage_releases, 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 +45,9 @@ list_tracks, lock, maybe_invalidate_cache_database, - playlist_exists, + release_within_collage, + track_within_playlist, + track_within_release, update_cache, update_cache_evict_nonexistent_releases, update_cache_for_releases, @@ -1107,7 +1107,7 @@ def test_update_cache_playlists_on_release_rename(config: Config) -> None: @pytest.mark.usefixtures("seeded_cache") def test_list_releases(config: Config) -> None: expected = [ - CachedRelease( + Release( datafile_mtime="999", id="r1", source_path=Path(config.music_source_dir / "r1"), @@ -1142,7 +1142,7 @@ def test_list_releases(config: Config) -> None: releaseartists=ArtistMapping(main=[Artist("Techno Man"), Artist("Bass Man")]), metahash="1", ), - CachedRelease( + Release( datafile_mtime="999", id="r2", source_path=Path(config.music_source_dir / "r2"), @@ -1171,7 +1171,7 @@ def test_list_releases(config: Config) -> None: ), metahash="2", ), - CachedRelease( + Release( datafile_mtime="999", id="r3", source_path=Path(config.music_source_dir / "r3"), @@ -1205,7 +1205,7 @@ def test_list_releases(config: Config) -> None: def test_get_release_and_associated_tracks(config: Config) -> None: release = get_release(config, "r1") assert release is not None - assert release == CachedRelease( + assert release == Release( datafile_mtime="999", id="r1", source_path=Path(config.music_source_dir / "r1"), @@ -1242,7 +1242,7 @@ def test_get_release_and_associated_tracks(config: Config) -> None: ) expected_tracks = [ - CachedTrack( + Track( id="t1", source_path=config.music_source_dir / "r1" / "01.m4a", source_mtime="999", @@ -1255,7 +1255,7 @@ def test_get_release_and_associated_tracks(config: Config) -> None: metahash="1", release=release, ), - CachedTrack( + Track( id="t2", source_path=config.music_source_dir / "r1" / "02.m4a", source_mtime="999", @@ -1270,8 +1270,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 +1291,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=[ @@ -1311,7 +1311,7 @@ def test_get_release_logtext(config: Config) -> None: @pytest.mark.usefixtures("seeded_cache") def test_list_tracks(config: Config) -> None: expected = [ - CachedTrack( + Track( id="t1", source_path=config.music_source_dir / "r1" / "01.m4a", source_mtime="999", @@ -1322,7 +1322,7 @@ def test_list_tracks(config: Config) -> None: duration_seconds=120, trackartists=ArtistMapping(main=[Artist("Techno Man"), Artist("Bass Man")]), metahash="1", - release=CachedRelease( + release=Release( datafile_mtime="999", id="r1", source_path=Path(config.music_source_dir / "r1"), @@ -1358,7 +1358,7 @@ def test_list_tracks(config: Config) -> None: metahash="1", ), ), - CachedTrack( + Track( id="t2", source_path=config.music_source_dir / "r1" / "02.m4a", source_mtime="999", @@ -1369,7 +1369,7 @@ def test_list_tracks(config: Config) -> None: duration_seconds=240, trackartists=ArtistMapping(main=[Artist("Techno Man"), Artist("Bass Man")]), metahash="2", - release=CachedRelease( + release=Release( datafile_mtime="999", id="r1", source_path=Path(config.music_source_dir / "r1"), @@ -1405,7 +1405,7 @@ def test_list_tracks(config: Config) -> None: metahash="1", ), ), - CachedTrack( + Track( id="t3", source_path=config.music_source_dir / "r2" / "01.m4a", source_mtime="999", @@ -1418,7 +1418,7 @@ def test_list_tracks(config: Config) -> None: main=[Artist("Violin Woman")], guest=[Artist("Conductor Woman")] ), metahash="3", - release=CachedRelease( + release=Release( id="r2", source_path=config.music_source_dir / "r2", cover_image_path=config.music_source_dir / "r2" / "cover.jpg", @@ -1448,7 +1448,7 @@ def test_list_tracks(config: Config) -> None: metahash="2", ), ), - CachedTrack( + Track( id="t4", source_path=config.music_source_dir / "r3" / "01.m4a", source_mtime="999", @@ -1459,7 +1459,7 @@ def test_list_tracks(config: Config) -> None: duration_seconds=120, trackartists=ArtistMapping(), metahash="4", - release=CachedRelease( + release=Release( id="r3", source_path=config.music_source_dir / "r3", cover_image_path=None, @@ -1492,7 +1492,7 @@ def test_list_tracks(config: Config) -> None: @pytest.mark.usefixtures("seeded_cache") def test_get_track(config: Config) -> None: - assert get_track(config, "t1") == CachedTrack( + assert get_track(config, "t1") == Track( id="t1", source_path=config.music_source_dir / "r1" / "01.m4a", source_mtime="999", @@ -1503,7 +1503,7 @@ def test_get_track(config: Config) -> None: duration_seconds=120, trackartists=ArtistMapping(main=[Artist("Techno Man"), Artist("Bass Man")]), metahash="1", - release=CachedRelease( + release=Release( datafile_mtime="999", id="r1", source_path=Path(config.music_source_dir / "r1"), @@ -1542,25 +1542,27 @@ def test_get_track(config: Config) -> None: @pytest.mark.usefixtures("seeded_cache") -def test_get_path_of_track_in_release(config: Config) -> None: - assert ( - get_path_of_track_in_release(config, "t1", "r1") - == config.music_source_dir / "r1" / "01.m4a" - ) - assert get_path_of_track_in_release(config, "t3", "r1") is None - assert get_path_of_track_in_release(config, "lalala", "r1") is None - assert get_path_of_track_in_release(config, "t1", "lalala") is None +def test_track_within_release(config: Config) -> None: + assert track_within_release(config, "t1", "r1") + assert not track_within_release(config, "t3", "r1") + assert not track_within_release(config, "lalala", "r1") + assert not track_within_release(config, "t1", "lalala") @pytest.mark.usefixtures("seeded_cache") -def test_get_path_of_track_in_playlist(config: Config) -> None: - assert ( - get_path_of_track_in_playlist(config, "t1", "Lala Lisa") - == config.music_source_dir / "r1" / "01.m4a" - ) - assert get_path_of_track_in_playlist(config, "t2", "Lala Lisa") is None - assert get_path_of_track_in_playlist(config, "lalala", "Lala Lisa") is None - assert get_path_of_track_in_playlist(config, "t1", "lalala") is None +def test_track_within_playlist(config: Config) -> None: + assert track_within_playlist(config, "t1", "Lala Lisa") + assert not track_within_playlist(config, "t2", "Lala Lisa") + assert not track_within_playlist(config, "lalala", "Lala Lisa") + assert not track_within_playlist(config, "t1", "lalala") + + +@pytest.mark.usefixtures("seeded_cache") +def test_release_within_collage(config: Config) -> None: + assert release_within_collage(config, "r1", "Rose Gold") + assert not release_within_collage(config, "r1", "Ruby Red") + assert not release_within_collage(config, "lalala", "Rose Gold") + assert not release_within_collage(config, "r1", "lalala") @pytest.mark.usefixtures("seeded_cache") @@ -1627,16 +1629,12 @@ 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 == CachedCollage( + assert get_collage(config, "Rose Gold") == Collage( name="Rose Gold", source_mtime="999", - release_ids=["r1", "r2"], ) - assert releases == [ - CachedRelease( + assert get_collage_releases(config, "Rose Gold") == [ + Release( id="r1", source_path=config.music_source_dir / "r1", cover_image_path=None, @@ -1671,7 +1669,7 @@ def test_get_collage(config: Config) -> None: releaseartists=ArtistMapping(main=[Artist("Techno Man"), Artist("Bass Man")]), metahash="1", ), - CachedRelease( + Release( id="r2", source_path=config.music_source_dir / "r2", cover_image_path=config.music_source_dir / "r2" / "cover.jpg", @@ -1702,21 +1700,11 @@ def test_get_collage(config: Config) -> None: ), ] - cdata = get_collage(config, "Ruby Red") - assert cdata is not None - collage, releases = cdata - assert collage == CachedCollage( + 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,17 +1715,13 @@ 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 == CachedPlaylist( + 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 == [ - CachedTrack( + assert get_playlist_tracks(config, "Lala Lisa") == [ + Track( id="t1", source_path=config.music_source_dir / "r1" / "01.m4a", source_mtime="999", @@ -1748,7 +1732,7 @@ def test_get_playlist(config: Config) -> None: duration_seconds=120, trackartists=ArtistMapping(main=[Artist("Techno Man"), Artist("Bass Man")]), metahash="1", - release=CachedRelease( + release=Release( datafile_mtime="999", id="r1", source_path=Path(config.music_source_dir / "r1"), @@ -1784,7 +1768,7 @@ def test_get_playlist(config: Config) -> None: metahash="1", ), ), - CachedTrack( + Track( id="t3", source_path=config.music_source_dir / "r2" / "01.m4a", source_mtime="999", @@ -1797,7 +1781,7 @@ def test_get_playlist(config: Config) -> None: main=[Artist("Violin Woman")], guest=[Artist("Conductor Woman")] ), metahash="3", - release=CachedRelease( + release=Release( id="r2", source_path=config.music_source_dir / "r2", cover_image_path=config.music_source_dir / "r2" / "cover.jpg", @@ -1830,21 +1814,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 fa2c9a6..338a5f2 100644 --- a/rose/releases.py +++ b/rose/releases.py @@ -21,14 +21,14 @@ from rose.audiotags import AudioTags, RoseDate from rose.cache import ( STORED_DATA_FILE_REGEX, - CachedRelease, - CachedTrack, - calculate_release_logtext, + Release, + Track, 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, @@ -253,7 +253,7 @@ class MetadataRelease: tracks: dict[str, MetadataTrack] @classmethod - def from_cache(cls, release: CachedRelease, tracks: list[CachedTrack]) -> MetadataRelease: + def from_cache(cls, release: Release, tracks: list[Track]) -> MetadataRelease: return MetadataRelease( title=release.releasetitle, new=release.new, @@ -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 7c2c390..b90c619 100644 --- a/rose/releases_test.py +++ b/rose/releases_test.py @@ -10,11 +10,11 @@ from conftest import TEST_RELEASE_1 from rose.audiotags import AudioTags, RoseDate from rose.cache import ( - CachedRelease, - CachedTrack, + Release, + Track, connect, get_release, - get_tracks_associated_with_release, + get_tracks_of_release, update_cache, ) from rose.common import Artist, ArtistMapping @@ -181,7 +181,7 @@ def test_edit_release(monkeypatch: Any, config: Config, source_dir: Path) -> Non edit_release(config, release_id) release = get_release(config, release_id) assert release is not None - assert release == CachedRelease( + assert release == Release( id=release_id, source_path=release_path, cover_image_path=None, @@ -210,9 +210,9 @@ 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 == [ - CachedTrack( + Track( id=track_ids[0], source_path=release_path / "01.m4a", source_mtime=tracks[0].source_mtime, @@ -225,7 +225,7 @@ def test_edit_release(monkeypatch: Any, config: Config, source_dir: Path) -> Non metahash=tracks[0].metahash, release=release, ), - CachedTrack( + Track( id=track_ids[1], source_path=release_path / "02.m4a", source_mtime=tracks[1].source_mtime, @@ -355,7 +355,7 @@ def editfn(text: str, **_: Any) -> str: release = get_release(config, release_id) assert release is not None - assert release == CachedRelease( + assert release == Release( id=release_id, source_path=release_path, cover_image_path=None, @@ -379,9 +379,9 @@ 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 == [ - CachedTrack( + Track( id=track_ids[0], source_path=release_path / "01.m4a", source_mtime=tracks[0].source_mtime, @@ -394,7 +394,7 @@ def editfn(text: str, **_: Any) -> str: metahash=tracks[0].metahash, release=release, ), - CachedTrack( + Track( id=track_ids[1], source_path=release_path / "02.m4a", source_mtime=tracks[1].source_mtime, diff --git a/rose/rules.py b/rose/rules.py index d2401be..d905bd6 100644 --- a/rose/rules.py +++ b/rose/rules.py @@ -24,8 +24,8 @@ from rose.audiotags import AudioTags, RoseDate from rose.cache import ( - CachedRelease, - CachedTrack, + Release, + Track, connect, list_releases, list_tracks, @@ -679,8 +679,8 @@ def fast_search_for_matching_releases( def filter_track_false_positives_using_read_cache( matcher: MetadataMatcher, - tracks: list[CachedTrack], -) -> list[CachedTrack]: + tracks: list[Track], +) -> list[Track]: time_start = time.time() rval = [] for t in tracks: @@ -729,8 +729,8 @@ def filter_track_false_positives_using_read_cache( def filter_release_false_positives_using_read_cache( matcher: MetadataMatcher, - releases: list[CachedRelease], -) -> list[CachedRelease]: + releases: list[Release], +) -> list[Release]: time_start = time.time() rval = [] for r in releases: diff --git a/rose/templates.py b/rose/templates.py index e4a6930..758135e 100644 --- a/rose/templates.py +++ b/rose/templates.py @@ -21,7 +21,7 @@ from rose.common import Artist, ArtistMapping, RoseExpectedError if typing.TYPE_CHECKING: - from rose.cache import CachedRelease, CachedTrack + from rose.cache import Release, Track from rose.config import Config RELEASE_TYPE_FORMATTER = { @@ -281,9 +281,9 @@ class PathContext: playlist: str | None -def eval_release_template( +def evaluate_release_template( template: PathTemplate, - release: CachedRelease, + release: Release, context: PathContext | None = None, position: str | None = None, ) -> str: @@ -292,9 +292,9 @@ def eval_release_template( ) -def eval_track_template( +def evaluate_track_template( template: PathTemplate, - track: CachedTrack, + track: Track, context: PathContext | None = None, position: str | None = None, ) -> str: @@ -306,7 +306,7 @@ def eval_track_template( ) -def _calc_release_variables(release: CachedRelease, position: str | None) -> dict[str, Any]: +def _calc_release_variables(release: Release, position: str | None) -> dict[str, Any]: return { "added_at": release.added_at, "releasetitle": release.releasetitle, @@ -329,7 +329,7 @@ def _calc_release_variables(release: CachedRelease, position: str | None) -> dic } -def _calc_track_variables(track: CachedTrack, position: str | None) -> dict[str, Any]: +def _calc_track_variables(track: Track, position: str | None) -> dict[str, Any]: return { "added_at": track.release.added_at, "tracktitle": track.tracktitle, @@ -402,10 +402,10 @@ def preview_path_templates(c: Config) -> None: # fmt: on -def _get_preview_releases(c: Config) -> tuple[CachedRelease, CachedRelease, CachedRelease]: - from rose.cache import CachedRelease +def _get_preview_releases(c: Config) -> tuple[Release, Release, Release]: + from rose.cache import Release - kimlip = CachedRelease( + kimlip = Release( id="018b268e-ff1e-7a0c-9ac8-7bbb282761f2", source_path=c.music_source_dir / "LOONA - 2017. Kim Lip", cover_image_path=None, @@ -443,7 +443,7 @@ def _get_preview_releases(c: Config) -> tuple[CachedRelease, CachedRelease, Cach metahash="0", ) - youngforever = CachedRelease( + youngforever = Release( id="018b6021-f1e5-7d4b-b796-440fbbea3b13", source_path=c.music_source_dir / "BTS - 2016. Young Forever (花樣年華)", cover_image_path=None, @@ -484,7 +484,7 @@ def _get_preview_releases(c: Config) -> tuple[CachedRelease, CachedRelease, Cach metahash="0", ) - debussy = CachedRelease( + debussy = Release( id="018b268e-de0c-7cb2-8ffa-bcc2083c94e6", source_path=c.music_source_dir / "Debussy - 1907. Images performed by Cleveland Orchestra under Pierre Boulez (1992)", @@ -522,23 +522,23 @@ 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: # Import cycle trick :) - from rose.cache import CachedTrack + from rose.cache import Track kimlip, youngforever, debussy = _get_preview_releases(c) click.secho(f"{label}:", dim=True, underline=True) click.secho(" Sample 1: ", dim=True, nl=False) - track = CachedTrack( + track = Track( id="018b268e-ff1e-7a0c-9ac8-7bbb282761f1", source_path=c.music_source_dir / "LOONA - 2017. Kim Lip" / "01. Eclipse.opus", source_mtime="999", @@ -551,10 +551,10 @@ 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 = CachedTrack( + track = Track( id="018b6021-f1e5-7d4b-b796-440fbbea3b15", source_path=c.music_source_dir / "BTS - 2016. Young Forever (花樣年華)" @@ -569,10 +569,10 @@ 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 = CachedTrack( + track = Track( id="018b6514-6e65-78cc-94a5-fdb17418f090", source_path=c.music_source_dir / "Debussy - 1907. Images performed by Cleveland Orchestra under Pierre Boulez (1992)" @@ -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 cd6fe0e..d2dd243 100644 --- a/rose/templates_test.py +++ b/rose/templates_test.py @@ -5,19 +5,19 @@ from click.testing import CliRunner from rose.audiotags import RoseDate -from rose.cache import CachedRelease, CachedTrack +from rose.cache import Release, Track from rose.common import Artist, ArtistMapping from rose.config import Config from rose.templates import ( PathTemplate, PathTemplateConfig, _get_preview_releases, - eval_release_template, - eval_track_template, + evaluate_release_template, + evaluate_track_template, preview_path_templates, ) -EMPTY_CACHED_RELEASE = CachedRelease( +EMPTY_CACHED_RELEASE = Release( id="", source_path=Path(), cover_image_path=None, @@ -42,7 +42,7 @@ metahash="0", ) -EMPTY_CACHED_TRACK = CachedTrack( +EMPTY_CACHED_TRACK = Track( id="", source_path=Path("hi.m4a"), source_mtime="", @@ -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 910eb62..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, @@ -57,14 +57,16 @@ run_actions_on_track, set_playlist_cover_art, set_release_cover_art, - start_watchdog, toggle_release_new, update_cache, ) from rose_vfs import mount_virtualfs +from rose_watchdog import start_watchdog 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 eb0ded9..0557905 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,21 +56,17 @@ import llfuse from rose import ( - STORED_DATA_FILE_REGEX, SUPPORTED_AUDIO_EXTENSIONS, AudioTags, - CachedRelease, - CachedTrack, Config, PathContext, PathTemplate, + Release, RoseError, + Track, 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,14 @@ 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 +89,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 +101,18 @@ 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, + track_within_playlist, + track_within_release, +) logger = logging.getLogger(__name__) +STORED_DATA_FILE_REGEX = re.compile(r"\.rose\.([^.]+)\.toml") + K = TypeVar("K") V = TypeVar("V") T = TypeVar("T") @@ -409,8 +413,8 @@ def __init__(self, config: Config, sanitizer: Sanitizer): def list_release_paths( self, release_parent: VirtualPath, - releases: list[CachedRelease], - ) -> Iterator[tuple[CachedRelease, str]]: + releases: list[Release], + ) -> Iterator[tuple[Release, str]]: """ Given a parent directory and a list of releases, calculates the virtual directory names for those releases, and returns a zipped iterator of the releases and their virtual @@ -443,7 +447,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 +495,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( @@ -521,8 +525,8 @@ def list_release_paths( def list_track_paths( self, track_parent: VirtualPath, - tracks: list[CachedTrack], - ) -> Iterator[tuple[CachedTrack, str]]: + tracks: list[Track], + ) -> Iterator[tuple[Track, str]]: """ Given a parent directory and a list of tracks, calculates the virtual filenames for those tracks, and returns a zipped iterator of the tracks and their virtual filenames. @@ -556,7 +560,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 +605,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,35 +920,41 @@ 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) - for t in tracks: - if t.id == track_id: - return self.stat("file", t.source_path) - logger.debug("LOGICAL: Resolved track_id not found in the given tracklist") - raise llfuse.FUSEError(errno.ENOENT) + if not track_within_release(self.config, track_id, release.id): + logger.debug("LOGICAL: Resolved track_id not found in the given release") + raise llfuse.FUSEError(errno.ENOENT) + if track := get_track(self.config, track_id): + return self.stat("file", track.source_path) + raise RoseError( + "Impossible: Resolved track_id after track_within_release check does not exist" + ) def getattr(self, p: VirtualPath) -> dict[str, Any]: logger.debug(f"LOGICAL: Received getattr for {p=}") # 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) - raise llfuse.FUSEError(errno.ENOENT) + if not track_within_playlist(self.config, track_id, p.playlist): + raise llfuse.FUSEError(errno.ENOENT) + if track := get_track(self.config, track_id): + return self.stat("file", track.source_path) + raise RoseError( + "Impossible: Resolved track_id after track_within_playlist check does not exist" + ) return self.stat("dir") # 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: + if p.release: # TODO: Validate existence of release in collage. return self._getattr_release(p) return self.stat("dir") @@ -953,7 +963,7 @@ def getattr(self, p: VirtualPath) -> dict[str, Any]: la = self.sanitizer.unsanitize(p.label, p.label_parent) if not label_exists(self.config, la) or not self.can_show.label(la): raise llfuse.FUSEError(errno.ENOENT) - if p.release: + if p.release: # TODO: Validate existence of release in label. return self._getattr_release(p) return self.stat("dir") @@ -962,7 +972,7 @@ def getattr(self, p: VirtualPath) -> dict[str, Any]: d = self.sanitizer.unsanitize(p.descriptor, p.descriptor_parent) if not descriptor_exists(self.config, d) or not self.can_show.descriptor(d): raise llfuse.FUSEError(errno.ENOENT) - if p.release: + if p.release: # TODO: Validate existence of release in descriptor. return self._getattr_release(p) return self.stat("dir") @@ -971,7 +981,7 @@ def getattr(self, p: VirtualPath) -> dict[str, Any]: g = self.sanitizer.unsanitize(p.genre, p.genre_parent) if not genre_exists(self.config, g) or not self.can_show.genre(g): raise llfuse.FUSEError(errno.ENOENT) - if p.release: + if p.release: # TODO: Validate existence of release in genre. return self._getattr_release(p) return self.stat("dir") @@ -980,7 +990,7 @@ def getattr(self, p: VirtualPath) -> dict[str, Any]: a = self.sanitizer.unsanitize(p.artist, p.artist_parent) if not artist_exists(self.config, a) or not self.can_show.artist(a): raise llfuse.FUSEError(errno.ENOENT) - if p.release: + if p.release: # TODO: Validate existence of release in artist. return self._getattr_release(p) return self.stat("dir") @@ -1028,7 +1038,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 +1103,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 +1115,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 +1150,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 +1270,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 +1287,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 +1305,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) diff --git a/rose_watchdog/__init__.py b/rose_watchdog/__init__.py new file mode 100644 index 0000000..4744230 --- /dev/null +++ b/rose_watchdog/__init__.py @@ -0,0 +1,8 @@ +from rose import initialize_logging +from rose_watchdog.watcher import start_watchdog + +__all__ = [ + "start_watchdog", +] + +initialize_logging(__name__) diff --git a/rose/watcher.py b/rose_watchdog/watcher.py similarity index 100% rename from rose/watcher.py rename to rose_watchdog/watcher.py diff --git a/rose/watcher_test.py b/rose_watchdog/watcher_test.py similarity index 99% rename from rose/watcher_test.py rename to rose_watchdog/watcher_test.py index b32f76e..7441dd4 100644 --- a/rose/watcher_test.py +++ b/rose_watchdog/watcher_test.py @@ -7,7 +7,7 @@ from conftest import TEST_COLLAGE_1, TEST_PLAYLIST_1, TEST_RELEASE_2, TEST_RELEASE_3, retry_for_sec from rose.cache import connect from rose.config import Config -from rose.watcher import start_watchdog +from rose_watchdog.watcher import start_watchdog @contextmanager