TonieToolbox 0.2.2__py3-none-any.whl → 0.2.3__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 +1 -1
- TonieToolbox/__main__.py +121 -3
- TonieToolbox/dependency_manager.py +90 -0
- TonieToolbox/media_tags.py +471 -0
- TonieToolbox/recursive_processor.py +96 -11
- {tonietoolbox-0.2.2.dist-info → tonietoolbox-0.2.3.dist-info}/METADATA +60 -4
- {tonietoolbox-0.2.2.dist-info → tonietoolbox-0.2.3.dist-info}/RECORD +11 -10
- {tonietoolbox-0.2.2.dist-info → tonietoolbox-0.2.3.dist-info}/WHEEL +1 -1
- {tonietoolbox-0.2.2.dist-info → tonietoolbox-0.2.3.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.2.2.dist-info → tonietoolbox-0.2.3.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.2.2.dist-info → tonietoolbox-0.2.3.dist-info}/top_level.txt +0 -0
TonieToolbox/__init__.py
CHANGED
TonieToolbox/__main__.py
CHANGED
@@ -17,6 +17,7 @@ from .logger import setup_logging, get_logger
|
|
17
17
|
from .filename_generator import guess_output_filename
|
18
18
|
from .version_handler import check_for_updates, clear_version_cache
|
19
19
|
from .recursive_processor import process_recursive_folders
|
20
|
+
from .media_tags import is_available as is_media_tags_available, ensure_mutagen
|
20
21
|
|
21
22
|
def main():
|
22
23
|
"""Entry point for the TonieToolbox application."""
|
@@ -52,6 +53,15 @@ def main():
|
|
52
53
|
parser.add_argument('-D', '--detailed-compare', action='store_true',
|
53
54
|
help='Show detailed OGG page differences when comparing files')
|
54
55
|
|
56
|
+
# Media tag options
|
57
|
+
media_tag_group = parser.add_argument_group('Media Tag Options')
|
58
|
+
media_tag_group.add_argument('-m', '--use-media-tags', action='store_true',
|
59
|
+
help='Use media tags from audio files for naming')
|
60
|
+
media_tag_group.add_argument('--name-template', metavar='TEMPLATE', action='store',
|
61
|
+
help='Template for naming files using media tags. Example: "{album} - {artist}"')
|
62
|
+
media_tag_group.add_argument('--show-tags', action='store_true',
|
63
|
+
help='Show available media tags from input files')
|
64
|
+
|
55
65
|
# Version check options
|
56
66
|
version_group = parser.add_argument_group('Version Check Options')
|
57
67
|
version_group.add_argument('-S', '--skip-update-check', action='store_true',
|
@@ -85,14 +95,12 @@ def main():
|
|
85
95
|
logger = get_logger('main')
|
86
96
|
logger.debug("Starting TonieToolbox v%s with log level: %s", __version__, logging.getLevelName(log_level))
|
87
97
|
|
88
|
-
# Handle version cache operations
|
89
98
|
if args.clear_version_cache:
|
90
99
|
if clear_version_cache():
|
91
100
|
logger.info("Version cache cleared successfully")
|
92
101
|
else:
|
93
102
|
logger.info("No version cache to clear or error clearing cache")
|
94
103
|
|
95
|
-
# Check for updates
|
96
104
|
if not args.skip_update_check:
|
97
105
|
logger.debug("Checking for updates (force_refresh=%s)", args.force_refresh_cache)
|
98
106
|
is_latest, latest_version, message, update_confirmed = check_for_updates(
|
@@ -119,10 +127,24 @@ def main():
|
|
119
127
|
sys.exit(1)
|
120
128
|
logger.debug("Using opusenc binary: %s", opus_binary)
|
121
129
|
|
130
|
+
# Check for media tags library and handle --show-tags option
|
131
|
+
if (args.use_media_tags or args.show_tags or args.name_template) and not is_media_tags_available():
|
132
|
+
if not ensure_mutagen(auto_install=args.auto_download):
|
133
|
+
logger.warning("Media tags functionality requires the mutagen library but it could not be installed.")
|
134
|
+
if args.use_media_tags or args.show_tags:
|
135
|
+
logger.error("Cannot proceed with --use-media-tags or --show-tags without mutagen library")
|
136
|
+
sys.exit(1)
|
137
|
+
else:
|
138
|
+
logger.info("Successfully enabled media tag support")
|
139
|
+
|
122
140
|
# Handle recursive processing
|
123
141
|
if args.recursive:
|
124
142
|
logger.info("Processing folders recursively: %s", args.input_filename)
|
125
|
-
process_tasks = process_recursive_folders(
|
143
|
+
process_tasks = process_recursive_folders(
|
144
|
+
args.input_filename,
|
145
|
+
use_media_tags=args.use_media_tags,
|
146
|
+
name_template=args.name_template
|
147
|
+
)
|
126
148
|
|
127
149
|
if not process_tasks:
|
128
150
|
logger.error("No folders with audio files found for recursive processing")
|
@@ -178,8 +200,104 @@ def main():
|
|
178
200
|
logger.error("No files found for pattern %s", args.input_filename)
|
179
201
|
sys.exit(1)
|
180
202
|
|
203
|
+
# Show tags for input files if requested
|
204
|
+
if args.show_tags:
|
205
|
+
from .media_tags import get_file_tags
|
206
|
+
logger.info("Showing media tags for input files:")
|
207
|
+
|
208
|
+
for file_index, file_path in enumerate(files):
|
209
|
+
tags = get_file_tags(file_path)
|
210
|
+
if tags:
|
211
|
+
print(f"\nFile {file_index + 1}: {os.path.basename(file_path)}")
|
212
|
+
print("-" * 40)
|
213
|
+
for tag_name, tag_value in sorted(tags.items()):
|
214
|
+
print(f"{tag_name}: {tag_value}")
|
215
|
+
else:
|
216
|
+
print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
|
217
|
+
|
218
|
+
sys.exit(0)
|
219
|
+
|
220
|
+
# Use media tags for file naming if requested
|
221
|
+
guessed_name = None
|
222
|
+
if args.use_media_tags:
|
223
|
+
# If this is a single folder, try to get consistent album info
|
224
|
+
if len(files) > 1 and os.path.dirname(files[0]) == os.path.dirname(files[-1]):
|
225
|
+
folder_path = os.path.dirname(files[0])
|
226
|
+
|
227
|
+
from .media_tags import extract_album_info, format_metadata_filename
|
228
|
+
logger.debug("Extracting album info from folder: %s", folder_path)
|
229
|
+
|
230
|
+
album_info = extract_album_info(folder_path)
|
231
|
+
if album_info:
|
232
|
+
# Use album info for naming the output file
|
233
|
+
template = args.name_template or "{album} - {artist}"
|
234
|
+
new_name = format_metadata_filename(album_info, template)
|
235
|
+
|
236
|
+
if new_name:
|
237
|
+
logger.info("Using album metadata for output filename: %s", new_name)
|
238
|
+
guessed_name = new_name
|
239
|
+
else:
|
240
|
+
logger.debug("Could not format filename from album metadata")
|
241
|
+
|
242
|
+
# For single files, use the file's metadata
|
243
|
+
elif len(files) == 1:
|
244
|
+
from .media_tags import get_file_tags, format_metadata_filename
|
245
|
+
|
246
|
+
tags = get_file_tags(files[0])
|
247
|
+
if tags:
|
248
|
+
template = args.name_template or "{title} - {artist}"
|
249
|
+
new_name = format_metadata_filename(tags, template)
|
250
|
+
|
251
|
+
if new_name:
|
252
|
+
logger.info("Using file metadata for output filename: %s", new_name)
|
253
|
+
guessed_name = new_name
|
254
|
+
else:
|
255
|
+
logger.debug("Could not format filename from file metadata")
|
256
|
+
|
257
|
+
# For multiple files from different folders, try to use common tags if they exist
|
258
|
+
elif len(files) > 1:
|
259
|
+
from .media_tags import get_file_tags, format_metadata_filename
|
260
|
+
|
261
|
+
# Try to find common tags among files
|
262
|
+
common_tags = {}
|
263
|
+
for file_path in files:
|
264
|
+
tags = get_file_tags(file_path)
|
265
|
+
if tags:
|
266
|
+
for key, value in tags.items():
|
267
|
+
if key in ['album', 'albumartist', 'artist']:
|
268
|
+
if key not in common_tags:
|
269
|
+
common_tags[key] = value
|
270
|
+
# Only keep values that are the same across files
|
271
|
+
elif common_tags[key] != value:
|
272
|
+
common_tags[key] = None
|
273
|
+
|
274
|
+
# Remove None values
|
275
|
+
common_tags = {k: v for k, v in common_tags.items() if v is not None}
|
276
|
+
|
277
|
+
if common_tags:
|
278
|
+
template = args.name_template or "Collection - {album}" if 'album' in common_tags else "Collection"
|
279
|
+
new_name = format_metadata_filename(common_tags, template)
|
280
|
+
|
281
|
+
if new_name:
|
282
|
+
logger.info("Using common metadata for output filename: %s", new_name)
|
283
|
+
guessed_name = new_name
|
284
|
+
else:
|
285
|
+
logger.debug("Could not format filename from common metadata")
|
286
|
+
|
181
287
|
if args.output_filename:
|
182
288
|
out_filename = args.output_filename
|
289
|
+
elif guessed_name:
|
290
|
+
if args.output_to_source:
|
291
|
+
source_dir = os.path.dirname(files[0]) if files else '.'
|
292
|
+
out_filename = os.path.join(source_dir, guessed_name)
|
293
|
+
logger.debug("Using source location for output with media tags: %s", out_filename)
|
294
|
+
else:
|
295
|
+
output_dir = './output'
|
296
|
+
if not os.path.exists(output_dir):
|
297
|
+
logger.debug("Creating default output directory: %s", output_dir)
|
298
|
+
os.makedirs(output_dir, exist_ok=True)
|
299
|
+
out_filename = os.path.join(output_dir, guessed_name)
|
300
|
+
logger.debug("Using default output location with media tags: %s", out_filename)
|
183
301
|
else:
|
184
302
|
guessed_name = guess_output_filename(args.input_filename, files)
|
185
303
|
if args.output_to_source:
|
@@ -52,6 +52,10 @@ DEPENDENCIES = {
|
|
52
52
|
'darwin': {
|
53
53
|
'package': 'opus-tools'
|
54
54
|
}
|
55
|
+
},
|
56
|
+
'mutagen': {
|
57
|
+
'package': 'mutagen',
|
58
|
+
'python_package': True
|
55
59
|
}
|
56
60
|
}
|
57
61
|
|
@@ -365,6 +369,92 @@ def install_package(package_name):
|
|
365
369
|
logger.error("Failed to install %s: %s", package_name, e)
|
366
370
|
return False
|
367
371
|
|
372
|
+
def install_python_package(package_name):
|
373
|
+
"""
|
374
|
+
Attempt to install a Python package using pip.
|
375
|
+
|
376
|
+
Args:
|
377
|
+
package_name (str): Name of the package to install
|
378
|
+
|
379
|
+
Returns:
|
380
|
+
bool: True if installation was successful, False otherwise
|
381
|
+
"""
|
382
|
+
logger.info("Attempting to install Python package: %s", package_name)
|
383
|
+
try:
|
384
|
+
import subprocess
|
385
|
+
import sys
|
386
|
+
|
387
|
+
# Try to install the package using pip
|
388
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
|
389
|
+
logger.info("Successfully installed Python package: %s", package_name)
|
390
|
+
return True
|
391
|
+
except Exception as e:
|
392
|
+
logger.error("Failed to install Python package %s: %s", package_name, str(e))
|
393
|
+
return False
|
394
|
+
|
395
|
+
def check_python_package(package_name):
|
396
|
+
"""
|
397
|
+
Check if a Python package is installed.
|
398
|
+
|
399
|
+
Args:
|
400
|
+
package_name (str): Name of the package to check
|
401
|
+
|
402
|
+
Returns:
|
403
|
+
bool: True if the package is installed, False otherwise
|
404
|
+
"""
|
405
|
+
logger.debug("Checking if Python package is installed: %s", package_name)
|
406
|
+
try:
|
407
|
+
__import__(package_name)
|
408
|
+
logger.debug("Python package %s is installed", package_name)
|
409
|
+
return True
|
410
|
+
except ImportError:
|
411
|
+
logger.debug("Python package %s is not installed", package_name)
|
412
|
+
return False
|
413
|
+
|
414
|
+
def ensure_mutagen(auto_install=True):
|
415
|
+
"""
|
416
|
+
Ensure that the Mutagen library is available, installing it if necessary and allowed.
|
417
|
+
|
418
|
+
Args:
|
419
|
+
auto_install (bool): Whether to automatically install Mutagen if not found (defaults to True)
|
420
|
+
|
421
|
+
Returns:
|
422
|
+
bool: True if Mutagen is available, False otherwise
|
423
|
+
"""
|
424
|
+
logger.debug("Checking if Mutagen is available")
|
425
|
+
|
426
|
+
try:
|
427
|
+
import mutagen
|
428
|
+
logger.debug("Mutagen is already installed")
|
429
|
+
return True
|
430
|
+
except ImportError:
|
431
|
+
logger.debug("Mutagen is not installed")
|
432
|
+
|
433
|
+
if auto_install:
|
434
|
+
logger.info("Auto-install enabled, attempting to install Mutagen")
|
435
|
+
if install_python_package('mutagen'):
|
436
|
+
try:
|
437
|
+
import mutagen
|
438
|
+
logger.info("Successfully installed and imported Mutagen")
|
439
|
+
return True
|
440
|
+
except ImportError:
|
441
|
+
logger.error("Mutagen was installed but could not be imported")
|
442
|
+
else:
|
443
|
+
logger.error("Failed to install Mutagen")
|
444
|
+
else:
|
445
|
+
logger.warning("Mutagen is not installed and --auto-download is not used.")
|
446
|
+
|
447
|
+
return False
|
448
|
+
|
449
|
+
def is_mutagen_available():
|
450
|
+
"""
|
451
|
+
Check if the Mutagen library is available.
|
452
|
+
|
453
|
+
Returns:
|
454
|
+
bool: True if Mutagen is available, False otherwise
|
455
|
+
"""
|
456
|
+
return check_python_package('mutagen')
|
457
|
+
|
368
458
|
def ensure_dependency(dependency_name, auto_download=False):
|
369
459
|
"""
|
370
460
|
Ensure that a dependency is available, downloading it if necessary.
|
@@ -0,0 +1,471 @@
|
|
1
|
+
"""
|
2
|
+
Media tag processing functionality for the TonieToolbox package
|
3
|
+
|
4
|
+
This module handles reading and processing metadata tags from audio files,
|
5
|
+
which can be used to enhance Tonie file creation with proper track information.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
from typing import Dict, Any, Optional, List
|
10
|
+
import logging
|
11
|
+
from .logger import get_logger
|
12
|
+
from .dependency_manager import is_mutagen_available, ensure_mutagen
|
13
|
+
|
14
|
+
# Global variables to track dependency state and store module references
|
15
|
+
MUTAGEN_AVAILABLE = False
|
16
|
+
mutagen = None
|
17
|
+
ID3 = None
|
18
|
+
FLAC = None
|
19
|
+
MP4 = None
|
20
|
+
OggOpus = None
|
21
|
+
OggVorbis = None
|
22
|
+
|
23
|
+
def _import_mutagen():
|
24
|
+
"""
|
25
|
+
Import the mutagen modules and update global variables.
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
bool: True if import was successful, False otherwise
|
29
|
+
"""
|
30
|
+
global MUTAGEN_AVAILABLE, mutagen, ID3, FLAC, MP4, OggOpus, OggVorbis
|
31
|
+
|
32
|
+
try:
|
33
|
+
import mutagen as _mutagen
|
34
|
+
from mutagen.id3 import ID3 as _ID3
|
35
|
+
from mutagen.flac import FLAC as _FLAC
|
36
|
+
from mutagen.mp4 import MP4 as _MP4
|
37
|
+
from mutagen.oggopus import OggOpus as _OggOpus
|
38
|
+
from mutagen.oggvorbis import OggVorbis as _OggVorbis
|
39
|
+
|
40
|
+
# Assign to global variables
|
41
|
+
mutagen = _mutagen
|
42
|
+
ID3 = _ID3
|
43
|
+
FLAC = _FLAC
|
44
|
+
MP4 = _MP4
|
45
|
+
OggOpus = _OggOpus
|
46
|
+
OggVorbis = _OggVorbis
|
47
|
+
MUTAGEN_AVAILABLE = True
|
48
|
+
return True
|
49
|
+
except ImportError:
|
50
|
+
MUTAGEN_AVAILABLE = False
|
51
|
+
return False
|
52
|
+
|
53
|
+
# Try to import mutagen if it's available
|
54
|
+
if is_mutagen_available():
|
55
|
+
_import_mutagen()
|
56
|
+
|
57
|
+
logger = get_logger('media_tags')
|
58
|
+
|
59
|
+
# Define tag mapping for different formats to standardized names
|
60
|
+
# This helps normalize tags across different audio formats
|
61
|
+
TAG_MAPPING = {
|
62
|
+
# ID3 (MP3) tags
|
63
|
+
'TIT2': 'title',
|
64
|
+
'TALB': 'album',
|
65
|
+
'TPE1': 'artist',
|
66
|
+
'TPE2': 'albumartist',
|
67
|
+
'TCOM': 'composer',
|
68
|
+
'TRCK': 'tracknumber',
|
69
|
+
'TPOS': 'discnumber',
|
70
|
+
'TDRC': 'date',
|
71
|
+
'TCON': 'genre',
|
72
|
+
'TPUB': 'publisher',
|
73
|
+
'TCOP': 'copyright',
|
74
|
+
'COMM': 'comment',
|
75
|
+
|
76
|
+
# Vorbis tags (FLAC, OGG)
|
77
|
+
'title': 'title',
|
78
|
+
'album': 'album',
|
79
|
+
'artist': 'artist',
|
80
|
+
'albumartist': 'albumartist',
|
81
|
+
'composer': 'composer',
|
82
|
+
'tracknumber': 'tracknumber',
|
83
|
+
'discnumber': 'discnumber',
|
84
|
+
'date': 'date',
|
85
|
+
'genre': 'genre',
|
86
|
+
'publisher': 'publisher',
|
87
|
+
'copyright': 'copyright',
|
88
|
+
'comment': 'comment',
|
89
|
+
|
90
|
+
# MP4 (M4A, AAC) tags
|
91
|
+
'©nam': 'title',
|
92
|
+
'©alb': 'album',
|
93
|
+
'©ART': 'artist',
|
94
|
+
'aART': 'albumartist',
|
95
|
+
'©wrt': 'composer',
|
96
|
+
'trkn': 'tracknumber',
|
97
|
+
'disk': 'discnumber',
|
98
|
+
'©day': 'date',
|
99
|
+
'©gen': 'genre',
|
100
|
+
'©pub': 'publisher',
|
101
|
+
'cprt': 'copyright',
|
102
|
+
'©cmt': 'comment',
|
103
|
+
|
104
|
+
# Additional tags some files might have
|
105
|
+
'album_artist': 'albumartist',
|
106
|
+
'track': 'tracknumber',
|
107
|
+
'track_number': 'tracknumber',
|
108
|
+
'disc': 'discnumber',
|
109
|
+
'disc_number': 'discnumber',
|
110
|
+
'year': 'date',
|
111
|
+
'albuminterpret': 'albumartist', # German tag name
|
112
|
+
'interpret': 'artist', # German tag name
|
113
|
+
}
|
114
|
+
|
115
|
+
# Define replacements for special tag values
|
116
|
+
TAG_VALUE_REPLACEMENTS = {
|
117
|
+
"Die drei ???": "Die drei Fragezeichen",
|
118
|
+
"Die Drei ???": "Die drei Fragezeichen",
|
119
|
+
"DIE DREI ???": "Die drei Fragezeichen",
|
120
|
+
"Die drei !!!": "Die drei Ausrufezeichen",
|
121
|
+
"Die Drei !!!": "Die drei Ausrufezeichen",
|
122
|
+
"DIE DREI !!!": "Die drei Ausrufezeichen",
|
123
|
+
"TKKG™": "TKKG",
|
124
|
+
"Die drei ??? Kids": "Die drei Fragezeichen Kids",
|
125
|
+
"Die Drei ??? Kids": "Die drei Fragezeichen Kids",
|
126
|
+
"Bibi & Tina": "Bibi und Tina",
|
127
|
+
"Benjamin Blümchen™": "Benjamin Blümchen",
|
128
|
+
"???": "Fragezeichen",
|
129
|
+
"!!!": "Ausrufezeichen",
|
130
|
+
}
|
131
|
+
|
132
|
+
def normalize_tag_value(value: str) -> str:
|
133
|
+
"""
|
134
|
+
Normalize tag values by replacing special characters or known patterns
|
135
|
+
with more file-system-friendly alternatives.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
value: The original tag value
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
Normalized tag value
|
142
|
+
"""
|
143
|
+
if not value:
|
144
|
+
return value
|
145
|
+
|
146
|
+
# Check for direct replacements first
|
147
|
+
if value in TAG_VALUE_REPLACEMENTS:
|
148
|
+
logger.debug("Direct tag replacement: '%s' -> '%s'", value, TAG_VALUE_REPLACEMENTS[value])
|
149
|
+
return TAG_VALUE_REPLACEMENTS[value]
|
150
|
+
|
151
|
+
# Check for partial matches and replacements
|
152
|
+
result = value
|
153
|
+
for pattern, replacement in TAG_VALUE_REPLACEMENTS.items():
|
154
|
+
if pattern in result:
|
155
|
+
original = result
|
156
|
+
result = result.replace(pattern, replacement)
|
157
|
+
logger.debug("Partial tag replacement: '%s' -> '%s'", original, result)
|
158
|
+
|
159
|
+
# Special case for "Die drei ???" type patterns that might have been missed
|
160
|
+
result = result.replace("???", "Fragezeichen")
|
161
|
+
|
162
|
+
return result
|
163
|
+
|
164
|
+
def is_available() -> bool:
|
165
|
+
"""
|
166
|
+
Check if tag reading functionality is available.
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
bool: True if mutagen is available, False otherwise
|
170
|
+
"""
|
171
|
+
return MUTAGEN_AVAILABLE or is_mutagen_available()
|
172
|
+
|
173
|
+
def get_file_tags(file_path: str) -> Dict[str, Any]:
|
174
|
+
"""
|
175
|
+
Extract metadata tags from an audio file.
|
176
|
+
|
177
|
+
Args:
|
178
|
+
file_path: Path to the audio file
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
Dictionary containing standardized tag names and values
|
182
|
+
"""
|
183
|
+
global MUTAGEN_AVAILABLE
|
184
|
+
|
185
|
+
if not MUTAGEN_AVAILABLE:
|
186
|
+
# Try to ensure mutagen is available
|
187
|
+
if ensure_mutagen(auto_install=True):
|
188
|
+
# If successful, import the necessary modules
|
189
|
+
if not _import_mutagen():
|
190
|
+
logger.warning("Mutagen library not available. Cannot read media tags.")
|
191
|
+
return {}
|
192
|
+
else:
|
193
|
+
logger.warning("Mutagen library not available. Cannot read media tags.")
|
194
|
+
return {}
|
195
|
+
|
196
|
+
logger.debug("Reading tags from file: %s", file_path)
|
197
|
+
tags = {}
|
198
|
+
|
199
|
+
try:
|
200
|
+
# Use mutagen to identify and load the file
|
201
|
+
audio = mutagen.File(file_path)
|
202
|
+
if audio is None:
|
203
|
+
logger.warning("Could not identify file format: %s", file_path)
|
204
|
+
return tags
|
205
|
+
|
206
|
+
# Process different file types
|
207
|
+
if isinstance(audio, ID3) or hasattr(audio, 'ID3'):
|
208
|
+
# MP3 files
|
209
|
+
id3 = audio if isinstance(audio, ID3) else audio.ID3
|
210
|
+
for tag_key, tag_value in id3.items():
|
211
|
+
tag_name = tag_key.split(':')[0] # Handle ID3 tags with colons
|
212
|
+
if tag_name in TAG_MAPPING:
|
213
|
+
tag_value_str = str(tag_value)
|
214
|
+
tags[TAG_MAPPING[tag_name]] = normalize_tag_value(tag_value_str)
|
215
|
+
elif isinstance(audio, (FLAC, OggOpus, OggVorbis)):
|
216
|
+
# FLAC and OGG files
|
217
|
+
for tag_key, tag_values in audio.items():
|
218
|
+
tag_key_lower = tag_key.lower()
|
219
|
+
if tag_key_lower in TAG_MAPPING:
|
220
|
+
# Some tags might have multiple values, we'll take the first one
|
221
|
+
tag_value = tag_values[0] if tag_values else ''
|
222
|
+
tags[TAG_MAPPING[tag_key_lower]] = normalize_tag_value(tag_value)
|
223
|
+
elif isinstance(audio, MP4):
|
224
|
+
# MP4 files
|
225
|
+
for tag_key, tag_value in audio.items():
|
226
|
+
if tag_key in TAG_MAPPING:
|
227
|
+
if isinstance(tag_value, list):
|
228
|
+
if tag_key in ('trkn', 'disk'):
|
229
|
+
# Handle track and disc number tuples
|
230
|
+
if tag_value and isinstance(tag_value[0], tuple) and len(tag_value[0]) >= 1:
|
231
|
+
tags[TAG_MAPPING[tag_key]] = str(tag_value[0][0])
|
232
|
+
else:
|
233
|
+
tag_value_str = str(tag_value[0]) if tag_value else ''
|
234
|
+
tags[TAG_MAPPING[tag_key]] = normalize_tag_value(tag_value_str)
|
235
|
+
else:
|
236
|
+
tag_value_str = str(tag_value)
|
237
|
+
tags[TAG_MAPPING[tag_key]] = normalize_tag_value(tag_value_str)
|
238
|
+
else:
|
239
|
+
# Generic audio file - try to read any available tags
|
240
|
+
for tag_key, tag_value in audio.items():
|
241
|
+
tag_key_lower = tag_key.lower()
|
242
|
+
if tag_key_lower in TAG_MAPPING:
|
243
|
+
if isinstance(tag_value, list):
|
244
|
+
tag_value_str = str(tag_value[0]) if tag_value else ''
|
245
|
+
tags[TAG_MAPPING[tag_key_lower]] = normalize_tag_value(tag_value_str)
|
246
|
+
else:
|
247
|
+
tag_value_str = str(tag_value)
|
248
|
+
tags[TAG_MAPPING[tag_key_lower]] = normalize_tag_value(tag_value_str)
|
249
|
+
|
250
|
+
logger.debug("Successfully read %d tags from file", len(tags))
|
251
|
+
return tags
|
252
|
+
except Exception as e:
|
253
|
+
logger.error("Error reading tags from file %s: %s", file_path, str(e))
|
254
|
+
return tags
|
255
|
+
|
256
|
+
def extract_first_audio_file_tags(folder_path: str) -> Dict[str, str]:
|
257
|
+
"""
|
258
|
+
Extract tags from the first audio file in a folder.
|
259
|
+
|
260
|
+
Args:
|
261
|
+
folder_path: Path to folder containing audio files
|
262
|
+
|
263
|
+
Returns:
|
264
|
+
Dictionary containing standardized tag names and values
|
265
|
+
"""
|
266
|
+
from .audio_conversion import filter_directories
|
267
|
+
import glob
|
268
|
+
|
269
|
+
logger.debug("Looking for audio files in %s", folder_path)
|
270
|
+
files = filter_directories(glob.glob(os.path.join(folder_path, "*")))
|
271
|
+
|
272
|
+
if not files:
|
273
|
+
logger.debug("No audio files found in folder")
|
274
|
+
return {}
|
275
|
+
|
276
|
+
# Get tags from the first file
|
277
|
+
first_file = files[0]
|
278
|
+
logger.debug("Using first audio file for tags: %s", first_file)
|
279
|
+
|
280
|
+
return get_file_tags(first_file)
|
281
|
+
|
282
|
+
def extract_album_info(folder_path: str) -> Dict[str, str]:
|
283
|
+
"""
|
284
|
+
Extract album information from audio files in a folder.
|
285
|
+
Tries to get consistent album, artist and other information.
|
286
|
+
|
287
|
+
Args:
|
288
|
+
folder_path: Path to folder containing audio files
|
289
|
+
|
290
|
+
Returns:
|
291
|
+
Dictionary with extracted metadata (album, albumartist, etc.)
|
292
|
+
"""
|
293
|
+
from .audio_conversion import filter_directories
|
294
|
+
import glob
|
295
|
+
|
296
|
+
logger.debug("Extracting album information from folder: %s", folder_path)
|
297
|
+
|
298
|
+
# Get all audio files in the folder
|
299
|
+
audio_files = filter_directories(glob.glob(os.path.join(folder_path, "*")))
|
300
|
+
if not audio_files:
|
301
|
+
logger.debug("No audio files found in folder")
|
302
|
+
return {}
|
303
|
+
|
304
|
+
# Collect tag information from all files
|
305
|
+
all_tags = []
|
306
|
+
for file_path in audio_files:
|
307
|
+
tags = get_file_tags(file_path)
|
308
|
+
if tags:
|
309
|
+
all_tags.append(tags)
|
310
|
+
|
311
|
+
if not all_tags:
|
312
|
+
logger.debug("Could not read tags from any files in folder")
|
313
|
+
return {}
|
314
|
+
|
315
|
+
# Try to find consistent album information
|
316
|
+
result = {}
|
317
|
+
key_tags = ['album', 'albumartist', 'artist', 'date', 'genre']
|
318
|
+
|
319
|
+
for tag_name in key_tags:
|
320
|
+
# Count occurrences of each value
|
321
|
+
value_counts = {}
|
322
|
+
for tags in all_tags:
|
323
|
+
if tag_name in tags:
|
324
|
+
value = tags[tag_name]
|
325
|
+
if value in value_counts:
|
326
|
+
value_counts[value] += 1
|
327
|
+
else:
|
328
|
+
value_counts[value] = 1
|
329
|
+
|
330
|
+
# Use the most common value, or the first one if there's a tie
|
331
|
+
if value_counts:
|
332
|
+
most_common_value = max(value_counts.items(), key=lambda x: x[1])[0]
|
333
|
+
result[tag_name] = most_common_value
|
334
|
+
|
335
|
+
logger.debug("Extracted album info: %s", str(result))
|
336
|
+
return result
|
337
|
+
|
338
|
+
def get_file_metadata(file_path: str) -> Dict[str, str]:
|
339
|
+
"""
|
340
|
+
Get comprehensive metadata about a single audio file,
|
341
|
+
including both file tags and additional information.
|
342
|
+
|
343
|
+
Args:
|
344
|
+
file_path: Path to the audio file
|
345
|
+
|
346
|
+
Returns:
|
347
|
+
Dictionary containing metadata information
|
348
|
+
"""
|
349
|
+
metadata = {}
|
350
|
+
|
351
|
+
# Get basic file information
|
352
|
+
try:
|
353
|
+
basename = os.path.basename(file_path)
|
354
|
+
filename, extension = os.path.splitext(basename)
|
355
|
+
|
356
|
+
metadata['filename'] = filename
|
357
|
+
metadata['extension'] = extension.lower().replace('.', '')
|
358
|
+
metadata['path'] = file_path
|
359
|
+
|
360
|
+
# Get file size
|
361
|
+
metadata['filesize'] = os.path.getsize(file_path)
|
362
|
+
|
363
|
+
# Add tags from the file
|
364
|
+
tags = get_file_tags(file_path)
|
365
|
+
metadata.update(tags)
|
366
|
+
|
367
|
+
return metadata
|
368
|
+
except Exception as e:
|
369
|
+
logger.error("Error getting file metadata for %s: %s", file_path, str(e))
|
370
|
+
return metadata
|
371
|
+
|
372
|
+
def get_folder_metadata(folder_path: str) -> Dict[str, Any]:
|
373
|
+
"""
|
374
|
+
Get comprehensive metadata about a folder of audio files.
|
375
|
+
|
376
|
+
Args:
|
377
|
+
folder_path: Path to folder containing audio files
|
378
|
+
|
379
|
+
Returns:
|
380
|
+
Dictionary containing metadata information and list of files
|
381
|
+
"""
|
382
|
+
folder_metadata = {}
|
383
|
+
|
384
|
+
# Get basic folder information
|
385
|
+
folder_metadata['folder_name'] = os.path.basename(folder_path)
|
386
|
+
folder_metadata['folder_path'] = folder_path
|
387
|
+
|
388
|
+
# Try to extract album info
|
389
|
+
album_info = extract_album_info(folder_path)
|
390
|
+
folder_metadata.update(album_info)
|
391
|
+
|
392
|
+
# Also get folder name metadata using existing function
|
393
|
+
from .recursive_processor import extract_folder_meta
|
394
|
+
folder_name_meta = extract_folder_meta(folder_path)
|
395
|
+
|
396
|
+
# Combine the metadata, prioritizing tag-based over folder name based
|
397
|
+
for key, value in folder_name_meta.items():
|
398
|
+
if key not in folder_metadata or not folder_metadata[key]:
|
399
|
+
folder_metadata[key] = value
|
400
|
+
|
401
|
+
# Get list of audio files with their metadata
|
402
|
+
from .audio_conversion import filter_directories
|
403
|
+
import glob
|
404
|
+
|
405
|
+
audio_files = filter_directories(glob.glob(os.path.join(folder_path, "*")))
|
406
|
+
files_metadata = []
|
407
|
+
|
408
|
+
for file_path in audio_files:
|
409
|
+
file_metadata = get_file_metadata(file_path)
|
410
|
+
files_metadata.append(file_metadata)
|
411
|
+
|
412
|
+
folder_metadata['files'] = files_metadata
|
413
|
+
folder_metadata['file_count'] = len(files_metadata)
|
414
|
+
|
415
|
+
return folder_metadata
|
416
|
+
|
417
|
+
def format_metadata_filename(metadata: Dict[str, str], template: str = "{tracknumber} - {title}") -> str:
|
418
|
+
"""
|
419
|
+
Format a filename using metadata and a template string.
|
420
|
+
|
421
|
+
Args:
|
422
|
+
metadata: Dictionary of metadata tags
|
423
|
+
template: Template string with placeholders matching metadata keys
|
424
|
+
|
425
|
+
Returns:
|
426
|
+
Formatted string, or empty string if formatting fails
|
427
|
+
"""
|
428
|
+
try:
|
429
|
+
# Format track numbers correctly (e.g., "1" -> "01")
|
430
|
+
if 'tracknumber' in metadata:
|
431
|
+
track = metadata['tracknumber']
|
432
|
+
if '/' in track: # Handle "1/10" format
|
433
|
+
track = track.split('/')[0]
|
434
|
+
try:
|
435
|
+
metadata['tracknumber'] = f"{int(track):02d}"
|
436
|
+
except (ValueError, TypeError):
|
437
|
+
pass # Keep original value if not a simple number
|
438
|
+
|
439
|
+
# Format disc numbers the same way
|
440
|
+
if 'discnumber' in metadata:
|
441
|
+
disc = metadata['discnumber']
|
442
|
+
if '/' in disc: # Handle "1/2" format
|
443
|
+
disc = disc.split('/')[0]
|
444
|
+
try:
|
445
|
+
metadata['discnumber'] = f"{int(disc):02d}"
|
446
|
+
except (ValueError, TypeError):
|
447
|
+
pass
|
448
|
+
|
449
|
+
# Substitute keys in template
|
450
|
+
result = template
|
451
|
+
for key, value in metadata.items():
|
452
|
+
placeholder = "{" + key + "}"
|
453
|
+
if placeholder in result:
|
454
|
+
result = result.replace(placeholder, str(value))
|
455
|
+
|
456
|
+
# Clean up any remaining placeholders for missing metadata
|
457
|
+
import re
|
458
|
+
result = re.sub(r'\{[^}]+\}', '', result)
|
459
|
+
|
460
|
+
# Clean up consecutive spaces, dashes, etc.
|
461
|
+
result = re.sub(r'\s+', ' ', result)
|
462
|
+
result = re.sub(r'[-_\s]*-[-_\s]*', ' - ', result)
|
463
|
+
result = re.sub(r'^\s+|\s+$', '', result) # trim
|
464
|
+
|
465
|
+
# Replace characters that aren't allowed in filenames
|
466
|
+
result = re.sub(r'[<>:"/\\|?*]', '-', result)
|
467
|
+
|
468
|
+
return result
|
469
|
+
except Exception as e:
|
470
|
+
logger.error("Error formatting metadata: %s", str(e))
|
471
|
+
return ""
|
@@ -204,12 +204,101 @@ def extract_folder_meta(folder_path: str) -> Dict[str, str]:
|
|
204
204
|
return meta
|
205
205
|
|
206
206
|
|
207
|
-
def
|
207
|
+
def get_folder_name_from_metadata(folder_path: str, use_media_tags: bool = False, template: str = None) -> str:
|
208
|
+
"""
|
209
|
+
Generate a suitable output filename for a folder based on folder name
|
210
|
+
and optionally audio file metadata.
|
211
|
+
|
212
|
+
Args:
|
213
|
+
folder_path: Path to folder
|
214
|
+
use_media_tags: Whether to use media tags from audio files if available
|
215
|
+
template: Optional template for formatting output name using media tags
|
216
|
+
|
217
|
+
Returns:
|
218
|
+
String with cleaned output name
|
219
|
+
"""
|
220
|
+
# Start with folder name metadata
|
221
|
+
folder_meta = extract_folder_meta(folder_path)
|
222
|
+
output_name = None
|
223
|
+
|
224
|
+
# Try to get metadata from audio files if requested
|
225
|
+
if use_media_tags:
|
226
|
+
try:
|
227
|
+
# Import here to avoid circular imports
|
228
|
+
from .media_tags import extract_album_info, format_metadata_filename, is_available, normalize_tag_value
|
229
|
+
|
230
|
+
if is_available():
|
231
|
+
logger.debug("Using media tags to generate folder name for: %s", folder_path)
|
232
|
+
|
233
|
+
# Get album metadata from the files
|
234
|
+
album_info = extract_album_info(folder_path)
|
235
|
+
|
236
|
+
if album_info:
|
237
|
+
# Normalize all tag values to handle special characters
|
238
|
+
for key, value in album_info.items():
|
239
|
+
album_info[key] = normalize_tag_value(value)
|
240
|
+
|
241
|
+
# Add folder metadata as fallback values
|
242
|
+
if 'number' in folder_meta and folder_meta['number']:
|
243
|
+
if 'tracknumber' not in album_info or not album_info['tracknumber']:
|
244
|
+
album_info['tracknumber'] = folder_meta['number']
|
245
|
+
|
246
|
+
if 'title' in folder_meta and folder_meta['title']:
|
247
|
+
if 'album' not in album_info or not album_info['album']:
|
248
|
+
album_info['album'] = normalize_tag_value(folder_meta['title'])
|
249
|
+
|
250
|
+
# Use template or default format
|
251
|
+
format_template = template or "{album}"
|
252
|
+
if 'artist' in album_info and album_info['artist']:
|
253
|
+
format_template = format_template + " - {artist}"
|
254
|
+
if 'number' in folder_meta and folder_meta['number']:
|
255
|
+
format_template = "{tracknumber} - " + format_template
|
256
|
+
|
257
|
+
formatted_name = format_metadata_filename(album_info, format_template)
|
258
|
+
|
259
|
+
if formatted_name:
|
260
|
+
logger.debug("Generated name from media tags: %s", formatted_name)
|
261
|
+
output_name = formatted_name
|
262
|
+
except Exception as e:
|
263
|
+
logger.warning("Error using media tags for folder naming: %s", str(e))
|
264
|
+
|
265
|
+
# Fall back to folder name parsing if no media tags or if media tag extraction failed
|
266
|
+
if not output_name:
|
267
|
+
if folder_meta['number'] and folder_meta['title']:
|
268
|
+
# Apply normalization to the title from the folder name
|
269
|
+
try:
|
270
|
+
from .media_tags import normalize_tag_value
|
271
|
+
normalized_title = normalize_tag_value(folder_meta['title'])
|
272
|
+
output_name = f"{folder_meta['number']} - {normalized_title}"
|
273
|
+
except:
|
274
|
+
output_name = f"{folder_meta['number']} - {folder_meta['title']}"
|
275
|
+
else:
|
276
|
+
# Try to normalize the folder name itself
|
277
|
+
folder_name = os.path.basename(folder_path)
|
278
|
+
try:
|
279
|
+
from .media_tags import normalize_tag_value
|
280
|
+
output_name = normalize_tag_value(folder_name)
|
281
|
+
except:
|
282
|
+
output_name = folder_name
|
283
|
+
|
284
|
+
# Clean up the output name (remove invalid filename characters)
|
285
|
+
output_name = re.sub(r'[<>:"/\\|?*]', '_', output_name)
|
286
|
+
output_name = output_name.replace("???", "Fragezeichen")
|
287
|
+
output_name = output_name.replace("!!!", "Ausrufezeichen")
|
288
|
+
|
289
|
+
logger.debug("Final generated output name: %s", output_name)
|
290
|
+
return output_name
|
291
|
+
|
292
|
+
|
293
|
+
def process_recursive_folders(root_path: str, use_media_tags: bool = False,
|
294
|
+
name_template: str = None) -> List[Tuple[str, str, List[str]]]:
|
208
295
|
"""
|
209
296
|
Process folders recursively and prepare data for conversion.
|
210
297
|
|
211
298
|
Args:
|
212
299
|
root_path: Root directory to start processing from
|
300
|
+
use_media_tags: Whether to use media tags from audio files for naming
|
301
|
+
name_template: Optional template for formatting output names using media tags
|
213
302
|
|
214
303
|
Returns:
|
215
304
|
List of tuples: (output_filename, folder_path, list_of_audio_files)
|
@@ -230,17 +319,13 @@ def process_recursive_folders(root_path: str) -> List[Tuple[str, str, List[str]]
|
|
230
319
|
# Use natural sort order to ensure consistent results
|
231
320
|
audio_files = natural_sort(audio_files)
|
232
321
|
|
233
|
-
meta = extract_folder_meta(folder_path)
|
234
|
-
|
235
322
|
if audio_files:
|
236
|
-
#
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
# Clean up the output name (remove invalid filename characters)
|
243
|
-
output_name = re.sub(r'[<>:"/\\|?*]', '_', output_name)
|
323
|
+
# Generate output filename using metadata
|
324
|
+
output_name = get_folder_name_from_metadata(
|
325
|
+
folder_path,
|
326
|
+
use_media_tags=use_media_tags,
|
327
|
+
template=name_template
|
328
|
+
)
|
244
329
|
|
245
330
|
results.append((output_name, folder_path, audio_files))
|
246
331
|
logger.debug("Created processing task: %s -> %s (%d files)",
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: TonieToolbox
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.3
|
4
4
|
Summary: Convert audio files to Tonie box compatible format
|
5
5
|
Home-page: https://github.com/Quentendo64/TonieToolbox
|
6
6
|
Author: Quentendo64
|
@@ -46,6 +46,7 @@ A Python tool for converting audio files to Tonie box compatible format (TAF - T
|
|
46
46
|
- [Basic Usage](#basic-usage)
|
47
47
|
- [Advanced Options](#advanced-options)
|
48
48
|
- [Common Usage Examples](#common-usage-examples)
|
49
|
+
- [Media Tags](#media-tags)
|
49
50
|
- [Technical Details](#technical-details)
|
50
51
|
- [TAF File Structure](#taf-tonie-audio-format-file-structure)
|
51
52
|
- [File Analysis](#file-analysis)
|
@@ -67,15 +68,17 @@ The tool provides several capabilities:
|
|
67
68
|
- Split Tonie files into individual opus tracks
|
68
69
|
- Compare two TAF files for debugging differences
|
69
70
|
- Support various input formats through FFmpeg conversion
|
71
|
+
- Extract and use audio media tags (ID3, Vorbis Comments, etc.) for better file naming
|
70
72
|
|
71
73
|
## Requirements
|
72
74
|
|
73
75
|
- Python 3.6 or higher
|
74
|
-
- FFmpeg (for converting non-opus audio files)
|
76
|
+
- FFmpeg (for converting non-opus audio files)
|
75
77
|
- opus-tools (specifically `opusenc` for encoding to opus format)
|
78
|
+
- mutagen (for reading audio file metadata, auto-installed when needed)
|
76
79
|
|
77
80
|
Make sure FFmpeg and opus-tools are installed on your system and accessible in your PATH.
|
78
|
-
If the requirements are not found in PATH. TonieToolbox will download the missing requirements.
|
81
|
+
If the requirements are not found in PATH. TonieToolbox will download the missing requirements with --auto-download.
|
79
82
|
|
80
83
|
## Installation
|
81
84
|
|
@@ -166,7 +169,8 @@ Output:
|
|
166
169
|
```
|
167
170
|
usage: TonieToolbox.py [-h] [-v] [-t TIMESTAMP] [-f FFMPEG] [-o OPUSENC]
|
168
171
|
[-b BITRATE] [-c] [-a TAG] [-n] [-i] [-s] [-r] [-O]
|
169
|
-
[-A] [-k] [-C FILE2] [-D] [-
|
172
|
+
[-A] [-k] [-C FILE2] [-D] [-m] [--name-template TEMPLATE]
|
173
|
+
[--show-tags] [-d] [-T] [-q] [-Q]
|
170
174
|
SOURCE [TARGET]
|
171
175
|
|
172
176
|
Create Tonie compatible file from Ogg opus file(s).
|
@@ -198,6 +202,12 @@ optional arguments:
|
|
198
202
|
-D, --detailed-compare
|
199
203
|
Show detailed OGG page differences when comparing files
|
200
204
|
|
205
|
+
Media Tag Options:
|
206
|
+
-m, --use-media-tags Use media tags from audio files for naming
|
207
|
+
--name-template TEMPLATE
|
208
|
+
Template for naming files using media tags. Example: "{album} - {artist}"
|
209
|
+
--show-tags Show available media tags from input files
|
210
|
+
|
201
211
|
Version Check Options:
|
202
212
|
-S, --skip-update-check
|
203
213
|
Skip checking for updates
|
@@ -269,6 +279,52 @@ Process a music collection with nested album folders and save TAF files alongsid
|
|
269
279
|
tonietoolbox --recursive --output-to-source "\Hörspiele\"
|
270
280
|
```
|
271
281
|
|
282
|
+
### Media Tags
|
283
|
+
|
284
|
+
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.
|
285
|
+
|
286
|
+
#### View available tags in audio files:
|
287
|
+
|
288
|
+
To see what tags are available in your audio files:
|
289
|
+
|
290
|
+
```
|
291
|
+
tonietoolbox --show-tags input.mp3
|
292
|
+
```
|
293
|
+
|
294
|
+
This will display all readable tags from the file, which can be useful for creating naming templates.
|
295
|
+
|
296
|
+
#### Use media tags for file naming:
|
297
|
+
|
298
|
+
To use the metadata from audio files when generating output filenames:
|
299
|
+
|
300
|
+
```
|
301
|
+
tonietoolbox input.mp3 --use-media-tags
|
302
|
+
```
|
303
|
+
|
304
|
+
For single files, this will use a default template of "{title} - {artist}" for the output filename.
|
305
|
+
|
306
|
+
#### Custom naming templates:
|
307
|
+
|
308
|
+
You can specify custom templates for generating filenames based on the audio metadata:
|
309
|
+
|
310
|
+
```
|
311
|
+
tonietoolbox input.mp3 --use-media-tags --name-template "{artist} - {album} - {title}"
|
312
|
+
```
|
313
|
+
|
314
|
+
#### Recursive processing with media tags:
|
315
|
+
|
316
|
+
When processing folders recursively, media tags can provide more consistent naming:
|
317
|
+
|
318
|
+
```
|
319
|
+
tonietoolbox --recursive --use-media-tags "Music/Collection/"
|
320
|
+
```
|
321
|
+
|
322
|
+
This will attempt to use the album information from the audio files for naming the output files:
|
323
|
+
|
324
|
+
```
|
325
|
+
tonietoolbox --recursive --use-media-tags --name-template "{date} - {album} ({artist})" "Music/Collection/"
|
326
|
+
```
|
327
|
+
|
272
328
|
## Technical Details
|
273
329
|
|
274
330
|
### TAF (Tonie Audio Format) File Structure
|
@@ -1,21 +1,22 @@
|
|
1
|
-
TonieToolbox/__init__.py,sha256=
|
2
|
-
TonieToolbox/__main__.py,sha256=
|
1
|
+
TonieToolbox/__init__.py,sha256=mRuWBCYSLc6tAHyVDd70qM5LHgWH7R8ClFbFfILlEso,96
|
2
|
+
TonieToolbox/__main__.py,sha256=Otltlkl9Cd36BYs5_23ufCkdxguFFVzuVDwSvWDSPfI,17078
|
3
3
|
TonieToolbox/audio_conversion.py,sha256=ra72qsE8j2GEP_4kqDT9m6aKlnnREZhZAlpf7y83pA0,11202
|
4
4
|
TonieToolbox/constants.py,sha256=QQWQpnCI65GByLlXLOkt2n8nALLu4m6BWp0zuhI3M04,2021
|
5
|
-
TonieToolbox/dependency_manager.py,sha256=
|
5
|
+
TonieToolbox/dependency_manager.py,sha256=dRy8JxV3Dv1x8uWIXMonU0ArnCRknI1rkrj8MkJcyl4,27916
|
6
6
|
TonieToolbox/filename_generator.py,sha256=RqQHyGTKakuWR01yMSnFVMU_HfLw3rqFxKhXNIHdTlg,3441
|
7
7
|
TonieToolbox/logger.py,sha256=Up9fBVkOZwkY61_645bX4tienCpyVSkap-FeTV0v730,1441
|
8
|
+
TonieToolbox/media_tags.py,sha256=T8osaU101iGC43NaMbbHD8mwNh-dGa_uPj7IPlhMIno,16091
|
8
9
|
TonieToolbox/ogg_page.py,sha256=-ViaIRBgh5ayfwmyplL8QmmRr5P36X8W0DdHkSFUYUU,21948
|
9
10
|
TonieToolbox/opus_packet.py,sha256=OcHXEe3I_K4mWPUD55prpG42sZxJsEeAxqSbFxBmb0c,7895
|
10
|
-
TonieToolbox/recursive_processor.py,sha256=
|
11
|
+
TonieToolbox/recursive_processor.py,sha256=m70-oJ4MRXvI9OMy14sC9UsQIxwVEnEoA3hhLGdHhL4,13457
|
11
12
|
TonieToolbox/tonie_analysis.py,sha256=kp4Wx4cTDddtF2AlS6IX4xs1vQ-mpZ0gsAy4-UdRAAM,23287
|
12
13
|
TonieToolbox/tonie_file.py,sha256=vY0s8X4ln35ZXpdpGmBcIxgpTJAjduiVvBh34WObyrw,19647
|
13
14
|
TonieToolbox/tonie_header.proto,sha256=WaWfwO4VrwGtscK2ujfDRKtpeBpaVPoZhI8iMmR-C0U,202
|
14
15
|
TonieToolbox/tonie_header_pb2.py,sha256=s5bp4ULTEekgq6T61z9fDkRavyPM-3eREs20f_Pxxe8,3665
|
15
16
|
TonieToolbox/version_handler.py,sha256=7Zx-pgzAUhz6jMplvNal1wHyxidodVxaNcAV0EMph5k,9778
|
16
|
-
tonietoolbox-0.2.
|
17
|
-
tonietoolbox-0.2.
|
18
|
-
tonietoolbox-0.2.
|
19
|
-
tonietoolbox-0.2.
|
20
|
-
tonietoolbox-0.2.
|
21
|
-
tonietoolbox-0.2.
|
17
|
+
tonietoolbox-0.2.3.dist-info/licenses/LICENSE.md,sha256=rGoga9ZAgNco9fBapVFpWf6ri7HOBp1KRnt1uIruXMk,35190
|
18
|
+
tonietoolbox-0.2.3.dist-info/METADATA,sha256=2LiL3ouh2jIdA3CD7i8nSZY-PStXbhkT8GNTZVvpgrM,12444
|
19
|
+
tonietoolbox-0.2.3.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
20
|
+
tonietoolbox-0.2.3.dist-info/entry_points.txt,sha256=oqpeyBxel7aScg35Xr4gZKnf486S5KW9okqeBwyJxxc,60
|
21
|
+
tonietoolbox-0.2.3.dist-info/top_level.txt,sha256=Wkkm-2p7I3ENfS7ZbYtYUB2g-xwHrXVlERHfonsOPuE,13
|
22
|
+
tonietoolbox-0.2.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|