Skip to content

Commit

Permalink
cli todos
Browse files Browse the repository at this point in the history
  • Loading branch information
azuline committed Nov 3, 2023
1 parent 1c16764 commit 4e9cead
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 90 deletions.
2 changes: 1 addition & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def seeded_cache(config: Config) -> None:
(playlist_name, track_id, position, missing)
VALUES ('Lala Lisa' , 't1' , 1 , false)
, ('Lala Lisa' , 't3' , 2 , false);
""" # noqa: E501
"""
)

(config.music_source_dir / "!collages").mkdir()
Expand Down
16 changes: 8 additions & 8 deletions docs/AVAILABLE_COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ This document enumerates the commands available in Rosé's CLI.
First, a quick note on the structure: Rosé primarily organizes commands by the
resource they effect. Most commands are of the structure `rose {resource} {action}`.

- `fs/` _(See [Browsing with the Virtual Filesystem](./VIRTUAL_FILESYSTEM.md))_
- fs/ _(see [Browsing with the Virtual Filesystem](./VIRTUAL_FILESYSTEM.md))_
- `fs mount`: Mount the virtual filesystem onto the configured `$fuse_mount_dir`.
- `fs unmount`: Unmount the virtual filesystem by invoking `umount`.
- `cache/` _(See [Maintaining the Cache](./CACHE_MAINTENANCE.md))_
- cache/ _(see [Maintaining the Cache](./CACHE_MAINTENANCE.md))_
- `cache update`: Scan the source directory and update the read cache with
any new metadata changes.
- `cache watch`: Start a watcher that will trigger `cache update` for any
files and directories that have been modified.
- `cache unwatch`: Kill the running cache watcher process.
- `releases/` _(See [Managing Releases](./RELEASES.md))_
- releases/ _(see [Managing Releases](./RELEASES.md))_
- `releases print`: Print a single release's metadata in JSON.
- `releases print-all`: Print all releases' metadata in JSON, with an
optional matcher rule to filter out releases.
- `releases import`: Import a release directory into the managed source
- directory.
directory.
- `releases edit`: Edit a release's metadata as a text file in your
`$EDITOR`.
- `releases toggle-new`: Toggle the "new"-ness of a release.
Expand All @@ -40,12 +40,12 @@ resource they effect. Most commands are of the structure `rose {resource} {actio
suggest metadata improvements.
- `releases create-single`: Create a "phony" single release from a track and
copy the track into the new release.
- `tracks/`
- tracks/
- `tracks print`: Print a single track's metadata in JSON.
- `tracks print-all`: Print all tracks' metadata in JSON, with an optional
matcher rule to filter out tracks.
- `tracks run-rule`: Run one or more metadata actions on a track.
- `collages/` _(See [Managing Playlists & Collages](./PLAYLISTS_COLLAGES.md))_
- collages/ _(see [Managing Playlists & Collages](./PLAYLISTS_COLLAGES.md))_
- `collages print`: Print a single collage's metadata in JSON.
- `collages print-all`: Print all collages' metadata in JSON.
- `collages create`: Create a new collage.
Expand All @@ -56,7 +56,7 @@ resource they effect. Most commands are of the structure `rose {resource} {actio
- `collages rename`: Rename a collage.
- `collages add-release`: Add a release to a collage.
- `collages remove-release`: Remove a release from a collage.
- `playlists/` _(See [Managing Playlists & Collages](./PLAYLISTS_COLLAGES.md))_
- playlists/ _(see [Managing Playlists & Collages](./PLAYLISTS_COLLAGES.md))_
- `playlists print`: Print a single playlist's metadata in JSON.
- `playlists print-all`: Print all playlists' metadata in JSON.
- `playlists create`: Create a new playlist.
Expand All @@ -70,7 +70,7 @@ resource they effect. Most commands are of the structure `rose {resource} {actio
- `playlists set-cover`: Set the cover art for a playlist. Replaces any existing
cover art.
- `playlists delete-cover`: Remove the cover art of a playlist.
- `rules/` _(See [Managing Your Music Metadata](./METADATA_MANAGEMENT.md))_
- rules/ _(see [Managing Your Music Metadata](./METADATA_MANAGEMENT.md))_
- `rules run`: Run an ad hoc rule in the command line interface. You can also
easily test rules with the `--dry-run` flag.
- `rules run-stored`: Run the rules stored in the configuration file.
Expand Down
2 changes: 1 addition & 1 deletion rose/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
# 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
stream_template = "[%(asctime)s] %(levelname)s: %(message)s"
verbose_template = "[ts=%(asctime)s.%(msecs)d] [pid=%(process)d] [src=%(name)s:%(lineno)s] %(levelname)s: %(message)s" # noqa: E501
verbose_template = "[ts=%(asctime)s.%(msecs)d] [pid=%(process)d] [src=%(name)s:%(lineno)s] %(levelname)s: %(message)s"

stream_formatter = logging.Formatter(
stream_template if not LOG_EVEN_THOUGH_WERE_IN_TEST else verbose_template,
Expand Down
4 changes: 2 additions & 2 deletions rose/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def connect(c: Config) -> Iterator[sqlite3.Connection]:
conn.close()


def migrate_database(c: Config) -> None:
def maybe_invalidate_cache_database(c: Config) -> None:
"""
"Migrate" the database. If the schema in the database does not match that on disk, then nuke the
database and recreate it from scratch. Otherwise, no op.
Expand Down Expand Up @@ -1318,7 +1318,7 @@ def _update_cache_for_releases_executor(
WHERE t.id IN ({",".join(["?"]*len(upd_track_ids))})
OR r.id IN ({",".join(["?"]*len(upd_release_ids))})
GROUP BY t.id
""", # noqa: E501
""",
[*upd_track_ids, *upd_release_ids],
)

Expand Down
12 changes: 6 additions & 6 deletions rose/cache_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
list_playlists,
list_releases,
lock,
migrate_database,
maybe_invalidate_cache_database,
playlist_exists,
release_exists,
track_exists,
Expand All @@ -55,7 +55,7 @@ def test_schema(config: Config) -> None:
"""Test that the schema successfully bootstraps."""
with CACHE_SCHEMA_PATH.open("rb") as fp:
schema_hash = hashlib.sha256(fp.read()).hexdigest()
migrate_database(config)
maybe_invalidate_cache_database(config)
with connect(config) as conn:
cursor = conn.execute("SELECT schema_hash, config_hash, version FROM _schema_hash")
row = cursor.fetchone()
Expand Down Expand Up @@ -87,7 +87,7 @@ def test_migration(config: Config) -> None:

with CACHE_SCHEMA_PATH.open("rb") as fp:
latest_schema_hash = hashlib.sha256(fp.read()).hexdigest()
migrate_database(config)
maybe_invalidate_cache_database(config)
with connect(config) as conn:
cursor = conn.execute("SELECT schema_hash, config_hash, version FROM _schema_hash")
row = cursor.fetchone()
Expand Down Expand Up @@ -134,7 +134,7 @@ def test_update_cache_all(config: Config) -> None:
"""
INSERT INTO releases (id, source_path, virtual_dirname, added_at, datafile_mtime, title, releasetype, multidisc, formatted_artists)
VALUES ('aaaaaa', '/nonexistent', '0000-01-01T00:00:00+00:00', '999', 'nonexistent', 'aa', 'unknown', false, 'aa;aa')
""" # noqa: E501
"""
)

update_cache(config)
Expand Down Expand Up @@ -283,7 +283,7 @@ def test_update_cache_releases_preserves_track_ids_across_rebuilds(config: Confi

# Nuke the database.
config.cache_database_path.unlink()
migrate_database(config)
maybe_invalidate_cache_database(config)

# Repeat cache population.
update_cache_for_releases(config, [release_dir])
Expand Down Expand Up @@ -429,7 +429,7 @@ def test_update_cache_releases_delete_nonexistent(config: Config) -> None:
"""
INSERT INTO releases (id, source_path, virtual_dirname, added_at, datafile_mtime, title, releasetype, multidisc, formatted_artists)
VALUES ('aaaaaa', '/nonexistent', '0000-01-01T00:00:00+00:00', '999', 'nonexistent', 'aa', 'unknown', false, 'aa;aa')
""" # noqa: E501
"""
)
update_cache_evict_nonexistent_releases(config)
with connect(config) as conn:
Expand Down
58 changes: 26 additions & 32 deletions rose/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import click

from rose.cache import migrate_database, update_cache
from rose.cache import maybe_invalidate_cache_database, update_cache
from rose.collages import (
add_release_to_collage,
create_collage,
Expand Down Expand Up @@ -73,20 +73,17 @@ class Context:
# fmt: off
@click.group()
@click.option("--verbose", "-v", is_flag=True, help="Emit verbose logging.")
@click.option("--config", "-c", type=click.Path(path_type=Path), help="Override the config file location.") # noqa: E501
@click.option("--config", "-c", type=click.Path(path_type=Path), help="Override the config file location.")
@click.pass_context
# fmt: on
def cli(cc: click.Context, verbose: bool, config: Path | None = None) -> None:
"""A music manager with a virtual filesystem"""
cc.obj = Context(
config=Config.parse(config_path_override=config),
)

if verbose:
logging.getLogger().setLevel(logging.DEBUG)

# Migrate the database on every command invocation.
migrate_database(cc.obj.config)
maybe_invalidate_cache_database(cc.obj.config)


@cli.command()
Expand All @@ -110,7 +107,7 @@ def cache() -> None:

# fmt: off
@cache.command()
@click.option("--force", "-f", is_flag=True, help="Force re-read all data from disk, even for unchanged files.") # noqa: E501
@click.option("--force", "-f", is_flag=True, help="Force re-read all data from disk, even for unchanged files.")
@click.pass_obj
# fmt: on
def update(ctx: Context, force: bool) -> None:
Expand All @@ -120,7 +117,7 @@ def update(ctx: Context, force: bool) -> None:

# fmt: off
@cache.command()
@click.option("--foreground", "-f", is_flag=True, help="Run the filesystem watcher in the foreground (default: daemon).") # noqa: E501
@click.option("--foreground", "-f", is_flag=True, help="Run the filesystem watcher in the foreground (default: daemon).")
@click.pass_obj
# fmt: on
def watch(ctx: Context, foreground: bool) -> None:
Expand Down Expand Up @@ -155,7 +152,7 @@ def fs() -> None:

# fmt: off
@fs.command()
@click.option("--foreground", "-f", is_flag=True, help="Run the FUSE controller in the foreground (default: daemon).") # noqa: E501
@click.option("--foreground", "-f", is_flag=True, help="Run the FUSE controller in the foreground (default: daemon).")
@click.pass_obj
# fmt: on
def mount(ctx: Context, foreground: bool) -> None:
Expand Down Expand Up @@ -184,6 +181,7 @@ def unmount(ctx: Context) -> None:
@cli.group()
def releases() -> None:
"""Manage releases"""
# TODO: print / run-rule / extract-covers / add-metadata-url / search-metadata-urls / create-single / import


@releases.command(name="print-all")
Expand Down Expand Up @@ -211,6 +209,18 @@ def toggle_new(ctx: Context, release: str) -> None:
toggle_release_new(ctx.config, release)


@releases.command(name="delete")
@click.argument("release", type=str, nargs=1)
@click.pass_obj
def delete3(ctx: Context, release: str) -> None:
"""
Delete a release from the library. The release is moved to the trash bin, following the
freedesktop spec.
"""
release = parse_release_from_potential_path(ctx.config, release)
delete_release(ctx.config, release)


@releases.command()
@click.argument("release", type=str, nargs=1)
@click.argument("cover", type=click.Path(path_type=Path), nargs=1)
Expand All @@ -230,18 +240,6 @@ def delete_cover(ctx: Context, release: str) -> None:
delete_release_cover_art(ctx.config, release)


@releases.command(name="delete")
@click.argument("release", type=str, nargs=1)
@click.pass_obj
def delete3(ctx: Context, release: str) -> None:
"""
Delete a release from the library. The release is moved to the trash bin, following the
freedesktop spec.
"""
release = parse_release_from_potential_path(ctx.config, release)
delete_release(ctx.config, release)


@releases.command()
@click.argument("track_path", type=click.Path(path_type=Path), nargs=1)
@click.pass_obj
Expand All @@ -253,12 +251,14 @@ def create_single(ctx: Context, track_path: Path) -> None:
@cli.group()
def tracks() -> None:
"""Manage tracks"""
# TODO: print / print-all / run-rule
raise UnimplementedError("Coming soon!")


@cli.group()
def collages() -> None:
"""Manage collages"""
# TODO: print


@collages.command()
Expand Down Expand Up @@ -324,6 +324,7 @@ def print_all1(ctx: Context) -> None:
@cli.group()
def playlists() -> None:
"""Manage playlists"""
# TODO: print


@playlists.command(name="create")
Expand Down Expand Up @@ -406,13 +407,6 @@ def delete_cover2(ctx: Context, playlist: str) -> None:
delete_playlist_cover_art(ctx.config, playlist)


@playlists.command()
@click.pass_obj
def extract_covers(ctx: Context) -> None: # noqa: ARG001
"""Extract all embedded cover arts to external cover art files"""
raise UnimplementedError("Coming soon!")


@cli.group()
def rules() -> None:
"""Run metadata update rules on the entire library"""
Expand All @@ -422,7 +416,7 @@ def rules() -> None:
# fmt: off
@click.argument("matcher", type=str, nargs=1)
@click.argument("actions", type=str, nargs=-1)
@click.option("--dry-run", "-d", is_flag=True, help="Display intended changes without applying them.") # noqa: E501
@click.option("--dry-run", "-d", is_flag=True, help="Display intended changes without applying them.")
@click.option("--yes", "-y", is_flag=True, help="Bypass confirmation prompts.")
# fmt: on
@click.pass_obj
Expand All @@ -436,10 +430,10 @@ def run(ctx: Context, matcher: str, actions: list[str], dry_run: bool, yes: bool


@rules.command()
@click.option(
"--dry-run", "-d", is_flag=True, help="Display intended changes without applying them."
) # noqa: E501
# fmt: off
@click.option("--dry-run", "-d", is_flag=True, help="Display intended changes without applying them.")
@click.option("--yes", "-y", is_flag=True, help="Bypass confirmation prompts.")
# fmt: on
@click.pass_obj
def run_stored(ctx: Context, dry_run: bool, yes: bool) -> None:
"""Run the rules stored in the config"""
Expand Down
14 changes: 7 additions & 7 deletions rose/config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_config_full() -> None:
[[stored_metadata_rules]]
matcher = "tracktitle:lala"
actions = ["replace:hihi"]
""" # noqa: E501
"""
)

c = Config.parse(config_path_override=path)
Expand Down Expand Up @@ -333,7 +333,7 @@ def write(x: str) -> None:
Config.parse(config_path_override=path)
assert (
str(excinfo.value)
== f"Cannot specify both fuse_artists_whitelist and fuse_artists_blacklist in configuration file ({path}): must specify only one or the other" # noqa: E501
== f"Cannot specify both fuse_artists_whitelist and fuse_artists_blacklist in configuration file ({path}): must specify only one or the other"
)

# fuse_genres_whitelist + fuse_genres_blacklist
Expand All @@ -342,7 +342,7 @@ def write(x: str) -> None:
Config.parse(config_path_override=path)
assert (
str(excinfo.value)
== f"Cannot specify both fuse_genres_whitelist and fuse_genres_blacklist in configuration file ({path}): must specify only one or the other" # noqa: E501
== f"Cannot specify both fuse_genres_whitelist and fuse_genres_blacklist in configuration file ({path}): must specify only one or the other"
)

# fuse_labels_whitelist + fuse_labels_blacklist
Expand All @@ -351,7 +351,7 @@ def write(x: str) -> None:
Config.parse(config_path_override=path)
assert (
str(excinfo.value)
== f"Cannot specify both fuse_labels_whitelist and fuse_labels_blacklist in configuration file ({path}): must specify only one or the other" # noqa: E501
== f"Cannot specify both fuse_labels_whitelist and fuse_labels_blacklist in configuration file ({path}): must specify only one or the other"
)

# cover_art_stems
Expand Down Expand Up @@ -411,12 +411,12 @@ def write(x: str) -> None:
Config.parse(config_path_override=path)
assert (
str(excinfo.value)
== f"Invalid value in stored_metadata_rules in configuration file ({path}): list values must be a dict: got <class 'str'>" # noqa: E501
== f"Invalid value in stored_metadata_rules in configuration file ({path}): list values must be a dict: got <class 'str'>"
)
write(
config
+ '\nstored_metadata_rules = [{ matcher = "tracktitle:hi", actions = ["delete:hi"] }]'
) # noqa: E501
)
with pytest.raises(InvalidConfigValueError) as excinfo:
Config.parse(config_path_override=path)
assert (
Expand All @@ -427,5 +427,5 @@ def write(x: str) -> None:
delete:hi
^
Found another section after the action kind, but the delete action has no parameters. Please remove this section.
""" # noqa: E501
"""
)
2 changes: 1 addition & 1 deletion rose/releases_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def test_edit_release(monkeypatch: Any, config: Config, source_dir: Path) -> Non
cover_image_path=None,
added_at=release.added_at,
datafile_mtime=release.datafile_mtime,
virtual_dirname="{NEW} BLACKPINK;JISOO - 2222. I Really Love Blackpink - Single [J-Pop;Pop-Rap]", # noqa: E501
virtual_dirname="{NEW} BLACKPINK;JISOO - 2222. I Really Love Blackpink - Single [J-Pop;Pop-Rap]",
title="I Really Love Blackpink",
releasetype="single",
year=2222,
Expand Down
2 changes: 1 addition & 1 deletion rose/rule_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ def parse_action(raw: str, action_number: int) -> MetadataAction:
"split",
"add",
"delete",
] # noqa: E501
]
if action_kind not in valid_actions:
feedback = f"Invalid action kind: must be one of {{{', '.join(valid_actions)}}}."
if idx == 0 and ":" in raw:
Expand Down
Loading

0 comments on commit 4e9cead

Please sign in to comment.