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