TonieToolbox 0.2.3__py3-none-any.whl → 0.4.0__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.2.3'
5
+ __version__ = '0.4.0'
TonieToolbox/__main__.py CHANGED
@@ -18,13 +18,43 @@ from .filename_generator import guess_output_filename
18
18
  from .version_handler import check_for_updates, clear_version_cache
19
19
  from .recursive_processor import process_recursive_folders
20
20
  from .media_tags import is_available as is_media_tags_available, ensure_mutagen
21
+ from .teddycloud import upload_to_teddycloud, get_tags_from_teddycloud, get_file_paths
22
+ from .tonies_json import fetch_and_update_tonies_json
21
23
 
22
24
  def main():
23
25
  """Entry point for the TonieToolbox application."""
24
26
  parser = argparse.ArgumentParser(description='Create Tonie compatible file from Ogg opus file(s).')
25
27
  parser.add_argument('-v', '--version', action='version', version=f'TonieToolbox {__version__}',
26
28
  help='show program version and exit')
27
- parser.add_argument('input_filename', metavar='SOURCE', type=str,
29
+
30
+ # TeddyCloud options first to check for existence before requiring SOURCE
31
+ teddycloud_group = parser.add_argument_group('TeddyCloud Options')
32
+ teddycloud_group.add_argument('--upload', metavar='URL', action='store',
33
+ help='Upload to TeddyCloud instance (e.g., https://teddycloud.example.com). Supports .taf, .jpg, .jpeg, .png files.')
34
+ teddycloud_group.add_argument('--include-artwork', action='store_true',
35
+ help='Upload cover artwork image alongside the Tonie file when using --upload')
36
+ teddycloud_group.add_argument('--get-tags', action='store', metavar='URL',
37
+ help='Get available tags from TeddyCloud instance')
38
+ teddycloud_group.add_argument('--ignore-ssl-verify', action='store_true',
39
+ help='Ignore SSL certificate verification (for self-signed certificates)')
40
+ teddycloud_group.add_argument('--special-folder', action='store', metavar='FOLDER',
41
+ help='Special folder to upload to (currently only "library" is supported)', default='library')
42
+ teddycloud_group.add_argument('--path', action='store', metavar='PATH',
43
+ help='Path where to write the file on TeddyCloud server')
44
+ teddycloud_group.add_argument('--show-progress', action='store_true', default=True,
45
+ help='Show progress bar during file upload (default: enabled)')
46
+ teddycloud_group.add_argument('--connection-timeout', type=int, metavar='SECONDS', default=10,
47
+ help='Connection timeout in seconds (default: 10)')
48
+ teddycloud_group.add_argument('--read-timeout', type=int, metavar='SECONDS', default=300,
49
+ help='Read timeout in seconds (default: 300)')
50
+ teddycloud_group.add_argument('--max-retries', type=int, metavar='RETRIES', default=3,
51
+ help='Maximum number of retry attempts (default: 3)')
52
+ teddycloud_group.add_argument('--retry-delay', type=int, metavar='SECONDS', default=5,
53
+ help='Delay between retry attempts in seconds (default: 5)')
54
+ teddycloud_group.add_argument('--create-custom-json', action='store_true',
55
+ help='Fetch and update custom Tonies JSON data')
56
+
57
+ parser.add_argument('input_filename', metavar='SOURCE', type=str, nargs='?',
28
58
  help='input file or directory or a file list (.lst)')
29
59
  parser.add_argument('output_filename', metavar='TARGET', nargs='?', type=str,
30
60
  help='the output file name (default: ---ID---)')
@@ -77,8 +107,16 @@ def main():
77
107
  log_level_group.add_argument('-T', '--trace', action='store_true', help='Enable trace logging (very verbose)')
78
108
  log_level_group.add_argument('-q', '--quiet', action='store_true', help='Show only warnings and errors')
79
109
  log_level_group.add_argument('-Q', '--silent', action='store_true', help='Show only errors')
110
+ log_group.add_argument('--log-file', action='store_true', default=False,
111
+ help='Save logs to a timestamped file in .tonietoolbox folder')
80
112
 
81
113
  args = parser.parse_args()
114
+
115
+ # Validate that input_filename is provided if not using --get-tags or --upload-existing
116
+ if args.input_filename is None and not (args.get_tags or args.upload):
117
+ parser.error("the following arguments are required: SOURCE")
118
+
119
+ # Set up the logging level
82
120
  if args.trace:
83
121
  from .logger import TRACE
84
122
  log_level = TRACE
@@ -90,12 +128,16 @@ def main():
90
128
  log_level = logging.ERROR
91
129
  else:
92
130
  log_level = logging.INFO
93
-
94
- setup_logging(log_level)
131
+
132
+ setup_logging(log_level, log_to_file=args.log_file)
95
133
  logger = get_logger('main')
96
134
  logger.debug("Starting TonieToolbox v%s with log level: %s", __version__, logging.getLevelName(log_level))
97
135
 
136
+ # Log the command-line arguments at trace level for debugging purposes
137
+ logger.log(logging.DEBUG - 1, "Command-line arguments: %s", vars(args))
138
+
98
139
  if args.clear_version_cache:
140
+ logger.log(logging.DEBUG - 1, "Clearing version cache")
99
141
  if clear_version_cache():
100
142
  logger.info("Version cache cleared successfully")
101
143
  else:
@@ -108,9 +150,71 @@ def main():
108
150
  force_refresh=args.force_refresh_cache
109
151
  )
