TonieToolbox 0.4.1__py3-none-any.whl → 0.5.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/__main__.py CHANGED
@@ -7,27 +7,28 @@ 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
15
14
  from .dependency_manager import get_ffmpeg_binary, get_opus_binary
16
15
  from .logger import 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
19
+ from .media_tags import is_available as is_media_tags_available, ensure_mutagen, extract_album_info, format_metadata_filename
20
+ from .teddycloud import TeddyCloudClient
21
+ from .tags import get_tags
22
22
  from .tonies_json import fetch_and_update_tonies_json
23
+ from .artwork import upload_artwork
24
+
23
25
 
24
26
  def main():
25
27
  """Entry point for the TonieToolbox application."""
26
28
  parser = argparse.ArgumentParser(description='Create Tonie compatible file from Ogg opus file(s).')
27
29
  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
30
+ help='show program version and exit')
31
+ # ------------- Parser - Teddycloud -------------
31
32
  teddycloud_group = parser.add_argument_group('TeddyCloud Options')
32
33
  teddycloud_group.add_argument('--upload', metavar='URL', action='store',
33
34
  help='Upload to TeddyCloud instance (e.g., https://teddycloud.example.com). Supports .taf, .jpg, .jpeg, .png files.')
@@ -41,8 +42,6 @@ def main():
41
42
  help='Special folder to upload to (currently only "library" is supported)', default='library')
42
43
  teddycloud_group.add_argument('--path', action='store', metavar='PATH',
43
44
  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
45
  teddycloud_group.add_argument('--connection-timeout', type=int, metavar='SECONDS', default=10,
47
46
  help='Connection timeout in seconds (default: 10)')
48
47
  teddycloud_group.add_argument('--read-timeout', type=int, metavar='SECONDS', default=300,
@@ -53,18 +52,31 @@ def main():
53
52
  help='Delay between retry attempts in seconds (default: 5)')
54
53
  teddycloud_group.add_argument('--create-custom-json', action='store_true',
55
54
  help='Fetch and update custom Tonies JSON data')
55
+ # ------------- Parser - Authentication options for TeddyCloud -------------
56
+ teddycloud_group.add_argument('--username', action='store', metavar='USERNAME',
57
+ help='Username for basic authentication')
58
+ teddycloud_group.add_argument('--password', action='store', metavar='PASSWORD',
59
+ help='Password for basic authentication')
60
+ teddycloud_group.add_argument('--client-cert', action='store', metavar='CERT_FILE',
61
+ help='Path to client certificate file for certificate-based authentication')
62
+ teddycloud_group.add_argument('--client-key', action='store', metavar='KEY_FILE',
63
+ help='Path to client private key file for certificate-based authentication')
56
64
 
65
+ # ------------- Parser - Source Input -------------
57
66
  parser.add_argument('input_filename', metavar='SOURCE', type=str, nargs='?',
58
67
  help='input file or directory or a file list (.lst)')
59
68
  parser.add_argument('output_filename', metavar='TARGET', nargs='?', type=str,
60
69
  help='the output file name (default: ---ID---)')
61
70
  parser.add_argument('-t', '--timestamp', dest='user_timestamp', metavar='TIMESTAMP', action='store',
62
71
  help='set custom timestamp / bitstream serial')
63
-
72
+ # ------------- Parser - Librarys -------------
64
73
  parser.add_argument('-f', '--ffmpeg', help='specify location of ffmpeg', default=None)
65
74
  parser.add_argument('-o', '--opusenc', help='specify location of opusenc', default=None)
66
75
  parser.add_argument('-b', '--bitrate', type=int, help='set encoding bitrate in kbps (default: 96)', default=96)
67
76
  parser.add_argument('-c', '--cbr', action='store_true', help='encode in cbr mode')
77
+ parser.add_argument('--auto-download', action='store_true',
78
+ help='automatically download ffmpeg and opusenc if not found')
79
+ # ------------- Parser - TAF -------------
68
80
  parser.add_argument('-a', '--append-tonie-tag', metavar='TAG', action='store',
69
81
  help='append [TAG] to filename (must be an 8-character hex value)')
70
82
  parser.add_argument('-n', '--no-tonie-header', action='store_true', help='do not write Tonie header')
@@ -73,7 +85,9 @@ def main():
73
85
  parser.add_argument('-r', '--recursive', action='store_true', help='Process folders recursively')
74
86
  parser.add_argument('-O', '--output-to-source', action='store_true',
75
87
  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')
88
+ parser.add_argument('-fc', '--force-creation', action='store_true', default=False,
89
+ help='Force creation of Tonie file even if it already exists')
90
+ # ------------- Parser - Debug TAFs -------------
77
91
  parser.add_argument('-k', '--keep-temp', action='store_true',
78
92
  help='Keep temporary opus files in a temp folder for testing')
79
93
  parser.add_argument('-u', '--use-legacy-tags', action='store_true',
@@ -81,9 +95,8 @@ def main():
81
95
  parser.add_argument('-C', '--compare', action='store', metavar='FILE2',
82
96
  help='Compare input file with another .taf file for debugging')
83
97
  parser.add_argument('-D', '--detailed-compare', action='store_true',
84
- help='Show detailed OGG page differences when comparing files')
85
-
86
- # Media tag options
98
+ help='Show detailed OGG page differences when comparing files')
99
+ # ------------- Parser - Media Tag Options -------------
87
100
  media_tag_group = parser.add_argument_group('Media Tag Options')
88
101
  media_tag_group.add_argument('-m', '--use-media-tags', action='store_true',
89
102
  help='Use media tags from audio files for naming')
@@ -91,8 +104,7 @@ def main():
91
104
  help='Template for naming files using media tags. Example: "{album} - {artist}"')
92
105
  media_tag_group.add_argument('--show-tags', action='store_true',
93
106
  help='Show available media tags from input files')
94
-
95
- # Version check options
107
+ # ------------- Parser - Version handling -------------
96
108
  version_group = parser.add_argument_group('Version Check Options')
97
109
  version_group.add_argument('-S', '--skip-update-check', action='store_true',
98
110
  help='Skip checking for updates')
@@ -100,7 +112,7 @@ def main():
100
112
  help='Force refresh of update information from PyPI')
