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 +2 -1
- TonieToolbox/__main__.py +303 -141
- TonieToolbox/artwork.py +59 -10
- TonieToolbox/audio_conversion.py +106 -34
- TonieToolbox/constants.py +133 -10
- TonieToolbox/dependency_manager.py +679 -184
- TonieToolbox/filename_generator.py +57 -10
- TonieToolbox/integration.py +73 -0
- TonieToolbox/integration_macos.py +613 -0
- TonieToolbox/integration_ubuntu.py +2 -0
- TonieToolbox/integration_windows.py +445 -0
- TonieToolbox/logger.py +9 -10
- TonieToolbox/media_tags.py +24 -104
- TonieToolbox/ogg_page.py +41 -41
- TonieToolbox/opus_packet.py +15 -15
- TonieToolbox/recursive_processor.py +34 -34
- TonieToolbox/tags.py +4 -5
- TonieToolbox/teddycloud.py +164 -51
- TonieToolbox/tonie_analysis.py +26 -24
- TonieToolbox/tonie_file.py +88 -72
- TonieToolbox/tonies_json.py +830 -37
- TonieToolbox/version_handler.py +14 -20
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0.dist-info}/METADATA +257 -177
- tonietoolbox-0.6.0.dist-info/RECORD +30 -0
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0.dist-info}/WHEEL +1 -1
- tonietoolbox-0.5.0a1.dist-info/RECORD +0 -26
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0.dist-info}/top_level.txt +0 -0
TonieToolbox/__init__.py
CHANGED
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
|
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: "{
|
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(
|
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
|
-
# -------------
|
208
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
if
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
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("
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
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
|
-
|
291
|
-
|
292
|
-
|
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
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
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
|
-
|
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 "{
|
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
|
-
|
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
|
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
|
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=
|
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
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
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()
|