110
152
 
153
+ logger.log(logging.DEBUG - 1, "Update check results: is_latest=%s, latest_version=%s, update_confirmed=%s",
154
+ is_latest, latest_version, update_confirmed)
155
+
111
156
  if not is_latest and not update_confirmed and not (args.silent or args.quiet):
112
157
  logger.info("Update available but user chose to continue without updating.")
113
158
 
159
+ # Handle get-tags from TeddyCloud if requested
160
+ if args.get_tags:
161
+ logger.debug("Getting tags from TeddyCloud: %s", args.get_tags)
162
+ teddycloud_url = args.get_tags
163
+ success = get_tags_from_teddycloud(teddycloud_url, args.ignore_ssl_verify)
164
+ logger.log(logging.DEBUG - 1, "Exiting with code %d", 0 if success else 1)
165
+ sys.exit(0 if success else 1)
166
+
167
+ # Handle upload to TeddyCloud if requested
168
+ if args.upload:
169
+ teddycloud_url = args.upload
170
+ logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
171
+
172
+ if not args.input_filename:
173
+ logger.error("Missing input file for --upload. Provide a file path as SOURCE argument.")
174
+ sys.exit(1)
175
+
176
+ # Check if the input file is already a .taf file or an image file
177
+ if os.path.exists(args.input_filename) and (args.input_filename.lower().endswith('.taf') or
178
+ args.input_filename.lower().endswith(('.jpg', '.jpeg', '.png'))):
179
+ # Direct upload of existing TAF or image file
180
+ logger.debug("Direct upload of existing TAF or image file detected")
181
+ # Use get_file_paths to handle Windows backslashes and resolve the paths correctly
182
+ file_paths = get_file_paths(args.input_filename)
183
+
184
+ if not file_paths:
185
+ logger.error("No files found for pattern %s", args.input_filename)
186
+ sys.exit(1)
187
+
188
+ logger.info("Found %d file(s) to upload to TeddyCloud %s", len(file_paths), teddycloud_url)
189
+
190
+ for file_path in file_paths:
191
+ # Only upload supported file types
192
+ if not file_path.lower().endswith(('.taf', '.jpg', '.jpeg', '.png')):
193
+ logger.warning("Skipping unsupported file type: %s", file_path)
194
+ continue
195
+
196
+ logger.info("Uploading %s to TeddyCloud %s", file_path, teddycloud_url)
197
+ upload_success = upload_to_teddycloud(
198
+ file_path, teddycloud_url, args.ignore_ssl_verify,
199
+ args.special_folder, args.path, args.show_progress,
200
+ args.connection_timeout, args.read_timeout,
201
+ args.max_retries, args.retry_delay
202
+ )
203
+
204
+ if not upload_success:
205
+ logger.error("Failed to upload %s to TeddyCloud", file_path)
206
+ sys.exit(1)
207
+ else:
208
+ logger.info("Successfully uploaded %s to TeddyCloud", file_path)
209
+
210
+ logger.log(logging.DEBUG - 1, "Exiting after direct upload with code 0")
211
+ sys.exit(0)
212
+
213
+ # If we get here, it's not a TAF or image file, so continue with normal processing
214
+ # which will convert the input files and upload the result later
215
+ logger.debug("Input is not a direct upload file, continuing with conversion workflow")
216
+ pass
217
+
114
218
  ffmpeg_binary = args.ffmpeg