101
113
  version_group.add_argument('-X', '--clear-version-cache', action='store_true',
102
114
  help='Clear cached version information')
103
-
115
+ # ------------- Parser - Logging -------------
104
116
  log_group = parser.add_argument_group('Logging Options')
105
117
  log_level_group = log_group.add_mutually_exclusive_group()
106
118
  log_level_group.add_argument('-d', '--debug', action='store_true', help='Enable debug logging')
@@ -109,14 +121,13 @@ def main():
109
121
  log_level_group.add_argument('-Q', '--silent', action='store_true', help='Show only errors')
110
122
  log_group.add_argument('--log-file', action='store_true', default=False,
111
123
  help='Save logs to a timestamped file in .tonietoolbox folder')
112
-
113
124
  args = parser.parse_args()
114
125
 
115
- # Validate that input_filename is provided if not using --get-tags or --upload-existing
126
+ # ------------- Parser - Source Input -------------
116
127
  if args.input_filename is None and not (args.get_tags or args.upload):
117
128
  parser.error("the following arguments are required: SOURCE")
118
-
119
- # Set up the logging level
129
+
130
+ # ------------- Logging -------------
120
131
  if args.trace:
121
132
  from .logger import TRACE
122
133
  log_level = TRACE
@@ -127,17 +138,15 @@ def main():
127
138
  elif args.silent:
128
139
  log_level = logging.ERROR
129
140
  else:
130
- log_level = logging.INFO
131
-
141
+ log_level = logging.INFO
132
142
  setup_logging(log_level, log_to_file=args.log_file)
133
143
  logger = get_logger('main')
134
144
  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))
