TonieToolbox 0.2.1__py3-none-any.whl → 0.2.3__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.1'
5
+ __version__ = '0.2.3'
TonieToolbox/__main__.py CHANGED
@@ -17,6 +17,7 @@ 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
20
21
 
21
22
  def main():
22
23
  """Entry point for the TonieToolbox application."""
@@ -45,11 +46,22 @@ def main():
45
46
  parser.add_argument('-A', '--auto-download', action='store_true', help='Automatically download FFmpeg and opusenc if needed')
46
47
  parser.add_argument('-k', '--keep-temp', action='store_true',
47
48
  help='Keep temporary opus files in a temp folder for testing')
49
+ parser.add_argument('-u', '--use-legacy-tags', action='store_true',
50
+ help='Use legacy hardcoded tags instead of dynamic TonieToolbox tags')
48
51
  parser.add_argument('-C', '--compare', action='store', metavar='FILE2',
49
52
  help='Compare input file with another .taf file for debugging')
50
53
  parser.add_argument('-D', '--detailed-compare', action='store_true',
51
54
  help='Show detailed OGG page differences when comparing files')
52
55
 
56
+ # Media tag options
57
+ media_tag_group = parser.add_argument_group('Media Tag Options')
58
+ media_tag_group.add_argument('-m', '--use-media-tags', action='store_true',
59
+ help='Use media tags from audio files for naming')
60
+ media_tag_group.add_argument('--name-template', metavar='TEMPLATE', action='store',
61
+ help='Template for naming files using media tags. Example: "{album} - {artist}"')
62
+ media_tag_group.add_argument('--show-tags', action='store_true',
63
+ help='Show available media tags from input files')
64
+
53
65
  # Version check options
54
66
  version_group = parser.add_argument_group('Version Check Options')
55
67
  version_group.add_argument('-S', '--skip-update-check', action='store_true',
@@ -83,14 +95,12 @@ def main():
83
95
  logger = get_logger('main')
84
96
  logger.debug("Starting TonieToolbox v%s with log level: %s", __version__, logging.getLevelName(log_level))
85
97
 
86
- # Handle version cache operations
87
98
  if args.clear_version_cache:
88
99
  if clear_version_cache():
89
100
  logger.info("Version cache cleared successfully")
90
101
  else:
91
102
  logger.info("No version cache to clear or error clearing cache")
92
103
 
93
- # Check for updates
94
104
  if not args.skip_update_check:
95
105
  logger.debug("Checking for updates (force_refresh=%s)", args.force_refresh_cache)
96
106
  is_latest, latest_version, message, update_confirmed = check_for_updates(
@@ -117,10 +127,24 @@ def main():
117
127
  sys.exit(1)
118
128
  logger.debug("Using opusenc binary: %s", opus_binary)
119
129
 
130
+ # Check for media tags library and handle --show-tags option
131
+ if (args.use_media_tags or args.show_tags or args.name_template) and not is_media_tags_available():
132
+ if not ensure_mutagen(auto_install=args.auto_download):
133
+ logger.warning("Media tags functionality requires the mutagen library but it could not be installed.")
134
+ if args.use_media_tags or args.show_tags:
135
+ logger.error("Cannot proceed with --use-media-tags or --show-tags without mutagen library")
136
+ sys.exit(1)
137
+ else:
138
+ logger.info("Successfully enabled media tag support")
139
+
120
140
  # Handle recursive processing
121
141
  if args.recursive:
122
142
  logger.info("Processing folders recursively: %s", args.input_filename)
123
- process_tasks = process_recursive_folders(args.input_filename)
143
+ process_tasks = process_recursive_folders(
144
+ args.input_filename,
145
+ use_media_tags=args.use_media_tags,
146
+ name_template=args.name_template
147
+ )
124
148
 
125
149
  if not process_tasks:
126
150
  logger.error("No folders with audio files found for recursive processing")
@@ -143,7 +167,7 @@ def main():
143
167
 
144
168
  create_tonie_file(task_out_filename, audio_files, args.no_tonie_header, args.user_timestamp,
145
169
  args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
146
- args.auto_download)
170
+ args.auto_download, not args.use_legacy_tags)
147
171
  logger.info("Successfully created Tonie file: %s", task_out_filename)
148
172
 
149
173
  logger.info("Recursive processing completed. Created %d Tonie files.", len(process_tasks))
@@ -176,8 +200,104 @@ def main():
176
200
  logger.error("No files found for pattern %s", args.input_filename)
177
201
  sys.exit(1)
178
202
 
203
+ # Show tags for input files if requested
204
+ if args.show_tags:
205
+ from .media_tags import get_file_tags
206
+ logger.info("Showing media tags for input files:")
207
+
208
+ for file_index, file_path in enumerate(files):
209
+ tags = get_file_tags(file_path)
210
+ if tags:
211
+ print(f"\nFile {file_index + 1}: {os.path.basename(file_path)}")
212
+ print("-" * 40)
213
+ for tag_name, tag_value in sorted(tags.items()):
214
+ print(f"{tag_name}: {tag_value}")
215
+ else:
216
+ print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
217
+
218
+ sys.exit(0)
219
+
220
+ # Use media tags for file naming if requested
221
+ guessed_name = None
222
+ if args.use_media_tags:
223
+ # If this is a single folder, try to get consistent album info
224
+ if len(files) > 1 and os.path.dirname(files[0]) == os.path.dirname(files[-1]):
225
+ folder_path = os.path.dirname(files[0])
226
+
227
+ from .media_tags import extract_album_info, format_metadata_filename
228
+ logger.debug("Extracting album info from folder: %s", folder_path)
229
+
230
+ album_info = extract_album_info(folder_path)
231
+ if album_info:
232
+ # Use album info for naming the output file
233
+ template = args.name_template or "{album} - {artist}"
234
+ new_name = format_metadata_filename(album_info, template)
235
+
236
+ if new_name:
237
+ logger.info("Using album metadata for output filename: %s", new_name)
238
+ guessed_name = new_name
239
+ else:
240
+ logger.debug("Could not format filename from album metadata")
241
+
242
+ # For single files, use the file's metadata
243
+ elif len(files) == 1:
244
+ from .media_tags import get_file_tags, format_metadata_filename
245
+
246
+ tags = get_file_tags(files[0])
247
+ if tags:
248
+ template = args.name_template or "{title} - {artist}"
249
+ new_name = format_metadata_filename(tags, template)
250
+
251
+ if new_name:
252
+ logger.info("Using file metadata for output filename: %s", new_name)
253
+ guessed_name = new_name
254
+ else:
255
+ logger.debug("Could not format filename from file metadata")
256
+
257
+ # For multiple files from different folders, try to use common tags if they exist
258
+ elif len(files) > 1:
259
+ from .media_tags import get_file_tags, format_metadata_filename
260
+
261
+ # Try to find common tags among files
262
+ common_tags = {}
263
+ for file_path in files:
264
+ tags = get_file_tags(file_path)
265
+ if tags:
266
+ for key, value in tags.items():
267
+ if key in ['album', 'albumartist', 'artist']:
268
+ if key not in common_tags:
269
+ common_tags[key] = value
270
+ # Only keep values that are the same across files
271
+ elif common_tags[key] != value:
272
+ common_tags[key] = None
273
+
274
+ # Remove None values
275
+ common_tags = {k: v for k, v in common_tags.items() if v is not None}
276
+
277
+ if common_tags:
278
+ template = args.name_template or "Collection - {album}" if 'album' in common_tags else "Collection"
279
+ new_name = format_metadata_filename(common_tags, template)
280
+
281
+ if new_name:
282
+ logger.info("Using common metadata for output filename: %s", new_name)
283
+ guessed_name = new_name
284
+ else:
285
+ logger.debug("Could not format filename from common metadata")
286
+
179
287
  if args.output_filename:
180
288
  out_filename = args.output_filename
289
+ elif guessed_name:
290
+ if args.output_to_source:
291
+ source_dir = os.path.dirname(files[0]) if files else '.'
292
+ out_filename = os.path.join(source_dir, guessed_name)
293
+ logger.debug("Using source location for output with media tags: %s", out_filename)
294
+ else:
295
+ output_dir = './output'
296
+ if not os.path.exists(output_dir):
297
+ logger.debug("Creating default output directory: %s", output_dir)
298
+ os.makedirs(output_dir, exist_ok=True)
299
+ out_filename = os.path.join(output_dir, guessed_name)
300
+ logger.debug("Using default output location with media tags: %s", out_filename)
181
301
  else:
182
302
  guessed_name = guess_output_filename(args.input_filename, files)
183
303
  if args.output_to_source:
@@ -207,7 +327,8 @@ def main():
207
327
 
208
328
  logger.info("Creating Tonie file: %s with %d input file(s)", out_filename, len(files))
209
329
  create_tonie_file(out_filename, files, args.no_tonie_header, args.user_timestamp,
210
- args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp, args.auto_download)
330
+ args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
331
+ args.auto_download, not args.use_legacy_tags)
211
332
  logger.info("Successfully created Tonie file: %s", out_filename)
