TonieToolbox 0.6.0a4__py3-none-any.whl → 0.6.0rc1__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 +133 -16
- TonieToolbox/artwork.py +2 -2
- TonieToolbox/audio_conversion.py +2 -1
- TonieToolbox/constants.py +1 -0
- TonieToolbox/dependency_manager.py +2 -1
- TonieToolbox/filename_generator.py +51 -2
- TonieToolbox/integration.py +22 -23
- TonieToolbox/integration_macos.py +6 -1
- TonieToolbox/integration_ubuntu.py +1 -0
- TonieToolbox/integration_windows.py +7 -3
- TonieToolbox/logger.py +1 -0
- TonieToolbox/media_tags.py +2 -3
- TonieToolbox/ogg_page.py +2 -2
- TonieToolbox/opus_packet.py +2 -2
- TonieToolbox/recursive_processor.py +2 -1
- TonieToolbox/tags.py +1 -1
- TonieToolbox/teddycloud.py +114 -1
- TonieToolbox/tonie_analysis.py +2 -1
- TonieToolbox/tonie_file.py +2 -1
- TonieToolbox/tonies_json.py +2 -1
- TonieToolbox/version_handler.py +2 -5
- {tonietoolbox-0.6.0a4.dist-info → tonietoolbox-0.6.0rc1.dist-info}/METADATA +122 -89
- tonietoolbox-0.6.0rc1.dist-info/RECORD +30 -0
- {tonietoolbox-0.6.0a4.dist-info → tonietoolbox-0.6.0rc1.dist-info}/WHEEL +1 -1
- tonietoolbox-0.6.0a4.dist-info/RECORD +0 -30
- {tonietoolbox-0.6.0a4.dist-info → tonietoolbox-0.6.0rc1.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.6.0a4.dist-info → tonietoolbox-0.6.0rc1.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.6.0a4.dist-info → tonietoolbox-0.6.0rc1.dist-info}/top_level.txt +0 -0
TonieToolbox/__init__.py
CHANGED
TonieToolbox/__main__.py
CHANGED
@@ -13,7 +13,7 @@ from .tonie_file import create_tonie_file
|
|
13
13
|
from .tonie_analysis import check_tonie_file, check_tonie_file_cli, split_to_opus_files, compare_taf_files
|
14
14
|
from .dependency_manager import get_ffmpeg_binary, get_opus_binary, ensure_dependency
|
15
15
|
from .logger import TRACE, setup_logging, get_logger
|
16
|
-
from .filename_generator import guess_output_filename
|
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
19
|
from .media_tags import is_available as is_media_tags_available, ensure_mutagen, extract_album_info, format_metadata_filename, get_file_tags
|
@@ -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,
|
@@ -112,7 +112,9 @@ def main():
|
|
112
112
|
media_tag_group.add_argument('-m', '--use-media-tags', action='store_true',
|
113
113
|
help='Use media tags from audio files for naming')
|
114
114
|
media_tag_group.add_argument('--name-template', metavar='TEMPLATE', action='store',
|
115
|
-
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}"')
|
116
118
|
media_tag_group.add_argument('--show-tags', action='store_true',
|
117
119
|
help='Show available media tags from input files')
|
118
120
|
# ------------- Parser - Version handling -------------
|
@@ -150,7 +152,7 @@ def main():
|
|
150
152
|
else:
|
151
153
|
log_level = logging.INFO
|
152
154
|
setup_logging(log_level, log_to_file=args.log_file)
|
153
|
-
logger = get_logger(
|
155
|
+
logger = get_logger(__name__)
|
154
156
|
logger.debug("Starting TonieToolbox v%s with log level: %s", __version__, logging.getLevelName(log_level))
|
155
157
|
logger.debug("Command-line arguments: %s", vars(args))
|
156
158
|
|
@@ -279,8 +281,33 @@ def main():
|
|
279
281
|
file_path, file_size, file_ext)
|
280
282
|
logger.info("Uploading %s to TeddyCloud %s", file_path, teddycloud_url)
|
281
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
|
293
|
+
else:
|
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
|
+
|
282
309
|
response = client.upload_file(
|
283
|
-
destination_path=
|
310
|
+
destination_path=upload_path,
|
284
311
|
file_path=file_path,
|
285
312
|
special=args.special_folder,
|
286
313
|
)
|
@@ -483,25 +510,25 @@ def main():
|
|
483
510
|
|
484
511
|
guessed_name = None
|
485
512
|
if args.use_media_tags:
|
513
|
+
logger.debug("Using media tags for naming")
|
486
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")
|
487
516
|
folder_path = os.path.dirname(files[0])
|
488
517
|
logger.debug("Extracting album info from folder: %s", folder_path)
|
489
518
|
album_info = extract_album_info(folder_path)
|
490
519
|
if album_info:
|
491
|
-
template = args.name_template or "{
|
492
|
-
new_name = format_metadata_filename(album_info, template)
|
493
|
-
|
520
|
+
template = args.name_template or "{artist} - {album}"
|
521
|
+
new_name = format_metadata_filename(album_info, template)
|
494
522
|
if new_name:
|
495
523
|
logger.info("Using album metadata for output filename: %s", new_name)
|
496
524
|
guessed_name = new_name
|
497
525
|
else:
|
498
526
|
logger.debug("Could not format filename from album metadata")
|
499
527
|
elif len(files) == 1:
|
500
|
-
|
501
|
-
|
502
528
|
tags = get_file_tags(files[0])
|
503
529
|
if tags:
|
504
|
-
|
530
|
+
logger.debug("")
|
531
|
+
template = args.name_template or "{artist} - {title}"
|
505
532
|
new_name = format_metadata_filename(tags, template)
|
506
533
|
|
507
534
|
if new_name:
|
@@ -538,20 +565,71 @@ def main():
|
|
538
565
|
else:
|
539
566
|
logger.debug("Could not format filename from common metadata")
|
540
567
|
|
541
|
-
if args.output_filename:
|
568
|
+
if args.output_filename:
|
542
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
|
543
620
|
elif guessed_name:
|
621
|
+
logger.debug("Using guessed name for output: %s", guessed_name)
|
544
622
|
if args.output_to_source:
|
545
623
|
source_dir = os.path.dirname(files[0]) if files else '.'
|
546
624
|
out_filename = os.path.join(source_dir, guessed_name)
|
547
|
-
logger.debug("Using source location for output
|
625
|
+
logger.debug("Using source location for output: %s", out_filename)
|
548
626
|
else:
|
549
627
|
output_dir = './output'
|
550
628
|
if not os.path.exists(output_dir):
|
551
629
|
logger.debug("Creating default output directory: %s", output_dir)
|
552
630
|
os.makedirs(output_dir, exist_ok=True)
|
553
631
|
out_filename = os.path.join(output_dir, guessed_name)
|
554
|
-
logger.debug("Using default output location
|
632
|
+
logger.debug("Using default output location: %s", out_filename)
|
555
633
|
else:
|
556
634
|
guessed_name = guess_output_filename(args.input_filename, files)
|
557
635
|
if args.output_to_source:
|
@@ -581,6 +659,7 @@ def main():
|
|
581
659
|
|
582
660
|
if not out_filename.lower().endswith('.taf'):
|
583
661
|
out_filename += '.taf'
|
662
|
+
ensure_directory_exists(out_filename)
|
584
663
|
|
585
664
|
logger.info("Creating Tonie file: %s with %d input file(s)", out_filename, len(files))
|
586
665
|
create_tonie_file(out_filename, files, args.no_tonie_header, args.user_timestamp,
|
@@ -591,10 +670,48 @@ def main():
|
|
591
670
|
|
592
671
|
# ------------- Single File Upload -------------
|
593
672
|
artwork_url = None
|
594
|
-
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
|
+
|
595
712
|
response = client.upload_file(
|
596
713
|
file_path=out_filename,
|
597
|
-
destination_path=
|
714
|
+
destination_path=upload_path,
|
598
715
|
special=args.special_folder,
|
599
716
|
)
|
600
717
|
upload_success = response.get('success', False)
|
TonieToolbox/artwork.py
CHANGED
@@ -13,6 +13,7 @@ from .logger import get_logger
|
|
13
13
|
from .teddycloud import TeddyCloudClient
|
14
14
|
from .media_tags import extract_artwork, find_cover_image
|
15
15
|
|
16
|
+
logger = get_logger(__name__)
|
16
17
|
|
17
18
|
def upload_artwork(
|
18
19
|
client: TeddyCloudClient,
|
@@ -30,8 +31,7 @@ def upload_artwork(
|
|
30
31
|
audio_files (list[str]): List of audio files to extract artwork from if needed
|
31
32
|
Returns:
|
32
33
|
tuple[bool, Optional[str]]: (success, artwork_url) where success is a boolean and artwork_url is the URL of the uploaded artwork
|
33
|
-
"""
|
34
|
-
logger = get_logger('artwork')
|
34
|
+
"""
|
35
35
|
logger.info("Looking for artwork for Tonie file: %s", taf_filename)
|
36
36
|
taf_basename = os.path.basename(taf_filename)
|
37
37
|
taf_name = os.path.splitext(taf_basename)[0]
|
TonieToolbox/audio_conversion.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
"""
|
2
3
|
Audio conversion functionality for the TonieToolbox package
|
3
4
|
"""
|
@@ -10,7 +11,7 @@ from .dependency_manager import get_ffmpeg_binary, get_opus_binary
|
|
10
11
|
from .constants import SUPPORTED_EXTENSIONS
|
11
12
|
from .logger import get_logger
|
12
13
|
|
13
|
-
logger = get_logger(
|
14
|
+
logger = get_logger(__name__)
|
14
15
|
|
15
16
|
|
16
17
|
def get_opus_tempfile(
|
TonieToolbox/constants.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
"""
|
2
3
|
Dependency management for the TonieToolbox package.
|
3
4
|
|
@@ -22,7 +23,7 @@ import concurrent.futures
|
|
22
23
|
from tqdm.auto import tqdm
|
23
24
|
|
24
25
|
from .logger import get_logger
|
25
|
-
logger = get_logger(
|
26
|
+
logger = get_logger(__name__)
|
26
27
|
|
27
28
|
CACHE_DIR = os.path.join(os.path.expanduser("~"), ".tonietoolbox")
|
28
29
|
LIBS_DIR = os.path.join(CACHE_DIR, "libs")
|
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
"""
|
2
3
|
Module for generating intelligent output filenames for TonieToolbox.
|
3
4
|
"""
|
@@ -8,7 +9,7 @@ from pathlib import Path
|
|
8
9
|
from typing import List, Optional
|
9
10
|
from .logger import get_logger
|
10
11
|
|
11
|
-
logger = get_logger(
|
12
|
+
logger = get_logger(__name__)
|
12
13
|
|
13
14
|
def sanitize_filename(filename: str) -> str:
|
14
15
|
"""
|
@@ -89,4 +90,52 @@ def guess_output_filename(input_filename: str, input_files: list[str] = None) ->
|
|
89
90
|
except ValueError:
|
90
91
|
# Files might be on different drives
|
91
92
|
logger.debug("Could not determine common path, using generic name")
|
92
|
-
return "tonie_collection"
|
93
|
+
return "tonie_collection"
|
94
|
+
|
95
|
+
def apply_template_to_path(template, metadata):
|
96
|
+
"""
|
97
|
+
Apply metadata to a path template and ensure the path is valid.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
template: String template with {tag} placeholders
|
101
|
+
metadata: Dictionary of tag values
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
Formatted path with placeholders replaced by actual values
|
105
|
+
"""
|
106
|
+
if not template or not metadata:
|
107
|
+
return None
|
108
|
+
|
109
|
+
try:
|
110
|
+
# Replace any tags in the path with their values
|
111
|
+
formatted_path = template
|
112
|
+
for tag, value in metadata.items():
|
113
|
+
if value:
|
114
|
+
# Sanitize value for use in path
|
115
|
+
safe_value = re.sub(r'[<>:"|?*]', '_', str(value))
|
116
|
+
# Replace forward slashes with appropriate character, but NOT hyphens
|
117
|
+
safe_value = safe_value.replace('/', ' - ')
|
118
|
+
# Remove leading/trailing whitespace and dots
|
119
|
+
safe_value = safe_value.strip('. \t')
|
120
|
+
if not safe_value:
|
121
|
+
safe_value = "unknown"
|
122
|
+
|
123
|
+
placeholder = '{' + tag + '}'
|
124
|
+
formatted_path = formatted_path.replace(placeholder, safe_value)
|
125
|
+
|
126
|
+
# Check if there are any remaining placeholders
|
127
|
+
if re.search(r'{[^}]+}', formatted_path):
|
128
|
+
return None # Some placeholders couldn't be filled
|
129
|
+
|
130
|
+
# Normalize path separators for the OS
|
131
|
+
formatted_path = os.path.normpath(formatted_path)
|
132
|
+
return formatted_path
|
133
|
+
except Exception as e:
|
134
|
+
logger.error(f"Error applying template to path: {e}")
|
135
|
+
return None
|
136
|
+
|
137
|
+
def ensure_directory_exists(file_path):
|
138
|
+
"""Create the directory structure for a given file path if it doesn't exist."""
|
139
|
+
directory = os.path.dirname(file_path)
|
140
|
+
if directory:
|
141
|
+
os.makedirs(directory, exist_ok=True)
|
TonieToolbox/integration.py
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
import platform
|
3
|
+
import os
|
4
|
+
import subprocess
|
2
5
|
from .logger import get_logger
|
3
6
|
|
4
7
|
logger = get_logger(__name__)
|
5
8
|
|
6
9
|
def handle_integration(args):
|
7
|
-
import platform
|
8
10
|
if platform.system() == 'Windows':
|
9
11
|
from .integration_windows import WindowsClassicContextMenuIntegration as ContextMenuIntegration
|
10
12
|
if args.install_integration:
|
@@ -23,24 +25,26 @@ def handle_integration(args):
|
|
23
25
|
else:
|
24
26
|
logger.error("Integration uninstallation failed.")
|
25
27
|
return False
|
28
|
+
#elif platform.system() == 'Darwin':
|
29
|
+
# from .integration_macos import MacOSContextMenuIntegration as ContextMenuIntegration
|
30
|
+
# if args.install_integration:
|
31
|
+
# success = ContextMenuIntegration.install()
|
32
|
+
# if success:
|
33
|
+
# logger.info("Integration installed successfully.")
|
34
|
+
# return True
|
35
|
+
# else:
|
36
|
+
# logger.error("Integration installation failed.")
|
37
|
+
# return False
|
38
|
+
# elif args.uninstall_integration:
|
39
|
+
# success = ContextMenuIntegration.uninstall()
|
40
|
+
# if success:
|
41
|
+
# logger.info("Integration uninstalled successfully.")
|
42
|
+
# return True
|
43
|
+
# else:
|
44
|
+
# logger.error("Integration uninstallation failed.")
|
45
|
+
# return False
|
26
46
|
elif platform.system() == 'Darwin':
|
27
|
-
|
28
|
-
if args.install_integration:
|
29
|
-
success = ContextMenuIntegration.install()
|
30
|
-
if success:
|
31
|
-
logger.info("Integration installed successfully.")
|
32
|
-
return True
|
33
|
-
else:
|
34
|
-
logger.error("Integration installation failed.")
|
35
|
-
return False
|
36
|
-
elif args.uninstall_integration:
|
37
|
-
success = ContextMenuIntegration.uninstall()
|
38
|
-
if success:
|
39
|
-
logger.info("Integration uninstalled successfully.")
|
40
|
-
return True
|
41
|
-
else:
|
42
|
-
logger.error("Integration uninstallation failed.")
|
43
|
-
return False
|
47
|
+
raise NotImplementedError("Context menu integration is not supported on MacOS YET. But Soon™")
|
44
48
|
elif platform.system() == 'Linux':
|
45
49
|
raise NotImplementedError("Context menu integration is not supported on Linux YET. But Soon™")
|
46
50
|
else:
|
@@ -48,11 +52,6 @@ def handle_integration(args):
|
|
48
52
|
|
49
53
|
def handle_config():
|
50
54
|
"""Opens the configuration file in the default text editor."""
|
51
|
-
import os
|
52
|
-
import platform
|
53
|
-
import subprocess
|
54
|
-
|
55
|
-
|
56
55
|
config_path = os.path.join(os.path.expanduser("~"), ".tonietoolbox", "config.json")
|
57
56
|
if not os.path.exists(config_path):
|
58
57
|
logger.info(f"Configuration file not found at {config_path}.")
|
@@ -1,3 +1,8 @@
|
|
1
|
+
#!/usr/bin/python3
|
2
|
+
"""
|
3
|
+
Integration for MacOS Quick Actions (Services) for TonieToolbox.
|
4
|
+
This module provides functionality to create and manage Quick Actions.
|
5
|
+
"""
|
1
6
|
import os
|
2
7
|
import sys
|
3
8
|
import json
|
@@ -8,7 +13,7 @@ from .constants import SUPPORTED_EXTENSIONS, CONFIG_TEMPLATE,UTI_MAPPINGS,ICON_B
|
|
8
13
|
from .artwork import base64_to_ico
|
9
14
|
from .logger import get_logger
|
10
15
|
|
11
|
-
logger = get_logger(
|
16
|
+
logger = get_logger(__name__)
|
12
17
|
|
13
18
|
class MacOSContextMenuIntegration:
|
14
19
|
"""
|
@@ -1,4 +1,8 @@
|
|
1
|
-
|
1
|
+
#!/usr/bin/python3
|
2
|
+
"""
|
3
|
+
Integration for Windows "classic" context menu.
|
4
|
+
This module generates Windows registry entries to add a 'TonieToolbox' cascade menu.
|
5
|
+
"""
|
2
6
|
import os
|
3
7
|
import sys
|
4
8
|
import json
|
@@ -6,7 +10,7 @@ from .constants import SUPPORTED_EXTENSIONS, CONFIG_TEMPLATE, ICON_BASE64
|
|
6
10
|
from .artwork import base64_to_ico
|
7
11
|
from .logger import get_logger
|
8
12
|
|
9
|
-
logger = get_logger(
|
13
|
+
logger = get_logger(__name__)
|
10
14
|
|
11
15
|
class WindowsClassicContextMenuIntegration:
|
12
16
|
"""
|
@@ -119,7 +123,7 @@ class WindowsClassicContextMenuIntegration:
|
|
119
123
|
self.upload_taf_cmd = self._build_cmd(log_level_arg, use_upload=True, log_to_file=self.log_to_file)
|
120
124
|
self.upload_taf_artwork_cmd = self._build_cmd(log_level_arg, use_upload=True, use_artwork=True, log_to_file=self.log_to_file)
|
121
125
|
self.upload_taf_artwork_json_cmd = self._build_cmd(log_level_arg, use_upload=True, use_artwork=True, use_json=True, log_to_file=self.log_to_file)
|
122
|
-
self.compare_taf_cmd = self._build_cmd(log_level_arg, use_compare=True, keep_open=True, log_to_file=self.log_to_file)
|
126
|
+
#self.compare_taf_cmd = self._build_cmd(log_level_arg, use_compare=True, keep_open=True, log_to_file=self.log_to_file)
|
123
127
|
|
124
128
|
# Folder commands
|
125
129
|
self.convert_folder_cmd = self._build_cmd(f'{log_level_arg}', is_recursive=True, is_folder=True, log_to_file=self.log_to_file)
|
TonieToolbox/logger.py
CHANGED
TonieToolbox/media_tags.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
"""
|
2
3
|
Media tag processing functionality for the TonieToolbox package
|
3
4
|
|
@@ -14,6 +15,7 @@ from mutagen.flac import Picture
|
|
14
15
|
from .logger import get_logger
|
15
16
|
from .dependency_manager import is_mutagen_available, ensure_mutagen
|
16
17
|
from .constants import ARTWORK_NAMES, ARTWORK_EXTENSIONS, TAG_VALUE_REPLACEMENTS, TAG_MAPPINGS
|
18
|
+
logger = get_logger(__name__)
|
17
19
|
|
18
20
|
MUTAGEN_AVAILABLE = False
|
19
21
|
mutagen = None
|
@@ -56,9 +58,6 @@ def _import_mutagen():
|
|
56
58
|
if is_mutagen_available():
|
57
59
|
_import_mutagen()
|
58
60
|
|
59
|
-
logger = get_logger('media_tags')
|
60
|
-
|
61
|
-
|
62
61
|
def normalize_tag_value(value: str) -> str:
|
63
62
|
"""
|
64
63
|
Normalize tag values by replacing special characters or known patterns
|
TonieToolbox/ogg_page.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
"""
|
2
3
|
Classes and functions for handling OGG container pages
|
3
4
|
"""
|
@@ -14,8 +15,7 @@ from .constants import (
|
|
14
15
|
)
|
15
16
|
from .logger import get_logger
|
16
17
|
|
17
|
-
|
18
|
-
logger = get_logger('ogg_page')
|
18
|
+
logger = get_logger(__name__)
|
19
19
|
|
20
20
|
|
21
21
|
def create_crc_table() -> list[int]:
|
TonieToolbox/opus_packet.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
"""
|
2
3
|
Classes and functions for handling Opus audio packets
|
3
4
|
"""
|
@@ -6,8 +7,7 @@ import struct
|
|
6
7
|
from .constants import SAMPLE_RATE_KHZ
|
7
8
|
from .logger import get_logger
|
8
9
|
|
9
|
-
|
10
|
-
logger = get_logger('opus_packet')
|
10
|
+
logger = get_logger(__name__)
|
11
11
|
|
12
12
|
|
13
13
|
class OpusPacket:
|
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
"""
|
2
3
|
Recursive folder processing functionality for the TonieToolbox package
|
3
4
|
"""
|
@@ -11,7 +12,7 @@ import re
|
|
11
12
|
from .audio_conversion import filter_directories
|
12
13
|
from .logger import get_logger
|
13
14
|
|
14
|
-
logger = get_logger(
|
15
|
+
logger = get_logger(__name__)
|
15
16
|
|
16
17
|
|
17
18
|
def find_audio_folders(root_path: str) -> list[dict[str, any]]:
|
TonieToolbox/tags.py
CHANGED
TonieToolbox/teddycloud.py
CHANGED
@@ -9,8 +9,9 @@ import base64
|
|
9
9
|
import ssl
|
10
10
|
import socket
|
11
11
|
import requests
|
12
|
+
import json
|
12
13
|
from .logger import get_logger
|
13
|
-
logger = get_logger(
|
14
|
+
logger = get_logger(__name__)
|
14
15
|
DEFAULT_CONNECTION_TIMEOUT = 10
|
15
16
|
DEFAULT_READ_TIMEOUT = 15 # seconds
|
16
17
|
DEFAULT_MAX_RETRIES = 3
|
@@ -334,3 +335,115 @@ class TeddyCloudClient:
|
|
334
335
|
}
|
335
336
|
|
336
337
|
# ------------- Custom API Methods -------------
|
338
|
+
|
339
|
+
def _get_paths_cache_file(self) -> str:
|
340
|
+
"""
|
341
|
+
Get the path to the paths cache file.
|
342
|
+
|
343
|
+
Returns:
|
344
|
+
str: Path to the paths cache file
|
345
|
+
"""
|
346
|
+
cache_dir = os.path.join(os.path.expanduser("~"), ".tonietoolbox")
|
347
|
+
os.makedirs(cache_dir, exist_ok=True)
|
348
|
+
return os.path.join(cache_dir, "paths.json")
|
349
|
+
|
350
|
+
def _load_paths_cache(self) -> set:
|
351
|
+
"""
|
352
|
+
Load the paths cache from the cache file.
|
353
|
+
|
354
|
+
Returns:
|
355
|
+
set: Set of existing directory paths
|
356
|
+
"""
|
357
|
+
cache_file = self._get_paths_cache_file()
|
358
|
+
try:
|
359
|
+
if os.path.exists(cache_file):
|
360
|
+
with open(cache_file, 'r', encoding='utf-8') as f:
|
361
|
+
paths_data = json.load(f)
|
362
|
+
# Convert to set for faster lookups
|
363
|
+
return set(paths_data.get('paths', []))
|
364
|
+
return set()
|
365
|
+
except Exception as e:
|
366
|
+
logger.warning(f"Failed to load paths cache: {e}")
|
367
|
+
return set()
|
368
|
+
|
369
|
+
def _save_paths_cache(self, paths: set) -> None:
|
370
|
+
"""
|
371
|
+
Save the paths cache to the cache file.
|
372
|
+
|
373
|
+
Args:
|
374
|
+
paths (set): Set of directory paths to save
|
375
|
+
"""
|
376
|
+
cache_file = self._get_paths_cache_file()
|
377
|
+
try:
|
378
|
+
paths_data = {'paths': list(paths)}
|
379
|
+
with open(cache_file, 'w', encoding='utf-8') as f:
|
380
|
+
json.dump(paths_data, f, indent=2)
|
381
|
+
logger.debug(f"Saved {len(paths)} paths to cache file")
|
382
|
+
except Exception as e:
|
383
|
+
logger.warning(f"Failed to save paths cache: {e}")
|
384
|
+
|
385
|
+
def create_directories_recursive(self, path: str, overlay: str = None, special: str = "library") -> str:
|
386
|
+
"""
|
387
|
+
Create directories recursively on the TeddyCloud server.
|
388
|
+
|
389
|
+
This function handles both cases:
|
390
|
+
- Directories that already exist (prevents 500 errors)
|
391
|
+
- Parent directories that don't exist yet (creates them first)
|
392
|
+
|
393
|
+
This optimized version uses a local paths cache instead of querying the file index,
|
394
|
+
since the file index might not represent the correct folders.
|
395
|
+
|
396
|
+
Args:
|
397
|
+
path (str): Directory path to create (can contain multiple levels)
|
398
|
+
overlay (str | None): Settings overlay ID (optional)
|
399
|
+
special (str | None): Special folder source, only 'library' supported yet (optional)
|
400
|
+
|
401
|
+
Returns:
|
402
|
+
str: Response message from server
|
403
|
+
"""
|
404
|
+
path = path.replace('\\', '/').strip('/')
|
405
|
+
if not path:
|
406
|
+
return "Path is empty"
|
407
|
+
existing_dirs = self._load_paths_cache()
|
408
|
+
logger.debug(f"Loaded {len(existing_dirs)} existing paths from cache")
|
409
|
+
path_components = path.split('/')
|
410
|
+
current_path = ""
|
411
|
+
result = "OK"
|
412
|
+
paths_updated = False
|
413
|
+
for component in path_components:
|
414
|
+
if current_path:
|
415
|
+
current_path += f"/{component}"
|
416
|
+
else:
|
417
|
+
current_path = component
|
418
|
+
if current_path in existing_dirs:
|
419
|
+
logger.debug(f"Directory '{current_path}' exists in paths cache, skipping creation")
|
420
|
+
continue
|
421
|
+
|
422
|
+
try:
|
423
|
+
result = self.create_directory(current_path, overlay, special)
|
424
|
+
logger.debug(f"Created directory: {current_path}")
|
425
|
+
# Add the newly created directory to our cache
|
426
|
+
existing_dirs.add(current_path)
|
427
|
+
paths_updated = True
|
428
|
+
except requests.exceptions.HTTPError as e:
|
429
|
+
# If it's a 500 error, likely the directory already exists
|
430
|
+
if e.response.status_code == 500:
|
431
|
+
if "already exists" in e.response.text.lower():
|
432
|
+
logger.debug(f"Directory '{current_path}' already exists, continuing")
|
433
|
+
# Add to our cache for future operations
|
434
|
+
existing_dirs.add(current_path)
|
435
|
+
paths_updated = True
|
436
|
+
else:
|
437
|
+
# Log the actual error message but continue anyway
|
438
|
+
# This allows us to continue even if the error is something else
|
439
|
+
logger.warning(f"Warning while creating '{current_path}': {str(e)}")
|
440
|
+
else:
|
441
|
+
# Re-raise for other HTTP errors
|
442
|
+
logger.error(f"Failed to create directory '{current_path}': {str(e)}")
|
443
|
+
raise
|
444
|
+
|
445
|
+
# Save updated paths cache if any changes were made
|
446
|
+
if paths_updated:
|
447
|
+
self._save_paths_cache(existing_dirs)
|
448
|
+
|
449
|
+
return result
|
TonieToolbox/tonie_analysis.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
"""
|
2
3
|
Functions for analyzing Tonie files
|
3
4
|
"""
|
@@ -10,7 +11,7 @@ from . import tonie_header_pb2
|
|
10
11
|
from .ogg_page import OggPage
|
11
12
|
from .logger import get_logger
|
12
13
|
|
13
|
-
logger = get_logger(
|
14
|
+
logger = get_logger(__name__)
|
14
15
|
|
15
16
|
def format_time(ts: float) -> str:
|
16
17
|
"""
|
TonieToolbox/tonie_file.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
"""
|
2
3
|
Tonie file operations module
|
3
4
|
"""
|
@@ -15,7 +16,7 @@ from .ogg_page import OggPage
|
|
15
16
|
from .constants import OPUS_TAGS, SAMPLE_RATE_KHZ, TIMESTAMP_DEDUCT
|
16
17
|
from .logger import get_logger
|
17
18
|
|
18
|
-
logger = get_logger(
|
19
|
+
logger = get_logger(__name__)
|
19
20
|
|
20
21
|
|
21
22
|
def toniefile_comment_add(buffer: bytearray, length: int, comment_str: str) -> int:
|
TonieToolbox/tonies_json.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
"""
|
2
3
|
TonieToolbox module for handling the tonies.custom.json operations.
|
3
4
|
|
@@ -19,7 +20,7 @@ from .media_tags import get_file_tags, extract_album_info
|
|
19
20
|
from .constants import LANGUAGE_MAPPING, GENRE_MAPPING
|
20
21
|
from .teddycloud import TeddyCloudClient
|
21
22
|
|
22
|
-
logger = get_logger(
|
23
|
+
logger = get_logger(__name__)
|
23
24
|
|
24
25
|
class ToniesJsonHandlerv1:
|
25
26
|
"""Handler for tonies.custom.json operations using v1 format."""
|
TonieToolbox/version_handler.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
"""
|
2
3
|
Version handler to check if the latest version of TonieToolbox is being used.
|
3
4
|
"""
|
@@ -17,6 +18,7 @@ CACHE_DIR = os.path.join(os.path.expanduser("~"), ".tonietoolbox")
|
|
17
18
|
CACHE_FILE = os.path.join(CACHE_DIR, "version_cache.json")
|
18
19
|
CACHE_EXPIRY = 86400 # 24 hours in seconds
|
19
20
|
|
21
|
+
logger = get_logger(__name__)
|
20
22
|
|
21
23
|
def get_pypi_version(force_refresh: bool = False) -> tuple[str, str | None]:
|
22
24
|
"""
|
@@ -27,7 +29,6 @@ def get_pypi_version(force_refresh: bool = False) -> tuple[str, str | None]:
|
|
27
29
|
Returns:
|
28
30
|
tuple[str, str | None]: (latest_version, None) on success, (current_version, error_message) on failure
|
29
31
|
"""
|
30
|
-
logger = get_logger("version_handler")
|
31
32
|
logger.debug("Checking for latest version (force_refresh=%s)", force_refresh)
|
32
33
|
logger.debug("Current version: %s", __version__)
|
33
34
|
|
@@ -97,7 +98,6 @@ def compare_versions(v1: str, v2: str) -> int:
|
|
97
98
|
Returns:
|
98
99
|
int: -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
|
99
100
|
"""
|
100
|
-
logger = get_logger("version_handler")
|
101
101
|
logger.debug("Comparing versions: '%s' vs '%s'", v1, v2)
|
102
102
|
|
103
103
|
try:
|
@@ -145,7 +145,6 @@ def check_for_updates(quiet: bool = False, force_refresh: bool = False) -> tuple
|
|
145
145
|
message: string message about the update status or error
|
146
146
|
update_confirmed: boolean indicating if the user confirmed the update
|
147
147
|
"""
|
148
|
-
logger = get_logger("version_handler")
|
149
148
|
current_version = __version__
|
150
149
|
update_confirmed = False
|
151
150
|
|
@@ -204,7 +203,6 @@ def install_update() -> bool:
|
|
204
203
|
Returns:
|
205
204
|
bool: True if the update was successfully installed, False otherwise
|
206
205
|
"""
|
207
|
-
logger = get_logger("version_handler")
|
208
206
|
import subprocess
|
209
207
|
import sys
|
210
208
|
|
@@ -242,7 +240,6 @@ def clear_version_cache() -> bool:
|
|
242
240
|
Returns:
|
243
241
|
bool: True if cache was cleared, False otherwise
|
244
242
|
"""
|
245
|
-
logger = get_logger("version_handler")
|
246
243
|
|
247
244
|
try:
|
248
245
|
if os.path.exists(CACHE_FILE):
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: TonieToolbox
|
3
|
-
Version: 0.6.
|
3
|
+
Version: 0.6.0rc1
|
4
4
|
Summary: Convert audio files to Toniebox compatible format (.TAF) and interact with TeddyCloud.
|
5
5
|
Home-page: https://github.com/Quentendo64/TonieToolbox
|
6
6
|
Author: Quentendo64
|
@@ -49,6 +49,14 @@ A Toolkit for converting various audio formats into the Tonie-compatible TAF for
|
|
49
49
|
→ [HOWTO Guide for Beginners](HOWTO.md)
|
50
50
|
→ [Contributing Guidelines](CONTRIBUTING.md)
|
51
51
|
|
52
|
+
## 🎯 New Features (v0.6.0)
|
53
|
+
|
54
|
+
The latest release of TonieToolbox includes exciting new capabilities:
|
55
|
+
|
56
|
+
- **Enhanced Media Tag Support**: Better handling of complex audio libraries with advanced metadata extraction and usage of tags in your upload path (--path) or as output directory with the new argument --output-to-template eg. "C:\Music\\{albumartist}\\{album}"
|
57
|
+
- **Windows Context Menu Integration**: Right-click to convert audio files directly from File Explorer. Use --config-integration to configure the upload functions. If not needed just use --install-integration
|
58
|
+
|
59
|
+
|
52
60
|
## Table of Contents
|
53
61
|
|
54
62
|
- [Overview](#overview)
|
@@ -263,12 +271,17 @@ Output:
|
|
263
271
|
```shell
|
264
272
|
usage: TonieToolbox.py [-h] [-v] [--upload URL] [--include-artwork] [--get-tags URL]
|
265
273
|
[--ignore-ssl-verify] [--special-folder FOLDER] [--path PATH]
|
266
|
-
[--
|
267
|
-
[--
|
268
|
-
[--
|
274
|
+
[--connection-timeout SECONDS] [--read-timeout SECONDS]
|
275
|
+
[--max-retries RETRIES] [--retry-delay SECONDS]
|
276
|
+
[--create-custom-json] [--version-2] [--username USERNAME]
|
277
|
+
[--password PASSWORD] [--client-cert CERT_FILE]
|
278
|
+
[--client-key KEY_FILE] [-t TIMESTAMP] [-f FFMPEG]
|
269
279
|
[-o OPUSENC] [-b BITRATE] [-c] [-a TAG] [-n] [-i] [-s] [-r] [-O]
|
270
|
-
[-
|
271
|
-
[--
|
280
|
+
[-fc] [--no-mono-conversion] [-A] [-k] [-u] [-C FILE2] [-D]
|
281
|
+
[--config-integration] [--install-integration]
|
282
|
+
[--uninstall-integration] [-m] [--name-template TEMPLATE]
|
283
|
+
[--output-to-template PATH_TEMPLATE] [--show-tags]
|
284
|
+
[-S] [-F] [-X] [-d] [-T] [-q] [-Q] [--log-file]
|
272
285
|
SOURCE [TARGET]
|
273
286
|
|
274
287
|
Create Tonie compatible file from Ogg opus file(s).
|
@@ -284,8 +297,7 @@ TeddyCloud Options:
|
|
284
297
|
--ignore-ssl-verify Ignore SSL certificate verification (for self-signed certificates)
|
285
298
|
--special-folder FOLDER
|
286
299
|
Special folder to upload to (currently only "library" is supported)
|
287
|
-
--path PATH Path where to write the file on TeddyCloud server
|
288
|
-
--show-progress Show progress bar during file upload (default: enabled)
|
300
|
+
--path PATH Path where to write the file on TeddyCloud server (supports templates like "/{albumartist}/{album}")
|
289
301
|
--connection-timeout SECONDS
|
290
302
|
Connection timeout in seconds (default: 10)
|
291
303
|
--read-timeout SECONDS
|
@@ -295,12 +307,19 @@ TeddyCloud Options:
|
|
295
307
|
--retry-delay SECONDS
|
296
308
|
Delay between retry attempts in seconds (default: 5)
|
297
309
|
--create-custom-json Fetch and update custom Tonies JSON data
|
310
|
+
--version-2 Use version 2 of the Tonies JSON format (default: version 1)
|
311
|
+
--username USERNAME Username for basic authentication
|
312
|
+
--password PASSWORD Password for basic authentication
|
313
|
+
--client-cert CERT_FILE
|
314
|
+
Path to client certificate file for certificate-based authentication
|
315
|
+
--client-key KEY_FILE
|
316
|
+
Path to client private key file for certificate-based authentication
|
298
317
|
|
299
318
|
optional arguments:
|
300
319
|
-h, --help show this help message and exit
|
301
320
|
-v, --version show program version and exit
|
302
321
|
-t, --timestamp TIMESTAMP
|
303
|
-
set custom timestamp / bitstream serial
|
322
|
+
set custom timestamp / bitstream serial
|
304
323
|
-f, --ffmpeg FFMPEG specify location of ffmpeg
|
305
324
|
-o, --opusenc OPUSENC specify location of opusenc
|
306
325
|
-b, --bitrate BITRATE set encoding bitrate in kbps (default: 96)
|
@@ -313,17 +332,27 @@ optional arguments:
|
|
313
332
|
-r, --recursive Process folders recursively
|
314
333
|
-O, --output-to-source
|
315
334
|
Save output files in the source directory instead of output directory
|
335
|
+
-fc, --force-creation
|
336
|
+
Force creation of Tonie file even if it already exists
|
337
|
+
--no-mono-conversion Do not convert mono audio to stereo (default: convert mono to stereo)
|
316
338
|
-A, --auto-download Automatically download FFmpeg and opusenc if needed
|
317
339
|
-k, --keep-temp Keep temporary opus files in a temp folder for testing
|
318
|
-
-u, --use-legacy-tags Use legacy hardcoded tags instead of dynamic TonieToolbox tags
|
340
|
+
-u, --use-legacy-tags Use legacy hardcoded tags instead of dynamic TonieToolbox tags
|
319
341
|
-C, --compare FILE2 Compare input file with another .taf file for debugging
|
320
342
|
-D, --detailed-compare
|
321
343
|
Show detailed OGG page differences when comparing files
|
322
|
-
--
|
344
|
+
--config-integration Configure context menu integration
|
345
|
+
--install-integration
|
346
|
+
Integrate with the system (e.g., create context menu entries)
|
347
|
+
--uninstall-integration
|
348
|
+
Uninstall context menu integration
|
349
|
+
|
323
350
|
Media Tag Options:
|
324
351
|
-m, --use-media-tags Use media tags from audio files for naming
|
325
352
|
--name-template TEMPLATE
|
326
353
|
Template for naming files using media tags. Example: "{album} - {artist}"
|
354
|
+
--output-to-template PATH_TEMPLATE
|
355
|
+
Template for output path using media tags. Example: "C:\Music\{albumartist}\{album}"
|
327
356
|
--show-tags Show available media tags from input files
|
328
357
|
|
329
358
|
Version Check Options:
|
@@ -544,6 +573,87 @@ tonietoolbox --split my_tonie.taf --debug
|
|
544
573
|
tonietoolbox --recursive "Music/Collection/" --quiet
|
545
574
|
```
|
546
575
|
|
576
|
+
#### Force creation of TAF files
|
577
|
+
|
578
|
+
If a valid TAF file already exists, TonieToolbox will skip recreating it by default. To force creation even if the file exists:
|
579
|
+
|
580
|
+
```shell
|
581
|
+
# Force creation of a TAF file even if it already exists
|
582
|
+
tonietoolbox input.mp3 --force-creation
|
583
|
+
```
|
584
|
+
|
585
|
+
This is useful when you want to update the content or encoding settings of an existing TAF file.
|
586
|
+
|
587
|
+
#### Windows Context Menu Integration
|
588
|
+
|
589
|
+
TonieToolbox can integrate with Windows Explorer, allowing you to right-click on audio files or folders to convert them:
|
590
|
+
|
591
|
+
```shell
|
592
|
+
# Install context menu integration (one-time setup)
|
593
|
+
tonietoolbox --install-integration
|
594
|
+
|
595
|
+
# Configure context menu options
|
596
|
+
tonietoolbox --config-integration
|
597
|
+
|
598
|
+
# Remove context menu integration
|
599
|
+
tonietoolbox --uninstall-integration
|
600
|
+
```
|
601
|
+
|
602
|
+
After installation, you can right-click on any audio file or folder in Windows Explorer and select "Convert to Tonie Format".
|
603
|
+
|
604
|
+
When changing the configuration via `--config-integration`. Apply them to the integration by simply execute `tonietoolbox --install-integration` again.
|
605
|
+
|
606
|
+
#### Log File Generation
|
607
|
+
|
608
|
+
Save detailed logs to a timestamped file for troubleshooting complex operations:
|
609
|
+
|
610
|
+
```shell
|
611
|
+
# Enable log file generation
|
612
|
+
tonietoolbox input.mp3 --log-file
|
613
|
+
|
614
|
+
# Combine with debug logging for maximum detail
|
615
|
+
tonietoolbox --recursive input_directory/ --log-file --debug
|
616
|
+
```
|
617
|
+
|
618
|
+
Log files are saved in the `.tonietoolbox\logs` folder in your user directory.
|
619
|
+
|
620
|
+
#### Enhanced Media Tag Templates
|
621
|
+
|
622
|
+
Create custom directory structures based on media tags:
|
623
|
+
|
624
|
+
```shell
|
625
|
+
# Create output based on a path template
|
626
|
+
tonietoolbox input.mp3 --use-media-tags --output-to-template "C:\Music\{albumartist}\{album}"
|
627
|
+
|
628
|
+
# Use with recursive processing
|
629
|
+
tonietoolbox --recursive "Music/Collection/" --use-media-tags --output-to-template "Organized/{genre}/{year} - {album}"
|
630
|
+
```
|
631
|
+
|
632
|
+
This creates a directory structure based on the audio files' metadata and places the converted TAF files accordingly.
|
633
|
+
|
634
|
+
#### TeddyCloud Authentication Options
|
635
|
+
|
636
|
+
> **Note:** Authentication is based on the Features available when using [TeddyCloudStarter](https://github.com/Quentendo64/TeddyCloudStarter).
|
637
|
+
|
638
|
+
TonieToolbox supports multiple authentication methods for secure TeddyCloud connections:
|
639
|
+
|
640
|
+
```shell
|
641
|
+
# Basic authentication with username and password
|
642
|
+
tonietoolbox input.mp3 --upload https://teddycloud.example.com --username admin --password secret
|
643
|
+
|
644
|
+
# Certificate-based authentication
|
645
|
+
tonietoolbox input.mp3 --upload https://teddycloud.example.com --client-cert certificate.crt --client-key private.key
|
646
|
+
```
|
647
|
+
|
648
|
+
#### Custom JSON Format Versioning
|
649
|
+
|
650
|
+
Choose between different versions of the Tonies JSON format:
|
651
|
+
|
652
|
+
```shell
|
653
|
+
# Use version 2 of the Tonies JSON format (enhanced metadata)
|
654
|
+
tonietoolbox input.mp3 --upload https://teddycloud.example.com --create-custom-json --version-2
|
655
|
+
```
|
656
|
+
|
547
657
|
### Media Tags
|
548
658
|
|
549
659
|
TonieToolbox can read metadata tags from audio files (such as ID3 tags in MP3 files, Vorbis comments in FLAC/OGG files, etc.) and use them to create more meaningful filenames or display information about your audio collection.
|
@@ -700,81 +810,7 @@ tonietoolbox "C:\Music\Classical\Bach" --use-media-tags --name-template "{compos
|
|
700
810
|
The first command shows what tags are available, allowing you to create precise naming templates for classical music collections.
|
701
811
|
|
702
812
|
## Technical Details
|
703
|
-
|
704
|
-
### TAF (Tonie Audio Format) File Structure
|
705
|
-
|
706
|
-
The Tonie Audio Format (TAF) consists of several parts:
|
707
|
-
|
708
|
-
#### 1. Tonie Header (0x1000 bytes)
|
709
|
-
|
710
|
-
Located at the beginning of the file, structured as:
|
711
|
-
|
712
|
-
- A 4-byte big-endian integer specifying the header length
|
713
|
-
- A Protocol Buffer encoded header (defined in `tonie_header.proto`)
|
714
|
-
- Padding to fill the entire 4096 bytes (0x1000)
|
715
|
-
|
716
|
-
The Protocol Buffer structure contains:
|
717
|
-
|
718
|
-
```protobuf
|
719
|
-
message TonieHeader {
|
720
|
-
bytes dataHash = 1; // SHA1 hash of the audio data
|
721
|
-
uint32 dataLength = 2; // Length of the audio data in bytes
|
722
|
-
uint32 timestamp = 3; // Unix timestamp (also used as bitstream serial number)
|
723
|
-
repeated uint32 chapterPages = 4 [packed=true]; // Page numbers for chapter starts
|
724
|
-
bytes padding = 5; // Padding to fill up the header
|
725
|
-
}
|
726
|
-
```
|
727
|
-
|
728
|
-
#### 2. Audio Data
|
729
|
-
|
730
|
-
The audio data consists of:
|
731
|
-
|
732
|
-
- Opus encoded audio in Ogg container format
|
733
|
-
- Every page after the header has a fixed size of 4096 bytes (0x1000)
|
734
|
-
- First page contains the Opus identification header
|
735
|
-
- Second page contains the Opus comments/tags
|
736
|
-
- Remaining pages contain the actual audio data
|
737
|
-
- All pages use the same bitstream serial number (timestamp from header)
|
738
|
-
|
739
|
-
#### 3. Special Requirements
|
740
|
-
|
741
|
-
For optimal compatibility with Tonie boxes:
|
742
|
-
|
743
|
-
- Audio should be stereo (2 channels)
|
744
|
-
- Sample rate must be 48 kHz
|
745
|
-
- Pages must be aligned to 4096 byte boundaries
|
746
|
-
- Bitrate of 96 kbps VBR is recommended
|
747
|
-
|
748
|
-
**Mono audio handling:**
|
749
|
-
|
750
|
-
- By default, TonieToolbox will automatically convert mono audio files to stereo for compatibility.
|
751
|
-
- To disable this behavior (and require your input to already be stereo), use the `--no-mono-conversion` flag.
|
752
|
-
|
753
|
-
### File Analysis
|
754
|
-
|
755
|
-
When using the `--info` flag, TonieToolbox checks and displays detailed information about a .TAF (Tonie Audio File):
|
756
|
-
|
757
|
-
- SHA1 hash validation
|
758
|
-
- Timestamp/bitstream serial consistency
|
759
|
-
- Opus data length verification
|
760
|
-
- Opus header validation (version, channels, sample rate)
|
761
|
-
- Page alignment and size validation
|
762
|
-
- Total runtime
|
763
|
-
- Track listing with durations
|
764
|
-
|
765
|
-
### File Comparison
|
766
|
-
|
767
|
-
When using the `--compare` flag, TonieToolbox provides a detailed comparison of two .TAF files:
|
768
|
-
|
769
|
-
- File size comparison
|
770
|
-
- Header size verification
|
771
|
-
- Timestamp comparison
|
772
|
-
- Data length validation
|
773
|
-
- SHA1 hash verification
|
774
|
-
- Chapter page structure analysis
|
775
|
-
- OGG page-by-page comparison (with `--detailed-compare` flag)
|
776
|
-
|
777
|
-
This is particularly useful for debugging when creating TAF files with different tools or parameters.
|
813
|
+
[Moved to TECHNICAL.md](TECHNICAL.md)
|
778
814
|
|
779
815
|
## Related Projects
|
780
816
|
|
@@ -784,9 +820,6 @@ This project is inspired by and builds upon the work of other Tonie-related open
|
|
784
820
|
- [teddycloud](https://github.com/toniebox-reverse-engineering/teddycloud) - Self-hosted alternative to the Tonie cloud / Boxine cloud for managing custom content
|
785
821
|
- [TeddyCloudStarter](https://github.com/Quentendo64/TeddyCloudStarter) - A Wizard for Docker-based deployment of [teddycloud](https://github.com/toniebox-reverse-engineering/teddycloud)
|
786
822
|
|
787
|
-
## Contributing
|
788
|
-
|
789
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
790
823
|
|
791
824
|
## Legal Notice
|
792
825
|
|
@@ -0,0 +1,30 @@
|
|
1
|
+
TonieToolbox/__init__.py,sha256=HhwGRpXWdTEn172esWH5wvevmLmbNCNZhYw3mb7VH2Q,118
|
2
|
+
TonieToolbox/__main__.py,sha256=KSDasW-QaTzStXSKtemMKxoFV8IWvCmq1Oig5yo8BQs,41573
|
3
|
+
TonieToolbox/artwork.py,sha256=BhAjLWqzui8zUOU9z9P4H3zbCCTOB94aT0MPlqEvZoY,5605
|
4
|
+
TonieToolbox/audio_conversion.py,sha256=xXXsRGPS_yQ_mqtFJiqupLbeXN8__Q2Hg05C94EPC_8,17199
|
5
|
+
TonieToolbox/constants.py,sha256=nTKDBy55PTv6Tazda3rsZZcckcm3AZOyWXa1LDuTuEk,33989
|
6
|
+
TonieToolbox/dependency_manager.py,sha256=sQv0vNE7Gp_RqdR9GOwF8jJgu8GTw0Krppg5MjG5qJU,49432
|
7
|
+
TonieToolbox/filename_generator.py,sha256=NJrRxv0sm_9G5ZDp1QG39mktgJnz5yGC8HUs7NhLYFg,5293
|
8
|
+
TonieToolbox/integration.py,sha256=jZuv17i5qRBpDiy_jKDpDO0YxFBYGmJJgLzGpqfpPfs,3226
|
9
|
+
TonieToolbox/integration_macos.py,sha256=YxK7gIqiy1QTp7EM1M1OPC-mgb3gaIoI6Xfu3eUAyX0,27311
|
10
|
+
TonieToolbox/integration_ubuntu.py,sha256=_Da2s7CmuP7kts0W-yyt4odZA8jr14d8iisVF_ipLDA,52
|
11
|
+
TonieToolbox/integration_windows.py,sha256=Ptzow2cuva0PkvaIlTEvcpk8_NzAbL7I80BQlfx5SZk,24012
|
12
|
+
TonieToolbox/logger.py,sha256=ppAnOJAKNwwbt5q3NdF2WHgHemZjP0QWRaUx8UrkQK8,3139
|
13
|
+
TonieToolbox/media_tags.py,sha256=pOLcqnY4sUudI-gBre0A7le3nA7ZDkhUNkozUdRVHiE,20843
|
14
|
+
TonieToolbox/ogg_page.py,sha256=CpL3mHoRsfHBBrT2r4x5p3LBPgHfYQxb_WQsCZq6SI0,22341
|
15
|
+
TonieToolbox/opus_packet.py,sha256=skKzr5sVpL7L77JisqqGL4Kz86acOGDzwbFRqRL2oSM,8017
|
16
|
+
TonieToolbox/recursive_processor.py,sha256=IcTmQY6OF01KUfqjYE7F0CFl1SeWcSLzkB_Du6R8deI,13910
|
17
|
+
TonieToolbox/tags.py,sha256=usNcZxMEAMP7vEfeNOtg70YBJpHiWuHb_XKCyEafsYc,2702
|
18
|
+
TonieToolbox/teddycloud.py,sha256=1fpRzhJrOiUXTBhX9JNL9rlO5obIl32WMoYz5RWz0zA,18486
|
19
|
+
TonieToolbox/tonie_analysis.py,sha256=KkaZ6vt4sntMTfutyQApI8gvCirMjTW8aeIBSbhtllA,30798
|
20
|
+
TonieToolbox/tonie_file.py,sha256=z7yRmK0ujZ6SjF78-xJImGmQ2bS-Q7b-6d2ryYJnmwA,21769
|
21
|
+
TonieToolbox/tonie_header.proto,sha256=WaWfwO4VrwGtscK2ujfDRKtpeBpaVPoZhI8iMmR-C0U,202
|
22
|
+
TonieToolbox/tonie_header_pb2.py,sha256=s5bp4ULTEekgq6T61z9fDkRavyPM-3eREs20f_Pxxe8,3665
|
23
|
+
TonieToolbox/tonies_json.py,sha256=Icu5ClFbRK0gFuefJkHQNRjeOW79tQV-GZ3hoIATo3M,60768
|
24
|
+
TonieToolbox/version_handler.py,sha256=Q-i5_0r5lX3dlwDLI2lMAx4X10UZGN2rvMcntmyuiT4,9862
|
25
|
+
tonietoolbox-0.6.0rc1.dist-info/licenses/LICENSE.md,sha256=rGoga9ZAgNco9fBapVFpWf6ri7HOBp1KRnt1uIruXMk,35190
|
26
|
+
tonietoolbox-0.6.0rc1.dist-info/METADATA,sha256=XDVnTt3hL1wFoxSAopsTj9wJweDg6tqRIStAKBb7Fmw,29087
|
27
|
+
tonietoolbox-0.6.0rc1.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
28
|
+
tonietoolbox-0.6.0rc1.dist-info/entry_points.txt,sha256=oqpeyBxel7aScg35Xr4gZKnf486S5KW9okqeBwyJxxc,60
|
29
|
+
tonietoolbox-0.6.0rc1.dist-info/top_level.txt,sha256=Wkkm-2p7I3ENfS7ZbYtYUB2g-xwHrXVlERHfonsOPuE,13
|
30
|
+
tonietoolbox-0.6.0rc1.dist-info/RECORD,,
|
@@ -1,30 +0,0 @@
|
|
1
|
-
TonieToolbox/__init__.py,sha256=P-i-pmjFHddy0r76UYtLh4dFA2jppC0wc_BqhDAzfqw,98
|
2
|
-
TonieToolbox/__main__.py,sha256=F_FHZC7rLejBdEQ4plOg6dcyV4lgFKZDvJtQHpCthKs,34934
|
3
|
-
TonieToolbox/artwork.py,sha256=VRDa9Ydeq7AsXhRKYyEqpfFNH64AA1Xek7yo60bn8OI,5606
|
4
|
-
TonieToolbox/audio_conversion.py,sha256=0GpC6mSRYikIjf_A1w26LAnYtCP2gpHLEKozOISapnM,17190
|
5
|
-
TonieToolbox/constants.py,sha256=DabAgzKeG8trpV9I6tie1FA6mt3BhTRf_gF5UwtMpE4,33970
|
6
|
-
TonieToolbox/dependency_manager.py,sha256=7HCyUhVLZ6l0W1HPdYr3AgLc935S43qdTLpf1PipPkQ,49425
|
7
|
-
TonieToolbox/filename_generator.py,sha256=ATCG4w8uN1vyAqvmdhOtpJLlb9QFKCnYIdBViYqpHjw,3464
|
8
|
-
TonieToolbox/integration.py,sha256=TXCkXxmeMGtw8G8M-jUVuKa7i_hI0JvVoVlIvYFvEnc,3097
|
9
|
-
TonieToolbox/integration_macos.py,sha256=HWHBcMU5BRe50ak2nqB9kIFJS83sJ8t2_B_0FMhat2U,27159
|
10
|
-
TonieToolbox/integration_ubuntu.py,sha256=MU6W0xRCdoHBxrIiOIHePqYTF5Wvn4JxBnDQUPf6fgg,33
|
11
|
-
TonieToolbox/integration_windows.py,sha256=7F_8Dh7fWHuvCfcigM5TB-bF5CupzM6iQ-YqOvvNVFQ,23939
|
12
|
-
TonieToolbox/logger.py,sha256=Q_cXbCWfzNmt5q6fvVzeM8IugkD24CSZAVjuf16n6b4,3120
|
13
|
-
TonieToolbox/media_tags.py,sha256=oDlLe0AyvmIdQlqPzH74AUCqwbZZ-49AQKAJdrW26XE,20830
|
14
|
-
TonieToolbox/ogg_page.py,sha256=IHdP0er0TYjyLfON8zes11FdQtRab3QNxeK6sxnAX08,22340
|
15
|
-
TonieToolbox/opus_packet.py,sha256=yz5jvViGZ-nGZjEaQ1gCKd-j1jPW402szYirbEl4izA,8019
|
16
|
-
TonieToolbox/recursive_processor.py,sha256=6JD4b5sGnCe45GkJqWua9u9ZHv-RC77BRV58qQ8pA3Q,13904
|
17
|
-
TonieToolbox/tags.py,sha256=7BowNWmbJDymvJ0hPVAAXwJRAfPklLokQQuV8FVldSI,2700
|
18
|
-
TonieToolbox/teddycloud.py,sha256=5PSV5Qp0VRfDG78kGPbKQ7bXxxB1sfmGNHYhvIumVF8,13794
|
19
|
-
TonieToolbox/tonie_analysis.py,sha256=KYDfWi9EmA5EvVyq_h_gcxrZ5Eaa5etYsF152GRgyqI,30787
|
20
|
-
TonieToolbox/tonie_file.py,sha256=YntpgpmtWTe64WWqJmWWo2sXyMUnZ57pp3sKGoY-Yik,21754
|
21
|
-
TonieToolbox/tonie_header.proto,sha256=WaWfwO4VrwGtscK2ujfDRKtpeBpaVPoZhI8iMmR-C0U,202
|
22
|
-
TonieToolbox/tonie_header_pb2.py,sha256=s5bp4ULTEekgq6T61z9fDkRavyPM-3eREs20f_Pxxe8,3665
|
23
|
-
TonieToolbox/tonies_json.py,sha256=YGS2wtaDudxxSy7QuRLWaE5n4bf_AyoSvVLH1Vdh8SE,60754
|
24
|
-
TonieToolbox/version_handler.py,sha256=MLpJ9mSEHkcSoyePnACpfINHTSB1q1_4iEgcT1tGqNU,10028
|
25
|
-
tonietoolbox-0.6.0a4.dist-info/licenses/LICENSE.md,sha256=rGoga9ZAgNco9fBapVFpWf6ri7HOBp1KRnt1uIruXMk,35190
|
26
|
-
tonietoolbox-0.6.0a4.dist-info/METADATA,sha256=1ygRy4_HXQsn3C_TcLydyvkdpTppmYECp4OH7eQ2NTE,27009
|
27
|
-
tonietoolbox-0.6.0a4.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
|
28
|
-
tonietoolbox-0.6.0a4.dist-info/entry_points.txt,sha256=oqpeyBxel7aScg35Xr4gZKnf486S5KW9okqeBwyJxxc,60
|
29
|
-
tonietoolbox-0.6.0a4.dist-info/top_level.txt,sha256=Wkkm-2p7I3ENfS7ZbYtYUB2g-xwHrXVlERHfonsOPuE,13
|
30
|
-
tonietoolbox-0.6.0a4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|