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/__init__.py
CHANGED
TonieToolbox/__main__.py
CHANGED
@@ -18,13 +18,43 @@ from .filename_generator import guess_output_filename
|
|
18
18
|
from .version_handler import check_for_updates, clear_version_cache
|
19
19
|
from .recursive_processor import process_recursive_folders
|
20
20
|
from .media_tags import is_available as is_media_tags_available, ensure_mutagen
|
21
|
+
from .teddycloud import upload_to_teddycloud, get_tags_from_teddycloud, get_file_paths
|
22
|
+
from .tonies_json import fetch_and_update_tonies_json
|
21
23
|
|
22
24
|
def main():
|
23
25
|
"""Entry point for the TonieToolbox application."""
|
24
26
|
parser = argparse.ArgumentParser(description='Create Tonie compatible file from Ogg opus file(s).')
|
25
27
|
parser.add_argument('-v', '--version', action='version', version=f'TonieToolbox {__version__}',
|
26
28
|
help='show program version and exit')
|
27
|
-
|
29
|
+
|
30
|
+
# TeddyCloud options first to check for existence before requiring SOURCE
|
31
|
+
teddycloud_group = parser.add_argument_group('TeddyCloud Options')
|
32
|
+
teddycloud_group.add_argument('--upload', metavar='URL', action='store',
|
33
|
+
help='Upload to TeddyCloud instance (e.g., https://teddycloud.example.com). Supports .taf, .jpg, .jpeg, .png files.')
|
34
|
+
teddycloud_group.add_argument('--include-artwork', action='store_true',
|
35
|
+
help='Upload cover artwork image alongside the Tonie file when using --upload')
|
36
|
+
teddycloud_group.add_argument('--get-tags', action='store', metavar='URL',
|
37
|
+
help='Get available tags from TeddyCloud instance')
|
38
|
+
teddycloud_group.add_argument('--ignore-ssl-verify', action='store_true',
|
39
|
+
help='Ignore SSL certificate verification (for self-signed certificates)')
|
40
|
+
teddycloud_group.add_argument('--special-folder', action='store', metavar='FOLDER',
|
41
|
+
help='Special folder to upload to (currently only "library" is supported)', default='library')
|
42
|
+
teddycloud_group.add_argument('--path', action='store', metavar='PATH',
|
43
|
+
help='Path where to write the file on TeddyCloud server')
|
44
|
+
teddycloud_group.add_argument('--show-progress', action='store_true', default=True,
|
45
|
+
help='Show progress bar during file upload (default: enabled)')
|
46
|
+
teddycloud_group.add_argument('--connection-timeout', type=int, metavar='SECONDS', default=10,
|
47
|
+
help='Connection timeout in seconds (default: 10)')
|
48
|
+
teddycloud_group.add_argument('--read-timeout', type=int, metavar='SECONDS', default=300,
|
49
|
+
help='Read timeout in seconds (default: 300)')
|
50
|
+
teddycloud_group.add_argument('--max-retries', type=int, metavar='RETRIES', default=3,
|
51
|
+
help='Maximum number of retry attempts (default: 3)')
|
52
|
+
teddycloud_group.add_argument('--retry-delay', type=int, metavar='SECONDS', default=5,
|
53
|
+
help='Delay between retry attempts in seconds (default: 5)')
|
54
|
+
teddycloud_group.add_argument('--create-custom-json', action='store_true',
|
55
|
+
help='Fetch and update custom Tonies JSON data')
|
56
|
+
|
57
|
+
parser.add_argument('input_filename', metavar='SOURCE', type=str, nargs='?',
|
28
58
|
help='input file or directory or a file list (.lst)')
|
29
59
|
parser.add_argument('output_filename', metavar='TARGET', nargs='?', type=str,
|
30
60
|
help='the output file name (default: ---ID---)')
|
@@ -77,8 +107,16 @@ def main():
|
|
77
107
|
log_level_group.add_argument('-T', '--trace', action='store_true', help='Enable trace logging (very verbose)')
|
78
108
|
log_level_group.add_argument('-q', '--quiet', action='store_true', help='Show only warnings and errors')
|
79
109
|
log_level_group.add_argument('-Q', '--silent', action='store_true', help='Show only errors')
|
110
|
+
log_group.add_argument('--log-file', action='store_true', default=False,
|
111
|
+
help='Save logs to a timestamped file in .tonietoolbox folder')
|
80
112
|
|
81
113
|
args = parser.parse_args()
|
114
|
+
|
115
|
+
# Validate that input_filename is provided if not using --get-tags or --upload-existing
|
116
|
+
if args.input_filename is None and not (args.get_tags or args.upload):
|
117
|
+
parser.error("the following arguments are required: SOURCE")
|
118
|
+
|
119
|
+
# Set up the logging level
|
82
120
|
if args.trace:
|
83
121
|
from .logger import TRACE
|
84
122
|
log_level = TRACE
|
@@ -90,12 +128,16 @@ def main():
|
|
90
128
|
log_level = logging.ERROR
|
91
129
|
else:
|
92
130
|
log_level = logging.INFO
|
93
|
-
|
94
|
-
setup_logging(log_level)
|
131
|
+
|
132
|
+
setup_logging(log_level, log_to_file=args.log_file)
|
95
133
|
logger = get_logger('main')
|
96
134
|
logger.debug("Starting TonieToolbox v%s with log level: %s", __version__, logging.getLevelName(log_level))
|
97
135
|
|
136
|
+
# Log the command-line arguments at trace level for debugging purposes
|
137
|
+
logger.log(logging.DEBUG - 1, "Command-line arguments: %s", vars(args))
|
138
|
+
|
98
139
|
if args.clear_version_cache:
|
140
|
+
logger.log(logging.DEBUG - 1, "Clearing version cache")
|
99
141
|
if clear_version_cache():
|
100
142
|
logger.info("Version cache cleared successfully")
|
101
143
|
else:
|
@@ -108,9 +150,71 @@ def main():
|
|
108
150
|
force_refresh=args.force_refresh_cache
|
109
151
|
)
|
110
152
|
|
153
|
+
logger.log(logging.DEBUG - 1, "Update check results: is_latest=%s, latest_version=%s, update_confirmed=%s",
|
154
|
+
is_latest, latest_version, update_confirmed)
|
155
|
+
|
111
156
|
if not is_latest and not update_confirmed and not (args.silent or args.quiet):
|
112
157
|
logger.info("Update available but user chose to continue without updating.")
|
113
158
|
|
159
|
+
# Handle get-tags from TeddyCloud if requested
|
160
|
+
if args.get_tags:
|
161
|
+
logger.debug("Getting tags from TeddyCloud: %s", args.get_tags)
|
162
|
+
teddycloud_url = args.get_tags
|
163
|
+
success = get_tags_from_teddycloud(teddycloud_url, args.ignore_ssl_verify)
|
164
|
+
logger.log(logging.DEBUG - 1, "Exiting with code %d", 0 if success else 1)
|
165
|
+
sys.exit(0 if success else 1)
|
166
|
+
|
167
|
+
# Handle upload to TeddyCloud if requested
|
168
|
+
if args.upload:
|
169
|
+
teddycloud_url = args.upload
|
170
|
+
logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
|
171
|
+
|
172
|
+
if not args.input_filename:
|
173
|
+
logger.error("Missing input file for --upload. Provide a file path as SOURCE argument.")
|
174
|
+
sys.exit(1)
|
175
|
+
|
176
|
+
# Check if the input file is already a .taf file or an image file
|
177
|
+
if os.path.exists(args.input_filename) and (args.input_filename.lower().endswith('.taf') or
|
178
|
+
args.input_filename.lower().endswith(('.jpg', '.jpeg', '.png'))):
|
179
|
+
# Direct upload of existing TAF or image file
|
180
|
+
logger.debug("Direct upload of existing TAF or image file detected")
|
181
|
+
# Use get_file_paths to handle Windows backslashes and resolve the paths correctly
|
182
|
+
file_paths = get_file_paths(args.input_filename)
|
183
|
+
|
184
|
+
if not file_paths:
|
185
|
+
logger.error("No files found for pattern %s", args.input_filename)
|
186
|
+
sys.exit(1)
|
187
|
+
|
188
|
+
logger.info("Found %d file(s) to upload to TeddyCloud %s", len(file_paths), teddycloud_url)
|
189
|
+
|
190
|
+
for file_path in file_paths:
|
191
|
+
# Only upload supported file types
|
192
|
+
if not file_path.lower().endswith(('.taf', '.jpg', '.jpeg', '.png')):
|
193
|
+
logger.warning("Skipping unsupported file type: %s", file_path)
|
194
|
+
continue
|
195
|
+
|
196
|
+
logger.info("Uploading %s to TeddyCloud %s", file_path, teddycloud_url)
|
197
|
+
upload_success = upload_to_teddycloud(
|
198
|
+
file_path, teddycloud_url, args.ignore_ssl_verify,
|
199
|
+
args.special_folder, args.path, args.show_progress,
|
200
|
+
args.connection_timeout, args.read_timeout,
|
201
|
+
args.max_retries, args.retry_delay
|
202
|
+
)
|
203
|
+
|
204
|
+
if not upload_success:
|
205
|
+
logger.error("Failed to upload %s to TeddyCloud", file_path)
|
206
|
+
sys.exit(1)
|
207
|
+
else:
|
208
|
+
logger.info("Successfully uploaded %s to TeddyCloud", file_path)
|
209
|
+
|
210
|
+
logger.log(logging.DEBUG - 1, "Exiting after direct upload with code 0")
|
211
|
+
sys.exit(0)
|
212
|
+
|
213
|
+
# If we get here, it's not a TAF or image file, so continue with normal processing
|
214
|
+
# which will convert the input files and upload the result later
|
215
|
+
logger.debug("Input is not a direct upload file, continuing with conversion workflow")
|
216
|
+
pass
|
217
|
+
|
114
218
|
ffmpeg_binary = args.ffmpeg
|
115
219
|
if ffmpeg_binary is None:
|
116
220
|
ffmpeg_binary = get_ffmpeg_binary(args.auto_download)
|
@@ -156,6 +260,7 @@ def main():
|
|
156
260
|
os.makedirs(output_dir, exist_ok=True)
|
157
261
|
logger.debug("Created output directory: %s", output_dir)
|
158
262
|
|
263
|
+
created_files = []
|
159
264
|
for task_index, (output_name, folder_path, audio_files) in enumerate(process_tasks):
|
160
265
|
if args.output_to_source:
|
161
266
|
task_out_filename = os.path.join(folder_path, f"{output_name}.taf")
|
@@ -169,8 +274,102 @@ def main():
|
|
169
274
|
args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
|
170
275
|
args.auto_download, not args.use_legacy_tags)
|
171
276
|
logger.info("Successfully created Tonie file: %s", task_out_filename)
|
277
|
+
created_files.append(task_out_filename)
|
172
278
|
|
173
279
|
logger.info("Recursive processing completed. Created %d Tonie files.", len(process_tasks))
|
280
|
+
|
281
|
+
# Handle upload to TeddyCloud if requested
|
282
|
+
if args.upload and created_files:
|
283
|
+
teddycloud_url = args.upload
|
284
|
+
|
285
|
+
for taf_file in created_files:
|
286
|
+
upload_success = upload_to_teddycloud(
|
287
|
+
taf_file, teddycloud_url, args.ignore_ssl_verify,
|
288
|
+
args.special_folder, args.path, args.show_progress,
|
289
|
+
args.connection_timeout, args.read_timeout,
|
290
|
+
args.max_retries, args.retry_delay
|
291
|
+
)
|
292
|
+
|
293
|
+
if not upload_success:
|
294
|
+
logger.error("Failed to upload %s to TeddyCloud", taf_file)
|
295
|
+
else:
|
296
|
+
logger.info("Successfully uploaded %s to TeddyCloud", taf_file)
|
297
|
+
|
298
|
+
# Handle artwork upload if requested
|
299
|
+
if args.include_artwork:
|
300
|
+
# Extract folder path from the current task
|
301
|
+
folder_path = os.path.dirname(taf_file)
|
302
|
+
taf_file_basename = os.path.basename(taf_file)
|
303
|
+
taf_name = os.path.splitext(taf_file_basename)[0] # Get name without extension
|
304
|
+
logger.info("Looking for artwork for %s", folder_path)
|
305
|
+
|
306
|
+
# Try to find cover image in the folder
|
307
|
+
from .media_tags import find_cover_image
|
308
|
+
artwork_path = find_cover_image(folder_path)
|
309
|
+
temp_artwork = None
|
310
|
+
|
311
|
+
# If no cover image found, try to extract it from one of the audio files
|
312
|
+
if not artwork_path:
|
313
|
+
# Get current task's audio files
|
314
|
+
for task_name, task_folder, task_files in process_tasks:
|
315
|
+
if task_folder == folder_path or os.path.normpath(task_folder) == os.path.normpath(folder_path):
|
316
|
+
if task_files and len(task_files) > 0:
|
317
|
+
# Try to extract from first file
|
318
|
+
from .media_tags import extract_artwork, ensure_mutagen
|
319
|
+
if ensure_mutagen(auto_install=args.auto_download):
|
320
|
+
temp_artwork = extract_artwork(task_files[0])
|
321
|
+
if temp_artwork:
|
322
|
+
artwork_path = temp_artwork
|
323
|
+
break
|
324
|
+
|
325
|
+
if artwork_path:
|
326
|
+
logger.info("Found artwork for %s: %s", folder_path, artwork_path)
|
327
|
+
artwork_upload_path = "/custom_img"
|
328
|
+
artwork_ext = os.path.splitext(artwork_path)[1]
|
329
|
+
|
330
|
+
# Create a temporary copy with the same name as the taf file
|
331
|
+
import shutil
|
332
|
+
renamed_artwork_path = None
|
333
|
+
try:
|
334
|
+
renamed_artwork_path = os.path.join(os.path.dirname(artwork_path),
|
335
|
+
f"{taf_name}{artwork_ext}")
|
336
|
+
|
337
|
+
if renamed_artwork_path != artwork_path:
|
338
|
+
shutil.copy2(artwork_path, renamed_artwork_path)
|
339
|
+
logger.debug("Created renamed artwork copy: %s", renamed_artwork_path)
|
340
|
+
|
341
|
+
logger.info("Uploading artwork to path: %s as %s%s",
|
342
|
+
artwork_upload_path, taf_name, artwork_ext)
|
343
|
+
|
344
|
+
artwork_upload_success = upload_to_teddycloud(
|
345
|
+
renamed_artwork_path, teddycloud_url, args.ignore_ssl_verify,
|
346
|
+
args.special_folder, artwork_upload_path, args.show_progress,
|
347
|
+
args.connection_timeout, args.read_timeout,
|
348
|
+
args.max_retries, args.retry_delay
|
349
|
+
)
|
350
|
+
|
351
|
+
if artwork_upload_success:
|
352
|
+
logger.info("Successfully uploaded artwork for %s", folder_path)
|
353
|
+
else:
|
354
|
+
logger.warning("Failed to upload artwork for %s", folder_path)
|
355
|
+
|
356
|
+
if renamed_artwork_path != artwork_path and os.path.exists(renamed_artwork_path):
|
357
|
+
try:
|
358
|
+
os.unlink(renamed_artwork_path)
|
359
|
+
logger.debug("Removed temporary renamed artwork file: %s", renamed_artwork_path)
|
360
|
+
except Exception as e:
|
361
|
+
logger.debug("Failed to remove temporary renamed artwork file: %s", e)
|
362
|
+
|
363
|
+
if temp_artwork and os.path.exists(temp_artwork) and temp_artwork != renamed_artwork_path:
|
364
|
+
try:
|
365
|
+
os.unlink(temp_artwork)
|
366
|
+
logger.debug("Removed temporary artwork file: %s", temp_artwork)
|
367
|
+
except Exception as e:
|
368
|
+
logger.debug("Failed to remove temporary artwork file: %s", e)
|
369
|
+
except Exception as e:
|
370
|
+
logger.error("Error during artwork renaming or upload: %s", e)
|
371
|
+
else:
|
372
|
+
logger.warning("No artwork found for %s", folder_path)
|
174
373
|
sys.exit(0)
|
175
374
|
|
176
375
|
# Handle directory or file input
|
@@ -214,7 +413,6 @@ def main():
|
|
214
413
|
print(f"{tag_name}: {tag_value}")
|
215
414
|
else:
|
216
415
|
print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
|
217
|
-
|
218
416
|
sys.exit(0)
|
219
417
|
|
220
418
|
# Use media tags for file naming if requested
|
@@ -324,13 +522,159 @@ def main():
|
|
324
522
|
|
325
523
|
if not out_filename.lower().endswith('.taf'):
|
326
524
|
out_filename += '.taf'
|
327
|
-
|
525
|
+
|
328
526
|
logger.info("Creating Tonie file: %s with %d input file(s)", out_filename, len(files))
|
329
527
|
create_tonie_file(out_filename, files, args.no_tonie_header, args.user_timestamp,
|
330
528
|
args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
|
331
529
|
args.auto_download, not args.use_legacy_tags)
|
332
530
|
logger.info("Successfully created Tonie file: %s", out_filename)
|
531
|
+
|
532
|
+
# Handle upload to TeddyCloud if requested
|
533
|
+
if args.upload:
|
534
|
+
teddycloud_url = args.upload
|
535
|
+
|
536
|
+
upload_success = upload_to_teddycloud(
|
537
|
+
out_filename, teddycloud_url, args.ignore_ssl_verify,
|
538
|
+
args.special_folder, args.path, args.show_progress,
|
539
|
+
args.connection_timeout, args.read_timeout,
|
540
|
+
args.max_retries, args.retry_delay
|
541
|
+
)
|
542
|
+
if not upload_success:
|
543
|
+
logger.error("Failed to upload %s to TeddyCloud", out_filename)
|
544
|
+
sys.exit(1)
|
545
|
+
else:
|
546
|
+
logger.info("Successfully uploaded %s to TeddyCloud", out_filename)
|
547
|
+
|
548
|
+
# Handle artwork upload if requested
|
549
|
+
if args.include_artwork:
|
550
|
+
logger.info("Looking for artwork to upload alongside the Tonie file")
|
551
|
+
artwork_path = None
|
552
|
+
|
553
|
+
# Try to find a cover image in the source directory first
|
554
|
+
source_dir = os.path.dirname(files[0]) if files else None
|
555
|
+
if source_dir:
|
556
|
+
from .media_tags import find_cover_image
|
557
|
+
artwork_path = find_cover_image(source_dir)
|
558
|
+
|
559
|
+
# If no cover in source directory, try to extract it from audio file
|
560
|
+
if not artwork_path and len(files) > 0:
|
561
|
+
from .media_tags import extract_artwork, ensure_mutagen
|
562
|
+
|
563
|
+
# Make sure mutagen is available for artwork extraction
|
564
|
+
if ensure_mutagen(auto_install=args.auto_download):
|
565
|
+
# Try to extract artwork from the first file
|
566
|
+
temp_artwork = extract_artwork(files[0])
|
567
|
+
if temp_artwork:
|
568
|
+
artwork_path = temp_artwork
|
569
|
+
# Note: this creates a temporary file that will be deleted after upload
|
570
|
+
|
571
|
+
# Upload the artwork if found
|
572
|
+
if artwork_path:
|
573
|
+
logger.info("Found artwork: %s", artwork_path)
|
574
|
+
|
575
|
+
# Create artwork upload path - keep same path but use "custom_img" folder
|
576
|
+
artwork_upload_path = args.path
|
577
|
+
if not artwork_upload_path:
|
578
|
+
artwork_upload_path = "/custom_img"
|
579
|
+
elif not artwork_upload_path.startswith("/custom_img"):
|
580
|
+
# Make sure we're using the custom_img folder
|
581
|
+
if artwork_upload_path.startswith("/"):
|
582
|
+
artwork_upload_path = "/custom_img" + artwork_upload_path
|
583
|
+
else:
|
584
|
+
artwork_upload_path = "/custom_img/" + artwork_upload_path
|
585
|
+
|
586
|
+
# Get the original artwork file extension
|
587
|
+
artwork_ext = os.path.splitext(artwork_path)[1]
|
588
|
+
|
589
|
+
# Create a temporary copy with the same name as the taf file
|
590
|
+
import shutil
|
591
|
+
renamed_artwork_path = None
|
592
|
+
try:
|
593
|
+
renamed_artwork_path = os.path.join(os.path.dirname(artwork_path),
|
594
|
+
f"{os.path.splitext(os.path.basename(out_filename))[0]}{artwork_ext}")
|
595
|
+
|
596
|
+
if renamed_artwork_path != artwork_path:
|
597
|
+
shutil.copy2(artwork_path, renamed_artwork_path)
|
598
|
+
logger.debug("Created renamed artwork copy: %s", renamed_artwork_path)
|
599
|
+
|
600
|
+
logger.info("Uploading artwork to path: %s as %s%s",
|
601
|
+
artwork_upload_path, os.path.splitext(os.path.basename(out_filename))[0], artwork_ext)
|
602
|
+
|
603
|
+
artwork_upload_success = upload_to_teddycloud(
|
604
|
+
renamed_artwork_path, teddycloud_url, args.ignore_ssl_verify,
|
605
|
+
args.special_folder, artwork_upload_path, args.show_progress,
|
606
|
+
args.connection_timeout, args.read_timeout,
|
607
|
+
args.max_retries, args.retry_delay
|
608
|
+
)
|
609
|
+
|
610
|
+
if artwork_upload_success:
|
611
|
+
logger.info("Successfully uploaded artwork")
|
612
|
+
else:
|
613
|
+
logger.warning("Failed to upload artwork")
|
614
|
+
|
615
|
+
# Clean up temporary renamed file
|
616
|
+
if renamed_artwork_path != artwork_path and os.path.exists(renamed_artwork_path):
|
617
|
+
try:
|
618
|
+
os.unlink(renamed_artwork_path)
|
619
|
+
logger.debug("Removed temporary renamed artwork file: %s", renamed_artwork_path)
|
620
|
+
except Exception as e:
|
621
|
+
logger.debug("Failed to remove temporary renamed artwork file: %s", e)
|
622
|
+
|
623
|
+
# Clean up temporary extracted artwork file if needed
|
624
|
+
if temp_artwork and os.path.exists(temp_artwork) and temp_artwork != renamed_artwork_path:
|
625
|
+
try:
|
626
|
+
os.unlink(temp_artwork)
|
627
|
+
logger.debug("Removed temporary artwork file: %s", temp_artwork)
|
628
|
+
except Exception as e:
|
629
|
+
logger.debug("Failed to remove temporary artwork file: %s", e)
|
630
|
+
except Exception as e:
|
631
|
+
logger.error("Error during artwork renaming or upload: %s", e)
|
632
|
+
else:
|
633
|
+
logger.warning("No artwork found to upload")
|
333
634
|
|
635
|
+
# Handle create-custom-json option
|
636
|
+
if args.create_custom_json and args.upload:
|
637
|
+
teddycloud_url = args.upload
|
638
|
+
artwork_url = None
|
639
|
+
|
640
|
+
# If artwork was uploaded, construct its URL for the JSON
|
641
|
+
if args.include_artwork:
|
642
|
+
taf_basename = os.path.splitext(os.path.basename(out_filename))[0]
|
643
|
+
artwork_ext = None
|
644
|
+
|
645
|
+
# Try to determine the artwork extension by checking what was uploaded
|
646
|
+
source_dir = os.path.dirname(files[0]) if files else None
|
647
|
+
if source_dir:
|
648
|
+
from .media_tags import find_cover_image
|
649
|
+
artwork_path = find_cover_image(source_dir)
|
650
|
+
if artwork_path:
|
651
|
+
artwork_ext = os.path.splitext(artwork_path)[1]
|
652
|
+
|
653
|
+
# If we couldn't determine extension from a found image, default to .jpg
|
654
|
+
if not artwork_ext:
|
655
|
+
artwork_ext = ".jpg"
|
656
|
+
|
657
|
+
# Construct the URL for the artwork based on TeddyCloud structure
|
658
|
+
artwork_path = args.path or "/custom_img"
|
659
|
+
if not artwork_path.endswith('/'):
|
660
|
+
artwork_path += '/'
|
661
|
+
|
662
|
+
artwork_url = f"{teddycloud_url}{artwork_path}{taf_basename}{artwork_ext}"
|
663
|
+
logger.debug("Using artwork URL: %s", artwork_url)
|
664
|
+
|
665
|
+
logger.info("Fetching and updating custom Tonies JSON data")
|
666
|
+
success = fetch_and_update_tonies_json(
|
667
|
+
teddycloud_url,
|
668
|
+
args.ignore_ssl_verify,
|
669
|
+
out_filename,
|
670
|
+
files,
|
671
|
+
artwork_url
|
672
|
+
)
|
673
|
+
|
674
|
+
if success:
|
675
|
+
logger.info("Successfully updated custom Tonies JSON data")
|
676
|
+
else:
|
677
|
+
logger.warning("Failed to update custom Tonies JSON data")
|
334
678
|
|
335
679
|
if __name__ == "__main__":
|
336
680
|
main()
|
TonieToolbox/audio_conversion.py
CHANGED
@@ -28,6 +28,9 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
28
28
|
Returns:
|
29
29
|
tuple: (file handle, temp_file_path) or (file handle, None) if keep_temp is False
|
30
30
|
"""
|
31
|
+
logger.trace("Entering get_opus_tempfile(ffmpeg_binary=%s, opus_binary=%s, filename=%s, bitrate=%d, vbr=%s, keep_temp=%s, auto_download=%s)",
|
32
|
+
ffmpeg_binary, opus_binary, filename, bitrate, vbr, keep_temp, auto_download)
|
33
|
+
|
31
34
|
logger.debug("Converting %s to Opus format (bitrate: %d kbps, vbr: %s)", filename, bitrate, vbr)
|
32
35
|
|
33
36
|
if ffmpeg_binary is None:
|
@@ -59,18 +62,19 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
59
62
|
|
60
63
|
logger.debug("Starting FFmpeg process")
|
61
64
|
try:
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
+
ffmpeg_cmd = [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav", "-ar", "48000", "-"]
|
66
|
+
logger.trace("FFmpeg command: %s", ffmpeg_cmd)
|
67
|
+
ffmpeg_process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE)
|
65
68
|
except FileNotFoundError:
|
66
69
|
logger.error("Error opening input file %s", filename)
|
67
70
|
raise RuntimeError(f"Error opening input file {filename}")
|
68
71
|
|
69
72
|
logger.debug("Starting opusenc process")
|
70
73
|
try:
|
74
|
+
opusenc_cmd = [opus_binary, "--quiet", vbr_parameter, "--bitrate", f"{bitrate:d}", "-", temp_path]
|
75
|
+
logger.trace("Opusenc command: %s", opusenc_cmd)
|
71
76
|
opusenc_process = subprocess.Popen(
|
72
|
-
|
73
|
-
stdin=ffmpeg_process.stdout, stderr=subprocess.DEVNULL)
|
77
|
+
opusenc_cmd, stdin=ffmpeg_process.stdout, stderr=subprocess.DEVNULL)
|
74
78
|
except Exception as e:
|
75
79
|
logger.error("Opus encoding failed: %s", str(e))
|
76
80
|
raise RuntimeError(f"Opus encoding failed: {str(e)}")
|
@@ -79,6 +83,8 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
79
83
|
opusenc_return = opusenc_process.wait()
|
80
84
|
ffmpeg_return = ffmpeg_process.wait()
|
81
85
|
|
86
|
+
logger.debug("Process return codes - FFmpeg: %d, Opus: %d", ffmpeg_return, opusenc_return)
|
87
|
+
|
82
88
|
if ffmpeg_return != 0:
|
83
89
|
logger.error("FFmpeg processing failed with return code %d", ffmpeg_return)
|
84
90
|
raise RuntimeError(f"FFmpeg processing failed with return code {ffmpeg_return}")
|
@@ -87,9 +93,10 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
87
93
|
logger.error("Opus encoding failed with return code %d", opusenc_return)
|
88
94
|
raise RuntimeError(f"Opus encoding failed with return code {opusenc_return}")
|
89
95
|
|
90
|
-
logger.debug("Opening temporary file for reading")
|
96
|
+
logger.debug("Opening temporary file for reading: %s", temp_path)
|
91
97
|
try:
|
92
98
|
tmp_file = open(temp_path, "rb")
|
99
|
+
logger.trace("Exiting get_opus_tempfile() with persistent temporary file")
|
93
100
|
return tmp_file, temp_path
|
94
101
|
except Exception as e:
|
95
102
|
logger.error("Failed to open temporary file: %s", str(e))
|
@@ -99,18 +106,19 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
99
106
|
|
100
107
|
logger.debug("Starting FFmpeg process")
|
101
108
|
try:
|
102
|
-
|
103
|
-
|
104
|
-
|
109
|
+
ffmpeg_cmd = [ffmpeg_binary, "-hide_banner", "-loglevel", "warning", "-i", filename, "-f", "wav", "-ar", "48000", "-"]
|
110
|
+
logger.trace("FFmpeg command: %s", ffmpeg_cmd)
|
111
|
+
ffmpeg_process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE)
|
105
112
|
except FileNotFoundError:
|
106
113
|
logger.error("Error opening input file %s", filename)
|
107
114
|
raise RuntimeError(f"Error opening input file {filename}")
|
108
115
|
|
109
116
|
logger.debug("Starting opusenc process")
|
110
117
|
try:
|
118
|
+
opusenc_cmd = [opus_binary, "--quiet", vbr_parameter, "--bitrate", f"{bitrate:d}", "-", "-"]
|
119
|
+
logger.trace("Opusenc command: %s", opusenc_cmd)
|
111
120
|
opusenc_process = subprocess.Popen(
|
112
|
-
|
113
|
-
stdin=ffmpeg_process.stdout, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
121
|
+
opusenc_cmd, stdin=ffmpeg_process.stdout, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
114
122
|
except Exception as e:
|
115
123
|
logger.error("Opus encoding failed: %s", str(e))
|
116
124
|
raise RuntimeError(f"Opus encoding failed: {str(e)}")
|
@@ -124,10 +132,14 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
124
132
|
for chunk in iter(lambda: opusenc_process.stdout.read(4096), b""):
|
125
133
|
tmp_file.write(chunk)
|
126
134
|
bytes_written += len(chunk)
|
135
|
+
if bytes_written % (1024 * 1024) == 0: # Log every 1MB
|
136
|
+
logger.trace("Written %d bytes so far", bytes_written)
|
127
137
|
|
128
138
|
opusenc_return = opusenc_process.wait()
|
129
139
|
ffmpeg_return = ffmpeg_process.wait()
|
130
140
|
|
141
|
+
logger.debug("Process return codes - FFmpeg: %d, Opus: %d", ffmpeg_return, opusenc_return)
|
142
|
+
|
131
143
|
if ffmpeg_return != 0:
|
132
144
|
logger.error("FFmpeg processing failed with return code %d", ffmpeg_return)
|
133
145
|
raise RuntimeError(f"FFmpeg processing failed with return code {ffmpeg_return}")
|
@@ -139,6 +151,7 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
|
|
139
151
|
logger.debug("Wrote %d bytes to temporary file", bytes_written)
|
140
152
|
tmp_file.seek(0)
|
141
153
|
|
154
|
+
logger.trace("Exiting get_opus_tempfile() with in-memory temporary file")
|
142
155
|
return tmp_file, None
|
143
156
|
|
144
157
|
|
@@ -152,6 +165,7 @@ def filter_directories(glob_list):
|
|
152
165
|
Returns:
|
153
166
|
list: Filtered list containing only supported audio files
|
154
167
|
"""
|
168
|
+
logger.trace("Entering filter_directories() with %d items", len(glob_list))
|
155
169
|
logger.debug("Filtering %d glob results for supported audio files", len(glob_list))
|
156
170
|
|
157
171
|
# Common audio file extensions supported by ffmpeg
|
@@ -171,6 +185,7 @@ def filter_directories(glob_list):
|
|
171
185
|
logger.trace("Skipping unsupported file: %s", name)
|
172
186
|
|
173
187
|
logger.debug("Found %d supported audio files after filtering", len(filtered))
|
188
|
+
logger.trace("Exiting filter_directories() with %d files", len(filtered))
|
174
189
|
return filtered
|
175
190
|
|
176
191
|
|
@@ -186,6 +201,7 @@ def get_input_files(input_filename):
|
|
186
201
|
Returns:
|
187
202
|
list: List of input file paths
|
188
203
|
"""
|
204
|
+
logger.trace("Entering get_input_files(input_filename=%s)", input_filename)
|
189
205
|
logger.debug("Getting input files for pattern: %s", input_filename)
|
190
206
|
|
191
207
|
if input_filename.endswith(".lst"):
|
@@ -196,6 +212,7 @@ def get_input_files(input_filename):
|
|
196
212
|
for line_num, line in enumerate(file_list, 1):
|
197
213
|
fname = line.strip()
|
198
214
|
if not fname or fname.startswith('#'): # Skip empty lines and comments
|
215
|
+
logger.trace("Skipping empty line or comment at line %d", line_num)
|
199
216
|
continue
|
200
217
|
|
201
218
|
# Remove any quote characters from path
|
@@ -204,10 +221,10 @@ def get_input_files(input_filename):
|
|
204
221
|
# Check if the path is absolute or has a drive letter (Windows)
|
205
222
|
if os.path.isabs(fname) or (len(fname) > 1 and fname[1] == ':'):
|
206
223
|
full_path = fname # Use as is if it's an absolute path
|
207
|
-
logger.trace("Using absolute path from
|
224
|
+
logger.trace("Using absolute path from line %d: %s", line_num, full_path)
|
208
225
|
else:
|
209
226
|
full_path = os.path.join(list_dir, fname)
|
210
|
-
logger.trace("Using relative path from
|
227
|
+
logger.trace("Using relative path from line %d: %s -> %s", line_num, fname, full_path)
|
211
228
|
|
212
229
|
# Handle directory paths by finding all audio files in the directory
|
213
230
|
if os.path.isdir(full_path):
|
@@ -216,11 +233,12 @@ def get_input_files(input_filename):
|
|
216
233
|
dir_files = sorted(filter_directories(glob.glob(dir_glob)))
|
217
234
|
if dir_files:
|
218
235
|
input_files.extend(dir_files)
|
219
|
-
logger.debug("Found %d audio files in directory", len(dir_files))
|
236
|
+
logger.debug("Found %d audio files in directory from line %d", len(dir_files), line_num)
|
220
237
|
else:
|
221
238
|
logger.warning("No audio files found in directory at line %d: %s", line_num, full_path)
|
222
239
|
elif os.path.isfile(full_path):
|
223
240
|
input_files.append(full_path)
|
241
|
+
logger.trace("Added file from line %d: %s", line_num, full_path)
|
224
242
|
else:
|
225
243
|
logger.warning("File not found at line %d: %s", line_num, full_path)
|
226
244
|
|
@@ -230,6 +248,7 @@ def get_input_files(input_filename):
|
|
230
248
|
input_files = sorted(filter_directories(glob.glob(input_filename)))
|
231
249
|
logger.debug("Found %d files matching pattern", len(input_files))
|
232
250
|
|
251
|
+
logger.trace("Exiting get_input_files() with %d files", len(input_files))
|
233
252
|
return input_files
|
234
253
|
|
235
254
|
|
@@ -244,13 +263,16 @@ def append_to_filename(output_filename, tag):
|
|
244
263
|
Returns:
|
245
264
|
str: Modified filename with tag
|
246
265
|
"""
|
266
|
+
logger.trace("Entering append_to_filename(output_filename=%s, tag=%s)", output_filename, tag)
|
247
267
|
logger.debug("Appending tag '%s' to filename: %s", tag, output_filename)
|
248
268
|
pos = output_filename.rfind('.')
|
249
269
|
if pos == -1:
|
250
270
|
result = f"{output_filename}_{tag}"
|
251
271
|
logger.debug("No extension found, result: %s", result)
|
272
|
+
logger.trace("Exiting append_to_filename() with result=%s", result)
|
252
273
|
return result
|
253
274
|
else:
|
254
275
|
result = f"{output_filename[:pos]}_{tag}{output_filename[pos:]}"
|
255
276
|
logger.debug("Extension found, result: %s", result)
|
277
|
+
logger.trace("Exiting append_to_filename() with result=%s", result)
|
256
278
|
return result
|