Skip to content

Commit

Permalink
add releases run-rule command
Browse files Browse the repository at this point in the history
  • Loading branch information
azuline committed Nov 3, 2023
1 parent 6213c3e commit 0e52cbb
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 5 deletions.
52 changes: 52 additions & 0 deletions docs/RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,58 @@ $ tree "1. Releases/"

See the "Text-Based Release Editing" section in [Improving Your Music Metadata](./METADATA_TOOLS.md).

## Run Rule Engine Action on Release

Rosé allows you to run an action from the rule engine on all tracks in a
release. With this command, you do not need to specify a matcher; this command
auto-matches all tracks in the release.

See [Improving Your Music Metadata](./METADATA_TOOLS.md) for documentation on
the rules engine.

```bash
$ rose releases run-rule '{NEW} The Strokes - 2001. Is This It' 'genre::add:Indie Rock'
The Strokes - 2001. Is This It/01. Is This It.opus
genre: [] -> ['Indie Rock']
The Strokes - 2001. Is This It/02. The Modern Age.opus
genre: [] -> ['Indie Rock']
The Strokes - 2001. Is This It/03. Soma.opus
genre: [] -> ['Indie Rock']
The Strokes - 2001. Is This It/04. Barely Legal.opus
genre: [] -> ['Indie Rock']
The Strokes - 2001. Is This It/05. Someday.opus
genre: [] -> ['Indie Rock']
The Strokes - 2001. Is This It/06. Alone, Together.opus
genre: [] -> ['Indie Rock']
The Strokes - 2001. Is This It/07. Last Nite.opus
genre: [] -> ['Indie Rock']
The Strokes - 2001. Is This It/08. Hard to Explain.opus
genre: [] -> ['Indie Rock']
The Strokes - 2001. Is This It/09. When It Started.opus
genre: [] -> ['Indie Rock']
The Strokes - 2001. Is This It/10. Trying Your Luck.opus
genre: [] -> ['Indie Rock']
The Strokes - 2001. Is This It/11. Take It or Leave It.opus
genre: [] -> ['Indie Rock']

Write changes to 11 tracks? [Y/n] y

[16:26:42] INFO: Writing tag changes for actions genre::add
[16:26:42] INFO: Wrote tag changes to The Strokes - 2001. Is This It/01. Is This It.opus
[16:26:42] INFO: Wrote tag changes to The Strokes - 2001. Is This It/02. The Modern Age.opus
[16:26:42] INFO: Wrote tag changes to The Strokes - 2001. Is This It/03. Soma.opus
[16:26:42] INFO: Wrote tag changes to The Strokes - 2001. Is This It/04. Barely Legal.opus
[16:26:42] INFO: Wrote tag changes to The Strokes - 2001. Is This It/05. Someday.opus
[16:26:42] INFO: Wrote tag changes to The Strokes - 2001. Is This It/06. Alone, Together.opus
[16:26:42] INFO: Wrote tag changes to The Strokes - 2001. Is This It/07. Last Nite.opus
[16:26:42] INFO: Wrote tag changes to The Strokes - 2001. Is This It/08. Hard to Explain.opus
[16:26:42] INFO: Wrote tag changes to The Strokes - 2001. Is This It/09. When It Started.opus
[16:26:42] INFO: Wrote tag changes to The Strokes - 2001. Is This It/10. Trying Your Luck.opus
[16:26:42] INFO: Wrote tag changes to The Strokes - 2001. Is This It/11. Take It or Leave It.opus

Applied tag changes to 11 tracks!
```

## Create "Phony" Single Release

Let's say that you did not enjoy a release, and want to delete it from your
Expand Down
24 changes: 23 additions & 1 deletion rose/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@
delete_release_cover_art,
dump_releases,
edit_release,
run_actions_on_release,
set_release_cover_art,
toggle_release_new,
)
from rose.rule_parser import MetadataRule
from rose.rule_parser import MetadataAction, MetadataRule
from rose.rules import execute_metadata_rule, execute_stored_metadata_rules
from rose.virtualfs import VirtualPath, mount_virtualfs, unmount_virtualfs
from rose.watcher import start_watchdog
Expand Down Expand Up @@ -233,6 +234,27 @@ def delete_cover(ctx: Context, release: str) -> None:
delete_release_cover_art(ctx.config, release)


