TonieToolbox 0.4.1__py3-none-any.whl → 0.5.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/__main__.py CHANGED
@@ -7,27 +7,27 @@ import argparse
7
7
  import os
8
8
  import sys
9
9
  import logging
10
-
11
10
  from . import __version__
12
11
  from .audio_conversion import get_input_files, append_to_filename
13
12
  from .tonie_file import create_tonie_file
14
- from .tonie_analysis import check_tonie_file, split_to_opus_files
13
+ from .tonie_analysis import check_tonie_file, check_tonie_file_cli, split_to_opus_files, compare_taf_files
15
14
  from .dependency_manager import get_ffmpeg_binary, get_opus_binary
16
- from .logger import setup_logging, get_logger
15
+ from .logger import TRACE, setup_logging, get_logger
17
16
  from .filename_generator import guess_output_filename
18
17
  from .version_handler import check_for_updates, clear_version_cache
19
18
  from .recursive_processor import process_recursive_folders
20
- from .media_tags import is_available as is_media_tags_available, ensure_mutagen
21
- from .teddycloud import upload_to_teddycloud, get_tags_from_teddycloud, get_file_paths
22
- from .tonies_json import fetch_and_update_tonies_json
19
+ from .media_tags import is_available as is_media_tags_available, ensure_mutagen, extract_album_info, format_metadata_filename, get_file_tags
20
+ from .teddycloud import TeddyCloudClient
21
+ from .tags import get_tags
22
+ from .tonies_json import fetch_and_update_tonies_json_v1, fetch_and_update_tonies_json_v2
23
+ from .artwork import upload_artwork
23
24
 
24
25
  def main():
25
26
  """Entry point for the TonieToolbox application."""
26
27
  parser = argparse.ArgumentParser(description='Create Tonie compatible file from Ogg opus file(s).')
27
28
  parser.add_argument('-v', '--version', action='version', version=f'TonieToolbox {__version__}',
28
- help='show program version and exit')
29
-
30
- # TeddyCloud options first to check for existence before requiring SOURCE
29
+ help='show program version and exit')
30
+ # ------------- Parser - Teddycloud -------------
31
31
  teddycloud_group = parser.add_argument_group('TeddyCloud Options')
32
32
  teddycloud_group.add_argument('--upload', metavar='URL', action='store',
33
33
  help='Upload to TeddyCloud instance (e.g., https://teddycloud.example.com). Supports .taf, .jpg, .jpeg, .png files.')
@@ -41,8 +41,6 @@ def main():
41
41
  help='Special folder to upload to (currently only "library" is supported)', default='library')
42
42
  teddycloud_group.add_argument('--path', action='store', metavar='PATH',
43
43
  help='Path where to write the file on TeddyCloud server')
44
- teddycloud_group.add_argument('--show-progress', action='store_true', default=True,
45
- help='Show progress bar during file upload (default: enabled)')
46
44
  teddycloud_group.add_argument('--connection-timeout', type=int, metavar='SECONDS', default=10,
47
45
  help='Connection timeout in seconds (default: 10)')
48
46
  teddycloud_group.add_argument('--read-timeout', type=int, metavar='SECONDS', default=300,
@@ -53,18 +51,33 @@ def main():
53
51
  help='Delay between retry attempts in seconds (default: 5)')
54
52
  teddycloud_group.add_argument('--create-custom-json', action='store_true',
55
53
  help='Fetch and update custom Tonies JSON data')
54
+ teddycloud_group.add_argument('--version-2', action='store_true',
55
+ help='Use version 2 of the Tonies JSON format (default: version 1)')
56
+ # ------------- Parser - Authentication options for TeddyCloud -------------
57
+ teddycloud_group.add_argument('--username', action='store', metavar='USERNAME',
58
+ help='Username for basic authentication')
59
+ teddycloud_group.add_argument('--password', action='store', metavar='PASSWORD',
60
+ help='Password for basic authentication')
61
+ teddycloud_group.add_argument('--client-cert', action='store', metavar='CERT_FILE',
62
+ help='Path to client certificate file for certificate-based authentication')
63
+ teddycloud_group.add_argument('--client-key', action='store', metavar='KEY_FILE',
64
+ help='Path to client private key file for certificate-based authentication')
56
65
 
66
+ # ------------- Parser - Source Input -------------
57
67
  parser.add_argument('input_filename', metavar='SOURCE', type=str, nargs='?',
58
68
  help='input file or directory or a file list (.lst)')
59
69
  parser.add_argument('output_filename', metavar='TARGET', nargs='?', type=str,
60
70
  help='the output file name (default: ---ID---)')
61
71
  parser.add_argument('-t', '--timestamp', dest='user_timestamp', metavar='TIMESTAMP', action='store',
62
72
  help='set custom timestamp / bitstream serial')
63
-
73
+ # ------------- Parser - Librarys -------------
64
74
  parser.add_argument('-f', '--ffmpeg', help='specify location of ffmpeg', default=None)
65
75
  parser.add_argument('-o', '--opusenc', help='specify location of opusenc', default=None)
66
76
  parser.add_argument('-b', '--bitrate', type=int, help='set encoding bitrate in kbps (default: 96)', default=96)
67
77
  parser.add_argument('-c', '--cbr', action='store_true', help='encode in cbr mode')
