diff --git a/rose/cache.py b/rose/cache.py index f8a8dec..0de0d66 100644 --- a/rose/cache.py +++ b/rose/cache.py @@ -1685,31 +1685,33 @@ def list_releases_delete_this( return releases -def list_releases(c: Config, release_ids: list[str]) -> list[CachedRelease]: +def list_releases(c: Config, release_ids: list[str] | None = None) -> list[CachedRelease]: + """Fetch data associated with given release IDs. Pass None to fetch all.""" + query = """ + SELECT + id + , source_path + , cover_image_path + , added_at + , datafile_mtime + , title + , releasetype + , year + , multidisc + , new + , genres + , labels + , artist_names + , artist_roles + FROM releases_view + """ + args = [] + if release_ids is not None: + query += f" WHERE id IN ({','.join(['?']*len(release_ids))})" + args = release_ids + query += " ORDER BY source_path" with connect(c) as conn: - cursor = conn.execute( - f""" - SELECT - id - , source_path - , cover_image_path - , added_at - , datafile_mtime - , title - , releasetype - , year - , multidisc - , new - , genres - , labels - , artist_names - , artist_roles - FROM releases_view - WHERE id IN ({','.join(['?']*len(release_ids))}) - ORDER BY source_path - """, - release_ids, - ) + cursor = conn.execute(query, args) releases: list[CachedRelease] = [] for row in cursor: releases.append( @@ -1870,30 +1872,32 @@ def calculate_release_logtext( return logtext -def list_tracks(c: Config, track_ids: list[str]) -> list[CachedTrack]: +def list_tracks(c: Config, track_ids: list[str] | None = None) -> list[CachedTrack]: + """Fetch data associated with given track IDs. Pass None to fetch all.""" + query = """ + SELECT + t.id + , t.release_id + , t.source_path + , t.source_mtime + , t.title + , t.release_id + , t.tracknumber + , t.discnumber + , t.duration_seconds + , t.artist_names + , t.artist_roles + , r.multidisc + FROM tracks_view t + JOIN releases r ON r.id = t.release_id + """ + args = [] + if track_ids is not None: + query += f" WHERE t.id IN ({','.join(['?']*len(track_ids))})" + args = track_ids + query += " ORDER BY r.source_path, FORMAT('%4d.%4d', t.discnumber, t.tracknumber)" with connect(c) as conn: - cursor = conn.execute( - f""" - SELECT - t.id - , t.release_id - , t.source_path - , t.source_mtime - , t.title - , t.release_id - , t.tracknumber - , t.discnumber - , t.duration_seconds - , t.artist_names - , t.artist_roles - , r.multidisc - FROM tracks_view t - JOIN releases r ON r.id = t.release_id - WHERE t.id IN ({','.join(['?']*len(track_ids))}) - ORDER BY r.source_path, FORMAT('%4d.%4d', t.discnumber, t.tracknumber) - """, - track_ids, - ) + cursor = conn.execute(query, args) rval = [] for row in cursor: rval.append( diff --git a/rose/cache_test.py b/rose/cache_test.py index beececd..69c7c14 100644 --- a/rose/cache_test.py +++ b/rose/cache_test.py @@ -1015,8 +1015,7 @@ def test_update_cache_playlists_on_release_rename(config: Config) -> None: @pytest.mark.usefixtures("seeded_cache") def test_list_releases(config: Config) -> None: - releases = list_releases(config, ["r1", "r2", "r3"]) - assert releases == [ + expected = [ CachedRelease( datafile_mtime="999", id="r1", @@ -1064,6 +1063,9 @@ def test_list_releases(config: Config) -> None: ), ] + assert list_releases(config) == expected + assert list_releases(config, ["r1"]) == expected[:1] + @pytest.mark.usefixtures("seeded_cache") def test_get_release_and_associated_tracks(config: Config) -> None: @@ -1171,7 +1173,7 @@ def test_get_release_logtext(config: Config) -> None: @pytest.mark.usefixtures("seeded_cache") def test_list_tracks(config: Config) -> None: - assert list_tracks(config, ["t1", "t2"]) == [ + expected = [ CachedTrack( id="t1", source_path=config.music_source_dir / "r1" / "01.m4a", @@ -1196,8 +1198,35 @@ def test_list_tracks(config: Config) -> None: artists=ArtistMapping(main=[Artist("Techno Man"), Artist("Bass Man")]), release_multidisc=False, ), + CachedTrack( + id="t3", + source_path=config.music_source_dir / "r2" / "01.m4a", + source_mtime="999", + title="Track 1", + release_id="r2", + tracknumber="01", + discnumber="01", + duration_seconds=120, + artists=ArtistMapping(main=[Artist("Violin Woman")], guest=[Artist("Conductor Woman")]), + release_multidisc=False, + ), + CachedTrack( + id="t4", + source_path=config.music_source_dir / "r3" / "01.m4a", + source_mtime="999", + title="Track 1", + release_id="r3", + tracknumber="01", + discnumber="01", + duration_seconds=120, + artists=ArtistMapping(), + release_multidisc=False, + ), ] + assert list_tracks(config) == expected + assert list_tracks(config, ["t1", "t2"]) == expected[:2] + @pytest.mark.usefixtures("seeded_cache") def test_get_track(config: Config) -> None: diff --git a/rose/releases.py b/rose/releases.py index 7acb417..5d2b427 100644 --- a/rose/releases.py +++ b/rose/releases.py @@ -25,7 +25,7 @@ calculate_release_logtext, get_release, get_tracks_associated_with_release, - list_releases_delete_this, + list_releases, lock, release_lock_name, update_cache_evict_nonexistent_releases, @@ -70,7 +70,7 @@ def dump_release(c: Config, release_id: str) -> str: def dump_releases(c: Config) -> str: - return json.dumps([r.dump() for r in list_releases_delete_this(c)]) + return json.dumps([r.dump() for r in list_releases(c)]) def delete_release(c: Config, release_id: str) -> None: diff --git a/rose/tracks.py b/rose/tracks.py index c2849c7..a7d91d9 100644 --- a/rose/tracks.py +++ b/rose/tracks.py @@ -10,6 +10,7 @@ from rose.audiotags import AudioTags from rose.cache import ( get_track, + list_tracks, ) from rose.common import RoseExpectedError from rose.config import Config @@ -23,6 +24,17 @@ 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_tracks(c: Config) -> str: + return json.dumps([t.dump() for t in list_tracks(c)]) + + def run_actions_on_track( c: Config, track_id: str, @@ -37,10 +49,3 @@ def run_actions_on_track( raise TrackDoesNotExistError(f"Track {track_id} does not exist") audiotag = AudioTags.from_file(track.source_path) execute_metadata_actions(c, actions, [audiotag], dry_run=dry_run, confirm_yes=confirm_yes) - - -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()) diff --git a/rose/tracks_test.py b/rose/tracks_test.py index 2f8c598..a54b2d2 100644 --- a/rose/tracks_test.py +++ b/rose/tracks_test.py @@ -6,7 +6,7 @@ from rose.audiotags import AudioTags from rose.config import Config from rose.rule_parser import MetadataAction -from rose.tracks import dump_track, run_actions_on_track +from rose.tracks import dump_track, dump_tracks, run_actions_on_track def test_run_action_on_track(config: Config, source_dir: Path) -> None: @@ -18,6 +18,86 @@ def test_run_action_on_track(config: Config, source_dir: Path) -> None: assert af.title == "Bop" +@pytest.mark.usefixtures("seeded_cache") +def test_dump_tracks(config: Config) -> None: + assert json.loads(dump_tracks(config)) == [ + { + "artists": { + "composer": [], + "djmixer": [], + "guest": [], + "main": [ + {"alias": False, "name": "Techno Man"}, + {"alias": False, "name": "Bass Man"}, + ], + "producer": [], + "remixer": [], + }, + "discnumber": "01", + "duration_seconds": 120, + "id": "t1", + "release_id": "r1", + "source_path": f"{config.music_source_dir}/r1/01.m4a", + "title": "Track 1", + "tracknumber": "01", + }, + { + "artists": { + "composer": [], + "djmixer": [], + "guest": [], + "main": [ + {"alias": False, "name": "Techno Man"}, + {"alias": False, "name": "Bass Man"}, + ], + "producer": [], + "remixer": [], + }, + "discnumber": "01", + "duration_seconds": 240, + "id": "t2", + "release_id": "r1", + "source_path": f"{config.music_source_dir}/r1/02.m4a", + "title": "Track 2", + "tracknumber": "02", + }, + { + "artists": { + "composer": [], + "djmixer": [], + "guest": [{"alias": False, "name": "Conductor Woman"}], + "main": [{"alias": False, "name": "Violin Woman"}], + "producer": [], + "remixer": [], + }, + "discnumber": "01", + "duration_seconds": 120, + "id": "t3", + "release_id": "r2", + "source_path": f"{config.music_source_dir}/r2/01.m4a", + "title": "Track 1", + "tracknumber": "01", + }, + { + "artists": { + "composer": [], + "djmixer": [], + "guest": [], + "main": [], + "producer": [], + "remixer": [], + }, + "discnumber": "01", + "duration_seconds": 120, + "id": "t4", + "release_id": "r3", + "source_path": f"{config.music_source_dir}/r3/01.m4a", + "title": "Track 1", + "tracknumber": "01", + }, + ] + + @pytest.mark.usefixtures("seeded_cache") def test_dump_track(config: Config) -> None: assert json.loads(dump_track(config, "t1")) == {