TonieToolbox 0.5.0a1__py3-none-any.whl → 0.5.1__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 +88 -68
- TonieToolbox/audio_conversion.py +75 -7
- TonieToolbox/media_tags.py +5 -4
- TonieToolbox/recursive_processor.py +10 -11
- TonieToolbox/tonie_file.py +17 -29
- TonieToolbox/tonies_json.py +799 -10
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.5.1.dist-info}/METADATA +141 -98
- tonietoolbox-0.5.1.dist-info/RECORD +26 -0
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.5.1.dist-info}/WHEEL +1 -1
- tonietoolbox-0.5.0a1.dist-info/RECORD +0 -26
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.5.1.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.5.1.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.5.1.dist-info}/top_level.txt +0 -0
TonieToolbox/__init__.py
CHANGED
TonieToolbox/__main__.py
CHANGED
@@ -10,19 +10,18 @@ import logging
|
|
10
10
|
from . import __version__
|
11
11
|
from .audio_conversion import get_input_files, append_to_filename
|
12
12
|
from .tonie_file import create_tonie_file
|
13
|
-
from .tonie_analysis import check_tonie_file, check_tonie_file_cli, split_to_opus_files
|
13
|
+
from .tonie_analysis import check_tonie_file, check_tonie_file_cli, split_to_opus_files, compare_taf_files
|
14
14
|
from .dependency_manager import get_ffmpeg_binary, get_opus_binary
|
15
|
-
from .logger import setup_logging, get_logger
|
15
|
+
from .logger import TRACE, setup_logging, get_logger
|
16
16
|
from .filename_generator import guess_output_filename
|
17
17
|
from .version_handler import check_for_updates, clear_version_cache
|
18
18
|
from .recursive_processor import process_recursive_folders
|
19
|
-
from .media_tags import is_available as is_media_tags_available, ensure_mutagen, extract_album_info, format_metadata_filename
|
19
|
+
from .media_tags import is_available as is_media_tags_available, ensure_mutagen, extract_album_info, format_metadata_filename, get_file_tags
|
20
20
|
from .teddycloud import TeddyCloudClient
|
21
21
|
from .tags import get_tags
|
22
|
-
from .tonies_json import
|
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
24
|
|
25
|
-
|
26
25
|
def main():
|
27
26
|
"""Entry point for the TonieToolbox application."""
|
28
27
|
parser = argparse.ArgumentParser(description='Create Tonie compatible file from Ogg opus file(s).')
|
@@ -52,6 +51,8 @@ def main():
|
|
52
51
|
help='Delay between retry attempts in seconds (default: 5)')
|
53
52
|
teddycloud_group.add_argument('--create-custom-json', action='store_true',
|
54
53
|
help='Fetch and update custom Tonies JSON data')
|
54
|
+
teddycloud_group.add_argument('--version-2', action='store_true',
|
55
|
+
help='Use version 2 of the Tonies JSON format (default: version 1)')
|
55
56
|
# ------------- Parser - Authentication options for TeddyCloud -------------
|
56
57
|
teddycloud_group.add_argument('--username', action='store', metavar='USERNAME',
|
57
58
|
help='Username for basic authentication')
|
@@ -87,6 +88,8 @@ def main():
|
|
87
88
|
help='Save output files in the source directory instead of output directory')
|
88
89
|
parser.add_argument('-fc', '--force-creation', action='store_true', default=False,
|
89
90
|
help='Force creation of Tonie file even if it already exists')
|
91
|
+
parser.add_argument('--no-mono-conversion', action='store_true',
|
92
|
+
help='Do not convert mono audio to stereo (default: convert mono to stereo)')
|
90
93
|
# ------------- Parser - Debug TAFs -------------
|
91
94
|
parser.add_argument('-k', '--keep-temp', action='store_true',
|
92
95
|
help='Keep temporary opus files in a temp folder for testing')
|
@@ -129,7 +132,6 @@ def main():
|
|
129
132
|
|
130
133
|
# ------------- Logging -------------
|
131
134
|
if args.trace:
|
132
|
-
from .logger import TRACE
|
133
135
|
log_level = TRACE
|
134
136
|
elif args.debug:
|
135
137
|
log_level = logging.DEBUG
|
@@ -203,27 +205,39 @@ def main():
|
|
203
205
|
success = get_tags(client)
|
204
206
|
logger.debug( "Exiting with code %d", 0 if success else 1)
|
205
207
|
sys.exit(0 if success else 1)
|
206
|
-
|
207
|
-
# -------------
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
208
|
+
|
209
|
+
# ------------- Show Media Tags -------------
|
210
|
+
if args.show_tags:
|
211
|
+
files = get_input_files(args.input_filename)
|
212
|
+
logger.debug("Found %d files to process", len(files))
|
213
|
+
if len(files) == 0:
|
214
|
+
logger.error("No files found for pattern %s", args.input_filename)
|
215
|
+
sys.exit(1)
|
216
|
+
for file_index, file_path in enumerate(files):
|
217
|
+
tags = get_file_tags(file_path)
|
218
|
+
if tags:
|
219
|
+
print(f"\nFile {file_index + 1}: {os.path.basename(file_path)}")
|
220
|
+
print("-" * 40)
|
221
|
+
for tag_name, tag_value in sorted(tags.items()):
|
222
|
+
print(f"{tag_name}: {tag_value}")
|
223
|
+
else:
|
224
|
+
print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
|
225
|
+
sys.exit(0)
|
226
|
+
# ------------- Direct Upload -------------
|
227
|
+
if os.path.exists(args.input_filename) and os.path.isfile(args.input_filename):
|
228
|
+
file_path = args.input_filename
|
229
|
+
file_size = os.path.getsize(file_path)
|
230
|
+
file_ext = os.path.splitext(file_path)[1].lower()
|
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)
|
212
236
|
|
213
|
-
if not args.input_filename:
|
214
|
-
logger.error("Missing input file for --upload. Provide a file path as SOURCE argument.")
|
215
|
-
logger.trace("Exiting with code 1 due to missing input file")
|
216
|
-
sys.exit(1)
|
217
237
|
|
218
|
-
if os.path.exists(args.input_filename) and os.path.isfile(args.input_filename):
|
219
|
-
file_path = args.input_filename
|
220
|
-
file_size = os.path.getsize(file_path)
|
221
|
-
file_ext = os.path.splitext(file_path)[1].lower()
|
222
|
-
|
223
238
|
logger.debug("File to upload: %s (size: %d bytes, type: %s)",
|
224
239
|
file_path, file_size, file_ext)
|
225
240
|
logger.info("Uploading %s to TeddyCloud %s", file_path, teddycloud_url)
|
226
|
-
|
227
241
|
logger.trace("Starting upload process for %s", file_path)
|
228
242
|
response = client.upload_file(
|
229
243
|
destination_path=args.path,
|
@@ -231,56 +245,59 @@ def main():
|
|
231
245
|
special=args.special_folder,
|
232
246
|
)
|
233
247
|
logger.trace("Upload response received: %s", response)
|
234
|
-
|
235
248
|
upload_success = response.get('success', False)
|
236
249
|
if not upload_success:
|
237
250
|
error_msg = response.get('message', 'Unknown error')
|
238
|
-
logger.error("Failed to upload %s to TeddyCloud: %s
|
251
|
+
logger.error("Failed to upload %s to TeddyCloud: %s (HTTP Status: %s, Response: %s)",
|
252
|
+
file_path, error_msg, response.get('status_code', 'Unknown'), response)
|
239
253
|
logger.trace("Exiting with code 1 due to upload failure")
|
240
254
|
sys.exit(1)
|
241
255
|
else:
|
242
256
|
logger.info("Successfully uploaded %s to TeddyCloud", file_path)
|
243
257
|
logger.debug("Upload response details: %s",
|
244
258
|
{k: v for k, v in response.items() if k != 'success'})
|
245
|
-
|
246
259
|
artwork_url = None
|
247
260
|
if args.include_artwork and file_path.lower().endswith('.taf'):
|
248
261
|
source_dir = os.path.dirname(file_path)
|
249
262
|
logger.info("Looking for artwork to upload for %s", file_path)
|
250
263
|
logger.debug("Searching for artwork in directory: %s", source_dir)
|
251
|
-
|
252
264
|
logger.trace("Calling upload_artwork function")
|
253
265
|
success, artwork_url = upload_artwork(client, file_path, source_dir, [])
|
254
266
|
logger.trace("upload_artwork returned: success=%s, artwork_url=%s",
|
255
267
|
success, artwork_url)
|
256
|
-
|
257
268
|
if success:
|
258
269
|
logger.info("Successfully uploaded artwork for %s", file_path)
|
259
270
|
logger.debug("Artwork URL: %s", artwork_url)
|
260
271
|
else:
|
261
272
|
logger.warning("Failed to upload artwork for %s", file_path)
|
262
273
|
logger.debug("No suitable artwork found or upload failed")
|
263
|
-
|
264
274
|
if args.create_custom_json and file_path.lower().endswith('.taf'):
|
265
275
|
output_dir = './output'
|
266
276
|
logger.debug("Creating/ensuring output directory for JSON: %s", output_dir)
|
267
277
|
if not os.path.exists(output_dir):
|
268
278
|
os.makedirs(output_dir, exist_ok=True)
|
269
279
|
logger.trace("Created output directory: %s", output_dir)
|
270
|
-
|
271
280
|
logger.debug("Updating tonies.custom.json with: taf=%s, artwork_url=%s",
|
272
281
|
file_path, artwork_url)
|
273
|
-
|
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)
|
274
289
|
if success:
|
275
290
|
logger.info("Successfully updated Tonies JSON for %s", file_path)
|
276
291
|
else:
|
277
292
|
logger.warning("Failed to update Tonies JSON for %s", file_path)
|
278
293
|
logger.debug("fetch_and_update_tonies_json returned failure")
|
279
|
-
|
280
294
|
logger.trace("Exiting after direct upload with code 0")
|
281
295
|
sys.exit(0)
|
282
296
|
elif not args.recursive:
|
283
|
-
|
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)
|
284
301
|
logger.debug("File exists: %s, Is file: %s",
|
285
302
|
os.path.exists(args.input_filename),
|
286
303
|
os.path.isfile(args.input_filename) if os.path.exists(args.input_filename) else False)
|
@@ -364,10 +381,16 @@ def main():
|
|
364
381
|
if not skip_creation:
|
365
382
|
create_tonie_file(task_out_filename, audio_files, args.no_tonie_header, args.user_timestamp,
|
366
383
|
args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
|
367
|
-
args.auto_download, not args.use_legacy_tags
|
384
|
+
args.auto_download, not args.use_legacy_tags,
|
385
|
+
no_mono_conversion=args.no_mono_conversion)
|
368
386
|
logger.info("Successfully created Tonie file: %s", task_out_filename)
|
369
387
|
|
370
388
|
created_files.append(task_out_filename)
|
389
|
+
|
390
|
+
# ------------- Initialization -------------------
|
391
|
+
|
392
|
+
artwork_url = None
|
393
|
+
|
371
394
|
# ------------- Recursive File Upload -------------
|
372
395
|
if args.upload:
|
373
396
|
response = client.upload_file(
|
@@ -391,12 +414,19 @@ def main():
|
|
391
414
|
logger.warning("Failed to upload artwork for %s", task_out_filename)
|
392
415
|
|
393
416
|
# tonies.custom.json generation
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
417
|
+
if args.create_custom_json:
|
418
|
+
base_path = os.path.dirname(args.input_filename)
|
419
|
+
json_output_dir = base_path if args.output_to_source else output_dir
|
420
|
+
client_param = client if 'client' in locals() else None
|
421
|
+
if args.version_2:
|
422
|
+
logger.debug("Using version 2 of the Tonies JSON format")
|
423
|
+
success = fetch_and_update_tonies_json_v2(client_param, task_out_filename, audio_files, artwork_url, json_output_dir)
|
424
|
+
else:
|
425
|
+
success = fetch_and_update_tonies_json_v1(client_param, task_out_filename, audio_files, artwork_url, json_output_dir)
|
426
|
+
if success:
|
427
|
+
logger.info("Successfully updated Tonies JSON for %s", task_out_filename)
|
428
|
+
else:
|
429
|
+
logger.warning("Failed to update Tonies JSON for %s", task_out_filename)
|
400
430
|
|
401
431
|
logger.info("Recursive processing completed. Created %d Tonie files.", len(process_tasks))
|
402
432
|
sys.exit(0)
|
@@ -415,7 +445,6 @@ def main():
|
|
415
445
|
split_to_opus_files(args.input_filename, args.output_filename)
|
416
446
|
sys.exit(0)
|
417
447
|
elif args.compare:
|
418
|
-
from .tonie_analysis import compare_taf_files
|
419
448
|
logger.info("Comparing Tonie files: %s and %s", args.input_filename, args.compare)
|
420
449
|
result = compare_taf_files(args.input_filename, args.compare, args.detailed_compare)
|
421
450
|
sys.exit(0 if result else 1)
|
@@ -426,26 +455,12 @@ def main():
|
|
426
455
|
if len(files) == 0:
|
427
456
|
logger.error("No files found for pattern %s", args.input_filename)
|
428
457
|
sys.exit(1)
|
429
|
-
|
430
|
-
from .media_tags import get_file_tags
|
431
|
-
logger.info("Showing media tags for input files:")
|
432
|
-
|
433
|
-
for file_index, file_path in enumerate(files):
|
434
|
-
tags = get_file_tags(file_path)
|
435
|
-
if tags:
|
436
|
-
print(f"\nFile {file_index + 1}: {os.path.basename(file_path)}")
|
437
|
-
print("-" * 40)
|
438
|
-
for tag_name, tag_value in sorted(tags.items()):
|
439
|
-
print(f"{tag_name}: {tag_value}")
|
440
|
-
else:
|
441
|
-
print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
|
442
|
-
sys.exit(0)
|
458
|
+
|
443
459
|
guessed_name = None
|
444
460
|
if args.use_media_tags:
|
445
461
|
if len(files) > 1 and os.path.dirname(files[0]) == os.path.dirname(files[-1]):
|
446
462
|
folder_path = os.path.dirname(files[0])
|
447
|
-
logger.debug("Extracting album info from folder: %s", folder_path)
|
448
|
-
|
463
|
+
logger.debug("Extracting album info from folder: %s", folder_path)
|
449
464
|
album_info = extract_album_info(folder_path)
|
450
465
|
if album_info:
|
451
466
|
template = args.name_template or "{album} - {artist}"
|
@@ -457,7 +472,7 @@ def main():
|
|
457
472
|
else:
|
458
473
|
logger.debug("Could not format filename from album metadata")
|
459
474
|
elif len(files) == 1:
|
460
|
-
|
475
|
+
|
461
476
|
|
462
477
|
tags = get_file_tags(files[0])
|
463
478
|
if tags:
|
@@ -471,9 +486,7 @@ def main():
|
|
471
486
|
logger.debug("Could not format filename from file metadata")
|
472
487
|
|
473
488
|
# For multiple files from different folders, try to use common tags if they exist
|
474
|
-
elif len(files) > 1:
|
475
|
-
from .media_tags import get_file_tags, format_metadata_filename
|
476
|
-
|
489
|
+
elif len(files) > 1:
|
477
490
|
# Try to find common tags among files
|
478
491
|
common_tags = {}
|
479
492
|
for file_path in files:
|
@@ -547,7 +560,8 @@ def main():
|
|
547
560
|
logger.info("Creating Tonie file: %s with %d input file(s)", out_filename, len(files))
|
548
561
|
create_tonie_file(out_filename, files, args.no_tonie_header, args.user_timestamp,
|
549
562
|
args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
|
550
|
-
args.auto_download, not args.use_legacy_tags
|
563
|
+
args.auto_download, not args.use_legacy_tags,
|
564
|
+
no_mono_conversion=args.no_mono_conversion)
|
551
565
|
logger.info("Successfully created Tonie file: %s", out_filename)
|
552
566
|
|
553
567
|
# ------------- Single File Upload -------------
|
@@ -571,13 +585,19 @@ def main():
|
|
571
585
|
logger.info("Successfully uploaded artwork for %s", out_filename)
|
572
586
|
else:
|
573
587
|
logger.warning("Failed to upload artwork for %s", out_filename)
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
588
|
+
|
589
|
+
if args.create_custom_json:
|
590
|
+
json_output_dir = source_dir if args.output_to_source else './output'
|
591
|
+
client_param = client if 'client' in locals() else None
|
592
|
+
if args.version_2:
|
593
|
+
logger.debug("Using version 2 of the Tonies JSON format")
|
594
|
+
success = fetch_and_update_tonies_json_v2(client_param, out_filename, files, artwork_url, json_output_dir)
|
595
|
+
else:
|
596
|
+
success = fetch_and_update_tonies_json_v1(client_param, out_filename, files, artwork_url, json_output_dir)
|
597
|
+
if success:
|
598
|
+
logger.info("Successfully updated Tonies JSON for %s", out_filename)
|
599
|
+
else:
|
600
|
+
logger.warning("Failed to update Tonies JSON for %s", out_filename)
|
581
601
|
|
582
602
|
if __name__ == "__main__":
|
583
603
|
main()
|
TonieToolbox/audio_conversion.py
CHANGED
@@ -12,7 +12,7 @@ from .logger import get_logger
|
|
12
12
|
logger = get_logger('audio_conversion')
|
13
13
|
|
14
14
|
|
15
|
-
def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitrate=48, vbr=True, keep_temp=False, auto_download=False):
|
15
|
+
def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitrate=48, vbr=True, keep_temp=False, auto_download=False, no_mono_conversion=False):
|
16
16
|
"""
|
17
17
|
Convert an audio file to Opus format and return a temporary file handle.
|
18
18
|
|
@@ -24,12 +24,13 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
24
24
|
vbr: Whether to use variable bitrate encoding
|
25
25
|
keep_temp: Whether to keep the temporary files for testing
|
26
26
|
auto_download: Whether to automatically download dependencies if not found
|
27
|
+
no_mono_conversion: Whether to skip mono to stereo conversion
|
27
28
|
|
28
29
|
Returns:
|
29
30
|
tuple: (file handle, temp_file_path) or (file handle, None) if keep_temp is False
|
30
31
|
"""
|
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)
|
32
|
+
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
|
+
ffmpeg_binary, opus_binary, filename, bitrate, vbr, keep_temp, auto_download, no_mono_conversion)
|
33
34
|
|
34
35
|
logger.debug("Converting %s to Opus format (bitrate: %d kbps, vbr: %s)", filename, bitrate, vbr)
|
35
36
|
|
@@ -52,6 +53,38 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
52
53
|
vbr_parameter = "--vbr" if vbr else "--hard-cbr"
|
53
54
|
logger.debug("Using encoding parameter: %s", vbr_parameter)
|
54
55
|
|
56
|
+
is_mono = False
|
57
|
+
ffprobe_path = None
|
58
|
+
ffmpeg_dir, ffmpeg_file = os.path.split(ffmpeg_binary)
|
59
|
+
ffprobe_candidates = [
|
60
|
+
os.path.join(ffmpeg_dir, 'ffprobe'),
|
61
|
+
os.path.join(ffmpeg_dir, 'ffprobe.exe'),
|
62
|
+
'ffprobe',
|
63
|
+
'ffprobe.exe',
|
64
|
+
]
|
65
|
+
for candidate in ffprobe_candidates:
|
66
|
+
if os.path.exists(candidate):
|
67
|
+
ffprobe_path = candidate
|
68
|
+
break
|
69
|
+
if ffprobe_path:
|
70
|
+
try:
|
71
|
+
probe_cmd = [ffprobe_path, '-v', 'error', '-select_streams', 'a:0', '-show_entries', 'stream=channels', '-of', 'default=noprint_wrappers=1:nokey=1', filename]
|
72
|
+
logger.debug(f"Probing audio channels with: {' '.join(probe_cmd)}")
|
73
|
+
result = subprocess.run(probe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
74
|
+
if result.returncode == 0:
|
75
|
+
channels = result.stdout.strip()
|
76
|
+
logger.debug(f"Detected channels: {channels}")
|
77
|
+
if channels == '1':
|
78
|
+
is_mono = True
|
79
|
+
else:
|
80
|
+
logger.warning(f"ffprobe failed to detect channels: {result.stderr}")
|
81
|
+
except Exception as e:
|
82
|
+
logger.warning(f"Mono detection failed: {e}")
|
83
|
+
else:
|
84
|
+
logger.warning("ffprobe not found, will always force stereo conversion for non-Opus input.")
|
85
|
+
is_mono = True # Always force stereo if we can't check
|
86
|
+
logger.info(f"Mono detected: {is_mono}, no_mono_conversion: {no_mono_conversion}")
|
87
|
+
|
55
88
|
temp_path = None
|
56
89
|
if keep_temp:
|
57
90
|
temp_dir = os.path.join(tempfile.gettempdir(), "tonie_toolbox_temp")
|
@@ -62,7 +95,12 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
62
95
|
|
63
96
|
logger.debug("Starting FFmpeg process")
|
64
97
|
try:
|
65
|
-
|
98
|
+
if is_mono and not no_mono_conversion:
|
99
|
+
ffmpeg_cmd = [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav", "-ar", "48000", "-ac", "2", "-"]
|
100
|
+
logger.info(f"Forcing stereo conversion for mono input: {' '.join(ffmpeg_cmd)}")
|
101
|
+
else:
|
102
|
+
ffmpeg_cmd = [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav", "-ar", "48000", "-"]
|
103
|
+
logger.info(f"FFmpeg command: {' '.join(ffmpeg_cmd)}")
|
66
104
|
logger.trace("FFmpeg command: %s", ffmpeg_cmd)
|
67
105
|
ffmpeg_process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE)
|
68
106
|
except FileNotFoundError:
|
@@ -106,7 +144,12 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
106
144
|
|
107
145
|
logger.debug("Starting FFmpeg process")
|
108
146
|
try:
|
109
|
-
|
147
|
+
if is_mono and not no_mono_conversion:
|
148
|
+
ffmpeg_cmd = [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav", "-ar", "48000", "-ac", "2", "-"]
|
149
|
+
logger.info(f"Forcing stereo conversion for mono input: {' '.join(ffmpeg_cmd)}")
|
150
|
+
else:
|
151
|
+
ffmpeg_cmd = [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav", "-ar", "48000", "-"]
|
152
|
+
logger.info(f"FFmpeg command: {' '.join(ffmpeg_cmd)}")
|
110
153
|
logger.trace("FFmpeg command: %s", ffmpeg_cmd)
|
111
154
|
ffmpeg_process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE)
|
112
155
|
except FileNotFoundError:
|
@@ -244,9 +287,34 @@ def get_input_files(input_filename):
|
|
244
287
|
|
245
288
|
logger.debug("Found %d files in list file", len(input_files))
|
246
289
|
else:
|
247
|
-
logger.debug("Processing
|
290
|
+
logger.debug("Processing input path: %s", input_filename)
|
291
|
+
|
292
|
+
# Try the exact pattern first
|
248
293
|
input_files = sorted(filter_directories(glob.glob(input_filename)))
|
249
|
-
|
294
|
+
if input_files:
|
295
|
+
logger.debug("Found %d files matching exact pattern", len(input_files))
|
296
|
+
else:
|
297
|
+
# If no extension is provided, try appending a wildcard for extension
|
298
|
+
_, ext = os.path.splitext(input_filename)
|
299
|
+
if not ext:
|
300
|
+
wildcard_pattern = input_filename + ".*"
|
301
|
+
logger.debug("No extension in pattern, trying with wildcard: %s", wildcard_pattern)
|
302
|
+
input_files = sorted(filter_directories(glob.glob(wildcard_pattern)))
|
303
|
+
|
304
|
+
# If still no files found, try treating it as a directory
|
305
|
+
if not input_files and os.path.exists(os.path.dirname(input_filename)):
|
306
|
+
potential_dir = input_filename
|
307
|
+
if os.path.isdir(potential_dir):
|
308
|
+
logger.debug("Treating input as directory: %s", potential_dir)
|
309
|
+
dir_glob = os.path.join(potential_dir, "*")
|
310
|
+
input_files = sorted(filter_directories(glob.glob(dir_glob)))
|
311
|
+
if input_files:
|
312
|
+
logger.debug("Found %d audio files in directory", len(input_files))
|
313
|
+
|
314
|
+
if input_files:
|
315
|
+
logger.debug("Found %d files after trying alternatives", len(input_files))
|
316
|
+
else:
|
317
|
+
logger.warning("No files found for pattern %s even after trying alternatives", input_filename)
|
250
318
|
|
251
319
|
logger.trace("Exiting get_input_files() with %d files", len(input_files))
|
252
320
|
return input_files
|
TonieToolbox/media_tags.py
CHANGED
@@ -267,6 +267,7 @@ def get_file_tags(file_path: str) -> Dict[str, Any]:
|
|
267
267
|
tags[TAG_MAPPING[tag_key_lower]] = normalize_tag_value(tag_value_str)
|
268
268
|
|
269
269
|
logger.debug("Successfully read %d tags from file", len(tags))
|
270
|
+
logger.debug("Tags: %s", str(tags))
|
270
271
|
return tags
|
271
272
|
except Exception as e:
|
272
273
|
logger.error("Error reading tags from file %s: %s", file_path, str(e))
|
@@ -330,12 +331,12 @@ def extract_album_info(folder_path: str) -> Dict[str, str]:
|
|
330
331
|
if not all_tags:
|
331
332
|
logger.debug("Could not read tags from any files in folder")
|
332
333
|
return {}
|
333
|
-
|
334
|
-
# Try to find consistent album information
|
335
334
|
result = {}
|
336
|
-
|
335
|
+
all_tag_names = set()
|
336
|
+
for tags in all_tags:
|
337
|
+
all_tag_names.update(tags.keys())
|
337
338
|
|
338
|
-
for tag_name in
|
339
|
+
for tag_name in all_tag_names:
|
339
340
|
# Count occurrences of each value
|
340
341
|
value_counts = {}
|
341
342
|
for tags in all_tags:
|
@@ -217,14 +217,10 @@ def get_folder_name_from_metadata(folder_path: str, use_media_tags: bool = False
|
|
217
217
|
Returns:
|
218
218
|
String with cleaned output name
|
219
219
|
"""
|
220
|
-
# Start with folder name metadata
|
221
220
|
folder_meta = extract_folder_meta(folder_path)
|
222
|
-
output_name = None
|
223
|
-
|
224
|
-
# Try to get metadata from audio files if requested
|
221
|
+
output_name = None
|
225
222
|
if use_media_tags:
|
226
223
|
try:
|
227
|
-
# Import here to avoid circular imports
|
228
224
|
from .media_tags import extract_album_info, format_metadata_filename, is_available, normalize_tag_value
|
229
225
|
|
230
226
|
if is_available():
|
@@ -247,12 +243,15 @@ def get_folder_name_from_metadata(folder_path: str, use_media_tags: bool = False
|
|
247
243
|
if 'album' not in album_info or not album_info['album']:
|
248
244
|
album_info['album'] = normalize_tag_value(folder_meta['title'])
|
249
245
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
246
|
+
if template:
|
247
|
+
format_template = template
|
248
|
+
logger.debug("Using provided name template: %s", format_template)
|
249
|
+
else:
|
250
|
+
format_template = "{album}"
|
251
|
+
if 'artist' in album_info and album_info['artist']:
|
252
|
+
format_template = format_template + " - {artist}"
|
253
|
+
if 'number' in folder_meta and folder_meta['number']:
|
254
|
+
format_template = "{tracknumber} - " + format_template
|
256
255
|
|
257
256
|
formatted_name = format_metadata_filename(album_info, format_template)
|
258
257
|
|
TonieToolbox/tonie_file.py
CHANGED
@@ -15,7 +15,6 @@ from .ogg_page import OggPage
|
|
15
15
|
from .constants import OPUS_TAGS, SAMPLE_RATE_KHZ, TIMESTAMP_DEDUCT
|
16
16
|
from .logger import get_logger
|
17
17
|
|
18
|
-
# Setup logging
|
19
18
|
logger = get_logger('tonie_file')
|
20
19
|
|
21
20
|
|
@@ -38,7 +37,6 @@ def toniefile_comment_add(buffer, length, comment_str):
|
|
38
37
|
buffer[length:length+4] = struct.pack("<I", str_length)
|
39
38
|
length += 4
|
40
39
|
|
41
|
-
# Add the actual string
|
42
40
|
buffer[length:length+str_length] = comment_str.encode('utf-8')
|
43
41
|
length += str_length
|
44
42
|
|
@@ -115,49 +113,38 @@ def prepare_opus_tags(page, custom_tags=False, bitrate=64, vbr=True, opus_binary
|
|
115
113
|
# Use custom tags for TonieToolbox
|
116
114
|
# Create buffer for opus tags (similar to teddyCloud implementation)
|
117
115
|
logger.debug("Creating custom Opus tags")
|
118
|
-
comment_data = bytearray(0x1B4)
|
119
|
-
|
120
|
-
# OpusTags signature
|
116
|
+
comment_data = bytearray(0x1B4)
|
121
117
|
comment_data_pos = 0
|
122
118
|
comment_data[comment_data_pos:comment_data_pos+8] = b"OpusTags"
|
123
|
-
comment_data_pos += 8
|
124
|
-
|
119
|
+
comment_data_pos += 8
|
125
120
|
# Vendor string
|
126
|
-
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, "TonieToolbox")
|
127
|
-
|
121
|
+
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, "TonieToolbox")
|
128
122
|
# Number of comments (3 comments: version, encoder info, and encoder options)
|
129
123
|
comments_count = 3
|
130
124
|
comment_data[comment_data_pos:comment_data_pos+4] = struct.pack("<I", comments_count)
|
131
|
-
comment_data_pos += 4
|
132
|
-
|
125
|
+
comment_data_pos += 4
|
133
126
|
# Add version information
|
134
127
|
from . import __version__
|
135
128
|
version_str = f"version={__version__}"
|
136
|
-
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, version_str)
|
137
|
-
|
129
|
+
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, version_str)
|
138
130
|
# Get actual opusenc version
|
139
131
|
from .dependency_manager import get_opus_version
|
140
132
|
encoder_info = get_opus_version(opus_binary)
|
141
|
-
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, f"encoder={encoder_info}")
|
142
|
-
|
133
|
+
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, f"encoder={encoder_info}")
|
143
134
|
# Create encoder options string with actual settings
|
144
135
|
vbr_opt = "--vbr" if vbr else "--cbr"
|
145
136
|
encoder_options = f"encoder_options=--bitrate {bitrate} {vbr_opt}"
|
146
|
-
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, encoder_options)
|
147
|
-
|
137
|
+
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, encoder_options)
|
148
138
|
# Add padding
|
149
139
|
remain = len(comment_data) - comment_data_pos - 4
|
150
140
|
comment_data[comment_data_pos:comment_data_pos+4] = struct.pack("<I", remain)
|
151
141
|
comment_data_pos += 4
|
152
|
-
comment_data[comment_data_pos:comment_data_pos+4] = b"pad="
|
153
|
-
|
142
|
+
comment_data[comment_data_pos:comment_data_pos+4] = b"pad="
|
154
143
|
# Create segments - handle data in chunks of 255 bytes maximum
|
155
|
-
comment_data = comment_data[:comment_data_pos + remain] # Trim to actual used size
|
156
|
-
|
144
|
+
comment_data = comment_data[:comment_data_pos + remain] # Trim to actual used size
|
157
145
|
# Split large data into smaller segments (each <= 255 bytes)
|
158
146
|
remaining_data = comment_data
|
159
|
-
first_segment = True
|
160
|
-
|
147
|
+
first_segment = True
|
161
148
|
while remaining_data:
|
162
149
|
chunk_size = min(255, len(remaining_data))
|
163
150
|
segment = OpusPacket(None)
|
@@ -377,7 +364,7 @@ def fix_tonie_header(out_file, chapters, timestamp, sha):
|
|
377
364
|
|
378
365
|
def create_tonie_file(output_file, input_files, no_tonie_header=False, user_timestamp=None,
|
379
366
|
bitrate=96, vbr=True, ffmpeg_binary=None, opus_binary=None, keep_temp=False, auto_download=False,
|
380
|
-
use_custom_tags=True):
|
367
|
+
use_custom_tags=True, no_mono_conversion=False):
|
381
368
|
"""
|
382
369
|
Create a Tonie file from input files.
|
383
370
|
|
@@ -393,13 +380,14 @@ def create_tonie_file(output_file, input_files, no_tonie_header=False, user_time
|
|
393
380
|
keep_temp: Whether to keep temporary opus files for testing
|
394
381
|
auto_download: Whether to automatically download dependencies if not found
|
395
382
|
use_custom_tags: Whether to use dynamic comment tags generated with toniefile_comment_add
|
383
|
+
no_mono_conversion: Whether to skip mono conversion during audio processing
|
396
384
|
"""
|
397
385
|
from .audio_conversion import get_opus_tempfile
|
398
386
|
|
399
387
|
logger.trace("Entering create_tonie_file(output_file=%s, input_files=%s, no_tonie_header=%s, user_timestamp=%s, "
|
400
|
-
"bitrate=%d, vbr=%s, ffmpeg_binary=%s, opus_binary=%s, keep_temp=%s, auto_download=%s, use_custom_tags=%s)",
|
388
|
+
"bitrate=%d, vbr=%s, ffmpeg_binary=%s, opus_binary=%s, keep_temp=%s, auto_download=%s, use_custom_tags=%s, no_mono_conversion=%s)",
|
401
389
|
output_file, input_files, no_tonie_header, user_timestamp, bitrate, vbr, ffmpeg_binary,
|
402
|
-
opus_binary, keep_temp, auto_download, use_custom_tags)
|
390
|
+
opus_binary, keep_temp, auto_download, use_custom_tags, no_mono_conversion)
|
403
391
|
|
404
392
|
logger.info("Creating Tonie file from %d input files", len(input_files))
|
405
393
|
logger.debug("Output file: %s, Bitrate: %d kbps, VBR: %s, No header: %s",
|
@@ -465,9 +453,9 @@ def create_tonie_file(output_file, input_files, no_tonie_header=False, user_time
|
|
465
453
|
handle = open(fname, "rb")
|
466
454
|
temp_file_path = None
|
467
455
|
else:
|
468
|
-
logger.debug("Converting %s to Opus format (bitrate: %d kbps, VBR: %s)",
|
469
|
-
fname, bitrate, vbr)
|
470
|
-
handle, temp_file_path = get_opus_tempfile(ffmpeg_binary, opus_binary, fname, bitrate, vbr, keep_temp, auto_download)
|
456
|
+
logger.debug("Converting %s to Opus format (bitrate: %d kbps, VBR: %s, no_mono_conversion: %s)",
|
457
|
+
fname, bitrate, vbr, no_mono_conversion)
|
458
|
+
handle, temp_file_path = get_opus_tempfile(ffmpeg_binary, opus_binary, fname, bitrate, vbr, keep_temp, auto_download, no_mono_conversion=no_mono_conversion)
|
471
459
|
if temp_file_path:
|
472
460
|
temp_files.append(temp_file_path)
|
473
461
|
logger.debug("Temporary opus file saved to: %s", temp_file_path)
|