78
+ parser.add_argument('--auto-download', action='store_true',
79
+ help='automatically download ffmpeg and opusenc if not found')
80
+ # ------------- Parser - TAF -------------
68
81
  parser.add_argument('-a', '--append-tonie-tag', metavar='TAG', action='store',
69
82
  help='append [TAG] to filename (must be an 8-character hex value)')
70
83
  parser.add_argument('-n', '--no-tonie-header', action='store_true', help='do not write Tonie header')
@@ -73,7 +86,11 @@ def main():
73
86
  parser.add_argument('-r', '--recursive', action='store_true', help='Process folders recursively')
74
87
  parser.add_argument('-O', '--output-to-source', action='store_true',
75
88
  help='Save output files in the source directory instead of output directory')
76
- parser.add_argument('-A', '--auto-download', action='store_true', help='Automatically download FFmpeg and opusenc if needed')
89
+ parser.add_argument('-fc', '--force-creation', action='store_true', default=False,
90
+ help='Force creation of Tonie file even if it already exists')
91
+ parser.add_argument('--no-mono-conversion', action='store_true',
92
+ help='Do not convert mono audio to stereo (default: convert mono to stereo)')
93
+ # ------------- Parser - Debug TAFs -------------
77
94
  parser.add_argument('-k', '--keep-temp', action='store_true',
78
95
  help='Keep temporary opus files in a temp folder for testing')
79
96
  parser.add_argument('-u', '--use-legacy-tags', action='store_true',
@@ -81,9 +98,8 @@ def main():
81
98
  parser.add_argument('-C', '--compare', action='store', metavar='FILE2',
82
99
  help='Compare input file with another .taf file for debugging')
83
100
  parser.add_argument('-D', '--detailed-compare', action='store_true',
84
- help='Show detailed OGG page differences when comparing files')
85
-
86
- # Media tag options
101
+ help='Show detailed OGG page differences when comparing files')
102
+ # ------------- Parser - Media Tag Options -------------
87
103
  media_tag_group = parser.add_argument_group('Media Tag Options')
88
104
  media_tag_group.add_argument('-m', '--use-media-tags', action='store_true',
89
105
  help='Use media tags from audio files for naming')
@@ -91,8 +107,7 @@ def main():
91
107
  help='Template for naming files using media tags. Example: "{album} - {artist}"')
92
108
  media_tag_group.add_argument('--show-tags', action='store_true',
93
109
  help='Show available media tags from input files')
94
-
95
- # Version check options
110
+ # ------------- Parser - Version handling -------------
96
111
  version_group = parser.add_argument_group('Version Check Options')
97
112
  version_group.add_argument('-S', '--skip-update-check', action='store_true',
98
113
  help='Skip checking for updates')
@@ -100,7 +115,7 @@ def main():
100
115
  help='Force refresh of update information from PyPI')
101
116
  version_group.add_argument('-X', '--clear-version-cache', action='store_true',
102
117
  help='Clear cached version information')
103
-
118
+ # ------------- Parser - Logging -------------
104
119
  log_group = parser.add_argument_group('Logging Options')
105
120
  log_level_group = log_group.add_mutually_exclusive_group()
106
121
  log_level_group.add_argument('-d', '--debug', action='store_true', help='Enable debug logging')
@@ -109,16 +124,14 @@ def main():
109
124
  log_level_group.add_argument('-Q', '--silent', action='store_true', help='Show only errors')
110
125
  log_group.add_argument('--log-file', action='store_true', default=False,
111
126
  help='Save logs to a timestamped file in .tonietoolbox folder')
112
-
113
127
  args = parser.parse_args()
114
128
 
115
- # Validate that input_filename is provided if not using --get-tags or --upload-existing
129
+ # ------------- Parser - Source Input -------------
116
130
  if args.input_filename is None and not (args.get_tags or args.upload):
117
131
  parser.error("the following arguments are required: SOURCE")
118
-
119
- # Set up the logging level
132
+
133
+ # ------------- Logging -------------
120
134
  if args.trace:
121
- from .logger import TRACE
122
135
  log_level = TRACE
123
136
  elif args.debug:
124
137
  log_level = logging.DEBUG
@@ -127,17 +140,15 @@ def main():
127
140
  elif args.silent:
128
141
  log_level = logging.ERROR
129
142
  else:
130
- log_level = logging.INFO
131
-
143
+ log_level = logging.INFO
132
144
  setup_logging(log_level, log_to_file=args.log_file)
133
145
  logger = get_logger('main')
134
146
  logger.debug("Starting TonieToolbox v%s with log level: %s", __version__, logging.getLevelName(log_level))
135
-
136
- # Log the command-line arguments at trace level for debugging purposes
137
- logger.log(logging.DEBUG - 1, "Command-line arguments: %s", vars(args))
147
+ logger.debug("Command-line arguments: %s", vars(args))
138
148
 
149
+ # ------------- Version handling -------------
139
150
  if args.clear_version_cache:
140
- logger.log(logging.DEBUG - 1, "Clearing version cache")
151
+ logger.debug("Clearing version cache")
141
152
  if clear_version_cache():
142
153
  logger.info("Version cache cleared successfully")
143
154
  else:
@@ -150,73 +161,155 @@ def main():
150
161
  force_refresh=args.force_refresh_cache
151
162
  )
152
163
 