115
219
  if ffmpeg_binary is None:
116
220
  ffmpeg_binary = get_ffmpeg_binary(args.auto_download)
@@ -156,6 +260,7 @@ def main():
156
260
  os.makedirs(output_dir, exist_ok=True)
157
261
  logger.debug("Created output directory: %s", output_dir)
158
262
 
263
+ created_files = []
159
264
  for task_index, (output_name, folder_path, audio_files) in enumerate(process_tasks):
160
265
  if args.output_to_source:
161
266
  task_out_filename = os.path.join(folder_path, f"{output_name}.taf")
@@ -169,8 +274,102 @@ def main():
169
274
  args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
170
275
  args.auto_download, not args.use_legacy_tags)
171
276
  logger.info("Successfully created Tonie file: %s", task_out_filename)
277
+ created_files.append(task_out_filename)
172
278
 
173
279
  logger.info("Recursive processing completed. Created %d Tonie files.", len(process_tasks))
280
+
281
+ # Handle upload to TeddyCloud if requested
282
+ if args.upload and created_files:
283
+ teddycloud_url = args.upload
284
+
285
+ for taf_file in created_files:
286
+ upload_success = upload_to_teddycloud(
287
+ taf_file, teddycloud_url, args.ignore_ssl_verify,
288
+ args.special_folder, args.path, args.show_progress,
289
+ args.connection_timeout, args.read_timeout,
290
+ args.max_retries, args.retry_delay
291
+ )
292
+
293
+ if not upload_success:
294
+ logger.error("Failed to upload %s to TeddyCloud", taf_file)
295
+ else:
296
+ logger.info("Successfully uploaded %s to TeddyCloud", taf_file)
297
+
298
+ # Handle artwork upload if requested
299
+ if args.include_artwork:
300
+ # Extract folder path from the current task
301
+ folder_path = os.path.dirname(taf_file)
302
+ taf_file_basename = os.path.basename(taf_file)
303
+ taf_name = os.path.splitext(taf_file_basename)[0] # Get name without extension
304
+ logger.info("Looking for artwork for %s", folder_path)
305
+
306
+ # Try to find cover image in the folder
307
+ from .media_tags import find_cover_image
308
+ artwork_path = find_cover_image(folder_path)
309
+ temp_artwork = None
310
+
311
+ # If no cover image found, try to extract it from one of the audio files
312
+ if not artwork_path:
313
+ # Get current task's audio files
314
+ for task_name, task_folder, task_files in process_tasks:
315
+ if task_folder == folder_path or os.path.normpath(task_folder) == os.path.normpath(folder_path):
316
+ if task_files and len(task_files) > 0:
317
+ # Try to extract from first file
318
+ from .media_tags import extract_artwork, ensure_mutagen
319
+ if ensure_mutagen(auto_install=args.auto_download):
320
+ temp_artwork = extract_artwork(task_files[0])
321
+ if temp_artwork:
322
+ artwork_path = temp_artwork
323
+ break
324
+
325
+ if artwork_path:
326
+ logger.info("Found artwork for %s: %s", folder_path, artwork_path)
327
+ artwork_upload_path = "/custom_img"
328
+ artwork_ext = os.path.splitext(artwork_path)[1]
329
+
330
+ # Create a temporary copy with the same name as the taf file
331
+ import shutil
332
+ renamed_artwork_path = None
333
+ try:
334
+ renamed_artwork_path = os.path.join(os.path.dirname(artwork_path),
335
+ f"{taf_name}{artwork_ext}")
336
+
337
+ if renamed_artwork_path != artwork_path:
338
+ shutil.copy2(artwork_path, renamed_artwork_path)
339
+ logger.debug("Created renamed artwork copy: %s", renamed_artwork_path)
340
+
341
+ logger.info("Uploading artwork to path: %s as %s%s",
342
+ artwork_upload_path, taf_name, artwork_ext)
343
+
344
+ artwork_upload_success = upload_to_teddycloud(
345
+ renamed_artwork_path, teddycloud_url, args.ignore_ssl_verify,
346
+ args.special_folder, artwork_upload_path, args.show_progress,
347
+ args.connection_timeout, args.read_timeout,
348
+ args.max_retries, args.retry_delay
349
+ )
350
+
351
+ if artwork_upload_success:
352
+ logger.info("Successfully uploaded artwork for %s", folder_path)
353
+ else:
354
+ logger.warning("Failed to upload artwork for %s", folder_path)
355
+
356
+ if renamed_artwork_path != artwork_path and os.path.exists(renamed_artwork_path):
357
+ try:
358
+ os.unlink(renamed_artwork_path)
359
+ logger.debug("Removed temporary renamed artwork file: %s", renamed_artwork_path)
360
+ except Exception as e:
361
+ logger.debug("Failed to remove temporary renamed artwork file: %s", e)
362
+
363
+ if temp_artwork and os.path.exists(temp_artwork) and temp_artwork != renamed_artwork_path:
364
+ try:
365
+ os.unlink(temp_artwork)
366
+ logger.debug("Removed temporary artwork file: %s", temp_artwork)
367
+ except Exception as e:
368
+ logger.debug("Failed to remove temporary artwork file: %s", e)
369
+ except Exception as e:
370
+ logger.error("Error during artwork renaming or upload: %s", e)
371
+ else:
372
+ logger.warning("No artwork found for %s", folder_path)
174
373
  sys.exit(0)