145
+ logger.debug("Command-line arguments: %s", vars(args))
138
146
 
147
+ # ------------- Version handling -------------
139
148
  if args.clear_version_cache:
140
- logger.log(logging.DEBUG - 1, "Clearing version cache")
149
+ logger.debug("Clearing version cache")
141
150
  if clear_version_cache():
142
151
  logger.info("Version cache cleared successfully")
143
152
  else:
@@ -150,73 +159,143 @@ def main():
150
159
  force_refresh=args.force_refresh_cache
151
160
  )
152
161
 
153
- logger.log(logging.DEBUG - 1, "Update check results: is_latest=%s, latest_version=%s, update_confirmed=%s",
162
+ logger.debug( "Update check results: is_latest=%s, latest_version=%s, update_confirmed=%s",
154
163
  is_latest, latest_version, update_confirmed)
155
164
 
156
165
  if not is_latest and not update_confirmed and not (args.silent or args.quiet):
157
166
  logger.info("Update available but user chose to continue without updating.")
158
167
 
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)
166
-
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)
171
-
172
- if not args.input_filename:
173
- logger.error("Missing input file for --upload. Provide a file path as SOURCE argument.")
168
+ # ------------- Normalize Path Input -------------
169
+ if args.input_filename:
170
+ logger.debug("Original input path: %s", args.input_filename)
171
+ # Strip quotes from the beginning and end
172
+ args.input_filename = args.input_filename.strip('"\'')
173
+ # Handle paths that end with a backslash
174
+ if args.input_filename.endswith('\\'):
175
+ args.input_filename = args.input_filename.rstrip('\\')
176
+ logger.debug("Normalized input path: %s", args.input_filename)
177
+
178
+ # ------------- Setup TeddyCloudClient-------------
179
+ if args.upload or args.get_tags:
180
+ if args.upload:
181
+ teddycloud_url = args.upload
182
+ elif args.get_tags:
183
+ teddycloud_url = args.get_tags
184
+ if not teddycloud_url:
185
+ logger.error("TeddyCloud URL is required for --upload or --get-tags")
174
186
  sys.exit(1)
187
+ try:
188
+ client = TeddyCloudClient(
189
+ base_url=teddycloud_url,
190
+ ignore_ssl_verify=args.ignore_ssl_verify,
191
+ username=args.username,
192
+ password=args.password,
193
+ cert_file=args.client_cert,
194
+ key_file=args.client_key
195
+ )
196
+ logger.debug("TeddyCloud client initialized successfully")
197
+ except Exception as e:
198
+ logger.error("Failed to initialize TeddyCloud client: %s", str(e))
199
+ sys.exit(1)
200
+
201
+ if args.get_tags:
202
+ logger.debug("Getting tags from TeddyCloud: %s", teddycloud_url)
203
+ success = get_tags(client)
204
+ logger.debug( "Exiting with code %d", 0 if success else 1)
205
+ sys.exit(0 if success else 1)
175
206
 
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)
207
+ # ------------- Direct Upload -------------
208
+ if args.upload and not args.recursive:
209
+ logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
210
+ logger.trace("TeddyCloud upload parameters: path=%s, special_folder=%s, ignore_ssl=%s",
211
+ 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")
186
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()
187
222
 
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
-
223
+ logger.debug("File to upload: %s (size: %d bytes, type: %s)",
224
+ file_path, file_size, file_ext)
196
225
  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
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,
202
232
  )
233
+ logger.trace("Upload response received: %s", response)
203
234
 
235
+ upload_success = response.get('success', False)
204
236
  if not upload_success:
205
- logger.error("Failed to upload %s to TeddyCloud", file_path)
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")
206
240
  sys.exit(1)
207
241
  else:
208
242
  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
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)
217
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")
293
+
294
+ # ------------- Librarys / Prereqs -------------
295
+ logger.debug("Checking for external dependencies")
218
296
  ffmpeg_binary = args.ffmpeg
219
297
  if ffmpeg_binary is None:
