TonieToolbox 0.5.0a1__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.0a1'
6
+ __version__ = '0.6.0'
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
14
- from .dependency_manager import get_ffmpeg_binary, get_opus_binary
15
- from .logger import setup_logging, get_logger
16
- from .filename_generator import guess_output_filename
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, ensure_dependency
15
+ from .logger import TRACE, setup_logging, get_logger
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
- 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, handle_config
25
25
 
26
26
  def main():
27
27
  """Entry point for the TonieToolbox application."""
@@ -41,7 +41,7 @@ def main():
41
41
  teddycloud_group.add_argument('--special-folder', action='store', metavar='FOLDER',
42
42
  help='Special folder to upload to (currently only "library" is supported)', default='library')
43
43
  teddycloud_group.add_argument('--path', action='store', metavar='PATH',
44
- 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}")')
45
45
  teddycloud_group.add_argument('--connection-timeout', type=int, metavar='SECONDS', default=10,
46
46
  help='Connection timeout in seconds (default: 10)')
47
47
  teddycloud_group.add_argument('--read-timeout', type=int, metavar='SECONDS', default=300,
@@ -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,13 +99,22 @@ 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('--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')
99
110
  # ------------- Parser - Media Tag Options -------------
100
111
  media_tag_group = parser.add_argument_group('Media Tag Options')
101
112
  media_tag_group.add_argument('-m', '--use-media-tags', action='store_true',
102
113
  help='Use media tags from audio files for naming')
103
114
  media_tag_group.add_argument('--name-template', metavar='TEMPLATE', action='store',
104
- 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}"')
105
118
  media_tag_group.add_argument('--show-tags', action='store_true',
106
119
  help='Show available media tags from input files')
107
120
  # ------------- Parser - Version handling -------------
@@ -124,12 +137,11 @@ def main():
124
137
  args = parser.parse_args()
125
138
 
126
139
  # ------------- Parser - Source Input -------------
127
- 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):
128
141
  parser.error("the following arguments are required: SOURCE")
129
142
 
130
143
  # ------------- Logging -------------
131
144
  if args.trace:
132
- from .logger import TRACE
133
145
  log_level = TRACE
134
146
  elif args.debug:
135
147
  log_level = logging.DEBUG
@@ -140,7 +152,7 @@ def main():
140
152
  else:
141
153
  log_level = logging.INFO
142
154
  setup_logging(log_level, log_to_file=args.log_file)
143
- logger = get_logger('main')
155
+ logger = get_logger(__name__)
144
156
  logger.debug("Starting TonieToolbox v%s with log level: %s", __version__, logging.getLevelName(log_level))
145
157
  logger.debug("Command-line arguments: %s", vars(args))
146
158
 
@@ -165,6 +177,40 @@ def main():
165
177
  if not is_latest and not update_confirmed and not (args.silent or args.quiet):
166
178
  logger.info("Update available but user chose to continue without updating.")
167
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)
168
214
  # ------------- Normalize Path Input -------------
169
215
  if args.input_filename:
170
216
  logger.debug("Original input path: %s", args.input_filename)
@@ -203,93 +249,116 @@ def main():
203
249
  success = get_tags(client)
204
250
  logger.debug( "Exiting with code %d", 0 if success else 1)
205
251
  sys.exit(0 if success else 1)
206
-
207
- # ------------- Direct Upload -------------
208
- if args.upload and not args.recursive:
252
+
253
+ # ------------- Show Media Tags -------------
254
+ if args.show_tags:
255
+ files = get_input_files(args.input_filename)
256
+ logger.debug("Found %d files to process", len(files))
257
+ if len(files) == 0:
258
+ logger.error("No files found for pattern %s", args.input_filename)
259
+ sys.exit(1)
260
+ for file_index, file_path in enumerate(files):
261
+ tags = get_file_tags(file_path)
262
+ if tags:
263
+ print(f"\nFile {file_index + 1}: {os.path.basename(file_path)}")
264
+ print("-" * 40)
265
+ for tag_name, tag_value in sorted(tags.items()):
266
+ print(f"{tag_name}: {tag_value}")
267
+ else:
268
+ print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
269
+ sys.exit(0)
270
+ # ------------- Direct Upload -------------
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()
275
+
276
+ if args.upload and not args.recursive and file_ext == '.taf':
209
277
  logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
