diff --git a/rose/__init__.py b/rose/__init__.py index eab5454..7f95e15 100644 --- a/rose/__init__.py +++ b/rose/__init__.py @@ -3,6 +3,7 @@ AudioTags, RoseDate, UnsupportedFiletypeError, + UnsupportedTagValueTypeError, ) from rose.cache import ( Collage, @@ -23,6 +24,7 @@ get_release, get_track, get_tracks_of_release, + get_tracks_of_releases, label_exists, list_artists, list_collages, @@ -30,6 +32,8 @@ list_genres, list_labels, list_playlists, + list_releases, + list_tracks, lock, make_release_logtext, make_track_logtext, @@ -48,6 +52,9 @@ update_cache_for_releases, ) from rose.collages import ( + CollageAlreadyExistsError, + CollageDoesNotExistError, + DescriptionMismatchError, add_release_to_collage, create_collage, delete_collage, @@ -65,8 +72,16 @@ sanitize_dirname, sanitize_filename, ) -from rose.config import Config +from rose.config import ( + Config, + ConfigDecodeError, + ConfigNotFoundError, + InvalidConfigValueError, + MissingConfigKeyError, +) from rose.playlists import ( + PlaylistAlreadyExistsError, + PlaylistDoesNotExistError, add_track_to_playlist, create_playlist, delete_playlist, @@ -77,33 +92,46 @@ set_playlist_cover_art, ) from rose.releases import ( + InvalidCoverArtFileError, + ReleaseDoesNotExistError, + ReleaseEditFailedError, + UnknownArtistRoleError, create_single_release, delete_release, delete_release_cover_art, edit_release, + find_releases_matching_rule, run_actions_on_release, set_release_cover_art, toggle_release_new, ) -from rose.rule_parser import MetadataAction, MetadataMatcher, MetadataRule -from rose.rules import execute_metadata_rule, execute_stored_metadata_rules +from rose.rule_parser import InvalidRuleError, MetadataAction, MetadataMatcher, MetadataRule +from rose.rules import ( + InvalidReplacementValueError, + TrackTagNotAllowedError, + execute_metadata_rule, + execute_stored_metadata_rules, +) from rose.templates import ( + InvalidPathTemplateError, PathContext, PathTemplate, evaluate_release_template, evaluate_track_template, get_sample_music, ) -from rose.tracks import run_actions_on_track +from rose.tracks import TrackDoesNotExistError, find_tracks_matching_rule, run_actions_on_track __all__ = [ # Plumbing "initialize_logging", "VERSION", - # Errors "RoseError", "RoseExpectedError", - "UnsupportedFiletypeError", + "DescriptionMismatchError", + "InvalidCoverArtFileError", + "ReleaseDoesNotExistError", + "ReleaseEditFailedError", # Utilities "sanitize_dirname", "sanitize_filename", @@ -112,6 +140,10 @@ "SUPPORTED_AUDIO_EXTENSIONS", # Configuration "Config", + "ConfigNotFoundError", + "ConfigDecodeError", + "MissingConfigKeyError", + "InvalidConfigValueError", # Cache "maybe_invalidate_cache_database", "update_cache", @@ -129,6 +161,8 @@ # Tagging "AudioTags", "RoseDate", + "UnsupportedFiletypeError", + "UnsupportedTagValueTypeError", # Rule Engine "MetadataAction", "MetadataMatcher", @@ -137,31 +171,42 @@ "execute_stored_metadata_rules", "run_actions_on_release", "run_actions_on_track", + "InvalidRuleError", + "InvalidReplacementValueError", + "TrackTagNotAllowedError", # Path Templates "PathContext", "PathTemplate", "evaluate_release_template", "evaluate_track_template", "get_sample_music", + "InvalidPathTemplateError", # Releases "Release", "create_single_release", "delete_release", "delete_release_cover_art", "edit_release", + "list_releases", + "find_releases_matching_rule", "get_release", "set_release_cover_art", "toggle_release_new", # Tracks "Track", "get_track", + "find_tracks_matching_rule", + "list_tracks", "get_tracks_of_release", + "get_tracks_of_releases", "track_within_release", + "TrackDoesNotExistError", # Artists "Artist", "ArtistMapping", "artist_exists", "list_artists", + "UnknownArtistRoleError", # Genres "GenreEntry", "list_genres", @@ -186,6 +231,8 @@ "remove_release_from_collage", "release_within_collage", "rename_collage", + "CollageDoesNotExistError", + "CollageAlreadyExistsError", # Playlists "Playlist", "add_track_to_playlist", @@ -200,6 +247,8 @@ "remove_track_from_playlist", "rename_playlist", "set_playlist_cover_art", + "PlaylistDoesNotExistError", + "PlaylistAlreadyExistsError", ] initialize_logging(__name__) diff --git a/rose/cache.py b/rose/cache.py index 48f0860..6cf4c2f 100644 --- a/rose/cache.py +++ b/rose/cache.py @@ -225,32 +225,6 @@ class Release: releaseartists: ArtistMapping metahash: str - def dump(self) -> dict[str, Any]: - return { - "id": self.id, - "source_path": str(self.source_path.resolve()), - "cover_image_path": str(self.cover_image_path.resolve()) - if self.cover_image_path - else None, - "added_at": self.added_at, - "releasetitle": self.releasetitle, - "releasetype": self.releasetype, - "releasedate": str(self.releasedate) if self.releasedate else None, - "originaldate": str(self.originaldate) if self.originaldate else None, - "compositiondate": str(self.compositiondate) if self.compositiondate else None, - "catalognumber": self.catalognumber, - "edition": self.edition, - "new": self.new, - "disctotal": self.disctotal, - "genres": self.genres, - "parent_genres": self.parent_genres, - "secondary_genres": self.secondary_genres, - "parent_secondary_genres": self.parent_secondary_genres, - "descriptors": self.descriptors, - "labels": self.labels, - "releaseartists": self.releaseartists.dump(), - } - 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 [] @@ -298,48 +272,6 @@ class Track: release: Release - def dump(self, with_release_info: bool = True) -> dict[str, Any]: - r = { - "id": self.id, - "source_path": str(self.source_path.resolve()), - "tracktitle": self.tracktitle, - "tracknumber": self.tracknumber, - "tracktotal": self.tracktotal, - "discnumber": self.discnumber, - "duration_seconds": self.duration_seconds, - "trackartists": self.trackartists.dump(), - } - if with_release_info: - r.update( - { - "release_id": self.release.id, - "added_at": self.release.added_at, - "releasetitle": self.release.releasetitle, - "releasetype": self.release.releasetype, - "disctotal": self.release.disctotal, - "releasedate": str(self.release.releasedate) - if self.release.releasedate - else None, - "originaldate": str(self.release.originaldate) - if self.release.originaldate - else None, - "compositiondate": str(self.release.compositiondate) - if self.release.compositiondate - else None, - "catalognumber": self.release.catalognumber, - "edition": self.release.edition, - "new": self.release.new, - "genres": self.release.genres, - "parent_genres": self.release.parent_genres, - "secondary_genres": self.release.secondary_genres, - "parent_secondary_genres": self.release.parent_secondary_genres, - "descriptors": self.release.descriptors, - "labels": self.release.labels, - "releaseartists": self.release.releaseartists.dump(), - } - ) - return r - def cached_track_from_view( c: Config, diff --git a/rose/collages.py b/rose/collages.py index 2b71fdb..5d7ac17 100644 --- a/rose/collages.py +++ b/rose/collages.py @@ -2,7 +2,6 @@ The collages module provides functions for interacting with collages. """ -import json import logging from pathlib import Path from typing import Any @@ -14,16 +13,14 @@ from rose.cache import ( collage_lock_name, - get_collage, - get_collage_releases, get_release_logtext, - list_collages, lock, update_cache_evict_nonexistent_collages, update_cache_for_collages, ) from rose.common import RoseExpectedError from rose.config import Config +from rose.releases import ReleaseDoesNotExistError logger = logging.getLogger(__name__) @@ -40,10 +37,6 @@ class CollageAlreadyExistsError(RoseExpectedError): pass -class ReleaseDoesNotExistError(RoseExpectedError): - pass - - def create_collage(c: Config, name: str) -> None: (c.music_source_dir / "!collages").mkdir(parents=True, exist_ok=True) path = collage_path(c, name) @@ -145,30 +138,6 @@ def add_release_to_collage( update_cache_for_collages(c, [collage_name], force=True) -def dump_collage(c: Config, collage_name: str) -> str: - 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(collage_releases): - releases.append({"position": idx + 1, **rls.dump()}) - return json.dumps({"name": collage_name, "releases": releases}) - - -def dump_all_collages(c: Config) -> str: - out: list[dict[str, Any]] = [] - for name in list_collages(c): - 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(collage_releases): - releases.append({"position": idx + 1, **rls.dump()}) - out.append({"name": name, "releases": releases}) - return json.dumps(out) - - def edit_collage_in_editor(c: Config, collage_name: str) -> None: path = collage_path(c, collage_name) if not path.exists(): diff --git a/rose/collages_test.py b/rose/collages_test.py index d4a75c6..f63eaea 100644 --- a/rose/collages_test.py +++ b/rose/collages_test.py @@ -1,8 +1,6 @@ -import json from pathlib import Path from typing import Any -import pytest import tomllib from rose.cache import connect, update_cache @@ -10,8 +8,6 @@ add_release_to_collage, create_collage, delete_collage, - dump_all_collages, - dump_collage, edit_collage_in_editor, remove_release_from_collage, rename_collage, @@ -118,187 +114,6 @@ def test_rename_collage(config: Config, source_dir: Path) -> None: assert not cursor.fetchone()[0] -@pytest.mark.usefixtures("seeded_cache") -def test_dump_collage(config: Config) -> None: - out = dump_collage(config, "Rose Gold") - assert json.loads(out) == { - "name": "Rose Gold", - "releases": [ - { - "position": 1, - "id": "r1", - "source_path": f"{config.music_source_dir}/r1", - "cover_image_path": None, - "added_at": "0000-01-01T00:00:00+00:00", - "releasetitle": "Release 1", - "releasetype": "album", - "releasedate": "2023", - "compositiondate": None, - "catalognumber": None, - "new": False, - "disctotal": 1, - "genres": ["Techno", "Deep House"], - "parent_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - ], - "labels": ["Silk Music"], - "originaldate": None, - "edition": None, - "secondary_genres": ["Rominimal", "Ambient"], - "parent_secondary_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - "Tech House", - ], - "descriptors": ["Warm", "Hot"], - "releaseartists": { - "main": [ - {"name": "Techno Man", "alias": False}, - {"name": "Bass Man", "alias": False}, - ], - "guest": [], - "remixer": [], - "conductor": [], - "producer": [], - "composer": [], - "djmixer": [], - }, - }, - { - "position": 2, - "id": "r2", - "source_path": f"{config.music_source_dir}/r2", - "cover_image_path": f"{config.music_source_dir}/r2/cover.jpg", - "added_at": "0000-01-01T00:00:00+00:00", - "releasetitle": "Release 2", - "releasetype": "album", - "releasedate": "2021", - "compositiondate": None, - "catalognumber": "DG-001", - "new": True, - "disctotal": 1, - "genres": ["Modern Classical"], - "parent_genres": ["Classical Music", "Western Classical Music"], - "labels": ["Native State"], - "originaldate": "2019", - "edition": "Deluxe", - "secondary_genres": ["Orchestral"], - "parent_secondary_genres": [ - "Classical Music", - "Western Classical Music", - ], - "descriptors": ["Wet"], - "releaseartists": { - "main": [{"name": "Violin Woman", "alias": False}], - "guest": [{"name": "Conductor Woman", "alias": False}], - "remixer": [], - "conductor": [], - "producer": [], - "composer": [], - "djmixer": [], - }, - }, - ], - } - - -@pytest.mark.usefixtures("seeded_cache") -def test_dump_collages(config: Config) -> None: - out = dump_all_collages(config) - assert json.loads(out) == [ - { - "name": "Rose Gold", - "releases": [ - { - "position": 1, - "id": "r1", - "source_path": f"{config.music_source_dir}/r1", - "cover_image_path": None, - "added_at": "0000-01-01T00:00:00+00:00", - "releasetitle": "Release 1", - "releasetype": "album", - "releasedate": "2023", - "compositiondate": None, - "catalognumber": None, - "new": False, - "disctotal": 1, - "genres": ["Techno", "Deep House"], - "parent_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - ], - "labels": ["Silk Music"], - "originaldate": None, - "edition": None, - "secondary_genres": ["Rominimal", "Ambient"], - "parent_secondary_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - "Tech House", - ], - "descriptors": ["Warm", "Hot"], - "releaseartists": { - "main": [ - {"name": "Techno Man", "alias": False}, - {"name": "Bass Man", "alias": False}, - ], - "guest": [], - "remixer": [], - "conductor": [], - "producer": [], - "composer": [], - "djmixer": [], - }, - }, - { - "position": 2, - "id": "r2", - "source_path": f"{config.music_source_dir}/r2", - "cover_image_path": f"{config.music_source_dir}/r2/cover.jpg", - "added_at": "0000-01-01T00:00:00+00:00", - "releasetitle": "Release 2", - "releasetype": "album", - "releasedate": "2021", - "compositiondate": None, - "catalognumber": "DG-001", - "new": True, - "disctotal": 1, - "genres": ["Modern Classical"], - "parent_genres": ["Classical Music", "Western Classical Music"], - "labels": ["Native State"], - "originaldate": "2019", - "edition": "Deluxe", - "secondary_genres": ["Orchestral"], - "parent_secondary_genres": [ - "Classical Music", - "Western Classical Music", - ], - "descriptors": ["Wet"], - "releaseartists": { - "main": [{"name": "Violin Woman", "alias": False}], - "guest": [{"name": "Conductor Woman", "alias": False}], - "remixer": [], - "conductor": [], - "producer": [], - "composer": [], - "djmixer": [], - }, - }, - ], - }, - {"name": "Ruby Red", "releases": []}, - ] - - def test_edit_collages_ordering(monkeypatch: Any, config: Config, source_dir: Path) -> None: filepath = source_dir / "!collages" / "Rose Gold.toml" monkeypatch.setattr("rose.collages.click.edit", lambda x: "\n".join(reversed(x.split("\n")))) diff --git a/rose/playlists.py b/rose/playlists.py index 80fa336..cc3ce22 100644 --- a/rose/playlists.py +++ b/rose/playlists.py @@ -2,7 +2,6 @@ The playlists module provides functions for interacting with playlists. """ -import json import logging import shutil from collections import Counter @@ -15,37 +14,25 @@ from send2trash import send2trash from rose.cache import ( - get_playlist, - get_playlist_tracks, get_track_logtext, - list_playlists, lock, playlist_lock_name, update_cache_evict_nonexistent_playlists, update_cache_for_playlists, ) +from rose.collages import DescriptionMismatchError from rose.common import RoseExpectedError from rose.config import Config +from rose.releases import InvalidCoverArtFileError +from rose.tracks import TrackDoesNotExistError logger = logging.getLogger(__name__) -class InvalidCoverArtFileError(RoseExpectedError): - pass - - -class DescriptionMismatchError(RoseExpectedError): - pass - - class PlaylistDoesNotExistError(RoseExpectedError): pass -class TrackDoesNotExistError(RoseExpectedError): - pass - - class PlaylistAlreadyExistsError(RoseExpectedError): pass @@ -152,42 +139,6 @@ def add_track_to_playlist( update_cache_for_playlists(c, [playlist_name], force=True) -def dump_playlist(c: Config, playlist_name: str) -> str: - 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(playlist_tracks): - tracks.append({"position": idx + 1, **trk.dump()}) - return json.dumps( - { - "name": playlist_name, - "cover_image_path": str(playlist.cover_path) if playlist.cover_path else None, - "tracks": tracks, - } - ) - - -def dump_all_playlists(c: Config) -> str: - out: list[dict[str, Any]] = [] - for name in list_playlists(c): - 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(playlist_tracks): - tracks.append({"position": idx + 1, **trk.dump()}) - out.append( - { - "name": name, - "cover_image_path": str(playlist.cover_path) if playlist.cover_path else None, - "tracks": tracks, - } - ) - return json.dumps(out) - - def edit_playlist_in_editor(c: Config, playlist_name: str) -> None: path = playlist_path(c, playlist_name) if not path.exists(): diff --git a/rose/playlists_test.py b/rose/playlists_test.py index ed5d789..ae887b6 100644 --- a/rose/playlists_test.py +++ b/rose/playlists_test.py @@ -1,9 +1,7 @@ -import json import shutil from pathlib import Path from typing import Any -import pytest import tomllib from conftest import TEST_PLAYLIST_1, TEST_RELEASE_1 @@ -14,8 +12,6 @@ create_playlist, delete_playlist, delete_playlist_cover_art, - dump_all_playlists, - dump_playlist, edit_playlist_in_editor, remove_track_from_playlist, rename_playlist, @@ -124,251 +120,6 @@ def test_rename_playlist(config: Config, source_dir: Path) -> None: assert not cursor.fetchone()[0] -@pytest.mark.usefixtures("seeded_cache") -def test_dump_playlist(config: Config) -> None: - out = dump_playlist(config, "Lala Lisa") - assert json.loads(out) == { - "name": "Lala Lisa", - "cover_image_path": f"{config.music_source_dir}/!playlists/Lala Lisa.jpg", - "tracks": [ - { - "position": 1, - "id": "t1", - "source_path": f"{config.music_source_dir}/r1/01.m4a", - "tracktitle": "Track 1", - "tracknumber": "01", - "tracktotal": 2, - "discnumber": "01", - "disctotal": 1, - "duration_seconds": 120, - "trackartists": { - "main": [ - {"name": "Techno Man", "alias": False}, - {"name": "Bass Man", "alias": False}, - ], - "guest": [], - "remixer": [], - "producer": [], - "composer": [], - "conductor": [], - "djmixer": [], - }, - "added_at": "0000-01-01T00:00:00+00:00", - "release_id": "r1", - "releasetitle": "Release 1", - "releasetype": "album", - "releasedate": "2023", - "compositiondate": None, - "catalognumber": None, - "new": False, - "genres": ["Techno", "Deep House"], - "parent_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - ], - "labels": ["Silk Music"], - "originaldate": None, - "edition": None, - "secondary_genres": ["Rominimal", "Ambient"], - "parent_secondary_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - "Tech House", - ], - "descriptors": ["Warm", "Hot"], - "releaseartists": { - "main": [ - {"name": "Techno Man", "alias": False}, - {"name": "Bass Man", "alias": False}, - ], - "guest": [], - "remixer": [], - "producer": [], - "composer": [], - "conductor": [], - "djmixer": [], - }, - }, - { - "position": 2, - "id": "t3", - "source_path": f"{config.music_source_dir}/r2/01.m4a", - "tracktitle": "Track 1", - "tracknumber": "01", - "tracktotal": 1, - "discnumber": "01", - "disctotal": 1, - "duration_seconds": 120, - "trackartists": { - "main": [{"name": "Violin Woman", "alias": False}], - "guest": [{"name": "Conductor Woman", "alias": False}], - "remixer": [], - "producer": [], - "composer": [], - "conductor": [], - "djmixer": [], - }, - "added_at": "0000-01-01T00:00:00+00:00", - "release_id": "r2", - "releasetitle": "Release 2", - "releasetype": "album", - "releasedate": "2021", - "compositiondate": None, - "catalognumber": "DG-001", - "new": True, - "genres": ["Modern Classical"], - "parent_genres": ["Classical Music", "Western Classical Music"], - "labels": ["Native State"], - "originaldate": "2019", - "edition": "Deluxe", - "secondary_genres": ["Orchestral"], - "parent_secondary_genres": [ - "Classical Music", - "Western Classical Music", - ], - "descriptors": ["Wet"], - "releaseartists": { - "main": [{"name": "Violin Woman", "alias": False}], - "guest": [{"name": "Conductor Woman", "alias": False}], - "remixer": [], - "producer": [], - "composer": [], - "conductor": [], - "djmixer": [], - }, - }, - ], - } - - -@pytest.mark.usefixtures("seeded_cache") -def test_dump_playlists(config: Config) -> None: - out = dump_all_playlists(config) - assert json.loads(out) == [ - { - "name": "Lala Lisa", - "cover_image_path": f"{config.music_source_dir}/!playlists/Lala Lisa.jpg", - "tracks": [ - { - "position": 1, - "id": "t1", - "source_path": f"{config.music_source_dir}/r1/01.m4a", - "tracktitle": "Track 1", - "tracknumber": "01", - "tracktotal": 2, - "discnumber": "01", - "disctotal": 1, - "duration_seconds": 120, - "trackartists": { - "main": [ - {"name": "Techno Man", "alias": False}, - {"name": "Bass Man", "alias": False}, - ], - "guest": [], - "remixer": [], - "producer": [], - "composer": [], - "conductor": [], - "djmixer": [], - }, - "added_at": "0000-01-01T00:00:00+00:00", - "release_id": "r1", - "releasetitle": "Release 1", - "releasetype": "album", - "releasedate": "2023", - "compositiondate": None, - "catalognumber": None, - "new": False, - "genres": ["Techno", "Deep House"], - "parent_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - ], - "labels": ["Silk Music"], - "originaldate": None, - "edition": None, - "secondary_genres": ["Rominimal", "Ambient"], - "parent_secondary_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - "Tech House", - ], - "descriptors": ["Warm", "Hot"], - "releaseartists": { - "main": [ - {"name": "Techno Man", "alias": False}, - {"name": "Bass Man", "alias": False}, - ], - "guest": [], - "remixer": [], - "producer": [], - "composer": [], - "conductor": [], - "djmixer": [], - }, - }, - { - "position": 2, - "id": "t3", - "source_path": f"{config.music_source_dir}/r2/01.m4a", - "tracktitle": "Track 1", - "tracknumber": "01", - "tracktotal": 1, - "discnumber": "01", - "disctotal": 1, - "duration_seconds": 120, - "trackartists": { - "main": [{"name": "Violin Woman", "alias": False}], - "guest": [{"name": "Conductor Woman", "alias": False}], - "remixer": [], - "producer": [], - "composer": [], - "conductor": [], - "djmixer": [], - }, - "added_at": "0000-01-01T00:00:00+00:00", - "release_id": "r2", - "releasetitle": "Release 2", - "releasetype": "album", - "releasedate": "2021", - "compositiondate": None, - "catalognumber": "DG-001", - "new": True, - "genres": ["Modern Classical"], - "parent_genres": ["Classical Music", "Western Classical Music"], - "labels": ["Native State"], - "originaldate": "2019", - "edition": "Deluxe", - "secondary_genres": ["Orchestral"], - "parent_secondary_genres": [ - "Classical Music", - "Western Classical Music", - ], - "descriptors": ["Wet"], - "releaseartists": { - "main": [{"name": "Violin Woman", "alias": False}], - "guest": [{"name": "Conductor Woman", "alias": False}], - "remixer": [], - "producer": [], - "composer": [], - "conductor": [], - "djmixer": [], - }, - }, - ], - }, - {"name": "Turtle Rabbit", "cover_image_path": None, "tracks": []}, - ] - - def test_edit_playlists_ordering(monkeypatch: Any, config: Config, source_dir: Path) -> None: filepath = source_dir / "!playlists" / "Lala Lisa.toml" monkeypatch.setattr("rose.playlists.click.edit", lambda x: "\n".join(reversed(x.split("\n")))) diff --git a/rose/releases.py b/rose/releases.py index 7346944..4630b97 100644 --- a/rose/releases.py +++ b/rose/releases.py @@ -5,7 +5,6 @@ from __future__ import annotations import dataclasses -import json import logging import re import shlex @@ -25,7 +24,6 @@ Track, get_release, get_tracks_of_release, - get_tracks_of_releases, list_releases, lock, make_release_logtext, @@ -68,32 +66,6 @@ class UnknownArtistRoleError(RoseExpectedError): pass -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_of_release(c, release) - return json.dumps( - {**release.dump(), "tracks": [t.dump(with_release_info=False) for t in tracks]} - ) - - -def dump_all_releases(c: Config, matcher: MetadataMatcher | None = None) -> str: - release_ids = None - if matcher: - release_ids = [x.id for x in fast_search_for_matching_releases(c, matcher)] - releases = list_releases(c, release_ids) - if matcher: - releases = filter_release_false_positives_using_read_cache(matcher, 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]} - for release, tracks in rt_pairs - ] - ) - - def delete_release(c: Config, release_id: str) -> None: release = get_release(c, release_id) if not release: @@ -477,6 +449,12 @@ def edit_release( update_cache_for_releases(c, [release.source_path], force=True) +def find_releases_matching_rule(c: Config, matcher: MetadataMatcher) -> list[Release]: + release_ids = [x.id for x in fast_search_for_matching_releases(c, matcher)] + releases = list_releases(c, release_ids) + return filter_release_false_positives_using_read_cache(matcher, releases) + + def run_actions_on_release( c: Config, release_id: str, diff --git a/rose/releases_test.py b/rose/releases_test.py index b90c619..d99ad82 100644 --- a/rose/releases_test.py +++ b/rose/releases_test.py @@ -1,4 +1,3 @@ -import json import re import shutil from pathlib import Path @@ -24,14 +23,12 @@ create_single_release, delete_release, delete_release_cover_art, - dump_all_releases, - dump_release, edit_release, run_actions_on_release, set_release_cover_art, toggle_release_new, ) -from rose.rule_parser import MetadataAction, MetadataMatcher +from rose.rule_parser import MetadataAction def test_delete_release(config: Config) -> None: @@ -446,355 +443,6 @@ def test_extract_single_release_with_trailing_space(config: Config) -> None: assert (source_path / "01. Trailing Space.m4a").is_file() -@pytest.mark.usefixtures("seeded_cache") -def test_dump_release(config: Config) -> None: - assert json.loads(dump_release(config, "r1")) == { - "id": "r1", - "source_path": f"{config.music_source_dir}/r1", - "cover_image_path": None, - "added_at": "0000-01-01T00:00:00+00:00", - "releasetitle": "Release 1", - "releasetype": "album", - "releasedate": "2023", - "compositiondate": None, - "catalognumber": None, - "new": False, - "disctotal": 1, - "genres": ["Techno", "Deep House"], - "parent_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - ], - "labels": ["Silk Music"], - "originaldate": None, - "edition": None, - "secondary_genres": ["Rominimal", "Ambient"], - "parent_secondary_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - "Tech House", - ], - "descriptors": ["Warm", "Hot"], - "releaseartists": { - "main": [ - {"name": "Techno Man", "alias": False}, - {"name": "Bass Man", "alias": False}, - ], - "guest": [], - "remixer": [], - "producer": [], - "composer": [], - "conductor": [], - "djmixer": [], - }, - "tracks": [ - { - "trackartists": { - "composer": [], - "djmixer": [], - "guest": [], - "main": [ - {"alias": False, "name": "Techno Man"}, - {"alias": False, "name": "Bass Man"}, - ], - "producer": [], - "remixer": [], - "conductor": [], - }, - "discnumber": "01", - "duration_seconds": 120, - "id": "t1", - "source_path": f"{config.music_source_dir}/r1/01.m4a", - "tracktitle": "Track 1", - "tracknumber": "01", - "tracktotal": 2, - }, - { - "trackartists": { - "composer": [], - "djmixer": [], - "guest": [], - "main": [ - {"alias": False, "name": "Techno Man"}, - {"alias": False, "name": "Bass Man"}, - ], - "producer": [], - "remixer": [], - "conductor": [], - }, - "discnumber": "01", - "duration_seconds": 240, - "id": "t2", - "source_path": f"{config.music_source_dir}/r1/02.m4a", - "tracktitle": "Track 2", - "tracknumber": "02", - "tracktotal": 2, - }, - ], - } - - -@pytest.mark.usefixtures("seeded_cache") -def test_dump_releases(config: Config) -> None: - assert json.loads(dump_all_releases(config)) == [ - { - "id": "r1", - "source_path": f"{config.music_source_dir}/r1", - "cover_image_path": None, - "added_at": "0000-01-01T00:00:00+00:00", - "releasetitle": "Release 1", - "releasetype": "album", - "releasedate": "2023", - "compositiondate": None, - "catalognumber": None, - "new": False, - "disctotal": 1, - "genres": ["Techno", "Deep House"], - "parent_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - ], - "labels": ["Silk Music"], - "originaldate": None, - "edition": None, - "secondary_genres": ["Rominimal", "Ambient"], - "parent_secondary_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - "Tech House", - ], - "descriptors": ["Warm", "Hot"], - "releaseartists": { - "main": [ - {"name": "Techno Man", "alias": False}, - {"name": "Bass Man", "alias": False}, - ], - "guest": [], - "remixer": [], - "producer": [], - "composer": [], - "conductor": [], - "djmixer": [], - }, - "tracks": [ - { - "trackartists": { - "composer": [], - "djmixer": [], - "guest": [], - "main": [ - {"alias": False, "name": "Techno Man"}, - {"alias": False, "name": "Bass Man"}, - ], - "producer": [], - "remixer": [], - "conductor": [], - }, - "discnumber": "01", - "duration_seconds": 120, - "id": "t1", - "source_path": f"{config.music_source_dir}/r1/01.m4a", - "tracktitle": "Track 1", - "tracknumber": "01", - "tracktotal": 2, - }, - { - "trackartists": { - "composer": [], - "djmixer": [], - "guest": [], - "main": [ - {"alias": False, "name": "Techno Man"}, - {"alias": False, "name": "Bass Man"}, - ], - "producer": [], - "remixer": [], - "conductor": [], - }, - "discnumber": "01", - "duration_seconds": 240, - "id": "t2", - "source_path": f"{config.music_source_dir}/r1/02.m4a", - "tracktitle": "Track 2", - "tracknumber": "02", - "tracktotal": 2, - }, - ], - }, - { - "id": "r2", - "source_path": f"{config.music_source_dir}/r2", - "cover_image_path": f"{config.music_source_dir}/r2/cover.jpg", - "added_at": "0000-01-01T00:00:00+00:00", - "releasetitle": "Release 2", - "releasetype": "album", - "releasedate": "2021", - "compositiondate": None, - "catalognumber": "DG-001", - "new": True, - "disctotal": 1, - "genres": ["Modern Classical"], - "parent_genres": ["Classical Music", "Western Classical Music"], - "labels": ["Native State"], - "originaldate": "2019", - "edition": "Deluxe", - "secondary_genres": ["Orchestral"], - "parent_secondary_genres": [ - "Classical Music", - "Western Classical Music", - ], - "descriptors": ["Wet"], - "releaseartists": { - "main": [{"name": "Violin Woman", "alias": False}], - "guest": [{"name": "Conductor Woman", "alias": False}], - "remixer": [], - "producer": [], - "composer": [], - "conductor": [], - "djmixer": [], - }, - "tracks": [ - { - "trackartists": { - "composer": [], - "djmixer": [], - "guest": [{"alias": False, "name": "Conductor Woman"}], - "main": [{"alias": False, "name": "Violin Woman"}], - "producer": [], - "remixer": [], - "conductor": [], - }, - "discnumber": "01", - "duration_seconds": 120, - "id": "t3", - "source_path": f"{config.music_source_dir}/r2/01.m4a", - "tracktitle": "Track 1", - "tracknumber": "01", - "tracktotal": 1, - } - ], - }, - { - "id": "r3", - "source_path": f"{config.music_source_dir}/r3", - "cover_image_path": None, - "added_at": "0000-01-01T00:00:00+00:00", - "releasetitle": "Release 3", - "releasetype": "album", - "releasedate": "2021-04-20", - "compositiondate": "1780", - "catalognumber": "DG-002", - "new": False, - "disctotal": 1, - "genres": [], - "parent_genres": [], - "labels": [], - "originaldate": None, - "edition": None, - "secondary_genres": [], - "parent_secondary_genres": [], - "descriptors": [], - "releaseartists": { - "main": [], - "guest": [], - "remixer": [], - "producer": [], - "composer": [], - "conductor": [], - "djmixer": [], - }, - "tracks": [ - { - "trackartists": { - "composer": [], - "djmixer": [], - "guest": [], - "main": [], - "producer": [], - "remixer": [], - "conductor": [], - }, - "discnumber": "01", - "duration_seconds": 120, - "id": "t4", - "source_path": f"{config.music_source_dir}/r3/01.m4a", - "tracktitle": "Track 1", - "tracknumber": "01", - "tracktotal": 1, - } - ], - }, - ] - - -@pytest.mark.usefixtures("seeded_cache") -def test_dump_releases_matcher(config: Config) -> None: - matcher = MetadataMatcher.parse("releasetitle:2$") - assert json.loads(dump_all_releases(config, matcher)) == [ - { - "id": "r2", - "source_path": f"{config.music_source_dir}/r2", - "cover_image_path": f"{config.music_source_dir}/r2/cover.jpg", - "added_at": "0000-01-01T00:00:00+00:00", - "releasetitle": "Release 2", - "releasetype": "album", - "releasedate": "2021", - "compositiondate": None, - "catalognumber": "DG-001", - "new": True, - "disctotal": 1, - "genres": ["Modern Classical"], - "parent_genres": ["Classical Music", "Western Classical Music"], - "labels": ["Native State"], - "originaldate": "2019", - "edition": "Deluxe", - "secondary_genres": ["Orchestral"], - "parent_secondary_genres": [ - "Classical Music", - "Western Classical Music", - ], - "descriptors": ["Wet"], - "releaseartists": { - "main": [{"name": "Violin Woman", "alias": False}], - "guest": [{"name": "Conductor Woman", "alias": False}], - "remixer": [], - "producer": [], - "composer": [], - "conductor": [], - "djmixer": [], - }, - "tracks": [ - { - "trackartists": { - "composer": [], - "djmixer": [], - "guest": [{"name": "Conductor Woman", "alias": False}], - "main": [{"name": "Violin Woman", "alias": False}], - "producer": [], - "remixer": [], - "conductor": [], - }, - "discnumber": "01", - "duration_seconds": 120, - "id": "t3", - "source_path": f"{config.music_source_dir}/r2/01.m4a", - "tracktitle": "Track 1", - "tracknumber": "01", - "tracktotal": 1, - } - ], - }, - ] - - def test_run_action_on_release(config: Config, source_dir: Path) -> None: action = MetadataAction.parse("tracktitle/replace:Bop") run_actions_on_release(config, "ilovecarly", [action]) diff --git a/rose/tracks.py b/rose/tracks.py index 07c1937..d7ab047 100644 --- a/rose/tracks.py +++ b/rose/tracks.py @@ -4,11 +4,11 @@ from __future__ import annotations -import json import logging from rose.audiotags import AudioTags from rose.cache import ( + Track, get_track, list_tracks, ) @@ -28,21 +28,10 @@ class TrackDoesNotExistError(RoseExpectedError): pass -def dump_track(c: Config, track_id: str) -> str: - track = get_track(c, track_id) - if track is None: - raise TrackDoesNotExistError(f"Track {track_id} does not exist") - return json.dumps(track.dump()) - - -def dump_all_tracks(c: Config, matcher: MetadataMatcher | None = None) -> str: - track_ids = None - if matcher: - track_ids = [t.id for t in fast_search_for_matching_tracks(c, matcher)] +def find_tracks_matching_rule(c: Config, matcher: MetadataMatcher) -> list[Track]: + track_ids = [t.id for t in fast_search_for_matching_tracks(c, matcher)] tracks = list_tracks(c, track_ids) - if matcher: - tracks = filter_track_false_positives_using_read_cache(matcher, tracks) - return json.dumps([t.dump() for t in tracks]) + return filter_track_false_positives_using_read_cache(matcher, tracks) def run_actions_on_track( diff --git a/rose/tracks_test.py b/rose/tracks_test.py index 118feab..b7d7cb6 100644 --- a/rose/tracks_test.py +++ b/rose/tracks_test.py @@ -1,12 +1,9 @@ -import json from pathlib import Path -import pytest - from rose.audiotags import AudioTags from rose.config import Config -from rose.rule_parser import MetadataAction, MetadataMatcher -from rose.tracks import dump_all_tracks, dump_track, run_actions_on_track +from rose.rule_parser import MetadataAction +from rose.tracks import run_actions_on_track def test_run_action_on_track(config: Config, source_dir: Path) -> None: @@ -16,416 +13,3 @@ def test_run_action_on_track(config: Config, source_dir: Path) -> None: run_actions_on_track(config, af.id, [action]) af = AudioTags.from_file(source_dir / "Test Release 2" / "01.m4a") assert af.tracktitle == "Bop" - - -@pytest.mark.usefixtures("seeded_cache") -def test_dump_tracks(config: Config) -> None: - assert json.loads(dump_all_tracks(config)) == [ - { - "trackartists": { - "composer": [], - "djmixer": [], - "guest": [], - "main": [ - {"alias": False, "name": "Techno Man"}, - {"alias": False, "name": "Bass Man"}, - ], - "producer": [], - "remixer": [], - "conductor": [], - }, - "discnumber": "01", - "disctotal": 1, - "duration_seconds": 120, - "id": "t1", - "source_path": f"{config.music_source_dir}/r1/01.m4a", - "tracktitle": "Track 1", - "tracknumber": "01", - "tracktotal": 2, - "added_at": "0000-01-01T00:00:00+00:00", - "release_id": "r1", - "releasetitle": "Release 1", - "releasetype": "album", - "releasedate": "2023", - "compositiondate": None, - "catalognumber": None, - "new": False, - "genres": ["Techno", "Deep House"], - "parent_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - ], - "labels": ["Silk Music"], - "originaldate": None, - "edition": None, - "secondary_genres": ["Rominimal", "Ambient"], - "parent_secondary_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - "Tech House", - ], - "descriptors": ["Warm", "Hot"], - "releaseartists": { - "main": [ - {"name": "Techno Man", "alias": False}, - {"name": "Bass Man", "alias": False}, - ], - "guest": [], - "remixer": [], - "conductor": [], - "producer": [], - "composer": [], - "djmixer": [], - }, - }, - { - "trackartists": { - "composer": [], - "djmixer": [], - "guest": [], - "main": [ - {"alias": False, "name": "Techno Man"}, - {"alias": False, "name": "Bass Man"}, - ], - "producer": [], - "remixer": [], - "conductor": [], - }, - "discnumber": "01", - "disctotal": 1, - "duration_seconds": 240, - "id": "t2", - "source_path": f"{config.music_source_dir}/r1/02.m4a", - "tracktitle": "Track 2", - "tracknumber": "02", - "tracktotal": 2, - "added_at": "0000-01-01T00:00:00+00:00", - "release_id": "r1", - "releasetitle": "Release 1", - "releasetype": "album", - "releasedate": "2023", - "compositiondate": None, - "catalognumber": None, - "new": False, - "genres": ["Techno", "Deep House"], - "parent_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - ], - "labels": ["Silk Music"], - "originaldate": None, - "edition": None, - "secondary_genres": ["Rominimal", "Ambient"], - "parent_secondary_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - "Tech House", - ], - "descriptors": ["Warm", "Hot"], - "releaseartists": { - "main": [ - {"name": "Techno Man", "alias": False}, - {"name": "Bass Man", "alias": False}, - ], - "guest": [], - "remixer": [], - "conductor": [], - "producer": [], - "composer": [], - "djmixer": [], - }, - }, - { - "trackartists": { - "composer": [], - "djmixer": [], - "guest": [{"alias": False, "name": "Conductor Woman"}], - "main": [{"alias": False, "name": "Violin Woman"}], - "producer": [], - "remixer": [], - "conductor": [], - }, - "discnumber": "01", - "disctotal": 1, - "duration_seconds": 120, - "id": "t3", - "source_path": f"{config.music_source_dir}/r2/01.m4a", - "tracktitle": "Track 1", - "tracknumber": "01", - "tracktotal": 1, - "added_at": "0000-01-01T00:00:00+00:00", - "release_id": "r2", - "releasetitle": "Release 2", - "releasetype": "album", - "releasedate": "2021", - "compositiondate": None, - "catalognumber": "DG-001", - "new": True, - "genres": ["Modern Classical"], - "parent_genres": ["Classical Music", "Western Classical Music"], - "labels": ["Native State"], - "originaldate": "2019", - "edition": "Deluxe", - "secondary_genres": ["Orchestral"], - "parent_secondary_genres": [ - "Classical Music", - "Western Classical Music", - ], - "descriptors": ["Wet"], - "releaseartists": { - "main": [{"name": "Violin Woman", "alias": False}], - "guest": [{"name": "Conductor Woman", "alias": False}], - "remixer": [], - "conductor": [], - "producer": [], - "composer": [], - "djmixer": [], - }, - }, - { - "trackartists": { - "composer": [], - "djmixer": [], - "guest": [], - "main": [], - "producer": [], - "remixer": [], - "conductor": [], - }, - "discnumber": "01", - "disctotal": 1, - "duration_seconds": 120, - "id": "t4", - "source_path": f"{config.music_source_dir}/r3/01.m4a", - "tracktitle": "Track 1", - "tracknumber": "01", - "tracktotal": 1, - "added_at": "0000-01-01T00:00:00+00:00", - "release_id": "r3", - "releasetitle": "Release 3", - "releasetype": "album", - "releasedate": "2021-04-20", - "compositiondate": "1780", - "catalognumber": "DG-002", - "new": False, - "genres": [], - "parent_genres": [], - "labels": [], - "originaldate": None, - "edition": None, - "secondary_genres": [], - "parent_secondary_genres": [], - "descriptors": [], - "releaseartists": { - "main": [], - "guest": [], - "remixer": [], - "conductor": [], - "producer": [], - "composer": [], - "djmixer": [], - }, - }, - ] - - -@pytest.mark.usefixtures("seeded_cache") -def test_dump_tracks_with_matcher(config: Config) -> None: - matcher = MetadataMatcher.parse("artist:Techno Man") - assert json.loads(dump_all_tracks(config, matcher)) == [ - { - "trackartists": { - "composer": [], - "djmixer": [], - "guest": [], - "main": [ - {"alias": False, "name": "Techno Man"}, - {"alias": False, "name": "Bass Man"}, - ], - "producer": [], - "remixer": [], - "conductor": [], - }, - "discnumber": "01", - "disctotal": 1, - "duration_seconds": 120, - "id": "t1", - "source_path": f"{config.music_source_dir}/r1/01.m4a", - "tracktitle": "Track 1", - "tracknumber": "01", - "tracktotal": 2, - "added_at": "0000-01-01T00:00:00+00:00", - "release_id": "r1", - "releasetitle": "Release 1", - "releasetype": "album", - "releasedate": "2023", - "compositiondate": None, - "catalognumber": None, - "new": False, - "genres": ["Techno", "Deep House"], - "parent_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - ], - "labels": ["Silk Music"], - "originaldate": None, - "edition": None, - "secondary_genres": ["Rominimal", "Ambient"], - "parent_secondary_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - "Tech House", - ], - "descriptors": ["Warm", "Hot"], - "releaseartists": { - "main": [ - {"name": "Techno Man", "alias": False}, - {"name": "Bass Man", "alias": False}, - ], - "guest": [], - "remixer": [], - "conductor": [], - "producer": [], - "composer": [], - "djmixer": [], - }, - }, - { - "trackartists": { - "composer": [], - "djmixer": [], - "guest": [], - "main": [ - {"alias": False, "name": "Techno Man"}, - {"alias": False, "name": "Bass Man"}, - ], - "producer": [], - "remixer": [], - "conductor": [], - }, - "discnumber": "01", - "disctotal": 1, - "duration_seconds": 240, - "id": "t2", - "source_path": f"{config.music_source_dir}/r1/02.m4a", - "tracktitle": "Track 2", - "tracknumber": "02", - "tracktotal": 2, - "added_at": "0000-01-01T00:00:00+00:00", - "release_id": "r1", - "releasetitle": "Release 1", - "releasetype": "album", - "releasedate": "2023", - "compositiondate": None, - "catalognumber": None, - "new": False, - "genres": ["Techno", "Deep House"], - "parent_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - ], - "labels": ["Silk Music"], - "originaldate": None, - "edition": None, - "secondary_genres": ["Rominimal", "Ambient"], - "parent_secondary_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - "Tech House", - ], - "descriptors": ["Warm", "Hot"], - "releaseartists": { - "main": [ - {"name": "Techno Man", "alias": False}, - {"name": "Bass Man", "alias": False}, - ], - "guest": [], - "remixer": [], - "conductor": [], - "producer": [], - "composer": [], - "djmixer": [], - }, - }, - ] - - -@pytest.mark.usefixtures("seeded_cache") -def test_dump_track(config: Config) -> None: - assert json.loads(dump_track(config, "t1")) == { - "trackartists": { - "composer": [], - "djmixer": [], - "guest": [], - "main": [ - {"alias": False, "name": "Techno Man"}, - {"alias": False, "name": "Bass Man"}, - ], - "producer": [], - "remixer": [], - "conductor": [], - }, - "discnumber": "01", - "disctotal": 1, - "duration_seconds": 120, - "id": "t1", - "source_path": f"{config.music_source_dir}/r1/01.m4a", - "tracktitle": "Track 1", - "tracknumber": "01", - "tracktotal": 2, - "added_at": "0000-01-01T00:00:00+00:00", - "release_id": "r1", - "releasetitle": "Release 1", - "releasetype": "album", - "releasedate": "2023", - "compositiondate": None, - "catalognumber": None, - "new": False, - "genres": ["Techno", "Deep House"], - "parent_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - ], - "labels": ["Silk Music"], - "originaldate": None, - "edition": None, - "secondary_genres": ["Rominimal", "Ambient"], - "parent_secondary_genres": [ - "Dance", - "Electronic", - "Electronic Dance Music", - "House", - "Tech House", - ], - "descriptors": ["Warm", "Hot"], - "releaseartists": { - "main": [ - {"name": "Techno Man", "alias": False}, - {"name": "Bass Man", "alias": False}, - ], - "guest": [], - "remixer": [], - "producer": [], - "composer": [], - "conductor": [], - "djmixer": [], - }, - } diff --git a/rose_cli/cli.py b/rose_cli/cli.py index 85d82d5..d0a590e 100644 --- a/rose_cli/cli.py +++ b/rose_cli/cli.py @@ -34,21 +34,12 @@ delete_playlist_cover_art, delete_release, delete_release_cover_art, - dump_all_collages, - dump_all_playlists, - dump_all_releases, - dump_all_tracks, - dump_collage, - dump_playlist, - dump_release, - dump_track, edit_collage_in_editor, edit_playlist_in_editor, edit_release, execute_metadata_rule, execute_stored_metadata_rules, maybe_invalidate_cache_database, - preview_path_templates, remove_release_from_collage, remove_track_from_playlist, rename_collage, @@ -60,6 +51,17 @@ toggle_release_new, update_cache, ) +from rose_cli.dump import ( + dump_all_collages, + dump_all_playlists, + dump_all_releases, + dump_all_tracks, + dump_collage, + dump_playlist, + dump_release, + dump_track, +) +from rose_cli.templates import preview_path_templates from rose_vfs import mount_virtualfs from rose_watchdog import start_watchdog diff --git a/rose_cli/cli_test.py b/rose_cli/cli_test.py index df49e7f..c9a4df0 100644 --- a/rose_cli/cli_test.py +++ b/rose_cli/cli_test.py @@ -6,8 +6,7 @@ import pytest from click.testing import CliRunner -from rose.audiotags import AudioTags -from rose.config import Config +from rose import AudioTags, Config from rose_cli.cli import ( Context, InvalidReleaseArgError, diff --git a/rose_cli/dump.py b/rose_cli/dump.py new file mode 100644 index 0000000..0193d63 --- /dev/null +++ b/rose_cli/dump.py @@ -0,0 +1,189 @@ +import json +from typing import Any + +from rose import ( + CollageDoesNotExistError, + Config, + MetadataMatcher, + PlaylistDoesNotExistError, + Release, + ReleaseDoesNotExistError, + Track, + TrackDoesNotExistError, + find_releases_matching_rule, + find_tracks_matching_rule, + get_collage, + get_collage_releases, + get_playlist, + get_playlist_tracks, + get_release, + get_track, + get_tracks_of_release, + get_tracks_of_releases, + list_collages, + list_playlists, + list_releases, + list_tracks, +) + + +def release_to_json(r: Release) -> dict[str, Any]: + return { + "id": r.id, + "source_path": str(r.source_path.resolve()), + "cover_image_path": str(r.cover_image_path.resolve()) if r.cover_image_path else None, + "added_at": r.added_at, + "releasetitle": r.releasetitle, + "releasetype": r.releasetype, + "releasedate": str(r.releasedate) if r.releasedate else None, + "originaldate": str(r.originaldate) if r.originaldate else None, + "compositiondate": str(r.compositiondate) if r.compositiondate else None, + "catalognumber": r.catalognumber, + "edition": r.edition, + "new": r.new, + "disctotal": r.disctotal, + "genres": r.genres, + "parent_genres": r.parent_genres, + "secondary_genres": r.secondary_genres, + "parent_secondary_genres": r.parent_secondary_genres, + "descriptors": r.descriptors, + "labels": r.labels, + "releaseartists": r.releaseartists.dump(), + } + + +def track_to_json(t: Track, with_release_info: bool = True) -> dict[str, Any]: + r = { + "id": t.id, + "source_path": str(t.source_path.resolve()), + "tracktitle": t.tracktitle, + "tracknumber": t.tracknumber, + "tracktotal": t.tracktotal, + "discnumber": t.discnumber, + "duration_seconds": t.duration_seconds, + "trackartists": t.trackartists.dump(), + } + if with_release_info: + r.update( + { + "release_id": t.release.id, + "added_at": t.release.added_at, + "releasetitle": t.release.releasetitle, + "releasetype": t.release.releasetype, + "disctotal": t.release.disctotal, + "releasedate": str(t.release.releasedate) if t.release.releasedate else None, + "originaldate": str(t.release.originaldate) if t.release.originaldate else None, + "compositiondate": str(t.release.compositiondate) + if t.release.compositiondate + else None, + "catalognumber": t.release.catalognumber, + "edition": t.release.edition, + "new": t.release.new, + "genres": t.release.genres, + "parent_genres": t.release.parent_genres, + "secondary_genres": t.release.secondary_genres, + "parent_secondary_genres": t.release.parent_secondary_genres, + "descriptors": t.release.descriptors, + "labels": t.release.labels, + "releaseartists": t.release.releaseartists.dump(), + } + ) + return r + + +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_of_release(c, release) + return json.dumps( + { + **release_to_json(release), + "tracks": [track_to_json(t, with_release_info=False) for t in tracks], + } + ) + + +def dump_all_releases(c: Config, matcher: MetadataMatcher | None = None) -> str: + releases = find_releases_matching_rule(c, matcher) if matcher else list_releases(c) + return json.dumps( + [ + { + **release_to_json(release), + "tracks": [track_to_json(t, with_release_info=False) for t in tracks], + } + for release, tracks in get_tracks_of_releases(c, releases) + ] + ) + + +def dump_track(c: Config, track_id: str) -> str: + track = get_track(c, track_id) + if track is None: + raise TrackDoesNotExistError(f"Track {track_id} does not exist") + return json.dumps(track_to_json(track)) + + +def dump_all_tracks(c: Config, matcher: MetadataMatcher | None = None) -> str: + tracks = find_tracks_matching_rule(c, matcher) if matcher else list_tracks(c) + return json.dumps([track_to_json(t) for t in tracks]) + + +def dump_collage(c: Config, collage_name: str) -> str: + 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(collage_releases): + releases.append({"position": idx + 1, **release_to_json(rls)}) + return json.dumps({"name": collage_name, "releases": releases}) + + +def dump_all_collages(c: Config) -> str: + out: list[dict[str, Any]] = [] + for name in list_collages(c): + 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(collage_releases): + releases.append({"position": idx + 1, **release_to_json(rls)}) + out.append({"name": name, "releases": releases}) + return json.dumps(out) + + +def dump_playlist(c: Config, playlist_name: str) -> str: + 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(playlist_tracks): + tracks.append({"position": idx + 1, **track_to_json(trk)}) + return json.dumps( + { + "name": playlist_name, + "cover_image_path": str(playlist.cover_path) if playlist.cover_path else None, + "tracks": tracks, + } + ) + + +def dump_all_playlists(c: Config) -> str: + out: list[dict[str, Any]] = [] + for name in list_playlists(c): + 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(playlist_tracks): + tracks.append({"position": idx + 1, **track_to_json(trk)}) + out.append( + { + "name": name, + "cover_image_path": str(playlist.cover_path) if playlist.cover_path else None, + "tracks": tracks, + } + ) + return json.dumps(out) diff --git a/rose_cli/dump_test.py b/rose_cli/dump_test.py new file mode 100644 index 0000000..ebd1103 --- /dev/null +++ b/rose_cli/dump_test.py @@ -0,0 +1,1203 @@ +import json + +import pytest + +from rose import Config, MetadataMatcher +from rose_cli.dump import ( + dump_all_collages, + dump_all_playlists, + dump_all_releases, + dump_all_tracks, + dump_collage, + dump_playlist, + dump_release, + dump_track, +) + + +@pytest.mark.usefixtures("seeded_cache") +def test_dump_release(config: Config) -> None: + assert json.loads(dump_release(config, "r1")) == { + "id": "r1", + "source_path": f"{config.music_source_dir}/r1", + "cover_image_path": None, + "added_at": "0000-01-01T00:00:00+00:00", + "releasetitle": "Release 1", + "releasetype": "album", + "releasedate": "2023", + "compositiondate": None, + "catalognumber": None, + "new": False, + "disctotal": 1, + "genres": ["Techno", "Deep House"], + "parent_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + ], + "labels": ["Silk Music"], + "originaldate": None, + "edition": None, + "secondary_genres": ["Rominimal", "Ambient"], + "parent_secondary_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + "Tech House", + ], + "descriptors": ["Warm", "Hot"], + "releaseartists": { + "main": [ + {"name": "Techno Man", "alias": False}, + {"name": "Bass Man", "alias": False}, + ], + "guest": [], + "remixer": [], + "producer": [], + "composer": [], + "conductor": [], + "djmixer": [], + }, + "tracks": [ + { + "trackartists": { + "composer": [], + "djmixer": [], + "guest": [], + "main": [ + {"alias": False, "name": "Techno Man"}, + {"alias": False, "name": "Bass Man"}, + ], + "producer": [], + "remixer": [], + "conductor": [], + }, + "discnumber": "01", + "duration_seconds": 120, + "id": "t1", + "source_path": f"{config.music_source_dir}/r1/01.m4a", + "tracktitle": "Track 1", + "tracknumber": "01", + "tracktotal": 2, + }, + { + "trackartists": { + "composer": [], + "djmixer": [], + "guest": [], + "main": [ + {"alias": False, "name": "Techno Man"}, + {"alias": False, "name": "Bass Man"}, + ], + "producer": [], + "remixer": [], + "conductor": [], + }, + "discnumber": "01", + "duration_seconds": 240, + "id": "t2", + "source_path": f"{config.music_source_dir}/r1/02.m4a", + "tracktitle": "Track 2", + "tracknumber": "02", + "tracktotal": 2, + }, + ], + } + + +@pytest.mark.usefixtures("seeded_cache") +def test_dump_releases(config: Config) -> None: + assert json.loads(dump_all_releases(config)) == [ + { + "id": "r1", + "source_path": f"{config.music_source_dir}/r1", + "cover_image_path": None, + "added_at": "0000-01-01T00:00:00+00:00", + "releasetitle": "Release 1", + "releasetype": "album", + "releasedate": "2023", + "compositiondate": None, + "catalognumber": None, + "new": False, + "disctotal": 1, + "genres": ["Techno", "Deep House"], + "parent_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + ], + "labels": ["Silk Music"], + "originaldate": None, + "edition": None, + "secondary_genres": ["Rominimal", "Ambient"], + "parent_secondary_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + "Tech House", + ], + "descriptors": ["Warm", "Hot"], + "releaseartists": { + "main": [ + {"name": "Techno Man", "alias": False}, + {"name": "Bass Man", "alias": False}, + ], + "guest": [], + "remixer": [], + "producer": [], + "composer": [], + "conductor": [], + "djmixer": [], + }, + "tracks": [ + { + "trackartists": { + "composer": [], + "djmixer": [], + "guest": [], + "main": [ + {"alias": False, "name": "Techno Man"}, + {"alias": False, "name": "Bass Man"}, + ], + "producer": [], + "remixer": [], + "conductor": [], + }, + "discnumber": "01", + "duration_seconds": 120, + "id": "t1", + "source_path": f"{config.music_source_dir}/r1/01.m4a", + "tracktitle": "Track 1", + "tracknumber": "01", + "tracktotal": 2, + }, + { + "trackartists": { + "composer": [], + "djmixer": [], + "guest": [], + "main": [ + {"alias": False, "name": "Techno Man"}, + {"alias": False, "name": "Bass Man"}, + ], + "producer": [], + "remixer": [], + "conductor": [], + }, + "discnumber": "01", + "duration_seconds": 240, + "id": "t2", + "source_path": f"{config.music_source_dir}/r1/02.m4a", + "tracktitle": "Track 2", + "tracknumber": "02", + "tracktotal": 2, + }, + ], + }, + { + "id": "r2", + "source_path": f"{config.music_source_dir}/r2", + "cover_image_path": f"{config.music_source_dir}/r2/cover.jpg", + "added_at": "0000-01-01T00:00:00+00:00", + "releasetitle": "Release 2", + "releasetype": "album", + "releasedate": "2021", + "compositiondate": None, + "catalognumber": "DG-001", + "new": True, + "disctotal": 1, + "genres": ["Modern Classical"], + "parent_genres": ["Classical Music", "Western Classical Music"], + "labels": ["Native State"], + "originaldate": "2019", + "edition": "Deluxe", + "secondary_genres": ["Orchestral"], + "parent_secondary_genres": [ + "Classical Music", + "Western Classical Music", + ], + "descriptors": ["Wet"], + "releaseartists": { + "main": [{"name": "Violin Woman", "alias": False}], + "guest": [{"name": "Conductor Woman", "alias": False}], + "remixer": [], + "producer": [], + "composer": [], + "conductor": [], + "djmixer": [], + }, + "tracks": [ + { + "trackartists": { + "composer": [], + "djmixer": [], + "guest": [{"alias": False, "name": "Conductor Woman"}], + "main": [{"alias": False, "name": "Violin Woman"}], + "producer": [], + "remixer": [], + "conductor": [], + }, + "discnumber": "01", + "duration_seconds": 120, + "id": "t3", + "source_path": f"{config.music_source_dir}/r2/01.m4a", + "tracktitle": "Track 1", + "tracknumber": "01", + "tracktotal": 1, + } + ], + }, + { + "id": "r3", + "source_path": f"{config.music_source_dir}/r3", + "cover_image_path": None, + "added_at": "0000-01-01T00:00:00+00:00", + "releasetitle": "Release 3", + "releasetype": "album", + "releasedate": "2021-04-20", + "compositiondate": "1780", + "catalognumber": "DG-002", + "new": False, + "disctotal": 1, + "genres": [], + "parent_genres": [], + "labels": [], + "originaldate": None, + "edition": None, + "secondary_genres": [], + "parent_secondary_genres": [], + "descriptors": [], + "releaseartists": { + "main": [], + "guest": [], + "remixer": [], + "producer": [], + "composer": [], + "conductor": [], + "djmixer": [], + }, + "tracks": [ + { + "trackartists": { + "composer": [], + "djmixer": [], + "guest": [], + "main": [], + "producer": [], + "remixer": [], + "conductor": [], + }, + "discnumber": "01", + "duration_seconds": 120, + "id": "t4", + "source_path": f"{config.music_source_dir}/r3/01.m4a", + "tracktitle": "Track 1", + "tracknumber": "01", + "tracktotal": 1, + } + ], + }, + ] + + +@pytest.mark.usefixtures("seeded_cache") +def test_dump_releases_matcher(config: Config) -> None: + matcher = MetadataMatcher.parse("releasetitle:2$") + assert json.loads(dump_all_releases(config, matcher)) == [ + { + "id": "r2", + "source_path": f"{config.music_source_dir}/r2", + "cover_image_path": f"{config.music_source_dir}/r2/cover.jpg", + "added_at": "0000-01-01T00:00:00+00:00", + "releasetitle": "Release 2", + "releasetype": "album", + "releasedate": "2021", + "compositiondate": None, + "catalognumber": "DG-001", + "new": True, + "disctotal": 1, + "genres": ["Modern Classical"], + "parent_genres": ["Classical Music", "Western Classical Music"], + "labels": ["Native State"], + "originaldate": "2019", + "edition": "Deluxe", + "secondary_genres": ["Orchestral"], + "parent_secondary_genres": [ + "Classical Music", + "Western Classical Music", + ], + "descriptors": ["Wet"], + "releaseartists": { + "main": [{"name": "Violin Woman", "alias": False}], + "guest": [{"name": "Conductor Woman", "alias": False}], + "remixer": [], + "producer": [], + "composer": [], + "conductor": [], + "djmixer": [], + }, + "tracks": [ + { + "trackartists": { + "composer": [], + "djmixer": [], + "guest": [{"name": "Conductor Woman", "alias": False}], + "main": [{"name": "Violin Woman", "alias": False}], + "producer": [], + "remixer": [], + "conductor": [], + }, + "discnumber": "01", + "duration_seconds": 120, + "id": "t3", + "source_path": f"{config.music_source_dir}/r2/01.m4a", + "tracktitle": "Track 1", + "tracknumber": "01", + "tracktotal": 1, + } + ], + }, + ] + + +@pytest.mark.usefixtures("seeded_cache") +def test_dump_tracks(config: Config) -> None: + assert json.loads(dump_all_tracks(config)) == [ + { + "trackartists": { + "composer": [], + "djmixer": [], + "guest": [], + "main": [ + {"alias": False, "name": "Techno Man"}, + {"alias": False, "name": "Bass Man"}, + ], + "producer": [], + "remixer": [], + "conductor": [], + }, + "discnumber": "01", + "disctotal": 1, + "duration_seconds": 120, + "id": "t1", + "source_path": f"{config.music_source_dir}/r1/01.m4a", + "tracktitle": "Track 1", + "tracknumber": "01", + "tracktotal": 2, + "added_at": "0000-01-01T00:00:00+00:00", + "release_id": "r1", + "releasetitle": "Release 1", + "releasetype": "album", + "releasedate": "2023", + "compositiondate": None, + "catalognumber": None, + "new": False, + "genres": ["Techno", "Deep House"], + "parent_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + ], + "labels": ["Silk Music"], + "originaldate": None, + "edition": None, + "secondary_genres": ["Rominimal", "Ambient"], + "parent_secondary_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + "Tech House", + ], + "descriptors": ["Warm", "Hot"], + "releaseartists": { + "main": [ + {"name": "Techno Man", "alias": False}, + {"name": "Bass Man", "alias": False}, + ], + "guest": [], + "remixer": [], + "conductor": [], + "producer": [], + "composer": [], + "djmixer": [], + }, + }, + { + "trackartists": { + "composer": [], + "djmixer": [], + "guest": [], + "main": [ + {"alias": False, "name": "Techno Man"}, + {"alias": False, "name": "Bass Man"}, + ], + "producer": [], + "remixer": [], + "conductor": [], + }, + "discnumber": "01", + "disctotal": 1, + "duration_seconds": 240, + "id": "t2", + "source_path": f"{config.music_source_dir}/r1/02.m4a", + "tracktitle": "Track 2", + "tracknumber": "02", + "tracktotal": 2, + "added_at": "0000-01-01T00:00:00+00:00", + "release_id": "r1", + "releasetitle": "Release 1", + "releasetype": "album", + "releasedate": "2023", + "compositiondate": None, + "catalognumber": None, + "new": False, + "genres": ["Techno", "Deep House"], + "parent_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + ], + "labels": ["Silk Music"], + "originaldate": None, + "edition": None, + "secondary_genres": ["Rominimal", "Ambient"], + "parent_secondary_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + "Tech House", + ], + "descriptors": ["Warm", "Hot"], + "releaseartists": { + "main": [ + {"name": "Techno Man", "alias": False}, + {"name": "Bass Man", "alias": False}, + ], + "guest": [], + "remixer": [], + "conductor": [], + "producer": [], + "composer": [], + "djmixer": [], + }, + }, + { + "trackartists": { + "composer": [], + "djmixer": [], + "guest": [{"alias": False, "name": "Conductor Woman"}], + "main": [{"alias": False, "name": "Violin Woman"}], + "producer": [], + "remixer": [], + "conductor": [], + }, + "discnumber": "01", + "disctotal": 1, + "duration_seconds": 120, + "id": "t3", + "source_path": f"{config.music_source_dir}/r2/01.m4a", + "tracktitle": "Track 1", + "tracknumber": "01", + "tracktotal": 1, + "added_at": "0000-01-01T00:00:00+00:00", + "release_id": "r2", + "releasetitle": "Release 2", + "releasetype": "album", + "releasedate": "2021", + "compositiondate": None, + "catalognumber": "DG-001", + "new": True, + "genres": ["Modern Classical"], + "parent_genres": ["Classical Music", "Western Classical Music"], + "labels": ["Native State"], + "originaldate": "2019", + "edition": "Deluxe", + "secondary_genres": ["Orchestral"], + "parent_secondary_genres": [ + "Classical Music", + "Western Classical Music", + ], + "descriptors": ["Wet"], + "releaseartists": { + "main": [{"name": "Violin Woman", "alias": False}], + "guest": [{"name": "Conductor Woman", "alias": False}], + "remixer": [], + "conductor": [], + "producer": [], + "composer": [], + "djmixer": [], + }, + }, + { + "trackartists": { + "composer": [], + "djmixer": [], + "guest": [], + "main": [], + "producer": [], + "remixer": [], + "conductor": [], + }, + "discnumber": "01", + "disctotal": 1, + "duration_seconds": 120, + "id": "t4", + "source_path": f"{config.music_source_dir}/r3/01.m4a", + "tracktitle": "Track 1", + "tracknumber": "01", + "tracktotal": 1, + "added_at": "0000-01-01T00:00:00+00:00", + "release_id": "r3", + "releasetitle": "Release 3", + "releasetype": "album", + "releasedate": "2021-04-20", + "compositiondate": "1780", + "catalognumber": "DG-002", + "new": False, + "genres": [], + "parent_genres": [], + "labels": [], + "originaldate": None, + "edition": None, + "secondary_genres": [], + "parent_secondary_genres": [], + "descriptors": [], + "releaseartists": { + "main": [], + "guest": [], + "remixer": [], + "conductor": [], + "producer": [], + "composer": [], + "djmixer": [], + }, + }, + ] + + +@pytest.mark.usefixtures("seeded_cache") +def test_dump_tracks_with_matcher(config: Config) -> None: + matcher = MetadataMatcher.parse("artist:Techno Man") + assert json.loads(dump_all_tracks(config, matcher)) == [ + { + "trackartists": { + "composer": [], + "djmixer": [], + "guest": [], + "main": [ + {"alias": False, "name": "Techno Man"}, + {"alias": False, "name": "Bass Man"}, + ], + "producer": [], + "remixer": [], + "conductor": [], + }, + "discnumber": "01", + "disctotal": 1, + "duration_seconds": 120, + "id": "t1", + "source_path": f"{config.music_source_dir}/r1/01.m4a", + "tracktitle": "Track 1", + "tracknumber": "01", + "tracktotal": 2, + "added_at": "0000-01-01T00:00:00+00:00", + "release_id": "r1", + "releasetitle": "Release 1", + "releasetype": "album", + "releasedate": "2023", + "compositiondate": None, + "catalognumber": None, + "new": False, + "genres": ["Techno", "Deep House"], + "parent_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + ], + "labels": ["Silk Music"], + "originaldate": None, + "edition": None, + "secondary_genres": ["Rominimal", "Ambient"], + "parent_secondary_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + "Tech House", + ], + "descriptors": ["Warm", "Hot"], + "releaseartists": { + "main": [ + {"name": "Techno Man", "alias": False}, + {"name": "Bass Man", "alias": False}, + ], + "guest": [], + "remixer": [], + "conductor": [], + "producer": [], + "composer": [], + "djmixer": [], + }, + }, + { + "trackartists": { + "composer": [], + "djmixer": [], + "guest": [], + "main": [ + {"alias": False, "name": "Techno Man"}, + {"alias": False, "name": "Bass Man"}, + ], + "producer": [], + "remixer": [], + "conductor": [], + }, + "discnumber": "01", + "disctotal": 1, + "duration_seconds": 240, + "id": "t2", + "source_path": f"{config.music_source_dir}/r1/02.m4a", + "tracktitle": "Track 2", + "tracknumber": "02", + "tracktotal": 2, + "added_at": "0000-01-01T00:00:00+00:00", + "release_id": "r1", + "releasetitle": "Release 1", + "releasetype": "album", + "releasedate": "2023", + "compositiondate": None, + "catalognumber": None, + "new": False, + "genres": ["Techno", "Deep House"], + "parent_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + ], + "labels": ["Silk Music"], + "originaldate": None, + "edition": None, + "secondary_genres": ["Rominimal", "Ambient"], + "parent_secondary_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + "Tech House", + ], + "descriptors": ["Warm", "Hot"], + "releaseartists": { + "main": [ + {"name": "Techno Man", "alias": False}, + {"name": "Bass Man", "alias": False}, + ], + "guest": [], + "remixer": [], + "conductor": [], + "producer": [], + "composer": [], + "djmixer": [], + }, + }, + ] + + +@pytest.mark.usefixtures("seeded_cache") +def test_dump_track(config: Config) -> None: + assert json.loads(dump_track(config, "t1")) == { + "trackartists": { + "composer": [], + "djmixer": [], + "guest": [], + "main": [ + {"alias": False, "name": "Techno Man"}, + {"alias": False, "name": "Bass Man"}, + ], + "producer": [], + "remixer": [], + "conductor": [], + }, + "discnumber": "01", + "disctotal": 1, + "duration_seconds": 120, + "id": "t1", + "source_path": f"{config.music_source_dir}/r1/01.m4a", + "tracktitle": "Track 1", + "tracknumber": "01", + "tracktotal": 2, + "added_at": "0000-01-01T00:00:00+00:00", + "release_id": "r1", + "releasetitle": "Release 1", + "releasetype": "album", + "releasedate": "2023", + "compositiondate": None, + "catalognumber": None, + "new": False, + "genres": ["Techno", "Deep House"], + "parent_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + ], + "labels": ["Silk Music"], + "originaldate": None, + "edition": None, + "secondary_genres": ["Rominimal", "Ambient"], + "parent_secondary_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + "Tech House", + ], + "descriptors": ["Warm", "Hot"], + "releaseartists": { + "main": [ + {"name": "Techno Man", "alias": False}, + {"name": "Bass Man", "alias": False}, + ], + "guest": [], + "remixer": [], + "producer": [], + "composer": [], + "conductor": [], + "djmixer": [], + }, + } + + +@pytest.mark.usefixtures("seeded_cache") +def test_dump_collage(config: Config) -> None: + out = dump_collage(config, "Rose Gold") + assert json.loads(out) == { + "name": "Rose Gold", + "releases": [ + { + "position": 1, + "id": "r1", + "source_path": f"{config.music_source_dir}/r1", + "cover_image_path": None, + "added_at": "0000-01-01T00:00:00+00:00", + "releasetitle": "Release 1", + "releasetype": "album", + "releasedate": "2023", + "compositiondate": None, + "catalognumber": None, + "new": False, + "disctotal": 1, + "genres": ["Techno", "Deep House"], + "parent_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + ], + "labels": ["Silk Music"], + "originaldate": None, + "edition": None, + "secondary_genres": ["Rominimal", "Ambient"], + "parent_secondary_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + "Tech House", + ], + "descriptors": ["Warm", "Hot"], + "releaseartists": { + "main": [ + {"name": "Techno Man", "alias": False}, + {"name": "Bass Man", "alias": False}, + ], + "guest": [], + "remixer": [], + "conductor": [], + "producer": [], + "composer": [], + "djmixer": [], + }, + }, + { + "position": 2, + "id": "r2", + "source_path": f"{config.music_source_dir}/r2", + "cover_image_path": f"{config.music_source_dir}/r2/cover.jpg", + "added_at": "0000-01-01T00:00:00+00:00", + "releasetitle": "Release 2", + "releasetype": "album", + "releasedate": "2021", + "compositiondate": None, + "catalognumber": "DG-001", + "new": True, + "disctotal": 1, + "genres": ["Modern Classical"], + "parent_genres": ["Classical Music", "Western Classical Music"], + "labels": ["Native State"], + "originaldate": "2019", + "edition": "Deluxe", + "secondary_genres": ["Orchestral"], + "parent_secondary_genres": [ + "Classical Music", + "Western Classical Music", + ], + "descriptors": ["Wet"], + "releaseartists": { + "main": [{"name": "Violin Woman", "alias": False}], + "guest": [{"name": "Conductor Woman", "alias": False}], + "remixer": [], + "conductor": [], + "producer": [], + "composer": [], + "djmixer": [], + }, + }, + ], + } + + +@pytest.mark.usefixtures("seeded_cache") +def test_dump_collages(config: Config) -> None: + out = dump_all_collages(config) + assert json.loads(out) == [ + { + "name": "Rose Gold", + "releases": [ + { + "position": 1, + "id": "r1", + "source_path": f"{config.music_source_dir}/r1", + "cover_image_path": None, + "added_at": "0000-01-01T00:00:00+00:00", + "releasetitle": "Release 1", + "releasetype": "album", + "releasedate": "2023", + "compositiondate": None, + "catalognumber": None, + "new": False, + "disctotal": 1, + "genres": ["Techno", "Deep House"], + "parent_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + ], + "labels": ["Silk Music"], + "originaldate": None, + "edition": None, + "secondary_genres": ["Rominimal", "Ambient"], + "parent_secondary_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + "Tech House", + ], + "descriptors": ["Warm", "Hot"], + "releaseartists": { + "main": [ + {"name": "Techno Man", "alias": False}, + {"name": "Bass Man", "alias": False}, + ], + "guest": [], + "remixer": [], + "conductor": [], + "producer": [], + "composer": [], + "djmixer": [], + }, + }, + { + "position": 2, + "id": "r2", + "source_path": f"{config.music_source_dir}/r2", + "cover_image_path": f"{config.music_source_dir}/r2/cover.jpg", + "added_at": "0000-01-01T00:00:00+00:00", + "releasetitle": "Release 2", + "releasetype": "album", + "releasedate": "2021", + "compositiondate": None, + "catalognumber": "DG-001", + "new": True, + "disctotal": 1, + "genres": ["Modern Classical"], + "parent_genres": ["Classical Music", "Western Classical Music"], + "labels": ["Native State"], + "originaldate": "2019", + "edition": "Deluxe", + "secondary_genres": ["Orchestral"], + "parent_secondary_genres": [ + "Classical Music", + "Western Classical Music", + ], + "descriptors": ["Wet"], + "releaseartists": { + "main": [{"name": "Violin Woman", "alias": False}], + "guest": [{"name": "Conductor Woman", "alias": False}], + "remixer": [], + "conductor": [], + "producer": [], + "composer": [], + "djmixer": [], + }, + }, + ], + }, + {"name": "Ruby Red", "releases": []}, + ] + + +@pytest.mark.usefixtures("seeded_cache") +def test_dump_playlist(config: Config) -> None: + out = dump_playlist(config, "Lala Lisa") + assert json.loads(out) == { + "name": "Lala Lisa", + "cover_image_path": f"{config.music_source_dir}/!playlists/Lala Lisa.jpg", + "tracks": [ + { + "position": 1, + "id": "t1", + "source_path": f"{config.music_source_dir}/r1/01.m4a", + "tracktitle": "Track 1", + "tracknumber": "01", + "tracktotal": 2, + "discnumber": "01", + "disctotal": 1, + "duration_seconds": 120, + "trackartists": { + "main": [ + {"name": "Techno Man", "alias": False}, + {"name": "Bass Man", "alias": False}, + ], + "guest": [], + "remixer": [], + "producer": [], + "composer": [], + "conductor": [], + "djmixer": [], + }, + "added_at": "0000-01-01T00:00:00+00:00", + "release_id": "r1", + "releasetitle": "Release 1", + "releasetype": "album", + "releasedate": "2023", + "compositiondate": None, + "catalognumber": None, + "new": False, + "genres": ["Techno", "Deep House"], + "parent_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + ], + "labels": ["Silk Music"], + "originaldate": None, + "edition": None, + "secondary_genres": ["Rominimal", "Ambient"], + "parent_secondary_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + "Tech House", + ], + "descriptors": ["Warm", "Hot"], + "releaseartists": { + "main": [ + {"name": "Techno Man", "alias": False}, + {"name": "Bass Man", "alias": False}, + ], + "guest": [], + "remixer": [], + "producer": [], + "composer": [], + "conductor": [], + "djmixer": [], + }, + }, + { + "position": 2, + "id": "t3", + "source_path": f"{config.music_source_dir}/r2/01.m4a", + "tracktitle": "Track 1", + "tracknumber": "01", + "tracktotal": 1, + "discnumber": "01", + "disctotal": 1, + "duration_seconds": 120, + "trackartists": { + "main": [{"name": "Violin Woman", "alias": False}], + "guest": [{"name": "Conductor Woman", "alias": False}], + "remixer": [], + "producer": [], + "composer": [], + "conductor": [], + "djmixer": [], + }, + "added_at": "0000-01-01T00:00:00+00:00", + "release_id": "r2", + "releasetitle": "Release 2", + "releasetype": "album", + "releasedate": "2021", + "compositiondate": None, + "catalognumber": "DG-001", + "new": True, + "genres": ["Modern Classical"], + "parent_genres": ["Classical Music", "Western Classical Music"], + "labels": ["Native State"], + "originaldate": "2019", + "edition": "Deluxe", + "secondary_genres": ["Orchestral"], + "parent_secondary_genres": [ + "Classical Music", + "Western Classical Music", + ], + "descriptors": ["Wet"], + "releaseartists": { + "main": [{"name": "Violin Woman", "alias": False}], + "guest": [{"name": "Conductor Woman", "alias": False}], + "remixer": [], + "producer": [], + "composer": [], + "conductor": [], + "djmixer": [], + }, + }, + ], + } + + +@pytest.mark.usefixtures("seeded_cache") +def test_dump_playlists(config: Config) -> None: + out = dump_all_playlists(config) + assert json.loads(out) == [ + { + "name": "Lala Lisa", + "cover_image_path": f"{config.music_source_dir}/!playlists/Lala Lisa.jpg", + "tracks": [ + { + "position": 1, + "id": "t1", + "source_path": f"{config.music_source_dir}/r1/01.m4a", + "tracktitle": "Track 1", + "tracknumber": "01", + "tracktotal": 2, + "discnumber": "01", + "disctotal": 1, + "duration_seconds": 120, + "trackartists": { + "main": [ + {"name": "Techno Man", "alias": False}, + {"name": "Bass Man", "alias": False}, + ], + "guest": [], + "remixer": [], + "producer": [], + "composer": [], + "conductor": [], + "djmixer": [], + }, + "added_at": "0000-01-01T00:00:00+00:00", + "release_id": "r1", + "releasetitle": "Release 1", + "releasetype": "album", + "releasedate": "2023", + "compositiondate": None, + "catalognumber": None, + "new": False, + "genres": ["Techno", "Deep House"], + "parent_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + ], + "labels": ["Silk Music"], + "originaldate": None, + "edition": None, + "secondary_genres": ["Rominimal", "Ambient"], + "parent_secondary_genres": [ + "Dance", + "Electronic", + "Electronic Dance Music", + "House", + "Tech House", + ], + "descriptors": ["Warm", "Hot"], + "releaseartists": { + "main": [ + {"name": "Techno Man", "alias": False}, + {"name": "Bass Man", "alias": False}, + ], + "guest": [], + "remixer": [], + "producer": [], + "composer": [], + "conductor": [], + "djmixer": [], + }, + }, + { + "position": 2, + "id": "t3", + "source_path": f"{config.music_source_dir}/r2/01.m4a", + "tracktitle": "Track 1", + "tracknumber": "01", + "tracktotal": 1, + "discnumber": "01", + "disctotal": 1, + "duration_seconds": 120, + "trackartists": { + "main": [{"name": "Violin Woman", "alias": False}], + "guest": [{"name": "Conductor Woman", "alias": False}], + "remixer": [], + "producer": [], + "composer": [], + "conductor": [], + "djmixer": [], + }, + "added_at": "0000-01-01T00:00:00+00:00", + "release_id": "r2", + "releasetitle": "Release 2", + "releasetype": "album", + "releasedate": "2021", + "compositiondate": None, + "catalognumber": "DG-001", + "new": True, + "genres": ["Modern Classical"], + "parent_genres": ["Classical Music", "Western Classical Music"], + "labels": ["Native State"], + "originaldate": "2019", + "edition": "Deluxe", + "secondary_genres": ["Orchestral"], + "parent_secondary_genres": [ + "Classical Music", + "Western Classical Music", + ], + "descriptors": ["Wet"], + "releaseartists": { + "main": [{"name": "Violin Woman", "alias": False}], + "guest": [{"name": "Conductor Woman", "alias": False}], + "remixer": [], + "producer": [], + "composer": [], + "conductor": [], + "djmixer": [], + }, + }, + ], + }, + {"name": "Turtle Rabbit", "cover_image_path": None, "tracks": []}, + ] diff --git a/rose_cli/templates.py b/rose_cli/templates.py index 0b65ead..31f7d74 100644 --- a/rose_cli/templates.py +++ b/rose_cli/templates.py @@ -1,7 +1,12 @@ import click -from rose import Config, PathTemplate, evaluate_release_template, evaluate_track_template -from rose.templates import get_sample_music +from rose import ( + Config, + PathTemplate, + evaluate_release_template, + evaluate_track_template, + get_sample_music, +) def preview_path_templates(c: Config) -> None: diff --git a/rose_cli/templates_test.py b/rose_cli/templates_test.py index 50a25f8..2dea070 100644 --- a/rose_cli/templates_test.py +++ b/rose_cli/templates_test.py @@ -1,7 +1,7 @@ import click from click.testing import CliRunner -from rose.config import Config +from rose import Config from rose_cli.templates import ( preview_path_templates, ) diff --git a/rose_vfs/virtualfs.py b/rose_vfs/virtualfs.py index e856fce..60f12c4 100644 --- a/rose_vfs/virtualfs.py +++ b/rose_vfs/virtualfs.py @@ -78,7 +78,9 @@ evaluate_track_template, genre_exists, get_collage, + get_collage_releases, get_playlist, + get_playlist_tracks, get_release, get_track, get_tracks_of_release, @@ -99,14 +101,12 @@ sanitize_filename, set_playlist_cover_art, set_release_cover_art, + track_within_playlist, + track_within_release, update_cache_for_releases, ) from rose.cache import ( - get_collage_releases, - get_playlist_tracks, list_releases_delete_this, - track_within_playlist, - track_within_release, ) logger = logging.getLogger(__name__) diff --git a/rose_vfs/virtualfs_test.py b/rose_vfs/virtualfs_test.py index 03f0f19..566b3a9 100644 --- a/rose_vfs/virtualfs_test.py +++ b/rose_vfs/virtualfs_test.py @@ -10,8 +10,7 @@ import pytest from conftest import retry_for_sec -from rose.audiotags import AudioTags -from rose.config import Config +from rose import AudioTags, Config from rose_vfs.virtualfs import mount_virtualfs, unmount_virtualfs R1_VNAME = "Techno Man & Bass Man - 2023. Release 1" diff --git a/rose_watchdog/watcher.py b/rose_watchdog/watcher.py index b7bb7be..5317a11 100644 --- a/rose_watchdog/watcher.py +++ b/rose_watchdog/watcher.py @@ -36,7 +36,8 @@ ) from watchdog.observers import Observer -from rose.cache import ( +from rose import ( + Config, update_cache_evict_nonexistent_collages, update_cache_evict_nonexistent_playlists, update_cache_evict_nonexistent_releases, @@ -44,7 +45,6 @@ update_cache_for_playlists, update_cache_for_releases, ) -from rose.config import Config logger = logging.getLogger(__name__) diff --git a/rose_watchdog/watcher_test.py b/rose_watchdog/watcher_test.py index 7441dd4..ba0af11 100644 --- a/rose_watchdog/watcher_test.py +++ b/rose_watchdog/watcher_test.py @@ -5,8 +5,8 @@ from multiprocessing import Process from conftest import TEST_COLLAGE_1, TEST_PLAYLIST_1, TEST_RELEASE_2, TEST_RELEASE_3, retry_for_sec +from rose import Config from rose.cache import connect -from rose.config import Config from rose_watchdog.watcher import start_watchdog