TonieToolbox 0.5.1__py3-none-any.whl → 0.6.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 +2 -1
- TonieToolbox/__main__.py +240 -98
- TonieToolbox/artwork.py +59 -10
- TonieToolbox/audio_conversion.py +33 -29
- TonieToolbox/constants.py +133 -10
- TonieToolbox/dependency_manager.py +679 -184
- TonieToolbox/filename_generator.py +57 -10
- TonieToolbox/integration.py +73 -0
- TonieToolbox/integration_macos.py +613 -0
- TonieToolbox/integration_ubuntu.py +2 -0
- TonieToolbox/integration_windows.py +445 -0
- TonieToolbox/logger.py +9 -10
- TonieToolbox/media_tags.py +19 -100
- TonieToolbox/ogg_page.py +41 -41
- TonieToolbox/opus_packet.py +15 -15
- TonieToolbox/recursive_processor.py +24 -23
- TonieToolbox/tags.py +4 -5
- TonieToolbox/teddycloud.py +164 -51
- TonieToolbox/tonie_analysis.py +26 -24
- TonieToolbox/tonie_file.py +73 -45
- TonieToolbox/tonies_json.py +71 -67
- TonieToolbox/version_handler.py +14 -20
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0.dist-info}/METADATA +129 -92
- tonietoolbox-0.6.0.dist-info/RECORD +30 -0
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0.dist-info}/WHEEL +1 -1
- tonietoolbox-0.5.1.dist-info/RECORD +0 -26
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0.dist-info}/top_level.txt +0 -0
TonieToolbox/__init__.py
CHANGED
TonieToolbox/__main__.py
CHANGED
@@ -11,9 +11,9 @@ from . import __version__
|
|
11
11
|
from .audio_conversion import get_input_files, append_to_filename
|
12
12
|
from .tonie_file import create_tonie_file
|
13
13
|
from .tonie_analysis import check_tonie_file, check_tonie_file_cli, split_to_opus_files, compare_taf_files
|
14
|
-
from .dependency_manager import get_ffmpeg_binary, get_opus_binary
|
14
|
+
from .dependency_manager import get_ffmpeg_binary, get_opus_binary, ensure_dependency
|
15
15
|
from .logger import TRACE, setup_logging, get_logger
|
16
|
-
from .filename_generator import guess_output_filename
|
16
|
+
from .filename_generator import guess_output_filename, apply_template_to_path,ensure_directory_exists
|
17
17
|
from .version_handler import check_for_updates, clear_version_cache
|
18
18
|
from .recursive_processor import process_recursive_folders
|
19
19
|
from .media_tags import is_available as is_media_tags_available, ensure_mutagen, extract_album_info, format_metadata_filename, get_file_tags
|
@@ -21,6 +21,7 @@ from .teddycloud import TeddyCloudClient
|
|
21
21
|
from .tags import get_tags
|
22
22
|
from .tonies_json import fetch_and_update_tonies_json_v1, fetch_and_update_tonies_json_v2
|
23
23
|
from .artwork import upload_artwork
|
24
|
+
from .integration import handle_integration, handle_config
|
24
25
|
|
25
26
|
def main():
|
26
27
|
"""Entry point for the TonieToolbox application."""
|
@@ -40,7 +41,7 @@ def main():
|
|
40
41
|
teddycloud_group.add_argument('--special-folder', action='store', metavar='FOLDER',
|
41
42
|
help='Special folder to upload to (currently only "library" is supported)', default='library')
|
42
43
|
teddycloud_group.add_argument('--path', action='store', metavar='PATH',
|
43
|
-
help='Path where to write the file on TeddyCloud server')
|
44
|
+
help='Path where to write the file on TeddyCloud server (supports templates like "/{albumartist}/{album}")')
|
44
45
|
teddycloud_group.add_argument('--connection-timeout', type=int, metavar='SECONDS', default=10,
|
45
46
|
help='Connection timeout in seconds (default: 10)')
|
46
47
|
teddycloud_group.add_argument('--read-timeout', type=int, metavar='SECONDS', default=300,
|
@@ -98,13 +99,22 @@ def main():
|
|
98
99
|
parser.add_argument('-C', '--compare', action='store', metavar='FILE2',
|
99
100
|
help='Compare input file with another .taf file for debugging')
|
100
101
|
parser.add_argument('-D', '--detailed-compare', action='store_true',
|
101
|
-
help='Show detailed OGG page differences when comparing files')
|
102
|
+
help='Show detailed OGG page differences when comparing files')
|
103
|
+
# ------------- Parser - Context Menu Integration -------------
|
104
|
+
parser.add_argument('--config-integration', action='store_true',
|
105
|
+
help='Configure context menu integration')
|
106
|
+
parser.add_argument('--install-integration', action='store_true',
|
107
|
+
help='Integrate with the system (e.g., create context menu entries)')
|
108
|
+
parser.add_argument('--uninstall-integration', action='store_true',
|
109
|
+
help='Uninstall context menu integration')
|
102
110
|
# ------------- Parser - Media Tag Options -------------
|
103
111
|
media_tag_group = parser.add_argument_group('Media Tag Options')
|
104
112
|
media_tag_group.add_argument('-m', '--use-media-tags', action='store_true',
|
105
113
|
help='Use media tags from audio files for naming')
|
106
114
|
media_tag_group.add_argument('--name-template', metavar='TEMPLATE', action='store',
|
107
|
-
help='Template for naming files using media tags. Example: "{
|
115
|
+
help='Template for naming files using media tags. Example: "{albumartist} - {album}"')
|
116
|
+
media_tag_group.add_argument('--output-to-template', metavar='PATH_TEMPLATE', action='store',
|
117
|
+
help='Template for output path using media tags. Example: "C:\\Music\\{albumartist}\\{album}"')
|
108
118
|
media_tag_group.add_argument('--show-tags', action='store_true',
|
109
119
|
help='Show available media tags from input files')
|
110
120
|
# ------------- Parser - Version handling -------------
|
@@ -127,7 +137,7 @@ def main():
|
|
127
137
|
args = parser.parse_args()
|
128
138
|
|
129
139
|
# ------------- Parser - Source Input -------------
|
130
|
-
if args.input_filename is None and not (args.get_tags or args.upload):
|
140
|
+
if args.input_filename is None and not (args.get_tags or args.upload or args.install_integration or args.uninstall_integration or args.config_integration or args.auto_download):
|
131
141
|
parser.error("the following arguments are required: SOURCE")
|
132
142
|
|
133
143
|
# ------------- Logging -------------
|
@@ -142,7 +152,7 @@ def main():
|
|
142
152
|
else:
|
143
153
|
log_level = logging.INFO
|
144
154
|
setup_logging(log_level, log_to_file=args.log_file)
|
145
|
-
logger = get_logger(
|
155
|
+
logger = get_logger(__name__)
|
146
156
|
logger.debug("Starting TonieToolbox v%s with log level: %s", __version__, logging.getLevelName(log_level))
|
147
157
|
logger.debug("Command-line arguments: %s", vars(args))
|
148
158
|
|
@@ -167,6 +177,40 @@ def main():
|
|
167
177
|
if not is_latest and not update_confirmed and not (args.silent or args.quiet):
|
168
178
|
logger.info("Update available but user chose to continue without updating.")
|
169
179
|
|
180
|
+
# ------------- Autodownload & Dependency Checks -------------
|
181
|
+
if args.auto_download:
|
182
|
+
logger.debug("Auto-download requested for ffmpeg and opusenc")
|
183
|
+
ffmpeg_binary = get_ffmpeg_binary(auto_download=True)
|
184
|
+
opus_binary = get_opus_binary(auto_download=True)
|
185
|
+
if ffmpeg_binary and opus_binary:
|
186
|
+
logger.info("FFmpeg and opusenc downloaded successfully.")
|
187
|
+
if args.input_filename is None:
|
188
|
+
sys.exit(0)
|
189
|
+
else:
|
190
|
+
logger.error("Failed to download ffmpeg or opusenc. Please install them manually.")
|
191
|
+
sys.exit(1)
|
192
|
+
|
193
|
+
# ------------- Context Menu Integration -------------
|
194
|
+
if args.install_integration or args.uninstall_integration:
|
195
|
+
if ensure_dependency('ffmpeg') and ensure_dependency('opusenc'):
|
196
|
+
logger.debug("Context menu integration requested: install=%s, uninstall=%s",
|
197
|
+
args.install_integration, args.uninstall_integration)
|
198
|
+
success = handle_integration(args)
|
199
|
+
if success:
|
200
|
+
if args.install_integration:
|
201
|
+
logger.info("Context menu integration installed successfully")
|
202
|
+
else:
|
203
|
+
logger.info("Context menu integration uninstalled successfully")
|
204
|
+
else:
|
205
|
+
logger.error("Failed to handle context menu integration")
|
206
|
+
sys.exit(0)
|
207
|
+
else:
|
208
|
+
logger.error("FFmpeg and opusenc are required for context menu integration")
|
209
|
+
sys.exit(1)
|
210
|
+
if args.config_integration:
|
211
|
+
logger.debug("Opening configuration file for editing")
|
212
|
+
handle_config()
|
213
|
+
sys.exit(0)
|
170
214
|
# ------------- Normalize Path Input -------------
|
171
215
|
if args.input_filename:
|
172
216
|
logger.debug("Original input path: %s", args.input_filename)
|
@@ -224,89 +268,97 @@ def main():
|
|
224
268
|
print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
|
225
269
|
sys.exit(0)
|
226
270
|
# ------------- Direct Upload -------------
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
if args.upload and not args.recursive and file_ext == '.taf':
|
233
|
-
logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
|
234
|
-
logger.trace("TeddyCloud upload parameters: path=%s, special_folder=%s, ignore_ssl=%s",
|
235
|
-
args.path, args.special_folder, args.ignore_ssl_verify)
|
236
|
-
|
237
|
-
|
238
|
-
logger.debug("File to upload: %s (size: %d bytes, type: %s)",
|
239
|
-
file_path, file_size, file_ext)
|
240
|
-
logger.info("Uploading %s to TeddyCloud %s", file_path, teddycloud_url)
|
241
|
-
logger.trace("Starting upload process for %s", file_path)
|
242
|
-
response = client.upload_file(
|
243
|
-
destination_path=args.path,
|
244
|
-
file_path=file_path,
|
245
|
-
special=args.special_folder,
|
246
|
-
)
|
247
|
-
logger.trace("Upload response received: %s", response)
|
248
|
-
upload_success = response.get('success', False)
|
249
|
-
if not upload_success:
|
250
|
-
error_msg = response.get('message', 'Unknown error')
|
251
|
-
logger.error("Failed to upload %s to TeddyCloud: %s (HTTP Status: %s, Response: %s)",
|
252
|
-
file_path, error_msg, response.get('status_code', 'Unknown'), response)
|
253
|
-
logger.trace("Exiting with code 1 due to upload failure")
|
254
|
-
sys.exit(1)
|
255
|
-
else:
|
256
|
-
logger.info("Successfully uploaded %s to TeddyCloud", file_path)
|
257
|
-
logger.debug("Upload response details: %s",
|
258
|
-
{k: v for k, v in response.items() if k != 'success'})
|
259
|
-
artwork_url = None
|
260
|
-
if args.include_artwork and file_path.lower().endswith('.taf'):
|
261
|
-
source_dir = os.path.dirname(file_path)
|
262
|
-
logger.info("Looking for artwork to upload for %s", file_path)
|
263
|
-
logger.debug("Searching for artwork in directory: %s", source_dir)
|
264
|
-
logger.trace("Calling upload_artwork function")
|
265
|
-
success, artwork_url = upload_artwork(client, file_path, source_dir, [])
|
266
|
-
logger.trace("upload_artwork returned: success=%s, artwork_url=%s",
|
267
|
-
success, artwork_url)
|
268
|
-
if success:
|
269
|
-
logger.info("Successfully uploaded artwork for %s", file_path)
|
270
|
-
logger.debug("Artwork URL: %s", artwork_url)
|
271
|
-
else:
|
272
|
-
logger.warning("Failed to upload artwork for %s", file_path)
|
273
|
-
logger.debug("No suitable artwork found or upload failed")
|
274
|
-
if args.create_custom_json and file_path.lower().endswith('.taf'):
|
275
|
-
output_dir = './output'
|
276
|
-
logger.debug("Creating/ensuring output directory for JSON: %s", output_dir)
|
277
|
-
if not os.path.exists(output_dir):
|
278
|
-
os.makedirs(output_dir, exist_ok=True)
|
279
|
-
logger.trace("Created output directory: %s", output_dir)
|
280
|
-
logger.debug("Updating tonies.custom.json with: taf=%s, artwork_url=%s",
|
281
|
-
file_path, artwork_url)
|
282
|
-
client_param = client
|
271
|
+
if os.path.exists(args.input_filename) and os.path.isfile(args.input_filename):
|
272
|
+
file_path = args.input_filename
|
273
|
+
file_size = os.path.getsize(file_path)
|
274
|
+
file_ext = os.path.splitext(file_path)[1].lower()
|
283
275
|
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
276
|
+
if args.upload and not args.recursive and file_ext == '.taf':
|
277
|
+
logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
|
278
|
+
logger.trace("TeddyCloud upload parameters: path=%s, special_folder=%s, ignore_ssl=%s",
|
279
|
+
args.path, args.special_folder, args.ignore_ssl_verify)
|
280
|
+
logger.debug("File to upload: %s (size: %d bytes, type: %s)",
|
281
|
+
file_path, file_size, file_ext)
|
282
|
+
logger.info("Uploading %s to TeddyCloud %s", file_path, teddycloud_url)
|
283
|
+
logger.trace("Starting upload process for %s", file_path)
|
284
|
+
|
285
|
+
upload_path = args.path
|
286
|
+
if upload_path and '{' in upload_path and args.use_media_tags:
|
287
|
+
metadata = get_file_tags(file_path)
|
288
|
+
if metadata:
|
289
|
+
formatted_path = apply_template_to_path(upload_path, metadata)
|
290
|
+
if formatted_path:
|
291
|
+
logger.info("Using dynamic upload path from template: %s", formatted_path)
|
292
|
+
upload_path = formatted_path
|
291
293
|
else:
|
292
|
-
logger.warning("
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
294
|
+
logger.warning("Could not apply all tags to path template '%s'. Using as-is.", upload_path)
|
295
|
+
|
296
|
+
# Create directories recursively if path is provided
|
297
|
+
if upload_path:
|
298
|
+
logger.debug("Creating directory structure on server: %s", upload_path)
|
299
|
+
try:
|
300
|
+
client.create_directories_recursive(
|
301
|
+
path=upload_path,
|
302
|
+
special=args.special_folder
|
303
|
+
)
|
304
|
+
logger.debug("Successfully created directory structure on server")
|
305
|
+
except Exception as e:
|
306
|
+
logger.warning("Failed to create directory structure on server: %s", str(e))
|
307
|
+
logger.debug("Continuing with upload anyway, in case the directory already exists")
|
308
|
+
|
309
|
+
response = client.upload_file(
|
310
|
+
destination_path=upload_path,
|
311
|
+
file_path=file_path,
|
312
|
+
special=args.special_folder,
|
313
|
+
)
|
314
|
+
logger.trace("Upload response received: %s", response)
|
315
|
+
upload_success = response.get('success', False)
|
316
|
+
if not upload_success:
|
317
|
+
error_msg = response.get('message', 'Unknown error')
|
318
|
+
logger.error("Failed to upload %s to TeddyCloud: %s (HTTP Status: %s, Response: %s)",
|
319
|
+
file_path, error_msg, response.get('status_code', 'Unknown'), response)
|
320
|
+
logger.trace("Exiting with code 1 due to upload failure")
|
305
321
|
sys.exit(1)
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
322
|
+
else:
|
323
|
+
logger.info("Successfully uploaded %s to TeddyCloud", file_path)
|
324
|
+
logger.debug("Upload response details: %s",
|
325
|
+
{k: v for k, v in response.items() if k != 'success'})
|
326
|
+
artwork_url = None
|
327
|
+
if args.include_artwork and file_path.lower().endswith('.taf'):
|
328
|
+
source_dir = os.path.dirname(file_path)
|
329
|
+
logger.info("Looking for artwork to upload for %s", file_path)
|
330
|
+
logger.debug("Searching for artwork in directory: %s", source_dir)
|
331
|
+
logger.trace("Calling upload_artwork function")
|
332
|
+
success, artwork_url = upload_artwork(client, file_path, source_dir, [])
|
333
|
+
logger.trace("upload_artwork returned: success=%s, artwork_url=%s",
|
334
|
+
success, artwork_url)
|
335
|
+
if success:
|
336
|
+
logger.info("Successfully uploaded artwork for %s", file_path)
|
337
|
+
logger.debug("Artwork URL: %s", artwork_url)
|
338
|
+
else:
|
339
|
+
logger.warning("Failed to upload artwork for %s", file_path)
|
340
|
+
logger.debug("No suitable artwork found or upload failed")
|
341
|
+
if args.create_custom_json and file_path.lower().endswith('.taf'):
|
342
|
+
output_dir = './output'
|
343
|
+
logger.debug("Creating/ensuring output directory for JSON: %s", output_dir)
|
344
|
+
if not os.path.exists(output_dir):
|
345
|
+
os.makedirs(output_dir, exist_ok=True)
|
346
|
+
logger.trace("Created output directory: %s", output_dir)
|
347
|
+
logger.debug("Updating tonies.custom.json with: taf=%s, artwork_url=%s",
|
348
|
+
file_path, artwork_url)
|
349
|
+
client_param = client
|
350
|
+
if args.version_2:
|
351
|
+
logger.debug("Using version 2 of the Tonies JSON format")
|
352
|
+
success = fetch_and_update_tonies_json_v2(client_param, file_path, [], artwork_url, output_dir)
|
353
|
+
else:
|
354
|
+
success = fetch_and_update_tonies_json_v1(client_param, file_path, [], artwork_url, output_dir)
|
355
|
+
if success:
|
356
|
+
logger.info("Successfully updated Tonies JSON for %s", file_path)
|
357
|
+
else:
|
358
|
+
logger.warning("Failed to update Tonies JSON for %s", file_path)
|
359
|
+
logger.debug("fetch_and_update_tonies_json returned failure")
|
360
|
+
logger.trace("Exiting after direct upload with code 0")
|
361
|
+
sys.exit(0)
|
310
362
|
|
311
363
|
# ------------- Librarys / Prereqs -------------
|
312
364
|
logger.debug("Checking for external dependencies")
|
@@ -458,25 +510,25 @@ def main():
|
|
458
510
|
|
459
511
|
guessed_name = None
|
460
512
|
if args.use_media_tags:
|
513
|
+
logger.debug("Using media tags for naming")
|
461
514
|
if len(files) > 1 and os.path.dirname(files[0]) == os.path.dirname(files[-1]):
|
515
|
+
logger.debug("Multiple files in the same folder, trying to extract album info")
|
462
516
|
folder_path = os.path.dirname(files[0])
|
463
517
|
logger.debug("Extracting album info from folder: %s", folder_path)
|
464
518
|
album_info = extract_album_info(folder_path)
|
465
519
|
if album_info:
|
466
|
-
template = args.name_template or "{
|
467
|
-
new_name = format_metadata_filename(album_info, template)
|
468
|
-
|
520
|
+
template = args.name_template or "{artist} - {album}"
|
521
|
+
new_name = format_metadata_filename(album_info, template)
|
469
522
|
if new_name:
|
470
523
|
logger.info("Using album metadata for output filename: %s", new_name)
|
471
524
|
guessed_name = new_name
|
472
525
|
else:
|
473
526
|
logger.debug("Could not format filename from album metadata")
|
474
527
|
elif len(files) == 1:
|
475
|
-
|
476
|
-
|
477
528
|
tags = get_file_tags(files[0])
|
478
529
|
if tags:
|
479
|
-
|
530
|
+
logger.debug("")
|
531
|
+
template = args.name_template or "{artist} - {title}"
|
480
532
|
new_name = format_metadata_filename(tags, template)
|
481
533
|
|
482
534
|
if new_name:
|
@@ -513,20 +565,71 @@ def main():
|
|
513
565
|
else:
|
514
566
|
logger.debug("Could not format filename from common metadata")
|
515
567
|
|
516
|
-
if args.output_filename:
|
568
|
+
if args.output_filename:
|
517
569
|
out_filename = args.output_filename
|
570
|
+
logger.debug("Output filename specified: %s", out_filename)
|
571
|
+
elif args.output_to_template and args.use_media_tags:
|
572
|
+
# Get metadata from files
|
573
|
+
if len(files) > 1 and os.path.dirname(files[0]) == os.path.dirname(files[-1]):
|
574
|
+
metadata = extract_album_info(os.path.dirname(files[0]))
|
575
|
+
elif len(files) == 1:
|
576
|
+
metadata = get_file_tags(files[0])
|
577
|
+
else:
|
578
|
+
# Try to get common tags for multiple files
|
579
|
+
metadata = {}
|
580
|
+
for file_path in files:
|
581
|
+
tags = get_file_tags(file_path)
|
582
|
+
if tags:
|
583
|
+
for key, value in tags.items():
|
584
|
+
if key not in metadata:
|
585
|
+
metadata[key] = value
|
586
|
+
elif metadata[key] != value:
|
587
|
+
metadata[key] = None
|
588
|
+
metadata = {k: v for k, v in metadata.items() if v is not None}
|
589
|
+
|
590
|
+
if metadata:
|
591
|
+
formatted_path = apply_template_to_path(args.output_to_template, metadata)
|
592
|
+
logger.debug("Formatted path from template: %s", formatted_path)
|
593
|
+
if formatted_path:
|
594
|
+
ensure_directory_exists(formatted_path)
|
595
|
+
if guessed_name:
|
596
|
+
logger.debug("Using guessed name for output: %s", guessed_name)
|
597
|
+
out_filename = os.path.join(formatted_path, guessed_name)
|
598
|
+
else:
|
599
|
+
logger.debug("Using template path for output: %s", formatted_path)
|
600
|
+
out_filename = formatted_path
|
601
|
+
logger.info("Using template path for output: %s", out_filename)
|
602
|
+
else:
|
603
|
+
logger.warning("Could not apply template to path. Using default output location.")
|
604
|
+
# Fall back to default output handling
|
605
|
+
if guessed_name:
|
606
|
+
logger.debug("Using guessed name for output: %s", guessed_name)
|
607
|
+
if args.output_to_source:
|
608
|
+
source_dir = os.path.dirname(files[0]) if files else '.'
|
609
|
+
out_filename = os.path.join(source_dir, guessed_name)
|
610
|
+
logger.debug("Using source location for output: %s", out_filename)
|
611
|
+
else:
|
612
|
+
output_dir = './output'
|
613
|
+
if not os.path.exists(output_dir):
|
614
|
+
os.makedirs(output_dir, exist_ok=True)
|
615
|
+
out_filename = os.path.join(output_dir, guessed_name)
|
616
|
+
logger.debug("Using default output location: %s", out_filename)
|
617
|
+
else:
|
618
|
+
logger.warning("No metadata available to apply to template path. Using default output location.")
|
619
|
+
# Fall back to default output handling
|
518
620
|
elif guessed_name:
|
621
|
+
logger.debug("Using guessed name for output: %s", guessed_name)
|
519
622
|
if args.output_to_source:
|
520
623
|
source_dir = os.path.dirname(files[0]) if files else '.'
|
521
624
|
out_filename = os.path.join(source_dir, guessed_name)
|
522
|
-
logger.debug("Using source location for output
|
625
|
+
logger.debug("Using source location for output: %s", out_filename)
|
523
626
|
else:
|
524
627
|
output_dir = './output'
|
525
628
|
if not os.path.exists(output_dir):
|
526
629
|
logger.debug("Creating default output directory: %s", output_dir)
|
527
630
|
os.makedirs(output_dir, exist_ok=True)
|
528
631
|
out_filename = os.path.join(output_dir, guessed_name)
|
529
|
-
logger.debug("Using default output location
|
632
|
+
logger.debug("Using default output location: %s", out_filename)
|
530
633
|
else:
|
531
634
|
guessed_name = guess_output_filename(args.input_filename, files)
|
532
635
|
if args.output_to_source:
|
@@ -556,6 +659,7 @@ def main():
|
|
556
659
|
|
557
660
|
if not out_filename.lower().endswith('.taf'):
|
558
661
|
out_filename += '.taf'
|
662
|
+
ensure_directory_exists(out_filename)
|
559
663
|
|
560
664
|
logger.info("Creating Tonie file: %s with %d input file(s)", out_filename, len(files))
|
561
665
|
create_tonie_file(out_filename, files, args.no_tonie_header, args.user_timestamp,
|
@@ -566,10 +670,48 @@ def main():
|
|
566
670
|
|
567
671
|
# ------------- Single File Upload -------------
|
568
672
|
artwork_url = None
|
569
|
-
if args.upload:
|
673
|
+
if args.upload:
|
674
|
+
upload_path = args.path
|
675
|
+
if upload_path and '{' in upload_path and args.use_media_tags:
|
676
|
+
metadata = {}
|
677
|
+
if len(files) > 1 and os.path.dirname(files[0]) == os.path.dirname(files[-1]):
|
678
|
+
metadata = extract_album_info(os.path.dirname(files[0]))
|
679
|
+
elif len(files) == 1:
|
680
|
+
metadata = get_file_tags(files[0])
|
681
|
+
else:
|
682
|
+
for file_path in files:
|
683
|
+
tags = get_file_tags(file_path)
|
684
|
+
if tags:
|
685
|
+
for key, value in tags.items():
|
686
|
+
if key not in metadata:
|
687
|
+
metadata[key] = value
|
688
|
+
elif metadata[key] != value:
|
689
|
+
metadata[key] = None
|
690
|
+
metadata = {k: v for k, v in metadata.items() if v is not None}
|
691
|
+
if metadata:
|
692
|
+
formatted_path = apply_template_to_path(upload_path, metadata)
|
693
|
+
if formatted_path:
|
694
|
+
logger.info("Using dynamic upload path from template: %s", formatted_path)
|
695
|
+
upload_path = formatted_path
|
696
|
+
else:
|
697
|
+
logger.warning("Could not apply all tags to path template '%s'. Using as-is.", upload_path)
|
698
|
+
|
699
|
+
# Create directories recursively if path is provided
|
700
|
+
if upload_path:
|
701
|
+
logger.debug("Creating directory structure on server: %s", upload_path)
|
702
|
+
try:
|
703
|
+
client.create_directories_recursive(
|
704
|
+
path=upload_path,
|
705
|
+
special=args.special_folder
|
706
|
+
)
|
707
|
+
logger.debug("Successfully created directory structure on server")
|
708
|
+
except Exception as e:
|
709
|
+
logger.warning("Failed to create directory structure on server: %s", str(e))
|
710
|
+
logger.debug("Continuing with upload anyway, in case the directory already exists")
|
711
|
+
|
570
712
|
response = client.upload_file(
|
571
713
|
file_path=out_filename,
|
572
|
-
destination_path=
|
714
|
+
destination_path=upload_path,
|
573
715
|
special=args.special_folder,
|
574
716
|
)
|
575
717
|
upload_success = response.get('success', False)
|
TonieToolbox/artwork.py
CHANGED
@@ -4,6 +4,7 @@ Artwork handling functionality for TonieToolbox.
|
|
4
4
|
"""
|
5
5
|
|
6
6
|
import os
|
7
|
+
import base64
|
7
8
|
import tempfile
|
8
9
|
import shutil
|
9
10
|
from typing import List, Optional, Tuple
|
@@ -12,20 +13,25 @@ from .logger import get_logger
|
|
12
13
|
from .teddycloud import TeddyCloudClient
|
13
14
|
from .media_tags import extract_artwork, find_cover_image
|
14
15
|
|
16
|
+
logger = get_logger(__name__)
|
15
17
|
|
16
|
-
def upload_artwork(
|
18
|
+
def upload_artwork(
|
19
|
+
client: TeddyCloudClient,
|
20
|
+
taf_filename: str,
|
21
|
+
source_path: str,
|
22
|
+
audio_files: list[str],
|
23
|
+
) -> tuple[bool, Optional[str]]:
|
17
24
|
"""
|
18
25
|
Find and upload artwork for a Tonie file.
|
19
|
-
|
26
|
+
|
20
27
|
Args:
|
21
|
-
client: TeddyCloudClient instance to use for API communication
|
22
|
-
taf_filename: The filename of the Tonie file (.taf)
|
23
|
-
source_path: Source directory to look for artwork
|
24
|
-
audio_files: List of audio files to extract artwork from if needed
|
28
|
+
client (TeddyCloudClient): TeddyCloudClient instance to use for API communication
|
29
|
+
taf_filename (str): The filename of the Tonie file (.taf)
|
30
|
+
source_path (str): Source directory to look for artwork
|
31
|
+
audio_files (list[str]): List of audio files to extract artwork from if needed
|
25
32
|
Returns:
|
26
|
-
tuple: (success, artwork_url) where success is a boolean and artwork_url is the URL of the uploaded artwork
|
27
|
-
"""
|
28
|
-
logger = get_logger('artwork')
|
33
|
+
tuple[bool, Optional[str]]: (success, artwork_url) where success is a boolean and artwork_url is the URL of the uploaded artwork
|
34
|
+
"""
|
29
35
|
logger.info("Looking for artwork for Tonie file: %s", taf_filename)
|
30
36
|
taf_basename = os.path.basename(taf_filename)
|
31
37
|
taf_name = os.path.splitext(taf_basename)[0]
|
@@ -102,4 +108,47 @@ def upload_artwork(client: TeddyCloudClient, taf_filename, source_path, audio_fi
|
|
102
108
|
except Exception as e:
|
103
109
|
logger.debug("Failed to remove temporary artwork file: %s", e)
|
104
110
|
|
105
|
-
return upload_success, artwork_url
|
111
|
+
return upload_success, artwork_url
|
112
|
+
|
113
|
+
def ico_to_base64(ico_path):
|
114
|
+
"""
|
115
|
+
Convert an ICO file to a base64 string
|
116
|
+
|
117
|
+
Args:
|
118
|
+
ico_path: Path to the ICO file
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
Base64 encoded string of the ICO file
|
122
|
+
"""
|
123
|
+
if not os.path.exists(ico_path):
|
124
|
+
raise FileNotFoundError(f"ICO file not found: {ico_path}")
|
125
|
+
|
126
|
+
with open(ico_path, "rb") as ico_file:
|
127
|
+
ico_bytes = ico_file.read()
|
128
|
+
|
129
|
+
base64_string = base64.b64encode(ico_bytes).decode('utf-8')
|
130
|
+
return base64_string
|
131
|
+
|
132
|
+
|
133
|
+
def base64_to_ico(base64_string, output_path):
|
134
|
+
"""
|
135
|
+
Convert a base64 string back to an ICO file
|
136
|
+
|
137
|
+
Args:
|
138
|
+
base64_string: Base64 encoded string of the ICO file
|
139
|
+
output_path: Path where to save the ICO file
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
Path to the saved ICO file
|
143
|
+
"""
|
144
|
+
ico_bytes = base64.b64decode(base64_string)
|
145
|
+
|
146
|
+
# Create directory if it doesn't exist
|
147
|
+
output_dir = os.path.dirname(output_path)
|
148
|
+
if output_dir and not os.path.exists(output_dir):
|
149
|
+
os.makedirs(output_dir)
|
150
|
+
|
151
|
+
with open(output_path, "wb") as ico_file:
|
152
|
+
ico_file.write(ico_bytes)
|
153
|
+
|
154
|
+
return output_path
|