@releases.command()
# fmt: off
@click.argument("release", 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.")
@click.option("--yes", "-y", is_flag=True, help="Bypass confirmation prompts.")
# fmt: on
@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"""
release = parse_release_from_potential_path(ctx.config, release)
parsed_actions = [MetadataAction.parse(a) for a in actions]
run_actions_on_release(
ctx.config,
release,
parsed_actions,
dry_run=dry_run,
confirm_yes=not yes,
)


@releases.command()
@click.argument("track_path", type=click.Path(path_type=Path), nargs=1)
@click.pass_obj
Expand Down
18 changes: 18 additions & 0 deletions rose/releases.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
)
from rose.common import InvalidCoverArtFileError, RoseError, valid_uuid
from rose.config import Config
from rose.rule_parser import MetadataAction
from rose.rules import execute_metadata_actions

logger = logging.getLogger()

Expand Down Expand Up @@ -367,6 +369,22 @@ def edit_release(
update_cache_for_releases(c, [release.source_path], force=True)


def run_actions_on_release(
c: Config,
release_id_or_virtual_dirname: str,
actions: list[MetadataAction],
*,
dry_run: bool = False,
confirm_yes: bool = False,
) -> None:
"""Run rule engine actions on a release."""
rdata = get_release(c, release_id_or_virtual_dirname)
if rdata is None:
raise ReleaseDoesNotExistError(f"Release {release_id_or_virtual_dirname} does not exist")
audiotags = [AudioTags.from_file(t.source_path) for t in rdata[1]]
execute_metadata_actions(c, actions, audiotags, dry_run=dry_run, confirm_yes=confirm_yes)


def create_single_release(c: Config, track_path: Path) -> None:
"""Takes a track and copies it into a brand new "single" release with only that track."""
if not track_path.is_file():
Expand Down
9 changes: 9 additions & 0 deletions rose/releases_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
dump_releases,
edit_release,
resolve_release_ids,
run_actions_on_release,
set_release_cover_art,
toggle_release_new,
)
from rose.rule_parser import MetadataAction


def test_delete_release(config: Config) -> None:
Expand Down Expand Up @@ -450,6 +452,13 @@ def test_dump_releases(config: Config) -> None:
]


def test_run_action_on_release(config: Config, source_dir: Path) -> None:
action = MetadataAction.parse("tracktitle::replace:Bop")
run_actions_on_release(config, "ilovecarly", [action])
af = AudioTags.from_file(source_dir / "Test Release 2" / "01.m4a")
assert af.title == "Bop"


def test_resolve_release_ids(config: Config) -> None:
shutil.copytree(TEST_RELEASE_1, config.music_source_dir / TEST_RELEASE_1.name)
update_cache(config)
Expand Down
14 changes: 10 additions & 4 deletions rose/rule_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ def parse(cls, raw: str) -> MetadataMatcher:
feedback="Found another section after the pattern, but the pattern must be the last section. Perhaps you meant to escape this colon?",
)

return cls(tags=tags, pattern=pattern)
matcher = cls(tags=tags, pattern=pattern)
logger.debug(f"Parsed rule matcher {raw=} as {matcher=}")
return matcher


@dataclass
Expand Down Expand Up @@ -230,13 +232,15 @@ def __str__(self) -> str:
def parse(
cls,
raw: str,
action_number: int,
action_number: int | None = None,
# If there is a matcher for the action, pass it here to set the defaults.
matcher: MetadataMatcher | None = None,
) -> MetadataAction:
idx = 0
# Common arguments to feed into Syntax Error.
err = {"rule_name": f"action {action_number}", "rule": raw}
err = {"rule": raw, "rule_name": "action"}
if action_number:
err["rule_name"] += f" {action_number}"

# First, determine whether we have a matcher section or not. The matcher section is optional,
# but present if there is an unescaped `::`.
Expand Down Expand Up @@ -436,7 +440,9 @@ def parse(
else: # pragma: no cover
raise RoseError(f"Impossible: unknown action_kind {action_kind=}")

return cls(behavior=behavior, tags=tags, pattern=pattern)
action = cls(behavior=behavior, tags=tags, pattern=pattern)
logger.debug(f"Parsed rule action {raw=} {matcher=} as {action=}")
return action


@dataclass
Expand Down

0 comments on commit 0e52cbb

Please sign in to comment.