210
278
  logger.trace("TeddyCloud upload parameters: path=%s, special_folder=%s, ignore_ssl=%s",
211
279
  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")
216
- 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)
241
- 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)
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
276
293
  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")
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")
288
321
  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")
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)
293
362
 
294
363
  # ------------- Librarys / Prereqs -------------
295
364
  logger.debug("Checking for external dependencies")
@@ -364,10 +433,16 @@ def main():
364
433
  if not skip_creation:
365
434
  create_tonie_file(task_out_filename, audio_files, args.no_tonie_header, args.user_timestamp,
366
435
  args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
367
- args.auto_download, not args.use_legacy_tags)
436
+ args.auto_download, not args.use_legacy_tags,
437
+ no_mono_conversion=args.no_mono_conversion)
368
438
  logger.info("Successfully created Tonie file: %s", task_out_filename)
369
439
 
370
440
  created_files.append(task_out_filename)
441
+
442
+ # ------------- Initialization -------------------
443
+
444
+ artwork_url = None
445
+
371
446
  # ------------- Recursive File Upload -------------
372
447
  if args.upload:
373
448
  response = client.upload_file(
@@ -391,12 +466,19 @@ def main():
391
466
  logger.warning("Failed to upload artwork for %s", task_out_filename)
392
467
 
393
468
  # 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)
469
+ if args.create_custom_json:
470
+ base_path = os.path.dirname(args.input_filename)
471
+ json_output_dir = base_path if args.output_to_source else output_dir
472
+ client_param = client if 'client' in locals() else None
473
+ if args.version_2:
474
+ logger.debug("Using version 2 of the Tonies JSON format")
475
+ success = fetch_and_update_tonies_json_v2(client_param, task_out_filename, audio_files, artwork_url, json_output_dir)
476
+ else:
477
+ success = fetch_and_update_tonies_json_v1(client_param, task_out_filename, audio_files, artwork_url, json_output_dir)
478
+ if success:
479
+ logger.info("Successfully updated Tonies JSON for %s", task_out_filename)
480
+ else:
481
+ logger.warning("Failed to update Tonies JSON for %s", task_out_filename)
400
482
 
401
483
  logger.info("Recursive processing completed. Created %d Tonie files.", len(process_tasks))
402
484
  sys.exit(0)
@@ -415,7 +497,6 @@ def main():
415
497
  split_to_opus_files(args.input_filename, args.output_filename)
416
498
  sys.exit(0)
417
499
  elif args.compare:
418
- from .tonie_analysis import compare_taf_files
419
500
  logger.info("Comparing Tonie files: %s and %s", args.input_filename, args.compare)
420
501
  result = compare_taf_files(args.input_filename, args.compare, args.detailed_compare)
421
502
  sys.exit(0 if result else 1)
@@ -426,42 +507,28 @@ def main():
426
507
  if len(files) == 0:
427
508
  logger.error("No files found for pattern %s", args.input_filename)
428
509
  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)
510
+
443
511
  guessed_name = None
444
512
  if args.use_media_tags:
513
+ logger.debug("Using media tags for naming")
445
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")
446
516
  folder_path = os.path.dirname(files[0])
447
- logger.debug("Extracting album info from folder: %s", folder_path)
448
-
517
+ logger.debug("Extracting album info from folder: %s", folder_path)
449
518
  album_info = extract_album_info(folder_path)
450
519
  if album_info:
451
- template = args.name_template or "{album} - {artist}"
452
- new_name = format_metadata_filename(album_info, template)
453
-
520
+ template = args.name_template or "{artist} - {album}"
521
+ new_name = format_metadata_filename(album_info, template)
454
522
  if new_name:
455
523
  logger.info("Using album metadata for output filename: %s", new_name)
456
524
  guessed_name = new_name
457
525
  else:
