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 +1 -1
- TonieToolbox/__main__.py +349 -5
- TonieToolbox/audio_conversion.py +36 -14
- TonieToolbox/constants.py +77 -2
- TonieToolbox/dependency_manager.py +5 -5
- TonieToolbox/logger.py +51 -5
- TonieToolbox/media_tags.py +189 -7
- TonieToolbox/teddycloud.py +679 -0
- TonieToolbox/tonie_file.py +32 -12
- TonieToolbox/tonies_json.py +502 -0
- {tonietoolbox-0.2.3.dist-info → tonietoolbox-0.4.0.dist-info}/METADATA +296 -6
- tonietoolbox-0.4.0.dist-info/RECORD +24 -0
- tonietoolbox-0.2.3.dist-info/RECORD +0 -22
- {tonietoolbox-0.2.3.dist-info → tonietoolbox-0.4.0.dist-info}/WHEEL +0 -0
- {tonietoolbox-0.2.3.dist-info → tonietoolbox-0.4.0.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.2.3.dist-info → tonietoolbox-0.4.0.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.2.3.dist-info → tonietoolbox-0.4.0.dist-info}/top_level.txt +0 -0
TonieToolbox/constants.py
CHANGED
@@ -9,7 +9,82 @@ TOO_MANY_SEGMENTS = -4
|
|
9
9
|
TIMESTAMP_DEDUCT = 0x50000000
|
10
10
|
OPUS_TAGS = [
|
11
11
|
bytearray(
|
12
|
-
b"\x4F\x70\x75\x73\x54\x61\x67\x73\x0D\x00\x00\x00\x4C\x61\x76\x66\x35\x38\x2E\x32\x30\x2E\x31\x30\x30\x03\x00\x00\x00\x26\x00\x00\x00\x65\x6E\x63\x6F\x64\x65\x72\x3D\x6F\x70\x75\x73\x65\x6E\x63\x20\x66\x72\x6F\x6D\x20\x6F\x70\x75\x73\x2D\x74\x6F\x6F\x6C\x73\x20\x30\x2E\x31\x2E\x31\x30\x2A\x00\x00\x00\x65\x6E\x63\x6F\x64\x65\x72\x5F\x6F\x70\x74\x69\x6F\x6E\x73\x3D\x2D\x2D\x71\x75\x69\x65\x74\x20\x2D\x2D\x62\x69\x74\x72\x61\x74\x65\x20\x39\x36\x20\x2D\x2D\x76\x62\x72\x3B\x01\x00\x00\x70\x61\x64\x3D\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30
|
12
|
+
b"\x4F\x70\x75\x73\x54\x61\x67\x73\x0D\x00\x00\x00\x4C\x61\x76\x66\x35\x38\x2E\x32\x30\x2E\x31\x30\x30\x03\x00\x00\x00\x26\x00\x00\x00\x65\x6E\x63\x6F\x64\x65\x72\x3D\x6F\x70\x75\x73\x65\x6E\x63\x20\x66\x72\x6F\x6D\x20\x6F\x70\x75\x73\x2D\x74\x6F\x6F\x6C\x73\x20\x30\x2E\x31\x2E\x31\x30\x2A\x00\x00\x00\x65\x6E\x63\x6F\x64\x65\x72\x5F\x6F\x70\x74\x69\x6F\x6E\x73\x3D\x2D\x2D\x71\x75\x69\x65\x74\x20\x2D\x2D\x62\x69\x74\x72\x61\x74\x65\x20\x39\x36\x20\x2D\x2D\x76\x62\x72\x3B\x01\x00\x00\x70\x61\x64\x3D\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30"),
|
13
13
|
bytearray(
|
14
14
|
b"\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30")
|
15
|
-
]
|
15
|
+
]
|
16
|
+
|
17
|
+
# Mapping of language tags to ISO codes
|
18
|
+
LANGUAGE_MAPPING = {
|
19
|
+
# Common language names to ISO codes
|
20
|
+
'deutsch': 'de-de',
|
21
|
+
'german': 'de-de',
|
22
|
+
'english': 'en-us',
|
23
|
+
'englisch': 'en-us',
|
24
|
+
'français': 'fr-fr',
|
25
|
+
'french': 'fr-fr',
|
26
|
+
'franzosisch': 'fr-fr',
|
27
|
+
'italiano': 'it-it',
|
28
|
+
'italian': 'it-it',
|
29
|
+
'italienisch': 'it-it',
|
30
|
+
'español': 'es-es',
|
31
|
+
'spanish': 'es-es',
|
32
|
+
'spanisch': 'es-es',
|
33
|
+
# Two-letter codes
|
34
|
+
'de': 'de-de',
|
35
|
+
'en': 'en-us',
|
36
|
+
'fr': 'fr-fr',
|
37
|
+
'it': 'it-it',
|
38
|
+
'es': 'es-es',
|
39
|
+
}
|
40
|
+
|
41
|
+
# Mapping of genre tags to tonie categories
|
42
|
+
GENRE_MAPPING = {
|
43
|
+
# Standard Tonie category names from tonies.json
|
44
|
+
'hörspiel': 'Hörspiele & Hörbücher',
|
45
|
+
'hörbuch': 'Hörspiele & Hörbücher',
|
46
|
+
'hörbücher': 'Hörspiele & Hörbücher',
|
47
|
+
'hörspiele': 'Hörspiele & Hörbücher',
|
48
|
+
'audiobook': 'Hörspiele & Hörbücher',
|
49
|
+
'audio book': 'Hörspiele & Hörbücher',
|
50
|
+
'audio play': 'Hörspiele & Hörbücher',
|
51
|
+
'audio-play': 'Hörspiele & Hörbücher',
|
52
|
+
'audiospiel': 'Hörspiele & Hörbücher',
|
53
|
+
'geschichte': 'Hörspiele & Hörbücher',
|
54
|
+
'geschichten': 'Hörspiele & Hörbücher',
|
55
|
+
'erzählung': 'Hörspiele & Hörbücher',
|
56
|
+
|
57
|
+
# Music related genres
|
58
|
+
'musik': 'music',
|
59
|
+
'lieder': 'music',
|
60
|
+
'songs': 'music',
|
61
|
+
'music': 'music',
|
62
|
+
'lied': 'music',
|
63
|
+
'song': 'music',
|
64
|
+
|
65
|
+
# More specific categories
|
66
|
+
'kinder': 'Hörspiele & Hörbücher',
|
67
|
+
'children': 'Hörspiele & Hörbücher',
|
68
|
+
'märchen': 'Hörspiele & Hörbücher',
|
69
|
+
'fairy tale': 'Hörspiele & Hörbücher',
|
70
|
+
'märche': 'Hörspiele & Hörbücher',
|
71
|
+
|
72
|
+
'wissen': 'Wissen & Hörmagazine',
|
73
|
+
'knowledge': 'Wissen & Hörmagazine',
|
74
|
+
'sachbuch': 'Wissen & Hörmagazine',
|
75
|
+
'learning': 'Wissen & Hörmagazine',
|
76
|
+
'educational': 'Wissen & Hörmagazine',
|
77
|
+
'bildung': 'Wissen & Hörmagazine',
|
78
|
+
'information': 'Wissen & Hörmagazine',
|
79
|
+
|
80
|
+
'schlaf': 'Schlaflieder & Entspannung',
|
81
|
+
'sleep': 'Schlaflieder & Entspannung',
|
82
|
+
'meditation': 'Schlaflieder & Entspannung',
|
83
|
+
'entspannung': 'Schlaflieder & Entspannung',
|
84
|
+
'relaxation': 'Schlaflieder & Entspannung',
|
85
|
+
'schlaflied': 'Schlaflieder & Entspannung',
|
86
|
+
'einschlafhilfe': 'Schlaflieder & Entspannung',
|
87
|
+
|
88
|
+
# Default to standard format for custom
|
89
|
+
'custom': 'Hörspiele & Hörbücher',
|
90
|
+
}
|
@@ -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.
|
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.
|
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.
|
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.
|
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.
|
528
|
+
logger.debug("Using previously downloaded %s: %s", dependency_name, existing_binary)
|
529
529
|
return existing_binary
|
530
530
|
except:
|
531
531
|
pass
|
TonieToolbox/logger.py
CHANGED
@@ -5,6 +5,8 @@ Logging configuration for the TonieToolbox package.
|
|
5
5
|
import logging
|
6
6
|
import os
|
7
7
|
import sys
|
8
|
+
from datetime import datetime
|
9
|
+
from pathlib import Path
|
8
10
|
|
9
11
|
# Define log levels and their names
|
10
12
|
TRACE = 5 # Custom level for ultra-verbose debugging
|
@@ -19,20 +21,39 @@ def trace(self, message, *args, **kwargs):
|
|
19
21
|
# Add trace method to the Logger class
|
20
22
|
logging.Logger.trace = trace
|
21
23
|
|
22
|
-
def
|
24
|
+
def get_log_file_path():
|
25
|
+
"""
|
26
|
+
Get the path to the log file in the .tonietoolbox folder with timestamp.
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
Path: Path to the log file
|
30
|
+
"""
|
31
|
+
# Create .tonietoolbox folder in user's home directory if it doesn't exist
|
32
|
+
log_dir = Path.home() / '.tonietoolbox'
|
33
|
+
log_dir.mkdir(exist_ok=True)
|
34
|
+
|
35
|
+
# Create timestamp string for the filename
|
36
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
37
|
+
|
38
|
+
# Define log file path with timestamp
|
39
|
+
log_file = log_dir / f'tonietoolbox_{timestamp}.log'
|
40
|
+
|
41
|
+
return log_file
|
42
|
+
|
43
|
+
def setup_logging(level=logging.INFO, log_to_file=False):
|
23
44
|
"""
|
24
45
|
Set up logging configuration for the entire application.
|
25
46
|
|
26
47
|
Args:
|
27
48
|
level: Logging level (default: logging.INFO)
|
49
|
+
log_to_file: Whether to log to a file (default: False)
|
28
50
|
|
29
51
|
Returns:
|
30
52
|
logging.Logger: Root logger instance
|
31
53
|
"""
|
32
|
-
#
|
33
|
-
logging.
|
34
|
-
|
35
|
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
54
|
+
# Create formatter
|
55
|
+
formatter = logging.Formatter(
|
56
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
36
57
|
datefmt='%Y-%m-%d %H:%M:%S'
|
37
58
|
)
|
38
59
|
|
@@ -40,6 +61,31 @@ def setup_logging(level=logging.INFO):
|
|
40
61
|
root_logger = logging.getLogger('TonieToolbox')
|
41
62
|
root_logger.setLevel(level)
|
42
63
|
|
64
|
+
# Remove any existing handlers to avoid duplicate logs
|
65
|
+
for handler in root_logger.handlers[:]:
|
66
|
+
root_logger.removeHandler(handler)
|
67
|
+
|
68
|
+
# Console handler
|
69
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
70
|
+
console_handler.setFormatter(formatter)
|
71
|
+
console_handler.setLevel(level)
|
72
|
+
root_logger.addHandler(console_handler)
|
73
|
+
|
74
|
+
# File handler (if enabled)
|
75
|
+
if log_to_file:
|
76
|
+
try:
|
77
|
+
log_file = get_log_file_path()
|
78
|
+
file_handler = logging.FileHandler(
|
79
|
+
log_file,
|
80
|
+
encoding='utf-8'
|
81
|
+
)
|
82
|
+
file_handler.setFormatter(formatter)
|
83
|
+
file_handler.setLevel(level)
|
84
|
+
root_logger.addHandler(file_handler)
|
85
|
+
root_logger.info(f"Log file created at: {log_file}")
|
86
|
+
except Exception as e:
|
87
|
+
root_logger.error(f"Failed to set up file logging: {e}")
|
88
|
+
|
43
89
|
return root_logger
|
44
90
|
|
45
91
|
def get_logger(name):
|
TonieToolbox/media_tags.py
CHANGED
@@ -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
|
|
@@ -206,12 +209,28 @@ def get_file_tags(file_path: str) -> Dict[str, Any]:
|
|
206
209
|
# Process different file types
|
207
210
|
if isinstance(audio, ID3) or hasattr(audio, 'ID3'):
|
208
211
|
# MP3 files
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
212
|
+
try:
|
213
|
+
id3 = audio if isinstance(audio, ID3) else audio.ID3
|
214
|
+
for tag_key, tag_value in id3.items():
|
215
|
+
tag_name = tag_key.split(':')[0] # Handle ID3 tags with colons
|
216
|
+
if tag_name in TAG_MAPPING:
|
217
|
+
tag_value_str = str(tag_value)
|
218
|
+
tags[TAG_MAPPING[tag_name]] = normalize_tag_value(tag_value_str)
|
219
|
+
except (AttributeError, TypeError) as e:
|
220
|
+
logger.debug("Error accessing ID3 tags: %s", e)
|
221
|
+
# Try alternative approach for ID3 tags
|
222
|
+
try:
|
223
|
+
if hasattr(audio, 'tags') and audio.tags:
|
224
|
+
for tag_key in audio.tags.keys():
|
225
|
+
if tag_key in TAG_MAPPING:
|
226
|
+
tag_value = audio.tags[tag_key]
|
227
|
+
if hasattr(tag_value, 'text'):
|
228
|
+
tag_value_str = str(tag_value.text[0]) if tag_value.text else ''
|
229
|
+
else:
|
230
|
+
tag_value_str = str(tag_value)
|
231
|
+
tags[TAG_MAPPING[tag_key]] = normalize_tag_value(tag_value_str)
|
232
|
+
except Exception as e:
|
233
|
+
logger.debug("Alternative ID3 tag reading failed: %s", e)
|
215
234
|
elif isinstance(audio, (FLAC, OggOpus, OggVorbis)):
|
216
235
|
# FLAC and OGG files
|
217
236
|
for tag_key, tag_values in audio.items():
|
@@ -468,4 +487,167 @@ def format_metadata_filename(metadata: Dict[str, str], template: str = "{tracknu
|
|
468
487
|
return result
|
469
488
|
except Exception as e:
|
470
489
|
logger.error("Error formatting metadata: %s", str(e))
|
471
|
-
return ""
|
490
|
+
return ""
|
491
|
+
|
492
|
+
def extract_artwork(file_path: str, output_path: Optional[str] = None) -> Optional[str]:
|
493
|
+
"""
|
494
|
+
Extract artwork from an audio file.
|
495
|
+
|
496
|
+
Args:
|
497
|
+
file_path: Path to the audio file
|
498
|
+
output_path: Path where to save the extracted artwork.
|
499
|
+
If None, a temporary file will be created.
|
500
|
+
|
501
|
+
Returns:
|
502
|
+
Path to the extracted artwork file, or None if no artwork was found
|
503
|
+
"""
|
504
|
+
if not MUTAGEN_AVAILABLE:
|
505
|
+
logger.debug("Mutagen not available - cannot extract artwork")
|
506
|
+
return None
|
507
|
+
|
508
|
+
if not os.path.exists(file_path):
|
509
|
+
logger.error("File not found: %s", file_path)
|
510
|
+
return None
|
511
|
+
|
512
|
+
try:
|
513
|
+
file_ext = os.path.splitext(file_path.lower())[1]
|
514
|
+
artwork_data = None
|
515
|
+
mime_type = None
|
516
|
+
|
517
|
+
# Extract artwork based on file type
|
518
|
+
if file_ext == '.mp3':
|
519
|
+
audio = mutagen.File(file_path)
|
520
|
+
|
521
|
+
# Try to get artwork from APIC frames
|
522
|
+
if audio.tags:
|
523
|
+
for frame in audio.tags.values():
|
524
|
+
if frame.FrameID == 'APIC':
|
525
|
+
artwork_data = frame.data
|
526
|
+
mime_type = frame.mime
|
527
|
+
break
|
528
|
+
|
529
|
+
elif file_ext == '.flac':
|
530
|
+
audio = FLAC(file_path)
|
531
|
+
|
532
|
+
# Get pictures from FLAC
|
533
|
+
if audio.pictures:
|
534
|
+
artwork_data = audio.pictures[0].data
|
535
|
+
mime_type = audio.pictures[0].mime
|
536
|
+
|
537
|
+
elif file_ext in ['.m4a', '.mp4', '.aac']:
|
538
|
+
audio = MP4(file_path)
|
539
|
+
|
540
|
+
# Check 'covr' atom
|
541
|
+
if 'covr' in audio:
|
542
|
+
artwork_data = audio['covr'][0]
|
543
|
+
# Determine mime type based on data format
|
544
|
+
if isinstance(artwork_data, mutagen.mp4.MP4Cover):
|
545
|
+
if artwork_data.format == mutagen.mp4.MP4Cover.FORMAT_JPEG:
|
546
|
+
mime_type = 'image/jpeg'
|
547
|
+
elif artwork_data.format == mutagen.mp4.MP4Cover.FORMAT_PNG:
|
548
|
+
mime_type = 'image/png'
|
549
|
+
else:
|
550
|
+
mime_type = 'image/jpeg' # Default guess
|
551
|
+
|
552
|
+
elif file_ext == '.ogg':
|
553
|
+
try:
|
554
|
+
audio = OggVorbis(file_path)
|
555
|
+
except:
|
556
|
+
try:
|
557
|
+
audio = OggOpus(file_path)
|
558
|
+
except:
|
559
|
+
logger.debug("Could not determine OGG type for %s", file_path)
|
560
|
+
return None
|
561
|
+
|
562
|
+
# For OGG files, metadata pictures are more complex to extract
|
563
|
+
if 'metadata_block_picture' in audio:
|
564
|
+
picture_data = base64.b64decode(audio['metadata_block_picture'][0])
|
565
|
+
flac_picture = Picture(data=picture_data)
|
566
|
+
artwork_data = flac_picture.data
|
567
|
+
mime_type = flac_picture.mime
|
568
|
+
|
569
|
+
# If we found artwork data, save it to a file
|
570
|
+
if artwork_data:
|
571
|
+
# Determine file extension from mime type
|
572
|
+
if mime_type == 'image/jpeg':
|
573
|
+
ext = '.jpg'
|
574
|
+
elif mime_type == 'image/png':
|
575
|
+
ext = '.png'
|
576
|
+
else:
|
577
|
+
ext = '.jpg' # Default to jpg
|
578
|
+
|
579
|
+
# Create output path if not provided
|
580
|
+
if not output_path:
|
581
|
+
# Create a temporary file
|
582
|
+
temp_file = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
|
583
|
+
output_path = temp_file.name
|
584
|
+
temp_file.close()
|
585
|
+
elif not os.path.splitext(output_path)[1]:
|
586
|
+
# Add extension if not in the output path
|
587
|
+
output_path += ext
|
588
|
+
|
589
|
+
# Write artwork to file
|
590
|
+
with open(output_path, 'wb') as f:
|
591
|
+
f.write(artwork_data)
|
592
|
+
|
593
|
+
logger.info("Extracted artwork saved to %s", output_path)
|
594
|
+
return output_path
|
595
|
+
else:
|
596
|
+
logger.debug("No artwork found in file: %s", file_path)
|
597
|
+
return None
|
598
|
+
|
599
|
+
except Exception as e:
|
600
|
+
logger.debug("Error extracting artwork: %s", e)
|
601
|
+
return None
|
602
|
+
|
603
|
+
def find_cover_image(source_dir):
|
604
|
+
"""
|
605
|
+
Find a cover image in the source directory.
|
606
|
+
|
607
|
+
Args:
|
608
|
+
source_dir: Path to the directory to search for cover images
|
609
|
+
|
610
|
+
Returns:
|
611
|
+
str: Path to the found cover image, or None if not found
|
612
|
+
"""
|
613
|
+
if not os.path.isdir(source_dir):
|
614
|
+
return None
|
615
|
+
|
616
|
+
# Common cover image file names
|
617
|
+
cover_names = [
|
618
|
+
'cover', 'folder', 'album', 'front', 'artwork', 'image',
|
619
|
+
'albumart', 'albumartwork', 'booklet'
|
620
|
+
]
|
621
|
+
|
622
|
+
# Common image extensions
|
623
|
+
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif']
|
624
|
+
|
625
|
+
# Try different variations
|
626
|
+
for name in cover_names:
|
627
|
+
for ext in image_extensions:
|
628
|
+
# Try exact name match
|
629
|
+
cover_path = os.path.join(source_dir, name + ext)
|
630
|
+
if os.path.exists(cover_path):
|
631
|
+
logger.debug("Found cover image: %s", cover_path)
|
632
|
+
return cover_path
|
633
|
+
|
634
|
+
# Try case-insensitive match
|
635
|
+
for file in os.listdir(source_dir):
|
636
|
+
if file.lower() == (name + ext).lower():
|
637
|
+
cover_path = os.path.join(source_dir, file)
|
638
|
+
logger.debug("Found cover image: %s", cover_path)
|
639
|
+
return cover_path
|
640
|
+
|
641
|
+
# If no exact matches, try finding any file containing the cover names
|
642
|
+
for file in os.listdir(source_dir):
|
643
|
+
file_lower = file.lower()
|
644
|
+
file_ext = os.path.splitext(file_lower)[1]
|
645
|
+
if file_ext in image_extensions:
|
646
|
+
for name in cover_names:
|
647
|
+
if name in file_lower:
|
648
|
+
cover_path = os.path.join(source_dir, file)
|
649
|
+
logger.debug("Found cover image: %s", cover_path)
|
650
|
+
return cover_path
|
651
|
+
|
652
|
+
logger.debug("No cover image found in directory: %s", source_dir)
|
653
|
+
return None
|