212
333
 
213
334
 
@@ -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.
@@ -532,4 +622,49 @@ def get_opus_binary(auto_download=False):
532
622
  Returns:
533
623
  str: Path to the opusenc binary if available, None otherwise
534
624
  """
535
- return ensure_dependency('opusenc', auto_download)
625
+ return ensure_dependency('opusenc', auto_download)
626
+
627
+ def get_opus_version(opus_binary=None):
628
+ """
629
+ Get the version of opusenc.
630
+
631
+ Args:
632
+ opus_binary: Path to the opusenc binary
633
+
634
+ Returns:
635
+ str: The version string of opusenc, or a fallback string if the version cannot be determined
636
+ """
637
+ import subprocess
638
+ import re
639
+
640
+ logger = get_logger('dependency_manager')
641
+
642
+ if opus_binary is None:
643
+ opus_binary = get_opus_binary()
644
+
645
+ if opus_binary is None:
646
+ logger.debug("opusenc binary not found, using fallback version string")
647
+ return "opusenc from opus-tools XXX" # Fallback
648
+
649
+ try:
650
+ # Run opusenc --version and capture output
651
+ result = subprocess.run([opus_binary, "--version"],
652
+ capture_output=True, text=True, check=False)
653
+
654
+ # Extract version information from output
655
+ version_output = result.stdout.strip() or result.stderr.strip()
656
+
657
+ if version_output:
658
+ # Try to extract just the version information using regex
659
+ match = re.search(r"(opusenc.*)", version_output)
660
+ if match:
661
+ return match.group(1)
662
+ else:
663
+ return version_output.splitlines()[0] # Use first line
664
+ else:
665
+ logger.debug("Could not determine opusenc version, using fallback")
666
+ return "opusenc from opus-tools XXX" # Fallback
667
+
668
+ except Exception as e:
669
+ logger.debug(f"Error getting opusenc version: {str(e)}")
670
+ return "opusenc from opus-tools XXX" # Fallback