175
374
 
176
375
  # Handle directory or file input
@@ -214,7 +413,6 @@ def main():
214
413
  print(f"{tag_name}: {tag_value}")
215
414
  else:
216
415
  print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
217
-
218
416
  sys.exit(0)
219
417
 
220
418
  # Use media tags for file naming if requested
@@ -324,13 +522,159 @@ def main():
324
522
 
325
523
  if not out_filename.lower().endswith('.taf'):
326
524
  out_filename += '.taf'
327
-
525
+
328
526
  logger.info("Creating Tonie file: %s with %d input file(s)", out_filename, len(files))
329
527
  create_tonie_file(out_filename, files, args.no_tonie_header, args.user_timestamp,
330
528
  args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
331
529
  args.auto_download, not args.use_legacy_tags)
332
530
  logger.info("Successfully created Tonie file: %s", out_filename)
531
+
532
+ # Handle upload to TeddyCloud if requested
533
+ if args.upload:
534
+ teddycloud_url = args.upload
535
+
536
+ upload_success = upload_to_teddycloud(
537
+ out_filename, teddycloud_url, args.ignore_ssl_verify,
538
+ args.special_folder, args.path, args.show_progress,
539
+ args.connection_timeout, args.read_timeout,
540
+ args.max_retries, args.retry_delay
541
+ )
542
+ if not upload_success:
543
+ logger.error("Failed to upload %s to TeddyCloud", out_filename)
544
+ sys.exit(1)
545
+ else:
546
+ logger.info("Successfully uploaded %s to TeddyCloud", out_filename)
547
+
548
+ # Handle artwork upload if requested
549
+ if args.include_artwork:
550
+ logger.info("Looking for artwork to upload alongside the Tonie file")
551
+ artwork_path = None
552
+
553
+ # Try to find a cover image in the source directory first
554
+ source_dir = os.path.dirname(files[0]) if files else None
555
+ if source_dir:
556
+ from .media_tags import find_cover_image
557
+ artwork_path = find_cover_image(source_dir)
558
+
559
+ # If no cover in source directory, try to extract it from audio file
560
+ if not artwork_path and len(files) > 0:
561
+ from .media_tags import extract_artwork, ensure_mutagen
562
+
563
+ # Make sure mutagen is available for artwork extraction
564
+ if ensure_mutagen(auto_install=args.auto_download):
565
+ # Try to extract artwork from the first file
566
+ temp_artwork = extract_artwork(files[0])
567
+ if temp_artwork:
568
+ artwork_path = temp_artwork
569
+ # Note: this creates a temporary file that will be deleted after upload
570
+
571
+ # Upload the artwork if found
572
+ if artwork_path:
573
+ logger.info("Found artwork: %s", artwork_path)
574
+
575
+ # Create artwork upload path - keep same path but use "custom_img" folder
576
+ artwork_upload_path = args.path
577
+ if not artwork_upload_path:
578
+ artwork_upload_path = "/custom_img"
579
+ elif not artwork_upload_path.startswith("/custom_img"):
580
+ # Make sure we're using the custom_img folder
581
+ if artwork_upload_path.startswith("/"):
582
+ artwork_upload_path = "/custom_img" + artwork_upload_path
583
+ else:
584
+ artwork_upload_path = "/custom_img/" + artwork_upload_path
585
+
586
+ # Get the original artwork file extension
587
+ artwork_ext = os.path.splitext(artwork_path)[1]
588
+
589
+ # Create a temporary copy with the same name as the taf file
590
+ import shutil
591
+ renamed_artwork_path = None
592
+ try:
593
+ renamed_artwork_path = os.path.join(os.path.dirname(artwork_path),
594
+ f"{os.path.splitext(os.path.basename(out_filename))[0]}{artwork_ext}")
595
+
596
+ if renamed_artwork_path != artwork_path:
597
+ shutil.copy2(artwork_path, renamed_artwork_path)
598
+ logger.debug("Created renamed artwork copy: %s", renamed_artwork_path)
599
+
600
+ logger.info("Uploading artwork to path: %s as %s%s",
601
+ artwork_upload_path, os.path.splitext(os.path.basename(out_filename))[0], artwork_ext)
602
+
603
+ artwork_upload_success = upload_to_teddycloud(
604
+ renamed_artwork_path, teddycloud_url, args.ignore_ssl_verify,
605
+ args.special_folder, artwork_upload_path, args.show_progress,
606
+ args.connection_timeout, args.read_timeout,
607
+ args.max_retries, args.retry_delay
608
+ )
609
+
610
+ if artwork_upload_success:
611
+ logger.info("Successfully uploaded artwork")
612
+ else:
613
+ logger.warning("Failed to upload artwork")
614
+
615
+ # Clean up temporary renamed file
616
+ if renamed_artwork_path != artwork_path and os.path.exists(renamed_artwork_path):
617
+ try:
618
+ os.unlink(renamed_artwork_path)
619
+ logger.debug("Removed temporary renamed artwork file: %s", renamed_artwork_path)
620
+ except Exception as e:
621
+ logger.debug("Failed to remove temporary renamed artwork file: %s", e)
622
+
623
+ # Clean up temporary extracted artwork file if needed
624
+ if temp_artwork and os.path.exists(temp_artwork) and temp_artwork != renamed_artwork_path:
625
+ try:
626
+ os.unlink(temp_artwork)
627
+ logger.debug("Removed temporary artwork file: %s", temp_artwork)
628
+ except Exception as e:
629
+ logger.debug("Failed to remove temporary artwork file: %s", e)
630
+ except Exception as e:
631
+ logger.error("Error during artwork renaming or upload: %s", e)
632
+ else:
633
+ logger.warning("No artwork found to upload")
333
634
 