153
- logger.log(logging.DEBUG - 1, "Update check results: is_latest=%s, latest_version=%s, update_confirmed=%s",
164
+ logger.debug( "Update check results: is_latest=%s, latest_version=%s, update_confirmed=%s",
154
165
  is_latest, latest_version, update_confirmed)
155
166
 
156
167
  if not is_latest and not update_confirmed and not (args.silent or args.quiet):
157
168
  logger.info("Update available but user chose to continue without updating.")
158
169
 
159
- # Handle get-tags from TeddyCloud if requested
160
- if args.get_tags:
161
- logger.debug("Getting tags from TeddyCloud: %s", args.get_tags)
162
- teddycloud_url = args.get_tags
163
- success = get_tags_from_teddycloud(teddycloud_url, args.ignore_ssl_verify)
164
- logger.log(logging.DEBUG - 1, "Exiting with code %d", 0 if success else 1)
165
- sys.exit(0 if success else 1)
170
+ # ------------- Normalize Path Input -------------
171
+ if args.input_filename:
172
+ logger.debug("Original input path: %s", args.input_filename)
173
+ # Strip quotes from the beginning and end
174
+ args.input_filename = args.input_filename.strip('"\'')
175
+ # Handle paths that end with a backslash
176
+ if args.input_filename.endswith('\\'):
177
+ args.input_filename = args.input_filename.rstrip('\\')
178
+ logger.debug("Normalized input path: %s", args.input_filename)
179
+
180
+ # ------------- Setup TeddyCloudClient-------------
181
+ if args.upload or args.get_tags:
182
+ if args.upload:
183
+ teddycloud_url = args.upload
184
+ elif args.get_tags:
185
+ teddycloud_url = args.get_tags
186
+ if not teddycloud_url:
187
+ logger.error("TeddyCloud URL is required for --upload or --get-tags")
188
+ sys.exit(1)
189
+ try:
190
+ client = TeddyCloudClient(
191
+ base_url=teddycloud_url,
192
+ ignore_ssl_verify=args.ignore_ssl_verify,
193
+ username=args.username,
194
+ password=args.password,
195
+ cert_file=args.client_cert,
196
+ key_file=args.client_key
197
+ )
198
+ logger.debug("TeddyCloud client initialized successfully")
199
+ except Exception as e:
200
+ logger.error("Failed to initialize TeddyCloud client: %s", str(e))
201
+ sys.exit(1)
202
+
203
+ if args.get_tags:
204
+ logger.debug("Getting tags from TeddyCloud: %s", teddycloud_url)
205
+ success = get_tags(client)
206
+ logger.debug( "Exiting with code %d", 0 if success else 1)
207
+ sys.exit(0 if success else 1)
166
208
 
167
- # Handle upload to TeddyCloud if requested
168
- if args.upload:
169
- teddycloud_url = args.upload
170
- logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
209
+ # ------------- Show Media Tags -------------
210
+ if args.show_tags:
211
+ logger.info("Showing media tags for input files:")
171
212
 
172
- if not args.input_filename:
173
- logger.error("Missing input file for --upload. Provide a file path as SOURCE argument.")
174
- sys.exit(1)
175
-
176
- # Check if the input file is already a .taf file or an image file
177
- if os.path.exists(args.input_filename) and (args.input_filename.lower().endswith('.taf') or
178
- args.input_filename.lower().endswith(('.jpg', '.jpeg', '.png'))):
179
- # Direct upload of existing TAF or image file
180
- logger.debug("Direct upload of existing TAF or image file detected")
181
- # Use get_file_paths to handle Windows backslashes and resolve the paths correctly
182
- file_paths = get_file_paths(args.input_filename)
183
-
184
- if not file_paths:
185
- logger.error("No files found for pattern %s", args.input_filename)
186
- sys.exit(1)
187
-
188
- logger.info("Found %d file(s) to upload to TeddyCloud %s", len(file_paths), teddycloud_url)
189
-
190
- for file_path in file_paths:
191
- # Only upload supported file types
192
- if not file_path.lower().endswith(('.taf', '.jpg', '.jpeg', '.png')):
193
- logger.warning("Skipping unsupported file type: %s", file_path)
194
- continue
195
-
213
+ for file_index, file_path in enumerate(files):
214
+ tags = get_file_tags(file_path)
215
+ if tags:
216
+ print(f"\nFile {file_index + 1}: {os.path.basename(file_path)}")
217
+ print("-" * 40)
218
+ for tag_name, tag_value in sorted(tags.items()):
219
+ print(f"{tag_name}: {tag_value}")
220
+ else:
221
+ print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
222
+ sys.exit(0)
223
+ # ------------- Direct Upload -------------
224
+ if os.path.exists(args.input_filename) and os.path.isfile(args.input_filename):
225
+ file_path = args.input_filename
226
+ file_size = os.path.getsize(file_path)
227
+ file_ext = os.path.splitext(file_path)[1].lower()
228
+
229
+ if args.upload and not args.recursive and file_ext == '.taf':
230
+ logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
231
+ logger.trace("TeddyCloud upload parameters: path=%s, special_folder=%s, ignore_ssl=%s",
232
+ args.path, args.special_folder, args.ignore_ssl_verify)
233
+
234
+
235
+ logger.debug("File to upload: %s (size: %d bytes, type: %s)",
236
+ file_path, file_size, file_ext)
196
237
  logger.info("Uploading %s to TeddyCloud %s", file_path, teddycloud_url)
197
- upload_success = upload_to_teddycloud(
198
- file_path, teddycloud_url, args.ignore_ssl_verify,
199
- args.special_folder, args.path, args.show_progress,
200
- args.connection_timeout, args.read_timeout,
201
- args.max_retries, args.retry_delay
238
+ logger.trace("Starting upload process for %s", file_path)
239
+ response = client.upload_file(
240
+ destination_path=args.path,
241
+ file_path=file_path,
242
+ special=args.special_folder,
202
243
  )
203
-
244
+ logger.trace("Upload response received: %s", response)
245
+ upload_success = response.get('success', False)
204
246
  if not upload_success:
205
- logger.error("Failed to upload %s to TeddyCloud", file_path)
247
+ error_msg = response.get('message', 'Unknown error')
248
+ logger.error("Failed to upload %s to TeddyCloud: %s (HTTP Status: %s, Response: %s)",
249
+ file_path, error_msg, response.get('status_code', 'Unknown'), response)
250
+ logger.trace("Exiting with code 1 due to upload failure")
206
251
  sys.exit(1)
207
252
  else:
208
253
  logger.info("Successfully uploaded %s to TeddyCloud", file_path)
209
-
210
- logger.log(logging.DEBUG - 1, "Exiting after direct upload with code 0")
211
- sys.exit(0)
212
-
213
- # If we get here, it's not a TAF or image file, so continue with normal processing
214
- # which will convert the input files and upload the result later
215
- logger.debug("Input is not a direct upload file, continuing with conversion workflow")
216
- pass
254
+ logger.debug("Upload response details: %s",
255
+ {k: v for k, v in response.items() if k != 'success'})
256
+ artwork_url = None
257
+ if args.include_artwork and file_path.lower().endswith('.taf'):
258
+ source_dir = os.path.dirname(file_path)
259
+ logger.info("Looking for artwork to upload for %s", file_path)
260
+ logger.debug("Searching for artwork in directory: %s", source_dir)
261
+ logger.trace("Calling upload_artwork function")
262
+ success, artwork_url = upload_artwork(client, file_path, source_dir, [])
263
+ logger.trace("upload_artwork returned: success=%s, artwork_url=%s",
264
+ success, artwork_url)
265
+ if success:
266
+ logger.info("Successfully uploaded artwork for %s", file_path)
267
+ logger.debug("Artwork URL: %s", artwork_url)
268
+ else:
269
+ logger.warning("Failed to upload artwork for %s", file_path)
270
+ logger.debug("No suitable artwork found or upload failed")
271
+ if args.create_custom_json and file_path.lower().endswith('.taf'):
272
+ output_dir = './output'
273
+ logger.debug("Creating/ensuring output directory for JSON: %s", output_dir)
274
+ if not os.path.exists(output_dir):
275
+ os.makedirs(output_dir, exist_ok=True)
276
+ logger.trace("Created output directory: %s", output_dir)
277
+ logger.debug("Updating tonies.custom.json with: taf=%s, artwork_url=%s",
278
+ file_path, artwork_url)
279
+ client_param = client
217
280
 
281
+ if args.version_2:
282
+ logger.debug("Using version 2 of the Tonies JSON format")
283
+ success = fetch_and_update_tonies_json_v2(client_param, file_path, [], artwork_url, output_dir)
284
+ else:
285
+ success = fetch_and_update_tonies_json_v1(client_param, file_path, [], artwork_url, output_dir)
286
+ if success:
287
+ logger.info("Successfully updated Tonies JSON for %s", file_path)
288
+ else:
289
+ logger.warning("Failed to update Tonies JSON for %s", file_path)
290
+ logger.debug("fetch_and_update_tonies_json returned failure")
291
+ logger.trace("Exiting after direct upload with code 0")
292
+ sys.exit(0)
293
+ elif not args.recursive:
294
+ if not os.path.exists(args.input_filename):
295
+ logger.error("File not found: %s", args.input_filename)
296
+ elif not os.path.isfile(args.input_filename):
297
+ logger.error("Not a regular file: %s", args.input_filename)
298
+ logger.debug("File exists: %s, Is file: %s",
299
+ os.path.exists(args.input_filename),
300
+ os.path.isfile(args.input_filename) if os.path.exists(args.input_filename) else False)
301
+ logger.trace("Exiting with code 1 due to invalid input file")
302
+ sys.exit(1)
303
+
304
+ if args.recursive and args.upload:
305
+ logger.info("Recursive mode with upload enabled: %s -> %s", args.input_filename, teddycloud_url)
306
+ logger.debug("Will process all files in directory recursively and upload to TeddyCloud")
307
+
308
+ # ------------- Librarys / Prereqs -------------
309
+ logger.debug("Checking for external dependencies")
218
310
  ffmpeg_binary = args.ffmpeg
219
311
  if ffmpeg_binary is None:
312
+ logger.debug("No FFmpeg specified, attempting to locate binary (auto_download=%s)", args.auto_download)
220
313
  ffmpeg_binary = get_ffmpeg_binary(args.auto_download)
221
314
  if ffmpeg_binary is None:
222
315
  logger.error("Could not find FFmpeg. Please install FFmpeg or specify its location using --ffmpeg or use --auto-download")
@@ -225,13 +318,13 @@ def main():
225
318
 
226
319
  opus_binary = args.opusenc
227
320
  if opus_binary is None:
321
+ logger.debug("No opusenc specified, attempting to locate binary (auto_download=%s)", args.auto_download)
228
322
  opus_binary = get_opus_binary(args.auto_download)
229
323
  if opus_binary is None:
230
324
  logger.error("Could not find opusenc. Please install opus-tools or specify its location using --opusenc or use --auto-download")
231
325
  sys.exit(1)
232
326
  logger.debug("Using opusenc binary: %s", opus_binary)
233
327
 
234
- # Check for media tags library and handle --show-tags option
235
328
  if (args.use_media_tags or args.show_tags or args.name_template) and not is_media_tags_available():
236
329
  if not ensure_mutagen(auto_install=args.auto_download):
237
330
  logger.warning("Media tags functionality requires the mutagen library but it could not be installed.")
@@ -240,8 +333,8 @@ def main():
240
333
  sys.exit(1)
241
334
  else:
242
335
  logger.info("Successfully enabled media tag support")
243
-
244
- # Handle recursive processing
336
+
337
+ # ------------- Recursive Processing -------------
245
338
  if args.recursive:
246
339
  logger.info("Processing folders recursively: %s", args.input_filename)
247
340
  process_tasks = process_recursive_folders(
@@ -266,113 +359,75 @@ def main():
266
359
  task_out_filename = os.path.join(folder_path, f"{output_name}.taf")
267
360
  else:
268
361
  task_out_filename = os.path.join(output_dir, f"{output_name}.taf")
269
-
362
+
363
+ skip_creation = False
364
+ if os.path.exists(task_out_filename):
365
+ logger.warning("Output file already exists: %s", task_out_filename)
366
+ valid_taf = check_tonie_file_cli(task_out_filename)
367
+
368
+ if valid_taf and not args.force_creation:
369
+ logger.warning("Valid Tonie file: %s", task_out_filename)
370
+ logger.warning("Skipping creation step for existing Tonie file: %s", task_out_filename)
371
+ skip_creation = True
372
+ else:
373
+ logger.info("Output file exists but is not a valid Tonie file, proceeding to create a new one.")
374
+
270
375
  logger.info("[%d/%d] Processing folder: %s -> %s",
271
376
  task_index + 1, len(process_tasks), folder_path, task_out_filename)
272
377
 
273
- create_tonie_file(task_out_filename, audio_files, args.no_tonie_header, args.user_timestamp,
274
- args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
275
- args.auto_download, not args.use_legacy_tags)
276
- logger.info("Successfully created Tonie file: %s", task_out_filename)
277
- created_files.append(task_out_filename)
378
+ if not skip_creation:
379
+ create_tonie_file(task_out_filename, audio_files, args.no_tonie_header, args.user_timestamp,
380
+ args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
381
+ args.auto_download, not args.use_legacy_tags,
382
+ no_mono_conversion=args.no_mono_conversion)
383
+ logger.info("Successfully created Tonie file: %s", task_out_filename)
278
384
 
279
- logger.info("Recursive processing completed. Created %d Tonie files.", len(process_tasks))
280
-
281
- # Handle upload to TeddyCloud if requested
282
- if args.upload and created_files:
283
- teddycloud_url = args.upload
385
+ created_files.append(task_out_filename)
386
+
387
+ # ------------- Initialization -------------------
388
+
389
+ artwork_url = None
284
390
 
285
- for taf_file in created_files:
286
- upload_success = upload_to_teddycloud(
287
- taf_file, teddycloud_url, args.ignore_ssl_verify,
288
- args.special_folder, args.path, args.show_progress,
289
- args.connection_timeout, args.read_timeout,
290
- args.max_retries, args.retry_delay
391
+ # ------------- Recursive File Upload -------------
392
+ if args.upload:
393
+ response = client.upload_file(
394
+ file_path=task_out_filename,
395
+ destination_path=args.path,
396
+ special=args.special_folder,
291
397
  )
398
+ upload_success = response.get('success', False)
292
399
 
293
400
  if not upload_success:
294
- logger.error("Failed to upload %s to TeddyCloud", taf_file)
401
+ logger.error("Failed to upload %s to TeddyCloud", task_out_filename)
295
402
  else:
296
- logger.info("Successfully uploaded %s to TeddyCloud", taf_file)
403
+ logger.info("Successfully uploaded %s to TeddyCloud", task_out_filename)
297
404
 
298
- # Handle artwork upload if requested
299
- if args.include_artwork:
300
- # Extract folder path from the current task
301
- folder_path = os.path.dirname(taf_file)
302
- taf_file_basename = os.path.basename(taf_file)
303
- taf_name = os.path.splitext(taf_file_basename)[0] # Get name without extension
304
- logger.info("Looking for artwork for %s", folder_path)
305
-
306
- # Try to find cover image in the folder
307
- from .media_tags import find_cover_image
308
- artwork_path = find_cover_image(folder_path)
309
- temp_artwork = None
310
-
311
- # If no cover image found, try to extract it from one of the audio files
312
- if not artwork_path:
313
- # Get current task's audio files
314
- for task_name, task_folder, task_files in process_tasks:
315
- if task_folder == folder_path or os.path.normpath(task_folder) == os.path.normpath(folder_path):
316
- if task_files and len(task_files) > 0:
317
- # Try to extract from first file
318
- from .media_tags import extract_artwork, ensure_mutagen
319
- if ensure_mutagen(auto_install=args.auto_download):
320
- temp_artwork = extract_artwork(task_files[0])
321
- if temp_artwork:
322
- artwork_path = temp_artwork
323
- break
324
-
325
- if artwork_path:
326
- logger.info("Found artwork for %s: %s", folder_path, artwork_path)
327
- artwork_upload_path = "/custom_img"
328
- artwork_ext = os.path.splitext(artwork_path)[1]
405
+ # Handle artwork upload
406
+ if args.include_artwork:
407
+ success, artwork_url = upload_artwork(client, task_out_filename, folder_path, audio_files)
408
+ if success:
409
+ logger.info("Successfully uploaded artwork for %s", task_out_filename)
410
+ else:
411
+ logger.warning("Failed to upload artwork for %s", task_out_filename)
329
412
 
330
- # Create a temporary copy with the same name as the taf file
331
- import shutil
332
- renamed_artwork_path = None
333
- try:
334
- renamed_artwork_path = os.path.join(os.path.dirname(artwork_path),
335
- f"{taf_name}{artwork_ext}")
336
-
337
- if renamed_artwork_path != artwork_path:
338
- shutil.copy2(artwork_path, renamed_artwork_path)
339
- logger.debug("Created renamed artwork copy: %s", renamed_artwork_path)
340
-
341
- logger.info("Uploading artwork to path: %s as %s%s",
342
- artwork_upload_path, taf_name, artwork_ext)
343
-
344
- artwork_upload_success = upload_to_teddycloud(
345
- renamed_artwork_path, teddycloud_url, args.ignore_ssl_verify,
346
- args.special_folder, artwork_upload_path, args.show_progress,
347
- args.connection_timeout, args.read_timeout,
348
- args.max_retries, args.retry_delay
349
- )
350
-
351
- if artwork_upload_success:
352
- logger.info("Successfully uploaded artwork for %s", folder_path)
353
- else:
354
- logger.warning("Failed to upload artwork for %s", folder_path)
355
-
356
- if renamed_artwork_path != artwork_path and os.path.exists(renamed_artwork_path):
357
- try:
358
- os.unlink(renamed_artwork_path)
359
- logger.debug("Removed temporary renamed artwork file: %s", renamed_artwork_path)
360
- except Exception as e:
361
- logger.debug("Failed to remove temporary renamed artwork file: %s", e)
362
-
363
- if temp_artwork and os.path.exists(temp_artwork) and temp_artwork != renamed_artwork_path:
364
- try:
365
- os.unlink(temp_artwork)
366
- logger.debug("Removed temporary artwork file: %s", temp_artwork)
367
- except Exception as e:
368
- logger.debug("Failed to remove temporary artwork file: %s", e)
369
- except Exception as e:
370
- logger.error("Error during artwork renaming or upload: %s", e)
371
- else:
372
- logger.warning("No artwork found for %s", folder_path)
413
+ # tonies.custom.json generation
414
+ if args.create_custom_json:
415
+ base_path = os.path.dirname(args.input_filename)
416
+ json_output_dir = base_path if args.output_to_source else output_dir
417
+ client_param = client if 'client' in locals() else None
418
+ if args.version_2:
419
+ logger.debug("Using version 2 of the Tonies JSON format")
420
+ success = fetch_and_update_tonies_json_v2(client_param, task_out_filename, audio_files, artwork_url, json_output_dir)
421
+ else:
422
+ success = fetch_and_update_tonies_json_v1(client_param, task_out_filename, audio_files, artwork_url, json_output_dir)
423
+ if success:
424
+ logger.info("Successfully updated Tonies JSON for %s", task_out_filename)
425
+ else:
426
+ logger.warning("Failed to update Tonies JSON for %s", task_out_filename)
427
+
428
+ logger.info("Recursive processing completed. Created %d Tonie files.", len(process_tasks))
373
429
  sys.exit(0)
374
-
375
- # Handle directory or file input
430
+ # ------------- Single File Processing -------------
376
431
  if os.path.isdir(args.input_filename):
377
432
  logger.debug("Input is a directory: %s", args.input_filename)
378
433
  args.input_filename += "/*"
@@ -387,7 +442,6 @@ def main():
387
442
  split_to_opus_files(args.input_filename, args.output_filename)
388
443
  sys.exit(0)
389
444
  elif args.compare:
390
- from .tonie_analysis import compare_taf_files
391
445
  logger.info("Comparing Tonie files: %s and %s", args.input_filename, args.compare)
392
446
  result = compare_taf_files(args.input_filename, args.compare, args.detailed_compare)
393
447
  sys.exit(0 if result else 1)
@@ -398,36 +452,14 @@ def main():
398
452
  if len(files) == 0:
399
453
  logger.error("No files found for pattern %s", args.input_filename)
400
454
  sys.exit(1)
401
-
402
- # Show tags for input files if requested
403
- if args.show_tags:
404
- from .media_tags import get_file_tags
405
- logger.info("Showing media tags for input files:")
406
-
407
- for file_index, file_path in enumerate(files):
408
- tags = get_file_tags(file_path)
409
- if tags:
410
- print(f"\nFile {file_index + 1}: {os.path.basename(file_path)}")
411
- print("-" * 40)
412
- for tag_name, tag_value in sorted(tags.items()):
413
- print(f"{tag_name}: {tag_value}")
414
- else:
415
- print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
416
- sys.exit(0)
417
-
418
- # Use media tags for file naming if requested
455
+
419
456
  guessed_name = None
420
457
  if args.use_media_tags:
421
- # If this is a single folder, try to get consistent album info
422
458
  if len(files) > 1 and os.path.dirname(files[0]) == os.path.dirname(files[-1]):
423
- folder_path = os.path.dirname(files[0])
424
-
425
- from .media_tags import extract_album_info, format_metadata_filename
426
- logger.debug("Extracting album info from folder: %s", folder_path)
427
-
459
+ folder_path = os.path.dirname(files[0])
460
+ logger.debug("Extracting album info from folder: %s", folder_path)
428
461
  album_info = extract_album_info(folder_path)
429
462
  if album_info:
430
- # Use album info for naming the output file
431
463
  template = args.name_template or "{album} - {artist}"
432
464
  new_name = format_metadata_filename(album_info, template)
433
465
 
@@ -436,10 +468,8 @@ def main():
436
468
  guessed_name = new_name
437
469
  else:
438
470
  logger.debug("Could not format filename from album metadata")
439
-
440
- # For single files, use the file's metadata
441
471
  elif len(files) == 1:
442
- from .media_tags import get_file_tags, format_metadata_filename
472
+
443
473
 
444
474
  tags = get_file_tags(files[0])
445
475
  if tags:
@@ -453,9 +483,7 @@ def main():
453
483
  logger.debug("Could not format filename from file metadata")
454
484
 
455
485
  # For multiple files from different folders, try to use common tags if they exist
456
- elif len(files) > 1:
457
- from .media_tags import get_file_tags, format_metadata_filename
458
-
486
+ elif len(files) > 1:
459
487
  # Try to find common tags among files
460
488
  common_tags = {}
461
489
  for file_path in files:
@@ -509,6 +537,9 @@ def main():
509
537
  os.makedirs(output_dir, exist_ok=True)
510
538
  out_filename = os.path.join(output_dir, guessed_name)
511
539
  logger.debug("Using default output location: %s", out_filename)
540
+
541
+ # Make sure source_dir is defined for later use with artwork upload
542
+ source_dir = os.path.dirname(files[0]) if files else '.'
512
543
 
513
544
  if args.append_tonie_tag:
514
545
  logger.debug("Appending Tonie tag to output filename")
@@ -526,155 +557,44 @@ def main():
526
557
  logger.info("Creating Tonie file: %s with %d input file(s)", out_filename, len(files))
527
558
  create_tonie_file(out_filename, files, args.no_tonie_header, args.user_timestamp,
528
559
  args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
529
- args.auto_download, not args.use_legacy_tags)
560
+ args.auto_download, not args.use_legacy_tags,
561
+ no_mono_conversion=args.no_mono_conversion)
530
562
  logger.info("Successfully created Tonie file: %s", out_filename)
531
563
 
532
- # Handle upload to TeddyCloud if requested
533
- if args.upload:
534
- teddycloud_url = args.upload
535
-
536
- upload_success = upload_to_teddycloud(
537
- out_filename, teddycloud_url, args.ignore_ssl_verify,
538
- args.special_folder, args.path, args.show_progress,
539
- args.connection_timeout, args.read_timeout,
540
- args.max_retries, args.retry_delay
564
+ # ------------- Single File Upload -------------
565
+ artwork_url = None
566
+ if args.upload:
567
+ response = client.upload_file(
568
+ file_path=out_filename,
569
+ destination_path=args.path,
570
+ special=args.special_folder,
541
571
  )
572
+ upload_success = response.get('success', False)
542
573
  if not upload_success:
543
574
  logger.error("Failed to upload %s to TeddyCloud", out_filename)
544
- sys.exit(1)
545
575
  else:
546
576
  logger.info("Successfully uploaded %s to TeddyCloud", out_filename)
547
-
548
- # Handle artwork upload if requested
549
- if args.include_artwork:
550
- logger.info("Looking for artwork to upload alongside the Tonie file")
551
- artwork_path = None
552
-
553
- # Try to find a cover image in the source directory first
554
- source_dir = os.path.dirname(files[0]) if files else None
555
- if source_dir:
556
- from .media_tags import find_cover_image
557
- artwork_path = find_cover_image(source_dir)
558
-
559
- # If no cover in source directory, try to extract it from audio file
560
- if not artwork_path and len(files) > 0:
561
- from .media_tags import extract_artwork, ensure_mutagen
562
-
563
- # Make sure mutagen is available for artwork extraction
564
- if ensure_mutagen(auto_install=args.auto_download):
565
- # Try to extract artwork from the first file
566
- temp_artwork = extract_artwork(files[0])
567
- if temp_artwork:
568
- artwork_path = temp_artwork
569
- # Note: this creates a temporary file that will be deleted after upload
570
-
571
- # Upload the artwork if found
572
- if artwork_path:
573
- logger.info("Found artwork: %s", artwork_path)
574
-
575
- # Create artwork upload path - keep same path but use "custom_img" folder
576
- artwork_upload_path = args.path
577
- if not artwork_upload_path:
578
- artwork_upload_path = "/custom_img"
579
- elif not artwork_upload_path.startswith("/custom_img"):
580
- # Make sure we're using the custom_img folder
581
- if artwork_upload_path.startswith("/"):
582
- artwork_upload_path = "/custom_img" + artwork_upload_path
583
- else:
584
- artwork_upload_path = "/custom_img/" + artwork_upload_path
585
-
586
- # Get the original artwork file extension
587
- artwork_ext = os.path.splitext(artwork_path)[1]
588
-
589
- # Create a temporary copy with the same name as the taf file
590
- import shutil
591
- renamed_artwork_path = None
592
- try:
593
- renamed_artwork_path = os.path.join(os.path.dirname(artwork_path),
594
- f"{os.path.splitext(os.path.basename(out_filename))[0]}{artwork_ext}")
595
-
596
- if renamed_artwork_path != artwork_path:
597
- shutil.copy2(artwork_path, renamed_artwork_path)
598
- logger.debug("Created renamed artwork copy: %s", renamed_artwork_path)
599
-
600
- logger.info("Uploading artwork to path: %s as %s%s",
601
- artwork_upload_path, os.path.splitext(os.path.basename(out_filename))[0], artwork_ext)
602
-
603
- artwork_upload_success = upload_to_teddycloud(
604
- renamed_artwork_path, teddycloud_url, args.ignore_ssl_verify,
605
- args.special_folder, artwork_upload_path, args.show_progress,
606
- args.connection_timeout, args.read_timeout,
607
- args.max_retries, args.retry_delay
608
- )
609
-
610
- if artwork_upload_success:
611
- logger.info("Successfully uploaded artwork")
612
- else:
613
- logger.warning("Failed to upload artwork")
614
-
615
- # Clean up temporary renamed file
616
- if renamed_artwork_path != artwork_path and os.path.exists(renamed_artwork_path):
617
- try:
618
- os.unlink(renamed_artwork_path)
619
- logger.debug("Removed temporary renamed artwork file: %s", renamed_artwork_path)
620
- except Exception as e:
621
- logger.debug("Failed to remove temporary renamed artwork file: %s", e)
622
-
623
- # Clean up temporary extracted artwork file if needed
624
- if temp_artwork and os.path.exists(temp_artwork) and temp_artwork != renamed_artwork_path:
625
- try:
626
- os.unlink(temp_artwork)
627
- logger.debug("Removed temporary artwork file: %s", temp_artwork)
628
- except Exception as e:
629
- logger.debug("Failed to remove temporary artwork file: %s", e)
630
- except Exception as e:
631
- logger.error("Error during artwork renaming or upload: %s", e)
632
- else:
633
- logger.warning("No artwork found to upload")
634
577
 
635
- # Handle create-custom-json option
636
- if args.create_custom_json and args.upload:
637
- teddycloud_url = args.upload
638
- artwork_url = None
639
-
640
- # If artwork was uploaded, construct its URL for the JSON
578
+ # Handle artwork upload
641
579
  if args.include_artwork:
642
- taf_basename = os.path.splitext(os.path.basename(out_filename))[0]
643
- artwork_ext = None
644
-
645
- # Try to determine the artwork extension by checking what was uploaded
646
- source_dir = os.path.dirname(files[0]) if files else None
647
- if source_dir:
648
- from .media_tags import find_cover_image
649
- artwork_path = find_cover_image(source_dir)
650
- if artwork_path:
651
- artwork_ext = os.path.splitext(artwork_path)[1]
652
-
653
- # If we couldn't determine extension from a found image, default to .jpg
654
- if not artwork_ext:
655
- artwork_ext = ".jpg"
656
-
657
- # Construct the URL for the artwork based on TeddyCloud structure
658
- artwork_path = args.path or "/custom_img"
659
- if not artwork_path.endswith('/'):
660
- artwork_path += '/'
661
-
662
- artwork_url = f"{teddycloud_url}{artwork_path}{taf_basename}{artwork_ext}"
663
- logger.debug("Using artwork URL: %s", artwork_url)
664
-
665
- logger.info("Fetching and updating custom Tonies JSON data")
666
- success = fetch_and_update_tonies_json(
667
- teddycloud_url,
668
- args.ignore_ssl_verify,
669
- out_filename,
670
- files,
671
- artwork_url
672
- )
673
-
580
+ success, artwork_url = upload_artwork(client, out_filename, source_dir, files)
581
+ if success:
582
+ logger.info("Successfully uploaded artwork for %s", out_filename)
583
+ else:
584
+ logger.warning("Failed to upload artwork for %s", out_filename)
585
+
586
+ if args.create_custom_json:
587
+ json_output_dir = source_dir if args.output_to_source else './output'
588
+ client_param = client if 'client' in locals() else None
589
+ if args.version_2:
590
+ logger.debug("Using version 2 of the Tonies JSON format")
591
+ success = fetch_and_update_tonies_json_v2(client_param, out_filename, files, artwork_url, json_output_dir)
592
+ else:
593
+ success = fetch_and_update_tonies_json_v1(client_param, out_filename, files, artwork_url, json_output_dir)
674
594
  if success:
675
- logger.info("Successfully updated custom Tonies JSON data")
595
+ logger.info("Successfully updated Tonies JSON for %s", out_filename)
676
596
  else:
677
- logger.warning("Failed to update custom Tonies JSON data")
597
+ logger.warning("Failed to update Tonies JSON for %s", out_filename)
678
598
 
679
599
  if __name__ == "__main__":
680
600
  main()