TonieToolbox 0.5.0a1__py3-none-any.whl → 0.6.0a1__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
@@ -2,4 +2,4 @@
2
2
  TonieToolbox - Convert audio files to Tonie box compatible format
3
3
  """
4
4
 
5
- __version__ = '0.5.0a1'
5
+ __version__ = '0.6.0a1'
TonieToolbox/__main__.py CHANGED
@@ -10,18 +10,18 @@ import logging
10
10
  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
- from .tonie_analysis import check_tonie_file, check_tonie_file_cli, split_to_opus_files
13
+ from .tonie_analysis import check_tonie_file, check_tonie_file_cli, split_to_opus_files, compare_taf_files
14
14
  from .dependency_manager import get_ffmpeg_binary, get_opus_binary
15
- from .logger import setup_logging, get_logger
15
+ from .logger import TRACE, setup_logging, get_logger
16
16
  from .filename_generator import guess_output_filename
17
17
  from .version_handler import check_for_updates, clear_version_cache
18
18
  from .recursive_processor import process_recursive_folders
19
- from .media_tags import is_available as is_media_tags_available, ensure_mutagen, extract_album_info, format_metadata_filename
19
+ from .media_tags import is_available as is_media_tags_available, ensure_mutagen, extract_album_info, format_metadata_filename, get_file_tags
20
20
  from .teddycloud import TeddyCloudClient
21
21
  from .tags import get_tags
22
- from .tonies_json import fetch_and_update_tonies_json
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
-
24
+ from .integration import handle_integration
25
25
 
26
26
  def main():
27
27
  """Entry point for the TonieToolbox application."""
@@ -52,6 +52,8 @@ def main():
52
52
  help='Delay between retry attempts in seconds (default: 5)')
53
53
  teddycloud_group.add_argument('--create-custom-json', action='store_true',
54
54
  help='Fetch and update custom Tonies JSON data')
55
+ teddycloud_group.add_argument('--version-2', action='store_true',
56
+ help='Use version 2 of the Tonies JSON format (default: version 1)')
55
57
  # ------------- Parser - Authentication options for TeddyCloud -------------
56
58
  teddycloud_group.add_argument('--username', action='store', metavar='USERNAME',
57
59
  help='Username for basic authentication')
@@ -87,6 +89,8 @@ def main():
87
89
  help='Save output files in the source directory instead of output directory')
88
90
  parser.add_argument('-fc', '--force-creation', action='store_true', default=False,
89
91
  help='Force creation of Tonie file even if it already exists')
92
+ parser.add_argument('--no-mono-conversion', action='store_true',
93
+ help='Do not convert mono audio to stereo (default: convert mono to stereo)')
90
94
  # ------------- Parser - Debug TAFs -------------
91
95
  parser.add_argument('-k', '--keep-temp', action='store_true',
92
96
  help='Keep temporary opus files in a temp folder for testing')
@@ -95,7 +99,12 @@ def main():
95
99
  parser.add_argument('-C', '--compare', action='store', metavar='FILE2',
96
100
  help='Compare input file with another .taf file for debugging')
97
101
  parser.add_argument('-D', '--detailed-compare', action='store_true',
98
- 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('--install-integration', action='store_true',
105
+ help='Integrate with the system (e.g., create context menu entries)')
106
+ parser.add_argument('--uninstall-integration', action='store_true',
107
+ help='Uninstall context menu integration')
99
108
  # ------------- Parser - Media Tag Options -------------
100
109
  media_tag_group = parser.add_argument_group('Media Tag Options')
101
110
  media_tag_group.add_argument('-m', '--use-media-tags', action='store_true',
@@ -124,12 +133,11 @@ def main():
124
133
  args = parser.parse_args()
125
134
 
126
135
  # ------------- Parser - Source Input -------------
127
- if args.input_filename is None and not (args.get_tags or args.upload):
136
+ if args.input_filename is None and not (args.get_tags or args.upload or args.install_integration or args.uninstall_integration):
128
137
  parser.error("the following arguments are required: SOURCE")
129
138
 
130
139
  # ------------- Logging -------------
131
140
  if args.trace:
132
- from .logger import TRACE
133
141
  log_level = TRACE
134
142
  elif args.debug:
135
143
  log_level = logging.DEBUG
@@ -164,7 +172,19 @@ def main():
164
172
 
165
173
  if not is_latest and not update_confirmed and not (args.silent or args.quiet):
166
174
  logger.info("Update available but user chose to continue without updating.")
167
-
175
+ # ------------- Context Menu Integration -------------
176
+ if args.install_integration or args.uninstall_integration:
177
+ logger.debug("Context menu integration requested: install=%s, uninstall=%s",
178
+ args.install_integration, args.uninstall_integration)
179
+ success = handle_integration(args)
180
+ if success:
181
+ if args.install_integration:
182
+ logger.info("Context menu integration installed successfully")
183
+ else:
184
+ logger.info("Context menu integration uninstalled successfully")
185
+ else:
186
+ logger.error("Failed to handle context menu integration")
187
+ sys.exit(0)
168
188
  # ------------- Normalize Path Input -------------
169
189
  if args.input_filename:
170
190
  logger.debug("Original input path: %s", args.input_filename)
@@ -203,93 +223,91 @@ def main():
203
223
  success = get_tags(client)
204
224
  logger.debug( "Exiting with code %d", 0 if success else 1)
205
225
  sys.exit(0 if success else 1)
206
-
207
- # ------------- Direct Upload -------------
208
- if args.upload and not args.recursive:
226
+
227
+ # ------------- Show Media Tags -------------
228
+ if args.show_tags:
229
+ files = get_input_files(args.input_filename)
230
+ logger.debug("Found %d files to process", len(files))
231
+ if len(files) == 0:
232
+ logger.error("No files found for pattern %s", args.input_filename)
233
+ sys.exit(1)
234
+ for file_index, file_path in enumerate(files):
235
+ tags = get_file_tags(file_path)
236
+ if tags:
237
+ print(f"\nFile {file_index + 1}: {os.path.basename(file_path)}")
238
+ print("-" * 40)
239
+ for tag_name, tag_value in sorted(tags.items()):
240
+ print(f"{tag_name}: {tag_value}")
241
+ else:
242
+ print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
243
+ sys.exit(0)
244
+ # ------------- Direct Upload -------------
245
+ if os.path.exists(args.input_filename) and os.path.isfile(args.input_filename):
246
+ file_path = args.input_filename
247
+ file_size = os.path.getsize(file_path)
248
+ file_ext = os.path.splitext(file_path)[1].lower()
249
+
250
+ if args.upload and not args.recursive and file_ext == '.taf':
209
251
  logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
210
252
  logger.trace("TeddyCloud upload parameters: path=%s, special_folder=%s, ignore_ssl=%s",
211
253
  args.path, args.special_folder, args.ignore_ssl_verify)
212
-
213
- if not args.input_filename:
214
- logger.error("Missing input file for --upload. Provide a file path as SOURCE argument.")
215
- logger.trace("Exiting with code 1 due to missing input file")
254
+ logger.debug("File to upload: %s (size: %d bytes, type: %s)",
255
+ file_path, file_size, file_ext)
256
+ logger.info("Uploading %s to TeddyCloud %s", file_path, teddycloud_url)
257
+ logger.trace("Starting upload process for %s", file_path)
258
+ response = client.upload_file(
259
+ destination_path=args.path,
260
+ file_path=file_path,
261
+ special=args.special_folder,
262
+ )
263
+ logger.trace("Upload response received: %s", response)
264
+ upload_success = response.get('success', False)
265
+ if not upload_success:
266
+ error_msg = response.get('message', 'Unknown error')
267
+ logger.error("Failed to upload %s to TeddyCloud: %s (HTTP Status: %s, Response: %s)",
268
+ file_path, error_msg, response.get('status_code', 'Unknown'), response)
269
+ logger.trace("Exiting with code 1 due to upload failure")
216
270
  sys.exit(1)
217
-
218
- if os.path.exists(args.input_filename) and os.path.isfile(args.input_filename):
219
- file_path = args.input_filename
220
- file_size = os.path.getsize(file_path)
221
- file_ext = os.path.splitext(file_path)[1].lower()
222
-
223
- logger.debug("File to upload: %s (size: %d bytes, type: %s)",
224
- file_path, file_size, file_ext)
225
- logger.info("Uploading %s to TeddyCloud %s", file_path, teddycloud_url)
226
-
227
- logger.trace("Starting upload process for %s", file_path)
228
- response = client.upload_file(
229
- destination_path=args.path,
230
- file_path=file_path,
231
- special=args.special_folder,
232
- )
233
- logger.trace("Upload response received: %s", response)
234
-
235
- upload_success = response.get('success', False)
236
- if not upload_success:
237
- error_msg = response.get('message', 'Unknown error')
238
- logger.error("Failed to upload %s to TeddyCloud: %s", file_path, error_msg)
239
- logger.trace("Exiting with code 1 due to upload failure")
240
- sys.exit(1)
271
+ else:
272
+ logger.info("Successfully uploaded %s to TeddyCloud", file_path)
273
+ logger.debug("Upload response details: %s",
274
+ {k: v for k, v in response.items() if k != 'success'})
275
+ artwork_url = None
276
+ if args.include_artwork and file_path.lower().endswith('.taf'):
277
+ source_dir = os.path.dirname(file_path)
278
+ logger.info("Looking for artwork to upload for %s", file_path)
279
+ logger.debug("Searching for artwork in directory: %s", source_dir)
280
+ logger.trace("Calling upload_artwork function")
281
+ success, artwork_url = upload_artwork(client, file_path, source_dir, [])
282
+ logger.trace("upload_artwork returned: success=%s, artwork_url=%s",
283
+ success, artwork_url)
284
+ if success:
285
+ logger.info("Successfully uploaded artwork for %s", file_path)
286
+ logger.debug("Artwork URL: %s", artwork_url)
241
287
  else:
242
- logger.info("Successfully uploaded %s to TeddyCloud", file_path)
243
- logger.debug("Upload response details: %s",
244
- {k: v for k, v in response.items() if k != 'success'})
245
-
246
- artwork_url = None
247
- if args.include_artwork and file_path.lower().endswith('.taf'):
248
- source_dir = os.path.dirname(file_path)
249
- logger.info("Looking for artwork to upload for %s", file_path)
250
- logger.debug("Searching for artwork in directory: %s", source_dir)
251
-
252
- logger.trace("Calling upload_artwork function")
253
- success, artwork_url = upload_artwork(client, file_path, source_dir, [])
254
- logger.trace("upload_artwork returned: success=%s, artwork_url=%s",
255
- success, artwork_url)
256
-
257
- if success:
258
- logger.info("Successfully uploaded artwork for %s", file_path)
259
- logger.debug("Artwork URL: %s", artwork_url)
260
- else:
261
- logger.warning("Failed to upload artwork for %s", file_path)
262
- logger.debug("No suitable artwork found or upload failed")
263
-
264
- if args.create_custom_json and file_path.lower().endswith('.taf'):
265
- output_dir = './output'
266
- logger.debug("Creating/ensuring output directory for JSON: %s", output_dir)
267
- if not os.path.exists(output_dir):
268
- os.makedirs(output_dir, exist_ok=True)
269
- logger.trace("Created output directory: %s", output_dir)
270
-
271
- logger.debug("Updating tonies.custom.json with: taf=%s, artwork_url=%s",
272
- file_path, artwork_url)
273
- success = fetch_and_update_tonies_json(client, file_path, [], artwork_url, output_dir)
274
- if success:
275
- logger.info("Successfully updated Tonies JSON for %s", file_path)
276
- else:
277
- logger.warning("Failed to update Tonies JSON for %s", file_path)
278
- logger.debug("fetch_and_update_tonies_json returned failure")
279
-
280
- logger.trace("Exiting after direct upload with code 0")
281
- sys.exit(0)
282
- elif not args.recursive:
283
- logger.error("File not found or not a regular file: %s", args.input_filename)
284
- logger.debug("File exists: %s, Is file: %s",
285
- os.path.exists(args.input_filename),
286
- os.path.isfile(args.input_filename) if os.path.exists(args.input_filename) else False)
287
- logger.trace("Exiting with code 1 due to invalid input file")
288
- sys.exit(1)
289
-
290
- if args.recursive and args.upload:
291
- logger.info("Recursive mode with upload enabled: %s -> %s", args.input_filename, teddycloud_url)
292
- logger.debug("Will process all files in directory recursively and upload to TeddyCloud")
288
+ logger.warning("Failed to upload artwork for %s", file_path)
289
+ logger.debug("No suitable artwork found or upload failed")
290
+ if args.create_custom_json and file_path.lower().endswith('.taf'):
291
+ output_dir = './output'
292
+ logger.debug("Creating/ensuring output directory for JSON: %s", output_dir)
293
+ if not os.path.exists(output_dir):
294
+ os.makedirs(output_dir, exist_ok=True)
295
+ logger.trace("Created output directory: %s", output_dir)
296
+ logger.debug("Updating tonies.custom.json with: taf=%s, artwork_url=%s",
297
+ file_path, artwork_url)
298
+ client_param = client
299
+ if args.version_2:
300
+ logger.debug("Using version 2 of the Tonies JSON format")
301
+ success = fetch_and_update_tonies_json_v2(client_param, file_path, [], artwork_url, output_dir)
302
+ else:
303
+ success = fetch_and_update_tonies_json_v1(client_param, file_path, [], artwork_url, output_dir)
304
+ if success:
305
+ logger.info("Successfully updated Tonies JSON for %s", file_path)
306
+ else:
307
+ logger.warning("Failed to update Tonies JSON for %s", file_path)
308
+ logger.debug("fetch_and_update_tonies_json returned failure")
309
+ logger.trace("Exiting after direct upload with code 0")
310
+ sys.exit(0)
293
311
 
294
312
  # ------------- Librarys / Prereqs -------------
295
313
  logger.debug("Checking for external dependencies")
@@ -364,10 +382,16 @@ def main():
364
382
  if not skip_creation:
365
383
  create_tonie_file(task_out_filename, audio_files, args.no_tonie_header, args.user_timestamp,
366
384
  args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
367
- args.auto_download, not args.use_legacy_tags)
385
+ args.auto_download, not args.use_legacy_tags,
386
+ no_mono_conversion=args.no_mono_conversion)
368
387
  logger.info("Successfully created Tonie file: %s", task_out_filename)
369
388
 
370
389
  created_files.append(task_out_filename)
390
+
391
+ # ------------- Initialization -------------------
392
+
393
+ artwork_url = None
394
+
371
395
  # ------------- Recursive File Upload -------------
372
396
  if args.upload:
373
397
  response = client.upload_file(
@@ -391,12 +415,19 @@ def main():
391
415
  logger.warning("Failed to upload artwork for %s", task_out_filename)
392
416
 
393
417
  # tonies.custom.json generation
394
- if args.create_custom_json:
395
- success = fetch_and_update_tonies_json(client, task_out_filename, audio_files, artwork_url, output_dir)
396
- if success:
397
- logger.info("Successfully updated Tonies JSON for %s", task_out_filename)
398
- else:
399
- logger.warning("Failed to update Tonies JSON for %s", task_out_filename)
418
+ if args.create_custom_json:
419
+ base_path = os.path.dirname(args.input_filename)
420
+ json_output_dir = base_path if args.output_to_source else output_dir
421
+ client_param = client if 'client' in locals() else None
422
+ if args.version_2:
423
+ logger.debug("Using version 2 of the Tonies JSON format")
424
+ success = fetch_and_update_tonies_json_v2(client_param, task_out_filename, audio_files, artwork_url, json_output_dir)
425
+ else:
426
+ success = fetch_and_update_tonies_json_v1(client_param, task_out_filename, audio_files, artwork_url, json_output_dir)
427
+ if success:
428
+ logger.info("Successfully updated Tonies JSON for %s", task_out_filename)
429
+ else:
430
+ logger.warning("Failed to update Tonies JSON for %s", task_out_filename)
400
431
 
401
432
  logger.info("Recursive processing completed. Created %d Tonie files.", len(process_tasks))
402
433
  sys.exit(0)
@@ -415,7 +446,6 @@ def main():
415
446
  split_to_opus_files(args.input_filename, args.output_filename)
416
447
  sys.exit(0)
417
448
  elif args.compare:
418
- from .tonie_analysis import compare_taf_files
419
449
  logger.info("Comparing Tonie files: %s and %s", args.input_filename, args.compare)
420
450
  result = compare_taf_files(args.input_filename, args.compare, args.detailed_compare)
421
451
  sys.exit(0 if result else 1)
@@ -426,26 +456,12 @@ def main():
426
456
  if len(files) == 0:
427
457
  logger.error("No files found for pattern %s", args.input_filename)
428
458
  sys.exit(1)
429
- if args.show_tags:
430
- from .media_tags import get_file_tags
431
- logger.info("Showing media tags for input files:")
432
-
433
- for file_index, file_path in enumerate(files):
434
- tags = get_file_tags(file_path)
435
- if tags:
436
- print(f"\nFile {file_index + 1}: {os.path.basename(file_path)}")
437
- print("-" * 40)
438
- for tag_name, tag_value in sorted(tags.items()):
439
- print(f"{tag_name}: {tag_value}")
440
- else:
441
- print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
442
- sys.exit(0)
459
+
443
460
  guessed_name = None
444
461
  if args.use_media_tags:
445
462
  if len(files) > 1 and os.path.dirname(files[0]) == os.path.dirname(files[-1]):
446
463
  folder_path = os.path.dirname(files[0])
447
- logger.debug("Extracting album info from folder: %s", folder_path)
448
-
464
+ logger.debug("Extracting album info from folder: %s", folder_path)
449
465
  album_info = extract_album_info(folder_path)
450
466
  if album_info:
451
467
  template = args.name_template or "{album} - {artist}"
@@ -457,7 +473,7 @@ def main():
457
473
  else:
458
474
  logger.debug("Could not format filename from album metadata")
459
475
  elif len(files) == 1:
460
- from .media_tags import get_file_tags, format_metadata_filename
476
+
461
477
 
462
478
  tags = get_file_tags(files[0])
463
479
  if tags:
@@ -471,9 +487,7 @@ def main():
471
487
  logger.debug("Could not format filename from file metadata")
472
488
 
473
489
  # For multiple files from different folders, try to use common tags if they exist
474
- elif len(files) > 1:
475
- from .media_tags import get_file_tags, format_metadata_filename
476
-
490
+ elif len(files) > 1:
477
491
  # Try to find common tags among files
478
492
  common_tags = {}
479
493
  for file_path in files:
@@ -547,7 +561,8 @@ def main():
547
561
  logger.info("Creating Tonie file: %s with %d input file(s)", out_filename, len(files))
548
562
  create_tonie_file(out_filename, files, args.no_tonie_header, args.user_timestamp,
549
563
  args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
550
- args.auto_download, not args.use_legacy_tags)
564
+ args.auto_download, not args.use_legacy_tags,
565
+ no_mono_conversion=args.no_mono_conversion)
551
566
  logger.info("Successfully created Tonie file: %s", out_filename)
552
567
 
553
568
  # ------------- Single File Upload -------------
@@ -571,13 +586,19 @@ def main():
571
586
  logger.info("Successfully uploaded artwork for %s", out_filename)
572
587
  else:
573
588
  logger.warning("Failed to upload artwork for %s", out_filename)
574
- # tonies.custom.json generation
575
- if args.create_custom_json:
576
- success = fetch_and_update_tonies_json(client, out_filename, files, artwork_url)
577
- if success:
578
- logger.info("Successfully updated Tonies JSON for %s", out_filename)
579
- else:
580
- logger.warning("Failed to update Tonies JSON for %s", out_filename)
589
+
590
+ if args.create_custom_json:
591
+ json_output_dir = source_dir if args.output_to_source else './output'
592
+ client_param = client if 'client' in locals() else None
593
+ if args.version_2:
594
+ logger.debug("Using version 2 of the Tonies JSON format")
595
+ success = fetch_and_update_tonies_json_v2(client_param, out_filename, files, artwork_url, json_output_dir)
596
+ else:
597
+ success = fetch_and_update_tonies_json_v1(client_param, out_filename, files, artwork_url, json_output_dir)
598
+ if success:
599
+ logger.info("Successfully updated Tonies JSON for %s", out_filename)
600
+ else:
601
+ logger.warning("Failed to update Tonies JSON for %s", out_filename)
581
602
 
582
603
  if __name__ == "__main__":
583
604
  main()
TonieToolbox/artwork.py CHANGED
@@ -13,17 +13,22 @@ from .teddycloud import TeddyCloudClient
13
13
  from .media_tags import extract_artwork, find_cover_image
14
14
 
15
15
 
16
- def upload_artwork(client: TeddyCloudClient, taf_filename, source_path, audio_files) -> Tuple[bool, Optional[str]]:
16
+ def upload_artwork(
17
+ client: TeddyCloudClient,
18
+ taf_filename: str,
19
+ source_path: str,
20
+ audio_files: list[str],
21
+ ) -> tuple[bool, Optional[str]]:
17
22
  """
18
23
  Find and upload artwork for a Tonie file.
19
-
24
+
20
25
  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
26
+ client (TeddyCloudClient): TeddyCloudClient instance to use for API communication
27
+ taf_filename (str): The filename of the Tonie file (.taf)
28
+ source_path (str): Source directory to look for artwork
29
+ audio_files (list[str]): List of audio files to extract artwork from if needed
25
30
  Returns:
26
- tuple: (success, artwork_url) where success is a boolean and artwork_url is the URL of the uploaded artwork
31
+ tuple[bool, Optional[str]]: (success, artwork_url) where success is a boolean and artwork_url is the URL of the uploaded artwork
27
32
  """
28
33
  logger = get_logger('artwork')
29
34
  logger.info("Looking for artwork for Tonie file: %s", taf_filename)