635
+ # Handle create-custom-json option
636
+ if args.create_custom_json and args.upload:
637
+ teddycloud_url = args.upload
638
+ artwork_url = None
639
+
640
+ # If artwork was uploaded, construct its URL for the JSON
641
+ if args.include_artwork:
642
+ taf_basename = os.path.splitext(os.path.basename(out_filename))[0]
643
+ artwork_ext = None
644
+
645
+ # Try to determine the artwork extension by checking what was uploaded
646
+ source_dir = os.path.dirname(files[0]) if files else None
647
+ if source_dir:
648
+ from .media_tags import find_cover_image
649
+ artwork_path = find_cover_image(source_dir)
650
+ if artwork_path:
651
+ artwork_ext = os.path.splitext(artwork_path)[1]
652
+
653
+ # If we couldn't determine extension from a found image, default to .jpg
654
+ if not artwork_ext:
655
+ artwork_ext = ".jpg"
656
+
657
+ # Construct the URL for the artwork based on TeddyCloud structure
658
+ artwork_path = args.path or "/custom_img"
659
+ if not artwork_path.endswith('/'):
660
+ artwork_path += '/'
661
+
662
+ artwork_url = f"{teddycloud_url}{artwork_path}{taf_basename}{artwork_ext}"
663
+ logger.debug("Using artwork URL: %s", artwork_url)
664
+
665
+ logger.info("Fetching and updating custom Tonies JSON data")
666
+ success = fetch_and_update_tonies_json(
667
+ teddycloud_url,
668
+ args.ignore_ssl_verify,
669
+ out_filename,
670
+ files,
671
+ artwork_url
672
+ )
673
+
674
+ if success:
675
+ logger.info("Successfully updated custom Tonies JSON data")
676
+ else:
677
+ logger.warning("Failed to update custom Tonies JSON data")
334
678
 
