TonieToolbox 0.5.0a1__py3-none-any.whl → 0.6.0a1__py3-none-any.whl
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.
- TonieToolbox/__init__.py +1 -1
- TonieToolbox/__main__.py +148 -127
- TonieToolbox/artwork.py +12 -7
- TonieToolbox/audio_conversion.py +104 -33
- TonieToolbox/config.py +1 -0
- TonieToolbox/constants.py +93 -9
- TonieToolbox/filename_generator.py +6 -8
- TonieToolbox/integration.py +20 -0
- TonieToolbox/integration_macos.py +428 -0
- TonieToolbox/integration_ubuntu.py +1 -0
- TonieToolbox/integration_windows.py +404 -0
- TonieToolbox/logger.py +8 -10
- TonieToolbox/media_tags.py +22 -101
- TonieToolbox/ogg_page.py +39 -39
- TonieToolbox/opus_packet.py +13 -13
- TonieToolbox/recursive_processor.py +32 -33
- TonieToolbox/tags.py +3 -4
- TonieToolbox/teddycloud.py +50 -50
- TonieToolbox/tonie_analysis.py +24 -23
- TonieToolbox/tonie_file.py +86 -71
- TonieToolbox/tonies_json.py +828 -36
- TonieToolbox/version_handler.py +12 -15
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0a1.dist-info}/METADATA +141 -98
- tonietoolbox-0.6.0a1.dist-info/RECORD +31 -0
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0a1.dist-info}/WHEEL +1 -1
- tonietoolbox-0.5.0a1.dist-info/RECORD +0 -26
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0a1.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0a1.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0a1.dist-info}/top_level.txt +0 -0
TonieToolbox/audio_conversion.py
CHANGED
@@ -7,29 +7,39 @@ import glob
|
|
7
7
|
import subprocess
|
8
8
|
import tempfile
|
9
9
|
from .dependency_manager import get_ffmpeg_binary, get_opus_binary
|
10
|
+
from .constants import SUPPORTED_EXTENSIONS
|
10
11
|
from .logger import get_logger
|
11
12
|
|
12
13
|
logger = get_logger('audio_conversion')
|
13
14
|
|
14
15
|
|
15
|
-
def get_opus_tempfile(
|
16
|
+
def get_opus_tempfile(
|
17
|
+
ffmpeg_binary: str = None,
|
18
|
+
opus_binary: str = None,
|
19
|
+
filename: str = None,
|
20
|
+
bitrate: int = 48,
|
21
|
+
vbr: bool = True,
|
22
|
+
keep_temp: bool = False,
|
23
|
+
auto_download: bool = False,
|
24
|
+
no_mono_conversion: bool = False
|
25
|
+
) -> tuple[tempfile.SpooledTemporaryFile | None, str | None]:
|
16
26
|
"""
|
17
27
|
Convert an audio file to Opus format and return a temporary file handle.
|
18
28
|
|
19
29
|
Args:
|
20
|
-
ffmpeg_binary: Path to the ffmpeg binary. If None, will be auto-detected or downloaded.
|
21
|
-
opus_binary: Path to the opusenc binary. If None, will be auto-detected or downloaded.
|
22
|
-
filename: Path to the input audio file
|
23
|
-
bitrate: Bitrate for the Opus encoding in kbps
|
24
|
-
vbr: Whether to use variable bitrate encoding
|
25
|
-
keep_temp: Whether to keep the temporary files for testing
|
26
|
-
auto_download: Whether to automatically download dependencies if not found
|
27
|
-
|
30
|
+
ffmpeg_binary (str | None): Path to the ffmpeg binary. If None, will be auto-detected or downloaded.
|
31
|
+
opus_binary (str | None): Path to the opusenc binary. If None, will be auto-detected or downloaded.
|
32
|
+
filename (str | None): Path to the input audio file
|
33
|
+
bitrate (int): Bitrate for the Opus encoding in kbps
|
34
|
+
vbr (bool): Whether to use variable bitrate encoding
|
35
|
+
keep_temp (bool): Whether to keep the temporary files for testing
|
36
|
+
auto_download (bool): Whether to automatically download dependencies if not found
|
37
|
+
no_mono_conversion (bool): Whether to skip mono to stereo conversion
|
28
38
|
Returns:
|
29
|
-
tuple: (file handle, temp_file_path) or (file handle, None) if keep_temp is False
|
39
|
+
tuple[tempfile.SpooledTemporaryFile | None, str | None]: (file handle, temp_file_path) or (file handle, None) if keep_temp is False
|
30
40
|
"""
|
31
|
-
logger.trace("Entering get_opus_tempfile(ffmpeg_binary=%s, opus_binary=%s, filename=%s, bitrate=%d, vbr=%s, keep_temp=%s, auto_download=%s)",
|
32
|
-
ffmpeg_binary, opus_binary, filename, bitrate, vbr, keep_temp, auto_download)
|
41
|
+
logger.trace("Entering get_opus_tempfile(ffmpeg_binary=%s, opus_binary=%s, filename=%s, bitrate=%d, vbr=%s, keep_temp=%s, auto_download=%s, no_mono_conversion=%s)",
|
42
|
+
ffmpeg_binary, opus_binary, filename, bitrate, vbr, keep_temp, auto_download, no_mono_conversion)
|
33
43
|
|
34
44
|
logger.debug("Converting %s to Opus format (bitrate: %d kbps, vbr: %s)", filename, bitrate, vbr)
|
35
45
|
|
@@ -52,6 +62,38 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
52
62
|
vbr_parameter = "--vbr" if vbr else "--hard-cbr"
|
53
63
|
logger.debug("Using encoding parameter: %s", vbr_parameter)
|
54
64
|
|
65
|
+
is_mono = False
|
66
|
+
ffprobe_path = None
|
67
|
+
ffmpeg_dir, ffmpeg_file = os.path.split(ffmpeg_binary)
|
68
|
+
ffprobe_candidates = [
|
69
|
+
os.path.join(ffmpeg_dir, 'ffprobe'),
|
70
|
+
os.path.join(ffmpeg_dir, 'ffprobe.exe'),
|
71
|
+
'ffprobe',
|
72
|
+
'ffprobe.exe',
|
73
|
+
]
|
74
|
+
for candidate in ffprobe_candidates:
|
75
|
+
if os.path.exists(candidate):
|
76
|
+
ffprobe_path = candidate
|
77
|
+
break
|
78
|
+
if ffprobe_path:
|
79
|
+
try:
|
80
|
+
probe_cmd = [ffprobe_path, '-v', 'error', '-select_streams', 'a:0', '-show_entries', 'stream=channels', '-of', 'default=noprint_wrappers=1:nokey=1', filename]
|
81
|
+
logger.debug(f"Probing audio channels with: {' '.join(probe_cmd)}")
|
82
|
+
result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
83
|
+
if result.returncode == 0:
|
84
|
+
channels = result.stdout.strip()
|
85
|
+
logger.debug(f"Detected channels: {channels}")
|
86
|
+
if channels == '1':
|
87
|
+
is_mono = True
|
88
|
+
else:
|
89
|
+
logger.warning(f"ffprobe failed to detect channels: {result.stderr}")
|
90
|
+
except Exception as e:
|
91
|
+
logger.warning(f"Mono detection failed: {e}")
|
92
|
+
else:
|
93
|
+
logger.warning("ffprobe not found, will always force stereo conversion for non-Opus input.")
|
94
|
+
is_mono = True # Always force stereo if we can't check
|
95
|
+
logger.info(f"Mono detected: {is_mono}, no_mono_conversion: {no_mono_conversion}")
|
96
|
+
|
55
97
|
temp_path = None
|
56
98
|
if keep_temp:
|
57
99
|
temp_dir = os.path.join(tempfile.gettempdir(), "tonie_toolbox_temp")
|
@@ -62,7 +104,12 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
62
104
|
|
63
105
|
logger.debug("Starting FFmpeg process")
|
64
106
|
try:
|
65
|
-
|
107
|
+
if is_mono and not no_mono_conversion:
|
108
|
+
ffmpeg_cmd = [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav", "-ar", "48000", "-ac", "2", "-"]
|
109
|
+
logger.info(f"Forcing stereo conversion for mono input: {' '.join(ffmpeg_cmd)}")
|
110
|
+
else:
|
111
|
+
ffmpeg_cmd = [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav", "-ar", "48000", "-"]
|
112
|
+
logger.info(f"FFmpeg command: {' '.join(ffmpeg_cmd)}")
|
66
113
|
logger.trace("FFmpeg command: %s", ffmpeg_cmd)
|
67
114
|
ffmpeg_process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE)
|
68
115
|
except FileNotFoundError:
|
@@ -106,7 +153,12 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
106
153
|
|
107
154
|
logger.debug("Starting FFmpeg process")
|
108
155
|
try:
|
109
|
-
|
156
|
+
if is_mono and not no_mono_conversion:
|
157
|
+
ffmpeg_cmd = [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav", "-ar", "48000", "-ac", "2", "-"]
|
158
|
+
logger.info(f"Forcing stereo conversion for mono input: {' '.join(ffmpeg_cmd)}")
|
159
|
+
else:
|
160
|
+
ffmpeg_cmd = [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav", "-ar", "48000", "-"]
|
161
|
+
logger.info(f"FFmpeg command: {' '.join(ffmpeg_cmd)}")
|
110
162
|
logger.trace("FFmpeg command: %s", ffmpeg_cmd)
|
111
163
|
ffmpeg_process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE)
|
112
164
|
except FileNotFoundError:
|
@@ -155,24 +207,20 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
155
207
|
return tmp_file, None
|
156
208
|
|
157
209
|
|
158
|
-
def filter_directories(glob_list):
|
210
|
+
def filter_directories(glob_list: list[str]) -> list[str]:
|
159
211
|
"""
|
160
212
|
Filter a list of glob results to include only audio files that can be handled by ffmpeg.
|
161
213
|
|
162
214
|
Args:
|
163
|
-
glob_list: List of path names from glob.glob()
|
164
|
-
|
215
|
+
glob_list (list[str]): List of path names from glob.glob()
|
165
216
|
Returns:
|
166
|
-
list: Filtered list containing only supported audio files
|
217
|
+
list[str]: Filtered list containing only supported audio files
|
167
218
|
"""
|
168
219
|
logger.trace("Entering filter_directories() with %d items", len(glob_list))
|
169
220
|
logger.debug("Filtering %d glob results for supported audio files", len(glob_list))
|
170
221
|
|
171
|
-
|
172
|
-
|
173
|
-
'.wav', '.mp3', '.aac', '.m4a', '.flac', '.ogg', '.opus',
|
174
|
-
'.ape', '.wma', '.aiff', '.mp2', '.mp4', '.webm', '.mka'
|
175
|
-
]
|
222
|
+
supported_extensions = SUPPORTED_EXTENSIONS
|
223
|
+
logger.debug("Supported audio file extensions: %s", supported_extensions)
|
176
224
|
|
177
225
|
filtered = []
|
178
226
|
for name in glob_list:
|
@@ -189,17 +237,16 @@ def filter_directories(glob_list):
|
|
189
237
|
return filtered
|
190
238
|
|
191
239
|
|
192
|
-
def get_input_files(input_filename):
|
240
|
+
def get_input_files(input_filename: str) -> list[str]:
|
193
241
|
"""
|
194
242
|
Get a list of input files to process.
|
195
243
|
|
196
244
|
Supports direct file paths, directory paths, glob patterns, and .lst files.
|
197
245
|
|
198
246
|
Args:
|
199
|
-
input_filename: Input file pattern or list file path
|
200
|
-
|
247
|
+
input_filename (str): Input file pattern or list file path
|
201
248
|
Returns:
|
202
|
-
list: List of input file paths
|
249
|
+
list[str]: List of input file paths
|
203
250
|
"""
|
204
251
|
logger.trace("Entering get_input_files(input_filename=%s)", input_filename)
|
205
252
|
logger.debug("Getting input files for pattern: %s", input_filename)
|
@@ -244,22 +291,46 @@ def get_input_files(input_filename):
|
|
244
291
|
|
245
292
|
logger.debug("Found %d files in list file", len(input_files))
|
246
293
|
else:
|
247
|
-
logger.debug("Processing
|
294
|
+
logger.debug("Processing input path: %s", input_filename)
|
295
|
+
|
296
|
+
# Try the exact pattern first
|
248
297
|
input_files = sorted(filter_directories(glob.glob(input_filename)))
|
249
|
-
|
298
|
+
if input_files:
|
299
|
+
logger.debug("Found %d files matching exact pattern", len(input_files))
|
300
|
+
else:
|
301
|
+
# If no extension is provided, try appending a wildcard for extension
|
302
|
+
_, ext = os.path.splitext(input_filename)
|
303
|
+
if not ext:
|
304
|
+
wildcard_pattern = input_filename + ".*"
|
305
|
+
logger.debug("No extension in pattern, trying with wildcard: %s", wildcard_pattern)
|
306
|
+
input_files = sorted(filter_directories(glob.glob(wildcard_pattern)))
|
307
|
+
|
308
|
+
# If still no files found, try treating it as a directory
|
309
|
+
if not input_files and os.path.exists(os.path.dirname(input_filename)):
|
310
|
+
potential_dir = input_filename
|
311
|
+
if os.path.isdir(potential_dir):
|
312
|
+
logger.debug("Treating input as directory: %s", potential_dir)
|
313
|
+
dir_glob = os.path.join(potential_dir, "*")
|
314
|
+
input_files = sorted(filter_directories(glob.glob(dir_glob)))
|
315
|
+
if input_files:
|
316
|
+
logger.debug("Found %d audio files in directory", len(input_files))
|
317
|
+
|
318
|
+
if input_files:
|
319
|
+
logger.debug("Found %d files after trying alternatives", len(input_files))
|
320
|
+
else:
|
321
|
+
logger.warning("No files found for pattern %s even after trying alternatives", input_filename)
|
250
322
|
|
251
323
|
logger.trace("Exiting get_input_files() with %d files", len(input_files))
|
252
324
|
return input_files
|
253
325
|
|
254
326
|
|
255
|
-
def append_to_filename(output_filename, tag):
|
327
|
+
def append_to_filename(output_filename: str, tag: str) -> str:
|
256
328
|
"""
|
257
329
|
Append a tag to a filename, preserving the extension.
|
258
330
|
|
259
331
|
Args:
|
260
|
-
output_filename: Original filename
|
261
|
-
tag: Tag to append (typically an 8-character hex value)
|
262
|
-
|
332
|
+
output_filename (str): Original filename
|
333
|
+
tag (str): Tag to append (typically an 8-character hex value)
|
263
334
|
Returns:
|
264
335
|
str: Modified filename with tag
|
265
336
|
"""
|
TonieToolbox/config.py
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
## TODO: Add config.py
|
TonieToolbox/constants.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
"""
|
2
2
|
Constants used throughout the TonieToolbox package
|
3
3
|
"""
|
4
|
-
SAMPLE_RATE_KHZ = 48
|
5
|
-
ONLY_CONVERT_FRAMEPACKING = -1
|
6
|
-
OTHER_PACKET_NEEDED = -2
|
7
|
-
DO_NOTHING = -3
|
8
|
-
TOO_MANY_SEGMENTS = -4
|
9
|
-
TIMESTAMP_DEDUCT = 0x50000000
|
10
|
-
OPUS_TAGS = [
|
4
|
+
SAMPLE_RATE_KHZ: int = 48
|
5
|
+
ONLY_CONVERT_FRAMEPACKING: int = -1
|
6
|
+
OTHER_PACKET_NEEDED: int = -2
|
7
|
+
DO_NOTHING: int = -3
|
8
|
+
TOO_MANY_SEGMENTS: int = -4
|
9
|
+
TIMESTAMP_DEDUCT: int = 0x50000000
|
10
|
+
OPUS_TAGS: list[bytearray] = [
|
11
11
|
bytearray(
|
12
12
|
b"\x4F\x70\x75\x73\x54\x61\x67\x73\x0D\x00\x00\x00\x4C\x61\x76\x66\x35\x38\x2E\x32\x30\x2E\x31\x30\x30\x03\x00\x00\x00\x26\x00\x00\x00\x65\x6E\x63\x6F\x64\x65\x72\x3D\x6F\x70\x75\x73\x65\x6E\x63\x20\x66\x72\x6F\x6D\x20\x6F\x70\x75\x73\x2D\x74\x6F\x6F\x6C\x73\x20\x30\x2E\x31\x2E\x31\x30\x2A\x00\x00\x00\x65\x6E\x63\x6F\x64\x65\x72\x5F\x6F\x70\x74\x69\x6F\x6E\x73\x3D\x2D\x2D\x71\x75\x69\x65\x74\x20\x2D\x2D\x62\x69\x74\x72\x61\x74\x65\x20\x39\x36\x20\x2D\x2D\x76\x62\x72\x3B\x01\x00\x00\x70\x61\x64\x3D\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30"),
|
13
13
|
bytearray(
|
@@ -15,7 +15,7 @@ OPUS_TAGS = [
|
|
15
15
|
]
|
16
16
|
|
17
17
|
# Mapping of language tags to ISO codes
|
18
|
-
LANGUAGE_MAPPING = {
|
18
|
+
LANGUAGE_MAPPING: dict[str, str] = {
|
19
19
|
# Common language names to ISO codes
|
20
20
|
'deutsch': 'de-de',
|
21
21
|
'german': 'de-de',
|
@@ -39,7 +39,7 @@ LANGUAGE_MAPPING = {
|
|
39
39
|
}
|
40
40
|
|
41
41
|
# Mapping of genre tags to tonie categories
|
42
|
-
GENRE_MAPPING = {
|
42
|
+
GENRE_MAPPING: dict[str, str] = {
|
43
43
|
# Standard Tonie category names from tonies.json
|
44
44
|
'hörspiel': 'Hörspiele & Hörbücher',
|
45
45
|
'hörbuch': 'Hörspiele & Hörbücher',
|
@@ -87,4 +87,88 @@ GENRE_MAPPING = {
|
|
87
87
|
|
88
88
|
# Default to standard format for custom
|
89
89
|
'custom': 'Hörspiele & Hörbücher',
|
90
|
+
}
|
91
|
+
|
92
|
+
# Supported file extensions for audio files
|
93
|
+
SUPPORTED_EXTENSIONS = [
|
94
|
+
'.wav', '.mp3', '.aac', '.m4a', '.flac', '.ogg', '.opus',
|
95
|
+
'.ape', '.wma', '.aiff', '.mp2', '.mp4', '.webm', '.mka'
|
96
|
+
]
|
97
|
+
|
98
|
+
ARTWORK_NAMES = [
|
99
|
+
'cover', 'folder', 'album', 'front', 'artwork', 'image',
|
100
|
+
'albumart', 'albumartwork', 'booklet'
|
101
|
+
]
|
102
|
+
ARTWORK_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.bmp', '.gif']
|
103
|
+
|
104
|
+
|
105
|
+
TAG_VALUE_REPLACEMENTS = {
|
106
|
+
"Die drei ???": "Die drei Fragezeichen",
|
107
|
+
"Die Drei ???": "Die drei Fragezeichen",
|
108
|
+
"DIE DREI ???": "Die drei Fragezeichen",
|
109
|
+
"Die drei !!!": "Die drei Ausrufezeichen",
|
110
|
+
"Die Drei !!!": "Die drei Ausrufezeichen",
|
111
|
+
"DIE DREI !!!": "Die drei Ausrufezeichen",
|
112
|
+
"TKKG™": "TKKG",
|
113
|
+
"Die drei ??? Kids": "Die drei Fragezeichen Kids",
|
114
|
+
"Die Drei ??? Kids": "Die drei Fragezeichen Kids",
|
115
|
+
"Bibi & Tina": "Bibi und Tina",
|
116
|
+
"Benjamin Blümchen™": "Benjamin Blümchen",
|
117
|
+
"???": "Fragezeichen",
|
118
|
+
"!!!": "Ausrufezeichen",
|
119
|
+
}
|
120
|
+
|
121
|
+
TAG_MAPPINGS = {
|
122
|
+
# ID3 (MP3) tags
|
123
|
+
'TIT2': 'title',
|
124
|
+
'TALB': 'album',
|
125
|
+
'TPE1': 'artist',
|
126
|
+
'TPE2': 'albumartist',
|
127
|
+
'TCOM': 'composer',
|
128
|
+
'TRCK': 'tracknumber',
|
129
|
+
'TPOS': 'discnumber',
|
130
|
+
'TDRC': 'date',
|
131
|
+
'TCON': 'genre',
|
132
|
+
'TPUB': 'publisher',
|
133
|
+
'TCOP': 'copyright',
|
134
|
+
'COMM': 'comment',
|
135
|
+
|
136
|
+
# Vorbis tags (FLAC, OGG)
|
137
|
+
'title': 'title',
|
138
|
+
'album': 'album',
|
139
|
+
'artist': 'artist',
|
140
|
+
'albumartist': 'albumartist',
|
141
|
+
'composer': 'composer',
|
142
|
+
'tracknumber': 'tracknumber',
|
143
|
+
'discnumber': 'discnumber',
|
144
|
+
'date': 'date',
|
145
|
+
'genre': 'genre',
|
146
|
+
'publisher': 'publisher',
|
147
|
+
'copyright': 'copyright',
|
148
|
+
'comment': 'comment',
|
149
|
+
|
150
|
+
# MP4 (M4A, AAC) tags
|
151
|
+
'©nam': 'title',
|
152
|
+
'©alb': 'album',
|
153
|
+
'©ART': 'artist',
|
154
|
+
'aART': 'albumartist',
|
155
|
+
'©wrt': 'composer',
|
156
|
+
'trkn': 'tracknumber',
|
157
|
+
'disk': 'discnumber',
|
158
|
+
'©day': 'date',
|
159
|
+
'©gen': 'genre',
|
160
|
+
'©pub': 'publisher',
|
161
|
+
'cprt': 'copyright',
|
162
|
+
'©cmt': 'comment',
|
163
|
+
|
164
|
+
# Additional tags some files might have
|
165
|
+
'album_artist': 'albumartist',
|
166
|
+
'track': 'tracknumber',
|
167
|
+
'track_number': 'tracknumber',
|
168
|
+
'disc': 'discnumber',
|
169
|
+
'disc_number': 'discnumber',
|
170
|
+
'year': 'date',
|
171
|
+
'albuminterpret': 'albumartist', # German tag name
|
172
|
+
'interpret': 'artist', # German tag name
|
173
|
+
|
90
174
|
}
|
@@ -15,10 +15,9 @@ def sanitize_filename(filename: str) -> str:
|
|
15
15
|
Sanitize a filename by removing invalid characters and trimming.
|
16
16
|
|
17
17
|
Args:
|
18
|
-
filename: The filename to sanitize
|
19
|
-
|
18
|
+
filename (str): The filename to sanitize
|
20
19
|
Returns:
|
21
|
-
A sanitized filename
|
20
|
+
str: A sanitized filename
|
22
21
|
"""
|
23
22
|
# Remove invalid characters for filenames
|
24
23
|
sanitized = re.sub(r'[<>:"/\\|?*]', '_', filename)
|
@@ -29,7 +28,7 @@ def sanitize_filename(filename: str) -> str:
|
|
29
28
|
return "tonie"
|
30
29
|
return sanitized
|
31
30
|
|
32
|
-
def guess_output_filename(input_filename: str, input_files:
|
31
|
+
def guess_output_filename(input_filename: str, input_files: list[str] = None) -> str:
|
33
32
|
"""
|
34
33
|
Generate a sensible output filename based on input file or directory.
|
35
34
|
|
@@ -40,11 +39,10 @@ def guess_output_filename(input_filename: str, input_files: List[str] = None) ->
|
|
40
39
|
4. For multiple files: Use the common parent directory name
|
41
40
|
|
42
41
|
Args:
|
43
|
-
input_filename: The input filename or pattern
|
44
|
-
input_files: List of resolved input files (optional)
|
45
|
-
|
42
|
+
input_filename (str): The input filename or pattern
|
43
|
+
input_files (list[str] | None): List of resolved input files (optional)
|
46
44
|
Returns:
|
47
|
-
Generated output filename without extension
|
45
|
+
str: Generated output filename without extension
|
48
46
|
"""
|
49
47
|
logger.debug("Guessing output filename from input: %s", input_filename)
|
50
48
|
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import platform
|
2
|
+
|
3
|
+
def handle_integration(args):
|
4
|
+
import platform
|
5
|
+
if platform.system() == 'Windows':
|
6
|
+
from .integration_windows import WindowsClassicContextMenuIntegration as ContextMenuIntegration
|
7
|
+
if args.install_integration:
|
8
|
+
ContextMenuIntegration.install()
|
9
|
+
elif args.uninstall_integration:
|
10
|
+
ContextMenuIntegration.uninstall()
|
11
|
+
elif platform.system() == 'Darwin':
|
12
|
+
from .integration_macos import MacOSContextMenuIntegration as ContextMenuIntegration
|
13
|
+
if args.install_integration:
|
14
|
+
ContextMenuIntegration.install()
|
15
|
+
elif args.uninstall_integration:
|
16
|
+
ContextMenuIntegration.uninstall()
|
17
|
+
elif platform.system() == 'Linux':
|
18
|
+
raise NotImplementedError("Context menu integration is not supported on Linux YET. But Soon™")
|
19
|
+
else:
|
20
|
+
raise NotImplementedError(f"Context menu integration is not supported on this OS: {platform.system()}")
|