TonieToolbox 0.2.3__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.3'
5
+ __version__ = '0.3.0'
TonieToolbox/__main__.py CHANGED
@@ -18,13 +18,40 @@ 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
21
22
 
22
23
  def main():
23
24
  """Entry point for the TonieToolbox application."""
24
25
  parser = argparse.ArgumentParser(description='Create Tonie compatible file from Ogg opus file(s).')
25
26
  parser.add_argument('-v', '--version', action='version', version=f'TonieToolbox {__version__}',
26
27
  help='show program version and exit')
27
- 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='?',
28
55
  help='input file or directory or a file list (.lst)')
29
56
  parser.add_argument('output_filename', metavar='TARGET', nargs='?', type=str,
30
57
  help='the output file name (default: ---ID---)')
@@ -79,6 +106,11 @@ def main():
79
106
  log_level_group.add_argument('-Q', '--silent', action='store_true', help='Show only errors')
80
107
 
81
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
+
82
114
  if args.trace:
83
115
  from .logger import TRACE
84
116
  log_level = TRACE
@@ -90,11 +122,12 @@ def main():
90
122
  log_level = logging.ERROR
91
123
  else:
92
124
  log_level = logging.INFO
93
-
125
+
94
126
  setup_logging(log_level)
95
127
  logger = get_logger('main')
96
128
  logger.debug("Starting TonieToolbox v%s with log level: %s", __version__, logging.getLevelName(log_level))
97
129
 
130
+
98
131
  if args.clear_version_cache:
99
132
  if clear_version_cache():
100
133
  logger.info("Version cache cleared successfully")
@@ -111,6 +144,59 @@ def main():
111
144
  if not is_latest and not update_confirmed and not (args.silent or args.quiet):
112
145
  logger.info("Update available but user chose to continue without updating.")
113
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
+
114
200
  ffmpeg_binary = args.ffmpeg
115
201
  if ffmpeg_binary is None:
116
202
  ffmpeg_binary = get_ffmpeg_binary(args.auto_download)
@@ -156,6 +242,7 @@ def main():
156
242
  os.makedirs(output_dir, exist_ok=True)
157
243
  logger.debug("Created output directory: %s", output_dir)
158
244
 
245
+ created_files = []
159
246
  for task_index, (output_name, folder_path, audio_files) in enumerate(process_tasks):
160
247
  if args.output_to_source:
161
248
  task_out_filename = os.path.join(folder_path, f"{output_name}.taf")
@@ -169,8 +256,102 @@ def main():
169
256
  args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
170
257
  args.auto_download, not args.use_legacy_tags)
171
258
  logger.info("Successfully created Tonie file: %s", task_out_filename)
259
+ created_files.append(task_out_filename)
172
260
 
173
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)
174
355
  sys.exit(0)
175
356
 
176
357
  # Handle directory or file input
@@ -214,7 +395,6 @@ def main():
214
395
  print(f"{tag_name}: {tag_value}")
215
396
  else:
216
397
  print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
217
-
218
398
  sys.exit(0)
219
399
 
220
400
  # Use media tags for file naming if requested
@@ -324,13 +504,115 @@ def main():
324
504
 
325
505
  if not out_filename.lower().endswith('.taf'):
326
506
  out_filename += '.taf'
327
-
507
+
328
508
  logger.info("Creating Tonie file: %s with %d input file(s)", out_filename, len(files))
329
509
  create_tonie_file(out_filename, files, args.no_tonie_header, args.user_timestamp,
330
510
  args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
331
511
  args.auto_download, not args.use_legacy_tags)
332
512
  logger.info("Successfully created Tonie file: %s", out_filename)
333
-
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")
334
616
 
335
617
  if __name__ == "__main__":
336
618
  main()
@@ -466,7 +466,7 @@ def ensure_dependency(dependency_name, auto_download=False):
466
466
  Returns:
467
467
  str: Path to the binary if available, None otherwise
