diff --git a/docs/TEMPLATES.md b/docs/TEMPLATES.md index 3b23454..a818299 100644 --- a/docs/TEMPLATES.md +++ b/docs/TEMPLATES.md @@ -99,6 +99,11 @@ releaseartists.composer: list[Artist] releaseartists.conductor: list[Artist] releaseartists.djmixer: list[Artist] position: str # If in a collage context, the zero-padded position of the release in the collage. +context.genre: str # The current genre being viewed in the Virtual Filesystem. +context.label: str # The current label being viewed in the Virtual Filesystem. +context.artist: str # The current artist being viewed in the Virtual Filesystem. +context.collage: str # The current collage being viewed in the Virtual Filesystem. +context.playlist: str # The current playlist being viewed in the Virtual Filesystem. ``` And provides the template variables for tracks: @@ -141,6 +146,11 @@ releaseartists.composer: list[Artist] releaseartists.conductor: list[Artist] releaseartists.djmixer: list[Artist] position: str # If in a playlist context, the zero-padded position of the track in the playlist. +context.genre: str # The current genre being viewed in the Virtual Filesystem. +context.label: str # The current label being viewed in the Virtual Filesystem. +context.artist: str # The current artist being viewed in the Virtual Filesystem. +context.collage: str # The current collage being viewed in the Virtual Filesystem. +context.playlist: str # The current playlist being viewed in the Virtual Filesystem. ``` Rosé also provides the following custom filters: diff --git a/rose/templates.py b/rose/templates.py index 7fd4c74..524a223 100644 --- a/rose/templates.py +++ b/rose/templates.py @@ -251,21 +251,36 @@ def parse(self) -> None: raise InvalidPathTemplateError(f"Failed to compile template: {e}", key=key) from e +@dataclasses.dataclass +class PathContext: + genre: str | None + artist: str | None + label: str | None + collage: str | None + playlist: str | None + + def eval_release_template( template: PathTemplate, release: CachedRelease, + context: PathContext | None = None, position: str | None = None, ) -> str: - return _collapse_spacing(template.compiled.render(**_calc_release_variables(release, position))) + return _collapse_spacing( + template.compiled.render(context=context, **_calc_release_variables(release, position)) + ) def eval_track_template( template: PathTemplate, track: CachedTrack, + context: PathContext | None = None, position: str | None = None, ) -> str: return ( - _collapse_spacing(template.compiled.render(**_calc_track_variables(track, position))) + _collapse_spacing( + template.compiled.render(context=context, **_calc_track_variables(track, position)) + ) + track.source_path.suffix ) @@ -480,11 +495,11 @@ def _preview_release_template(c: Config, label: str, template: PathTemplate) -> kimlip, youngforever, debussy = _get_preview_releases(c) click.secho(f"{label}:", dim=True, underline=True) click.secho(" Sample 1: ", dim=True, nl=False) - click.secho(eval_release_template(template, kimlip, "1")) + click.secho(eval_release_template(template, kimlip, position="1")) click.secho(" Sample 2: ", dim=True, nl=False) - click.secho(eval_release_template(template, youngforever, "2")) + click.secho(eval_release_template(template, youngforever, position="2")) click.secho(" Sample 3: ", dim=True, nl=False) - click.secho(eval_release_template(template, debussy, "3")) + click.secho(eval_release_template(template, debussy, position="3")) def _preview_track_template(c: Config, label: str, template: PathTemplate) -> None: @@ -509,7 +524,7 @@ def _preview_track_template(c: Config, label: str, template: PathTemplate) -> No metahash="0", release=kimlip, ) - click.secho(eval_track_template(template, track, "1")) + click.secho(eval_track_template(template, track, position="1")) click.secho(" Sample 2: ", dim=True, nl=False) track = CachedTrack( @@ -527,7 +542,7 @@ def _preview_track_template(c: Config, label: str, template: PathTemplate) -> No metahash="0", release=youngforever, ) - click.secho(eval_track_template(template, track, "2")) + click.secho(eval_track_template(template, track, position="2")) click.secho(" Sample 3: ", dim=True, nl=False) track = CachedTrack( @@ -549,4 +564,4 @@ def _preview_track_template(c: Config, label: str, template: PathTemplate) -> No metahash="0", release=debussy, ) - click.secho(eval_track_template(template, track, "3")) + click.secho(eval_track_template(template, track, position="3")) diff --git a/rose/templates_test.py b/rose/templates_test.py index 9388b1d..336b274 100644 --- a/rose/templates_test.py +++ b/rose/templates_test.py @@ -73,7 +73,7 @@ def test_default_templates() -> None: == "A1, A2 & A3 (feat. BB) (prod. PP) - 2023. Title - Single" ) assert ( - eval_release_template(templates.collages.release, release, "4") + eval_release_template(templates.collages.release, release, position="4") == "4. A1, A2 & A3 (feat. BB) (prod. PP) - 2023. Title - Single" ) @@ -81,7 +81,7 @@ def test_default_templates() -> None: release.releasetitle = "Title" assert eval_release_template(templates.source.release, release) == "Unknown Artists - Title" assert ( - eval_release_template(templates.collages.release, release, "4") + eval_release_template(templates.collages.release, release, position="4") == "4. Unknown Artists - Title" ) @@ -89,7 +89,10 @@ def test_default_templates() -> None: track.tracknumber = "2" track.tracktitle = "Trick" assert eval_track_template(templates.source.track, track) == "02. Trick.m4a" - assert eval_track_template(templates.playlists, track, "4") == "4. Unknown Artists - Trick.m4a" + assert ( + eval_track_template(templates.playlists, track, position="4") + == "4. Unknown Artists - Trick.m4a" + ) track = deepcopy(EMPTY_CACHED_TRACK) track.release.disctotal = 2 @@ -105,7 +108,7 @@ def test_default_templates() -> None: == "04-02. Trick (feat. Hi, High & Hye).m4a" ) assert ( - eval_track_template(templates.playlists, track, "4") + eval_track_template(templates.playlists, track, position="4") == "4. Main (feat. Hi, High & Hye) - Trick.m4a" ) diff --git a/rose_vfs/virtualfs.py b/rose_vfs/virtualfs.py index e6ba27b..a16111c 100644 --- a/rose_vfs/virtualfs.py +++ b/rose_vfs/virtualfs.py @@ -104,6 +104,7 @@ update_cache_for_releases, ) from rose.cache import list_releases_delete_this +from rose.templates import PathContext logger = logging.getLogger(__name__) @@ -226,6 +227,16 @@ def label_parent(self) -> VirtualPath: """Parent path of a label: Used as an input to the Sanitizer.""" return VirtualPath(view=self.view) + @property + def collage_parent(self) -> VirtualPath: + """Parent path of a collage: Used as an input to the Sanitizer.""" + return VirtualPath(view=self.view) + + @property + def playlist_parent(self) -> VirtualPath: + """Parent path of a playlist: Used as an input to the Sanitizer.""" + return VirtualPath(view=self.view) + @classmethod def parse(cls, path: Path) -> VirtualPath: parts = str(path.resolve()).split("/")[1:] # First part is always empty string. @@ -355,9 +366,10 @@ class VirtualNameGenerator: paths, new paths will take precedence. """ - def __init__(self, config: Config): + def __init__(self, config: Config, sanitizer: Sanitizer): # fmt: off self._config = config + self._sanitizer = sanitizer # These are the stateful maps that we use to remember path mappings. They are maps from the # (parent_path, virtual path) -> entity ID. # @@ -423,7 +435,29 @@ def list_release_paths( f"VNAMES: Reused cached virtual dirname {vname} for release {logtext} in {time.time()-time_start} seconds" ) except KeyError: - vname = eval_release_template(template, release, position) + context = PathContext( + genre=self._sanitizer.unsanitize( + release_parent.genre, + release_parent.genre_parent, + ) + if release_parent.genre + else None, + label=self._sanitizer.unsanitize( + release_parent.label, + release_parent.label_parent, + ) + if release_parent.label + else None, + artist=self._sanitizer.unsanitize( + release_parent.artist, + release_parent.artist_parent, + ) + if release_parent.artist + else None, + collage=release_parent.collage, + playlist=None, + ) + vname = eval_release_template(template, release, context, position) vname = sanitize_dirname(self._config, vname, False) self._release_template_eval_cache[cachekey] = vname logger.debug( @@ -501,7 +535,29 @@ def list_track_paths( try: vname = self._track_template_eval_cache[cachekey] except KeyError: - vname = eval_track_template(template, track, position) + context = PathContext( + genre=self._sanitizer.unsanitize( + track_parent.genre, + track_parent.genre_parent, + ) + if track_parent.genre + else None, + label=self._sanitizer.unsanitize( + track_parent.label, + track_parent.label_parent, + ) + if track_parent.label + else None, + artist=self._sanitizer.unsanitize( + track_parent.artist, + track_parent.artist_parent, + ) + if track_parent.artist + else None, + collage=track_parent.collage, + playlist=track_parent.playlist, + ) + vname = eval_track_template(template, track, context, position) vname = sanitize_filename(self._config, vname, False) logger.debug( f"VNAMES: Generated virtual filename {vname} for track {logtext} in {time.time() - time_start} seconds" @@ -696,8 +752,8 @@ class RoseLogicalCore: def __init__(self, config: Config, fhandler: FileHandleManager): self.config = config self.fhandler = fhandler - self.vnames = VirtualNameGenerator(config) self.sanitizer = Sanitizer(config, self) + self.vnames = VirtualNameGenerator(config, self.sanitizer) self.can_show = CanShower(config) # This map stores the state for "file creation" operations. We currently have two file # creation operations: