rekordbox-edit 0.4.0.dev21__tar.gz → 0.4.0.dev23__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.dev21 → rekordbox_edit-0.4.0.dev23}/CHANGELOG.md +7 -1
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/PKG-INFO +1 -1
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/pyproject.toml +1 -1
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/rekordbox_edit/commands/edit.py +37 -11
- rekordbox_edit-0.4.0.dev23/tests/commands/test_edit.py +530 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/uv.lock +1 -1
- rekordbox_edit-0.4.0.dev21/tests/commands/test_edit.py +0 -248
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.agent-style/RULES.md +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.agent-style/claude-code.md +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.github/actions/commitizen-bump/action.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.github/actions/commitizen-bump/commitizen-bump.sh +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.github/actions/install/action.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.github/actions/lint/action.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.github/actions/test/action.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.github/workflows/cd.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.github/workflows/ci.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.github/workflows/publish.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.github/workflows/release.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.gitignore +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.pre-commit-config.yaml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/AGENTS.md +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/CLAUDE.md +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/CONTRIBUTING.md +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/LICENSE +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/Makefile +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/README.md +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/codecov.yml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/rekordbox_edit/__init__.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/rekordbox_edit/_click.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/rekordbox_edit/cli.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/rekordbox_edit/commands/__init__.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/rekordbox_edit/commands/convert.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/rekordbox_edit/commands/search.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/rekordbox_edit/logger.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/rekordbox_edit/query.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/rekordbox_edit/utils.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/renovate.json5 +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/ruff.toml +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/tests/__init__.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/tests/commands/__init__.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/tests/commands/test_convert.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/tests/commands/test_search.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/tests/conftest.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/tests/test_logger.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/tests/test_query.py +0 -0
- {rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
## v0.4.0.
|
|
1
|
+
## v0.4.0.dev23 (2026-05-17)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
- feat(edit): add --multi to allow batch edits past single-track guard
|
|
5
|
+
- feat(edit): add --match for literal find/replace within field value
|
|
6
|
+
- Introduces a _compute_new_value helper and --match option so that
|
|
7
|
+
rbe edit can replace a substring of the current field value instead of
|
|
8
|
+
performing a wholesale replacement; None current values and non-matching
|
|
9
|
+
patterns are treated as no-ops.
|
|
4
10
|
- chore: add max-complexity lint rule
|
|
5
11
|
- docs: update AGENTS.md
|
|
6
12
|
- test: add coverage for --interactive + --yes skipping all confirms
|
|
@@ -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.dev23
|
|
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.dev23"
|
|
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"
|
|
@@ -26,6 +26,18 @@ FIELD_COLUMNS = {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
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
|
+
|
|
29
41
|
|
|
30
42
|
@click.command(
|
|
31
43
|
epilog=f"Debug logs for each run can be found at:\n{get_debug_file_path().parent}"
|
|
@@ -54,6 +66,18 @@ FIELD_COLUMNS = {
|
|
|
54
66
|
required=True,
|
|
55
67
|
help="The new value to write to the field",
|
|
56
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
|
+
@click.option(
|
|
77
|
+
"--multi",
|
|
78
|
+
is_flag=True,
|
|
79
|
+
help="Allow editing more than one track (required when filters match multiple tracks)",
|
|
80
|
+
)
|
|
57
81
|
@track_ids_argument
|
|
58
82
|
@click.argument(
|
|
59
83
|
"field",
|
|
@@ -62,10 +86,12 @@ FIELD_COLUMNS = {
|
|
|
62
86
|
def edit_command(
|
|
63
87
|
field: str,
|
|
64
88
|
replace_value: str,
|
|
89
|
+
match_pattern: str | None,
|
|
90
|
+
multi: bool,
|
|
65
91
|
dry_run: bool,
|
|
66
92
|
yes: bool,
|
|
67
93
|
interactive: bool,
|
|
68
|
-
track_ids:
|
|
94
|
+
track_ids: List[str] | None,
|
|
69
95
|
track_id: List[str] | None,
|
|
70
96
|
playlist: List[str] | None,
|
|
71
97
|
exact_playlist: List[str] | None,
|
|
@@ -99,9 +125,7 @@ def edit_command(
|
|
|
99
125
|
)
|
|
100
126
|
|
|
101
127
|
if piped_stdin and not (dry_run or yes):
|
|
102
|
-
raise click.UsageError(
|
|
103
|
-
"Piping track IDs into edit requires --dry-run or --yes"
|
|
104
|
-
)
|
|
128
|
+
raise click.UsageError("Piping track IDs into edit requires --dry-run or --yes")
|
|
105
129
|
|
|
106
130
|
db = Rekordbox6Database()
|
|
107
131
|
if not db.session:
|
|
@@ -127,20 +151,22 @@ def edit_command(
|
|
|
127
151
|
tracks = result.scalars().all()
|
|
128
152
|
|
|
129
153
|
col_name = FIELD_COLUMNS[field]
|
|
130
|
-
edits = [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
edits = []
|
|
155
|
+
for track in tracks:
|
|
156
|
+
current = getattr(track, col_name)
|
|
157
|
+
new_value = _compute_new_value(current, match_pattern, replace_value)
|
|
158
|
+
if new_value is None or new_value == current:
|
|
159
|
+
continue
|
|
160
|
+
edits.append((track, new_value))
|
|
135
161
|
|
|
136
162
|
if not edits:
|
|
137
163
|
logger.info("No changes to make.")
|
|
138
164
|
return
|
|
139
165
|
|
|
140
|
-
if len(edits) > 1:
|
|
166
|
+
if len(edits) > 1 and not multi:
|
|
141
167
|
raise click.UsageError(
|
|
142
168
|
f"Found {len(edits)} tracks that would be edited. "
|
|
143
|
-
"Refine your filters,
|
|
169
|
+
"Refine your filters, use --dry-run to inspect, or pass --multi to edit all."
|
|
144
170
|
)
|
|
145
171
|
|
|
146
172
|
print_track_info([t for t, _ in edits])
|
|
@@ -0,0 +1,530 @@
|
|
|
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()
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class TestEditCommandPhase4:
|
|
433
|
+
"""Phase 4: --multi flag to allow batch edits past the single-track guard."""
|
|
434
|
+
|
|
435
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
436
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
437
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
438
|
+
def test_multi_allows_editing_multiple_tracks(
|
|
439
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
440
|
+
):
|
|
441
|
+
"""--multi bypasses the single-track guard and edits all matched tracks."""
|
|
442
|
+
tracks = [
|
|
443
|
+
make_djmd_content_item(Title="Track A (feat. X)"),
|
|
444
|
+
make_djmd_content_item(Title="Track B (feat. Y)"),
|
|
445
|
+
]
|
|
446
|
+
mock_db, mock_result = _make_db_and_result(tracks)
|
|
447
|
+
mock_db_class.return_value = mock_db
|
|
448
|
+
mock_gfc.return_value = mock_result
|
|
449
|
+
mock_confirm.return_value = True
|
|
450
|
+
|
|
451
|
+
from click.testing import CliRunner
|
|
452
|
+
result = CliRunner().invoke(
|
|
453
|
+
edit_command,
|
|
454
|
+
["Title", "--match", "feat.", "--replace", "ft.", "--multi"],
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
assert result.exit_code == 0
|
|
458
|
+
assert tracks[0].Title == "Track A (ft. X)"
|
|
459
|
+
assert tracks[1].Title == "Track B (ft. Y)"
|
|
460
|
+
mock_db.session.commit.assert_called_once()
|
|
461
|
+
|
|
462
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
463
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
464
|
+
def test_without_multi_still_aborts_on_multiple(
|
|
465
|
+
self, mock_db_class, mock_gfc, make_djmd_content_item
|
|
466
|
+
):
|
|
467
|
+
"""Without --multi, the single-track guard still aborts on multiple matches."""
|
|
468
|
+
tracks = [
|
|
469
|
+
make_djmd_content_item(Title="Track A"),
|
|
470
|
+
make_djmd_content_item(Title="Track B"),
|
|
471
|
+
]
|
|
472
|
+
mock_db, mock_result = _make_db_and_result(tracks)
|
|
473
|
+
mock_db_class.return_value = mock_db
|
|
474
|
+
mock_gfc.return_value = mock_result
|
|
475
|
+
|
|
476
|
+
from click.testing import CliRunner
|
|
477
|
+
result = CliRunner().invoke(
|
|
478
|
+
edit_command, ["Title", "--replace", "New", "--yes"]
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
assert result.exit_code != 0
|
|
482
|
+
mock_db.session.commit.assert_not_called()
|
|
483
|
+
|
|
484
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
485
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
486
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
487
|
+
def test_multi_with_yes_skips_confirm(
|
|
488
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
489
|
+
):
|
|
490
|
+
"""--multi --yes edits multiple tracks without prompting."""
|
|
491
|
+
tracks = [
|
|
492
|
+
make_djmd_content_item(Title="Track A"),
|
|
493
|
+
make_djmd_content_item(Title="Track B"),
|
|
494
|
+
]
|
|
495
|
+
mock_db, mock_result = _make_db_and_result(tracks)
|
|
496
|
+
mock_db_class.return_value = mock_db
|
|
497
|
+
mock_gfc.return_value = mock_result
|
|
498
|
+
|
|
499
|
+
from click.testing import CliRunner
|
|
500
|
+
result = CliRunner().invoke(
|
|
501
|
+
edit_command,
|
|
502
|
+
["Title", "--replace", "New", "--multi", "--yes"],
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
assert result.exit_code == 0
|
|
506
|
+
mock_confirm.assert_not_called()
|
|
507
|
+
mock_db.session.commit.assert_called_once()
|
|
508
|
+
|
|
509
|
+
@patch("rekordbox_edit.commands.edit.confirm")
|
|
510
|
+
@patch("rekordbox_edit.commands.edit.get_filtered_content")
|
|
511
|
+
@patch("rekordbox_edit.commands.edit.Rekordbox6Database")
|
|
512
|
+
def test_multi_single_track_still_works(
|
|
513
|
+
self, mock_db_class, mock_gfc, mock_confirm, make_djmd_content_item
|
|
514
|
+
):
|
|
515
|
+
"""--multi with only one matching track works fine (guard is not inverted)."""
|
|
516
|
+
track = make_djmd_content_item(Title="Only Track")
|
|
517
|
+
mock_db, mock_result = _make_db_and_result([track])
|
|
518
|
+
mock_db_class.return_value = mock_db
|
|
519
|
+
mock_gfc.return_value = mock_result
|
|
520
|
+
mock_confirm.return_value = True
|
|
521
|
+
|
|
522
|
+
from click.testing import CliRunner
|
|
523
|
+
result = CliRunner().invoke(
|
|
524
|
+
edit_command,
|
|
525
|
+
["Title", "--replace", "New Title", "--multi"],
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
assert result.exit_code == 0
|
|
529
|
+
assert track.Title == "New Title"
|
|
530
|
+
mock_db.session.commit.assert_called_once()
|
|
@@ -1,248 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
{rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.github/actions/commitizen-bump/action.yml
RENAMED
|
File without changes
|
|
File without changes
|
{rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/.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
|
|
File without changes
|
|
File without changes
|
{rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/rekordbox_edit/commands/__init__.py
RENAMED
|
File without changes
|
{rekordbox_edit-0.4.0.dev21 → rekordbox_edit-0.4.0.dev23}/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
|
|
File without changes
|