rekordbox-edit 0.4.0.dev20__tar.gz → 0.4.0.dev22__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/AGENTS.md +16 -2
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/CHANGELOG.md +20 -3
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/PKG-INFO +1 -1
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/pyproject.toml +1 -1
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/cli.py +3 -3
- rekordbox_edit-0.4.0.dev22/rekordbox_edit/commands/edit.py +195 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/ruff.toml +6 -2
- rekordbox_edit-0.4.0.dev22/tests/commands/test_edit.py +429 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/uv.lock +1 -1
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.agent-style/RULES.md +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.agent-style/claude-code.md +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.github/actions/commitizen-bump/action.yml +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.github/actions/commitizen-bump/commitizen-bump.sh +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.github/actions/install/action.yml +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.github/actions/lint/action.yml +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.github/actions/test/action.yml +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.github/workflows/cd.yml +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.github/workflows/ci.yml +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.github/workflows/publish.yml +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.github/workflows/release.yml +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.gitignore +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.pre-commit-config.yaml +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/CLAUDE.md +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/CONTRIBUTING.md +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/LICENSE +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/Makefile +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/README.md +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/codecov.yml +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/__init__.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/_click.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/__init__.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/convert.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/search.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/logger.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/query.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/utils.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/renovate.json5 +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/tests/__init__.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/tests/commands/__init__.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/tests/commands/test_convert.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/tests/commands/test_search.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/tests/conftest.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/tests/test_logger.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/tests/test_query.py +0 -0
- {rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/tests/test_utils.py +0 -0
|
@@ -60,11 +60,23 @@ Full directive text, BAD/GOOD example pairs, and rationale per rule: see `.agent
|
|
|
60
60
|
## Code style
|
|
61
61
|
|
|
62
62
|
- Only comment non-obvious code — hidden constraints, subtle invariants, workarounds. If a future reader would not be confused by the comment's absence, leave it out.
|
|
63
|
-
- Do not record conversation history, decisions, or task context in comments.
|
|
63
|
+
- Do not record conversation history, decisions, or task context in comments.
|
|
64
64
|
- Keep function and class header comments terse. No more than a paragraph.
|
|
65
65
|
- Name identifiers so the code explains itself; rename rather than annotate.
|
|
66
|
+
- Follow the "Rule of Three" as a guidepost for factoring out shared logic.
|
|
67
|
+
- Break out long functions in to multiple smaller testable units once they exceed a few hundred lines of code. Command entry points can be a bit longer than other functions.
|
|
66
68
|
- Emphasis on consistency of patterns across the codebase, except where clarity for other devs would be improved by diverging from a pattern.
|
|
67
69
|
|
|
70
|
+
### Logging and Exceptions
|
|
71
|
+
|
|
72
|
+
Every module should get and use its own `logger` for all logging purposes and follow semantic best practices of log levels:
|
|
73
|
+
|
|
74
|
+
- `debug` logs should be used to record relevant application state at significant logic branches and points of execution. These are not printed to the user unless the `--print debug` option is provided.
|
|
75
|
+
- `info` logs are the primary means of showing output to the user, and should be used sparingly/as necessary.
|
|
76
|
+
- `warning` logs communicate potential issues with application state or dangerous/unexpected circumstances that do not prevent execution from continuing.
|
|
77
|
+
- `error` logs communicate that invalid application or system state was encountered and needs attention, or invalid input was given, and execution cannot continue.
|
|
78
|
+
- `critical` logs communicate that an unexpected/unstable state was reached and execution was interrupted.
|
|
79
|
+
|
|
68
80
|
## Testing
|
|
69
81
|
|
|
70
82
|
- Cover user-visible behavior and code paths that pose risk to the database or user file system. Skip tests that exist only to raise coverage.
|
|
@@ -76,7 +88,9 @@ Full directive text, BAD/GOOD example pairs, and rationale per rule: see `.agent
|
|
|
76
88
|
|
|
77
89
|
- Implement the smallest functional slice first, then layer features in follow-up commits. Each commit should be "green" and independently deployable.
|
|
78
90
|
- Follow Conventional Commits (see `CONTRIBUTING.md` and the schema in `pyproject.toml`). No `Co-Authored-By` trailer.
|
|
79
|
-
-
|
|
91
|
+
- Commit descriptions should be terse and use active tenses (e.g. "add feature")
|
|
92
|
+
- Commit bodies should describe changes, but should not include agent conversation context, decisions, or plan notes.
|
|
93
|
+
- The agent may commit. But the user always handles pushes, PR creation, and rebases after merges.
|
|
80
94
|
- Do not commit spec, brainstorming, or design documents (e.g. anything under `docs/superpowers/specs/`).
|
|
81
95
|
|
|
82
96
|
## Tooling
|
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
## v0.4.0.
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
## v0.4.0.dev22 (2026-05-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
- feat(edit): add --match for literal find/replace within field value
|
|
5
|
+
- Introduces a _compute_new_value helper and --match option so that
|
|
6
|
+
rbe edit can replace a substring of the current field value instead of
|
|
7
|
+
performing a wholesale replacement; None current values and non-matching
|
|
8
|
+
patterns are treated as no-ops.
|
|
9
|
+
- chore: add max-complexity lint rule
|
|
10
|
+
- docs: update AGENTS.md
|
|
11
|
+
- test: add coverage for --interactive + --yes skipping all confirms
|
|
12
|
+
- feat: adds edit command
|
|
13
|
+
- Adds an `edit` subcommand to the CLI with:
|
|
14
|
+
- Title field support via a required FIELD argument and --replace option
|
|
15
|
+
- Single-track safety guard that aborts when >1 track would be modified
|
|
16
|
+
- --dry-run mode that previews changes without writing to the database
|
|
17
|
+
- --yes flag to skip confirmation, --interactive to confirm per-track
|
|
18
|
+
- Scripting mode (--print=ids) requiring --yes or --dry-run
|
|
19
|
+
- Piped stdin rejection without --yes or --dry-run
|
|
20
|
+
- All global filter flags forwarded to get_filtered_content
|
|
4
21
|
- docs: add AGENTS.md with project conventions and CLAUDE.md symlink
|
|
5
22
|
- chore: add renovate config
|
|
6
23
|
- feat: Add --path and --exact-path search filters
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rekordbox-edit
|
|
3
|
-
Version: 0.4.0.
|
|
3
|
+
Version: 0.4.0.dev22
|
|
4
4
|
Summary: Tools for managing and modifying a RekordBox library en-masse
|
|
5
5
|
Project-URL: Homepage, https://github.com/jviall/rekordbox-edit
|
|
6
6
|
Project-URL: Repository, https://github.com/jviall/rekordbox-edit
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rekordbox-edit"
|
|
7
|
-
version = "0.4.0.
|
|
7
|
+
version = "0.4.0.dev22"
|
|
8
8
|
description = "Tools for managing and modifying a RekordBox library en-masse"
|
|
9
9
|
authors = [{ name = "James Viall", email= "jamesviall@pm.me"}]
|
|
10
10
|
license = "MIT"
|
|
@@ -7,6 +7,7 @@ import sys
|
|
|
7
7
|
import click
|
|
8
8
|
|
|
9
9
|
from rekordbox_edit.commands.convert import convert_command
|
|
10
|
+
from rekordbox_edit.commands.edit import edit_command
|
|
10
11
|
from rekordbox_edit.commands.search import search_command
|
|
11
12
|
from rekordbox_edit.logger import get_debug_file_path, setup_logging
|
|
12
13
|
|
|
@@ -23,9 +24,8 @@ def cli():
|
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
cli.add_command(search_command)
|
|
26
|
-
cli.add_command(
|
|
27
|
-
|
|
28
|
-
)
|
|
27
|
+
cli.add_command(convert_command)
|
|
28
|
+
cli.add_command(edit_command)
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def main():
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Edit command for rekordbox-edit."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from pyrekordbox import Rekordbox6Database
|
|
9
|
+
|
|
10
|
+
from rekordbox_edit._click import (
|
|
11
|
+
PrintChoice,
|
|
12
|
+
add_click_options,
|
|
13
|
+
global_click_filters,
|
|
14
|
+
print_option,
|
|
15
|
+
track_ids_argument,
|
|
16
|
+
)
|
|
17
|
+
from rekordbox_edit.logger import get_debug_file_path, set_level
|
|
18
|
+
from rekordbox_edit.query import get_filtered_content
|
|
19
|
+
from rekordbox_edit.utils import UserQuit, confirm, print_track_info
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Maps CLI field names to DjmdContent column attribute names.
|
|
24
|
+
FIELD_COLUMNS = {
|
|
25
|
+
"Title": "Title",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _compute_new_value(
|
|
30
|
+
current: str | int | None,
|
|
31
|
+
match_pattern: str | None,
|
|
32
|
+
replace_value: str | int,
|
|
33
|
+
) -> str | int | None:
|
|
34
|
+
"""Derive the new field value."""
|
|
35
|
+
if current is None:
|
|
36
|
+
return None
|
|
37
|
+
if match_pattern is not None:
|
|
38
|
+
return str(current).replace(match_pattern, str(replace_value))
|
|
39
|
+
return replace_value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@click.command(
|
|
43
|
+
epilog=f"Debug logs for each run can be found at:\n{get_debug_file_path().parent}"
|
|
44
|
+
)
|
|
45
|
+
@add_click_options([*global_click_filters, print_option])
|
|
46
|
+
@click.option(
|
|
47
|
+
"--interactive",
|
|
48
|
+
"-i",
|
|
49
|
+
is_flag=True,
|
|
50
|
+
help="Confirm each track individually before editing",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"--yes",
|
|
54
|
+
"-y",
|
|
55
|
+
is_flag=True,
|
|
56
|
+
help="Skip confirmation prompt",
|
|
57
|
+
)
|
|
58
|
+
@click.option(
|
|
59
|
+
"--dry-run",
|
|
60
|
+
is_flag=True,
|
|
61
|
+
help="Show what would change without writing to the database",
|
|
62
|
+
)
|
|
63
|
+
@click.option(
|
|
64
|
+
"--replace",
|
|
65
|
+
"replace_value",
|
|
66
|
+
required=True,
|
|
67
|
+
help="The new value to write to the field",
|
|
68
|
+
)
|
|
69
|
+
@click.option(
|
|
70
|
+
"--match",
|
|
71
|
+
"match_pattern",
|
|
72
|
+
default=None,
|
|
73
|
+
metavar="PATTERN",
|
|
74
|
+
help="Find this literal string within the field value and replace only that portion",
|
|
75
|
+
)
|
|
76
|
+
@track_ids_argument
|
|
77
|
+
@click.argument(
|
|
78
|
+
"field",
|
|
79
|
+
type=click.Choice(list(FIELD_COLUMNS.keys()), case_sensitive=False),
|
|
80
|
+
)
|
|
81
|
+
def edit_command(
|
|
82
|
+
field: str,
|
|
83
|
+
replace_value: str,
|
|
84
|
+
match_pattern: str | None,
|
|
85
|
+
dry_run: bool,
|
|
86
|
+
yes: bool,
|
|
87
|
+
interactive: bool,
|
|
88
|
+
track_ids: List[str] | None,
|
|
89
|
+
track_id: List[str] | None,
|
|
90
|
+
playlist: List[str] | None,
|
|
91
|
+
exact_playlist: List[str] | None,
|
|
92
|
+
album: List[str] | None,
|
|
93
|
+
exact_album: List[str] | None,
|
|
94
|
+
artist: List[str] | None,
|
|
95
|
+
exact_artist: List[str] | None,
|
|
96
|
+
title: List[str] | None,
|
|
97
|
+
exact_title: List[str] | None,
|
|
98
|
+
path: List[str] | None,
|
|
99
|
+
exact_path: List[str] | None,
|
|
100
|
+
format: List[str] | None,
|
|
101
|
+
match_all: bool,
|
|
102
|
+
print_opt: PrintChoice | None,
|
|
103
|
+
):
|
|
104
|
+
"""Edit a metadata field on tracks in the RekordBox database."""
|
|
105
|
+
|
|
106
|
+
set_level(print_opt)
|
|
107
|
+
|
|
108
|
+
piped_stdin = False
|
|
109
|
+
if not sys.stdin.isatty():
|
|
110
|
+
stdin_data = sys.stdin.read().strip()
|
|
111
|
+
if stdin_data:
|
|
112
|
+
piped_stdin = True
|
|
113
|
+
track_ids = list(track_ids or []) + stdin_data.split()
|
|
114
|
+
|
|
115
|
+
scripting_mode = print_opt in (PrintChoice.IDS, PrintChoice.SILENT)
|
|
116
|
+
if scripting_mode and not (dry_run or yes):
|
|
117
|
+
raise click.UsageError(
|
|
118
|
+
"--print=ids or --print=silent requires --dry-run or --yes to skip confirmation"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if piped_stdin and not (dry_run or yes):
|
|
122
|
+
raise click.UsageError("Piping track IDs into edit requires --dry-run or --yes")
|
|
123
|
+
|
|
124
|
+
db = Rekordbox6Database()
|
|
125
|
+
if not db.session:
|
|
126
|
+
raise RuntimeError("Failed to connect to Rekordbox Database: No Session.")
|
|
127
|
+
|
|
128
|
+
result = get_filtered_content(
|
|
129
|
+
db,
|
|
130
|
+
track_id_args=track_ids,
|
|
131
|
+
track_ids=track_id,
|
|
132
|
+
playlists=playlist,
|
|
133
|
+
exact_playlists=exact_playlist,
|
|
134
|
+
artists=artist,
|
|
135
|
+
exact_artists=exact_artist,
|
|
136
|
+
albums=album,
|
|
137
|
+
exact_albums=exact_album,
|
|
138
|
+
titles=title,
|
|
139
|
+
exact_titles=exact_title,
|
|
140
|
+
paths=path,
|
|
141
|
+
exact_paths=exact_path,
|
|
142
|
+
formats=format,
|
|
143
|
+
match_all=match_all,
|
|
144
|
+
)
|
|
145
|
+
tracks = result.scalars().all()
|
|
146
|
+
|
|
147
|
+
col_name = FIELD_COLUMNS[field]
|
|
148
|
+
edits = []
|
|
149
|
+
for track in tracks:
|
|
150
|
+
current = getattr(track, col_name)
|
|
151
|
+
new_value = _compute_new_value(current, match_pattern, replace_value)
|
|
152
|
+
if new_value is None or new_value == current:
|
|
153
|
+
continue
|
|
154
|
+
edits.append((track, new_value))
|
|
155
|
+
|
|
156
|
+
if not edits:
|
|
157
|
+
logger.info("No changes to make.")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
if len(edits) > 1:
|
|
161
|
+
raise click.UsageError(
|
|
162
|
+
f"Found {len(edits)} tracks that would be edited. "
|
|
163
|
+
"Refine your filters, or use --dry-run to inspect."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
print_track_info([t for t, _ in edits])
|
|
167
|
+
|
|
168
|
+
if dry_run:
|
|
169
|
+
if print_opt is PrintChoice.IDS:
|
|
170
|
+
print(" ".join(str(t.ID) for t, _ in edits))
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
if not yes and not interactive:
|
|
174
|
+
try:
|
|
175
|
+
if not confirm(f"Apply {len(edits)} edit(s)?", default=True):
|
|
176
|
+
logger.info("Cancelled.")
|
|
177
|
+
return
|
|
178
|
+
except UserQuit:
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
for track, new_value in edits:
|
|
182
|
+
if interactive and not yes:
|
|
183
|
+
try:
|
|
184
|
+
if not confirm(f" Edit {track.ID}?", default=True):
|
|
185
|
+
continue
|
|
186
|
+
except UserQuit:
|
|
187
|
+
logger.info("Cancelled.")
|
|
188
|
+
return
|
|
189
|
+
setattr(track, col_name, new_value)
|
|
190
|
+
|
|
191
|
+
db.session.commit()
|
|
192
|
+
logger.info(f"Applied {len(edits)} edit(s).")
|
|
193
|
+
|
|
194
|
+
if print_opt is PrintChoice.IDS:
|
|
195
|
+
print(" ".join(str(t.ID) for t, _ in edits))
|
|
@@ -44,9 +44,10 @@ select = [
|
|
|
44
44
|
"E7",
|
|
45
45
|
"E9",
|
|
46
46
|
"F",
|
|
47
|
-
"W"
|
|
48
|
-
|
|
47
|
+
"W",
|
|
48
|
+
"C90",
|
|
49
49
|
]
|
|
50
|
+
|
|
50
51
|
ignore = []
|
|
51
52
|
|
|
52
53
|
# Allow fix for all enabled rules (when `--fix`) is provided.
|
|
@@ -56,6 +57,9 @@ unfixable = []
|
|
|
56
57
|
# Allow unused variables when underscore-prefixed.
|
|
57
58
|
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
58
59
|
|
|
60
|
+
[lint.mccabe]
|
|
61
|
+
max-complexity = 50
|
|
62
|
+
|
|
59
63
|
[format]
|
|
60
64
|
# Like Black, use double quotes for strings.
|
|
61
65
|
quote-style = "double"
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"""Unit tests for edit command."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import Mock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from rekordbox_edit.commands.edit import edit_command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture(autouse=True)
|
|
11
|
+
def mock_logger():
|
|
12
|
+
with patch("rekordbox_edit.commands.edit.logger") as mock_log:
|
|
13
|
+
yield mock_log
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _make_db_and_result(tracks):
|
|
17
|
+
"""Return (mock_db_class, mock_get_filtered_content) with given tracks."""
|
|
18
|
+
mock_db = Mock()
|
|
19
|
+
mock_db.session = Mock()
|
|
20
|
+
mock_result = Mock()
|
|
21
|
+
mock_result.scalars.return_value.all.return_value = tracks
|
|
22
|
+
return mock_db, mock_result
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestEditCommandPhase1:
|
|
26
|
+
"""Phase 1: Title field, wholesale replace, single-track guard, confirm flow."""
|
|
27
|
+
|
|
28
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
29
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
30
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
31
|
+
def test_sets_title_and_commits(
|
|
32
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
33
|
+
):
|
|
34
|
+
"""--replace sets the field on the matched track and commits the session."""
|
|
35
|
+
track = make_djmd_content_item(Title="Old Title")
|
|
36
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
37
|
+
mock_db_class.return_value = mock_db
|
|
38
|
+
mock_gfc.return_value = mock_result
|
|
39
|
+
mock_confirm.return_value = True
|
|
40
|
+
|
|
41
|
+
from click.testing import CliRunner
|
|
42
|
+
result = CliRunner().invoke(edit_command, ["Title", "--replace", "New Title"])
|
|
43
|
+
|
|
44
|
+
assert result.exit_code == 0
|
|
45
|
+
assert track.Title == "New Title"
|
|
46
|
+
mock_db.session.commit.assert_called_once()
|
|
47
|
+
|
|
48
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
49
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
50
|
+
def test_noop_tracks_are_skipped(
|
|
51
|
+
self, mock_db_class, mock_gfc, make_djmd_content_item
|
|
52
|
+
):
|
|
53
|
+
"""Tracks whose current value already equals --replace are not committed."""
|
|
54
|
+
track = make_djmd_content_item(Title="Same Title")
|
|
55
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
56
|
+
mock_db_class.return_value = mock_db
|
|
57
|
+
mock_gfc.return_value = mock_result
|
|
58
|
+
|
|
59
|
+
from click.testing import CliRunner
|
|
60
|
+
result = CliRunner().invoke(edit_command, ["Title", "--replace", "Same Title", "--yes"])
|
|
61
|
+
|
|
62
|
+
assert result.exit_code == 0
|
|
63
|
+
mock_db.session.commit.assert_not_called()
|
|
64
|
+
|
|
65
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
66
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
67
|
+
def test_single_track_guard_aborts_on_multiple_matches(
|
|
68
|
+
self, mock_db_class, mock_gfc, make_djmd_content_item
|
|
69
|
+
):
|
|
70
|
+
"""When >1 track would change and --multi is absent, exit with non-zero code."""
|
|
71
|
+
tracks = [
|
|
72
|
+
make_djmd_content_item(Title="Track A"),
|
|
73
|
+
make_djmd_content_item(Title="Track B"),
|
|
74
|
+
]
|
|
75
|
+
mock_db, mock_result = _make_db_and_result(tracks)
|
|
76
|
+
mock_db_class.return_value = mock_db
|
|
77
|
+
mock_gfc.return_value = mock_result
|
|
78
|
+
|
|
79
|
+
from click.testing import CliRunner
|
|
80
|
+
result = CliRunner().invoke(edit_command, ["Title", "--replace", "New", "--yes"])
|
|
81
|
+
|
|
82
|
+
assert result.exit_code != 0
|
|
83
|
+
mock_db.session.commit.assert_not_called()
|
|
84
|
+
|
|
85
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
86
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
87
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
88
|
+
def test_dry_run_does_not_commit(
|
|
89
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
90
|
+
):
|
|
91
|
+
"""--dry-run shows preview but does not write to the database."""
|
|
92
|
+
track = make_djmd_content_item(Title="Old Title")
|
|
93
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
94
|
+
mock_db_class.return_value = mock_db
|
|
95
|
+
mock_gfc.return_value = mock_result
|
|
96
|
+
|
|
97
|
+
from click.testing import CliRunner
|
|
98
|
+
result = CliRunner().invoke(edit_command, ["Title", "--replace", "New Title", "--dry-run"])
|
|
99
|
+
|
|
100
|
+
assert result.exit_code == 0
|
|
101
|
+
mock_db.session.commit.assert_not_called()
|
|
102
|
+
mock_confirm.assert_not_called()
|
|
103
|
+
|
|
104
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
105
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
106
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
107
|
+
def test_yes_skips_confirm(
|
|
108
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
109
|
+
):
|
|
110
|
+
"""--yes applies changes without prompting."""
|
|
111
|
+
track = make_djmd_content_item(Title="Old Title")
|
|
112
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
113
|
+
mock_db_class.return_value = mock_db
|
|
114
|
+
mock_gfc.return_value = mock_result
|
|
115
|
+
|
|
116
|
+
from click.testing import CliRunner
|
|
117
|
+
result = CliRunner().invoke(edit_command, ["Title", "--replace", "New Title", "--yes"])
|
|
118
|
+
|
|
119
|
+
assert result.exit_code == 0
|
|
120
|
+
mock_confirm.assert_not_called()
|
|
121
|
+
mock_db.session.commit.assert_called_once()
|
|
122
|
+
|
|
123
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
124
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
125
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
126
|
+
def test_interactive_confirms_each_track(
|
|
127
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
128
|
+
):
|
|
129
|
+
"""--interactive prompts per track (skipping the batch prompt)."""
|
|
130
|
+
track = make_djmd_content_item(Title="Old Title")
|
|
131
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
132
|
+
mock_db_class.return_value = mock_db
|
|
133
|
+
mock_gfc.return_value = mock_result
|
|
134
|
+
mock_confirm.return_value = True
|
|
135
|
+
|
|
136
|
+
from click.testing import CliRunner
|
|
137
|
+
result = CliRunner().invoke(
|
|
138
|
+
edit_command, ["Title", "--replace", "New Title", "--interactive"]
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
assert result.exit_code == 0
|
|
142
|
+
mock_confirm.assert_called_once()
|
|
143
|
+
mock_db.session.commit.assert_called_once()
|
|
144
|
+
|
|
145
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
146
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
147
|
+
def test_piped_stdin_requires_yes_or_dry_run(self, mock_db_class, mock_gfc):
|
|
148
|
+
"""Piping track IDs into edit without --yes or --dry-run is rejected."""
|
|
149
|
+
mock_db = Mock()
|
|
150
|
+
mock_db.session = Mock()
|
|
151
|
+
mock_db_class.return_value = mock_db
|
|
152
|
+
|
|
153
|
+
from click.testing import CliRunner
|
|
154
|
+
result = CliRunner().invoke(
|
|
155
|
+
edit_command, ["Title", "--replace", "New Title"], input="12345"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
assert result.exit_code != 0
|
|
159
|
+
|
|
160
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
161
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
162
|
+
def test_scripting_mode_requires_yes_or_dry_run(self, mock_db_class, mock_gfc):
|
|
163
|
+
"""--print=ids without --yes or --dry-run is rejected."""
|
|
164
|
+
mock_db = Mock()
|
|
165
|
+
mock_db.session = Mock()
|
|
166
|
+
mock_db_class.return_value = mock_db
|
|
167
|
+
|
|
168
|
+
from click.testing import CliRunner
|
|
169
|
+
result = CliRunner().invoke(
|
|
170
|
+
edit_command, ["Title", "--replace", "New Title", "--print", "ids"]
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
assert result.exit_code != 0
|
|
174
|
+
|
|
175
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
176
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
177
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
178
|
+
def test_filters_forwarded_to_get_filtered_content(
|
|
179
|
+
self, mock_db_class, mock_gfc, mock_confirm
|
|
180
|
+
):
|
|
181
|
+
"""All global filter flags are forwarded to get_filtered_content."""
|
|
182
|
+
mock_db = Mock()
|
|
183
|
+
mock_db.session = Mock()
|
|
184
|
+
mock_db_class.return_value = mock_db
|
|
185
|
+
mock_result = Mock()
|
|
186
|
+
mock_result.scalars.return_value.all.return_value = []
|
|
187
|
+
mock_gfc.return_value = mock_result
|
|
188
|
+
|
|
189
|
+
from click.testing import CliRunner
|
|
190
|
+
CliRunner().invoke(
|
|
191
|
+
edit_command,
|
|
192
|
+
[
|
|
193
|
+
"Title",
|
|
194
|
+
"--replace", "New Title",
|
|
195
|
+
"--artist", "Bicep",
|
|
196
|
+
"--format", "flac",
|
|
197
|
+
"--match-all",
|
|
198
|
+
"--yes",
|
|
199
|
+
],
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
kwargs = mock_gfc.call_args.kwargs
|
|
203
|
+
assert kwargs["artists"] == ("Bicep",)
|
|
204
|
+
assert kwargs["formats"] == ("flac",)
|
|
205
|
+
assert kwargs["match_all"] is True
|
|
206
|
+
|
|
207
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
208
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
209
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
210
|
+
def test_interactive_and_yes_skips_all_confirms(
|
|
211
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
212
|
+
):
|
|
213
|
+
"""--interactive + --yes skips both batch and per-track confirms."""
|
|
214
|
+
track = make_djmd_content_item(Title="Old Title")
|
|
215
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
216
|
+
mock_db_class.return_value = mock_db
|
|
217
|
+
mock_gfc.return_value = mock_result
|
|
218
|
+
|
|
219
|
+
from click.testing import CliRunner
|
|
220
|
+
result = CliRunner().invoke(
|
|
221
|
+
edit_command,
|
|
222
|
+
["Title", "--replace", "New Title", "--interactive", "--yes"],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
assert result.exit_code == 0
|
|
226
|
+
mock_confirm.assert_not_called()
|
|
227
|
+
mock_db.session.commit.assert_called_once()
|
|
228
|
+
|
|
229
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
230
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
231
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
232
|
+
def test_print_ids_outputs_edited_track_ids(
|
|
233
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
234
|
+
):
|
|
235
|
+
"""--print=ids outputs the IDs of edited tracks after committing."""
|
|
236
|
+
track = make_djmd_content_item(ID="99999", Title="Old Title")
|
|
237
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
238
|
+
mock_db_class.return_value = mock_db
|
|
239
|
+
mock_gfc.return_value = mock_result
|
|
240
|
+
|
|
241
|
+
from click.testing import CliRunner
|
|
242
|
+
result = CliRunner().invoke(
|
|
243
|
+
edit_command,
|
|
244
|
+
["Title", "--replace", "New Title", "--print", "ids", "--yes"],
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
assert result.exit_code == 0
|
|
248
|
+
assert "99999" in result.output
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TestEditCommandPhase3:
|
|
252
|
+
"""Phase 3: --match flag for literal find/replace within field value."""
|
|
253
|
+
|
|
254
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
255
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
256
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
257
|
+
def test_match_replaces_substring(
|
|
258
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
259
|
+
):
|
|
260
|
+
"""--match substitutes the pattern within the current value."""
|
|
261
|
+
track = make_djmd_content_item(Title="Dark Matter (feat. Jane)")
|
|
262
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
263
|
+
mock_db_class.return_value = mock_db
|
|
264
|
+
mock_gfc.return_value = mock_result
|
|
265
|
+
mock_confirm.return_value = True
|
|
266
|
+
|
|
267
|
+
from click.testing import CliRunner
|
|
268
|
+
result = CliRunner().invoke(
|
|
269
|
+
edit_command,
|
|
270
|
+
["Title", "--match", "feat.", "--replace", "ft."],
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
assert result.exit_code == 0
|
|
274
|
+
assert track.Title == "Dark Matter (ft. Jane)"
|
|
275
|
+
mock_db.session.commit.assert_called_once()
|
|
276
|
+
|
|
277
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
278
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
279
|
+
def test_match_no_match_is_noop(
|
|
280
|
+
self, mock_db_class, mock_gfc, make_djmd_content_item
|
|
281
|
+
):
|
|
282
|
+
"""When --match pattern is not found in current value, track is a no-op."""
|
|
283
|
+
track = make_djmd_content_item(Title="Dark Matter")
|
|
284
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
285
|
+
mock_db_class.return_value = mock_db
|
|
286
|
+
mock_gfc.return_value = mock_result
|
|
287
|
+
|
|
288
|
+
from click.testing import CliRunner
|
|
289
|
+
result = CliRunner().invoke(
|
|
290
|
+
edit_command,
|
|
291
|
+
["Title", "--match", "feat.", "--replace", "ft.", "--yes"],
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
assert result.exit_code == 0
|
|
295
|
+
mock_db.session.commit.assert_not_called()
|
|
296
|
+
|
|
297
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
298
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
299
|
+
def test_match_with_none_current_value_is_noop(
|
|
300
|
+
self, mock_db_class, mock_gfc, make_djmd_content_item
|
|
301
|
+
):
|
|
302
|
+
"""--match on a track where the field is None is silently skipped."""
|
|
303
|
+
track = make_djmd_content_item(Title=None)
|
|
304
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
305
|
+
mock_db_class.return_value = mock_db
|
|
306
|
+
mock_gfc.return_value = mock_result
|
|
307
|
+
|
|
308
|
+
from click.testing import CliRunner
|
|
309
|
+
result = CliRunner().invoke(
|
|
310
|
+
edit_command,
|
|
311
|
+
["Title", "--match", "feat.", "--replace", "ft.", "--yes"],
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
assert result.exit_code == 0
|
|
315
|
+
mock_db.session.commit.assert_not_called()
|
|
316
|
+
|
|
317
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
318
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
319
|
+
def test_match_treats_pattern_as_literal(
|
|
320
|
+
self, mock_db_class, mock_gfc, make_djmd_content_item
|
|
321
|
+
):
|
|
322
|
+
"""--match treats the pattern as a literal string, not a regex."""
|
|
323
|
+
# "1.0" as a regex would also match "1X0"; as a literal it should not
|
|
324
|
+
track = make_djmd_content_item(Title="Version 1X0")
|
|
325
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
326
|
+
mock_db_class.return_value = mock_db
|
|
327
|
+
mock_gfc.return_value = mock_result
|
|
328
|
+
|
|
329
|
+
from click.testing import CliRunner
|
|
330
|
+
result = CliRunner().invoke(
|
|
331
|
+
edit_command,
|
|
332
|
+
["Title", "--match", "1.0", "--replace", "2.0", "--yes"],
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
assert result.exit_code == 0
|
|
336
|
+
mock_db.session.commit.assert_not_called()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class TestEditCommandUnicode:
|
|
340
|
+
"""Unicode and multibyte character handling in edit command fields."""
|
|
341
|
+
|
|
342
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
343
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
344
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
345
|
+
def test_wholesale_replace_with_unicode_value(
|
|
346
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
347
|
+
):
|
|
348
|
+
"""--replace accepts and writes multibyte unicode titles correctly."""
|
|
349
|
+
track = make_djmd_content_item(Title="Original Title")
|
|
350
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
351
|
+
mock_db_class.return_value = mock_db
|
|
352
|
+
mock_gfc.return_value = mock_result
|
|
353
|
+
mock_confirm.return_value = True
|
|
354
|
+
|
|
355
|
+
from click.testing import CliRunner
|
|
356
|
+
result = CliRunner().invoke(
|
|
357
|
+
edit_command,
|
|
358
|
+
["Title", "--replace", "日本語タイトル"],
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
assert result.exit_code == 0
|
|
362
|
+
assert track.Title == "日本語タイトル"
|
|
363
|
+
mock_db.session.commit.assert_called_once()
|
|
364
|
+
|
|
365
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
366
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
367
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
368
|
+
def test_wholesale_replace_of_unicode_current_value(
|
|
369
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
370
|
+
):
|
|
371
|
+
"""A track with a unicode title can be replaced wholesale."""
|
|
372
|
+
track = make_djmd_content_item(Title="Ü-Bahn Nights (feat. Ångström)")
|
|
373
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
374
|
+
mock_db_class.return_value = mock_db
|
|
375
|
+
mock_gfc.return_value = mock_result
|
|
376
|
+
mock_confirm.return_value = True
|
|
377
|
+
|
|
378
|
+
from click.testing import CliRunner
|
|
379
|
+
result = CliRunner().invoke(
|
|
380
|
+
edit_command,
|
|
381
|
+
["Title", "--replace", "U-Bahn Nights (feat. Angstrom)"],
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
assert result.exit_code == 0
|
|
385
|
+
assert track.Title == "U-Bahn Nights (feat. Angstrom)"
|
|
386
|
+
mock_db.session.commit.assert_called_once()
|
|
387
|
+
|
|
388
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
389
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
390
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
391
|
+
def test_match_replace_within_unicode_title(
|
|
392
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
393
|
+
):
|
|
394
|
+
"""--match correctly finds and replaces a substring within a unicode title."""
|
|
395
|
+
track = make_djmd_content_item(Title="夜 feat. 山田太郎")
|
|
396
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
397
|
+
mock_db_class.return_value = mock_db
|
|
398
|
+
mock_gfc.return_value = mock_result
|
|
399
|
+
mock_confirm.return_value = True
|
|
400
|
+
|
|
401
|
+
from click.testing import CliRunner
|
|
402
|
+
result = CliRunner().invoke(
|
|
403
|
+
edit_command,
|
|
404
|
+
["Title", "--match", "feat.", "--replace", "ft."],
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
assert result.exit_code == 0
|
|
408
|
+
assert track.Title == "夜 ft. 山田太郎"
|
|
409
|
+
mock_db.session.commit.assert_called_once()
|
|
410
|
+
|
|
411
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
412
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
413
|
+
def test_noop_when_unicode_values_are_equal(
|
|
414
|
+
self, mock_db_class, mock_gfc, make_djmd_content_item
|
|
415
|
+
):
|
|
416
|
+
"""No edit is made when the current unicode value already equals --replace."""
|
|
417
|
+
track = make_djmd_content_item(Title="Ø (Disambiguation)")
|
|
418
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
419
|
+
mock_db_class.return_value = mock_db
|
|
420
|
+
mock_gfc.return_value = mock_result
|
|
421
|
+
|
|
422
|
+
from click.testing import CliRunner
|
|
423
|
+
result = CliRunner().invoke(
|
|
424
|
+
edit_command,
|
|
425
|
+
["Title", "--replace", "Ø (Disambiguation)", "--yes"],
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
assert result.exit_code == 0
|
|
429
|
+
mock_db.session.commit.assert_not_called()
|
|
File without changes
|
|
File without changes
|
{rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.github/actions/commitizen-bump/action.yml
RENAMED
|
File without changes
|
|
File without changes
|
{rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/.github/actions/install/action.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/__init__.py
RENAMED
|
File without changes
|
{rekordbox_edit-0.4.0.dev20 → rekordbox_edit-0.4.0.dev22}/rekordbox_edit/commands/convert.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|