468
468
  """
469
- logger.info("Ensuring dependency: %s", dependency_name)
469
+ logger.debug("Ensuring dependency: %s", dependency_name)
470
470
  system = get_system()
471
471
 
472
472
  if system not in ['windows', 'linux', 'darwin']:
@@ -496,7 +496,7 @@ def ensure_dependency(dependency_name, auto_download=False):
496
496
  existing_binary = find_binary_in_extracted_dir(dependency_dir, binary_path)
497
497
  if existing_binary and os.path.exists(existing_binary):
498
498
  # Verify that the binary works
499
- logger.info("Found previously downloaded %s: %s", dependency_name, existing_binary)
499
+ logger.debug("Found previously downloaded %s: %s", dependency_name, existing_binary)
500
500
  try:
501
501
  if os.access(existing_binary, os.X_OK) or system == 'windows':
502
502
  if system in ['linux', 'darwin']:
@@ -509,14 +509,14 @@ def ensure_dependency(dependency_name, auto_download=False):
509
509
  try:
510
510
  result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5)
511
511
  if result.returncode == 0:
512
- logger.info("Using previously downloaded %s: %s", dependency_name, existing_binary)
512
+ logger.debug("Using previously downloaded %s: %s", dependency_name, existing_binary)
513
513
  return existing_binary
514
514
  except:
515
515
  # If --version fails, try without arguments
516
516
  try:
517
517
  result = subprocess.run([existing_binary], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5)
518
518
  if result.returncode == 0:
519
- logger.info("Using previously downloaded %s: %s", dependency_name, existing_binary)
519
+ logger.debug("Using previously downloaded %s: %s", dependency_name, existing_binary)
520
520
  return existing_binary
521
521
  except:
522
522
  pass
@@ -525,7 +525,7 @@ def ensure_dependency(dependency_name, auto_download=False):
525
525
  try:
526
526
  result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5)
527
527
  if result.returncode == 0:
528
- logger.info("Using previously downloaded %s: %s", dependency_name, existing_binary)
528
+ logger.debug("Using previously downloaded %s: %s", dependency_name, existing_binary)
529
529
  return existing_binary
530
530
  except:
531
531
  pass
@@ -8,6 +8,9 @@ which can be used to enhance Tonie file creation with proper track information.
8
8
  import os
9
9
  from typing import Dict, Any, Optional, List
10
10
  import logging
11
+ import tempfile
12
+ import base64
13
+ from mutagen.flac import Picture
11
14
  from .logger import get_logger
12
15
  from .dependency_manager import is_mutagen_available, ensure_mutagen
13
16
 
@@ -468,4 +471,167 @@ def format_metadata_filename(metadata: Dict[str, str], template: str = "{tracknu
468
471
  return result
469
472
  except Exception as e:
470
473
  logger.error("Error formatting metadata: %s", str(e))
471
- return ""
474
+ return ""
475
+
476
+ def extract_artwork(file_path: str, output_path: Optional[str] = None) -> Optional[str]:
477
+ """
478
+ Extract artwork from an audio file.
479
+
480
+ Args:
481
+ file_path: Path to the audio file
482
+ output_path: Path where to save the extracted artwork.
483
+ If None, a temporary file will be created.
484
+
485
+ Returns:
486
+ Path to the extracted artwork file, or None if no artwork was found
487
+ """
488
+ if not MUTAGEN_AVAILABLE:
489
+ logger.debug("Mutagen not available - cannot extract artwork")
490
+ return None
491
+
492
+ if not os.path.exists(file_path):
493
+ logger.error("File not found: %s", file_path)
494
+ return None
495
+
496
+ try:
497
+ file_ext = os.path.splitext(file_path.lower())[1]
498
+ artwork_data = None
499
+ mime_type = None
500
+
501
+ # Extract artwork based on file type
502
+ if file_ext == '.mp3':
503
+ audio = mutagen.File(file_path)
504
+
505
+ # Try to get artwork from APIC frames
506
+ if audio.tags:
507
+ for frame in audio.tags.values():
508
+ if frame.FrameID == 'APIC':
509
+ artwork_data = frame.data
510
+ mime_type = frame.mime
511
+ break
512
+
513
+ elif file_ext == '.flac':
514
+ audio = FLAC(file_path)
515
+
516
+ # Get pictures from FLAC
517
+ if audio.pictures:
518
+ artwork_data = audio.pictures[0].data
519
+ mime_type = audio.pictures[0].mime
520
+
521
+ elif file_ext in ['.m4a', '.mp4', '.aac']:
522
+ audio = MP4(file_path)
523
+
524
+ # Check 'covr' atom
525
+ if 'covr' in audio:
526
+ artwork_data = audio['covr'][0]
527
+ # Determine mime type based on data format
528
+ if isinstance(artwork_data, mutagen.mp4.MP4Cover):
529
+ if artwork_data.format == mutagen.mp4.MP4Cover.FORMAT_JPEG:
530
+ mime_type = 'image/jpeg'
531
+ elif artwork_data.format == mutagen.mp4.MP4Cover.FORMAT_PNG:
532
+ mime_type = 'image/png'
533
+ else:
534
+ mime_type = 'image/jpeg' # Default guess
535
+
536
+ elif file_ext == '.ogg':
537
+ try:
538
+ audio = OggVorbis(file_path)
539
+ except:
540
+ try:
541
+ audio = OggOpus(file_path)
542
+ except:
543
+ logger.debug("Could not determine OGG type for %s", file_path)
544
+ return None
545
+
546
+ # For OGG files, metadata pictures are more complex to extract
547
+ if 'metadata_block_picture' in audio:
548
+ picture_data = base64.b64decode(audio['metadata_block_picture'][0])
549
+ flac_picture = Picture(data=picture_data)
550
+ artwork_data = flac_picture.data
551
+ mime_type = flac_picture.mime
552
+
553
+ # If we found artwork data, save it to a file
554
+ if artwork_data:
555
+ # Determine file extension from mime type
556
+ if mime_type == 'image/jpeg':
557
+ ext = '.jpg'
558
+ elif mime_type == 'image/png':
559
+ ext = '.png'
560
+ else:
561
+ ext = '.jpg' # Default to jpg
562
+
563
+ # Create output path if not provided
564
+ if not output_path:
565
+ # Create a temporary file
566
+ temp_file = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
567
+ output_path = temp_file.name
568
+ temp_file.close()
569
+ elif not os.path.splitext(output_path)[1]:
570
+ # Add extension if not in the output path
571
+ output_path += ext
572
+
573
+ # Write artwork to file
574
+ with open(output_path, 'wb') as f:
575
+ f.write(artwork_data)
576
+
577
+ logger.info("Extracted artwork saved to %s", output_path)
578
+ return output_path
579
+ else:
580
+ logger.debug("No artwork found in file: %s", file_path)
581
+ return None
582
+
583
+ except Exception as e:
584
+ logger.debug("Error extracting artwork: %s", e)
585
+ return None
586
+
587
+ def find_cover_image(source_dir):
588
+ """
589
+ Find a cover image in the source directory.
590
+
591
+ Args:
592
+ source_dir: Path to the directory to search for cover images
593
+
594
+ Returns:
595
+ str: Path to the found cover image, or None if not found
596
+ """
597
+ if not os.path.isdir(source_dir):
598
+ return None
599
+
600
+ # Common cover image file names
601
+ cover_names = [
602
+ 'cover', 'folder', 'album', 'front', 'artwork', 'image',
603
+ 'albumart', 'albumartwork', 'booklet'
604
+ ]
605
+
606
+ # Common image extensions
607
+ image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif']
608
+
609
+ # Try different variations
610
+ for name in cover_names:
611
+ for ext in image_extensions:
612
+ # Try exact name match
613
+ cover_path = os.path.join(source_dir, name + ext)
614
+ if os.path.exists(cover_path):
615
+ logger.debug("Found cover image: %s", cover_path)
616
+ return cover_path
617
+
618
+ # Try case-insensitive match
619
+ for file in os.listdir(source_dir):
620
+ if file.lower() == (name + ext).lower():
621
+ cover_path = os.path.join(source_dir, file)
622
+ logger.debug("Found cover image: %s", cover_path)
623
+ return cover_path
624
+
625
+ # If no exact matches, try finding any file containing the cover names
626
+ for file in os.listdir(source_dir):
627
+ file_lower = file.lower()
628
+ file_ext = os.path.splitext(file_lower)[1]
629
+ if file_ext in image_extensions:
630
+ for name in cover_names:
631
+ if name in file_lower:
632
+ cover_path = os.path.join(source_dir, file)
633
+ logger.debug("Found cover image: %s", cover_path)
634
+ return cover_path
635
+
636
+ logger.debug("No cover image found in directory: %s", source_dir)
637
+ return None