335
679
  if __name__ == "__main__":
336
680
  main()
@@ -28,6 +28,9 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
28
28
  Returns:
29
29
  tuple: (file handle, temp_file_path) or (file handle, None) if keep_temp is False
30
30
  """
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)
33
+
31
34
  logger.debug("Converting %s to Opus format (bitrate: %d kbps, vbr: %s)", filename, bitrate, vbr)
32
35
 
33
36
  if ffmpeg_binary is None:
@@ -59,18 +62,19 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
59
62
 
60
63
  logger.debug("Starting FFmpeg process")
61
64
  try:
62
- ffmpeg_process = subprocess.Popen(
63
- [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav",
64
- "-ar", "48000", "-"], stdout=subprocess.PIPE)
65
+ ffmpeg_cmd = [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav", "-ar", "48000", "-"]
66
+ logger.trace("FFmpeg command: %s", ffmpeg_cmd)
67
+ ffmpeg_process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE)
65
68
  except FileNotFoundError:
66
69
  logger.error("Error opening input file %s", filename)
67
70
  raise RuntimeError(f"Error opening input file {filename}")
68
71
 
69
72
  logger.debug("Starting opusenc process")
70
73
  try:
74
+ opusenc_cmd = [opus_binary, "--quiet", vbr_parameter, "--bitrate", f"{bitrate:d}", "-", temp_path]
75
+ logger.trace("Opusenc command: %s", opusenc_cmd)
71
76
  opusenc_process = subprocess.Popen(
72
- [opus_binary, "--quiet", vbr_parameter, "--bitrate", f"{bitrate:d}", "-", temp_path],
73
- stdin=ffmpeg_process.stdout, stderr=subprocess.DEVNULL)
77
+ opusenc_cmd, stdin=ffmpeg_process.stdout, stderr=subprocess.DEVNULL)
74
78
  except Exception as e:
75
79
  logger.error("Opus encoding failed: %s", str(e))
76
80
  raise RuntimeError(f"Opus encoding failed: {str(e)}")
@@ -79,6 +83,8 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
79
83
  opusenc_return = opusenc_process.wait()
80
84
  ffmpeg_return = ffmpeg_process.wait()
81
85
 
86
+ logger.debug("Process return codes - FFmpeg: %d, Opus: %d", ffmpeg_return, opusenc_return)
87
+
82
88
  if ffmpeg_return != 0:
83
89
  logger.error("FFmpeg processing failed with return code %d", ffmpeg_return)
84
90
  raise RuntimeError(f"FFmpeg processing failed with return code {ffmpeg_return}")
@@ -87,9 +93,10 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
87
93
  logger.error("Opus encoding failed with return code %d", opusenc_return)
88
94
  raise RuntimeError(f"Opus encoding failed with return code {opusenc_return}")
89
95
 
90
- logger.debug("Opening temporary file for reading")
96
+ logger.debug("Opening temporary file for reading: %s", temp_path)
91
97
  try:
92
98
  tmp_file = open(temp_path, "rb")
99
+ logger.trace("Exiting get_opus_tempfile() with persistent temporary file")
93
100
  return tmp_file, temp_path
94
101
  except Exception as e:
95
102
  logger.error("Failed to open temporary file: %s", str(e))
@@ -99,18 +106,19 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
99
106
 
100
107
  logger.debug("Starting FFmpeg process")
101
108
  try:
102
- ffmpeg_process = subprocess.Popen(
103
- [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav",
104
- "-ar", "48000", "-"], stdout=subprocess.PIPE)
109
+ ffmpeg_cmd = [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav", "-ar", "48000", "-"]
110
+ logger.trace("FFmpeg command: %s", ffmpeg_cmd)
111
+ ffmpeg_process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE)
105
112
  except FileNotFoundError:
106
113
  logger.error("Error opening input file %s", filename)
107
114
  raise RuntimeError(f"Error opening input file {filename}")
108
115
 
109
116
  logger.debug("Starting opusenc process")
110
117
  try:
118
+ opusenc_cmd = [opus_binary, "--quiet", vbr_parameter, "--bitrate", f"{bitrate:d}", "-", "-"]
119
+ logger.trace("Opusenc command: %s", opusenc_cmd)
111
120
  opusenc_process = subprocess.Popen(
112
- [opus_binary, "--quiet", vbr_parameter, "--bitrate", f"{bitrate:d}", "-", "-"],
113
- stdin=ffmpeg_process.stdout, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
121
+ opusenc_cmd, stdin=ffmpeg_process.stdout, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
114
122
  except Exception as e:
115
123
  logger.error("Opus encoding failed: %s", str(e))
116
124
  raise RuntimeError(f"Opus encoding failed: {str(e)}")
@@ -124,10 +132,14 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
124
132
  for chunk in iter(lambda: opusenc_process.stdout.read(4096), b""):
125
133
  tmp_file.write(chunk)
126
134
  bytes_written += len(chunk)
135
+ if bytes_written % (1024 * 1024) == 0: # Log every 1MB
136
+ logger.trace("Written %d bytes so far", bytes_written)
127
137
 
128
138
  opusenc_return = opusenc_process.wait()
129
139
  ffmpeg_return = ffmpeg_process.wait()
130
140
 
141
+ logger.debug("Process return codes - FFmpeg: %d, Opus: %d", ffmpeg_return, opusenc_return)
142
+
131
143
  if ffmpeg_return != 0:
132
144
  logger.error("FFmpeg processing failed with return code %d", ffmpeg_return)
133
145
  raise RuntimeError(f"FFmpeg processing failed with return code {ffmpeg_return}")
@@ -139,6 +151,7 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
139
151
  logger.debug("Wrote %d bytes to temporary file", bytes_written)
140
152
  tmp_file.seek(0)
141
153
 
154
+ logger.trace("Exiting get_opus_tempfile() with in-memory temporary file")
142
155
  return tmp_file, None
143
156
 
144
157
 
@@ -152,6 +165,7 @@ def filter_directories(glob_list):
152
165
  Returns:
153
166
  list: Filtered list containing only supported audio files
154
167
  """
