TonieToolbox 0.6.0a3__py3-none-any.whl → 0.6.0a4__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 +29 -11
- TonieToolbox/artwork.py +45 -1
- TonieToolbox/constants.py +4 -1
- TonieToolbox/dependency_manager.py +673 -184
- TonieToolbox/integration.py +5 -1
- TonieToolbox/integration_macos.py +9 -4
- TonieToolbox/integration_windows.py +6 -2
- {tonietoolbox-0.6.0a3.dist-info → tonietoolbox-0.6.0a4.dist-info}/METADATA +6 -2
- {tonietoolbox-0.6.0a3.dist-info → tonietoolbox-0.6.0a4.dist-info}/RECORD +14 -14
- {tonietoolbox-0.6.0a3.dist-info → tonietoolbox-0.6.0a4.dist-info}/WHEEL +0 -0
- {tonietoolbox-0.6.0a3.dist-info → tonietoolbox-0.6.0a4.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.6.0a3.dist-info → tonietoolbox-0.6.0a4.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.6.0a3.dist-info → tonietoolbox-0.6.0a4.dist-info}/top_level.txt +0 -0
@@ -9,12 +9,17 @@ import os
|
|
9
9
|
import sys
|
10
10
|
import platform
|
11
11
|
import subprocess
|
12
|
+
import requests
|
13
|
+
from requests.adapters import HTTPAdapter
|
14
|
+
from urllib3.util.retry import Retry
|
12
15
|
import shutil
|
13
16
|
import zipfile
|
14
17
|
import tarfile
|
15
|
-
import urllib.request
|
16
18
|
import time
|
17
|
-
|
19
|
+
import hashlib
|
20
|
+
import tempfile
|
21
|
+
import concurrent.futures
|
22
|
+
from tqdm.auto import tqdm
|
18
23
|
|
19
24
|
from .logger import get_logger
|
20
25
|
logger = get_logger('dependency_manager')
|
@@ -27,12 +32,18 @@ DEPENDENCIES = {
|
|
27
32
|
'windows': {
|
28
33
|
'url': 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip',
|
29
34
|
'bin_path': 'bin/ffmpeg.exe',
|
30
|
-
'extract_dir': 'ffmpeg'
|
35
|
+
'extract_dir': 'ffmpeg',
|
36
|
+
'mirrors': [
|
37
|
+
''
|
38
|
+
]
|
31
39
|
},
|
32
40
|
'linux': {
|
33
41
|
'url': 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz',
|
34
42
|
'bin_path': 'ffmpeg',
|
35
|
-
'extract_dir': 'ffmpeg'
|
43
|
+
'extract_dir': 'ffmpeg',
|
44
|
+
'mirrors': [
|
45
|
+
''
|
46
|
+
]
|
36
47
|
},
|
37
48
|
'darwin': {
|
38
49
|
'url': 'https://evermeet.cx/ffmpeg/get/zip',
|
@@ -44,7 +55,10 @@ DEPENDENCIES = {
|
|
44
55
|
'windows': {
|
45
56
|
'url': 'https://archive.mozilla.org/pub/opus/win32/opus-tools-0.2-opus-1.3.zip',
|
46
57
|
'bin_path': 'opusenc.exe',
|
47
|
-
'extract_dir': 'opusenc'
|
58
|
+
'extract_dir': 'opusenc',
|
59
|
+
'mirrors': [
|
60
|
+
''
|
61
|
+
]
|
48
62
|
},
|
49
63
|
'linux': {
|
50
64
|
'package': 'opus-tools'
|
@@ -67,61 +81,324 @@ def get_system():
|
|
67
81
|
|
68
82
|
def get_user_data_dir():
|
69
83
|
"""Get the user data directory for storing downloaded dependencies."""
|
70
|
-
app_dir =
|
84
|
+
app_dir = CACHE_DIR
|
71
85
|
logger.debug("Using application data directory: %s", app_dir)
|
72
86
|
|
73
87
|
os.makedirs(app_dir, exist_ok=True)
|
74
88
|
return app_dir
|
75
89
|
|
76
|
-
def
|
90
|
+
def create_session():
|
91
|
+
"""
|
92
|
+
Create a requests session with retry capabilities.
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
requests.Session: Configured session with retries
|
96
|
+
"""
|
97
|
+
session = requests.Session()
|
98
|
+
retry_strategy = Retry(
|
99
|
+
total=3,
|
100
|
+
backoff_factor=1,
|
101
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
102
|
+
allowed_methods=["HEAD", "GET", "OPTIONS"]
|
103
|
+
)
|
104
|
+
adapter = HTTPAdapter(max_retries=retry_strategy, pool_connections=10, pool_maxsize=10)
|
105
|
+
session.mount("http://", adapter)
|
106
|
+
session.mount("https://", adapter)
|
107
|
+
return session
|
108
|
+
|
109
|
+
def configure_tqdm():
|
110
|
+
"""
|
111
|
+
Configure tqdm to ensure it displays properly in various environments.
|
112
|
+
"""
|
113
|
+
# Check if we're in a notebook environment or standard terminal
|
114
|
+
is_notebook = 'ipykernel' in sys.modules
|
115
|
+
|
116
|
+
# Set global defaults for tqdm
|
117
|
+
tqdm.monitor_interval = 0 # Prevent monitor thread issues
|
118
|
+
|
119
|
+
# Return common kwargs for consistency
|
120
|
+
return {
|
121
|
+
'file': sys.stdout,
|
122
|
+
'leave': True,
|
123
|
+
'dynamic_ncols': True,
|
124
|
+
'mininterval': 0.5,
|
125
|
+
'smoothing': 0.2,
|
126
|
+
'ncols': 100 if not is_notebook else None,
|
127
|
+
'disable': False
|
128
|
+
}
|
129
|
+
|
130
|
+
def download_file(url, destination, chunk_size=1024*1024, timeout=30, use_tqdm=True):
|
77
131
|
"""
|
78
|
-
Download a file from a URL to the specified destination.
|
132
|
+
Download a file from a URL to the specified destination using optimized methods.
|
79
133
|
|
80
134
|
Args:
|
81
135
|
url (str): The URL of the file to download
|
82
136
|
destination (str): The path to save the file to
|
137
|
+
chunk_size (int): Size of chunks to download (default: 1MB)
|
138
|
+
timeout (int): Connection timeout in seconds (default: 30s)
|
139
|
+
use_tqdm (bool): Whether to display a progress bar (default: True)
|
83
140
|
|
84
141
|
Returns:
|
85
142
|
bool: True if download was successful, False otherwise
|
86
143
|
"""
|
87
144
|
try:
|
88
145
|
logger.info("Downloading %s to %s", url, destination)
|
89
|
-
headers = {'User-Agent': 'TonieToolbox-dependency-downloader/1.
|
90
|
-
req = urllib.request.Request(url, headers=headers)
|
146
|
+
headers = {'User-Agent': 'TonieToolbox-dependency-downloader/1.1'}
|
91
147
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
148
|
+
# Create a directory for the destination file if it doesn't exist
|
149
|
+
os.makedirs(os.path.dirname(os.path.abspath(destination)), exist_ok=True)
|
150
|
+
|
151
|
+
# Use a session for connection pooling and retries
|
152
|
+
session = create_session()
|
153
|
+
|
154
|
+
# Start with a HEAD request to get the file size before downloading
|
155
|
+
head_response = session.head(url, headers=headers, timeout=timeout)
|
156
|
+
head_response.raise_for_status()
|
157
|
+
file_size = int(head_response.headers.get('Content-Length', 0))
|
158
|
+
logger.debug("File size: %d bytes", file_size)
|
159
|
+
|
160
|
+
# Now start the download
|
161
|
+
response = session.get(url, headers=headers, stream=True, timeout=timeout)
|
162
|
+
response.raise_for_status() # Raise exception for 4XX/5XX status codes
|
163
|
+
# Set up the progress bar
|
164
|
+
desc = os.path.basename(destination)
|
165
|
+
if len(desc) > 25:
|
166
|
+
desc = desc[:22] + "..."
|
167
|
+
|
168
|
+
with open(destination, 'wb') as out_file:
|
169
|
+
if use_tqdm and file_size > 0:
|
170
|
+
# Force tqdm to output to console
|
171
|
+
pbar = tqdm(
|
172
|
+
total=file_size,
|
173
|
+
unit='B',
|
174
|
+
unit_scale=True,
|
175
|
+
desc=desc,
|
176
|
+
**configure_tqdm()
|
177
|
+
)
|
106
178
|
|
107
|
-
|
108
|
-
|
109
|
-
|
179
|
+
for chunk in response.iter_content(chunk_size=chunk_size):
|
180
|
+
if not chunk:
|
181
|
+
continue
|
182
|
+
out_file.write(chunk)
|
183
|
+
pbar.update(len(chunk))
|
184
|
+
pbar.close()
|
185
|
+
# Print an empty line after progress is done
|
186
|
+
print("")
|
187
|
+
else:
|
188
|
+
# Fallback if no file size or tqdm is disabled
|
189
|
+
downloaded = 0
|
190
|
+
for chunk in response.iter_content(chunk_size=chunk_size):
|
191
|
+
if not chunk:
|
192
|
+
continue
|
193
|
+
downloaded += len(chunk)
|
194
|
+
out_file.write(chunk)
|
195
|
+
if file_size > 0:
|
196
|
+
percent = downloaded * 100 / file_size
|
197
|
+
logger.debug("Download progress: %.1f%%", percent)
|
110
198
|
|
111
199
|
logger.info("Download completed successfully")
|
112
200
|
return True
|
113
|
-
except
|
114
|
-
logger.error("Failed to download %s: %s", url, e)
|
201
|
+
except requests.exceptions.SSLError as e:
|
202
|
+
logger.error("Failed to download %s: SSL Error: %s", url, e)
|
115
203
|
# On macOS, provide more helpful error message for SSL certificate issues
|
116
|
-
if platform.system() == 'Darwin'
|
204
|
+
if platform.system() == 'Darwin':
|
117
205
|
logger.error("SSL certificate verification failed on macOS. This is a known issue.")
|
118
206
|
logger.error("You can solve this by running: /Applications/Python 3.x/Install Certificates.command")
|
119
207
|
logger.error("Or by using the --auto-download flag which will bypass certificate verification.")
|
120
208
|
return False
|
209
|
+
except requests.exceptions.RequestException as e:
|
210
|
+
logger.error("Failed to download %s: %s", url, e)
|
211
|
+
return False
|
212
|
+
except Exception as e:
|
213
|
+
logger.error("Unexpected error downloading %s: %s", url, e)
|
214
|
+
return False
|
215
|
+
|
216
|
+
def download_file_multipart(url, destination, num_parts=4, chunk_size=1024*1024, timeout=30):
|
217
|
+
"""
|
218
|
+
Download a file in multiple parts concurrently for better performance.
|
219
|
+
|
220
|
+
Args:
|
221
|
+
url (str): The URL of the file to download
|
222
|
+
destination (str): The path to save the file to
|
223
|
+
num_parts (int): Number of parts to download concurrently
|
224
|
+
chunk_size (int): Size of chunks to download (default: 1MB)
|
225
|
+
timeout (int): Connection timeout in seconds (default: 30s)
|
226
|
+
|
227
|
+
Returns:
|
228
|
+
bool: True if download was successful, False otherwise
|
229
|
+
"""
|
230
|
+
try:
|
231
|
+
logger.info("Starting multi-part download of %s with %d parts", url, num_parts)
|
232
|
+
headers = {'User-Agent': 'TonieToolbox-dependency-downloader/1.1'}
|
233
|
+
|
234
|
+
session = create_session()
|
235
|
+
response = session.head(url, headers=headers, timeout=timeout)
|
236
|
+
response.raise_for_status()
|
237
|
+
|
238
|
+
file_size = int(response.headers.get('Content-Length', 0))
|
239
|
+
if file_size <= 0:
|
240
|
+
logger.warning("Multi-part download requested but Content-Length not available, falling back to regular download")
|
241
|
+
return download_file(url, destination, chunk_size, timeout)
|
242
|
+
|
243
|
+
# If file size is too small for multipart, fallback to regular download
|
244
|
+
if file_size < num_parts * 1024 * 1024 * 5: # Less than 5MB per part
|
245
|
+
logger.debug("File size too small for efficient multi-part download, using regular download")
|
246
|
+
return download_file(url, destination, chunk_size, timeout)
|
247
|
+
|
248
|
+
# Calculate part sizes
|
249
|
+
part_size = file_size // num_parts
|
250
|
+
ranges = [(i * part_size, min((i + 1) * part_size - 1, file_size - 1))
|
251
|
+
for i in range(num_parts)]
|
252
|
+
if ranges[-1][1] < file_size - 1:
|
253
|
+
ranges[-1] = (ranges[-1][0], file_size - 1)
|
254
|
+
|
255
|
+
# Create temporary directory for parts
|
256
|
+
temp_dir = tempfile.mkdtemp(prefix="tonietoolbox_download_")
|
257
|
+
part_files = [os.path.join(temp_dir, f"part_{i}") for i in range(num_parts)]
|
258
|
+
|
259
|
+
# Define the download function for each part
|
260
|
+
def download_part(part_idx):
|
261
|
+
start, end = ranges[part_idx]
|
262
|
+
part_path = part_files[part_idx]
|
263
|
+
|
264
|
+
headers_with_range = headers.copy()
|
265
|
+
headers_with_range['Range'] = f'bytes={start}-{end}'
|
266
|
+
|
267
|
+
part_size = end - start + 1
|
268
|
+
|
269
|
+
try:
|
270
|
+
response = session.get(url, headers=headers_with_range, stream=True, timeout=timeout)
|
271
|
+
response.raise_for_status()
|
272
|
+
# Set up progress bar for this part
|
273
|
+
desc = f"Part {part_idx+1}/{num_parts}"
|
274
|
+
with tqdm(
|
275
|
+
total=part_size,
|
276
|
+
unit='B',
|
277
|
+
unit_scale=True,
|
278
|
+
desc=desc,
|
279
|
+
position=part_idx,
|
280
|
+
**configure_tqdm()
|
281
|
+
) as pbar:
|
282
|
+
with open(part_path, 'wb') as f:
|
283
|
+
for chunk in response.iter_content(chunk_size=chunk_size):
|
284
|
+
if not chunk:
|
285
|
+
continue
|
286
|
+
f.write(chunk)
|
287
|
+
pbar.update(len(chunk))
|
288
|
+
|
289
|
+
return True
|
290
|
+
except Exception as e:
|
291
|
+
logger.error("Error downloading part %d: %s", part_idx, str(e))
|
292
|
+
return False
|
293
|
+
|
294
|
+
# Download all parts in parallel
|
295
|
+
logger.info("Starting concurrent download of %d parts...", num_parts)
|
296
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=num_parts) as executor:
|
297
|
+
futures = [executor.submit(download_part, i) for i in range(num_parts)]
|
298
|
+
all_successful = all(future.result() for future in concurrent.futures.as_completed(futures))
|
299
|
+
|
300
|
+
if not all_successful:
|
301
|
+
logger.error("One or more parts failed to download")
|
302
|
+
|
303
|
+
# Clean up
|
304
|
+
for part_file in part_files:
|
305
|
+
if os.path.exists(part_file):
|
306
|
+
os.remove(part_file)
|
307
|
+
os.rmdir(temp_dir)
|
308
|
+
|
309
|
+
return False
|
310
|
+
|
311
|
+
# Combine all parts into the final file
|
312
|
+
logger.info("All parts downloaded successfully, combining into final file")
|
313
|
+
with open(destination, 'wb') as outfile:
|
314
|
+
for part_file in part_files:
|
315
|
+
with open(part_file, 'rb') as infile:
|
316
|
+
shutil.copyfileobj(infile, outfile)
|
317
|
+
os.remove(part_file)
|
318
|
+
|
319
|
+
# Clean up temp directory
|
320
|
+
os.rmdir(temp_dir)
|
321
|
+
|
322
|
+
logger.info("Multi-part download completed successfully")
|
323
|
+
return True
|
324
|
+
|
325
|
+
except Exception as e:
|
326
|
+
logger.error("Failed multi-part download: %s", str(e))
|
327
|
+
# Fall back to regular download
|
328
|
+
logger.info("Falling back to regular download method")
|
329
|
+
return download_file(url, destination, chunk_size, timeout)
|
330
|
+
|
331
|
+
def smart_download(url, destination, use_multipart=True, min_size_for_multipart=20*1024*1024, num_parts=4, use_tqdm=True):
|
332
|
+
"""
|
333
|
+
Smart download function that selects the best download method based on file size.
|
334
|
+
|
335
|
+
Args:
|
336
|
+
url (str): The URL of the file to download
|
337
|
+
destination (str): The path to save the file to
|
338
|
+
use_multipart (bool): Whether to allow multi-part downloads (default: True)
|
339
|
+
min_size_for_multipart (int): Minimum file size in bytes to use multi-part download (default: 20MB)
|
340
|
+
num_parts (int): Number of parts for multi-part download (default: 4)
|
341
|
+
use_tqdm (bool): Whether to display progress bars (default: True)
|
342
|
+
|
343
|
+
Returns:
|
344
|
+
bool: True if download was successful, False otherwise
|
345
|
+
"""
|
346
|
+
try:
|
347
|
+
# Check if multipart is enabled and get file size
|
348
|
+
if not use_multipart:
|
349
|
+
return download_file(url, destination, use_tqdm=use_tqdm)
|
350
|
+
|
351
|
+
# Create session and check file size
|
352
|
+
session = create_session()
|
353
|
+
response = session.head(url, timeout=30)
|
354
|
+
file_size = int(response.headers.get('Content-Length', 0))
|
355
|
+
|
356
|
+
if file_size >= min_size_for_multipart and use_multipart:
|
357
|
+
logger.info("File size (%d bytes) is suitable for multi-part download", file_size)
|
358
|
+
print(f"Starting multi-part download of {os.path.basename(destination)} ({file_size/1024/1024:.1f} MB)")
|
359
|
+
return download_file_multipart(url, destination, num_parts=num_parts)
|
360
|
+
else:
|
361
|
+
logger.debug("Using standard download method (file size: %d bytes)", file_size)
|
362
|
+
return download_file(url, destination, use_tqdm=use_tqdm)
|
363
|
+
except Exception as e:
|
364
|
+
logger.warning("Error determining download method: %s, falling back to standard download", e)
|
365
|
+
return download_file(url, destination, use_tqdm=use_tqdm)
|
366
|
+
|
367
|
+
def download_with_mirrors(url, destination, mirrors=None):
|
368
|
+
"""
|
369
|
+
Try downloading a file from the primary URL and fall back to mirrors if needed.
|
370
|
+
|
371
|
+
Args:
|
372
|
+
url (str): Primary URL to download from
|
373
|
+
destination (str): Path to save the file to
|
374
|
+
mirrors (list): List of alternative URLs to try if primary fails
|
375
|
+
|
376
|
+
Returns:
|
377
|
+
bool: True if download was successful from any source, False otherwise
|
378
|
+
"""
|
379
|
+
logger.debug("Starting download with primary URL and %s mirrors",
|
380
|
+
"0" if mirrors is None else len(mirrors))
|
381
|
+
|
382
|
+
# Try the primary URL first
|
383
|
+
if smart_download(url, destination):
|
384
|
+
logger.debug("Download successful from primary URL")
|
385
|
+
return True
|
386
|
+
|
387
|
+
# If primary URL fails and we have mirrors, try them
|
388
|
+
if mirrors:
|
389
|
+
for i, mirror_url in enumerate(mirrors, 1):
|
390
|
+
logger.info("Primary download failed, trying mirror %d of %d",
|
391
|
+
i, len(mirrors))
|
392
|
+
if smart_download(mirror_url, destination):
|
393
|
+
logger.info("Download successful from mirror %d", i)
|
394
|
+
return True
|
395
|
+
|
396
|
+
logger.error("All download attempts failed")
|
397
|
+
return False
|
121
398
|
|
122
399
|
def extract_archive(archive_path, extract_dir):
|
123
400
|
"""
|
124
|
-
Extract an archive file to the specified directory.
|
401
|
+
Extract an archive file to the specified directory using optimized methods.
|
125
402
|
|
126
403
|
Args:
|
127
404
|
archive_path (str): Path to the archive file
|
@@ -134,36 +411,66 @@ def extract_archive(archive_path, extract_dir):
|
|
134
411
|
logger.info("Extracting %s to %s", archive_path, extract_dir)
|
135
412
|
os.makedirs(extract_dir, exist_ok=True)
|
136
413
|
|
137
|
-
# Extract to a temporary
|
138
|
-
temp_extract_dir =
|
139
|
-
|
414
|
+
# Extract to a secure temporary directory
|
415
|
+
temp_extract_dir = tempfile.mkdtemp(prefix="tonietoolbox_extract_")
|
416
|
+
logger.debug("Using temporary extraction directory: %s", temp_extract_dir)
|
140
417
|
|
141
418
|
if archive_path.endswith('.zip'):
|
142
419
|
logger.debug("Extracting ZIP archive")
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
420
|
+
try:
|
421
|
+
# Use a with statement for proper cleanup
|
422
|
+
with zipfile.ZipFile(archive_path, 'r') as zip_ref:
|
423
|
+
# Get the list of files for informational purposes
|
424
|
+
files_extracted = zip_ref.namelist()
|
425
|
+
total_size = sum(info.file_size for info in zip_ref.infolist())
|
426
|
+
logger.debug("ZIP contains %d files, total size: %d bytes",
|
427
|
+
len(files_extracted), total_size)
|
428
|
+
|
429
|
+
# Extract with progress indication for large archives
|
430
|
+
if total_size > 50*1024*1024: # 50 MB
|
431
|
+
# Use configure_tqdm() for consistent parameters
|
432
|
+
tqdm_params = configure_tqdm()
|
433
|
+
with tqdm(
|
434
|
+
total=total_size,
|
435
|
+
unit='B',
|
436
|
+
unit_scale=True,
|
437
|
+
desc="Extracting ZIP",
|
438
|
+
**tqdm_params
|
439
|
+
) as pbar:
|
440
|
+
for file in zip_ref.infolist():
|
441
|
+
zip_ref.extract(file, temp_extract_dir)
|
442
|
+
pbar.update(file.file_size)
|
443
|
+
# Print empty line after progress completion
|
444
|
+
print("")
|
445
|
+
else:
|
446
|
+
zip_ref.extractall(temp_extract_dir)
|
447
|
+
except zipfile.BadZipFile as e:
|
448
|
+
logger.error("Bad ZIP file: %s", str(e))
|
449
|
+
return False
|
450
|
+
|
147
451
|
elif archive_path.endswith(('.tar.gz', '.tgz')):
|
148
452
|
logger.debug("Extracting TAR.GZ archive")
|
149
453
|
with tarfile.open(archive_path, 'r:gz') as tar_ref:
|
150
|
-
tar_ref.extractall(temp_extract_dir)
|
151
454
|
files_extracted = tar_ref.getnames()
|
152
|
-
logger.
|
455
|
+
logger.debug("TAR.GZ contains %d files", len(files_extracted))
|
456
|
+
tar_ref.extractall(path=temp_extract_dir)
|
457
|
+
|
153
458
|
elif archive_path.endswith(('.tar.xz', '.txz')):
|
154
459
|
logger.debug("Extracting TAR.XZ archive")
|
155
460
|
with tarfile.open(archive_path, 'r:xz') as tar_ref:
|
156
|
-
tar_ref.extractall(temp_extract_dir)
|
157
461
|
files_extracted = tar_ref.getnames()
|
158
|
-
logger.
|
462
|
+
logger.debug("TAR.XZ contains %d files", len(files_extracted))
|
463
|
+
tar_ref.extractall(path=temp_extract_dir)
|
464
|
+
|
159
465
|
elif archive_path.endswith('.tar'):
|
160
466
|
logger.debug("Extracting TAR archive")
|
161
467
|
with tarfile.open(archive_path, 'r') as tar_ref:
|
162
|
-
tar_ref.extractall(temp_extract_dir)
|
163
468
|
files_extracted = tar_ref.getnames()
|
164
|
-
logger.
|
469
|
+
logger.debug("TAR contains %d files", len(files_extracted))
|
470
|
+
tar_ref.extractall(path=temp_extract_dir)
|
165
471
|
else:
|
166
472
|
logger.error("Unsupported archive format: %s", archive_path)
|
473
|
+
shutil.rmtree(temp_extract_dir, ignore_errors=True)
|
167
474
|
return False
|
168
475
|
|
169
476
|
logger.info("Archive extracted successfully")
|
@@ -235,7 +542,7 @@ def extract_archive(archive_path, extract_dir):
|
|
235
542
|
|
236
543
|
# Clean up the temporary extraction directory
|
237
544
|
try:
|
238
|
-
shutil.rmtree(temp_extract_dir)
|
545
|
+
shutil.rmtree(temp_extract_dir, ignore_errors=True)
|
239
546
|
logger.debug("Removed temporary extraction directory")
|
240
547
|
except Exception as e:
|
241
548
|
logger.warning("Failed to remove temporary extraction directory: %s", e)
|
@@ -334,132 +641,6 @@ def check_binary_in_path(binary_name):
|
|
334
641
|
|
335
642
|
return None
|
336
643
|
|
337
|
-
def install_package(package_name):
|
338
|
-
"""
|
339
|
-
Attempt to install a package using the system's package manager.
|
340
|
-
|
341
|
-
Args:
|
342
|
-
package_name (str): Name of the package to install
|
343
|
-
|
344
|
-
Returns:
|
345
|
-
bool: True if installation was successful, False otherwise
|
346
|
-
"""
|
347
|
-
system = get_system()
|
348
|
-
logger.info("Attempting to install %s on %s", package_name, system)
|
349
|
-
|
350
|
-
try:
|
351
|
-
if system == 'linux':
|
352
|
-
# Try apt-get (Debian/Ubuntu)
|
353
|
-
if shutil.which('apt-get'):
|
354
|
-
logger.info("Installing %s using apt-get", package_name)
|
355
|
-
subprocess.run(['sudo', 'apt-get', 'update'], check=True)
|
356
|
-
subprocess.run(['sudo', 'apt-get', 'install', '-y', package_name], check=True)
|
357
|
-
return True
|
358
|
-
# Try yum (CentOS/RHEL)
|
359
|
-
elif shutil.which('yum'):
|
360
|
-
logger.info("Installing %s using yum", package_name)
|
361
|
-
subprocess.run(['sudo', 'yum', 'install', '-y', package_name], check=True)
|
362
|
-
return True
|
363
|
-
|
364
|
-
elif system == 'darwin':
|
365
|
-
# Try Homebrew
|
366
|
-
if shutil.which('brew'):
|
367
|
-
logger.info("Installing %s using homebrew", package_name)
|
368
|
-
subprocess.run(['brew', 'install', package_name], check=True)
|
369
|
-
return True
|
370
|
-
|
371
|
-
logger.warning("Could not automatically install %s. Please install it manually.", package_name)
|
372
|
-
return False
|
373
|
-
except subprocess.CalledProcessError as e:
|
374
|
-
logger.error("Failed to install %s: %s", package_name, e)
|
375
|
-
return False
|
376
|
-
|
377
|
-
def install_python_package(package_name):
|
378
|
-
"""
|
379
|
-
Attempt to install a Python package using pip.
|
380
|
-
|
381
|
-
Args:
|
382
|
-
package_name (str): Name of the package to install
|
383
|
-
|
384
|
-
Returns:
|
385
|
-
bool: True if installation was successful, False otherwise
|
386
|
-
"""
|
387
|
-
logger.info("Attempting to install Python package: %s", package_name)
|
388
|
-
try:
|
389
|
-
import subprocess
|
390
|
-
import sys
|
391
|
-
|
392
|
-
# Try to install the package using pip
|
393
|
-
subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
|
394
|
-
logger.info("Successfully installed Python package: %s", package_name)
|
395
|
-
return True
|
396
|
-
except Exception as e:
|
397
|
-
logger.error("Failed to install Python package %s: %s", package_name, str(e))
|
398
|
-
return False
|
399
|
-
|
400
|
-
def check_python_package(package_name):
|
401
|
-
"""
|
402
|
-
Check if a Python package is installed.
|
403
|
-
|
404
|
-
Args:
|
405
|
-
package_name (str): Name of the package to check
|
406
|
-
|
407
|
-
Returns:
|
408
|
-
bool: True if the package is installed, False otherwise
|
409
|
-
"""
|
410
|
-
logger.debug("Checking if Python package is installed: %s", package_name)
|
411
|
-
try:
|
412
|
-
__import__(package_name)
|
413
|
-
logger.debug("Python package %s is installed", package_name)
|
414
|
-
return True
|
415
|
-
except ImportError:
|
416
|
-
logger.debug("Python package %s is not installed", package_name)
|
417
|
-
return False
|
418
|
-
|
419
|
-
def ensure_mutagen(auto_install=True):
|
420
|
-
"""
|
421
|
-
Ensure that the Mutagen library is available, installing it if necessary and allowed.
|
422
|
-
|
423
|
-
Args:
|
424
|
-
auto_install (bool): Whether to automatically install Mutagen if not found (defaults to True)
|
425
|
-
|
426
|
-
Returns:
|
427
|
-
bool: True if Mutagen is available, False otherwise
|
428
|
-
"""
|
429
|
-
logger.debug("Checking if Mutagen is available")
|
430
|
-
|
431
|
-
try:
|
432
|
-
import mutagen
|
433
|
-
logger.debug("Mutagen is already installed")
|
434
|
-
return True
|
435
|
-
except ImportError:
|
436
|
-
logger.debug("Mutagen is not installed")
|
437
|
-
|
438
|
-
if auto_install:
|
439
|
-
logger.info("Auto-install enabled, attempting to install Mutagen")
|
440
|
-
if install_python_package('mutagen'):
|
441
|
-
try:
|
442
|
-
import mutagen
|
443
|
-
logger.info("Successfully installed and imported Mutagen")
|
444
|
-
return True
|
445
|
-
except ImportError:
|
446
|
-
logger.error("Mutagen was installed but could not be imported")
|
447
|
-
else:
|
448
|
-
logger.error("Failed to install Mutagen")
|
449
|
-
else:
|
450
|
-
logger.warning("Mutagen is not installed and --auto-download is not used.")
|
451
|
-
|
452
|
-
return False
|
453
|
-
|
454
|
-
def is_mutagen_available():
|
455
|
-
"""
|
456
|
-
Check if the Mutagen library is available.
|
457
|
-
|
458
|
-
Returns:
|
459
|
-
bool: True if Mutagen is available, False otherwise
|
460
|
-
"""
|
461
|
-
return check_python_package('mutagen')
|
462
|
-
|
463
644
|
def ensure_dependency(dependency_name, auto_download=False):
|
464
645
|
"""
|
465
646
|
Ensure that a dependency is available, downloading it if necessary.
|
@@ -491,7 +672,7 @@ def ensure_dependency(dependency_name, auto_download=False):
|
|
491
672
|
bin_name = dependency_name if dependency_name != 'opusenc' else 'opusenc'
|
492
673
|
|
493
674
|
# Create a specific folder for this dependency
|
494
|
-
dependency_dir = os.path.join(user_data_dir, dependency_name)
|
675
|
+
dependency_dir = os.path.join(user_data_dir, 'libs', dependency_name)
|
495
676
|
|
496
677
|
# First priority: Check if we already downloaded and extracted it previously
|
497
678
|
# When auto_download is True, we'll skip this check and download fresh versions
|
@@ -582,6 +763,7 @@ def ensure_dependency(dependency_name, auto_download=False):
|
|
582
763
|
|
583
764
|
# Set up download paths
|
584
765
|
download_url = dependency_info['url']
|
766
|
+
mirrors = dependency_info.get('mirrors', [])
|
585
767
|
|
586
768
|
# Create dependency-specific directory
|
587
769
|
os.makedirs(dependency_dir, exist_ok=True)
|
@@ -591,7 +773,10 @@ def ensure_dependency(dependency_name, auto_download=False):
|
|
591
773
|
archive_path = os.path.join(dependency_dir, f"{dependency_name}{archive_ext}")
|
592
774
|
logger.debug("Using archive path: %s", archive_path)
|
593
775
|
|
594
|
-
|
776
|
+
# Use our improved download function with mirrors and tqdm progress bar
|
777
|
+
print(f"Downloading {dependency_name}...")
|
778
|
+
if download_with_mirrors(download_url, archive_path, mirrors):
|
779
|
+
print(f"Extracting {dependency_name}...")
|
595
780
|
if extract_archive(archive_path, dependency_dir):
|
596
781
|
binary = find_binary_in_extracted_dir(dependency_dir, binary_path)
|
597
782
|
if binary:
|
@@ -605,29 +790,248 @@ def ensure_dependency(dependency_name, auto_download=False):
|
|
605
790
|
logger.error("Failed to set up %s", dependency_name)
|
606
791
|
return None
|
607
792
|
|
608
|
-
def
|
793
|
+
def install_package(package_name):
|
609
794
|
"""
|
610
|
-
|
795
|
+
Attempt to install a package using the system's package manager.
|
611
796
|
|
612
797
|
Args:
|
613
|
-
|
798
|
+
package_name (str): Name of the package to install
|
799
|
+
|
800
|
+
Returns:
|
801
|
+
bool: True if installation was successful, False otherwise
|
802
|
+
"""
|
803
|
+
system = get_system()
|
804
|
+
logger.info("Attempting to install %s on %s", package_name, system)
|
614
805
|
|
806
|
+
try:
|
807
|
+
if system == 'linux':
|
808
|
+
# Try apt-get (Debian/Ubuntu)
|
809
|
+
if shutil.which('apt-get'):
|
810
|
+
logger.info("Installing %s using apt-get", package_name)
|
811
|
+
subprocess.run(['sudo', 'apt-get', 'update'], check=True)
|
812
|
+
subprocess.run(['sudo', 'apt-get', 'install', '-y', package_name], check=True)
|
813
|
+
return True
|
814
|
+
# Try yum (CentOS/RHEL)
|
815
|
+
elif shutil.which('yum'):
|
816
|
+
logger.info("Installing %s using yum", package_name)
|
817
|
+
subprocess.run(['sudo', 'yum', 'install', '-y', package_name], check=True)
|
818
|
+
return True
|
819
|
+
|
820
|
+
elif system == 'darwin':
|
821
|
+
# Try Homebrew
|
822
|
+
if shutil.which('brew'):
|
823
|
+
logger.info("Installing %s using homebrew", package_name)
|
824
|
+
subprocess.run(['brew', 'install', package_name], check=True)
|
825
|
+
return True
|
826
|
+
|
827
|
+
logger.warning("Could not automatically install %s. Please install it manually.", package_name)
|
828
|
+
return False
|
829
|
+
except subprocess.CalledProcessError as e:
|
830
|
+
logger.error("Failed to install %s: %s", package_name, e)
|
831
|
+
return False
|
832
|
+
|
833
|
+
def get_ffmpeg_binary(auto_download=False):
|
834
|
+
"""
|
835
|
+
Get the path to the FFmpeg binary, downloading it if necessary and allowed.
|
836
|
+
|
837
|
+
Args:
|
838
|
+
auto_download (bool): Whether to automatically download FFmpeg if not found (defaults to False)
|
839
|
+
|
615
840
|
Returns:
|
616
|
-
str: Path to the FFmpeg binary if available
|
841
|
+
str: Path to the FFmpeg binary, or None if not available
|
617
842
|
"""
|
618
|
-
|
843
|
+
logger.debug("Getting FFmpeg binary")
|
844
|
+
|
845
|
+
# Define the expected binary path
|
846
|
+
local_dir = os.path.join(get_user_data_dir(), 'libs', 'ffmpeg')
|
847
|
+
if sys.platform == 'win32':
|
848
|
+
binary_path = os.path.join(local_dir, 'ffmpeg.exe')
|
849
|
+
else:
|
850
|
+
binary_path = os.path.join(local_dir, 'ffmpeg')
|
851
|
+
|
852
|
+
# Check if binary exists
|
853
|
+
if os.path.exists(binary_path) and os.path.isfile(binary_path):
|
854
|
+
logger.debug("FFmpeg binary found at %s", binary_path)
|
855
|
+
return binary_path
|
856
|
+
|
857
|
+
# Check if a system-wide FFmpeg is available
|
858
|
+
try:
|
859
|
+
if sys.platform == 'win32':
|
860
|
+
# On Windows, look for ffmpeg in PATH
|
861
|
+
from shutil import which
|
862
|
+
system_binary = which('ffmpeg')
|
863
|
+
if system_binary:
|
864
|
+
logger.debug("System-wide FFmpeg found at %s", system_binary)
|
865
|
+
return system_binary
|
866
|
+
else:
|
867
|
+
# On Unix-like systems, use 'which' command
|
868
|
+
system_binary = subprocess.check_output(['which', 'ffmpeg']).decode('utf-8').strip()
|
869
|
+
if system_binary:
|
870
|
+
logger.debug("System-wide FFmpeg found at %s", system_binary)
|
871
|
+
return system_binary
|
872
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
873
|
+
logger.debug("No system-wide FFmpeg found")
|
874
|
+
|
875
|
+
# Download if allowed
|
876
|
+
if auto_download:
|
877
|
+
logger.info("Auto-download enabled, forcing download/installation of ffmpeg")
|
878
|
+
print("Downloading ffmpeg...")
|
879
|
+
|
880
|
+
# Create directory if it doesn't exist
|
881
|
+
os.makedirs(local_dir, exist_ok=True)
|
882
|
+
|
883
|
+
# Download FFmpeg based on platform
|
884
|
+
if sys.platform == 'win32':
|
885
|
+
url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip"
|
886
|
+
archive_path = os.path.join(local_dir, "ffmpeg.zip")
|
887
|
+
|
888
|
+
# Download the file
|
889
|
+
logger.info("Downloading %s to %s", url, archive_path)
|
890
|
+
download_with_mirrors(url, archive_path)
|
891
|
+
|
892
|
+
# Extract the archive
|
893
|
+
print("Extracting ffmpeg...")
|
894
|
+
logger.info("Extracting %s to %s", archive_path, local_dir)
|
895
|
+
extract_archive(archive_path, local_dir)
|
896
|
+
|
897
|
+
# Find the binary in the extracted files
|
898
|
+
for root, dirs, files in os.walk(local_dir):
|
899
|
+
if 'ffmpeg.exe' in files:
|
900
|
+
binary_path = os.path.join(root, 'ffmpeg.exe')
|
901
|
+
break
|
902
|
+
|
903
|
+
# Verify the binary exists
|
904
|
+
if not os.path.exists(binary_path):
|
905
|
+
logger.error("FFmpeg binary not found after extraction")
|
906
|
+
return None
|
907
|
+
|
908
|
+
logger.info("Successfully set up ffmpeg: %s", binary_path)
|
909
|
+
return binary_path
|
910
|
+
|
911
|
+
elif sys.platform == 'darwin': # macOS
|
912
|
+
url = "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip"
|
913
|
+
archive_path = os.path.join(local_dir, "ffmpeg.zip")
|
914
|
+
|
915
|
+
# Download and extract
|
916
|
+
download_with_mirrors(url, archive_path)
|
917
|
+
extract_archive(archive_path, local_dir)
|
918
|
+
|
919
|
+
# Make binary executable
|
920
|
+
binary_path = os.path.join(local_dir, "ffmpeg")
|
921
|
+
os.chmod(binary_path, 0o755)
|
922
|
+
logger.info("Successfully set up ffmpeg: %s", binary_path)
|
923
|
+
return binary_path
|
924
|
+
|
925
|
+
else: # Linux and others
|
926
|
+
url = "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz"
|
927
|
+
archive_path = os.path.join(local_dir, "ffmpeg.tar.xz")
|
928
|
+
|
929
|
+
# Download and extract
|
930
|
+
download_with_mirrors(url, archive_path)
|
931
|
+
extract_archive(archive_path, local_dir)
|
932
|
+
|
933
|
+
# Find the binary in the extracted files
|
934
|
+
for root, dirs, files in os.walk(local_dir):
|
935
|
+
if 'ffmpeg' in files:
|
936
|
+
binary_path = os.path.join(root, 'ffmpeg')
|
937
|
+
os.chmod(binary_path, 0o755)
|
938
|
+
logger.info("Successfully set up ffmpeg: %s", binary_path)
|
939
|
+
return binary_path
|
940
|
+
|
941
|
+
logger.error("FFmpeg binary not found after extraction")
|
942
|
+
return None
|
943
|
+
else:
|
944
|
+
logger.warning("FFmpeg is not available and --auto-download is not used.")
|
945
|
+
return None
|
619
946
|
|
620
947
|
def get_opus_binary(auto_download=False):
|
621
948
|
"""
|
622
|
-
Get the path to the
|
949
|
+
Get the path to the Opus binary, downloading it if necessary and allowed.
|
623
950
|
|
624
951
|
Args:
|
625
|
-
auto_download (bool): Whether to automatically download
|
626
|
-
|
952
|
+
auto_download (bool): Whether to automatically download Opus if not found (defaults to False)
|
953
|
+
|
627
954
|
Returns:
|
628
|
-
str: Path to the
|
955
|
+
str: Path to the Opus binary, or None if not available
|
629
956
|
"""
|
630
|
-
|
957
|
+
logger.debug("Getting Opus binary")
|
958
|
+
|
959
|
+
# Define the expected binary path
|
960
|
+
local_dir = os.path.join(get_user_data_dir(), 'libs', 'opusenc')
|
961
|
+
if sys.platform == 'win32':
|
962
|
+
binary_path = os.path.join(local_dir, 'opusenc.exe')
|
963
|
+
else:
|
964
|
+
binary_path = os.path.join(local_dir, 'opusenc')
|
965
|
+
|
966
|
+
# Check if binary exists
|
967
|
+
if os.path.exists(binary_path) and os.path.isfile(binary_path):
|
968
|
+
logger.debug("Opus binary found at %s", binary_path)
|
969
|
+
return binary_path
|
970
|
+
|
971
|
+
# Check if a system-wide Opus is available
|
972
|
+
try:
|
973
|
+
if sys.platform == 'win32':
|
974
|
+
# On Windows, look for opusenc in PATH
|
975
|
+
from shutil import which
|
976
|
+
system_binary = which('opusenc')
|
977
|
+
if system_binary:
|
978
|
+
logger.debug("System-wide Opus found at %s", system_binary)
|
979
|
+
return system_binary
|
980
|
+
else:
|
981
|
+
# On Unix-like systems, use 'which' command
|
982
|
+
system_binary = subprocess.check_output(['which', 'opusenc']).decode('utf-8').strip()
|
983
|
+
if system_binary:
|
984
|
+
logger.debug("System-wide Opus found at %s", system_binary)
|
985
|
+
return system_binary
|
986
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
987
|
+
logger.debug("No system-wide Opus found")
|
988
|
+
|
989
|
+
# Download if allowed
|
990
|
+
if auto_download:
|
991
|
+
logger.info("Auto-download enabled, forcing download/installation of opusenc")
|
992
|
+
print("Downloading opusenc...")
|
993
|
+
|
994
|
+
# Create directory if it doesn't exist
|
995
|
+
os.makedirs(local_dir, exist_ok=True)
|
996
|
+
|
997
|
+
# Download Opus based on platform
|
998
|
+
if sys.platform == 'win32':
|
999
|
+
url = "https://archive.mozilla.org/pub/opus/win32/opus-tools-0.2-opus-1.3.zip"
|
1000
|
+
archive_path = os.path.join(local_dir, "opusenc.zip")
|
1001
|
+
else:
|
1002
|
+
# For non-Windows, we'll need to compile from source or find precompiled binaries
|
1003
|
+
logger.error("Automatic download of Opus for non-Windows platforms is not supported yet")
|
1004
|
+
return None
|
1005
|
+
|
1006
|
+
# Download the file
|
1007
|
+
logger.info("Downloading %s to %s", url, archive_path)
|
1008
|
+
download_with_mirrors(url, archive_path)
|
1009
|
+
|
1010
|
+
# Extract the archive
|
1011
|
+
print("Extracting opusenc...")
|
1012
|
+
logger.info("Extracting %s to %s", archive_path, local_dir)
|
1013
|
+
extract_archive(archive_path, local_dir)
|
1014
|
+
|
1015
|
+
# For Windows, the binary should now be in the directory
|
1016
|
+
if sys.platform == 'win32':
|
1017
|
+
binary_path = os.path.join(local_dir, 'opusenc.exe')
|
1018
|
+
if not os.path.exists(binary_path):
|
1019
|
+
# Try to find it in the extracted directory structure
|
1020
|
+
for root, dirs, files in os.walk(local_dir):
|
1021
|
+
if 'opusenc.exe' in files:
|
1022
|
+
binary_path = os.path.join(root, 'opusenc.exe')
|
1023
|
+
break
|
1024
|
+
|
1025
|
+
# Verify the binary exists
|
1026
|
+
if not os.path.exists(binary_path):
|
1027
|
+
logger.error("Opus binary not found after extraction")
|
1028
|
+
return None
|
1029
|
+
|
1030
|
+
logger.info("Successfully set up opusenc: %s", binary_path)
|
1031
|
+
return binary_path
|
1032
|
+
else:
|
1033
|
+
logger.warning("Opus is not available and --auto-download is not used.")
|
1034
|
+
return None
|
631
1035
|
|
632
1036
|
def get_opus_version(opus_binary=None):
|
633
1037
|
"""
|
@@ -672,4 +1076,89 @@ def get_opus_version(opus_binary=None):
|
|
672
1076
|
|
673
1077
|
except Exception as e:
|
674
1078
|
logger.debug(f"Error getting opusenc version: {str(e)}")
|
675
|
-
return "opusenc from opus-tools XXX" # Fallback
|
1079
|
+
return "opusenc from opus-tools XXX" # Fallback
|
1080
|
+
|
1081
|
+
def check_python_package(package_name):
|
1082
|
+
"""
|
1083
|
+
Check if a Python package is installed.
|
1084
|
+
|
1085
|
+
Args:
|
1086
|
+
package_name (str): Name of the package to check
|
1087
|
+
|
1088
|
+
Returns:
|
1089
|
+
bool: True if the package is installed, False otherwise
|
1090
|
+
"""
|
1091
|
+
logger.debug("Checking if Python package is installed: %s", package_name)
|
1092
|
+
try:
|
1093
|
+
__import__(package_name)
|
1094
|
+
logger.debug("Python package %s is installed", package_name)
|
1095
|
+
return True
|
1096
|
+
except ImportError:
|
1097
|
+
logger.debug("Python package %s is not installed", package_name)
|
1098
|
+
return False
|
1099
|
+
|
1100
|
+
def install_python_package(package_name):
|
1101
|
+
"""
|
1102
|
+
Attempt to install a Python package using pip.
|
1103
|
+
|
1104
|
+
Args:
|
1105
|
+
package_name (str): Name of the package to install
|
1106
|
+
|
1107
|
+
Returns:
|
1108
|
+
bool: True if installation was successful, False otherwise
|
1109
|
+
"""
|
1110
|
+
logger.info("Attempting to install Python package: %s", package_name)
|
1111
|
+
try:
|
1112
|
+
import subprocess
|
1113
|
+
|
1114
|
+
# Try to install the package using pip
|
1115
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
|
1116
|
+
logger.info("Successfully installed Python package: %s", package_name)
|
1117
|
+
return True
|
1118
|
+
except Exception as e:
|
1119
|
+
logger.error("Failed to install Python package %s: %s", package_name, str(e))
|
1120
|
+
return False
|
1121
|
+
|
1122
|
+
def ensure_mutagen(auto_install=True):
|
1123
|
+
"""
|
1124
|
+
Ensure that the Mutagen library is available, installing it if necessary and allowed.
|
1125
|
+
|
1126
|
+
Args:
|
1127
|
+
auto_install (bool): Whether to automatically install Mutagen if not found (defaults to True)
|
1128
|
+
|
1129
|
+
Returns:
|
1130
|
+
bool: True if Mutagen is available, False otherwise
|
1131
|
+
"""
|
1132
|
+
logger.debug("Checking if Mutagen is available")
|
1133
|
+
|
1134
|
+
try:
|
1135
|
+
import mutagen
|
1136
|
+
logger.debug("Mutagen is already installed")
|
1137
|
+
return True
|
1138
|
+
except ImportError:
|
1139
|
+
logger.debug("Mutagen is not installed")
|
1140
|
+
|
1141
|
+
if auto_install:
|
1142
|
+
logger.info("Auto-install enabled, attempting to install Mutagen")
|
1143
|
+
if install_python_package('mutagen'):
|
1144
|
+
try:
|
1145
|
+
import mutagen
|
1146
|
+
logger.info("Successfully installed and imported Mutagen")
|
1147
|
+
return True
|
1148
|
+
except ImportError:
|
1149
|
+
logger.error("Mutagen was installed but could not be imported")
|
1150
|
+
else:
|
1151
|
+
logger.error("Failed to install Mutagen")
|
1152
|
+
else:
|
1153
|
+
logger.warning("Mutagen is not installed and --auto-download is not used.")
|
1154
|
+
|
1155
|
+
return False
|
1156
|
+
|
1157
|
+
def is_mutagen_available():
|
1158
|
+
"""
|
1159
|
+
Check if the Mutagen library is available.
|
1160
|
+
|
1161
|
+
Returns:
|
1162
|
+
bool: True if Mutagen is available, False otherwise
|
1163
|
+
"""
|
1164
|
+
return check_python_package('mutagen')
|