458
526
  logger.debug("Could not format filename from album metadata")
459
527
  elif len(files) == 1:
460
- from .media_tags import get_file_tags, format_metadata_filename
461
-
462
528
  tags = get_file_tags(files[0])
463
529
  if tags:
464
- template = args.name_template or "{title} - {artist}"
530
+ logger.debug("")
531
+ template = args.name_template or "{artist} - {title}"
465
532
  new_name = format_metadata_filename(tags, template)
466
533
 
467
534
  if new_name:
@@ -471,9 +538,7 @@ def main():
471
538
  logger.debug("Could not format filename from file metadata")
472
539
 
473
540
  # 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
-
541
+ elif len(files) > 1:
477
542
  # Try to find common tags among files
478
543
  common_tags = {}
479
544
  for file_path in files:
@@ -500,20 +565,71 @@ def main():
500
565
  else:
501
566
  logger.debug("Could not format filename from common metadata")
502
567
 
503
- if args.output_filename:
568
+ if args.output_filename:
504
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
505
620
  elif guessed_name:
621
+ logger.debug("Using guessed name for output: %s", guessed_name)
506
622
  if args.output_to_source:
507
623
  source_dir = os.path.dirname(files[0]) if files else '.'
508
624
  out_filename = os.path.join(source_dir, guessed_name)
509
- logger.debug("Using source location for output with media tags: %s", out_filename)
625
+ logger.debug("Using source location for output: %s", out_filename)
510
626
  else:
511
627
  output_dir = './output'
512
628
  if not os.path.exists(output_dir):
513
629
  logger.debug("Creating default output directory: %s", output_dir)
514
630
  os.makedirs(output_dir, exist_ok=True)
515
631
  out_filename = os.path.join(output_dir, guessed_name)
516
- logger.debug("Using default output location with media tags: %s", out_filename)
632
+ logger.debug("Using default output location: %s", out_filename)
517
633
  else:
518
634
  guessed_name = guess_output_filename(args.input_filename, files)
519
635
  if args.output_to_source:
@@ -543,19 +659,59 @@ def main():
543
659
 
544
660
  if not out_filename.lower().endswith('.taf'):
545
661
  out_filename += '.taf'
662
+ ensure_directory_exists(out_filename)
546
663
 
547
664
  logger.info("Creating Tonie file: %s with %d input file(s)", out_filename, len(files))
548
665
  create_tonie_file(out_filename, files, args.no_tonie_header, args.user_timestamp,
549
666
  args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
550
- args.auto_download, not args.use_legacy_tags)
667
+ args.auto_download, not args.use_legacy_tags,
668
+ no_mono_conversion=args.no_mono_conversion)
551
669
  logger.info("Successfully created Tonie file: %s", out_filename)
552
670
 
553
671
  # ------------- Single File Upload -------------
554
672
  artwork_url = None
555
- 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
+
556
712
  response = client.upload_file(
557
713
  file_path=out_filename,
558
- destination_path=args.path,
714
+ destination_path=upload_path,
559
715
  special=args.special_folder,
560
716
  )
561
717
  upload_success = response.get('success', False)
@@ -571,13 +727,19 @@ def main():
571
727
  logger.info("Successfully uploaded artwork for %s", out_filename)
572
728
  else:
573
729
  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)
730
+
731
+ if args.create_custom_json:
732
+ json_output_dir = source_dir if args.output_to_source else './output'
733
+ client_param = client if 'client' in locals() else None
734
+ if args.version_2:
735
+ logger.debug("Using version 2 of the Tonies JSON format")
736
+ success = fetch_and_update_tonies_json_v2(client_param, out_filename, files, artwork_url, json_output_dir)
737
+ else:
738
+ success = fetch_and_update_tonies_json_v1(client_param, out_filename, files, artwork_url, json_output_dir)
739
+ if success:
740
+ logger.info("Successfully updated Tonies JSON for %s", out_filename)
741
+ else:
742
+ logger.warning("Failed to update Tonies JSON for %s", out_filename)
581
743
 
582
744
  if __name__ == "__main__":
583
745
  main()