168
+ logger.trace("Entering filter_directories() with %d items", len(glob_list))
155
169
  logger.debug("Filtering %d glob results for supported audio files", len(glob_list))
156
170
 
157
171
  # Common audio file extensions supported by ffmpeg
@@ -171,6 +185,7 @@ def filter_directories(glob_list):
171
185
  logger.trace("Skipping unsupported file: %s", name)
172
186
 
173
187
  logger.debug("Found %d supported audio files after filtering", len(filtered))
188
+ logger.trace("Exiting filter_directories() with %d files", len(filtered))
174
189
  return filtered
175
190
 
176
191
 
@@ -186,6 +201,7 @@ def get_input_files(input_filename):
186
201
  Returns:
187
202
  list: List of input file paths
188
203
  """
204
+ logger.trace("Entering get_input_files(input_filename=%s)", input_filename)
189
205
  logger.debug("Getting input files for pattern: %s", input_filename)
190
206
 
191
207
  if input_filename.endswith(".lst"):
@@ -196,6 +212,7 @@ def get_input_files(input_filename):
196
212
  for line_num, line in enumerate(file_list, 1):
197
213
  fname = line.strip()
198
214
  if not fname or fname.startswith('#'): # Skip empty lines and comments
215
+ logger.trace("Skipping empty line or comment at line %d", line_num)
199
216
  continue
200
217
 
201
218
  # Remove any quote characters from path
@@ -204,10 +221,10 @@ def get_input_files(input_filename):
204
221
  # Check if the path is absolute or has a drive letter (Windows)
205
222
  if os.path.isabs(fname) or (len(fname) > 1 and fname[1] == ':'):
206
223
  full_path = fname # Use as is if it's an absolute path
207
- logger.trace("Using absolute path from list: %s", full_path)
224
+ logger.trace("Using absolute path from line %d: %s", line_num, full_path)
208
225
  else:
209
226
  full_path = os.path.join(list_dir, fname)
210
- logger.trace("Using relative path from list: %s", full_path)
227
+ logger.trace("Using relative path from line %d: %s -> %s", line_num, fname, full_path)
211
228
 
212
229
  # Handle directory paths by finding all audio files in the directory
213
230
  if os.path.isdir(full_path):
@@ -216,11 +233,12 @@ def get_input_files(input_filename):
216
233
  dir_files = sorted(filter_directories(glob.glob(dir_glob)))
217
234
  if dir_files:
218
235
  input_files.extend(dir_files)
219
- logger.debug("Found %d audio files in directory", len(dir_files))
236
+ logger.debug("Found %d audio files in directory from line %d", len(dir_files), line_num)
220
237
  else:
221
238
  logger.warning("No audio files found in directory at line %d: %s", line_num, full_path)
222
239
  elif os.path.isfile(full_path):
223
240
  input_files.append(full_path)
241
+ logger.trace("Added file from line %d: %s", line_num, full_path)
224
242
  else:
225
243
  logger.warning("File not found at line %d: %s", line_num, full_path)
226
244
 
@@ -230,6 +248,7 @@ def get_input_files(input_filename):
230
248
  input_files = sorted(filter_directories(glob.glob(input_filename)))
231
249
  logger.debug("Found %d files matching pattern", len(input_files))
232
250
 
251
+ logger.trace("Exiting get_input_files() with %d files", len(input_files))
233
252
  return input_files
234
253
 
235
254
 
@@ -244,13 +263,16 @@ def append_to_filename(output_filename, tag):
244
263
  Returns:
245
264
  str: Modified filename with tag
246
265
  """
266
+ logger.trace("Entering append_to_filename(output_filename=%s, tag=%s)", output_filename, tag)
247
267
  logger.debug("Appending tag '%s' to filename: %s", tag, output_filename)
248
268
  pos = output_filename.rfind('.')
249
269
  if pos == -1:
250
270
  result = f"{output_filename}_{tag}"
251
271
  logger.debug("No extension found, result: %s", result)
272
+ logger.trace("Exiting append_to_filename() with result=%s", result)
252
273
  return result
253
274
  else:
254
275
  result = f"{output_filename[:pos]}_{tag}{output_filename[pos:]}"
255
276
  logger.debug("Extension found, result: %s", result)
277
+ logger.trace("Exiting append_to_filename() with result=%s", result)
256
278
  return result