rekordbox-edit 0.7.0.dev44__tar.gz → 0.7.0.dev49__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.dev44 → rekordbox_edit-0.7.0.dev49}/.gitignore +2 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/CHANGELOG.md +12 -3
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/PKG-INFO +1 -1
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/pyproject.toml +1 -1
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/api/convert.py +83 -65
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/cli/convert.py +6 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/display.py +3 -3
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/models.py +4 -1
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/utils.py +9 -6
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/ruff.toml +2 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/api/test_convert.py +307 -163
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/cli/test_convert.py +16 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/test_utils.py +4 -8
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/uv.lock +1 -1
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.agent-style/RULES.md +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.agent-style/claude-code.md +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.github/actions/build-release-notes/action.yml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.github/actions/commit-check/action.yml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.github/actions/commitizen-bump/action.yml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.github/actions/commitizen-bump/commitizen-bump.sh +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.github/actions/e2e/action.yml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.github/actions/install/action.yml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.github/actions/lint/action.yml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.github/actions/test/action.yml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.github/workflows/cd.yml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.github/workflows/ci.yml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.github/workflows/publish.yml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.github/workflows/release.yml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.pre-commit-config.yaml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.python-version +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/.readthedocs.yaml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/AGENTS.md +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/CLAUDE.md +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/CONTRIBUTING.md +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/LICENSE +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/Makefile +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/README.md +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/codecov.yml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/docker-compose.yml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/docs/api.md +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/docs/commands/convert.md +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/docs/commands/edit.md +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/docs/commands/search.md +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/docs/filtering.md +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/docs/index.md +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/docs/stylesheets/extra.css +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/mkdocs.yml +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/__init__.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/_click.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/api/__init__.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/api/_utils.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/api/edit.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/api/search.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/cli/__init__.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/cli/_utils.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/cli/edit.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/cli/main.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/cli/search.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/logger.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/rekordbox_edit/query.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/renovate.json5 +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/__init__.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/api/__init__.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/api/test_edit.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/api/test_search.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/api/test_utils.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/cli/__init__.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/cli/test_edit.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/cli/test_main.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/cli/test_search.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/cli/test_utils.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/conftest.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/Dockerfile +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/__init__.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/__snapshots__/test_journey/test_search_full_json_snapshot[macos].json +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/__snapshots__/test_journey/test_search_full_json_snapshot[windows].json +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/__snapshots__/test_journey.ambr +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/conftest.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/fixtures/audio/01-flac-44_1k-16b.flac +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/fixtures/audio/02-flac-96k-24b.flac +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/fixtures/audio/03-alac-44_1k-16b.m4a +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/fixtures/audio/04-alac-48k-24b.m4a +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/fixtures/audio/05-aiff-44_1k-16b.aiff +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/fixtures/audio/06-wav-96k-24b.wav +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/fixtures/audio/07-mp3-44_1k-320cbr.mp3 +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/fixtures/audio/08-mp3-44_1k-v0vbr.mp3 +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/fixtures/audio/09-aac-44_1k-256kbps.m4a +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/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.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/fixtures/macos/master.6.8.6.db +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/fixtures/windows/master.6.8.6.db +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/e2e/test_journey.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/test_display.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/test_logger.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/test_models.py +0 -0
- {rekordbox_edit-0.7.0.dev44 → rekordbox_edit-0.7.0.dev49}/tests/test_query.py +0 -0
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
## v0.7.0.
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
## v0.7.0.dev49 (2026-07-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
- chore: ignore research/decision docs
|
|
5
|
+
- fix(convert): update FileSize, OrgFilePath cols; PPTH ANLZ tag
|
|
6
|
+
- refactor(convert): merge shared logic of ffmpeg helpers
|
|
7
|
+
- Replace _convert_to_hi_res and _convert_to_mp3 with a single _run_ffmpeg
|
|
8
|
+
runner and two pure output-kwargs builders.
|
|
9
|
+
- fix(convert): skip unsupported source formats via input whitelist
|
|
10
|
+
- Previously we were skipping via a blacklist, which allowed
|
|
11
|
+
unknown/unsupported formats to create undefined behavior.
|
|
12
|
+
- refactor(convert): change get_file_type_name a simple display map
|
|
4
13
|
- chore(deps): update dependency ty to v0.0.59 (#134)
|
|
5
14
|
- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
|
|
6
15
|
- chore(deps): update dependency syrupy to v5.5.3 (#133)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rekordbox-edit
|
|
3
|
-
Version: 0.7.0.
|
|
3
|
+
Version: 0.7.0.dev49
|
|
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.7.0.
|
|
7
|
+
version = "0.7.0.dev49"
|
|
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"
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
-
import posixpath
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from typing import Literal, Tuple
|
|
8
7
|
|
|
@@ -22,6 +21,7 @@ from rekordbox_edit.models import (
|
|
|
22
21
|
)
|
|
23
22
|
from rekordbox_edit.query import get_filtered_content
|
|
24
23
|
from rekordbox_edit.utils import (
|
|
24
|
+
InputFormats,
|
|
25
25
|
OutputFormats,
|
|
26
26
|
get_audio_info,
|
|
27
27
|
get_extension_for_format,
|
|
@@ -34,6 +34,9 @@ logger = logging.getLogger(__name__)
|
|
|
34
34
|
TARGET_BIT_DEPTH = 16
|
|
35
35
|
TARGET_SAMPLE_RATE = 44100
|
|
36
36
|
|
|
37
|
+
# Rekordbox FileType codes RBE converts from: the hi-res lossless whitelist.
|
|
38
|
+
_INPUT_FILE_TYPES = {get_file_type_for_format(fmt.value) for fmt in InputFormats}
|
|
39
|
+
|
|
37
40
|
_HI_RES_CODECS = {
|
|
38
41
|
"aiff": "pcm_s16be",
|
|
39
42
|
"wav": "pcm_s16le",
|
|
@@ -74,20 +77,13 @@ def _classify_source_fidelity(
|
|
|
74
77
|
return "lossy", effective_rate
|
|
75
78
|
|
|
76
79
|
|
|
77
|
-
def
|
|
78
|
-
"""
|
|
79
|
-
|
|
80
|
-
sources."""
|
|
81
|
-
from rekordbox_edit.utils import ffmpeg_in_path, get_ffmpeg_directions
|
|
82
|
-
|
|
83
|
-
if not ffmpeg_in_path():
|
|
84
|
-
raise Exception(f"FFmpeg not found in PATH.{get_ffmpeg_directions()}")
|
|
85
|
-
|
|
80
|
+
def _hi_res_output_kwargs(output_format, sample_rate) -> dict:
|
|
81
|
+
"""ffmpeg output kwargs for a hi-res conversion at the target bit depth and
|
|
82
|
+
the given sample rate."""
|
|
86
83
|
codec = _HI_RES_CODECS.get(output_format.value)
|
|
87
84
|
if codec is None:
|
|
88
85
|
raise Exception(f"Unsupported hi-res format: {output_format}")
|
|
89
|
-
|
|
90
|
-
output_kwargs = {
|
|
86
|
+
kwargs = {
|
|
91
87
|
"acodec": codec,
|
|
92
88
|
"ar": sample_rate,
|
|
93
89
|
"map_metadata": 0,
|
|
@@ -95,35 +91,26 @@ def _convert_to_hi_res(input_path, output_path, output_format, sample_rate):
|
|
|
95
91
|
}
|
|
96
92
|
# PCM codecs fix the bit depth by name; the flac encoder needs it spelled out.
|
|
97
93
|
if output_format.value == "flac":
|
|
98
|
-
|
|
94
|
+
kwargs["sample_fmt"] = "s16"
|
|
95
|
+
return kwargs
|
|
99
96
|
|
|
100
|
-
logger.debug(
|
|
101
|
-
f"Selected codec: {codec} targeting {TARGET_BIT_DEPTH}-bit/{sample_rate} Hz"
|
|
102
|
-
)
|
|
103
97
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if e.stderr:
|
|
116
|
-
stderr = e.stderr.decode() if isinstance(e.stderr, bytes) else e.stderr
|
|
117
|
-
logger.debug(f"FFmpeg stderr:\n{stderr}")
|
|
118
|
-
return False
|
|
119
|
-
except Exception as e:
|
|
120
|
-
logger.error(f"Conversion failed for {input_path}: {e}")
|
|
121
|
-
raise e
|
|
98
|
+
def _mp3_output_kwargs(sample_rate) -> dict:
|
|
99
|
+
"""ffmpeg output kwargs for MP3 320kbps CBR at the given sample rate."""
|
|
100
|
+
return {
|
|
101
|
+
"acodec": "libmp3lame",
|
|
102
|
+
"audio_bitrate": "320k",
|
|
103
|
+
"ar": sample_rate,
|
|
104
|
+
# libmp3lame takes planar input; s16p quantizes to 16-bit before encoding.
|
|
105
|
+
"sample_fmt": "s16p",
|
|
106
|
+
"map_metadata": 0,
|
|
107
|
+
"write_id3v2": 1,
|
|
108
|
+
}
|
|
122
109
|
|
|
123
110
|
|
|
124
|
-
def
|
|
125
|
-
"""
|
|
126
|
-
|
|
111
|
+
def _run_ffmpeg(input_path, output_path, output_kwargs: dict, label: str) -> bool:
|
|
112
|
+
"""Run a single ffmpeg conversion. Returns True on success, False on an
|
|
113
|
+
ffmpeg error; re-raises anything else. `label` names the target for logs."""
|
|
127
114
|
from rekordbox_edit.utils import ffmpeg_in_path, get_ffmpeg_directions
|
|
128
115
|
|
|
129
116
|
if not ffmpeg_in_path():
|
|
@@ -132,21 +119,11 @@ def _convert_to_mp3(input_path, mp3_path):
|
|
|
132
119
|
try:
|
|
133
120
|
(
|
|
134
121
|
ffmpeg.input(input_path)
|
|
135
|
-
.output(
|
|
136
|
-
mp3_path,
|
|
137
|
-
acodec="libmp3lame",
|
|
138
|
-
audio_bitrate="320k",
|
|
139
|
-
ar=TARGET_SAMPLE_RATE,
|
|
140
|
-
# libmp3lame takes planar input; s16p quantizes to 16-bit
|
|
141
|
-
# before encoding.
|
|
142
|
-
sample_fmt="s16p",
|
|
143
|
-
map_metadata=0,
|
|
144
|
-
write_id3v2=1,
|
|
145
|
-
)
|
|
122
|
+
.output(output_path, **output_kwargs)
|
|
146
123
|
.overwrite_output()
|
|
147
124
|
.run(capture_stdout=True, capture_stderr=True)
|
|
148
125
|
)
|
|
149
|
-
logger.debug(f"Conversion to
|
|
126
|
+
logger.debug(f"Conversion to {label} succeeded: {output_path}")
|
|
150
127
|
return True
|
|
151
128
|
except FfmpegError as e:
|
|
152
129
|
logger.error(f"FFmpeg conversion failed for {input_path}: {e}")
|
|
@@ -160,15 +137,21 @@ def _convert_to_mp3(input_path, mp3_path):
|
|
|
160
137
|
|
|
161
138
|
|
|
162
139
|
def _update_database_record(
|
|
163
|
-
db
|
|
140
|
+
db: Rekordbox6Database,
|
|
141
|
+
content_id: str,
|
|
142
|
+
new_filename: str,
|
|
143
|
+
new_folder: str,
|
|
144
|
+
output_format: str,
|
|
164
145
|
) -> None:
|
|
165
146
|
"""Update database record with new file information."""
|
|
166
147
|
content = db.get_content().filter_by(ID=content_id).first()
|
|
167
148
|
if not content:
|
|
168
149
|
raise Exception(f"Content record with ID {content_id} not found")
|
|
169
150
|
|
|
170
|
-
|
|
171
|
-
|
|
151
|
+
old_path = content.FolderPath
|
|
152
|
+
converted_file_path = Path(new_folder, new_filename)
|
|
153
|
+
converted_db_path = str(converted_file_path).replace("\\", "/")
|
|
154
|
+
converted_audio_info = get_audio_info(converted_file_path)
|
|
172
155
|
converted_bitrate = converted_audio_info["bitrate"]
|
|
173
156
|
|
|
174
157
|
if output_format.upper() == "MP3" and converted_bitrate is None:
|
|
@@ -193,8 +176,13 @@ def _update_database_record(
|
|
|
193
176
|
content.BitDepth = converted_bit_depth
|
|
194
177
|
|
|
195
178
|
content.FileNameL = new_filename
|
|
196
|
-
content.FolderPath =
|
|
179
|
+
content.FolderPath = converted_db_path
|
|
197
180
|
content.FileType = file_type
|
|
181
|
+
content.FileSize = os.path.getsize(converted_file_path)
|
|
182
|
+
|
|
183
|
+
# Original location: only update if it matches FolderPath
|
|
184
|
+
if content.OrgFolderPath == old_path:
|
|
185
|
+
content.OrgFolderPath = converted_db_path
|
|
198
186
|
|
|
199
187
|
# FLAC stores bitrate as 0 in Rekordbox to represent VBR
|
|
200
188
|
if output_format.upper() == "FLAC":
|
|
@@ -203,6 +191,24 @@ def _update_database_record(
|
|
|
203
191
|
content.BitRate = converted_bitrate
|
|
204
192
|
|
|
205
193
|
|
|
194
|
+
def _update_anlz_paths(
|
|
195
|
+
db: Rekordbox6Database, content: DjmdContent, new_filename: str
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Rewrite the PPTH path tag in a track's ANLZ files to the converted file
|
|
198
|
+
name, in rekordbox's device-relative ``?/<name>`` form.
|
|
199
|
+
|
|
200
|
+
No-op for tracks without an analysis.
|
|
201
|
+
"""
|
|
202
|
+
if not content.AnalysisDataPath:
|
|
203
|
+
return
|
|
204
|
+
new_ppth = f"?/{new_filename}"
|
|
205
|
+
anlz_files = db.read_anlz_files(content.ID)
|
|
206
|
+
for anlz_path, anlz in anlz_files.items():
|
|
207
|
+
anlz.set_path(new_ppth)
|
|
208
|
+
anlz.save(anlz_path)
|
|
209
|
+
logger.debug(f"Updated PPTH of {anlz_path} to {new_ppth}")
|
|
210
|
+
|
|
211
|
+
|
|
206
212
|
def _cleanup_converted_files(converted_ops: list[ConvertOp]) -> None:
|
|
207
213
|
"""Clean up converted output files on error or rollback."""
|
|
208
214
|
logger.debug("Cleaning up converted files due to aborted conversion.")
|
|
@@ -252,14 +258,18 @@ def _classify_convert(content, args: ConvertRequest) -> ConvertOp | SkippedTrack
|
|
|
252
258
|
"""Return ConvertOp if this track should be converted, or SkippedTrack with
|
|
253
259
|
reason if not."""
|
|
254
260
|
target = get_file_type_for_format(args.format_out)
|
|
255
|
-
|
|
256
|
-
m4a = get_file_type_for_format("M4A")
|
|
257
|
-
if content.FileType in (target, mp3, m4a):
|
|
261
|
+
if content.FileType == target:
|
|
258
262
|
logger.debug(
|
|
259
263
|
f"skip convert id={content.ID} reason=already_target_format "
|
|
260
264
|
f"file_type={content.FileType} target={target}"
|
|
261
265
|
)
|
|
262
266
|
return SkippedTrack(id=str(content.ID), reason="already_target_format")
|
|
267
|
+
if content.FileType not in _INPUT_FILE_TYPES:
|
|
268
|
+
logger.debug(
|
|
269
|
+
f"skip convert id={content.ID} reason=unsupported_source_format "
|
|
270
|
+
f"file_type={content.FileType}"
|
|
271
|
+
)
|
|
272
|
+
return SkippedTrack(id=str(content.ID), reason="unsupported_source_format")
|
|
263
273
|
output_path, _, _ = _get_output_path(content, args.format_out)
|
|
264
274
|
if not args.overwrite and os.path.exists(output_path):
|
|
265
275
|
logger.debug(
|
|
@@ -276,11 +286,7 @@ def _classify_convert(content, args: ConvertRequest) -> ConvertOp | SkippedTrack
|
|
|
276
286
|
id=str(content.ID),
|
|
277
287
|
source_path=content.FolderPath or "",
|
|
278
288
|
output_path=output_path,
|
|
279
|
-
source_file_type=(
|
|
280
|
-
get_file_type_name(content.FileType)
|
|
281
|
-
if content.FileType is not None
|
|
282
|
-
else None
|
|
283
|
-
),
|
|
289
|
+
source_file_type=get_file_type_name(content.FileType),
|
|
284
290
|
source_bit_depth=content.BitDepth,
|
|
285
291
|
source_sample_rate=content.SampleRate,
|
|
286
292
|
output_file_type=args.format_out.upper(),
|
|
@@ -370,16 +376,19 @@ def convert(
|
|
|
370
376
|
|
|
371
377
|
if args.format_out.upper() == "MP3":
|
|
372
378
|
output_sample_rate = TARGET_SAMPLE_RATE
|
|
373
|
-
success =
|
|
379
|
+
success = _run_ffmpeg(
|
|
380
|
+
src, op.output_path, _mp3_output_kwargs(output_sample_rate), "mp3"
|
|
381
|
+
)
|
|
374
382
|
else:
|
|
375
383
|
fidelity, output_sample_rate = _classify_source_fidelity(src)
|
|
376
384
|
if fidelity == "lossless":
|
|
377
385
|
lossless_op_ids.add(op.id)
|
|
378
|
-
|
|
386
|
+
output_format = OutputFormats(args.format_out.lower())
|
|
387
|
+
success = _run_ffmpeg(
|
|
379
388
|
src,
|
|
380
389
|
op.output_path,
|
|
381
|
-
|
|
382
|
-
|
|
390
|
+
_hi_res_output_kwargs(output_format, output_sample_rate),
|
|
391
|
+
output_format.value,
|
|
383
392
|
)
|
|
384
393
|
|
|
385
394
|
if not success:
|
|
@@ -415,6 +424,15 @@ def convert(
|
|
|
415
424
|
_rollback_and_cleanup(db, converted_ops)
|
|
416
425
|
raise
|
|
417
426
|
|
|
427
|
+
for op in converted_ops:
|
|
428
|
+
content = content_map[op.id]
|
|
429
|
+
try:
|
|
430
|
+
_update_anlz_paths(db, content, os.path.basename(op.output_path))
|
|
431
|
+
except Exception as e:
|
|
432
|
+
logger.warning(
|
|
433
|
+
f"Failed to update ANLZ path tags for {content.FileNameL}: {e}"
|
|
434
|
+
)
|
|
435
|
+
|
|
418
436
|
if args.delete_originals == "all":
|
|
419
437
|
deletable_ops = converted_ops
|
|
420
438
|
elif args.delete_originals == "lossless":
|
|
@@ -127,9 +127,15 @@ def convert_command(db, **kwargs):
|
|
|
127
127
|
|
|
128
128
|
def _report_skips(skipped) -> None:
|
|
129
129
|
already_target = sum(1 for s in skipped if s.reason == "already_target_format")
|
|
130
|
+
unsupported = sum(1 for s in skipped if s.reason == "unsupported_source_format")
|
|
130
131
|
conflicts = sum(1 for s in skipped if s.reason == "output_file_exists")
|
|
131
132
|
if already_target:
|
|
132
133
|
logger.warning(f"Skipping {already_target} file(s): already in target format")
|
|
134
|
+
if unsupported:
|
|
135
|
+
logger.warning(
|
|
136
|
+
f"Skipping {unsupported} file(s): unsupported source format "
|
|
137
|
+
"(only FLAC, AIFF, WAV are converted)"
|
|
138
|
+
)
|
|
133
139
|
if conflicts:
|
|
134
140
|
logger.warning(
|
|
135
141
|
f"Skipping {conflicts} file(s): output exists (use --overwrite to convert)"
|
|
@@ -78,13 +78,13 @@ def _cell_value(track: Track, column: PrintableField) -> str:
|
|
|
78
78
|
if column is PrintableField.FileType:
|
|
79
79
|
if track.FileType is None:
|
|
80
80
|
return ""
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
except ValueError:
|
|
81
|
+
name = get_file_type_name(track.FileType)
|
|
82
|
+
if name is None:
|
|
84
83
|
logger.warning(
|
|
85
84
|
f"Unexpected FileType [{track.FileType}] for track ID [{track.ID}]"
|
|
86
85
|
)
|
|
87
86
|
return "UNKNOWN"
|
|
87
|
+
return name
|
|
88
88
|
if column is PrintableField.FolderPath:
|
|
89
89
|
return os.path.dirname(track.FolderPath or "")
|
|
90
90
|
value = getattr(track, column.value, None)
|
|
@@ -126,7 +126,10 @@ class Track(BaseModel):
|
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
SkipReason: TypeAlias = Literal[
|
|
129
|
-
"no_change",
|
|
129
|
+
"no_change",
|
|
130
|
+
"already_target_format",
|
|
131
|
+
"unsupported_source_format",
|
|
132
|
+
"output_file_exists",
|
|
130
133
|
]
|
|
131
134
|
|
|
132
135
|
|
|
@@ -18,8 +18,14 @@ class UserQuit(Exception):
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
# File type mappings for Rekordbox database
|
|
21
|
-
def get_file_type_name(file_type_code: int):
|
|
22
|
-
"""
|
|
21
|
+
def get_file_type_name(file_type_code: int | None) -> str | None:
|
|
22
|
+
"""Map a Rekordbox FileType code to a display name, or None if unmapped.
|
|
23
|
+
|
|
24
|
+
A total display map, not a guard: Rekordbox stores many codes RBE does not
|
|
25
|
+
convert (AAC, ALAC, video). Each caller chooses its own fallback for None.
|
|
26
|
+
"""
|
|
27
|
+
if file_type_code is None:
|
|
28
|
+
return None
|
|
23
29
|
_get_file_type_name = {
|
|
24
30
|
0: "MP3",
|
|
25
31
|
1: "MP3",
|
|
@@ -28,10 +34,7 @@ def get_file_type_name(file_type_code: int):
|
|
|
28
34
|
11: "WAV",
|
|
29
35
|
12: "AIFF",
|
|
30
36
|
}
|
|
31
|
-
|
|
32
|
-
if name is None:
|
|
33
|
-
raise ValueError(f"Unknown file_type: {file_type_code}")
|
|
34
|
-
return name
|
|
37
|
+
return _get_file_type_name.get(file_type_code)
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
def get_file_type_for_format(format_name: str):
|