From 0f3e1e43ce4fd3a30a5b3cd25e627b0327303393 Mon Sep 17 00:00:00 2001 From: blissful Date: Wed, 1 May 2024 18:09:00 -0400 Subject: [PATCH] add recently added releases and descriptor views; rename release templates --- conftest.py | 2 + docs/CONFIGURATION.md | 21 ++-- docs/TEMPLATES.md | 16 ++-- rose/__init__.py | 6 ++ rose/cache.py | 24 +++++ rose/cache_test.py | 14 +++ rose/collages.py | 6 +- rose/config.py | 42 +++++++- rose/config_test.py | 64 +++++++++---- rose/templates.py | 93 +++++++++++------- rose/templates_test.py | 66 ++++++++----- rose_cli/cli_test.py | 6 +- rose_vfs/virtualfs.py | 189 +++++++++++++++++++++++++----------- rose_vfs/virtualfs_test.py | 191 ++++++++++++++++++++----------------- 14 files changed, 501 insertions(+), 239 deletions(-) diff --git a/conftest.py b/conftest.py index 571f5d3..4e5b473 100644 --- a/conftest.py +++ b/conftest.py @@ -77,9 +77,11 @@ def config(isolated_dir: Path) -> Config: artist_aliases_parents_map={}, fuse_artists_whitelist=None, fuse_genres_whitelist=None, + fuse_descriptors_whitelist=None, fuse_labels_whitelist=None, fuse_artists_blacklist=None, fuse_genres_blacklist=None, + fuse_descriptors_blacklist=None, fuse_labels_blacklist=None, cover_art_stems=["cover", "folder", "art", "front"], valid_art_exts=["jpg", "jpeg", "png"], diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 084fc19..44626bb 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -59,24 +59,27 @@ artist_aliases = [ { artist = "tripleS", aliases = ["EVOLution", "LOVElution", "+(KR)ystal Eyes", "Acid Angel From Asia", "Acid Eyes"] }, ] -# Artists, genres, and labels to show in their respective top-level virtual -# filesystem directories. By # default, all artists, genres, and labels are -# shown. However, if this configuration parameter is specified, the list can be -# restricted to a specific few values. This is useful if you only care about a -# few specific genres and labels. +# Artists, genres, descriptors, and labels to show in their respective +# top-level virtual filesystem directories. By default, all artists, genres, +# and labels are shown. However, if this configuration parameter is specified, +# the list can be restricted to a specific few values. This is useful if you +# only care about a few specific genres and labels. fuse_artists_whitelist = [ "xxx", "yyy" ] fuse_genres_whitelist = [ "xxx", "yyy" ] +fuse_descriptors_whitelist = [ "xxx", "yyy" ] fuse_labels_whitelist = [ "xxx", "yyy" ] -# Artists, genres, and labels to hide from the virtual filesystem navigation. -# These options remove specific entities from their respective top-level -# virtual filesystem directories. This is useful if there are a few values you -# don't find useful, e.g. a random featuring artist or one super niche genre. +# Artists, genres, descriptors, and labels to hide from the virtual filesystem +# navigation. These options remove specific entities from their respective +# top-level virtual filesystem directories. This is useful if there are a few +# values you don't find useful, e.g. a random featuring artist or one super +# niche genre. # # These options are mutually exclusive with the fuse_*_whitelist options; if # both are specified for a given entity type, the configuration will not # validate. fuse_artists_blacklist = [ "xxx" ] fuse_genres_blacklist = [ "xxx" ] +fuse_descriptors_blacklist = [ "xxx" ] fuse_labels_blacklist = [ "xxx" ] # When Rosé scans a release directory, it looks for cover art that matches: diff --git a/docs/TEMPLATES.md b/docs/TEMPLATES.md index 5ada246..9dfe6ed 100644 --- a/docs/TEMPLATES.md +++ b/docs/TEMPLATES.md @@ -17,16 +17,20 @@ default.release = "..." default.track = "..." source.release = "..." source.track = "..." -all_releases.release = "..." -all_releases.track = "..." -new_releases.release = "..." -new_releases.track = "..." -recently_added_releases.release = "..." -recently_added_releases.track = "..." +releases.release = "..." +releases.track = "..." +releases_new.release = "..." +releases_new.track = "..." +releases_added_on.release = "..." +releases_added_on.track = "..." +releases_released_on.release = "..." +releases_released_on.track = "..." artists.release = "..." artists.track = "..." genres.release = "..." genres.track = "..." +descriptors.release = "..." +descriptors.track = "..." labels.release = "..." labels.track = "..." collages.release = "..." diff --git a/rose/__init__.py b/rose/__init__.py index a1e4548..48d9d16 100644 --- a/rose/__init__.py +++ b/rose/__init__.py @@ -19,6 +19,7 @@ calculate_release_logtext, calculate_track_logtext, collage_exists, + descriptor_exists, genre_exists, get_collage, get_path_of_track_in_playlist, @@ -30,6 +31,7 @@ label_exists, list_artists, list_collages, + list_descriptors, list_genres, list_labels, list_playlists, @@ -82,6 +84,7 @@ from rose.rule_parser import MetadataAction, MetadataMatcher, MetadataRule from rose.rules import execute_metadata_rule, execute_stored_metadata_rules from rose.templates import ( + PathContext, PathTemplate, eval_release_template, eval_track_template, @@ -119,8 +122,10 @@ "delete_playlist_cover_art", "delete_release", "delete_release_cover_art", + "descriptor_exists", "dump_all_collages", "dump_all_playlists", + "PathContext", "dump_all_releases", "dump_all_tracks", "dump_collage", @@ -145,6 +150,7 @@ "label_exists", "list_artists", "list_collages", + "list_descriptors", "list_genres", "list_labels", "list_playlists", diff --git a/rose/cache.py b/rose/cache.py index e49d99d..7bf3abd 100644 --- a/rose/cache.py +++ b/rose/cache.py @@ -1822,6 +1822,7 @@ def list_releases_delete_this( c: Config, artist_filter: str | None = None, genre_filter: str | None = None, + descriptor_filter: str | None = None, label_filter: str | None = None, new: bool | None = None, ) -> list[CachedRelease]: @@ -1856,6 +1857,14 @@ def list_releases_delete_this( """ args.extend(genres) args.extend(genres) + if descriptor_filter: + query += """ + AND EXISTS ( + SELECT * FROM releases_descriptors + WHERE release_id = id AND descriptor = ? + ) + """ + args.append(descriptor_filter) if label_filter: query += """ AND EXISTS ( @@ -2288,6 +2297,21 @@ def genre_exists(c: Config, genre: str) -> bool: return bool(cursor.fetchone()[0]) +def list_descriptors(c: Config) -> list[str]: + with connect(c) as conn: + cursor = conn.execute("SELECT DISTINCT descriptor FROM releases_descriptors") + return [row["descriptor"] for row in cursor] + + +def descriptor_exists(c: Config, descriptor: str) -> bool: + with connect(c) as conn: + cursor = conn.execute( + "SELECT EXISTS(SELECT * FROM releases_descriptors WHERE descriptor = ?)", + (descriptor,), + ) + return bool(cursor.fetchone()[0]) + + def list_labels(c: Config) -> list[str]: with connect(c) as conn: cursor = conn.execute("SELECT DISTINCT label FROM releases_labels") diff --git a/rose/cache_test.py b/rose/cache_test.py index 3eb343c..5ba3af9 100644 --- a/rose/cache_test.py +++ b/rose/cache_test.py @@ -20,6 +20,7 @@ artist_exists, collage_exists, connect, + descriptor_exists, genre_exists, get_collage, get_path_of_track_in_playlist, @@ -35,6 +36,7 @@ label_exists, list_artists, list_collages, + list_descriptors, list_genres, list_labels, list_playlists, @@ -1588,6 +1590,12 @@ def test_list_genres(config: Config) -> None: } +@pytest.mark.usefixtures("seeded_cache") +def test_list_descriptors(config: Config) -> None: + descriptors = list_descriptors(config) + assert set(descriptors) == {"Warm", "Hot", "Wet"} + + @pytest.mark.usefixtures("seeded_cache") def test_list_labels(config: Config) -> None: labels = list_labels(config) @@ -1856,6 +1864,12 @@ def test_genre_exists(config: Config) -> None: assert not genre_exists(config, "Lo-Fi House") +@pytest.mark.usefixtures("seeded_cache") +def test_descriptor_exists(config: Config) -> None: + assert descriptor_exists(config, "Warm") + assert not descriptor_exists(config, "Icy") + + @pytest.mark.usefixtures("seeded_cache") def test_label_exists(config: Config) -> None: assert label_exists(config, "Silk Music") diff --git a/rose/collages.py b/rose/collages.py index a9e151d..d77a019 100644 --- a/rose/collages.py +++ b/rose/collages.py @@ -103,11 +103,11 @@ def remove_release_from_collage(c: Config, collage_name: str, release_id: str) - with path.open("rb") as fp: data = tomllib.load(fp) old_releases = data.get("releases", []) - new_releases = [r for r in old_releases if r["uuid"] != release_id] - if old_releases == new_releases: + releases_new = [r for r in old_releases if r["uuid"] != release_id] + if old_releases == releases_new: logger.info(f"No-Op: Release {release_logtext} not in collage {collage_name}") return - data["releases"] = new_releases + data["releases"] = releases_new with path.open("wb") as fp: tomli_w.dump(data, fp) logger.info(f"Removed release {release_logtext} from collage {collage_name}") diff --git a/rose/config.py b/rose/config.py index 89ee4ee..1c2e6b7 100644 --- a/rose/config.py +++ b/rose/config.py @@ -71,9 +71,11 @@ class Config: fuse_artists_whitelist: list[str] | None fuse_genres_whitelist: list[str] | None + fuse_descriptors_whitelist: list[str] | None fuse_labels_whitelist: list[str] | None fuse_artists_blacklist: list[str] | None fuse_genres_blacklist: list[str] | None + fuse_descriptors_blacklist: list[str] | None fuse_labels_blacklist: list[str] | None cover_art_stems: list[str] @@ -202,6 +204,21 @@ def parse(cls, config_path_override: Path | None = None) -> Config: f"Invalid value for fuse_genres_whitelist in configuration file ({cfgpath}): {e}" ) from e + try: + fuse_descriptors_whitelist = data["fuse_descriptors_whitelist"] + del data["fuse_descriptors_whitelist"] + if not isinstance(fuse_descriptors_whitelist, list): + raise ValueError(f"Must be a list[str]: got {type(fuse_descriptors_whitelist)}") + for s in fuse_descriptors_whitelist: + if not isinstance(s, str): + raise ValueError(f"Each descriptor must be of type str: got {type(s)}") + except KeyError: + fuse_descriptors_whitelist = None + except ValueError as e: + raise InvalidConfigValueError( + f"Invalid value for fuse_descriptors_whitelist in configuration file ({cfgpath}): {e}" + ) from e + try: fuse_labels_whitelist = data["fuse_labels_whitelist"] del data["fuse_labels_whitelist"] @@ -247,6 +264,21 @@ def parse(cls, config_path_override: Path | None = None) -> Config: f"Invalid value for fuse_genres_blacklist in configuration file ({cfgpath}): {e}" ) from e + try: + fuse_descriptors_blacklist = data["fuse_descriptors_blacklist"] + del data["fuse_descriptors_blacklist"] + if not isinstance(fuse_descriptors_blacklist, list): + raise ValueError(f"Must be a list[str]: got {type(fuse_descriptors_blacklist)}") + for s in fuse_descriptors_blacklist: + if not isinstance(s, str): + raise ValueError(f"Each descriptor must be of type str: got {type(s)}") + except KeyError: + fuse_descriptors_blacklist = None + except ValueError as e: + raise InvalidConfigValueError( + f"Invalid value for fuse_descriptors_blacklist in configuration file ({cfgpath}): {e}" + ) from e + try: fuse_labels_blacklist = data["fuse_labels_blacklist"] del data["fuse_labels_blacklist"] @@ -417,11 +449,13 @@ def parse(cls, config_path_override: Path | None = None) -> Config: if tmpl_config := data.get("path_templates", None): for key in [ "source", - "all_releases", - "new_releases", - "recently_added_releases", + "releases", + "releases_new", + "releases_added_on", + "releases_released_on", "artists", "genres", + "descriptors", "labels", "collages", ]: @@ -475,9 +509,11 @@ def parse(cls, config_path_override: Path | None = None) -> Config: artist_aliases_parents_map=artist_aliases_parents_map, fuse_artists_whitelist=fuse_artists_whitelist, fuse_genres_whitelist=fuse_genres_whitelist, + fuse_descriptors_whitelist=fuse_descriptors_whitelist, fuse_labels_whitelist=fuse_labels_whitelist, fuse_artists_blacklist=fuse_artists_blacklist, fuse_genres_blacklist=fuse_genres_blacklist, + fuse_descriptors_blacklist=fuse_descriptors_blacklist, fuse_labels_blacklist=fuse_labels_blacklist, cover_art_stems=cover_art_stems, valid_art_exts=valid_art_exts, diff --git a/rose/config_test.py b/rose/config_test.py index ba79fae..5940fb1 100644 --- a/rose/config_test.py +++ b/rose/config_test.py @@ -52,8 +52,9 @@ def test_config_full() -> None: {{ artist = "Abakus", aliases = ["Cinnamon Chasers"] }}, {{ artist = "tripleS", aliases = ["EVOLution", "LOVElution", "+(KR)ystal Eyes", "Acid Angel From Asia", "Acid Eyes"] }}, ] - fuse_artists_blacklist = [ "xxx" ] - fuse_genres_blacklist = [ "yyy" ] + fuse_artists_blacklist = [ "www" ] + fuse_genres_blacklist = [ "xxx" ] + fuse_descriptors_blacklist = [ "yyy" ] fuse_labels_blacklist = [ "zzz" ] cover_art_stems = [ "aa", "bb" ] valid_art_exts = [ "tiff" ] @@ -75,12 +76,14 @@ def test_config_full() -> None: default.track = "{{{{ title }}}}" source.release = "{{{{ title }}}}" source.track = "{{{{ title }}}}" - all_releases.release = "{{{{ title }}}}" - all_releases.track = "{{{{ title }}}}" - new_releases.release = "{{{{ title }}}}" - new_releases.track = "{{{{ title }}}}" - recently_added_releases.release = "{{{{ title }}}}" - recently_added_releases.track = "{{{{ title }}}}" + releases.release = "{{{{ title }}}}" + releases.track = "{{{{ title }}}}" + releases_new.release = "{{{{ title }}}}" + releases_new.track = "{{{{ title }}}}" + releases_added_on.release = "{{{{ title }}}}" + releases_added_on.track = "{{{{ title }}}}" + releases_released_on.release = "{{{{ title }}}}" + releases_released_on.track = "{{{{ title }}}}" artists.release = "{{{{ title }}}}" artists.track = "{{{{ title }}}}" labels.release = "{{{{ title }}}}" @@ -117,9 +120,11 @@ def test_config_full() -> None: }, fuse_artists_whitelist=None, fuse_genres_whitelist=None, + fuse_descriptors_whitelist=None, fuse_labels_whitelist=None, - fuse_artists_blacklist=["xxx"], - fuse_genres_blacklist=["yyy"], + fuse_artists_blacklist=["www"], + fuse_genres_blacklist=["xxx"], + fuse_descriptors_blacklist=["yyy"], fuse_labels_blacklist=["zzz"], cover_art_stems=["aa", "bb"], valid_art_exts=["tiff"], @@ -129,13 +134,16 @@ def test_config_full() -> None: source=PathTemplatePair( release=PathTemplate("{{ title }}"), track=PathTemplate("{{ title }}") ), - all_releases=PathTemplatePair( + releases=PathTemplatePair( release=PathTemplate("{{ title }}"), track=PathTemplate("{{ title }}") ), - new_releases=PathTemplatePair( + releases_new=PathTemplatePair( release=PathTemplate("{{ title }}"), track=PathTemplate("{{ title }}") ), - recently_added_releases=PathTemplatePair( + releases_added_on=PathTemplatePair( + release=PathTemplate("{{ title }}"), track=PathTemplate("{{ title }}") + ), + releases_released_on=PathTemplatePair( release=PathTemplate("{{ title }}"), track=PathTemplate("{{ title }}") ), artists=PathTemplatePair( @@ -144,6 +152,9 @@ def test_config_full() -> None: genres=PathTemplatePair( release=PathTemplate("{{ title }}"), track=PathTemplate("{{ title }}") ), + descriptors=PathTemplatePair( + release=PathTemplate("{{ title }}"), track=PathTemplate("{{ title }}") + ), labels=PathTemplatePair( release=PathTemplate("{{ title }}"), track=PathTemplate("{{ title }}") ), @@ -198,18 +209,21 @@ def test_config_whitelist() -> None: """ music_source_dir = "~/.music-src" fuse_mount_dir = "~/music" - fuse_artists_whitelist = [ "xxx" ] - fuse_genres_whitelist = [ "yyy" ] + fuse_artists_whitelist = [ "www" ] + fuse_genres_whitelist = [ "xxx" ] + fuse_descriptors_whitelist = [ "yyy" ] fuse_labels_whitelist = [ "zzz" ] """ ) c = Config.parse(config_path_override=path) - assert c.fuse_artists_whitelist == ["xxx"] - assert c.fuse_genres_whitelist == ["yyy"] + assert c.fuse_artists_whitelist == ["www"] + assert c.fuse_genres_whitelist == ["xxx"] + assert c.fuse_descriptors_whitelist == ["yyy"] assert c.fuse_labels_whitelist == ["zzz"] assert c.fuse_artists_blacklist is None assert c.fuse_genres_blacklist is None + assert c.fuse_descriptors_blacklist is None assert c.fuse_labels_blacklist is None @@ -403,6 +417,22 @@ def write(x: str) -> None: == f"Invalid value for fuse_genres_blacklist in configuration file ({path}): Each genre must be of type str: got " ) + # fuse_descriptors_blacklist + write(config + '\nfuse_descriptors_blacklist = "lalala"') + with pytest.raises(InvalidConfigValueError) as excinfo: + Config.parse(config_path_override=path) + assert ( + str(excinfo.value) + == f"Invalid value for fuse_descriptors_blacklist in configuration file ({path}): Must be a list[str]: got " + ) + write(config + "\nfuse_descriptors_blacklist = [123]") + with pytest.raises(InvalidConfigValueError) as excinfo: + Config.parse(config_path_override=path) + assert ( + str(excinfo.value) + == f"Invalid value for fuse_descriptors_blacklist in configuration file ({path}): Each descriptor must be of type str: got " + ) + # fuse_labels_blacklist write(config + '\nfuse_labels_blacklist = "lalala"') with pytest.raises(InvalidConfigValueError) as excinfo: diff --git a/rose/templates.py b/rose/templates.py index 09e1952..dc7eb59 100644 --- a/rose/templates.py +++ b/rose/templates.py @@ -170,11 +170,13 @@ class PathTemplatePair: @dataclasses.dataclass class PathTemplateConfig: source: PathTemplatePair - all_releases: PathTemplatePair - new_releases: PathTemplatePair - recently_added_releases: PathTemplatePair + releases: PathTemplatePair + releases_new: PathTemplatePair + releases_added_on: PathTemplatePair + releases_released_on: PathTemplatePair artists: PathTemplatePair genres: PathTemplatePair + descriptors: PathTemplatePair labels: PathTemplatePair collages: PathTemplatePair playlists: PathTemplate @@ -186,14 +188,22 @@ def with_defaults( ) -> PathTemplateConfig: return PathTemplateConfig( source=deepcopy(default_pair), - all_releases=deepcopy(default_pair), - new_releases=deepcopy(default_pair), - recently_added_releases=PathTemplatePair( + releases=deepcopy(default_pair), + releases_new=deepcopy(default_pair), + releases_added_on=PathTemplatePair( release=PathTemplate("[{{ added_at[:10] }}] " + default_pair.release.text), track=deepcopy(default_pair.track), ), + releases_released_on=PathTemplatePair( + release=PathTemplate( + "[{{ originaldate or releasedate or '0000-00-00' }}] " + + default_pair.release.text + ), + track=deepcopy(default_pair.track), + ), artists=deepcopy(default_pair), genres=deepcopy(default_pair), + descriptors=deepcopy(default_pair), labels=deepcopy(default_pair), collages=PathTemplatePair( release=PathTemplate("{{ position }}. " + default_pair.release.text), @@ -219,18 +229,22 @@ def parse(self) -> None: _ = self.source.release.compiled key = "source.track" _ = self.source.track.compiled - key = "all_releases.release" - _ = self.all_releases.release.compiled - key = "all_releases.track" - _ = self.all_releases.track.compiled - key = "new_releases.release" - _ = self.new_releases.release.compiled - key = "new_releases.track" - _ = self.new_releases.track.compiled - key = "recently_added_releases.release" - _ = self.recently_added_releases.release.compiled - key = "recently_added_releases.track" - _ = self.recently_added_releases.track.compiled + key = "releases.release" + _ = self.releases.release.compiled + key = "releases.track" + _ = self.releases.track.compiled + key = "releases_new.release" + _ = self.releases_new.release.compiled + key = "releases_new.track" + _ = self.releases_new.track.compiled + key = "releases_added_on.release" + _ = self.releases_added_on.release.compiled + key = "releases_added_on.track" + _ = self.releases_added_on.track.compiled + key = "releases_released_on.release" + _ = self.releases_released_on.release.compiled + key = "releases_released_on.track" + _ = self.releases_released_on.track.compiled key = "artists.release" _ = self.artists.release.compiled key = "artists.track" @@ -239,6 +253,10 @@ def parse(self) -> None: _ = self.genres.release.compiled key = "genres.track" _ = self.genres.track.compiled + key = "descriptors.release" + _ = self.descriptors.release.compiled + key = "descriptors.track" + _ = self.descriptors.track.compiled key = "labels.release" _ = self.labels.release.compiled key = "labels.track" @@ -258,6 +276,7 @@ class PathContext: genre: str | None artist: str | None label: str | None + descriptor: str | None collage: str | None playlist: str | None @@ -352,28 +371,34 @@ def preview_path_templates(c: Config) -> None: _preview_release_template(c, "Source Directory - Release", c.path_templates.source.release) _preview_track_template(c, "Source Directory - Track", c.path_templates.source.track) click.echo() - _preview_release_template(c, "1. All Releases - Release", c.path_templates.all_releases.release) - _preview_track_template(c, "1. All Releases - Track", c.path_templates.all_releases.track) + _preview_release_template(c, "1. Releases - Release", c.path_templates.releases.release) + _preview_track_template(c, "1. Releases - Track", c.path_templates.releases.track) + click.echo() + _preview_release_template(c, "1. Releases (New) - Release", c.path_templates.releases_new.release) + _preview_track_template(c, "1. Releases (New) - Track", c.path_templates.releases_new.track) + click.echo() + _preview_release_template(c, "1. Releases (Added On) - Release", c.path_templates.releases_added_on.release) + _preview_track_template(c, "1. Releases (Added On) - Track", c.path_templates.releases_added_on.track) click.echo() - _preview_release_template(c, "2. New Releases - Release", c.path_templates.new_releases.release) - _preview_track_template(c, "2. New Releases - Track", c.path_templates.new_releases.track) + _preview_release_template(c, "1. Releases (Released On) - Release", c.path_templates.releases_added_on.release) + _preview_track_template(c, "1. Releases (Released On) - Track", c.path_templates.releases_added_on.track) click.echo() - _preview_release_template(c, "3. Recently Added Releases - Release", c.path_templates.recently_added_releases.release) - _preview_track_template(c, "3. Recently Added Releases - Track", c.path_templates.recently_added_releases.track) + _preview_release_template(c, "2. Artists - Release", c.path_templates.artists.release) + _preview_track_template(c, "2. Artists - Track", c.path_templates.artists.track) click.echo() - _preview_release_template(c, "4. Artists - Release", c.path_templates.artists.release) - _preview_track_template(c, "4. Artists - Track", c.path_templates.artists.track) + _preview_release_template(c, "3. Genres - Release", c.path_templates.genres.release) + _preview_track_template(c, "3. Genres - Track", c.path_templates.genres.track) click.echo() - _preview_release_template(c, "5. Genres - Release", c.path_templates.genres.release) - _preview_track_template(c, "5. Genres - Track", c.path_templates.genres.track) + _preview_release_template(c, "4. Descriptors - Release", c.path_templates.genres.release) + _preview_track_template(c, "4. Descriptors - Track", c.path_templates.genres.track) click.echo() - _preview_release_template(c, "6. Labels - Release", c.path_templates.labels.release) - _preview_track_template(c, "6. Labels - Track", c.path_templates.labels.track) + _preview_release_template(c, "5. Labels - Release", c.path_templates.labels.release) + _preview_track_template(c, "5. Labels - Track", c.path_templates.labels.track) click.echo() - _preview_release_template(c, "7. Collages - Release", c.path_templates.collages.release) - _preview_track_template(c, "7. Collages - Track", c.path_templates.collages.track) + _preview_release_template(c, "6. Collages - Release", c.path_templates.collages.release) + _preview_track_template(c, "6. Collages - Track", c.path_templates.collages.track) click.echo() - _preview_track_template(c, "8. Playlists - Track", c.path_templates.playlists) + _preview_track_template(c, "7. Playlists - Track", c.path_templates.playlists) # fmt: on @@ -553,7 +578,7 @@ def _preview_track_template(c: Config, label: str, template: PathTemplate) -> No / "Debussy - 1907. Images performed by Cleveland Orchestra under Pierre Boulez (1992)" / "01. Gigues: Modéré.opus", source_mtime="999", - tracktitle="Gigues: Modéré.opus", + tracktitle="Gigues: Modéré", tracknumber="1", tracktotal=6, discnumber="1", diff --git a/rose/templates_test.py b/rose/templates_test.py index 2482ade..cd6fe0e 100644 --- a/rose/templates_test.py +++ b/rose/templates_test.py @@ -131,75 +131,93 @@ def test_preview_templates(config: Config) -> None: Source Directory - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus - Sample 3: 01-01. Gigues: Modéré.opus.opus + Sample 3: 01-01. Gigues: Modéré.opus -1. All Releases - Release: +1. Releases - Release: Sample 1: Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: BTS - 2016. Young Forever (花樣年華) Sample 3: Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images -1. All Releases - Track: +1. Releases - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus - Sample 3: 01-01. Gigues: Modéré.opus.opus + Sample 3: 01-01. Gigues: Modéré.opus -2. New Releases - Release: +1. Releases (New) - Release: Sample 1: Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: BTS - 2016. Young Forever (花樣年華) Sample 3: Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images -2. New Releases - Track: +1. Releases (New) - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus - Sample 3: 01-01. Gigues: Modéré.opus.opus + Sample 3: 01-01. Gigues: Modéré.opus -3. Recently Added Releases - Release: +1. Releases (Added On) - Release: Sample 1: [2023-04-20] Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: [2023-06-09] BTS - 2016. Young Forever (花樣年華) Sample 3: [2023-09-06] Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images -3. Recently Added Releases - Track: +1. Releases (Added On) - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus - Sample 3: 01-01. Gigues: Modéré.opus.opus + Sample 3: 01-01. Gigues: Modéré.opus -4. Artists - Release: +1. Releases (Released On) - Release: + Sample 1: [2023-04-20] Kim Lip - 2017. Kim Lip - Single [NEW] + Sample 2: [2023-06-09] BTS - 2016. Young Forever (花樣年華) + Sample 3: [2023-09-06] Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images +1. Releases (Released On) - Track: + Sample 1: 01. Eclipse.opus + Sample 2: 02-05. House of Cards.opus + Sample 3: 01-01. Gigues: Modéré.opus + +2. Artists - Release: + Sample 1: Kim Lip - 2017. Kim Lip - Single [NEW] + Sample 2: BTS - 2016. Young Forever (花樣年華) + Sample 3: Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images +2. Artists - Track: + Sample 1: 01. Eclipse.opus + Sample 2: 02-05. House of Cards.opus + Sample 3: 01-01. Gigues: Modéré.opus + +3. Genres - Release: Sample 1: Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: BTS - 2016. Young Forever (花樣年華) Sample 3: Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images -4. Artists - Track: +3. Genres - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus - Sample 3: 01-01. Gigues: Modéré.opus.opus + Sample 3: 01-01. Gigues: Modéré.opus -5. Genres - Release: +4. Descriptors - Release: Sample 1: Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: BTS - 2016. Young Forever (花樣年華) Sample 3: Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images -5. Genres - Track: +4. Descriptors - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus - Sample 3: 01-01. Gigues: Modéré.opus.opus + Sample 3: 01-01. Gigues: Modéré.opus -6. Labels - Release: +5. Labels - Release: Sample 1: Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: BTS - 2016. Young Forever (花樣年華) Sample 3: Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images -6. Labels - Track: +5. Labels - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus - Sample 3: 01-01. Gigues: Modéré.opus.opus + Sample 3: 01-01. Gigues: Modéré.opus -7. Collages - Release: +6. Collages - Release: Sample 1: 1. Kim Lip - 2017. Kim Lip - Single [NEW] Sample 2: 2. BTS - 2016. Young Forever (花樣年華) Sample 3: 3. Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - 1992. Images -7. Collages - Track: +6. Collages - Track: Sample 1: 01. Eclipse.opus Sample 2: 02-05. House of Cards.opus - Sample 3: 01-01. Gigues: Modéré.opus.opus + Sample 3: 01-01. Gigues: Modéré.opus -8. Playlists - Track: +7. Playlists - Track: Sample 1: 1. Kim Lip - Eclipse.opus Sample 2: 2. BTS - House of Cards.opus - Sample 3: 3. Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - Gigues: Modéré.opus.opus + Sample 3: 3. Claude Debussy performed by Cleveland Orchestra under Pierre Boulez - Gigues: Modéré.opus """ ) diff --git a/rose_cli/cli_test.py b/rose_cli/cli_test.py index 48fa8b9..0c3cebd 100644 --- a/rose_cli/cli_test.py +++ b/rose_cli/cli_test.py @@ -76,7 +76,7 @@ def test_parse_track_id_from_path(config: Config, source_dir: Path) -> None: def test_parse_collage_name_from_path(config: Config, source_dir: Path) -> None: with start_virtual_fs(config): # Directory path is resolved. - path = str(config.fuse_mount_dir / "7. Collages" / "Rose Gold") + path = str(config.fuse_mount_dir / "6. Collages" / "Rose Gold") assert parse_collage_argument(path) == "Rose Gold" # File path is resolved. path = str(source_dir / "!collages" / "Rose Gold.toml") @@ -88,8 +88,8 @@ def test_parse_collage_name_from_path(config: Config, source_dir: Path) -> None: def test_parse_playlist_name_from_path(config: Config, source_dir: Path) -> None: with start_virtual_fs(config): # Directory path is resolved. - path = str(config.fuse_mount_dir / "8. Playlists" / "Lala Lisa") - assert parse_playlist_argument(path) == "Lala Lisa" + path = str(config.fuse_mount_dir / "7. Playlists" / "Lala Lisa") + assert parse_playlist_argument(path) # File path is resolved. path = str(source_dir / "!playlists" / "Lala Lisa.toml") assert parse_playlist_argument(path) == "Lala Lisa" diff --git a/rose_vfs/virtualfs.py b/rose_vfs/virtualfs.py index 2c8236d..978f91a 100644 --- a/rose_vfs/virtualfs.py +++ b/rose_vfs/virtualfs.py @@ -43,7 +43,6 @@ import logging import os import random -import re import stat import subprocess import tempfile @@ -62,6 +61,7 @@ CachedRelease, CachedTrack, Config, + PathContext, PathTemplate, RoseError, add_release_to_collage, @@ -76,6 +76,7 @@ delete_playlist, delete_playlist_cover_art, delete_release, + descriptor_exists, eval_release_template, eval_track_template, genre_exists, @@ -89,6 +90,7 @@ label_exists, list_artists, list_collages, + list_descriptors, list_genres, list_labels, list_playlists, @@ -104,7 +106,6 @@ update_cache_for_releases, ) from rose.cache import list_releases_delete_this -from rose.templates import PathContext logger = logging.getLogger(__name__) @@ -155,15 +156,6 @@ def get(self, key: K, default: T) -> V | T: return default -# In collages, playlists, and releases, we print directories with position of the release/track in -# the collage. When parsing, strip it out. Otherwise we will have to handle this parsing in every -# method. -POSITION_REGEX = re.compile(r"^([^.]+)\. ") -# In recently added, we print the date that the release was added to the library. When parsing, -# strip it out. -ADDED_AT_REGEX = re.compile(r"^\[[\d-]{10}\] ") - - @dataclass(frozen=True, slots=True) class VirtualPath: view: ( @@ -172,16 +164,19 @@ class VirtualPath: "Releases", "Artists", "Genres", + "Descriptors", "Labels", "Collages", "Playlists", "New", - "Recently Added", + "Added On", + "Released On", ] | None ) artist: str | None = None genre: str | None = None + descriptor: str | None = None label: str | None = None collage: str | None = None playlist: str | None = None @@ -195,6 +190,7 @@ def release_parent(self) -> VirtualPath: view=self.view, artist=self.artist, genre=self.genre, + descriptor=self.descriptor, label=self.label, collage=self.collage, ) @@ -206,6 +202,7 @@ def track_parent(self) -> VirtualPath: view=self.view, artist=self.artist, genre=self.genre, + descriptor=self.descriptor, label=self.label, collage=self.collage, playlist=self.playlist, @@ -222,6 +219,11 @@ def genre_parent(self) -> VirtualPath: """Parent path of a genre: Used as an input to the Sanitizer.""" return VirtualPath(view=self.view) + @property + def descriptor_parent(self) -> VirtualPath: + """Parent path of a descriptor: Used as an input to the Sanitizer.""" + return VirtualPath(view=self.view) + @property def label_parent(self) -> VirtualPath: """Parent path of a label: Used as an input to the Sanitizer.""" @@ -263,7 +265,7 @@ def parse(cls, path: Path) -> VirtualPath: return VirtualPath(view="Releases", release=parts[1], file=parts[2]) raise llfuse.FUSEError(errno.ENOENT) - if parts[0] == "2. Releases - New": + if parts[0] == "1. Releases - New": if len(parts) == 1: return VirtualPath(view="New") if len(parts) == 2: @@ -272,16 +274,25 @@ def parse(cls, path: Path) -> VirtualPath: return VirtualPath(view="New", release=parts[1], file=parts[2]) raise llfuse.FUSEError(errno.ENOENT) - if parts[0] == "3. Releases - Recently Added": + if parts[0] == "1. Releases - Added On": if len(parts) == 1: - return VirtualPath(view="Recently Added") + return VirtualPath(view="Added On") if len(parts) == 2: - return VirtualPath(view="Recently Added", release=parts[1]) + return VirtualPath(view="Added On", release=parts[1]) if len(parts) == 3: - return VirtualPath(view="Recently Added", release=parts[1], file=parts[2]) + return VirtualPath(view="Added On", release=parts[1], file=parts[2]) raise llfuse.FUSEError(errno.ENOENT) - if parts[0] == "4. Artists": + if parts[0] == "1. Releases - Released On": + if len(parts) == 1: + return VirtualPath(view="Released On") + if len(parts) == 2: + return VirtualPath(view="Released On", release=parts[1]) + if len(parts) == 3: + return VirtualPath(view="Released On", release=parts[1], file=parts[2]) + raise llfuse.FUSEError(errno.ENOENT) + + if parts[0] == "2. Artists": if len(parts) == 1: return VirtualPath(view="Artists") if len(parts) == 2: @@ -292,7 +303,7 @@ def parse(cls, path: Path) -> VirtualPath: return VirtualPath(view="Artists", artist=parts[1], release=parts[2], file=parts[3]) raise llfuse.FUSEError(errno.ENOENT) - if parts[0] == "5. Genres": + if parts[0] == "3. Genres": if len(parts) == 1: return VirtualPath(view="Genres") if len(parts) == 2: @@ -303,7 +314,20 @@ def parse(cls, path: Path) -> VirtualPath: return VirtualPath(view="Genres", genre=parts[1], release=parts[2], file=parts[3]) raise llfuse.FUSEError(errno.ENOENT) - if parts[0] == "6. Labels": + if parts[0] == "4. Descriptors": + if len(parts) == 1: + return VirtualPath(view="Descriptors") + if len(parts) == 2: + return VirtualPath(view="Descriptors", descriptor=parts[1]) + if len(parts) == 3: + return VirtualPath(view="Descriptors", descriptor=parts[1], release=parts[2]) + if len(parts) == 4: + return VirtualPath( + view="Descriptors", descriptor=parts[1], release=parts[2], file=parts[3] + ) + raise llfuse.FUSEError(errno.ENOENT) + + if parts[0] == "5. Labels": if len(parts) == 1: return VirtualPath(view="Labels") if len(parts) == 2: @@ -314,7 +338,7 @@ def parse(cls, path: Path) -> VirtualPath: return VirtualPath(view="Labels", label=parts[1], release=parts[2], file=parts[3]) raise llfuse.FUSEError(errno.ENOENT) - if parts[0] == "7. Collages": + if parts[0] == "6. Collages": if len(parts) == 1: return VirtualPath(view="Collages") if len(parts) == 2: @@ -327,7 +351,7 @@ def parse(cls, path: Path) -> VirtualPath: ) raise llfuse.FUSEError(errno.ENOENT) - if parts[0] == "8. Playlists": + if parts[0] == "7. Playlists": if len(parts) == 1: return VirtualPath(view="Playlists") if len(parts) == 2: @@ -399,15 +423,19 @@ def list_release_paths( # Determine the proper template. template = None if release_parent.view == "Releases": - template = self._config.path_templates.all_releases.release + template = self._config.path_templates.releases.release elif release_parent.view == "New": - template = self._config.path_templates.new_releases.release - elif release_parent.view == "Recently Added": - template = self._config.path_templates.recently_added_releases.release + template = self._config.path_templates.releases_new.release + elif release_parent.view == "Added On": + template = self._config.path_templates.releases_added_on.release + elif release_parent.view == "Released On": + template = self._config.path_templates.releases_released_on.release elif release_parent.view == "Artists": template = self._config.path_templates.artists.release elif release_parent.view == "Genres": template = self._config.path_templates.genres.release + elif release_parent.view == "Descriptors": + template = self._config.path_templates.descriptors.release elif release_parent.view == "Labels": template = self._config.path_templates.labels.release elif release_parent.view == "Collages": @@ -442,6 +470,12 @@ def list_release_paths( ) if release_parent.genre else None, + descriptor=self._sanitizer.unsanitize( + release_parent.descriptor, + release_parent.descriptor_parent, + ) + if release_parent.descriptor + else None, label=self._sanitizer.unsanitize( release_parent.label, release_parent.label_parent, @@ -500,15 +534,19 @@ def list_track_paths( # Determine the proper template. template = None if track_parent.view == "Releases": - template = self._config.path_templates.all_releases.track + template = self._config.path_templates.releases.track elif track_parent.view == "New": - template = self._config.path_templates.new_releases.track - elif track_parent.view == "Recently Added": - template = self._config.path_templates.recently_added_releases.track + template = self._config.path_templates.releases_new.track + elif track_parent.view == "Added On": + template = self._config.path_templates.releases_added_on.track + elif track_parent.view == "Released On": + template = self._config.path_templates.releases_released_on.track elif track_parent.view == "Artists": template = self._config.path_templates.artists.track elif track_parent.view == "Genres": template = self._config.path_templates.genres.track + elif track_parent.view == "Descriptors": + template = self._config.path_templates.descriptors.track elif track_parent.view == "Labels": template = self._config.path_templates.labels.track elif track_parent.view == "Collages": @@ -542,6 +580,12 @@ def list_track_paths( ) if track_parent.genre else None, + descriptor=self._sanitizer.unsanitize( + track_parent.descriptor, + track_parent.descriptor_parent, + ) + if track_parent.descriptor + else None, label=self._sanitizer.unsanitize( track_parent.label, track_parent.label_parent, @@ -668,6 +712,8 @@ def __init__(self, config: Config): self._artist_b = None self._genre_w = None self._genre_b = None + self._descriptor_w = None + self._descriptor_b = None self._label_w = None self._label_b = None @@ -679,6 +725,10 @@ def __init__(self, config: Config): self._genre_w = set(config.fuse_genres_whitelist) if config.fuse_genres_blacklist: self._genre_b = set(config.fuse_genres_blacklist) + if config.fuse_descriptors_whitelist: + self._descriptor_w = set(config.fuse_descriptors_whitelist) + if config.fuse_descriptors_blacklist: + self._descriptor_b = set(config.fuse_descriptors_blacklist) if config.fuse_labels_whitelist: self._label_w = set(config.fuse_labels_whitelist) if config.fuse_labels_blacklist: @@ -698,6 +748,13 @@ def genre(self, genre: str) -> bool: return genre not in self._genre_b return True + def descriptor(self, descriptor: str) -> bool: + if self._descriptor_w: + return descriptor in self._descriptor_w + elif self._descriptor_b: + return descriptor not in self._descriptor_b + return True + def label(self, label: str) -> bool: if self._label_w: return label in self._label_w @@ -869,7 +926,7 @@ def _getattr_release(self, p: VirtualPath) -> dict[str, Any]: def getattr(self, p: VirtualPath) -> dict[str, Any]: logger.debug(f"LOGICAL: Received getattr for {p=}") - # 8. Playlists + # 7. Playlists if p.playlist: if not playlist_exists(self.config, p.playlist): raise llfuse.FUSEError(errno.ENOENT) @@ -883,7 +940,7 @@ def getattr(self, p: VirtualPath) -> dict[str, Any]: raise llfuse.FUSEError(errno.ENOENT) return self.stat("dir") - # 7. Collages + # 6. Collages if p.collage: if not collage_exists(self.config, p.collage): raise llfuse.FUSEError(errno.ENOENT) @@ -891,37 +948,43 @@ def getattr(self, p: VirtualPath) -> dict[str, Any]: return self._getattr_release(p) return self.stat("dir") - # 6. Labels + # 5. Labels if p.label: - if not label_exists( - self.config, self.sanitizer.unsanitize(p.label, p.label_parent) - ) or not self.can_show.label(self.sanitizer.unsanitize(p.label, p.label_parent)): + 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: return self._getattr_release(p) return self.stat("dir") - # 5. Genres + # 4. Descriptors + if p.descriptor: + 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: + return self._getattr_release(p) + return self.stat("dir") + + # 3. Genres if p.genre: - if not genre_exists( - self.config, self.sanitizer.unsanitize(p.genre, p.genre_parent) - ) or not self.can_show.genre(self.sanitizer.unsanitize(p.genre, p.genre_parent)): + 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: return self._getattr_release(p) return self.stat("dir") - # 4. Artists + # 2. Artists if p.artist: - if not artist_exists( - self.config, self.sanitizer.unsanitize(p.artist, p.artist_parent) - ) or not self.can_show.artist(self.sanitizer.unsanitize(p.artist, p.artist_parent)): + 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: return self._getattr_release(p) return self.stat("dir") - # {1,2,3}. Releases + # 1. Releases if p.release: return self._getattr_release(p) @@ -949,13 +1012,15 @@ def readdir(self, p: VirtualPath) -> Iterator[tuple[str, dict[str, Any]]]: if p.view == "Root": yield from [ ("1. Releases", self.stat("dir")), - ("2. Releases - New", self.stat("dir")), - ("3. Releases - Recently Added", self.stat("dir")), - ("4. Artists", self.stat("dir")), - ("5. Genres", self.stat("dir")), - ("6. Labels", self.stat("dir")), - ("7. Collages", self.stat("dir")), - ("8. Playlists", self.stat("dir")), + ("1. Releases - New", self.stat("dir")), + ("1. Releases - Added On", self.stat("dir")), + ("1. Releases - Released On", self.stat("dir")), + ("2. Artists", self.stat("dir")), + ("3. Genres", self.stat("dir")), + ("4. Descriptors", self.stat("dir")), + ("5. Labels", self.stat("dir")), + ("6. Collages", self.stat("dir")), + ("7. Playlists", self.stat("dir")), ] return @@ -972,13 +1037,20 @@ def readdir(self, p: VirtualPath) -> Iterator[tuple[str, dict[str, Any]]]: return raise llfuse.FUSEError(errno.ENOENT) - if p.artist or p.genre or p.label or p.view in ["Releases", "New", "Recently Added"]: + if ( + p.artist + or p.genre + or p.descriptor + or p.label + or p.view in ["Releases", "New", "Added On", "Released On"] + ): # fmt: off releases = list_releases_delete_this( self.config, artist_filter=self.sanitizer.unsanitize(p.artist, p.artist_parent) if p.artist else None, - genre_filter=self.sanitizer.unsanitize(p.genre, p.artist_parent) if p.genre else None, - label_filter=self.sanitizer.unsanitize(p.label, p.artist_parent) if p.label else None, + genre_filter=self.sanitizer.unsanitize(p.genre, p.genre_parent) if p.genre else None, + descriptor_filter=self.sanitizer.unsanitize(p.descriptor, p.descriptor_parent) if p.descriptor else None, + label_filter=self.sanitizer.unsanitize(p.label, p.label_parent) if p.label else None, new=True if p.view == "New" else None, ) # fmt: on @@ -1000,6 +1072,13 @@ def readdir(self, p: VirtualPath) -> Iterator[tuple[str, dict[str, Any]]]: yield self.sanitizer.sanitize(genre), self.stat("dir") return + if p.view == "Descriptors": + for descriptor in list_descriptors(self.config): + if not self.can_show.descriptor(descriptor): + continue + yield self.sanitizer.sanitize(descriptor), self.stat("dir") + return + if p.view == "Labels": for label in list_labels(self.config): if not self.can_show.label(label): diff --git a/rose_vfs/virtualfs_test.py b/rose_vfs/virtualfs_test.py index d214f11..1410089 100644 --- a/rose_vfs/virtualfs_test.py +++ b/rose_vfs/virtualfs_test.py @@ -59,64 +59,79 @@ def can_read(p: Path) -> bool: assert (root / "1. Releases" / R2_VNAME / ".rose.r2.toml").is_file() assert can_read(root / "1. Releases" / R2_VNAME / ".rose.r2.toml") - assert (root / "2. Releases - New").is_dir() - assert (root / "2. Releases - New" / R3_VNAME).is_dir() - assert not (root / "2. Releases - New" / R2_VNAME).exists() - assert (root / "2. Releases - New" / R3_VNAME / "01. Track 1.m4a").is_file() - assert not (root / "2. Releases - New" / R3_VNAME / "lalala").exists() - - assert (root / "3. Releases - Recently Added").is_dir() - assert (root / "3. Releases - Recently Added" / f"[0000-01-01] {R2_VNAME}").exists() - assert not (root / "3. Releases - Recently Added" / R2_VNAME).exists() - assert (root / "3. Releases - Recently Added" / f"[0000-01-01] {R2_VNAME}" / "01. Track 1 (feat. Conductor Woman).m4a").is_file() - assert not (root / "3. Releases - Recently Added" / R2_VNAME / "lalala").exists() - - assert (root / "4. Artists").is_dir() - assert (root / "4. Artists" / "Bass Man").is_dir() - assert not (root / "4. Artists" / "lalala").exists() - assert (root / "4. Artists" / "Bass Man" / R1_VNAME).is_dir() - assert not (root / "4. Artists" / "Bass Man" / "lalala").exists() - assert (root / "4. Artists" / "Bass Man" / R1_VNAME / "01. Track 1.m4a").is_file() - assert not (root / "4. Artists" / "Bass Man" / R1_VNAME / "lalala.m4a").exists() - assert can_read(root / "4. Artists" / "Bass Man" / R1_VNAME / "01. Track 1.m4a") - - assert (root / "5. Genres").is_dir() - assert (root / "5. Genres" / "Techno").is_dir() - assert not (root / "5. Genres" / "lalala").exists() - assert (root / "5. Genres" / "Techno" / R1_VNAME).is_dir() - assert not (root / "5. Genres" / "Techno" / "lalala").exists() - assert (root / "5. Genres" / "Techno" / R1_VNAME / "01. Track 1.m4a").is_file() - assert not (root / "5. Genres" / "Techno" / R1_VNAME / "lalala.m4a").exists() - assert can_read(root / "5. Genres" / "Techno" / R1_VNAME / "01. Track 1.m4a") - - assert (root / "6. Labels").is_dir() - assert (root / "6. Labels" / "Silk Music").is_dir() - assert not (root / "6. Labels" / "lalala").exists() - assert (root / "6. Labels" / "Silk Music" / R1_VNAME).is_dir() - assert not (root / "6. Labels" / "Silk Music" / "lalala").exists() - assert (root / "6. Labels" / "Silk Music" / R1_VNAME / "01. Track 1.m4a").is_file() - assert not (root / "6. Labels" / "Silk Music" / R1_VNAME / "lalala").exists() - assert can_read(root / "6. Labels" / "Silk Music" / R1_VNAME / "01. Track 1.m4a") - - assert (root / "7. Collages").is_dir() - assert (root / "7. Collages" / "Rose Gold").is_dir() - assert (root / "7. Collages" / "Ruby Red").is_dir() - assert not (root / "7. Collages" / "lalala").exists() - assert (root / "7. Collages" / "Rose Gold" / f"1. {R1_VNAME}").is_dir() - assert not (root / "7. Collages" / "Rose Gold" / "lalala").exists() - assert (root / "7. Collages" / "Rose Gold" / f"1. {R1_VNAME}" / "01. Track 1.m4a").is_file() - assert not (root / "7. Collages" / "Rose Gold" / f"1. {R1_VNAME}" / "lalala").exists() - assert can_read(root / "7. Collages" / "Rose Gold" / f"1. {R1_VNAME}" / "01. Track 1.m4a") - - assert (root / "8. Playlists").is_dir() - assert (root / "8. Playlists" / "Lala Lisa").is_dir() - assert (root / "8. Playlists" / "Turtle Rabbit").is_dir() - assert not (root / "8. Playlists" / "lalala").exists() - assert (root / "8. Playlists" / "Lala Lisa" / "1. Techno Man & Bass Man - Track 1.m4a").is_file() - assert (root / "8. Playlists" / "Lala Lisa" / "cover.jpg").is_file() - assert not (root / "8. Playlists" / "Lala Lisa" / "lalala").exists() - assert can_read(root / "8. Playlists" / "Lala Lisa" / "1. Techno Man & Bass Man - Track 1.m4a") - assert can_read(root / "8. Playlists" / "Lala Lisa" / "cover.jpg") + assert (root / "1. Releases - New").is_dir() + assert (root / "1. Releases - New" / R3_VNAME).is_dir() + assert not (root / "1. Releases - New" / R2_VNAME).exists() + assert (root / "1. Releases - New" / R3_VNAME / "01. Track 1.m4a").is_file() + assert not (root / "1. Releases - New" / R3_VNAME / "lalala").exists() + + assert (root / "1. Releases - Added On").is_dir() + assert (root / "1. Releases - Added On" / f"[0000-01-01] {R2_VNAME}").exists() + assert not (root / "1. Releases - Added On" / R2_VNAME).exists() + assert (root / "1. Releases - Added On" / f"[0000-01-01] {R2_VNAME}" / "01. Track 1 (feat. Conductor Woman).m4a").is_file() + assert not (root / "1. Releases - Added On" / R2_VNAME / "lalala").exists() + + assert (root / "1. Releases - Released On").is_dir() + assert (root / "1. Releases - Released On" / f"[2019] {R2_VNAME}").exists() + assert not (root / "1. Releases - Released On" / R2_VNAME).exists() + assert (root / "1. Releases - Released On" / f"[2019] {R2_VNAME}" / "01. Track 1 (feat. Conductor Woman).m4a").is_file() + assert not (root / "1. Releases - Released On" / R2_VNAME / "lalala").exists() + + assert (root / "2. Artists").is_dir() + assert (root / "2. Artists" / "Bass Man").is_dir() + assert not (root / "2. Artists" / "lalala").exists() + assert (root / "2. Artists" / "Bass Man" / R1_VNAME).is_dir() + assert not (root / "2. Artists" / "Bass Man" / "lalala").exists() + assert (root / "2. Artists" / "Bass Man" / R1_VNAME / "01. Track 1.m4a").is_file() + assert not (root / "2. Artists" / "Bass Man" / R1_VNAME / "lalala.m4a").exists() + assert can_read(root / "2. Artists" / "Bass Man" / R1_VNAME / "01. Track 1.m4a") + + assert (root / "3. Genres").is_dir() + assert (root / "3. Genres" / "Techno").is_dir() + assert not (root / "3. Genres" / "lalala").exists() + assert (root / "3. Genres" / "Techno" / R1_VNAME).is_dir() + assert not (root / "3. Genres" / "Techno" / "lalala").exists() + assert (root / "3. Genres" / "Techno" / R1_VNAME / "01. Track 1.m4a").is_file() + assert not (root / "3. Genres" / "Techno" / R1_VNAME / "lalala.m4a").exists() + assert can_read(root / "3. Genres" / "Techno" / R1_VNAME / "01. Track 1.m4a") + + assert (root / "4. Descriptors").is_dir() + assert (root / "4. Descriptors" / "Warm").is_dir() + assert not (root / "4. Descriptors" / "lalala").exists() + assert (root / "4. Descriptors" / "Warm" / R1_VNAME).is_dir() + assert not (root / "4. Descriptors" / "Warm" / "lalala").exists() + assert (root / "4. Descriptors" / "Warm" / R1_VNAME / "01. Track 1.m4a").is_file() + assert not (root / "4. Descriptors" / "Warm" / R1_VNAME / "lalala.m4a").exists() + assert can_read(root / "4. Descriptors" / "Warm" / R1_VNAME / "01. Track 1.m4a") + + assert (root / "5. Labels").is_dir() + assert (root / "5. Labels" / "Silk Music").is_dir() + assert not (root / "5. Labels" / "lalala").exists() + assert (root / "5. Labels" / "Silk Music" / R1_VNAME).is_dir() + assert not (root / "5. Labels" / "Silk Music" / "lalala").exists() + assert (root / "5. Labels" / "Silk Music" / R1_VNAME / "01. Track 1.m4a").is_file() + assert not (root / "5. Labels" / "Silk Music" / R1_VNAME / "lalala").exists() + assert can_read(root / "5. Labels" / "Silk Music" / R1_VNAME / "01. Track 1.m4a") + + assert (root / "6. Collages").is_dir() + assert (root / "6. Collages" / "Rose Gold").is_dir() + assert (root / "6. Collages" / "Ruby Red").is_dir() + assert not (root / "6. Collages" / "lalala").exists() + assert (root / "6. Collages" / "Rose Gold" / f"1. {R1_VNAME}").is_dir() + assert not (root / "6. Collages" / "Rose Gold" / "lalala").exists() + assert (root / "6. Collages" / "Rose Gold" / f"1. {R1_VNAME}" / "01. Track 1.m4a").is_file() + assert not (root / "6. Collages" / "Rose Gold" / f"1. {R1_VNAME}" / "lalala").exists() + assert can_read(root / "6. Collages" / "Rose Gold" / f"1. {R1_VNAME}" / "01. Track 1.m4a") + + assert (root / "7. Playlists").is_dir() + assert (root / "7. Playlists" / "Lala Lisa").is_dir() + assert (root / "7. Playlists" / "Turtle Rabbit").is_dir() + assert not (root / "7. Playlists" / "lalala").exists() + assert (root / "7. Playlists" / "Lala Lisa" / "1. Techno Man & Bass Man - Track 1.m4a").is_file() + assert (root / "7. Playlists" / "Lala Lisa" / "cover.jpg").is_file() + assert not (root / "7. Playlists" / "Lala Lisa" / "lalala").exists() + assert can_read(root / "7. Playlists" / "Lala Lisa" / "1. Techno Man & Bass Man - Track 1.m4a") + assert can_read(root / "7. Playlists" / "Lala Lisa" / "cover.jpg") # fmt: on @@ -150,14 +165,14 @@ def test_virtual_filesystem_collage_actions(config: Config) -> None: with start_virtual_fs(config): # Create collage. - (root / "7. Collages" / "New Tee").mkdir(parents=True) + (root / "6. Collages" / "New Tee").mkdir(parents=True) assert (src / "!collages" / "New Tee.toml").is_file() # Rename collage. - (root / "7. Collages" / "New Tee").rename(root / "7. Collages" / "New Jeans") + (root / "6. Collages" / "New Tee").rename(root / "6. Collages" / "New Jeans") assert (src / "!collages" / "New Jeans.toml").is_file() assert not (src / "!collages" / "New Tee.toml").exists() # Add release to collage. - collage_dir = root / "7. Collages" / "New Jeans" + collage_dir = root / "6. Collages" / "New Jeans" subprocess.run( [ "cp", @@ -189,11 +204,11 @@ def test_virtual_filesystem_add_collage_release_with_any_dirname(config: Config) with start_virtual_fs(config): shutil.copytree( root / "1. Releases" / R1_VNAME, - root / "7. Collages" / "Ruby Red" / "LALA HAHA", + root / "6. Collages" / "Ruby Red" / "LALA HAHA", ) # fmt: off - assert (root / "7. Collages" / "Ruby Red" / f"1. {R1_VNAME}").is_dir() - assert (root / "7. Collages" / "Ruby Red" / f"1. {R1_VNAME}" / ".rose.r1.toml").is_file() + assert (root / "6. Collages" / "Ruby Red" / f"1. {R1_VNAME}").is_dir() + assert (root / "6. Collages" / "Ruby Red" / f"1. {R1_VNAME}" / ".rose.r1.toml").is_file() # fmt: on @@ -209,10 +224,10 @@ def test_virtual_filesystem_playlist_actions( with start_virtual_fs(config): # Create playlist. - (root / "8. Playlists" / "New Tee").mkdir(parents=True) + (root / "7. Playlists" / "New Tee").mkdir(parents=True) assert (src / "!playlists" / "New Tee.toml").is_file() # Rename playlist. - (root / "8. Playlists" / "New Tee").rename(root / "8. Playlists" / "New Jeans") + (root / "7. Playlists" / "New Tee").rename(root / "7. Playlists" / "New Jeans") assert (src / "!playlists" / "New Jeans.toml").is_file() assert not (src / "!playlists" / "New Tee.toml").exists() # Add track to playlist. @@ -225,22 +240,22 @@ def test_virtual_filesystem_playlist_actions( "cp", "-p", str(release_dir / filename), - str(root / "8. Playlists" / "New Jeans" / filename), + str(root / "7. Playlists" / "New Jeans" / filename), ], check=True, ) # Assert that we can see the attributes of the ghost file. - assert (root / "8. Playlists" / "New Jeans" / filename).is_file() - assert (root / "8. Playlists" / "New Jeans" / "1. BLACKPINK - Track 1.m4a").is_file() + assert (root / "7. Playlists" / "New Jeans" / filename).is_file() + assert (root / "7. Playlists" / "New Jeans" / "1. BLACKPINK - Track 1.m4a").is_file() with (src / "!playlists" / "New Jeans.toml").open("r") as fp: assert "BLACKPINK - Track 1 [1990].m4a" in fp.read() # Delete track from playlist. - (root / "8. Playlists" / "New Jeans" / "1. BLACKPINK - Track 1.m4a").unlink() - assert not (root / "8. Playlists" / "New Jeans" / "1. BLACKPINK - Track 1.m4a").exists() + (root / "7. Playlists" / "New Jeans" / "1. BLACKPINK - Track 1.m4a").unlink() + assert not (root / "7. Playlists" / "New Jeans" / "1. BLACKPINK - Track 1.m4a").exists() with (src / "!playlists" / "New Jeans.toml").open("r") as fp: assert "BLACKPINK - Track 1 [1990].m4a" not in fp.read() # Delete playlist. - (root / "8. Playlists" / "New Jeans").rmdir() + (root / "7. Playlists" / "New Jeans").rmdir() assert not (src / "!playlists" / "New Jeans.toml").exists() @@ -291,7 +306,7 @@ def test_virtual_filesystem_playlist_cover_art_actions( source_dir: Path, # noqa: ARG001 ) -> None: root = config.fuse_mount_dir - playlist_dir = root / "8. Playlists" / "Lala Lisa" + playlist_dir = root / "7. Playlists" / "Lala Lisa" with start_virtual_fs(config): assert (playlist_dir / "cover.jpg").is_file() # First write. @@ -376,16 +391,19 @@ def test_virtual_filesystem_blacklist(config: Config) -> None: config, fuse_artists_blacklist=["Bass Man"], fuse_genres_blacklist=["Techno"], + fuse_descriptors_blacklist=["Warm"], fuse_labels_blacklist=["Silk Music"], ) root = config.fuse_mount_dir with start_virtual_fs(new_config): - assert (root / "4. Artists" / "Techno Man").is_dir() - assert (root / "5. Genres" / "Deep House").is_dir() - assert (root / "6. Labels" / "Native State").is_dir() - assert not (root / "4. Artists" / "Bass Man").exists() - assert not (root / "5. Genres" / "Techno").exists() - assert not (root / "6. Labels" / "Silk Music").exists() + assert (root / "2. Artists" / "Techno Man").is_dir() + assert (root / "3. Genres" / "Deep House").is_dir() + assert (root / "4. Descriptors" / "Hot").exists() + assert (root / "5. Labels" / "Native State").is_dir() + assert not (root / "2. Artists" / "Bass Man").exists() + assert not (root / "3. Genres" / "Techno").exists() + assert not (root / "4. Descriptors" / "Warm").is_dir() + assert not (root / "5. Labels" / "Silk Music").exists() @pytest.mark.usefixtures("seeded_cache") @@ -394,13 +412,16 @@ def test_virtual_filesystem_whitelist(config: Config) -> None: config, fuse_artists_whitelist=["Bass Man"], fuse_genres_whitelist=["Techno"], + fuse_descriptors_whitelist=["Warm"], fuse_labels_whitelist=["Silk Music"], ) root = config.fuse_mount_dir with start_virtual_fs(new_config): - assert not (root / "4. Artists" / "Techno Man").exists() - assert not (root / "5. Genres" / "Deep House").exists() - assert not (root / "6. Labels" / "Native State").exists() - assert (root / "4. Artists" / "Bass Man").is_dir() - assert (root / "5. Genres" / "Techno").is_dir() - assert (root / "6. Labels" / "Silk Music").is_dir() + assert not (root / "2. Artists" / "Techno Man").exists() + assert not (root / "3. Genres" / "Deep House").exists() + assert not (root / "4. Descriptors" / "Hot").exists() + assert not (root / "5. Labels" / "Native State").exists() + assert (root / "2. Artists" / "Bass Man").is_dir() + assert (root / "3. Genres" / "Techno").is_dir() + assert (root / "4. Descriptors" / "Warm").is_dir() + assert (root / "5. Labels" / "Silk Music").is_dir()