diff --git a/flake.nix b/flake.nix index 2a538ee..c02c53a 100644 --- a/flake.nix +++ b/flake.nix @@ -47,7 +47,7 @@ ]; dev-cli = pkgs.writeShellScriptBin "rose" '' cd $ROSE_ROOT - python -m rose "$@" + python -m rose_cli "$@" ''; in { diff --git a/rose/__init__.py b/rose/__init__.py index dddb610..a1e4548 100644 --- a/rose/__init__.py +++ b/rose/__init__.py @@ -6,43 +6,216 @@ import appdirs -logger = logging.getLogger() -logger.setLevel(logging.INFO) - -# appdirs by default has Unix log to $XDG_CACHE_HOME, but I'd rather write logs to $XDG_STATE_HOME. -LOG_HOME = Path(appdirs.user_state_dir("rose")) -if appdirs.system == "darwin": - LOG_HOME = Path(appdirs.user_log_dir("rose")) - -LOG_HOME.mkdir(parents=True, exist_ok=True) -LOGFILE = LOG_HOME / "rose.log" - -# Useful for debugging problems with the virtual FS, since pytest doesn't capture that debug logging -# output. -LOG_EVEN_THOUGH_WERE_IN_TEST = os.environ.get("LOG_TEST", False) - -# Add a logging handler for stdout unless we are testing. Pytest -# captures logging output on its own, so by default, we do not attach our own. -if "pytest" not in sys.modules or LOG_EVEN_THOUGH_WERE_IN_TEST: # pragma: no cover - simple_formatter = logging.Formatter( - "[%(asctime)s] %(levelname)s: %(message)s", - datefmt="%H:%M:%S", - ) - verbose_formatter = logging.Formatter( - "[ts=%(asctime)s.%(msecs)03d] [pid=%(process)d] [src=%(name)s:%(lineno)s] %(levelname)s: %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - - stream_handler = logging.StreamHandler(sys.stderr) - stream_handler.setFormatter( - simple_formatter if not LOG_EVEN_THOUGH_WERE_IN_TEST else verbose_formatter - ) - logger.addHandler(stream_handler) - - file_handler = logging.handlers.RotatingFileHandler( - LOGFILE, - maxBytes=20 * 1024 * 1024, - backupCount=10, - ) - file_handler.setFormatter(verbose_formatter) - logger.addHandler(file_handler) +from rose.audiotags import ( + SUPPORTED_AUDIO_EXTENSIONS, + AudioTags, + UnsupportedFiletypeError, +) +from rose.cache import ( + STORED_DATA_FILE_REGEX, + CachedRelease, + CachedTrack, + artist_exists, + calculate_release_logtext, + calculate_track_logtext, + collage_exists, + genre_exists, + get_collage, + get_path_of_track_in_playlist, + get_playlist, + get_playlist_cover_path, + get_release, + get_track, + get_tracks_associated_with_release, + label_exists, + list_artists, + list_collages, + list_genres, + list_labels, + list_playlists, + maybe_invalidate_cache_database, + playlist_exists, + update_cache, + update_cache_for_releases, +) +from rose.collages import ( + add_release_to_collage, + create_collage, + delete_collage, + dump_all_collages, + dump_collage, + edit_collage_in_editor, + remove_release_from_collage, + rename_collage, +) +from rose.common import ( + VERSION, + RoseError, + RoseExpectedError, + sanitize_dirname, + sanitize_filename, +) +from rose.config import Config +from rose.playlists import ( + add_track_to_playlist, + create_playlist, + delete_playlist, + delete_playlist_cover_art, + dump_all_playlists, + dump_playlist, + edit_playlist_in_editor, + remove_track_from_playlist, + rename_playlist, + set_playlist_cover_art, +) +from rose.releases import ( + 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, MetadataRule +from rose.rules import execute_metadata_rule, execute_stored_metadata_rules +from rose.templates import ( + PathTemplate, + eval_release_template, + eval_track_template, + preview_path_templates, +) +from rose.tracks import dump_all_tracks, dump_track, run_actions_on_track +from rose.watcher import start_watchdog + +__all__ = [ + "AudioTags", + "CachedRelease", + "CachedTrack", + "Config", + "MetadataAction", + "MetadataMatcher", + "MetadataRule", + "PathTemplate", + "RoseError", + "RoseExpectedError", + "STORED_DATA_FILE_REGEX", # TODO: Revise: is_release_directory / is_track_file + "SUPPORTED_AUDIO_EXTENSIONS", + "UnsupportedFiletypeError", + "VERSION", + "add_release_to_collage", + "add_track_to_playlist", + "artist_exists", + "calculate_release_logtext", # TODO: Rename. + "calculate_track_logtext", # TODO: Rename. + "collage_exists", + "create_collage", + "create_playlist", + "create_single_release", + "delete_collage", + "delete_playlist", + "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", # TODO: Move editor part to CLI, make this file-submissions. + "edit_playlist_in_editor", # TODO: Move editor part to CLI, make this file-submissions. + "edit_release", + "eval_release_template", # TODO: Rename. + "eval_track_template", # TODO: Rename. + "execute_metadata_rule", + "execute_stored_metadata_rules", + "genre_exists", + "get_collage", + "get_path_of_track_in_playlist", # TODO: Redesign. + "get_playlist", + "get_playlist_cover_path", # TODO: Remove. + "get_release", + "get_track", + "get_tracks_associated_with_release", # TODO: Rename: `get_tracks_of_release` / `dump_release(with_tracks=tracks)` + "label_exists", + "list_artists", + "list_collages", + "list_genres", + "list_labels", + "list_playlists", + "maybe_invalidate_cache_database", + "playlist_exists", + "preview_path_templates", + "remove_release_from_collage", + "remove_track_from_playlist", + "rename_collage", + "rename_playlist", + "run_actions_on_release", + "run_actions_on_track", + "sanitize_dirname", + "sanitize_filename", + "set_playlist_cover_art", + "set_release_cover_art", + "start_watchdog", + "toggle_release_new", + "update_cache", + "update_cache_for_releases", +] + +__logging_initialized = False + + +def initialize_logging() -> None: + global __logging_initialized + if __logging_initialized: + return + __logging_initialized = True + + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + # appdirs by default has Unix log to $XDG_CACHE_HOME, but I'd rather write logs to $XDG_STATE_HOME. + log_home = Path(appdirs.user_state_dir("rose")) + if appdirs.system == "darwin": + log_home = Path(appdirs.user_log_dir("rose")) + + log_home.mkdir(parents=True, exist_ok=True) + log_file = log_home / "rose.log" + + # Useful for debugging problems with the virtual FS, since pytest doesn't capture that debug logging + # output. + log_despite_testing = os.environ.get("LOG_TEST", False) + + # Add a logging handler for stdout unless we are testing. Pytest + # captures logging output on its own, so by default, we do not attach our own. + if "pytest" not in sys.modules or log_despite_testing: # pragma: no cover + simple_formatter = logging.Formatter( + "[%(asctime)s] %(levelname)s: %(message)s", + datefmt="%H:%M:%S", + ) + verbose_formatter = logging.Formatter( + "[ts=%(asctime)s.%(msecs)03d] [pid=%(process)d] [src=%(name)s:%(lineno)s] %(levelname)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + stream_handler = logging.StreamHandler(sys.stderr) + stream_handler.setFormatter( + simple_formatter if not log_despite_testing else verbose_formatter + ) + logger.addHandler(stream_handler) + + file_handler = logging.handlers.RotatingFileHandler( + log_file, + maxBytes=20 * 1024 * 1024, + backupCount=10, + ) + file_handler.setFormatter(verbose_formatter) + logger.addHandler(file_handler) + + +initialize_logging() diff --git a/rose/common.py b/rose/common.py index 1de7ae6..1867c14 100644 --- a/rose/common.py +++ b/rose/common.py @@ -7,7 +7,6 @@ import hashlib import os.path import re -import uuid from collections.abc import Iterator from pathlib import Path from typing import Any, TypeVar @@ -72,14 +71,6 @@ def items(self) -> Iterator[tuple[str, list[Artist]]]: yield "djmixer", self.djmixer -def valid_uuid(x: str) -> bool: - try: - uuid.UUID(x) - return True - except ValueError: - return False - - def uniq(xs: list[T]) -> list[T]: rv: list[T] = [] seen: set[T] = set() diff --git a/rose/config_test.py b/rose/config_test.py index f24686d..982dfd6 100644 --- a/rose/config_test.py +++ b/rose/config_test.py @@ -4,7 +4,12 @@ import click import pytest -from rose.config import Config, ConfigNotFoundError, InvalidConfigValueError, MissingConfigKeyError +from rose.config import ( + Config, + ConfigNotFoundError, + InvalidConfigValueError, + MissingConfigKeyError, +) from rose.rule_parser import ( MatcherPattern, MetadataAction, diff --git a/rose_cli/__init__.py b/rose_cli/__init__.py new file mode 100644 index 0000000..5bbed21 --- /dev/null +++ b/rose_cli/__init__.py @@ -0,0 +1,3 @@ +from rose import initialize_logging + +initialize_logging() diff --git a/rose/__main__.py b/rose_cli/__main__.py similarity index 64% rename from rose/__main__.py rename to rose_cli/__main__.py index 1d0f13c..38acd1e 100644 --- a/rose/__main__.py +++ b/rose_cli/__main__.py @@ -2,14 +2,15 @@ import click -from rose.cli import cli -from rose.common import RoseExpectedError +from rose_cli.cli import CliExpectedError, cli def main() -> None: + from rose import RoseExpectedError + try: cli() - except RoseExpectedError as e: + except (RoseExpectedError, CliExpectedError) as e: click.secho(f"{e.__class__.__module__}.{e.__class__.__name__}: ", fg="red", nl=False) click.secho(str(e)) sys.exit(1) diff --git a/rose/cli.py b/rose_cli/cli.py similarity index 87% rename from rose/cli.py rename to rose_cli/cli.py index 6b7b9e1..bce26ac 100644 --- a/rose/cli.py +++ b/rose_cli/cli.py @@ -8,33 +8,83 @@ import os import signal import subprocess +import uuid from dataclasses import dataclass from multiprocessing import Process from pathlib import Path import click -from rose.common import VERSION, RoseExpectedError -from rose.config import Config +from rose import ( + STORED_DATA_FILE_REGEX, + VERSION, + AudioTags, + Config, + MetadataAction, + MetadataMatcher, + MetadataRule, + UnsupportedFiletypeError, + add_release_to_collage, + add_track_to_playlist, + create_collage, + create_playlist, + create_single_release, + delete_collage, + delete_playlist, + 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, + rename_playlist, + run_actions_on_release, + run_actions_on_track, + set_playlist_cover_art, + set_release_cover_art, + start_watchdog, + toggle_release_new, + update_cache, +) +from rose_vfs import mount_virtualfs logger = logging.getLogger(__name__) -class InvalidReleaseArgError(RoseExpectedError): +class CliExpectedError(Exception): pass -class InvalidTrackArgError(RoseExpectedError): +class InvalidReleaseArgError(CliExpectedError): pass -class DaemonAlreadyRunningError(RoseExpectedError): +class InvalidTrackArgError(CliExpectedError): + pass + + +class DaemonAlreadyRunningError(CliExpectedError): pass @dataclass class Context: - config: Config + config: "Config" @click.group() @@ -43,7 +93,6 @@ class Context: @click.pass_context def cli(cc: click.Context, verbose: bool, config: Path | None = None) -> None: """A music manager with a virtual filesystem.""" - from rose.cache import maybe_invalidate_cache_database cc.obj = Context( config=Config.parse(config_path_override=config), @@ -56,6 +105,7 @@ def cli(cc: click.Context, verbose: bool, config: Path | None = None) -> None: @cli.command() def version() -> None: """Print version.""" + click.echo(VERSION) @@ -76,7 +126,6 @@ def generate_completion(shell: str) -> None: @click.pass_obj def preview_templates(ctx: Context) -> None: """Preview the configured path templates with sample data.""" - from rose.templates import preview_path_templates preview_path_templates(ctx.config) @@ -91,7 +140,6 @@ def cache() -> None: @click.pass_obj def update(ctx: Context, force: bool) -> None: """Synchronize the read cache with new changes in the source directory.""" - from rose.cache import update_cache update_cache(ctx.config, force) @@ -101,7 +149,6 @@ def update(ctx: Context, force: bool) -> None: @click.pass_obj def watch(ctx: Context, foreground: bool) -> None: """Start a watchdog to auto-update the cache when the source directory changes.""" - from rose.watcher import start_watchdog if not foreground: daemonize(pid_path=ctx.config.watchdog_pid_path) @@ -136,9 +183,6 @@ def fs() -> None: @click.pass_obj def mount(ctx: Context, foreground: bool) -> None: """Mount the virtual filesystem.""" - from rose.cache import update_cache - from rose.virtualfs import mount_virtualfs - if not foreground: daemonize() @@ -157,7 +201,7 @@ def mount(ctx: Context, foreground: bool) -> None: @click.pass_obj def unmount(ctx: Context) -> None: """Unmount the virtual filesystem.""" - from rose.virtualfs import unmount_virtualfs + from rose_vfs import unmount_virtualfs unmount_virtualfs(ctx.config) @@ -173,8 +217,6 @@ def releases() -> None: @click.pass_obj def print_release(ctx: Context, release: str) -> None: """Print a single release (in JSON). Accepts a release's UUID/path.""" - from rose.releases import dump_release - release = parse_release_argument(release) click.echo(dump_release(ctx.config, release)) @@ -184,9 +226,6 @@ def print_release(ctx: Context, release: str) -> None: @click.pass_obj def print_all_releases(ctx: Context, matcher: str | None) -> None: """Print all releases (in JSON). Accepts an optional rules matcher to filter the releases.""" - from rose.releases import dump_all_releases - from rose.rule_parser import MetadataMatcher - parsed_matcher = MetadataMatcher.parse(matcher) if matcher else None click.echo(dump_all_releases(ctx.config, parsed_matcher)) @@ -195,10 +234,8 @@ def print_all_releases(ctx: Context, matcher: str | None) -> None: @click.argument("release", type=click.Path(), nargs=1) @click.option("--resume", "-r", type=click.Path(path_type=Path), nargs=1, help="Resume a failed release edit.") # fmt: skip @click.pass_obj -def edit_release(ctx: Context, release: str, resume: Path | None) -> None: +def edit_release_cmd(ctx: Context, release: str, resume: Path | None) -> None: """Edit a release's metadata in $EDITOR. Accepts a release's UUID/path.""" - from rose.releases import edit_release - release = parse_release_argument(release) edit_release(ctx.config, release, resume_file=resume) @@ -208,8 +245,6 @@ def edit_release(ctx: Context, release: str, resume: Path | None) -> None: @click.pass_obj def toggle_new(ctx: Context, release: str) -> None: """Toggle a release's "new"-ness. Accepts a release's UUID/path.""" - from rose.releases import toggle_release_new - release = parse_release_argument(release) toggle_release_new(ctx.config, release) @@ -217,13 +252,11 @@ def toggle_new(ctx: Context, release: str) -> None: @releases.command(name="delete") @click.argument("release", type=click.Path(), nargs=1) @click.pass_obj -def delete_release(ctx: Context, release: str) -> None: +def delete_release_cmd(ctx: Context, release: str) -> None: """ Delete a release from the library. The release is moved to the trash bin, following the freedesktop spec. Accepts a release's UUID/path. """ - from rose.releases import delete_release - release = parse_release_argument(release) delete_release(ctx.config, release) @@ -234,8 +267,6 @@ def delete_release(ctx: Context, release: str) -> None: @click.pass_obj def set_cover_release(ctx: Context, release: str, cover: Path) -> None: """Set/replace the cover art of a release. Accepts a release's UUID/path.""" - from rose.releases import set_release_cover_art - release = parse_release_argument(release) set_release_cover_art(ctx.config, release, cover) @@ -245,8 +276,6 @@ def set_cover_release(ctx: Context, release: str, cover: Path) -> None: @click.pass_obj def delete_cover_release(ctx: Context, release: str) -> None: """Delete the cover art of a release.""" - from rose.releases import delete_release_cover_art - release = parse_release_argument(release) delete_release_cover_art(ctx.config, release) @@ -259,9 +288,6 @@ def delete_cover_release(ctx: Context, release: str) -> None: @click.pass_obj def run_rule(ctx: Context, release: str, actions: list[str], dry_run: bool, yes: bool) -> None: """Run rule engine actions on all tracks in a release. Accepts a release's UUID/path.""" - from rose.releases import run_actions_on_release - from rose.rule_parser import MetadataAction - release = parse_release_argument(release) parsed_actions = [MetadataAction.parse(a) for a in actions] run_actions_on_release( @@ -281,8 +307,6 @@ def create_single(ctx: Context, track_path: Path) -> None: Create a single release for the given track, and copy the track into it. Only accepts a track path. """ - from rose.releases import create_single_release - create_single_release(ctx.config, track_path) @@ -296,8 +320,6 @@ def tracks() -> None: @click.pass_obj def print_track(ctx: Context, track: str) -> None: """Print a single track (in JSON). Accepts a tracks's UUID/path.""" - from rose.tracks import dump_track - track = parse_track_argument(track) click.echo(dump_track(ctx.config, track)) @@ -307,9 +329,6 @@ def print_track(ctx: Context, track: str) -> None: @click.pass_obj def print_all_track(ctx: Context, matcher: str | None = None) -> None: """Print all tracks (in JSON). Accepts an optional rules matcher to filter the tracks.""" - from rose.rule_parser import MetadataMatcher - from rose.tracks import dump_all_tracks - parsed_matcher = MetadataMatcher.parse(matcher) if matcher else None click.echo(dump_all_tracks(ctx.config, parsed_matcher)) @@ -322,9 +341,6 @@ def print_all_track(ctx: Context, matcher: str | None = None) -> None: @click.pass_obj def run_rule_track(ctx: Context, track: str, actions: list[str], dry_run: bool, yes: bool) -> None: """Run rule engine actions on a single track. Accepts a track's UUID/path.""" - from rose.rule_parser import MetadataAction - from rose.tracks import run_actions_on_track - track = parse_track_argument(track) parsed_actions = [MetadataAction.parse(a) for a in actions] run_actions_on_track( @@ -346,8 +362,6 @@ def collages() -> None: @click.pass_obj def create(ctx: Context, name: str) -> None: """Create a new collage.""" - from rose.collages import create_collage - create_collage(ctx.config, name) @@ -357,8 +371,6 @@ def create(ctx: Context, name: str) -> None: @click.pass_obj def rename(ctx: Context, old_name: str, new_name: str) -> None: """Rename a collage.""" - from rose.collages import rename_collage - rename_collage(ctx.config, old_name, new_name) @@ -367,8 +379,6 @@ def rename(ctx: Context, old_name: str, new_name: str) -> None: @click.pass_obj def delete(ctx: Context, collage: str) -> None: """Delete a collage.""" - from rose.collages import delete_collage - delete_collage(ctx.config, collage) @@ -378,8 +388,6 @@ def delete(ctx: Context, collage: str) -> None: @click.pass_obj def add_release(ctx: Context, collage: str, release: str) -> None: """Add a release to a collage. Accepts a collage's name and a release's UUID/path.""" - from rose.collages import add_release_to_collage - release = parse_release_argument(release) add_release_to_collage(ctx.config, collage, release) @@ -390,8 +398,6 @@ def add_release(ctx: Context, collage: str, release: str) -> None: @click.pass_obj def remove_release(ctx: Context, collage: str, release: str) -> None: """Remove a release from a collage. Accepts a collage's name and a release's UUID/path.""" - from rose.collages import remove_release_from_collage - release = parse_release_argument(release) remove_release_from_collage(ctx.config, collage, release) @@ -401,8 +407,6 @@ def remove_release(ctx: Context, collage: str, release: str) -> None: @click.pass_obj def edit(ctx: Context, collage: str) -> None: """Edit (reorder/remove releases from) a collage in $EDITOR. Accepts a collage's name.""" - from rose.collages import edit_collage_in_editor - edit_collage_in_editor(ctx.config, collage) @@ -411,8 +415,6 @@ def edit(ctx: Context, collage: str) -> None: @click.pass_obj def print_collage(ctx: Context, collage: str) -> None: """Print a collage (in JSON). Accepts a collage's name.""" - from rose.collages import dump_collage - click.echo(dump_collage(ctx.config, collage)) @@ -420,8 +422,6 @@ def print_collage(ctx: Context, collage: str) -> None: @click.pass_obj def print_all_collages(ctx: Context) -> None: """Print all collages (in JSON).""" - from rose.collages import dump_all_collages - click.echo(dump_all_collages(ctx.config)) @@ -433,10 +433,8 @@ def playlists() -> None: @playlists.command(name="create") @click.argument("name", type=str, nargs=1) @click.pass_obj -def create_playlist(ctx: Context, name: str) -> None: +def create_playlist_cmd(ctx: Context, name: str) -> None: """Create a new playlist.""" - from rose.playlists import create_playlist - create_playlist(ctx.config, name) @@ -444,20 +442,16 @@ def create_playlist(ctx: Context, name: str) -> None: @click.argument("old_name", type=str, nargs=1) @click.argument("new_name", type=str, nargs=1) @click.pass_obj -def rename_playlist(ctx: Context, old_name: str, new_name: str) -> None: +def rename_playlist_cmd(ctx: Context, old_name: str, new_name: str) -> None: """Rename a playlist. Accepts a playlist's name.""" - from rose.playlists import rename_playlist - rename_playlist(ctx.config, old_name, new_name) @playlists.command(name="delete") @click.argument("playlist", type=str, nargs=1) @click.pass_obj -def delete_playlist(ctx: Context, playlist: str) -> None: +def delete_playlist_cmd(ctx: Context, playlist: str) -> None: """Delete a playlist. Accepts a playlist's name.""" - from rose.playlists import delete_playlist - delete_playlist(ctx.config, playlist) @@ -467,8 +461,6 @@ def delete_playlist(ctx: Context, playlist: str) -> None: @click.pass_obj def add_track(ctx: Context, playlist: str, track: str) -> None: """Add a track to a playlist. Accepts a playlist name and a track's UUID/path.""" - from rose.playlists import add_track_to_playlist - track = parse_track_argument(track) add_track_to_playlist(ctx.config, playlist, track) @@ -479,8 +471,6 @@ def add_track(ctx: Context, playlist: str, track: str) -> None: @click.pass_obj def remove_track(ctx: Context, playlist: str, track: str) -> None: """Remove a track from a playlist. Accepts a playlist name and a track's UUID/path.""" - from rose.playlists import remove_track_from_playlist - track = parse_track_argument(track) remove_track_from_playlist(ctx.config, playlist, track) @@ -493,8 +483,6 @@ def edit_playlist(ctx: Context, playlist: str) -> None: Edit a playlist in $EDITOR. Reorder lines to update the ordering of tracks. Delete lines to delete tracks from the playlist. """ - from rose.playlists import edit_playlist_in_editor - edit_playlist_in_editor(ctx.config, playlist) @@ -503,8 +491,6 @@ def edit_playlist(ctx: Context, playlist: str) -> None: @click.pass_obj def print_playlist(ctx: Context, playlist: str) -> None: """Print a playlist (in JSON). Accepts a playlist's name.""" - from rose.playlists import dump_playlist - click.echo(dump_playlist(ctx.config, playlist)) @@ -512,8 +498,6 @@ def print_playlist(ctx: Context, playlist: str) -> None: @click.pass_obj def print_all_playlists(ctx: Context) -> None: """Print all playlists (in JSON).""" - from rose.playlists import dump_all_playlists - click.echo(dump_all_playlists(ctx.config)) @@ -525,8 +509,6 @@ def set_cover_playlist(ctx: Context, playlist: str, cover: Path) -> None: """ Set the cover art of a playlist. Accepts a playlist name and a path to an image. """ - from rose.playlists import set_playlist_cover_art - set_playlist_cover_art(ctx.config, playlist, cover) @@ -535,8 +517,6 @@ def set_cover_playlist(ctx: Context, playlist: str, cover: Path) -> None: @click.pass_obj def delete_cover_playlist(ctx: Context, playlist: str) -> None: """Delete the cover art of a playlist. Accepts a playlist name.""" - from rose.playlists import delete_playlist_cover_art - delete_playlist_cover_art(ctx.config, playlist) @@ -561,9 +541,6 @@ def run( ignore: list[str], ) -> None: """Run an ad hoc rule.""" - from rose.rule_parser import MetadataRule - from rose.rules import execute_metadata_rule - if not actions: logger.info("No-Op: No actions passed") return @@ -577,16 +554,11 @@ def run( @click.pass_obj def run_stored(ctx: Context, dry_run: bool, yes: bool) -> None: """Run the rules stored in the config.""" - from rose.rules import execute_stored_metadata_rules - execute_stored_metadata_rules(ctx.config, dry_run=dry_run, confirm_yes=not yes) def parse_release_argument(r: str) -> str: """Takes in a release argument and normalizes it to the release ID.""" - from rose.cache import STORED_DATA_FILE_REGEX - from rose.common import valid_uuid - if valid_uuid(r): logger.debug(f"Treating release argument {r} as UUID") return r @@ -615,9 +587,6 @@ def parse_release_argument(r: str) -> str: def parse_track_argument(t: str) -> str: """Takes in a track argument and normalizes it to the track ID.""" - from rose.audiotags import AudioTags, UnsupportedFiletypeError - from rose.common import valid_uuid - if valid_uuid(t): logger.debug(f"Treating track argument {t} as UUID") return t @@ -700,3 +669,11 @@ def daemonize(pid_path: Path | None = None) -> None: with pid_path.open("w") as fp: fp.write(str(pid)) os._exit(0) + + +def valid_uuid(x: str) -> bool: + try: + uuid.UUID(x) + return True + except ValueError: + return False diff --git a/rose/cli_test.py b/rose_cli/cli_test.py similarity index 98% rename from rose/cli_test.py rename to rose_cli/cli_test.py index c5f38c8..48fa8b9 100644 --- a/rose/cli_test.py +++ b/rose_cli/cli_test.py @@ -7,7 +7,8 @@ from click.testing import CliRunner from rose.audiotags import AudioTags -from rose.cli import ( +from rose.config import Config +from rose_cli.cli import ( Context, InvalidReleaseArgError, InvalidTrackArgError, @@ -18,8 +19,7 @@ unwatch, watch, ) -from rose.config import Config -from rose.virtualfs_test import start_virtual_fs +from rose_vfs.virtualfs_test import start_virtual_fs @pytest.mark.usefixtures("seeded_cache") diff --git a/rose_vfs/__init__.py b/rose_vfs/__init__.py new file mode 100644 index 0000000..9bcd7fc --- /dev/null +++ b/rose_vfs/__init__.py @@ -0,0 +1,9 @@ +from rose import initialize_logging +from rose_vfs.virtualfs import mount_virtualfs, unmount_virtualfs + +__all__ = [ + "mount_virtualfs", + "unmount_virtualfs", +] + +initialize_logging() diff --git a/rose/virtualfs.py b/rose_vfs/virtualfs.py similarity index 99% rename from rose/virtualfs.py rename to rose_vfs/virtualfs.py index 8d52de6..814841c 100644 --- a/rose/virtualfs.py +++ b/rose_vfs/virtualfs.py @@ -55,15 +55,29 @@ import llfuse -from rose.audiotags import SUPPORTED_AUDIO_EXTENSIONS, AudioTags -from rose.cache import ( +from rose import ( STORED_DATA_FILE_REGEX, + SUPPORTED_AUDIO_EXTENSIONS, + AudioTags, CachedRelease, CachedTrack, + Config, + PathTemplate, + RoseError, + add_release_to_collage, + add_track_to_playlist, artist_exists, calculate_release_logtext, calculate_track_logtext, collage_exists, + create_collage, + create_playlist, + delete_collage, + delete_playlist, + delete_playlist_cover_art, + delete_release, + eval_release_template, + eval_track_template, genre_exists, get_collage, get_path_of_track_in_playlist, @@ -78,33 +92,18 @@ list_genres, list_labels, list_playlists, - list_releases_delete_this, playlist_exists, - update_cache_for_releases, -) -from rose.collages import ( - add_release_to_collage, - create_collage, - delete_collage, remove_release_from_collage, - rename_collage, -) -from rose.common import RoseError, sanitize_dirname, sanitize_filename -from rose.config import Config -from rose.playlists import ( - add_track_to_playlist, - create_playlist, - delete_playlist, - delete_playlist_cover_art, remove_track_from_playlist, + rename_collage, rename_playlist, + sanitize_dirname, + sanitize_filename, set_playlist_cover_art, -) -from rose.releases import ( - delete_release, set_release_cover_art, + update_cache_for_releases, ) -from rose.templates import PathTemplate, eval_release_template, eval_track_template +from rose.cache import list_releases_delete_this logger = logging.getLogger(__name__) diff --git a/rose/virtualfs_test.py b/rose_vfs/virtualfs_test.py similarity index 99% rename from rose/virtualfs_test.py rename to rose_vfs/virtualfs_test.py index b8a2990..6e2eec0 100644 --- a/rose/virtualfs_test.py +++ b/rose_vfs/virtualfs_test.py @@ -12,7 +12,7 @@ from conftest import retry_for_sec from rose.audiotags import AudioTags from rose.config import Config -from rose.virtualfs import mount_virtualfs, unmount_virtualfs +from rose_vfs.virtualfs import mount_virtualfs, unmount_virtualfs R1_VNAME = "Techno Man & Bass Man - 2023. Release 1" R2_VNAME = "Violin Woman (feat. Conductor Woman) - 2021. Release 2" diff --git a/setup.py b/setup.py index 9739e66..49aafa9 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ author="blissful", author_email="blissful@sunsetglow.net", license="Apache-2.0", - entry_points={"console_scripts": ["rose = rose.__main__:main"]}, + entry_points={"console_scripts": ["rose = rose_cli.__main__:main"]}, packages=setuptools.find_namespace_packages(where="."), package_data={"rose": ["*.sql", ".version"]}, install_requires=[