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 CHANGED
@@ -1,5 +1,6 @@
1
+ #!/usr/bin/python3
1
2
  """
2
3
  TonieToolbox - Convert audio files to Tonie box compatible format
3
4
  """
4
5
 
5
- __version__ = '0.5.1'
6
+ __version__ = '0.6.0'
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: "{album} - {artist}"')
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('main')
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
- if os.path.exists(args.input_filename) and os.path.isfile(args.input_filename):
228
- file_path = args.input_filename
229
- file_size = os.path.getsize(file_path)
230
- file_ext = os.path.splitext(file_path)[1].lower()
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
- if args.version_2:
285
- logger.debug("Using version 2 of the Tonies JSON format")
286
- success = fetch_and_update_tonies_json_v2(client_param, file_path, [], artwork_url, output_dir)
287
- else:
288
- success = fetch_and_update_tonies_json_v1(client_param, file_path, [], artwork_url, output_dir)
289
- if success:
290
- logger.info("Successfully updated Tonies JSON for %s", file_path)
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("Failed to update Tonies JSON for %s", file_path)
293
- logger.debug("fetch_and_update_tonies_json returned failure")
294
- logger.trace("Exiting after direct upload with code 0")
295
- sys.exit(0)
296
- elif not args.recursive:
297
- if not os.path.exists(args.input_filename):
298
- logger.error("File not found: %s", args.input_filename)
299
- elif not os.path.isfile(args.input_filename):
300
- logger.error("Not a regular file: %s", args.input_filename)
301
- logger.debug("File exists: %s, Is file: %s",
302
- os.path.exists(args.input_filename),
303
- os.path.isfile(args.input_filename) if os.path.exists(args.input_filename) else False)
304
- logger.trace("Exiting with code 1 due to invalid input file")
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
- if args.recursive and args.upload:
308
- logger.info("Recursive mode with upload enabled: %s -> %s", args.input_filename, teddycloud_url)
309
- logger.debug("Will process all files in directory recursively and upload to TeddyCloud")
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 "{album} - {artist}"
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
- template = args.name_template or "{title} - {artist}"
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 with media tags: %s", out_filename)
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 with media tags: %s", out_filename)
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=args.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(client: TeddyCloudClient, taf_filename, source_path, audio_files) -> Tuple[bool, Optional[str]]:
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