TonieToolbox 0.6.0a4__py3-none-any.whl → 0.6.0a5__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 +3 -6
- TonieToolbox/integration_macos.py +6 -1
- TonieToolbox/integration_ubuntu.py +1 -0
- TonieToolbox/integration_windows.py +6 -2
- 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.0a5.dist-info}/METADATA +1 -1
- tonietoolbox-0.6.0a5.dist-info/RECORD +30 -0
- {tonietoolbox-0.6.0a4.dist-info → tonietoolbox-0.6.0a5.dist-info}/WHEEL +1 -1
- tonietoolbox-0.6.0a4.dist-info/RECORD +0 -30
- {tonietoolbox-0.6.0a4.dist-info → tonietoolbox-0.6.0a5.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.6.0a4.dist-info → tonietoolbox-0.6.0a5.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.6.0a4.dist-info → tonietoolbox-0.6.0a5.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:
|
@@ -48,11 +50,6 @@ def handle_integration(args):
|
|
48
50
|
|
49
51
|
def handle_config():
|
50
52
|
"""Opens the configuration file in the default text editor."""
|
51
|
-
import os
|
52
|
-
import platform
|
53
|
-
import subprocess
|
54
|
-
|
55
|
-
|
56
53
|
config_path = os.path.join(os.path.expanduser("~"), ".tonietoolbox", "config.json")
|
57
54
|
if not os.path.exists(config_path):
|
58
55
|
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
|
"""
|
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):
|
@@ -0,0 +1,30 @@
|
|
1
|
+
TonieToolbox/__init__.py,sha256=0gNvQWCJmnGv7_OCrSTA7qyNe3zUQ546m7TL9_SFtwY,117
|
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=qWRZfowNBIQJPPnGUJ52Xw0SqyfScuzoONPVtZI8yjA,3063
|
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=WBclD1bW5X8cJ-x3j3-YPnSkpWOQmzxQXjj2-gemPN0,24011
|
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.0a5.dist-info/licenses/LICENSE.md,sha256=rGoga9ZAgNco9fBapVFpWf6ri7HOBp1KRnt1uIruXMk,35190
|
26
|
+
tonietoolbox-0.6.0a5.dist-info/METADATA,sha256=Hzna0YOJdh0olBqOV921ZCfa62g8b_iL555_WeFsYdo,27009
|
27
|
+
tonietoolbox-0.6.0a5.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
28
|
+
tonietoolbox-0.6.0a5.dist-info/entry_points.txt,sha256=oqpeyBxel7aScg35Xr4gZKnf486S5KW9okqeBwyJxxc,60
|
29
|
+
tonietoolbox-0.6.0a5.dist-info/top_level.txt,sha256=Wkkm-2p7I3ENfS7ZbYtYUB2g-xwHrXVlERHfonsOPuE,13
|
30
|
+
tonietoolbox-0.6.0a5.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
|