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 CHANGED
@@ -2,4 +2,4 @@
2
2
  TonieToolbox - Convert audio files to Tonie box compatible format
3
3
  """
4
4
 
5
- __version__ = '0.5.0a1'
5
+ __version__ = '0.5.1'
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 fetch_and_update_tonies_json
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
- # ------------- Direct Upload -------------
208
- if args.upload and not args.recursive:
209
- logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
210
- logger.trace("TeddyCloud upload parameters: path=%s, special_folder=%s, ignore_ssl=%s",
211
- args.path, args.special_folder, args.ignore_ssl_verify)
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", file_path, error_msg)
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
- success = fetch_and_update_tonies_json(client, file_path, [], artwork_url, output_dir)
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
- logger.error("File not found or not a regular file: %s", args.input_filename)
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
- if args.create_custom_json:
395
- success = fetch_and_update_tonies_json(client, task_out_filename, audio_files, artwork_url, output_dir)
396
- if success:
397
- logger.info("Successfully updated Tonies JSON for %s", task_out_filename)
398
- else:
399
- logger.warning("Failed to update Tonies JSON for %s", task_out_filename)
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
- if args.show_tags:
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
- from .media_tags import get_file_tags, format_metadata_filename
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
- # tonies.custom.json generation
575
- if args.create_custom_json:
576
- success = fetch_and_update_tonies_json(client, out_filename, files, artwork_url)
577
- if success:
578
- logger.info("Successfully updated Tonies JSON for %s", out_filename)
579
- else:
580
- logger.warning("Failed to update Tonies JSON for %s", out_filename)
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()
@@ -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
- ffmpeg_cmd = [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav", "-ar", "48000", "-"]
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
- ffmpeg_cmd = [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav", "-ar", "48000", "-"]
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 glob pattern: %s", input_filename)
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
- logger.debug("Found %d files matching pattern", len(input_files))
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
@@ -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
- key_tags = ['album', 'albumartist', 'artist', 'date', 'genre']
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 key_tags:
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
- # Use template or default format
251
- format_template = template or "{album}"
252
- if 'artist' in album_info and album_info['artist']:
253
- format_template = format_template + " - {artist}"
254
- if 'number' in folder_meta and folder_meta['number']:
255
- format_template = "{tracknumber} - " + format_template
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
 
@@ -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) # Same size as in teddyCloud
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)