rekordbox-edit 0.7.0.dev72__tar.gz → 0.8.0.dev6__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.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/actions/build-release-notes/build_release_notes.py +66 -23
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/workflows/cd.yml +2 -2
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/workflows/publish.yml +2 -2
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.pre-commit-config.yaml +1 -1
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/CHANGELOG.md +17 -1
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/PKG-INFO +1 -1
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/docs/faqs.md +17 -8
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/docs/filtering.md +9 -4
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/pyproject.toml +1 -1
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/_click.py +4 -3
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/cli/_utils.py +1 -1
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/models.py +1 -1
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/query.py +24 -48
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/renovate.json5 +2 -2
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/test_query.py +66 -84
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/uv.lock +1 -1
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.agent-style/RULES.md +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.agent-style/claude-code.md +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/actions/build-release-notes/action.yml +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/actions/commit-check/action.yml +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/actions/commitizen-bump/action.yml +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/actions/commitizen-bump/commitizen-bump.sh +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/actions/e2e/action.yml +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/actions/install/action.yml +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/actions/lint/action.yml +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/actions/test/action.yml +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/workflows/ci.yml +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/workflows/release.yml +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.gitignore +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.python-version +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.readthedocs.yaml +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/AGENTS.md +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/CLAUDE.md +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/CONTRIBUTING.md +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/LICENSE +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/Makefile +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/README.md +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/codecov.yml +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/docker-compose.yml +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/docs/api.md +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/docs/commands/convert.md +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/docs/commands/edit.md +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/docs/commands/search.md +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/docs/index.md +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/docs/stylesheets/extra.css +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/mkdocs.yml +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/__init__.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/api/__init__.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/api/_utils.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/api/convert.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/api/edit.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/api/search.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/cli/__init__.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/cli/convert.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/cli/edit.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/cli/main.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/cli/search.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/display.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/logger.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/rekordbox_edit/utils.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/ruff.toml +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/__init__.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/api/__init__.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/api/test_convert.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/api/test_edit.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/api/test_search.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/api/test_utils.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/cli/__init__.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/cli/test_convert.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/cli/test_edit.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/cli/test_main.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/cli/test_search.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/cli/test_utils.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/conftest.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/Dockerfile +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/__init__.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/__snapshots__/test_journey/test_search_full_json_snapshot[macos].json +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/__snapshots__/test_journey/test_search_full_json_snapshot[windows].json +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/__snapshots__/test_journey.ambr +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/conftest.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/audio/01-flac-44_1k-16b.flac +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/audio/02-flac-96k-24b.flac +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/audio/03-alac-44_1k-16b.m4a +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/audio/04-alac-48k-24b.m4a +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/audio/05-aiff-44_1k-16b.aiff +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/audio/06-wav-96k-24b.wav +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/audio/07-mp3-44_1k-320cbr.mp3 +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/audio/08-mp3-44_1k-v0vbr.mp3 +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/audio/09-aac-44_1k-256kbps.m4a +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/audio/10-/303/274/303/261/303/256c/303/266d/303/251-flac-44_1k-16b.flac" +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/macos/master.6.8.6.db +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/windows/master.6.8.6.db +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/test_journey.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/test_display.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/test_logger.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/test_models.py +0 -0
- {rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/test_utils.py +0 -0
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
Lists one entry per commit (not per PR) between ``FROM_TAG`` (exclusive) and
|
|
5
5
|
``TO_TAG`` (inclusive). Each entry links the pull request that introduced the
|
|
6
|
-
commit and credits its author.
|
|
7
|
-
``
|
|
8
|
-
|
|
9
|
-
and the git author name.
|
|
6
|
+
commit and credits its author. PRs are resolved through a batched GraphQL query
|
|
7
|
+
(``Commit.associatedPullRequests``), which works for both squash and rebase
|
|
8
|
+
merges without a per-commit round trip. Commits with no associated PR fall back
|
|
9
|
+
to a short-SHA reference and the git author name.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -97,21 +97,63 @@ def read_commits(from_tag: str, to_tag: str) -> list[Commit]:
|
|
|
97
97
|
return commits
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
# GitHub's GraphQL node limit is generous; keep chunks small enough that the
|
|
101
|
+
# query fits comfortably in a single command-line argument on any platform.
|
|
102
|
+
_CHUNK = 100
|
|
103
|
+
|
|
104
|
+
_COMMIT_FIELD = (
|
|
105
|
+
'c{i}: object(oid: "{sha}") {{ ... on Commit {{ '
|
|
106
|
+
"associatedPullRequests(first: 5) {{ nodes {{ number merged author {{ login }} }} }} "
|
|
107
|
+
"}} }}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _chunks(items: list[str], size: int):
|
|
112
|
+
for start in range(0, len(items), size):
|
|
113
|
+
yield items[start : start + size]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _pick_pr(nodes: list[dict]) -> tuple[int, str] | None:
|
|
117
|
+
merged = [pr for pr in nodes if pr.get("merged")]
|
|
118
|
+
chosen = merged[0] if merged else (nodes[0] if nodes else None)
|
|
111
119
|
if not chosen:
|
|
112
120
|
return None
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
return chosen["number"], (chosen.get("author") or {}).get("login", "")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def resolve_prs(repo: str, shas: list[str]) -> dict[str, tuple[int, str]]:
|
|
125
|
+
"""Resolve every commit's PR (number, author_login) in batched GraphQL calls.
|
|
126
|
+
|
|
127
|
+
One request per ``_CHUNK`` commits replaces the previous per-commit REST call,
|
|
128
|
+
which dominated runtime. Commits with no associated PR are simply absent from
|
|
129
|
+
the returned mapping.
|
|
130
|
+
"""
|
|
131
|
+
owner, _, name = repo.partition("/")
|
|
132
|
+
resolved: dict[str, tuple[int, str]] = {}
|
|
133
|
+
for chunk in _chunks(shas, _CHUNK):
|
|
134
|
+
fields = " ".join(
|
|
135
|
+
_COMMIT_FIELD.format(i=i, sha=sha) for i, sha in enumerate(chunk)
|
|
136
|
+
)
|
|
137
|
+
query = (
|
|
138
|
+
f'query {{ repository(owner: "{owner}", name: "{name}") {{ {fields} }} }}'
|
|
139
|
+
)
|
|
140
|
+
result = _run(["gh", "api", "graphql", "-f", f"query={query}"])
|
|
141
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
142
|
+
continue
|
|
143
|
+
try:
|
|
144
|
+
repository = (json.loads(result.stdout).get("data") or {}).get("repository")
|
|
145
|
+
except json.JSONDecodeError:
|
|
146
|
+
continue
|
|
147
|
+
if not repository:
|
|
148
|
+
continue
|
|
149
|
+
for i, sha in enumerate(chunk):
|
|
150
|
+
node = repository.get(f"c{i}")
|
|
151
|
+
if not node:
|
|
152
|
+
continue
|
|
153
|
+
pr = _pick_pr((node.get("associatedPullRequests") or {}).get("nodes") or [])
|
|
154
|
+
if pr is not None:
|
|
155
|
+
resolved[sha] = pr
|
|
156
|
+
return resolved
|
|
115
157
|
|
|
116
158
|
|
|
117
159
|
def categorize(commit: Commit) -> str:
|
|
@@ -123,9 +165,8 @@ def categorize(commit: Commit) -> str:
|
|
|
123
165
|
return _UNCATEGORIZED_HEADER
|
|
124
166
|
|
|
125
167
|
|
|
126
|
-
def render_line(commit: Commit,
|
|
168
|
+
def render_line(commit: Commit, pr: tuple[int, str] | None) -> str:
|
|
127
169
|
subject = _TRAILING_PR.sub("", commit.subject)
|
|
128
|
-
pr = resolve_pr(repo, commit.sha)
|
|
129
170
|
if pr is not None:
|
|
130
171
|
number, login = pr
|
|
131
172
|
credit = f"#{number} by @{login}" if login else f"#{number}"
|
|
@@ -134,11 +175,13 @@ def render_line(commit: Commit, repo: str) -> str:
|
|
|
134
175
|
|
|
135
176
|
|
|
136
177
|
def build_notes(commits: list[Commit], repo: str) -> str:
|
|
178
|
+
included = [c for c in commits if not _SKIP_SUBJECT.match(c.subject)]
|
|
179
|
+
pr_map = resolve_prs(repo, [c.sha for c in included])
|
|
180
|
+
|
|
137
181
|
categories = {header: Category(header) for header in _CATEGORY_ORDER}
|
|
138
|
-
for commit in
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
categories[categorize(commit)].lines.append(render_line(commit, repo))
|
|
182
|
+
for commit in included:
|
|
183
|
+
line = render_line(commit, pr_map.get(commit.sha))
|
|
184
|
+
categories[categorize(commit)].lines.append(line)
|
|
142
185
|
|
|
143
186
|
sections = [
|
|
144
187
|
f"{cat.header}\n" + "\n".join(cat.lines)
|
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
|
26
26
|
steps:
|
|
27
27
|
- id: skip_check
|
|
28
|
-
uses: fkirc/skip-duplicate-actions@
|
|
28
|
+
uses: fkirc/skip-duplicate-actions@b974a9395958c231af965b70070979a577efa578 # v5
|
|
29
29
|
with:
|
|
30
30
|
concurrent_skipping: "same_content_newer"
|
|
31
31
|
cancel_others: "true"
|
|
@@ -93,7 +93,7 @@ jobs:
|
|
|
93
93
|
git push origin canary-v${{ steps.version.outputs.version }}
|
|
94
94
|
|
|
95
95
|
- name: Publish to PyPI
|
|
96
|
-
uses: pypa/gh-action-pypi-publish@
|
|
96
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
|
97
97
|
with:
|
|
98
98
|
skip-existing: true
|
|
99
99
|
|
|
@@ -39,7 +39,7 @@ jobs:
|
|
|
39
39
|
name: dist
|
|
40
40
|
path: dist/
|
|
41
41
|
- name: Publish package distributions 📦 to TestPyPI
|
|
42
|
-
uses: pypa/gh-action-pypi-publish@
|
|
42
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
|
43
43
|
with:
|
|
44
44
|
repository-url: https://test.pypi.org/legacy/
|
|
45
45
|
|
|
@@ -57,4 +57,4 @@ jobs:
|
|
|
57
57
|
path: dist
|
|
58
58
|
merge-multiple: true
|
|
59
59
|
- name: Publish to PyPI
|
|
60
|
-
uses: pypa/gh-action-pypi-publish@
|
|
60
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
## v0.
|
|
1
|
+
## v0.8.0.dev6 (2026-08-01)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
- feat!: replace --exact-path with case-insensitive --resolved-path
|
|
5
|
+
- Path filters now always match case-insensitively, mirroring how NTFS
|
|
6
|
+
and APFS treat paths. --resolved-path makes its argument absolute by
|
|
7
|
+
pure string math instead of Path.resolve(), so results no longer
|
|
8
|
+
depend on mounted drives, on-disk casing, or symlinks.
|
|
9
|
+
- chore(deps): update pre-commit hook commitizen-tools/commitizen to v4.17.0 (#159)
|
|
10
|
+
- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
|
|
11
|
+
- chore: reduce renovate schedule to weekends
|
|
12
|
+
- docs: add FAQ about converts impact on USB exports
|
|
13
|
+
- chore(deps): update github actions (#151)
|
|
14
|
+
- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
|
|
15
|
+
- ci: optimize the build_release_notes script
|
|
16
|
+
|
|
17
|
+
## v0.7.0 (2026-07-31)
|
|
2
18
|
|
|
3
19
|
|
|
4
20
|
- ci: more precise changelogs on releases
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rekordbox-edit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0.dev6
|
|
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
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Messing with your Rekordbox library is bound to raise some questions and concerns, particularly around preserving all the manual work you've done to organize, catalog, and prepare your library for gigs. This page is dedicated to answering those questions around what the impact and risks are of editing your rekordbox library or the underlying audio files using rekordbox-edit. If your question isn't answered here, please ask it in [a discussion](https://github.com/jviall/rekordbox-edit/discussions/new/choose).
|
|
4
4
|
|
|
5
|
-
## How
|
|
5
|
+
## How does Rekordbox's analysis work?
|
|
6
6
|
|
|
7
7
|
Analysis touches two places in your library. The database gets the summary values for things like tempo, key, and the flags Rekordbox uses to track analysis state. The heavier artifacts live in a per-track folder of analysis files (.ANLZ) containing the beat grid, waveforms, and phrase data. Your cues and loops live in neither; they are separate database records that only your own edits change.
|
|
8
8
|
|
|
9
9
|
What analysis writes is a deterministic estimate. Deterministic, because the same file analyzed twice produces byte-identical results. An estimate, because tempo, grid, and key are inferred from the audio, and Rekordbox's inference is sometimes wrong. Both aspects are worth considering; when the analysis of a converted file produces a different result, it is simply a different estimate from different data.
|
|
10
10
|
|
|
11
|
-
## How
|
|
11
|
+
## How does converting a track affect its analysis?
|
|
12
12
|
|
|
13
13
|
The reason conversion is safe comes down to how Rekordbox stores analysis: positions are timestamps. The beat grid is a list of times in milliseconds, cues are stored in milliseconds, and phrases are anchored to beats. None of it references sample counts or byte positions in the audio file. Change the codec, the sample rate, or the bit depth, and every stored position still names the same musical moment.
|
|
14
14
|
|
|
@@ -21,19 +21,19 @@ There are a few minor things left stale by a conversion (without any consequence
|
|
|
21
21
|
|
|
22
22
|
Converting *to* FLAC does not create that seek index automatically either; only an analysis can. I have *not* tested (yet) whether that matters in practice, but it makes a stronger case for re-analyzing after a conversion to FLAC than for other targets. FLAC also is not the most ideal target format in general for Rekordbox (even though it's preferred for streaming and music hoarders), because only the more recent Pioneer devices support it. AIFF is what I would pick if you insist on lossless.
|
|
23
23
|
|
|
24
|
-
## Does
|
|
24
|
+
## Does converting to MP3 shift the audio?
|
|
25
25
|
|
|
26
26
|
No. `rbe` uses LAME's gapless-playback encoding to avoid any start-of-file offset, and across many tests the MP3 320 output never differed in timing, sample for sample, from its lossless source. Cues and the beat grid stay exactly in register.
|
|
27
27
|
|
|
28
28
|
The only artifact is visual: the drawn waveform still reflects the lossless original rather than the MP3. A re-analysis would redraw it, but you'd be hard-pressed to notice the difference.
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## Does convert impact cues or beatgrids?
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
No. Cues, loops, and memory points are records in your library database, positioned in milliseconds and attached to the track's database entry, and `rbe convert` does not touch them. The beat grid lives in the analysis files, which convert also leaves alone. In all my testing, every cue and beat grid has been completely unchanged by a conversion, and works just as it did prior.
|
|
33
33
|
|
|
34
34
|
This includes hand-tuned grids. Because convert never rewrites the grid a manually corrected beat grid survives conversion unchanged. The one operation that can replace it is a re-analysis you run yourself; see below.
|
|
35
35
|
|
|
36
|
-
## Should I
|
|
36
|
+
## Should I re-analyze after converting?
|
|
37
37
|
|
|
38
38
|
You do not need to. The existing analysis remains correct for the converted file, and Rekordbox never re-analyzes on its own; it waits for you to ask. Skipping re-analysis keeps your grid and cues exactly as they are.
|
|
39
39
|
|
|
@@ -41,10 +41,19 @@ If you do re-analyze, the outcome depends on whether the conversion changed the
|
|
|
41
41
|
|
|
42
42
|
One real hazard: re-analysis replaces manually edited beat grids on any track that is not locked. If you plan to re-analyze after a bulk conversion, turn on Analysis Lock first for tracks whose grids you have tuned by hand.
|
|
43
43
|
|
|
44
|
-
## What
|
|
44
|
+
## What if I convert a track that's been exported to a USB?
|
|
45
|
+
|
|
46
|
+
Conversion only touches your library and the source file--it won't update your USB exports for you. The old version of the track that's on your USB will effectively be orphaned from the library. Depending on the library type (i.e. "Device Library" v.s. "One Device Library") re-syncing will either not do anything, or it will add your new converted versions but leave the stale copies behind. Manually transferring a playlist to a device and overwriting it (by drag-n-dropping in the library explorer tree) will also only add your new versions but not remove the old ones.
|
|
47
|
+
|
|
48
|
+
Perhaps a more crucial consequence of this orphaning of converted files on your USB, is that any cues, play counts, or other metadata of the converted files that is only on your USB and hasn't been synced back to Rekordbox will also be orphaned, and attempting to sync this back after a conversion won't work.
|
|
49
|
+
|
|
50
|
+
Ultimately, if you're planning on doing an in-place conversion of tracks in your library using this tool, you should consider making sure the cues, grid, and info on your USB sticks has been synced back to Rekordbox (if so desired), and be prepared to erase and freshly export your USBs in order to avoid any stale files left behind by a re-export.
|
|
51
|
+
|
|
52
|
+
## What should I do before converting?
|
|
45
53
|
|
|
46
54
|
- **Preview with `--dry-run`.** See exactly which tracks will convert and where the files will go before anything is written.
|
|
47
55
|
- **Close Rekordbox.** Writing to the database while Rekordbox has it open risks corruption.
|
|
48
|
-
- **Make fresh backups of your music and your Rekordbox database.** Conversion rewrites files and database rows in place; a current backup makes any
|
|
56
|
+
- **Make fresh backups of your music and your Rekordbox database.** Conversion rewrites files and database rows in place; a current backup makes any mistake reversible.
|
|
57
|
+
- **Sync back cues, grids, and info from your USBs** Converting a track that has been exported will orphan the exported version and any un-synced metadata from your library.
|
|
49
58
|
- **Keep your originals until you are satisfied.** The default `--delete-originals lossless` only deletes a source when no audio information was lost. Be deliberate before choosing `all`.
|
|
50
59
|
- **Lock hand-tuned beat grids if you plan to re-analyze.** Conversion won't mess with your grids, but a later bulk re-analysis replaces grids on unlocked tracks. Analysis Lock exists for exactly this.
|
|
@@ -18,14 +18,16 @@ Repeating a filter, or combining different filters, matches tracks that satisfy
|
|
|
18
18
|
| `--playlist TEXT` | playlist name contains `TEXT` |
|
|
19
19
|
| `--exact-playlist TEXT` | playlist name is exactly `TEXT` |
|
|
20
20
|
| `--format FMT` | Rekordbox file type is `FMT` (`mp3`, `mp4`, `aac`, `flac`, `alac`, `wav`, `aiff`, `video`, `invalid`) |
|
|
21
|
-
| `--path TEXT` | file path contains `TEXT
|
|
22
|
-
| `--
|
|
21
|
+
| `--path TEXT` | file path contains `TEXT`, case-insensitive |
|
|
22
|
+
| `--resolved-path TEXT` | file path contains `TEXT` after resolving it to an absolute path, case-insensitive |
|
|
23
23
|
| `--first N` | return only the first N results |
|
|
24
24
|
| `--last N` | return only the last N results |
|
|
25
25
|
|
|
26
26
|
> [!TIP]
|
|
27
27
|
> Filters that are "exact" are case-sensitive (e.g. `--exact-artist 'HOOBASTANK'` won't match "Hoobastank") whereas their counterparts (`--artist`) are case-insensitive and match substrings. Only one of either the `--first` and `--last` filters can be provided at a time as they're mutually exclusive.
|
|
28
28
|
|
|
29
|
+
> [!NOTE]
|
|
30
|
+
> Path filters match case-insensitively on every platform because the filesystems Rekordbox supports (NTFS and APFS) treat paths case-insensitively themselves.
|
|
29
31
|
|
|
30
32
|
### Examples
|
|
31
33
|
|
|
@@ -45,11 +47,14 @@ rbe search --playlist "house" --playlist "disco"
|
|
|
45
47
|
# All the songs in my library that aren't in any playlist
|
|
46
48
|
rbe search --playlist ""
|
|
47
49
|
|
|
48
|
-
# The first 10 tracks
|
|
50
|
+
# The first 10 tracks in a "Favorites" folder or with "track.wav" in the file path
|
|
49
51
|
rbe search --path "Favorites/" --path "track.wav" --first 10
|
|
50
52
|
|
|
51
53
|
# The track at an exact location
|
|
52
|
-
rbe search --
|
|
54
|
+
rbe search --resolved-path "/Users/djmustard/Music/banger.mp3"
|
|
55
|
+
|
|
56
|
+
# Every track under a folder, given relative to the current directory
|
|
57
|
+
rbe search --resolved-path "../Music/house/"
|
|
53
58
|
```
|
|
54
59
|
|
|
55
60
|
## Track ID Arguments
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rekordbox-edit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0.dev6"
|
|
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"
|
|
@@ -81,13 +81,14 @@ global_click_filters = [
|
|
|
81
81
|
"--path",
|
|
82
82
|
type=str,
|
|
83
83
|
multiple=True,
|
|
84
|
-
help="Find tracks whose file paths
|
|
84
|
+
help="Find tracks whose file paths contain this value (case-insensitive)",
|
|
85
85
|
),
|
|
86
86
|
click.option(
|
|
87
|
-
"--
|
|
87
|
+
"--resolved-path",
|
|
88
88
|
type=str,
|
|
89
89
|
multiple=True,
|
|
90
|
-
help="Find tracks whose file paths
|
|
90
|
+
help="Find tracks whose file paths contain this value after it is "
|
|
91
|
+
"resolved to an absolute path (case-insensitive)",
|
|
91
92
|
),
|
|
92
93
|
click.option(
|
|
93
94
|
"--format",
|
|
@@ -45,7 +45,7 @@ class FilterArgs(BaseModel):
|
|
|
45
45
|
album: list[str] = []
|
|
46
46
|
exact_album: list[str] = []
|
|
47
47
|
path: list[str] = []
|
|
48
|
-
|
|
48
|
+
resolved_path: list[str] = []
|
|
49
49
|
format: list[str] = []
|
|
50
50
|
first: int | None = Field(default=None, gt=0)
|
|
51
51
|
last: int | None = Field(default=None, gt=0)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import os
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from typing import List, Tuple, Union
|
|
4
5
|
|
|
@@ -148,57 +149,32 @@ class CollectionQuery:
|
|
|
148
149
|
logger.warning(f"Invalid format: {format_name}")
|
|
149
150
|
return new_inst
|
|
150
151
|
|
|
151
|
-
def by_path(self, path_str: str,
|
|
152
|
-
"""Filter by
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
152
|
+
def by_path(self, path_str: str, resolved: bool = False) -> "CollectionQuery":
|
|
153
|
+
"""Filter by case-insensitive substring of the track's file path.
|
|
154
|
+
FolderPath holds the full path to the file, including its name and
|
|
155
|
+
extension.
|
|
156
|
+
|
|
157
|
+
Separators get normalized to posix format, and a trailing '/' is kept
|
|
158
|
+
so the substring only matches directory components.
|
|
159
|
+
--path args match as given.
|
|
160
|
+
--resolved-path args are made absolute against the working directory
|
|
161
|
+
by pure string math (no filesystem access), so casing stays as typed
|
|
162
|
+
and symlinks are never followed.
|
|
160
163
|
"""
|
|
161
|
-
|
|
164
|
+
if not path_str:
|
|
165
|
+
return self
|
|
162
166
|
|
|
163
|
-
|
|
167
|
+
new_inst = self._copy()
|
|
168
|
+
has_trailing_sep = path_str.endswith(("/", "\\"))
|
|
164
169
|
|
|
165
|
-
if
|
|
166
|
-
|
|
167
|
-
if
|
|
168
|
-
|
|
169
|
-
name_part = ""
|
|
170
|
-
elif "/" not in path_str and "\\" not in path_str:
|
|
171
|
-
folder_part = ""
|
|
172
|
-
name_part = resolved.name
|
|
173
|
-
else:
|
|
174
|
-
folder_part = resolved.parent.as_posix() + "/"
|
|
175
|
-
name_part = resolved.name
|
|
170
|
+
if resolved:
|
|
171
|
+
pattern = Path(os.path.abspath(path_str)).as_posix()
|
|
172
|
+
if has_trailing_sep and not pattern.endswith("/"):
|
|
173
|
+
pattern += "/"
|
|
176
174
|
else:
|
|
177
|
-
|
|
178
|
-
if is_dir_only:
|
|
179
|
-
folder_part = normalized.as_posix()
|
|
180
|
-
name_part = ""
|
|
181
|
-
else:
|
|
182
|
-
parent_str = normalized.parent.as_posix()
|
|
183
|
-
folder_part = "" if parent_str == "." else parent_str
|
|
184
|
-
name_part = normalized.name
|
|
185
|
-
|
|
186
|
-
conditions: list[ColumnElement[bool]] = []
|
|
187
|
-
if folder_part:
|
|
188
|
-
if exact:
|
|
189
|
-
conditions.append(DjmdContent.FolderPath == folder_part)
|
|
190
|
-
else:
|
|
191
|
-
conditions.append(DjmdContent.FolderPath.ilike(f"%{folder_part}%"))
|
|
192
|
-
if name_part:
|
|
193
|
-
if exact:
|
|
194
|
-
conditions.append(DjmdContent.FileNameL == name_part)
|
|
195
|
-
else:
|
|
196
|
-
conditions.append(DjmdContent.FileNameL.ilike(f"%{name_part}%"))
|
|
197
|
-
|
|
198
|
-
if conditions:
|
|
199
|
-
combined = and_(*conditions) if len(conditions) > 1 else conditions[0]
|
|
200
|
-
new_inst._conditions.append(combined)
|
|
175
|
+
pattern = path_str.replace("\\", "/")
|
|
201
176
|
|
|
177
|
+
new_inst._conditions.append(DjmdContent.FolderPath.ilike(f"%{pattern}%"))
|
|
202
178
|
return new_inst
|
|
203
179
|
|
|
204
180
|
def limit(self, count: int) -> "CollectionQuery":
|
|
@@ -317,8 +293,8 @@ def get_filtered_content(
|
|
|
317
293
|
for path in filters.path:
|
|
318
294
|
query = query.by_path(path)
|
|
319
295
|
|
|
320
|
-
for
|
|
321
|
-
query = query.by_path(
|
|
296
|
+
for resolved_path in filters.resolved_path:
|
|
297
|
+
query = query.by_path(resolved_path, resolved=True)
|
|
322
298
|
|
|
323
299
|
if filters.match_all:
|
|
324
300
|
query = query.match_all()
|
|
@@ -468,58 +468,44 @@ class TestCollectionQuery:
|
|
|
468
468
|
assert " and " in stmt_str
|
|
469
469
|
assert " or " not in stmt_str
|
|
470
470
|
|
|
471
|
-
def
|
|
472
|
-
"""A
|
|
473
|
-
|
|
474
|
-
"""
|
|
471
|
+
def test_by_path_substring_against_folderpath_only(self):
|
|
472
|
+
"""A --path arg adds a single case-insensitive substring condition on
|
|
473
|
+
FolderPath, which holds the full file path. FileNameL is never queried."""
|
|
475
474
|
query = CollectionQuery()
|
|
476
|
-
new_query = query.by_path("
|
|
475
|
+
new_query = query.by_path("Daft Punk")
|
|
477
476
|
|
|
478
477
|
assert new_query is not query
|
|
479
478
|
assert len(new_query._conditions) == 1
|
|
480
479
|
condition_str = str(new_query._conditions[0])
|
|
481
|
-
assert "
|
|
480
|
+
assert "FolderPath" in condition_str
|
|
481
|
+
assert "FileNameL" not in condition_str
|
|
482
482
|
assert "LIKE lower" in condition_str
|
|
483
|
-
assert "FolderPath" not in condition_str
|
|
484
483
|
|
|
485
|
-
def
|
|
486
|
-
"""
|
|
487
|
-
The FileNameL condition is skipped — the filter still runs with just FolderPath.
|
|
488
|
-
"""
|
|
484
|
+
def test_by_path_wraps_with_wildcards(self):
|
|
485
|
+
"""The substring pattern is wrapped in % wildcards."""
|
|
489
486
|
query = CollectionQuery()
|
|
490
|
-
new_query = query.by_path("
|
|
487
|
+
new_query = query.by_path("track.mp3")
|
|
491
488
|
|
|
492
|
-
|
|
493
|
-
assert
|
|
494
|
-
condition_str = str(new_query._conditions[0])
|
|
495
|
-
assert "FileNameL" not in condition_str
|
|
496
|
-
assert "LIKE lower" in condition_str
|
|
497
|
-
assert "FolderPath" in condition_str
|
|
489
|
+
condition_str = _compile(new_query._conditions[0])
|
|
490
|
+
assert "%track.mp3%" in condition_str
|
|
498
491
|
|
|
499
|
-
def
|
|
500
|
-
"""A
|
|
492
|
+
def test_by_path_preserves_trailing_slash(self):
|
|
493
|
+
"""A trailing slash stays in the pattern so it only matches directory
|
|
494
|
+
components, not filename prefixes."""
|
|
501
495
|
query = CollectionQuery()
|
|
502
|
-
new_query = query.by_path("
|
|
496
|
+
new_query = query.by_path("Daft Punk/")
|
|
503
497
|
|
|
504
|
-
|
|
505
|
-
assert
|
|
506
|
-
condition_str = str(new_query._conditions[0])
|
|
507
|
-
assert "FolderPath" in condition_str
|
|
508
|
-
assert "FileNameL" in condition_str
|
|
509
|
-
assert " AND " in condition_str
|
|
510
|
-
assert condition_str.count("LIKE lower") == 2
|
|
498
|
+
condition_str = _compile(new_query._conditions[0])
|
|
499
|
+
assert "%Daft Punk/%" in condition_str
|
|
511
500
|
|
|
512
|
-
@pytest.mark.skipif(
|
|
513
|
-
platform.system() != "Windows", reason="backslash path parsing is Windows-only"
|
|
514
|
-
)
|
|
515
501
|
def test_by_path_backslash_normalised_to_forward_slash(self):
|
|
516
502
|
"""Backslash separators in --path input are normalised to forward slashes."""
|
|
517
503
|
query = CollectionQuery()
|
|
518
|
-
new_query = query.by_path("Music\\Artist\\
|
|
504
|
+
new_query = query.by_path("Music\\Artist\\")
|
|
519
505
|
|
|
520
|
-
|
|
521
|
-
assert "\\" not in
|
|
522
|
-
assert "Music/Artist" in
|
|
506
|
+
condition_str = _compile(new_query._conditions[0])
|
|
507
|
+
assert "\\" not in condition_str
|
|
508
|
+
assert "%Music/Artist/%" in condition_str
|
|
523
509
|
|
|
524
510
|
def test_by_path_no_resolve(self):
|
|
525
511
|
"""by_path does NOT resolve the path."""
|
|
@@ -530,87 +516,83 @@ class TestCollectionQuery:
|
|
|
530
516
|
# The raw string (or parts of it) must appear, not a resolved absolute path
|
|
531
517
|
assert ".." in condition_str and "relative" in condition_str
|
|
532
518
|
|
|
533
|
-
def
|
|
534
|
-
"""
|
|
519
|
+
def test_by_path_empty_arg_adds_no_condition(self):
|
|
520
|
+
"""An empty --path arg is a no-op rather than matching everything."""
|
|
535
521
|
query = CollectionQuery()
|
|
536
|
-
new_query = query.by_path("
|
|
522
|
+
new_query = query.by_path("")
|
|
537
523
|
|
|
538
|
-
|
|
539
|
-
assert "%" in condition_str
|
|
524
|
+
assert new_query._conditions == []
|
|
540
525
|
|
|
541
|
-
# --- by_path
|
|
526
|
+
# --- by_path resolved mode ---
|
|
542
527
|
|
|
543
|
-
def
|
|
544
|
-
"""
|
|
528
|
+
def test_by_resolved_path_resolves_relative_path(self):
|
|
529
|
+
"""Resolved mode makes relative paths absolute against the cwd before querying."""
|
|
530
|
+
cwd = Path(os.getcwd()).as_posix()
|
|
545
531
|
query = CollectionQuery()
|
|
546
|
-
new_query = query.by_path("track.mp3",
|
|
532
|
+
new_query = query.by_path("Album/track.mp3", resolved=True)
|
|
547
533
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
assert "FolderPath" not in condition_str
|
|
551
|
-
assert "FileNameL" in condition_str
|
|
552
|
-
assert "=" in condition_str
|
|
553
|
-
assert "LIKE lower" not in condition_str
|
|
534
|
+
condition_str = _compile(new_query._conditions[0])
|
|
535
|
+
assert f"%{cwd}/Album/track.mp3%" in condition_str
|
|
554
536
|
|
|
555
|
-
def
|
|
556
|
-
"""
|
|
537
|
+
def test_by_resolved_path_substring_case_insensitive(self):
|
|
538
|
+
"""Resolved mode is a case-insensitive substring check on FolderPath,
|
|
539
|
+
same as fuzzy mode, so a resolved folder path matches every track under it."""
|
|
557
540
|
query = CollectionQuery()
|
|
558
|
-
|
|
559
|
-
path = "/Test/Artist/"
|
|
560
|
-
new_query = query.by_path(path, exact=True)
|
|
541
|
+
new_query = query.by_path("/Test/Artist/file.mp3", resolved=True)
|
|
561
542
|
|
|
562
543
|
assert len(new_query._conditions) == 1
|
|
563
|
-
condition_str =
|
|
544
|
+
condition_str = str(new_query._conditions[0])
|
|
564
545
|
assert "FolderPath" in condition_str
|
|
565
546
|
assert "FileNameL" not in condition_str
|
|
566
|
-
assert "
|
|
567
|
-
assert path in condition_str
|
|
568
|
-
assert "like lower" not in condition_str
|
|
547
|
+
assert "LIKE lower" in condition_str
|
|
569
548
|
|
|
570
|
-
def
|
|
571
|
-
"""
|
|
549
|
+
def test_by_resolved_path_is_lexical_and_preserves_casing(self):
|
|
550
|
+
"""Resolution is pure string math: even for a directory that exists on
|
|
551
|
+
disk, the casing stays as typed instead of being rewritten to the
|
|
552
|
+
on-disk casing (Path.resolve would rewrite it on Windows)."""
|
|
553
|
+
miscased_cwd = Path(os.getcwd()).as_posix().swapcase()
|
|
572
554
|
query = CollectionQuery()
|
|
573
|
-
|
|
574
|
-
abs_path = "/Artist/track.mp3"
|
|
575
|
-
new_query = query.by_path(abs_path, exact=True)
|
|
555
|
+
new_query = query.by_path(f"{miscased_cwd}/track.mp3", resolved=True)
|
|
576
556
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
assert "FolderPath" in condition_str
|
|
580
|
-
assert "FileNameL" in condition_str
|
|
581
|
-
assert " AND " in condition_str
|
|
582
|
-
assert "LIKE lower" not in condition_str
|
|
557
|
+
condition_str = _compile(new_query._conditions[0])
|
|
558
|
+
assert f"{miscased_cwd}/track.mp3" in condition_str
|
|
583
559
|
|
|
584
|
-
def
|
|
585
|
-
"""
|
|
586
|
-
|
|
560
|
+
def test_by_resolved_path_preserves_trailing_slash(self):
|
|
561
|
+
"""A trailing slash survives resolution so the pattern only matches
|
|
562
|
+
directory components."""
|
|
587
563
|
query = CollectionQuery()
|
|
588
|
-
new_query = query.by_path("Album/
|
|
564
|
+
new_query = query.by_path("Album/", resolved=True)
|
|
589
565
|
|
|
590
566
|
condition_str = _compile(new_query._conditions[0])
|
|
591
|
-
|
|
592
|
-
assert cwd in condition_str
|
|
567
|
+
assert f"{Path(os.getcwd()).as_posix()}/Album/" in condition_str
|
|
593
568
|
|
|
594
569
|
@pytest.mark.skipif(
|
|
595
570
|
platform.system() != "Windows", reason="backslash path parsing is Windows-only"
|
|
596
571
|
)
|
|
597
|
-
def
|
|
598
|
-
"""Backslash separators in --
|
|
572
|
+
def test_by_resolved_path_backslash_normalised_to_forward_slash(self):
|
|
573
|
+
"""Backslash separators in --resolved-path input are normalised via as_posix()."""
|
|
599
574
|
query = CollectionQuery()
|
|
600
575
|
# Pass a Windows-style absolute path; as_posix() must produce forward slashes
|
|
601
576
|
new_query = query.by_path(
|
|
602
|
-
"C:\\Users\\foo\\music\\Artist\\track.mp3",
|
|
577
|
+
"C:\\Users\\foo\\music\\Artist\\track.mp3", resolved=True
|
|
603
578
|
)
|
|
604
579
|
|
|
605
580
|
condition_str = _compile(new_query._conditions[0])
|
|
606
581
|
assert "\\" not in condition_str
|
|
607
|
-
assert "/" in condition_str
|
|
582
|
+
assert "C:/Users/foo/music/Artist/track.mp3" in condition_str
|
|
583
|
+
|
|
584
|
+
def test_by_resolved_path_empty_arg_adds_no_condition(self):
|
|
585
|
+
"""An empty --resolved-path arg is a no-op rather than matching everything."""
|
|
586
|
+
query = CollectionQuery()
|
|
587
|
+
new_query = query.by_path("", resolved=True)
|
|
588
|
+
|
|
589
|
+
assert new_query._conditions == []
|
|
608
590
|
|
|
609
591
|
def test_by_path_returns_new_instance(self):
|
|
610
592
|
"""by_path always returns a new CollectionQuery instance."""
|
|
611
593
|
query = CollectionQuery()
|
|
612
594
|
assert query.by_path("track.mp3") is not query
|
|
613
|
-
assert query.by_path("track.mp3",
|
|
595
|
+
assert query.by_path("track.mp3", resolved=True) is not query
|
|
614
596
|
|
|
615
597
|
|
|
616
598
|
@pytest.fixture
|
|
@@ -699,9 +681,9 @@ class TestGetFilteredContent:
|
|
|
699
681
|
get_filtered_content(mock_db, FilterArgs(path=["Music/track.mp3"]))
|
|
700
682
|
mock_query.by_path.assert_called_once_with("Music/track.mp3")
|
|
701
683
|
|
|
702
|
-
def
|
|
703
|
-
get_filtered_content(mock_db, FilterArgs(
|
|
704
|
-
mock_query.by_path.assert_called_once_with("/Music/track.wav",
|
|
684
|
+
def test_resolved_path(self, mock_db, mock_query):
|
|
685
|
+
get_filtered_content(mock_db, FilterArgs(resolved_path=["/Music/track.wav"]))
|
|
686
|
+
mock_query.by_path.assert_called_once_with("/Music/track.wav", resolved=True)
|
|
705
687
|
|
|
706
688
|
def test_format(self, mock_db, mock_query):
|
|
707
689
|
get_filtered_content(mock_db, FilterArgs(format=["flac"]))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/actions/commit-check/action.yml
RENAMED
|
File without changes
|
{rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/.github/actions/commitizen-bump/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
|
|
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
|
|
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
|
|
File without changes
|
{rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/__snapshots__/test_journey.ambr
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/audio/06-wav-96k-24b.wav
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/macos/master.6.8.6.db
RENAMED
|
File without changes
|
{rekordbox_edit-0.7.0.dev72 → rekordbox_edit-0.8.0.dev6}/tests/e2e/fixtures/windows/master.6.8.6.db
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|