TonieToolbox 0.2.2__py3-none-any.whl → 0.3.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.2'
5
+ __version__ = '0.3.0'
TonieToolbox/__main__.py CHANGED
@@ -17,13 +17,41 @@ from .logger import setup_logging, get_logger
17
17
  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
+ 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
20
22
 
21
23
  def main():
22
24
  """Entry point for the TonieToolbox application."""
23
25
  parser = argparse.ArgumentParser(description='Create Tonie compatible file from Ogg opus file(s).')
24
26
  parser.add_argument('-v', '--version', action='version', version=f'TonieToolbox {__version__}',
25
27
  help='show program version and exit')
26
- parser.add_argument('input_filename', metavar='SOURCE', type=str,
28
+
29
+ # TeddyCloud options first to check for existence before requiring SOURCE
30
+ teddycloud_group = parser.add_argument_group('TeddyCloud Options')
31
+ teddycloud_group.add_argument('--upload', metavar='URL', action='store',
32
+ help='Upload to TeddyCloud instance (e.g., https://teddycloud.example.com). Supports .taf, .jpg, .jpeg, .png files.')
33
+ teddycloud_group.add_argument('--include-artwork', action='store_true',
34
+ help='Upload cover artwork image alongside the Tonie file when using --upload')
35
+ teddycloud_group.add_argument('--get-tags', action='store', metavar='URL',
36
+ help='Get available tags from TeddyCloud instance')
37
+ teddycloud_group.add_argument('--ignore-ssl-verify', action='store_true',
38
+ help='Ignore SSL certificate verification (for self-signed certificates)')
39
+ teddycloud_group.add_argument('--special-folder', action='store', metavar='FOLDER',
40
+ help='Special folder to upload to (currently only "library" is supported)', default='library')
41
+ teddycloud_group.add_argument('--path', action='store', metavar='PATH',
42
+ help='Path where to write the file on TeddyCloud server')
43
+ teddycloud_group.add_argument('--show-progress', action='store_true', default=True,
44
+ help='Show progress bar during file upload (default: enabled)')
45
+ teddycloud_group.add_argument('--connection-timeout', type=int, metavar='SECONDS', default=10,
46
+ help='Connection timeout in seconds (default: 10)')
47
+ teddycloud_group.add_argument('--read-timeout', type=int, metavar='SECONDS', default=300,
48
+ help='Read timeout in seconds (default: 300)')
49
+ teddycloud_group.add_argument('--max-retries', type=int, metavar='RETRIES', default=3,
50
+ help='Maximum number of retry attempts (default: 3)')
51
+ teddycloud_group.add_argument('--retry-delay', type=int, metavar='SECONDS', default=5,
52
+ help='Delay between retry attempts in seconds (default: 5)')
53
+
54
+ parser.add_argument('input_filename', metavar='SOURCE', type=str, nargs='?',
27
55
  help='input file or directory or a file list (.lst)')
28
56
  parser.add_argument('output_filename', metavar='TARGET', nargs='?', type=str,
29
57
  help='the output file name (default: ---ID---)')
@@ -52,6 +80,15 @@ def main():
52
80
  parser.add_argument('-D', '--detailed-compare', action='store_true',
53
81
  help='Show detailed OGG page differences when comparing files')
54
82
 
83
+ # Media tag options
84
+ media_tag_group = parser.add_argument_group('Media Tag Options')
85
+ media_tag_group.add_argument('-m', '--use-media-tags', action='store_true',
86
+ help='Use media tags from audio files for naming')
87
+ media_tag_group.add_argument('--name-template', metavar='TEMPLATE', action='store',
88
+ help='Template for naming files using media tags. Example: "{album} - {artist}"')
89
+ media_tag_group.add_argument('--show-tags', action='store_true',
90
+ help='Show available media tags from input files')
91
+
55
92
  # Version check options
56
93
  version_group = parser.add_argument_group('Version Check Options')
57
94
  version_group.add_argument('-S', '--skip-update-check', action='store_true',
@@ -69,6 +106,11 @@ def main():
69
106
  log_level_group.add_argument('-Q', '--silent', action='store_true', help='Show only errors')
70
107
 
71
108
  args = parser.parse_args()
109
+
110
+ # Validate that input_filename is provided if not using --get-tags or --upload-existing
111
+ if args.input_filename is None and not (args.get_tags or args.upload):
112
+ parser.error("the following arguments are required: SOURCE")
113
+
72
114
  if args.trace:
73
115
  from .logger import TRACE
74
116
  log_level = TRACE
@@ -80,19 +122,18 @@ def main():
80
122
  log_level = logging.ERROR
81
123
  else:
82
124
  log_level = logging.INFO
83
-
125
+
84
126
  setup_logging(log_level)
85
127
  logger = get_logger('main')
86
128
  logger.debug("Starting TonieToolbox v%s with log level: %s", __version__, logging.getLevelName(log_level))
87
129
 
88
- # Handle version cache operations
130
+
89
131
  if args.clear_version_cache:
90
132
  if clear_version_cache():
91
133
  logger.info("Version cache cleared successfully")
92
134
  else:
93
135
  logger.info("No version cache to clear or error clearing cache")
94
136
 
95
- # Check for updates
96
137
  if not args.skip_update_check:
97
138
  logger.debug("Checking for updates (force_refresh=%s)", args.force_refresh_cache)
98
139
  is_latest, latest_version, message, update_confirmed = check_for_updates(
@@ -103,6 +144,59 @@ def main():
103
144
  if not is_latest and not update_confirmed and not (args.silent or args.quiet):
104
145
  logger.info("Update available but user chose to continue without updating.")
105
146
 
147
+ # Handle get-tags from TeddyCloud if requested
148
+ if args.get_tags:
149
+ teddycloud_url = args.get_tags
150
+ success = get_tags_from_teddycloud(teddycloud_url, args.ignore_ssl_verify)
151
+ sys.exit(0 if success else 1)
152
+
153
+ # Handle upload to TeddyCloud if requested
154
+ if args.upload:
155
+ teddycloud_url = args.upload
156
+
157
+ if not args.input_filename:
158
+ logger.error("Missing input file for --upload. Provide a file path as SOURCE argument.")
159
+ sys.exit(1)
160
+
161
+ # Check if the input file is already a .taf file or an image file
162
+ if os.path.exists(args.input_filename) and (args.input_filename.lower().endswith('.taf') or
163
+ args.input_filename.lower().endswith(('.jpg', '.jpeg', '.png'))):
164
+ # Direct upload of existing TAF or image file
165
+ # Use get_file_paths to handle Windows backslashes and resolve the paths correctly
166
+ file_paths = get_file_paths(args.input_filename)
167
+
168
+ if not file_paths:
169
+ logger.error("No files found for pattern %s", args.input_filename)
170
+ sys.exit(1)
171
+
172
+ logger.info("Found %d file(s) to upload to TeddyCloud %s", len(file_paths), teddycloud_url)
173
+
174
+ for file_path in file_paths:
175
+ # Only upload supported file types
176
+ if not file_path.lower().endswith(('.taf', '.jpg', '.jpeg', '.png')):
177
+ logger.warning("Skipping unsupported file type: %s", file_path)
178
+ continue
179
+
180
+ logger.info("Uploading %s to TeddyCloud %s", file_path, teddycloud_url)
181
+ upload_success = upload_to_teddycloud(
182
+ file_path, teddycloud_url, args.ignore_ssl_verify,
183
+ args.special_folder, args.path, args.show_progress,
184
+ args.connection_timeout, args.read_timeout,
185
+ args.max_retries, args.retry_delay
186
+ )
187
+
188
+ if not upload_success:
189
+ logger.error("Failed to upload %s to TeddyCloud", file_path)
190
+ sys.exit(1)
191
+ else:
192
+ logger.info("Successfully uploaded %s to TeddyCloud", file_path)
193
+
194
+ sys.exit(0)
195
+
196
+ # If we get here, it's not a TAF or image file, so continue with normal processing
197
+ # which will convert the input files and upload the result later
198
+ pass
199
+
106
200
  ffmpeg_binary = args.ffmpeg
107
201
  if ffmpeg_binary is None:
108
202
  ffmpeg_binary = get_ffmpeg_binary(args.auto_download)
@@ -119,10 +213,24 @@ def main():
119
213
  sys.exit(1)
120
214
  logger.debug("Using opusenc binary: %s", opus_binary)
121
215
 
216
+ # Check for media tags library and handle --show-tags option
217
+ if (args.use_media_tags or args.show_tags or args.name_template) and not is_media_tags_available():
218
+ if not ensure_mutagen(auto_install=args.auto_download):
219
+ logger.warning("Media tags functionality requires the mutagen library but it could not be installed.")
220
+ if args.use_media_tags or args.show_tags:
221
+ logger.error("Cannot proceed with --use-media-tags or --show-tags without mutagen library")
222
+ sys.exit(1)
223
+ else:
224
+ logger.info("Successfully enabled media tag support")
225
+
122
226
  # Handle recursive processing
123
227
  if args.recursive:
124
228
  logger.info("Processing folders recursively: %s", args.input_filename)
125
- process_tasks = process_recursive_folders(args.input_filename)
229
+ process_tasks = process_recursive_folders(
230
+ args.input_filename,
231
+ use_media_tags=args.use_media_tags,
232
+ name_template=args.name_template
233
+ )
126
234
 
127
235
  if not process_tasks:
128
236
  logger.error("No folders with audio files found for recursive processing")
@@ -134,6 +242,7 @@ def main():
134
242
  os.makedirs(output_dir, exist_ok=True)
135
243
  logger.debug("Created output directory: %s", output_dir)
136
244
 
245
+ created_files = []
137
246
  for task_index, (output_name, folder_path, audio_files) in enumerate(process_tasks):
138
247
  if args.output_to_source:
139
248
  task_out_filename = os.path.join(folder_path, f"{output_name}.taf")
@@ -147,8 +256,102 @@ def main():
147
256
  args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
148
257
  args.auto_download, not args.use_legacy_tags)
149
258
  logger.info("Successfully created Tonie file: %s", task_out_filename)
259
+ created_files.append(task_out_filename)
150
260
 
151
261
  logger.info("Recursive processing completed. Created %d Tonie files.", len(process_tasks))
262
+
263
+ # Handle upload to TeddyCloud if requested
264
+ if args.upload and created_files:
265
+ teddycloud_url = args.upload
266
+
267
+ for taf_file in created_files:
268
+ upload_success = upload_to_teddycloud(
269
+ taf_file, teddycloud_url, args.ignore_ssl_verify,
270
+ args.special_folder, args.path, args.show_progress,
271
+ args.connection_timeout, args.read_timeout,
272
+ args.max_retries, args.retry_delay
273
+ )
274
+
275
+ if not upload_success:
276
+ logger.error("Failed to upload %s to TeddyCloud", taf_file)
277
+ else:
278
+ logger.info("Successfully uploaded %s to TeddyCloud", taf_file)
279
+
280
+ # Handle artwork upload if requested
281
+ if args.include_artwork:
282
+ # Extract folder path from the current task
283
+ folder_path = os.path.dirname(taf_file)
284
+ taf_file_basename = os.path.basename(taf_file)
285
+ taf_name = os.path.splitext(taf_file_basename)[0] # Get name without extension
286
+ logger.info("Looking for artwork for %s", folder_path)
287
+
288
+ # Try to find cover image in the folder
289
+ from .media_tags import find_cover_image
290
+ artwork_path = find_cover_image(folder_path)
291
+ temp_artwork = None
292
+
293
+ # If no cover image found, try to extract it from one of the audio files
294
+ if not artwork_path:
295
+ # Get current task's audio files
296
+ for task_name, task_folder, task_files in process_tasks:
297
+ if task_folder == folder_path or os.path.normpath(task_folder) == os.path.normpath(folder_path):
298
+ if task_files and len(task_files) > 0:
299
+ # Try to extract from first file
300
+ from .media_tags import extract_artwork, ensure_mutagen
301
+ if ensure_mutagen(auto_install=args.auto_download):
302
+ temp_artwork = extract_artwork(task_files[0])
303
+ if temp_artwork:
304
+ artwork_path = temp_artwork
305
+ break
306
+
307
+ if artwork_path:
308
+ logger.info("Found artwork for %s: %s", folder_path, artwork_path)
309
+ artwork_upload_path = "/custom_img"
310
+ artwork_ext = os.path.splitext(artwork_path)[1]
311
+
312
+ # Create a temporary copy with the same name as the taf file
313
+ import shutil
314
+ renamed_artwork_path = None
315
+ try:
316
+ renamed_artwork_path = os.path.join(os.path.dirname(artwork_path),
317
+ f"{taf_name}{artwork_ext}")
318
+
319
+ if renamed_artwork_path != artwork_path:
320
+ shutil.copy2(artwork_path, renamed_artwork_path)
321
+ logger.debug("Created renamed artwork copy: %s", renamed_artwork_path)
322
+
323
+ logger.info("Uploading artwork to path: %s as %s%s",
324
+ artwork_upload_path, taf_name, artwork_ext)
325
+
326
+ artwork_upload_success = upload_to_teddycloud(
327
+ renamed_artwork_path, teddycloud_url, args.ignore_ssl_verify,
328
+ args.special_folder, artwork_upload_path, args.show_progress,
329
+ args.connection_timeout, args.read_timeout,
330
+ args.max_retries, args.retry_delay
331
+ )
332
+
333
+ if artwork_upload_success:
334
+ logger.info("Successfully uploaded artwork for %s", folder_path)
335
+ else:
336
+ logger.warning("Failed to upload artwork for %s", folder_path)
337
+
338
+ if renamed_artwork_path != artwork_path and os.path.exists(renamed_artwork_path):
339
+ try:
340
+ os.unlink(renamed_artwork_path)
341
+ logger.debug("Removed temporary renamed artwork file: %s", renamed_artwork_path)
342
+ except Exception as e:
343
+ logger.debug("Failed to remove temporary renamed artwork file: %s", e)
344
+
345
+ if temp_artwork and os.path.exists(temp_artwork) and temp_artwork != renamed_artwork_path:
346
+ try:
347
+ os.unlink(temp_artwork)
348
+ logger.debug("Removed temporary artwork file: %s", temp_artwork)
349
+ except Exception as e:
350
+ logger.debug("Failed to remove temporary artwork file: %s", e)
351
+ except Exception as e:
352
+ logger.error("Error during artwork renaming or upload: %s", e)
353
+ else:
354
+ logger.warning("No artwork found for %s", folder_path)
152
355
  sys.exit(0)
153
356
 
154
357
  # Handle directory or file input
@@ -178,8 +381,103 @@ def main():
178
381
  logger.error("No files found for pattern %s", args.input_filename)
179
382
  sys.exit(1)
180
383
 
384
+ # Show tags for input files if requested
385
+ if args.show_tags:
386
+ from .media_tags import get_file_tags
387
+ logger.info("Showing media tags for input files:")
388
+
389
+ for file_index, file_path in enumerate(files):
390
+ tags = get_file_tags(file_path)
391
+ if tags:
392
+ print(f"\nFile {file_index + 1}: {os.path.basename(file_path)}")
393
+ print("-" * 40)
394
+ for tag_name, tag_value in sorted(tags.items()):
395
+ print(f"{tag_name}: {tag_value}")
396
+ else:
397
+ print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
398
+ sys.exit(0)
399
+
400
+ # Use media tags for file naming if requested
401
+ guessed_name = None
402
+ if args.use_media_tags:
403
+ # If this is a single folder, try to get consistent album info
404
+ if len(files) > 1 and os.path.dirname(files[0]) == os.path.dirname(files[-1]):
405
+ folder_path = os.path.dirname(files[0])
406
+
407
+ from .media_tags import extract_album_info, format_metadata_filename
408
+ logger.debug("Extracting album info from folder: %s", folder_path)
409
+
410
+ album_info = extract_album_info(folder_path)
411
+ if album_info:
412
+ # Use album info for naming the output file
413
+ template = args.name_template or "{album} - {artist}"
414
+ new_name = format_metadata_filename(album_info, template)
415
+
416
+ if new_name:
417
+ logger.info("Using album metadata for output filename: %s", new_name)
418
+ guessed_name = new_name
419
+ else:
420
+ logger.debug("Could not format filename from album metadata")
421
+
422
+ # For single files, use the file's metadata
423
+ elif len(files) == 1:
424
+ from .media_tags import get_file_tags, format_metadata_filename
425
+
426
+ tags = get_file_tags(files[0])
427
+ if tags:
428
+ template = args.name_template or "{title} - {artist}"
429
+ new_name = format_metadata_filename(tags, template)
430
+
431
+ if new_name:
432
+ logger.info("Using file metadata for output filename: %s", new_name)
433
+ guessed_name = new_name
434
+ else:
435
+ logger.debug("Could not format filename from file metadata")
436
+
437
+ # For multiple files from different folders, try to use common tags if they exist
438
+ elif len(files) > 1:
439
+ from .media_tags import get_file_tags, format_metadata_filename
440
+
441
+ # Try to find common tags among files
442
+ common_tags = {}
443
+ for file_path in files:
444
+ tags = get_file_tags(file_path)
445
+ if tags:
446
+ for key, value in tags.items():
447
+ if key in ['album', 'albumartist', 'artist']:
448
+ if key not in common_tags:
449
+ common_tags[key] = value
450
+ # Only keep values that are the same across files
451
+ elif common_tags[key] != value:
452
+ common_tags[key] = None
453
+
454
+ # Remove None values
455
+ common_tags = {k: v for k, v in common_tags.items() if v is not None}
456
+
457
+ if common_tags:
458
+ template = args.name_template or "Collection - {album}" if 'album' in common_tags else "Collection"
459
+ new_name = format_metadata_filename(common_tags, template)
460
+
461
+ if new_name:
462
+ logger.info("Using common metadata for output filename: %s", new_name)
463
+ guessed_name = new_name
464
+ else:
465
+ logger.debug("Could not format filename from common metadata")
466
+
181
467
  if args.output_filename:
182
468
  out_filename = args.output_filename
469
+ elif guessed_name:
470
+ if args.output_to_source:
471
+ source_dir = os.path.dirname(files[0]) if files else '.'
472
+ out_filename = os.path.join(source_dir, guessed_name)
473
+ logger.debug("Using source location for output with media tags: %s", out_filename)
474
+ else:
475
+ output_dir = './output'
476
+ if not os.path.exists(output_dir):
477
+ logger.debug("Creating default output directory: %s", output_dir)
478
+ os.makedirs(output_dir, exist_ok=True)
479
+ out_filename = os.path.join(output_dir, guessed_name)
480
+ logger.debug("Using default output location with media tags: %s", out_filename)
183
481
  else:
184
482
  guessed_name = guess_output_filename(args.input_filename, files)
185
483
  if args.output_to_source:
@@ -206,13 +504,115 @@ def main():
206
504
 
207
505
  if not out_filename.lower().endswith('.taf'):
208
506
  out_filename += '.taf'
209
-
507
+
210
508
  logger.info("Creating Tonie file: %s with %d input file(s)", out_filename, len(files))
211
509
  create_tonie_file(out_filename, files, args.no_tonie_header, args.user_timestamp,
212
510
  args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
213
511
  args.auto_download, not args.use_legacy_tags)
214
512
  logger.info("Successfully created Tonie file: %s", out_filename)
215
-
513
+
514
+ # Handle upload to TeddyCloud if requested
515
+ if args.upload:
516
+ teddycloud_url = args.upload
517
+
518
+ upload_success = upload_to_teddycloud(
519
+ out_filename, teddycloud_url, args.ignore_ssl_verify,
520
+ args.special_folder, args.path, args.show_progress,
521
+ args.connection_timeout, args.read_timeout,
522
+ args.max_retries, args.retry_delay
523
+ )
524
+ if not upload_success:
525
+ logger.error("Failed to upload %s to TeddyCloud", out_filename)
526
+ sys.exit(1)
527
+ else:
528
+ logger.info("Successfully uploaded %s to TeddyCloud", out_filename)
529
+
530
+ # Handle artwork upload if requested
531
+ if args.include_artwork:
532
+ logger.info("Looking for artwork to upload alongside the Tonie file")
533
+ artwork_path = None
534
+
535
+ # Try to find a cover image in the source directory first
536
+ source_dir = os.path.dirname(files[0]) if files else None
537
+ if source_dir:
538
+ from .media_tags import find_cover_image
539
+ artwork_path = find_cover_image(source_dir)
540
+
541
+ # If no cover in source directory, try to extract it from audio file
542
+ if not artwork_path and len(files) > 0:
543
+ from .media_tags import extract_artwork, ensure_mutagen
544
+
545
+ # Make sure mutagen is available for artwork extraction
546
+ if ensure_mutagen(auto_install=args.auto_download):
547
+ # Try to extract artwork from the first file
548
+ temp_artwork = extract_artwork(files[0])
549
+ if temp_artwork:
550
+ artwork_path = temp_artwork
551
+ # Note: this creates a temporary file that will be deleted after upload
552
+
553
+ # Upload the artwork if found
554
+ if artwork_path:
555
+ logger.info("Found artwork: %s", artwork_path)
556
+
557
+ # Create artwork upload path - keep same path but use "custom_img" folder
558
+ artwork_upload_path = args.path
559
+ if not artwork_upload_path:
560
+ artwork_upload_path = "/custom_img"
561
+ elif not artwork_upload_path.startswith("/custom_img"):
562
+ # Make sure we're using the custom_img folder
563
+ if artwork_upload_path.startswith("/"):
564
+ artwork_upload_path = "/custom_img" + artwork_upload_path
565
+ else:
566
+ artwork_upload_path = "/custom_img/" + artwork_upload_path
567
+
568
+ # Get the original artwork file extension
569
+ artwork_ext = os.path.splitext(artwork_path)[1]
570
+
571
+ # Create a temporary copy with the same name as the taf file
572
+ import shutil
573
+ renamed_artwork_path = None
574
+ try:
575
+ renamed_artwork_path = os.path.join(os.path.dirname(artwork_path),
576
+ f"{os.path.splitext(os.path.basename(out_filename))[0]}{artwork_ext}")
577
+
578
+ if renamed_artwork_path != artwork_path:
579
+ shutil.copy2(artwork_path, renamed_artwork_path)
580
+ logger.debug("Created renamed artwork copy: %s", renamed_artwork_path)
581
+
582
+ logger.info("Uploading artwork to path: %s as %s%s",
583
+ artwork_upload_path, os.path.splitext(os.path.basename(out_filename))[0], artwork_ext)
584
+
585
+ artwork_upload_success = upload_to_teddycloud(
586
+ renamed_artwork_path, teddycloud_url, args.ignore_ssl_verify,
587
+ args.special_folder, artwork_upload_path, args.show_progress,
588
+ args.connection_timeout, args.read_timeout,
589
+ args.max_retries, args.retry_delay
590
+ )
591
+
592
+ if artwork_upload_success:
593
+ logger.info("Successfully uploaded artwork")
594
+ else:
595
+ logger.warning("Failed to upload artwork")
596
+
597
+ # Clean up temporary renamed file
598
+ if renamed_artwork_path != artwork_path and os.path.exists(renamed_artwork_path):
599
+ try:
600
+ os.unlink(renamed_artwork_path)
601
+ logger.debug("Removed temporary renamed artwork file: %s", renamed_artwork_path)
602
+ except Exception as e:
603
+ logger.debug("Failed to remove temporary renamed artwork file: %s", e)
604
+
605
+ # Clean up temporary extracted artwork file if needed
606
+ if temp_artwork and os.path.exists(temp_artwork) and temp_artwork != renamed_artwork_path:
607
+ try:
608
+ os.unlink(temp_artwork)
609
+ logger.debug("Removed temporary artwork file: %s", temp_artwork)
610
+ except Exception as e:
611
+ logger.debug("Failed to remove temporary artwork file: %s", e)
612
+ except Exception as e:
613
+ logger.error("Error during artwork renaming or upload: %s", e)
614
+ else:
615
+ logger.warning("No artwork found to upload")
216
616
 
217
617
  if __name__ == "__main__":
218
618
  main()
@@ -52,6 +52,10 @@ DEPENDENCIES = {
52
52
  'darwin': {
53
53
  'package': 'opus-tools'
54
54
  }
55
+ },
56
+ 'mutagen': {
57
+ 'package': 'mutagen',
58
+ 'python_package': True
55
59
  }
56
60
  }
57
61
 
@@ -365,6 +369,92 @@ def install_package(package_name):
365
369
  logger.error("Failed to install %s: %s", package_name, e)
366
370
  return False
367
371
 
372
+ def install_python_package(package_name):
373
+ """
374
+ Attempt to install a Python package using pip.
375
+
376
+ Args:
377
+ package_name (str): Name of the package to install
378
+
379
+ Returns:
380
+ bool: True if installation was successful, False otherwise
381
+ """
382
+ logger.info("Attempting to install Python package: %s", package_name)
383
+ try:
384
+ import subprocess
385
+ import sys
386
+
387
+ # Try to install the package using pip
388
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
389
+ logger.info("Successfully installed Python package: %s", package_name)
390
+ return True
391
+ except Exception as e:
392
+ logger.error("Failed to install Python package %s: %s", package_name, str(e))
393
+ return False
394
+
395
+ def check_python_package(package_name):
396
+ """
397
+ Check if a Python package is installed.
398
+
399
+ Args:
400
+ package_name (str): Name of the package to check
401
+
402
+ Returns:
403
+ bool: True if the package is installed, False otherwise
404
+ """
405
+ logger.debug("Checking if Python package is installed: %s", package_name)
406
+ try:
407
+ __import__(package_name)
408
+ logger.debug("Python package %s is installed", package_name)
409
+ return True
410
+ except ImportError:
411
+ logger.debug("Python package %s is not installed", package_name)
412
+ return False
413
+
414
+ def ensure_mutagen(auto_install=True):
415
+ """
416
+ Ensure that the Mutagen library is available, installing it if necessary and allowed.
417
+
418
+ Args:
419
+ auto_install (bool): Whether to automatically install Mutagen if not found (defaults to True)
420
+
421
+ Returns:
422
+ bool: True if Mutagen is available, False otherwise
423
+ """
424
+ logger.debug("Checking if Mutagen is available")
425
+
426
+ try:
427
+ import mutagen
428
+ logger.debug("Mutagen is already installed")
429
+ return True
430
+ except ImportError:
431
+ logger.debug("Mutagen is not installed")
432
+
433
+ if auto_install:
434
+ logger.info("Auto-install enabled, attempting to install Mutagen")
435
+ if install_python_package('mutagen'):
436
+ try:
437
+ import mutagen
438
+ logger.info("Successfully installed and imported Mutagen")
439
+ return True
440
+ except ImportError:
441
+ logger.error("Mutagen was installed but could not be imported")
442
+ else:
443
+ logger.error("Failed to install Mutagen")
444
+ else:
445
+ logger.warning("Mutagen is not installed and --auto-download is not used.")
446
+
447
+ return False
448
+
449
+ def is_mutagen_available():
450
+ """
451
+ Check if the Mutagen library is available.
452
+
453
+ Returns:
454
+ bool: True if Mutagen is available, False otherwise
455
+ """
456
+ return check_python_package('mutagen')
457
+
368
458
  def ensure_dependency(dependency_name, auto_download=False):
369
459
  """
370
460
  Ensure that a dependency is available, downloading it if necessary.
@@ -376,7 +466,7 @@ def ensure_dependency(dependency_name, auto_download=False):
376
466
  Returns:
377
467
  str: Path to the binary if available, None otherwise
378
468
  """
379
- logger.info("Ensuring dependency: %s", dependency_name)
469
+ logger.debug("Ensuring dependency: %s", dependency_name)
380
470
  system = get_system()
381
471
 
382
472
  if system not in ['windows', 'linux', 'darwin']:
@@ -406,7 +496,7 @@ def ensure_dependency(dependency_name, auto_download=False):
406
496
  existing_binary = find_binary_in_extracted_dir(dependency_dir, binary_path)
407
497
  if existing_binary and os.path.exists(existing_binary):
408
498
  # Verify that the binary works
409
- logger.info("Found previously downloaded %s: %s", dependency_name, existing_binary)
499
+ logger.debug("Found previously downloaded %s: %s", dependency_name, existing_binary)
410
500
  try:
411
501
  if os.access(existing_binary, os.X_OK) or system == 'windows':
412
502
  if system in ['linux', 'darwin']:
@@ -419,14 +509,14 @@ def ensure_dependency(dependency_name, auto_download=False):
419
509
  try:
420
510
  result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5)
421
511
  if result.returncode == 0:
422
- logger.info("Using previously downloaded %s: %s", dependency_name, existing_binary)
512
+ logger.debug("Using previously downloaded %s: %s", dependency_name, existing_binary)
423
513
  return existing_binary
424
514
  except:
425
515
  # If --version fails, try without arguments
426
516
  try:
427
517
  result = subprocess.run([existing_binary], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5)
428
518
  if result.returncode == 0:
429
- logger.info("Using previously downloaded %s: %s", dependency_name, existing_binary)
519
+ logger.debug("Using previously downloaded %s: %s", dependency_name, existing_binary)
430
520
  return existing_binary
431
521
  except:
432
522
  pass
@@ -435,7 +525,7 @@ def ensure_dependency(dependency_name, auto_download=False):
435
525
  try:
436
526
  result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5)
437
527
  if result.returncode == 0:
438
- logger.info("Using previously downloaded %s: %s", dependency_name, existing_binary)
528
+ logger.debug("Using previously downloaded %s: %s", dependency_name, existing_binary)
439
529
  return existing_binary
440
530
  except:
441
531
  pass