TonieToolbox 0.5.1__py3-none-any.whl → 0.6.0a2__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 +91 -84
- TonieToolbox/artwork.py +12 -7
- TonieToolbox/audio_conversion.py +31 -28
- TonieToolbox/constants.py +110 -9
- TonieToolbox/filename_generator.py +6 -8
- TonieToolbox/integration.py +68 -0
- TonieToolbox/integration_macos.py +477 -0
- TonieToolbox/integration_ubuntu.py +1 -0
- TonieToolbox/integration_windows.py +437 -0
- TonieToolbox/logger.py +8 -10
- TonieToolbox/media_tags.py +17 -97
- TonieToolbox/ogg_page.py +39 -39
- TonieToolbox/opus_packet.py +13 -13
- TonieToolbox/recursive_processor.py +22 -22
- TonieToolbox/tags.py +3 -4
- TonieToolbox/teddycloud.py +50 -50
- TonieToolbox/tonie_analysis.py +24 -23
- TonieToolbox/tonie_file.py +71 -44
- TonieToolbox/tonies_json.py +69 -66
- TonieToolbox/version_handler.py +12 -15
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0a2.dist-info}/METADATA +3 -3
- tonietoolbox-0.6.0a2.dist-info/RECORD +30 -0
- tonietoolbox-0.5.1.dist-info/RECORD +0 -26
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0a2.dist-info}/WHEEL +0 -0
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0a2.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0a2.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0a2.dist-info}/top_level.txt +0 -0
TonieToolbox/__init__.py
CHANGED
TonieToolbox/__main__.py
CHANGED
@@ -21,6 +21,7 @@ from .teddycloud import TeddyCloudClient
|
|
21
21
|
from .tags import get_tags
|
22
22
|
from .tonies_json import fetch_and_update_tonies_json_v1, fetch_and_update_tonies_json_v2
|
23
23
|
from .artwork import upload_artwork
|
24
|
+
from .integration import handle_integration, handle_config
|
24
25
|
|
25
26
|
def main():
|
26
27
|
"""Entry point for the TonieToolbox application."""
|
@@ -98,7 +99,14 @@ def main():
|
|
98
99
|
parser.add_argument('-C', '--compare', action='store', metavar='FILE2',
|
99
100
|
help='Compare input file with another .taf file for debugging')
|
100
101
|
parser.add_argument('-D', '--detailed-compare', action='store_true',
|
101
|
-
help='Show detailed OGG page differences when comparing files')
|
102
|
+
help='Show detailed OGG page differences when comparing files')
|
103
|
+
# ------------- Parser - Context Menu Integration -------------
|
104
|
+
parser.add_argument('--config-integration', action='store_true',
|
105
|
+
help='Configure context menu integration')
|
106
|
+
parser.add_argument('--install-integration', action='store_true',
|
107
|
+
help='Integrate with the system (e.g., create context menu entries)')
|
108
|
+
parser.add_argument('--uninstall-integration', action='store_true',
|
109
|
+
help='Uninstall context menu integration')
|
102
110
|
# ------------- Parser - Media Tag Options -------------
|
103
111
|
media_tag_group = parser.add_argument_group('Media Tag Options')
|
104
112
|
media_tag_group.add_argument('-m', '--use-media-tags', action='store_true',
|
@@ -127,7 +135,7 @@ def main():
|
|
127
135
|
args = parser.parse_args()
|
128
136
|
|
129
137
|
# ------------- Parser - Source Input -------------
|
130
|
-
if args.input_filename is None and not (args.get_tags or args.upload):
|
138
|
+
if args.input_filename is None and not (args.get_tags or args.upload or args.install_integration or args.uninstall_integration or args.config_integration):
|
131
139
|
parser.error("the following arguments are required: SOURCE")
|
132
140
|
|
133
141
|
# ------------- Logging -------------
|
@@ -166,7 +174,23 @@ def main():
|
|
166
174
|
|
167
175
|
if not is_latest and not update_confirmed and not (args.silent or args.quiet):
|
168
176
|
logger.info("Update available but user chose to continue without updating.")
|
169
|
-
|
177
|
+
# ------------- Context Menu Integration -------------
|
178
|
+
if args.install_integration or args.uninstall_integration:
|
179
|
+
logger.debug("Context menu integration requested: install=%s, uninstall=%s",
|
180
|
+
args.install_integration, args.uninstall_integration)
|
181
|
+
success = handle_integration(args)
|
182
|
+
if success:
|
183
|
+
if args.install_integration:
|
184
|
+
logger.info("Context menu integration installed successfully")
|
185
|
+
else:
|
186
|
+
logger.info("Context menu integration uninstalled successfully")
|
187
|
+
else:
|
188
|
+
logger.error("Failed to handle context menu integration")
|
189
|
+
sys.exit(0)
|
190
|
+
if args.config_integration:
|
191
|
+
logger.debug("Opening configuration file for editing")
|
192
|
+
handle_config()
|
193
|
+
sys.exit(0)
|
170
194
|
# ------------- Normalize Path Input -------------
|
171
195
|
if args.input_filename:
|
172
196
|
logger.debug("Original input path: %s", args.input_filename)
|
@@ -224,89 +248,72 @@ def main():
|
|
224
248
|
print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
|
225
249
|
sys.exit(0)
|
226
250
|
# ------------- Direct Upload -------------
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
if args.upload and not args.recursive and file_ext == '.taf':
|
233
|
-
logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
|
234
|
-
logger.trace("TeddyCloud upload parameters: path=%s, special_folder=%s, ignore_ssl=%s",
|
235
|
-
args.path, args.special_folder, args.ignore_ssl_verify)
|
236
|
-
|
251
|
+
if os.path.exists(args.input_filename) and os.path.isfile(args.input_filename):
|
252
|
+
file_path = args.input_filename
|
253
|
+
file_size = os.path.getsize(file_path)
|
254
|
+
file_ext = os.path.splitext(file_path)[1].lower()
|
237
255
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
{k: v for k, v in response.items() if k != 'success'})
|
259
|
-
artwork_url = None
|
260
|
-
if args.include_artwork and file_path.lower().endswith('.taf'):
|
261
|
-
source_dir = os.path.dirname(file_path)
|
262
|
-
logger.info("Looking for artwork to upload for %s", file_path)
|
263
|
-
logger.debug("Searching for artwork in directory: %s", source_dir)
|
264
|
-
logger.trace("Calling upload_artwork function")
|
265
|
-
success, artwork_url = upload_artwork(client, file_path, source_dir, [])
|
266
|
-
logger.trace("upload_artwork returned: success=%s, artwork_url=%s",
|
267
|
-
success, artwork_url)
|
268
|
-
if success:
|
269
|
-
logger.info("Successfully uploaded artwork for %s", file_path)
|
270
|
-
logger.debug("Artwork URL: %s", artwork_url)
|
271
|
-
else:
|
272
|
-
logger.warning("Failed to upload artwork for %s", file_path)
|
273
|
-
logger.debug("No suitable artwork found or upload failed")
|
274
|
-
if args.create_custom_json and file_path.lower().endswith('.taf'):
|
275
|
-
output_dir = './output'
|
276
|
-
logger.debug("Creating/ensuring output directory for JSON: %s", output_dir)
|
277
|
-
if not os.path.exists(output_dir):
|
278
|
-
os.makedirs(output_dir, exist_ok=True)
|
279
|
-
logger.trace("Created output directory: %s", output_dir)
|
280
|
-
logger.debug("Updating tonies.custom.json with: taf=%s, artwork_url=%s",
|
281
|
-
file_path, artwork_url)
|
282
|
-
client_param = client
|
283
|
-
|
284
|
-
if args.version_2:
|
285
|
-
logger.debug("Using version 2 of the Tonies JSON format")
|
286
|
-
success = fetch_and_update_tonies_json_v2(client_param, file_path, [], artwork_url, output_dir)
|
287
|
-
else:
|
288
|
-
success = fetch_and_update_tonies_json_v1(client_param, file_path, [], artwork_url, output_dir)
|
289
|
-
if success:
|
290
|
-
logger.info("Successfully updated Tonies JSON for %s", file_path)
|
291
|
-
else:
|
292
|
-
logger.warning("Failed to update Tonies JSON for %s", file_path)
|
293
|
-
logger.debug("fetch_and_update_tonies_json returned failure")
|
294
|
-
logger.trace("Exiting after direct upload with code 0")
|
295
|
-
sys.exit(0)
|
296
|
-
elif not args.recursive:
|
297
|
-
if not os.path.exists(args.input_filename):
|
298
|
-
logger.error("File not found: %s", args.input_filename)
|
299
|
-
elif not os.path.isfile(args.input_filename):
|
300
|
-
logger.error("Not a regular file: %s", args.input_filename)
|
301
|
-
logger.debug("File exists: %s, Is file: %s",
|
302
|
-
os.path.exists(args.input_filename),
|
303
|
-
os.path.isfile(args.input_filename) if os.path.exists(args.input_filename) else False)
|
304
|
-
logger.trace("Exiting with code 1 due to invalid input file")
|
256
|
+
if args.upload and not args.recursive and file_ext == '.taf':
|
257
|
+
logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
|
258
|
+
logger.trace("TeddyCloud upload parameters: path=%s, special_folder=%s, ignore_ssl=%s",
|
259
|
+
args.path, args.special_folder, args.ignore_ssl_verify)
|
260
|
+
logger.debug("File to upload: %s (size: %d bytes, type: %s)",
|
261
|
+
file_path, file_size, file_ext)
|
262
|
+
logger.info("Uploading %s to TeddyCloud %s", file_path, teddycloud_url)
|
263
|
+
logger.trace("Starting upload process for %s", file_path)
|
264
|
+
response = client.upload_file(
|
265
|
+
destination_path=args.path,
|
266
|
+
file_path=file_path,
|
267
|
+
special=args.special_folder,
|
268
|
+
)
|
269
|
+
logger.trace("Upload response received: %s", response)
|
270
|
+
upload_success = response.get('success', False)
|
271
|
+
if not upload_success:
|
272
|
+
error_msg = response.get('message', 'Unknown error')
|
273
|
+
logger.error("Failed to upload %s to TeddyCloud: %s (HTTP Status: %s, Response: %s)",
|
274
|
+
file_path, error_msg, response.get('status_code', 'Unknown'), response)
|
275
|
+
logger.trace("Exiting with code 1 due to upload failure")
|
305
276
|
sys.exit(1)
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
277
|
+
else:
|
278
|
+
logger.info("Successfully uploaded %s to TeddyCloud", file_path)
|
279
|
+
logger.debug("Upload response details: %s",
|
280
|
+
{k: v for k, v in response.items() if k != 'success'})
|
281
|
+
artwork_url = None
|
282
|
+
if args.include_artwork and file_path.lower().endswith('.taf'):
|
283
|
+
source_dir = os.path.dirname(file_path)
|
284
|
+
logger.info("Looking for artwork to upload for %s", file_path)
|
285
|
+
logger.debug("Searching for artwork in directory: %s", source_dir)
|
286
|
+
logger.trace("Calling upload_artwork function")
|
287
|
+
success, artwork_url = upload_artwork(client, file_path, source_dir, [])
|
288
|
+
logger.trace("upload_artwork returned: success=%s, artwork_url=%s",
|
289
|
+
success, artwork_url)
|
290
|
+
if success:
|
291
|
+
logger.info("Successfully uploaded artwork for %s", file_path)
|
292
|
+
logger.debug("Artwork URL: %s", artwork_url)
|
293
|
+
else:
|
294
|
+
logger.warning("Failed to upload artwork for %s", file_path)
|
295
|
+
logger.debug("No suitable artwork found or upload failed")
|
296
|
+
if args.create_custom_json and file_path.lower().endswith('.taf'):
|
297
|
+
output_dir = './output'
|
298
|
+
logger.debug("Creating/ensuring output directory for JSON: %s", output_dir)
|
299
|
+
if not os.path.exists(output_dir):
|
300
|
+
os.makedirs(output_dir, exist_ok=True)
|
301
|
+
logger.trace("Created output directory: %s", output_dir)
|
302
|
+
logger.debug("Updating tonies.custom.json with: taf=%s, artwork_url=%s",
|
303
|
+
file_path, artwork_url)
|
304
|
+
client_param = client
|
305
|
+
if args.version_2:
|
306
|
+
logger.debug("Using version 2 of the Tonies JSON format")
|
307
|
+
success = fetch_and_update_tonies_json_v2(client_param, file_path, [], artwork_url, output_dir)
|
308
|
+
else:
|
309
|
+
success = fetch_and_update_tonies_json_v1(client_param, file_path, [], artwork_url, output_dir)
|
310
|
+
if success:
|
311
|
+
logger.info("Successfully updated Tonies JSON for %s", file_path)
|
312
|
+
else:
|
313
|
+
logger.warning("Failed to update Tonies JSON for %s", file_path)
|
314
|
+
logger.debug("fetch_and_update_tonies_json returned failure")
|
315
|
+
logger.trace("Exiting after direct upload with code 0")
|
316
|
+
sys.exit(0)
|
310
317
|
|
311
318
|
# ------------- Librarys / Prereqs -------------
|
312
319
|
logger.debug("Checking for external dependencies")
|
TonieToolbox/artwork.py
CHANGED
@@ -13,17 +13,22 @@ from .teddycloud import TeddyCloudClient
|
|
13
13
|
from .media_tags import extract_artwork, find_cover_image
|
14
14
|
|
15
15
|
|
16
|
-
def upload_artwork(
|
16
|
+
def upload_artwork(
|
17
|
+
client: TeddyCloudClient,
|
18
|
+
taf_filename: str,
|
19
|
+
source_path: str,
|
20
|
+
audio_files: list[str],
|
21
|
+
) -> tuple[bool, Optional[str]]:
|
17
22
|
"""
|
18
23
|
Find and upload artwork for a Tonie file.
|
19
|
-
|
24
|
+
|
20
25
|
Args:
|
21
|
-
client: TeddyCloudClient instance to use for API communication
|
22
|
-
taf_filename: The filename of the Tonie file (.taf)
|
23
|
-
source_path: Source directory to look for artwork
|
24
|
-
audio_files: List of audio files to extract artwork from if needed
|
26
|
+
client (TeddyCloudClient): TeddyCloudClient instance to use for API communication
|
27
|
+
taf_filename (str): The filename of the Tonie file (.taf)
|
28
|
+
source_path (str): Source directory to look for artwork
|
29
|
+
audio_files (list[str]): List of audio files to extract artwork from if needed
|
25
30
|
Returns:
|
26
|
-
tuple: (success, artwork_url) where success is a boolean and artwork_url is the URL of the uploaded artwork
|
31
|
+
tuple[bool, Optional[str]]: (success, artwork_url) where success is a boolean and artwork_url is the URL of the uploaded artwork
|
27
32
|
"""
|
28
33
|
logger = get_logger('artwork')
|
29
34
|
logger.info("Looking for artwork for Tonie file: %s", taf_filename)
|
TonieToolbox/audio_conversion.py
CHANGED
@@ -7,27 +7,36 @@ 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
|
-
no_mono_conversion: Whether to skip mono to stereo conversion
|
28
|
-
|
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
|
29
38
|
Returns:
|
30
|
-
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
|
31
40
|
"""
|
32
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)",
|
33
42
|
ffmpeg_binary, opus_binary, filename, bitrate, vbr, keep_temp, auto_download, no_mono_conversion)
|
@@ -198,24 +207,20 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
198
207
|
return tmp_file, None
|
199
208
|
|
200
209
|
|
201
|
-
def filter_directories(glob_list):
|
210
|
+
def filter_directories(glob_list: list[str]) -> list[str]:
|
202
211
|
"""
|
203
212
|
Filter a list of glob results to include only audio files that can be handled by ffmpeg.
|
204
213
|
|
205
214
|
Args:
|
206
|
-
glob_list: List of path names from glob.glob()
|
207
|
-
|
215
|
+
glob_list (list[str]): List of path names from glob.glob()
|
208
216
|
Returns:
|
209
|
-
list: Filtered list containing only supported audio files
|
217
|
+
list[str]: Filtered list containing only supported audio files
|
210
218
|
"""
|
211
219
|
logger.trace("Entering filter_directories() with %d items", len(glob_list))
|
212
220
|
logger.debug("Filtering %d glob results for supported audio files", len(glob_list))
|
213
221
|
|
214
|
-
|
215
|
-
|
216
|
-
'.wav', '.mp3', '.aac', '.m4a', '.flac', '.ogg', '.opus',
|
217
|
-
'.ape', '.wma', '.aiff', '.mp2', '.mp4', '.webm', '.mka'
|
218
|
-
]
|
222
|
+
supported_extensions = SUPPORTED_EXTENSIONS
|
223
|
+
logger.debug("Supported audio file extensions: %s", supported_extensions)
|
219
224
|
|
220
225
|
filtered = []
|
221
226
|
for name in glob_list:
|
@@ -232,17 +237,16 @@ def filter_directories(glob_list):
|
|
232
237
|
return filtered
|
233
238
|
|
234
239
|
|
235
|
-
def get_input_files(input_filename):
|
240
|
+
def get_input_files(input_filename: str) -> list[str]:
|
236
241
|
"""
|
237
242
|
Get a list of input files to process.
|
238
243
|
|
239
244
|
Supports direct file paths, directory paths, glob patterns, and .lst files.
|
240
245
|
|
241
246
|
Args:
|
242
|
-
input_filename: Input file pattern or list file path
|
243
|
-
|
247
|
+
input_filename (str): Input file pattern or list file path
|
244
248
|
Returns:
|
245
|
-
list: List of input file paths
|
249
|
+
list[str]: List of input file paths
|
246
250
|
"""
|
247
251
|
logger.trace("Entering get_input_files(input_filename=%s)", input_filename)
|
248
252
|
logger.debug("Getting input files for pattern: %s", input_filename)
|
@@ -320,14 +324,13 @@ def get_input_files(input_filename):
|
|
320
324
|
return input_files
|
321
325
|
|
322
326
|
|
323
|
-
def append_to_filename(output_filename, tag):
|
327
|
+
def append_to_filename(output_filename: str, tag: str) -> str:
|
324
328
|
"""
|
325
329
|
Append a tag to a filename, preserving the extension.
|
326
330
|
|
327
331
|
Args:
|
328
|
-
output_filename: Original filename
|
329
|
-
tag: Tag to append (typically an 8-character hex value)
|
330
|
-
|
332
|
+
output_filename (str): Original filename
|
333
|
+
tag (str): Tag to append (typically an 8-character hex value)
|
331
334
|
Returns:
|
332
335
|
str: Modified filename with tag
|
333
336
|
"""
|
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,105 @@ 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
|
+
|
174
|
+
}
|
175
|
+
|
176
|
+
CONFIG_TEMPLATE = {
|
177
|
+
"metadata": {
|
178
|
+
"description": "TonieToolbox configuration",
|
179
|
+
"config_version": "1.0"
|
180
|
+
},
|
181
|
+
"log_level": "silent", # Options: trace, debug, info, warning, error, critical, silent
|
182
|
+
"log_to_file": False, # True if you want to log to a file ~\.tonietoolbox\logs
|
183
|
+
"upload": {
|
184
|
+
"url": [""], # https://teddycloud.example.com
|
185
|
+
"ignore_ssl_verify": False, # True if you want to ignore SSL certificate verification
|
186
|
+
"username": "", # Basic Auth username
|
187
|
+
"password": "", # Basic Auth password
|
188
|
+
"client_cert_path": "", # Path to client certificate file
|
189
|
+
"client_cert_key_path": "" # Path to client certificate key file
|
190
|
+
}
|
90
191
|
}
|
@@ -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,68 @@
|
|
1
|
+
import platform
|
2
|
+
from .logger import get_logger
|
3
|
+
|
4
|
+
logger = get_logger(__name__)
|
5
|
+
|
6
|
+
def handle_integration(args):
|
7
|
+
import platform
|
8
|
+
if platform.system() == 'Windows':
|
9
|
+
from .integration_windows import WindowsClassicContextMenuIntegration as ContextMenuIntegration
|
10
|
+
if args.install_integration:
|
11
|
+
success = ContextMenuIntegration.install()
|
12
|
+
if success:
|
13
|
+
logger.info("Integration installed successfully.")
|
14
|
+
return True
|
15
|
+
else:
|
16
|
+
logger.error("Integration installation failed.")
|
17
|
+
return False
|
18
|
+
elif args.uninstall_integration:
|
19
|
+
success = ContextMenuIntegration.uninstall()
|
20
|
+
if success:
|
21
|
+
logger.info("Integration uninstalled successfully.")
|
22
|
+
return True
|
23
|
+
else:
|
24
|
+
logger.error("Integration uninstallation failed.")
|
25
|
+
return False
|
26
|
+
elif platform.system() == 'Darwin':
|
27
|
+
from .integration_macos import MacOSContextMenuIntegration as ContextMenuIntegration
|
28
|
+
if args.install_integration:
|
29
|
+
success = ContextMenuIntegration.install()
|
30
|
+
if success:
|
31
|
+
logger.info("Integration installed successfully.")
|
32
|
+
return True
|
33
|
+
else:
|
34
|
+
logger.error("Integration installation failed.")
|
35
|
+
return False
|
36
|
+
elif args.uninstall_integration:
|
37
|
+
success = ContextMenuIntegration.uninstall()
|
38
|
+
if success:
|
39
|
+
logger.info("Integration uninstalled successfully.")
|
40
|
+
return True
|
41
|
+
else:
|
42
|
+
logger.error("Integration uninstallation failed.")
|
43
|
+
return False
|
44
|
+
elif platform.system() == 'Linux':
|
45
|
+
raise NotImplementedError("Context menu integration is not supported on Linux YET. But Soon™")
|
46
|
+
else:
|
47
|
+
raise NotImplementedError(f"Context menu integration is not supported on this OS: {platform.system()}")
|
48
|
+
|
49
|
+
def handle_config():
|
50
|
+
"""Opens the configuration file in the default text editor."""
|
51
|
+
import os
|
52
|
+
import platform
|
53
|
+
import subprocess
|
54
|
+
|
55
|
+
config_path = os.path.join(os.path.expanduser("~"), ".tonietoolbox", "config.json")
|
56
|
+
|
57
|
+
if not os.path.exists(config_path):
|
58
|
+
logger.info(f"Configuration file not found at {config_path}.")
|
59
|
+
logger.info("Creating a new configuration file. Using --install-integration will create a new config file.")
|
60
|
+
return
|
61
|
+
if platform.system() == "Windows":
|
62
|
+
os.startfile(config_path)
|
63
|
+
elif platform.system() == "Darwin":
|
64
|
+
subprocess.call(["open", config_path])
|
65
|
+
elif platform.system() == "Linux":
|
66
|
+
subprocess.call(["xdg-open", config_path])
|
67
|
+
else:
|
68
|
+
logger.error(f"Unsupported OS: {platform.system()}")
|