298
+ logger.debug("No FFmpeg specified, attempting to locate binary (auto_download=%s)", args.auto_download)
220
299
  ffmpeg_binary = get_ffmpeg_binary(args.auto_download)
221
300
  if ffmpeg_binary is None:
222
301
  logger.error("Could not find FFmpeg. Please install FFmpeg or specify its location using --ffmpeg or use --auto-download")
@@ -225,13 +304,13 @@ def main():
225
304
 
226
305
  opus_binary = args.opusenc
227
306
  if opus_binary is None:
307
+ logger.debug("No opusenc specified, attempting to locate binary (auto_download=%s)", args.auto_download)
228
308
  opus_binary = get_opus_binary(args.auto_download)
229
309
  if opus_binary is None:
230
310
  logger.error("Could not find opusenc. Please install opus-tools or specify its location using --opusenc or use --auto-download")
231
311
  sys.exit(1)
232
312
  logger.debug("Using opusenc binary: %s", opus_binary)
233
313
 
234
- # Check for media tags library and handle --show-tags option
235
314
  if (args.use_media_tags or args.show_tags or args.name_template) and not is_media_tags_available():
236
315
  if not ensure_mutagen(auto_install=args.auto_download):
237
316
  logger.warning("Media tags functionality requires the mutagen library but it could not be installed.")
@@ -240,8 +319,8 @@ def main():
240
319
  sys.exit(1)
241
320
  else:
242
321
  logger.info("Successfully enabled media tag support")
243
-
244
- # Handle recursive processing
322
+
323
+ # ------------- Recursive Processing -------------
245
324
  if args.recursive:
246
325
  logger.info("Processing folders recursively: %s", args.input_filename)
247
326
  process_tasks = process_recursive_folders(
@@ -266,113 +345,62 @@ def main():
266
345
  task_out_filename = os.path.join(folder_path, f"{output_name}.taf")
267
346
  else:
268
347
  task_out_filename = os.path.join(output_dir, f"{output_name}.taf")
269
-
348
+
349
+ skip_creation = False
350
+ if os.path.exists(task_out_filename):
351
+ logger.warning("Output file already exists: %s", task_out_filename)
352
+ valid_taf = check_tonie_file_cli(task_out_filename)
353
+
354
+ if valid_taf and not args.force_creation:
355
+ logger.warning("Valid Tonie file: %s", task_out_filename)
356
+ logger.warning("Skipping creation step for existing Tonie file: %s", task_out_filename)
357
+ skip_creation = True
358
+ else:
359
+ logger.info("Output file exists but is not a valid Tonie file, proceeding to create a new one.")
360
+
270
361
  logger.info("[%d/%d] Processing folder: %s -> %s",
271
362
  task_index + 1, len(process_tasks), folder_path, task_out_filename)
272
363
 
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)
278
-
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
364
+ if not skip_creation:
365
+ create_tonie_file(task_out_filename, audio_files, args.no_tonie_header, args.user_timestamp,
366
+ args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
367
+ args.auto_download, not args.use_legacy_tags)
368
+ logger.info("Successfully created Tonie file: %s", task_out_filename)
284
369
 
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
370
+ created_files.append(task_out_filename)
371
+ # ------------- Recursive File Upload -------------
372
+ if args.upload:
373
+ response = client.upload_file(
374
+ file_path=task_out_filename,
375
+ destination_path=args.path,
376
+ special=args.special_folder,
291
377
  )
378
+ upload_success = response.get('success', False)
292
379
 
293
380
  if not upload_success:
294
- logger.error("Failed to upload %s to TeddyCloud", taf_file)
381
+ logger.error("Failed to upload %s to TeddyCloud", task_out_filename)
295
382
  else:
296
- logger.info("Successfully uploaded %s to TeddyCloud", taf_file)
383
+ logger.info("Successfully uploaded %s to TeddyCloud", task_out_filename)
297
384
 
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]
385
+ # Handle artwork upload
386
+ if args.include_artwork:
387
+ success, artwork_url = upload_artwork(client, task_out_filename, folder_path, audio_files)
388
+ if success:
389
+ logger.info("Successfully uploaded artwork for %s", task_out_filename)
390
+ else:
391
+ logger.warning("Failed to upload artwork for %s", task_out_filename)
329
392
 
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)
393
+ # 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)
400
+
401
+ logger.info("Recursive processing completed. Created %d Tonie files.", len(process_tasks))
373
402
  sys.exit(0)
374
-
375
- # Handle directory or file input
403
+ # ------------- Single File Processing -------------
376
404
  if os.path.isdir(args.input_filename):
377
405
  logger.debug("Input is a directory: %s", args.input_filename)
378
406
  args.input_filename += "/*"
@@ -398,8 +426,6 @@ def main():
398
426
  if len(files) == 0:
399
427
  logger.error("No files found for pattern %s", args.input_filename)
400
428
  sys.exit(1)
401
-
402
- # Show tags for input files if requested
403
429
  if args.show_tags:
404
430
  from .media_tags import get_file_tags
405
431
  logger.info("Showing media tags for input files:")
@@ -414,20 +440,14 @@ def main():
414
440
  else:
415
441
  print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
416
442
  sys.exit(0)
417
-
418
- # Use media tags for file naming if requested
419
443
  guessed_name = None
420
444
  if args.use_media_tags:
421
- # If this is a single folder, try to get consistent album info
422
445
  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
446
+ folder_path = os.path.dirname(files[0])
426
447
  logger.debug("Extracting album info from folder: %s", folder_path)
427
448
 
428
449
  album_info = extract_album_info(folder_path)
429
450
  if album_info:
430
- # Use album info for naming the output file
431
451
  template = args.name_template or "{album} - {artist}"
432
452
  new_name = format_metadata_filename(album_info, template)
433
453
 
@@ -436,8 +456,6 @@ def main():
436
456
  guessed_name = new_name
437
457
  else:
438
458
  logger.debug("Could not format filename from album metadata")
439
-
440
- # For single files, use the file's metadata
441
459
  elif len(files) == 1:
442
460
  from .media_tags import get_file_tags, format_metadata_filename
443
461
 
@@ -509,6 +527,9 @@ def main():
509
527
  os.makedirs(output_dir, exist_ok=True)
510
528
  out_filename = os.path.join(output_dir, guessed_name)
511
529
  logger.debug("Using default output location: %s", out_filename)
530
+
531
+ # Make sure source_dir is defined for later use with artwork upload
532
+ source_dir = os.path.dirname(files[0]) if files else '.'
512
533
 
513
534
  if args.append_tonie_tag:
514
535
  logger.debug("Appending Tonie tag to output filename")
@@ -529,152 +550,34 @@ def main():
529
550
  args.auto_download, not args.use_legacy_tags)
530
551
  logger.info("Successfully created Tonie file: %s", out_filename)
531
552
 
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
553
+ # ------------- Single File Upload -------------
554
+ artwork_url = None
555
+ if args.upload:
556
+ response = client.upload_file(
557
+ file_path=out_filename,
558
+ destination_path=args.path,
559
+ special=args.special_folder,
541
560
  )
561
+ upload_success = response.get('success', False)
542
562
  if not upload_success:
543
563
  logger.error("Failed to upload %s to TeddyCloud", out_filename)
544
- sys.exit(1)
545
564
  else:
546
565
  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
566
 
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
567
+ # Handle artwork upload
641
568
  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
-
674
- if success:
675
- logger.info("Successfully updated custom Tonies JSON data")
676
- else:
677
- logger.warning("Failed to update custom Tonies JSON data")
569
+ success, artwork_url = upload_artwork(client, out_filename, source_dir, files)
570
+ if success:
571
+ logger.info("Successfully uploaded artwork for %s", out_filename)
572
+ else:
573
+ 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)
678
581
 
679
582
  if __name__ == "__main__":
680
583
  main()