TonieToolbox 0.2.0__tar.gz → 0.2.2__tar.gz
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-0.2.0/TonieToolbox.egg-info → tonietoolbox-0.2.2}/PKG-INFO +40 -26
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/README.md +39 -25
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/__init__.py +1 -1
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/__main__.py +28 -25
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/dependency_manager.py +46 -1
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/tonie_analysis.py +66 -4
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/tonie_file.py +117 -21
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2/TonieToolbox.egg-info}/PKG-INFO +40 -26
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/LICENSE.md +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/MANIFEST.in +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/audio_conversion.py +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/constants.py +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/filename_generator.py +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/logger.py +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/ogg_page.py +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/opus_packet.py +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/recursive_processor.py +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/tonie_header.proto +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/tonie_header_pb2.py +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox/version_handler.py +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox.egg-info/SOURCES.txt +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox.egg-info/dependency_links.txt +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox.egg-info/entry_points.txt +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox.egg-info/requires.txt +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/TonieToolbox.egg-info/top_level.txt +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/pyproject.toml +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/setup.cfg +0 -0
- {tonietoolbox-0.2.0 → tonietoolbox-0.2.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: TonieToolbox
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.2
|
4
4
|
Summary: Convert audio files to Tonie box compatible format
|
5
5
|
Home-page: https://github.com/Quentendo64/TonieToolbox
|
6
6
|
Author: Quentendo64
|
@@ -159,15 +159,14 @@ tonietoolbox --recursive --output-to-source "Music/Albums"
|
|
159
159
|
Run the following command to see all available options:
|
160
160
|
|
161
161
|
```
|
162
|
-
tonietoolbox
|
162
|
+
tonietoolbox -h
|
163
163
|
```
|
164
164
|
|
165
165
|
Output:
|
166
166
|
```
|
167
|
-
usage: TonieToolbox.py [-h] [
|
168
|
-
[
|
169
|
-
[
|
170
|
-
[--compare FILE2] [--detailed-compare] [--debug] [--trace] [--quiet] [--silent]
|
167
|
+
usage: TonieToolbox.py [-h] [-v] [-t TIMESTAMP] [-f FFMPEG] [-o OPUSENC]
|
168
|
+
[-b BITRATE] [-c] [-a TAG] [-n] [-i] [-s] [-r] [-O]
|
169
|
+
[-A] [-k] [-C FILE2] [-D] [-d] [-T] [-q] [-Q]
|
171
170
|
SOURCE [TARGET]
|
172
171
|
|
173
172
|
Create Tonie compatible file from Ogg opus file(s).
|
@@ -178,25 +177,40 @@ positional arguments:
|
|
178
177
|
|
179
178
|
optional arguments:
|
180
179
|
-h, --help show this help message and exit
|
181
|
-
--
|
182
|
-
--
|
183
|
-
|
184
|
-
--
|
185
|
-
--
|
186
|
-
|
187
|
-
|
188
|
-
--
|
189
|
-
|
190
|
-
--
|
191
|
-
|
192
|
-
--
|
193
|
-
|
180
|
+
-v, --version show program version and exit
|
181
|
+
-t, --timestamp TIMESTAMP
|
182
|
+
set custom timestamp / bitstream serial / reference .taf file
|
183
|
+
-f, --ffmpeg FFMPEG specify location of ffmpeg
|
184
|
+
-o, --opusenc OPUSENC specify location of opusenc
|
185
|
+
-b, --bitrate BITRATE set encoding bitrate in kbps (default: 96)
|
186
|
+
-c, --cbr encode in cbr mode
|
187
|
+
-a, --append-tonie-tag TAG
|
188
|
+
append [TAG] to filename (must be an 8-character hex value)
|
189
|
+
-n, --no-tonie-header do not write Tonie header
|
190
|
+
-i, --info Check and display info about Tonie file
|
191
|
+
-s, --split Split Tonie file into opus tracks
|
192
|
+
-r, --recursive Process folders recursively
|
193
|
+
-O, --output-to-source
|
194
|
+
Save output files in the source directory instead of output directory
|
195
|
+
-A, --auto-download Automatically download FFmpeg and opusenc if needed
|
196
|
+
-k, --keep-temp Keep temporary opus files in a temp folder for testing
|
197
|
+
-C, --compare FILE2 Compare input file with another .taf file for debugging
|
198
|
+
-D, --detailed-compare
|
199
|
+
Show detailed OGG page differences when comparing files
|
200
|
+
|
201
|
+
Version Check Options:
|
202
|
+
-S, --skip-update-check
|
203
|
+
Skip checking for updates
|
204
|
+
-F, --force-refresh-cache
|
205
|
+
Force refresh of update information from PyPI
|
206
|
+
-X, --clear-version-cache
|
207
|
+
Clear cached version information
|
194
208
|
|
195
209
|
Logging Options:
|
196
|
-
--debug
|
197
|
-
--trace
|
198
|
-
--quiet
|
199
|
-
--silent
|
210
|
+
-d, --debug Enable debug logging
|
211
|
+
-T, --trace Enable trace logging (very verbose)
|
212
|
+
-q, --quiet Show only warnings and errors
|
213
|
+
-Q, --silent Show only errors
|
200
214
|
```
|
201
215
|
|
202
216
|
### Common Usage Examples
|
@@ -230,9 +244,9 @@ tonietoolbox file1.taf --compare file2.taf --detailed-compare
|
|
230
244
|
#### Custom timestamp options:
|
231
245
|
|
232
246
|
```
|
233
|
-
tonietoolbox input.mp3 --
|
234
|
-
tonietoolbox input.mp3 --
|
235
|
-
tonietoolbox input.mp3 --
|
247
|
+
tonietoolbox input.mp3 --timestamp 1745078762 # UNIX Timestamp
|
248
|
+
tonietoolbox input.mp3 --timestamp 0x6803C9EA # Bitstream time
|
249
|
+
tonietoolbox input.mp3 --timestamp ./reference.taf # Reference TAF for extraction
|
236
250
|
```
|
237
251
|
|
238
252
|
#### Set custom bitrate:
|
@@ -132,15 +132,14 @@ tonietoolbox --recursive --output-to-source "Music/Albums"
|
|
132
132
|
Run the following command to see all available options:
|
133
133
|
|
134
134
|
```
|
135
|
-
tonietoolbox
|
135
|
+
tonietoolbox -h
|
136
136
|
```
|
137
137
|
|
138
138
|
Output:
|
139
139
|
```
|
140
|
-
usage: TonieToolbox.py [-h] [
|
141
|
-
[
|
142
|
-
[
|
143
|
-
[--compare FILE2] [--detailed-compare] [--debug] [--trace] [--quiet] [--silent]
|
140
|
+
usage: TonieToolbox.py [-h] [-v] [-t TIMESTAMP] [-f FFMPEG] [-o OPUSENC]
|
141
|
+
[-b BITRATE] [-c] [-a TAG] [-n] [-i] [-s] [-r] [-O]
|
142
|
+
[-A] [-k] [-C FILE2] [-D] [-d] [-T] [-q] [-Q]
|
144
143
|
SOURCE [TARGET]
|
145
144
|
|
146
145
|
Create Tonie compatible file from Ogg opus file(s).
|
@@ -151,25 +150,40 @@ positional arguments:
|
|
151
150
|
|
152
151
|
optional arguments:
|
153
152
|
-h, --help show this help message and exit
|
154
|
-
--
|
155
|
-
--
|
156
|
-
|
157
|
-
--
|
158
|
-
--
|
159
|
-
|
160
|
-
|
161
|
-
--
|
162
|
-
|
163
|
-
--
|
164
|
-
|
165
|
-
--
|
166
|
-
|
153
|
+
-v, --version show program version and exit
|
154
|
+
-t, --timestamp TIMESTAMP
|
155
|
+
set custom timestamp / bitstream serial / reference .taf file
|
156
|
+
-f, --ffmpeg FFMPEG specify location of ffmpeg
|
157
|
+
-o, --opusenc OPUSENC specify location of opusenc
|
158
|
+
-b, --bitrate BITRATE set encoding bitrate in kbps (default: 96)
|
159
|
+
-c, --cbr encode in cbr mode
|
160
|
+
-a, --append-tonie-tag TAG
|
161
|
+
append [TAG] to filename (must be an 8-character hex value)
|
162
|
+
-n, --no-tonie-header do not write Tonie header
|
163
|
+
-i, --info Check and display info about Tonie file
|
164
|
+
-s, --split Split Tonie file into opus tracks
|
165
|
+
-r, --recursive Process folders recursively
|
166
|
+
-O, --output-to-source
|
167
|
+
Save output files in the source directory instead of output directory
|
168
|
+
-A, --auto-download Automatically download FFmpeg and opusenc if needed
|
169
|
+
-k, --keep-temp Keep temporary opus files in a temp folder for testing
|
170
|
+
-C, --compare FILE2 Compare input file with another .taf file for debugging
|
171
|
+
-D, --detailed-compare
|
172
|
+
Show detailed OGG page differences when comparing files
|
173
|
+
|
174
|
+
Version Check Options:
|
175
|
+
-S, --skip-update-check
|
176
|
+
Skip checking for updates
|
177
|
+
-F, --force-refresh-cache
|
178
|
+
Force refresh of update information from PyPI
|
179
|
+
-X, --clear-version-cache
|
180
|
+
Clear cached version information
|
167
181
|
|
168
182
|
Logging Options:
|
169
|
-
--debug
|
170
|
-
--trace
|
171
|
-
--quiet
|
172
|
-
--silent
|
183
|
+
-d, --debug Enable debug logging
|
184
|
+
-T, --trace Enable trace logging (very verbose)
|
185
|
+
-q, --quiet Show only warnings and errors
|
186
|
+
-Q, --silent Show only errors
|
173
187
|
```
|
174
188
|
|
175
189
|
### Common Usage Examples
|
@@ -203,9 +217,9 @@ tonietoolbox file1.taf --compare file2.taf --detailed-compare
|
|
203
217
|
#### Custom timestamp options:
|
204
218
|
|
205
219
|
```
|
206
|
-
tonietoolbox input.mp3 --
|
207
|
-
tonietoolbox input.mp3 --
|
208
|
-
tonietoolbox input.mp3 --
|
220
|
+
tonietoolbox input.mp3 --timestamp 1745078762 # UNIX Timestamp
|
221
|
+
tonietoolbox input.mp3 --timestamp 0x6803C9EA # Bitstream time
|
222
|
+
tonietoolbox input.mp3 --timestamp ./reference.taf # Reference TAF for extraction
|
209
223
|
```
|
210
224
|
|
211
225
|
#### Set custom bitrate:
|
@@ -21,50 +21,52 @@ from .recursive_processor import process_recursive_folders
|
|
21
21
|
def main():
|
22
22
|
"""Entry point for the TonieToolbox application."""
|
23
23
|
parser = argparse.ArgumentParser(description='Create Tonie compatible file from Ogg opus file(s).')
|
24
|
-
parser.add_argument('--version', action='version', version=f'TonieToolbox {__version__}',
|
24
|
+
parser.add_argument('-v', '--version', action='version', version=f'TonieToolbox {__version__}',
|
25
25
|
help='show program version and exit')
|
26
26
|
parser.add_argument('input_filename', metavar='SOURCE', type=str,
|
27
27
|
help='input file or directory or a file list (.lst)')
|
28
28
|
parser.add_argument('output_filename', metavar='TARGET', nargs='?', type=str,
|
29
29
|
help='the output file name (default: ---ID---)')
|
30
|
-
parser.add_argument('--
|
30
|
+
parser.add_argument('-t', '--timestamp', dest='user_timestamp', metavar='TIMESTAMP', action='store',
|
31
31
|
help='set custom timestamp / bitstream serial')
|
32
32
|
|
33
|
-
parser.add_argument('--ffmpeg', help='specify location of ffmpeg', default=None)
|
34
|
-
parser.add_argument('--opusenc', help='specify location of opusenc', default=None)
|
35
|
-
parser.add_argument('--bitrate', type=int, help='set encoding bitrate in kbps (default: 96)', default=96)
|
36
|
-
parser.add_argument('--cbr', action='store_true', help='encode in cbr mode')
|
37
|
-
parser.add_argument('--append-tonie-tag', metavar='TAG', action='store',
|
33
|
+
parser.add_argument('-f', '--ffmpeg', help='specify location of ffmpeg', default=None)
|
34
|
+
parser.add_argument('-o', '--opusenc', help='specify location of opusenc', default=None)
|
35
|
+
parser.add_argument('-b', '--bitrate', type=int, help='set encoding bitrate in kbps (default: 96)', default=96)
|
36
|
+
parser.add_argument('-c', '--cbr', action='store_true', help='encode in cbr mode')
|
37
|
+
parser.add_argument('-a', '--append-tonie-tag', metavar='TAG', action='store',
|
38
38
|
help='append [TAG] to filename (must be an 8-character hex value)')
|
39
|
-
parser.add_argument('--no-tonie-header', action='store_true', help='do not write Tonie header')
|
40
|
-
parser.add_argument('--info', action='store_true', help='Check and display info about Tonie file')
|
41
|
-
parser.add_argument('--split', action='store_true', help='Split Tonie file into opus tracks')
|
42
|
-
parser.add_argument('--recursive', action='store_true', help='Process folders recursively')
|
43
|
-
parser.add_argument('--output-to-source', action='store_true',
|
39
|
+
parser.add_argument('-n', '--no-tonie-header', action='store_true', help='do not write Tonie header')
|
40
|
+
parser.add_argument('-i', '--info', action='store_true', help='Check and display info about Tonie file')
|
41
|
+
parser.add_argument('-s', '--split', action='store_true', help='Split Tonie file into opus tracks')
|
42
|
+
parser.add_argument('-r', '--recursive', action='store_true', help='Process folders recursively')
|
43
|
+
parser.add_argument('-O', '--output-to-source', action='store_true',
|
44
44
|
help='Save output files in the source directory instead of output directory')
|
45
|
-
parser.add_argument('--auto-download', action='store_true', help='Automatically download FFmpeg and opusenc if needed')
|
46
|
-
parser.add_argument('--keep-temp', action='store_true',
|
45
|
+
parser.add_argument('-A', '--auto-download', action='store_true', help='Automatically download FFmpeg and opusenc if needed')
|
46
|
+
parser.add_argument('-k', '--keep-temp', action='store_true',
|
47
47
|
help='Keep temporary opus files in a temp folder for testing')
|
48
|
-
parser.add_argument('
|
48
|
+
parser.add_argument('-u', '--use-legacy-tags', action='store_true',
|
49
|
+
help='Use legacy hardcoded tags instead of dynamic TonieToolbox tags')
|
50
|
+
parser.add_argument('-C', '--compare', action='store', metavar='FILE2',
|
49
51
|
help='Compare input file with another .taf file for debugging')
|
50
|
-
parser.add_argument('--detailed-compare', action='store_true',
|
52
|
+
parser.add_argument('-D', '--detailed-compare', action='store_true',
|
51
53
|
help='Show detailed OGG page differences when comparing files')
|
52
54
|
|
53
55
|
# Version check options
|
54
56
|
version_group = parser.add_argument_group('Version Check Options')
|
55
|
-
version_group.add_argument('--skip-update-check', action='store_true',
|
57
|
+
version_group.add_argument('-S', '--skip-update-check', action='store_true',
|
56
58
|
help='Skip checking for updates')
|
57
|
-
version_group.add_argument('--force-refresh-cache', action='store_true',
|
59
|
+
version_group.add_argument('-F', '--force-refresh-cache', action='store_true',
|
58
60
|
help='Force refresh of update information from PyPI')
|
59
|
-
version_group.add_argument('--clear-version-cache', action='store_true',
|
61
|
+
version_group.add_argument('-X', '--clear-version-cache', action='store_true',
|
60
62
|
help='Clear cached version information')
|
61
63
|
|
62
64
|
log_group = parser.add_argument_group('Logging Options')
|
63
65
|
log_level_group = log_group.add_mutually_exclusive_group()
|
64
|
-
log_level_group.add_argument('--debug', action='store_true', help='Enable debug logging')
|
65
|
-
log_level_group.add_argument('--trace', action='store_true', help='Enable trace logging (very verbose)')
|
66
|
-
log_level_group.add_argument('--quiet', action='store_true', help='Show only warnings and errors')
|
67
|
-
log_level_group.add_argument('--silent', action='store_true', help='Show only errors')
|
66
|
+
log_level_group.add_argument('-d', '--debug', action='store_true', help='Enable debug logging')
|
67
|
+
log_level_group.add_argument('-T', '--trace', action='store_true', help='Enable trace logging (very verbose)')
|
68
|
+
log_level_group.add_argument('-q', '--quiet', action='store_true', help='Show only warnings and errors')
|
69
|
+
log_level_group.add_argument('-Q', '--silent', action='store_true', help='Show only errors')
|
68
70
|
|
69
71
|
args = parser.parse_args()
|
70
72
|
if args.trace:
|
@@ -143,7 +145,7 @@ def main():
|
|
143
145
|
|
144
146
|
create_tonie_file(task_out_filename, audio_files, args.no_tonie_header, args.user_timestamp,
|
145
147
|
args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
|
146
|
-
args.auto_download)
|
148
|
+
args.auto_download, not args.use_legacy_tags)
|
147
149
|
logger.info("Successfully created Tonie file: %s", task_out_filename)
|
148
150
|
|
149
151
|
logger.info("Recursive processing completed. Created %d Tonie files.", len(process_tasks))
|
@@ -207,7 +209,8 @@ def main():
|
|
207
209
|
|
208
210
|
logger.info("Creating Tonie file: %s with %d input file(s)", out_filename, len(files))
|
209
211
|
create_tonie_file(out_filename, files, args.no_tonie_header, args.user_timestamp,
|
210
|
-
args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
|
212
|
+
args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
|
213
|
+
args.auto_download, not args.use_legacy_tags)
|
211
214
|
logger.info("Successfully created Tonie file: %s", out_filename)
|
212
215
|
|
213
216
|
|
@@ -532,4 +532,49 @@ def get_opus_binary(auto_download=False):
|
|
532
532
|
Returns:
|
533
533
|
str: Path to the opusenc binary if available, None otherwise
|
534
534
|
"""
|
535
|
-
return ensure_dependency('opusenc', auto_download)
|
535
|
+
return ensure_dependency('opusenc', auto_download)
|
536
|
+
|
537
|
+
def get_opus_version(opus_binary=None):
|
538
|
+
"""
|
539
|
+
Get the version of opusenc.
|
540
|
+
|
541
|
+
Args:
|
542
|
+
opus_binary: Path to the opusenc binary
|
543
|
+
|
544
|
+
Returns:
|
545
|
+
str: The version string of opusenc, or a fallback string if the version cannot be determined
|
546
|
+
"""
|
547
|
+
import subprocess
|
548
|
+
import re
|
549
|
+
|
550
|
+
logger = get_logger('dependency_manager')
|
551
|
+
|
552
|
+
if opus_binary is None:
|
553
|
+
opus_binary = get_opus_binary()
|
554
|
+
|
555
|
+
if opus_binary is None:
|
556
|
+
logger.debug("opusenc binary not found, using fallback version string")
|
557
|
+
return "opusenc from opus-tools XXX" # Fallback
|
558
|
+
|
559
|
+
try:
|
560
|
+
# Run opusenc --version and capture output
|
561
|
+
result = subprocess.run([opus_binary, "--version"],
|
562
|
+
capture_output=True, text=True, check=False)
|
563
|
+
|
564
|
+
# Extract version information from output
|
565
|
+
version_output = result.stdout.strip() or result.stderr.strip()
|
566
|
+
|
567
|
+
if version_output:
|
568
|
+
# Try to extract just the version information using regex
|
569
|
+
match = re.search(r"(opusenc.*)", version_output)
|
570
|
+
if match:
|
571
|
+
return match.group(1)
|
572
|
+
else:
|
573
|
+
return version_output.splitlines()[0] # Use first line
|
574
|
+
else:
|
575
|
+
logger.debug("Could not determine opusenc version, using fallback")
|
576
|
+
return "opusenc from opus-tools XXX" # Fallback
|
577
|
+
|
578
|
+
except Exception as e:
|
579
|
+
logger.debug(f"Error getting opusenc version: {str(e)}")
|
580
|
+
return "opusenc from opus-tools XXX" # Fallback
|
@@ -72,7 +72,8 @@ def get_header_info(in_file):
|
|
72
72
|
|
73
73
|
Returns:
|
74
74
|
tuple: Header size, Tonie header object, file size, audio size, SHA1 sum,
|
75
|
-
Opus header found flag, Opus version, channel count, sample rate, bitstream serial number
|
75
|
+
Opus header found flag, Opus version, channel count, sample rate, bitstream serial number,
|
76
|
+
Opus comments dictionary
|
76
77
|
|
77
78
|
Raises:
|
78
79
|
RuntimeError: If OGG pages cannot be found
|
@@ -112,17 +113,64 @@ def get_header_info(in_file):
|
|
112
113
|
logger.debug("Opus header found: %s, Version: %d, Channels: %d, Sample rate: %d Hz, Serial: %d",
|
113
114
|
opus_head_found, opus_version, channel_count, sample_rate, bitstream_serial_no)
|
114
115
|
|
116
|
+
# Read and parse Opus comments from the second page
|
117
|
+
opus_comments = {}
|
115
118
|
found = OggPage.seek_to_page_header(in_file)
|
116
119
|
if not found:
|
117
120
|
logger.error("Second OGG page not found")
|
118
121
|
raise RuntimeError("Second ogg page not found")
|
119
122
|
|
120
|
-
OggPage(in_file)
|
123
|
+
second_page = OggPage(in_file)
|
121
124
|
logger.debug("Read second OGG page")
|
125
|
+
|
126
|
+
try:
|
127
|
+
# Combine all segments data for the second page
|
128
|
+
comment_data = bytearray()
|
129
|
+
for segment in second_page.segments:
|
130
|
+
comment_data.extend(segment.data)
|
131
|
+
|
132
|
+
if comment_data.startswith(b"OpusTags"):
|
133
|
+
pos = 8 # Skip "OpusTags"
|
134
|
+
# Extract vendor string
|
135
|
+
if pos + 4 <= len(comment_data):
|
136
|
+
vendor_length = struct.unpack("<I", comment_data[pos:pos+4])[0]
|
137
|
+
pos += 4
|
138
|
+
if pos + vendor_length <= len(comment_data):
|
139
|
+
vendor = comment_data[pos:pos+vendor_length].decode('utf-8', errors='replace')
|
140
|
+
opus_comments["vendor"] = vendor
|
141
|
+
pos += vendor_length
|
142
|
+
|
143
|
+
# Extract comments count
|
144
|
+
if pos + 4 <= len(comment_data):
|
145
|
+
comments_count = struct.unpack("<I", comment_data[pos:pos+4])[0]
|
146
|
+
pos += 4
|
147
|
+
|
148
|
+
# Extract individual comments
|
149
|
+
for i in range(comments_count):
|
150
|
+
if pos + 4 <= len(comment_data):
|
151
|
+
comment_length = struct.unpack("<I", comment_data[pos:pos+4])[0]
|
152
|
+
pos += 4
|
153
|
+
if pos + comment_length <= len(comment_data):
|
154
|
+
comment = comment_data[pos:pos+comment_length].decode('utf-8', errors='replace')
|
155
|
+
pos += comment_length
|
156
|
+
|
157
|
+
# Split comment into key/value if possible
|
158
|
+
if "=" in comment:
|
159
|
+
key, value = comment.split("=", 1)
|
160
|
+
opus_comments[key] = value
|
161
|
+
else:
|
162
|
+
opus_comments[f"comment_{i}"] = comment
|
163
|
+
else:
|
164
|
+
break
|
165
|
+
else:
|
166
|
+
break
|
167
|
+
except Exception as e:
|
168
|
+
logger.error("Failed to parse Opus comments: %s", str(e))
|
122
169
|
|
123
170
|
return (
|
124
171
|
header_size, tonie_header, file_size, audio_size, sha1sum,
|
125
|
-
opus_head_found, opus_version, channel_count, sample_rate, bitstream_serial_no
|
172
|
+
opus_head_found, opus_version, channel_count, sample_rate, bitstream_serial_no,
|
173
|
+
opus_comments
|
126
174
|
)
|
127
175
|
|
128
176
|
|
@@ -204,7 +252,7 @@ def check_tonie_file(filename):
|
|
204
252
|
|
205
253
|
with open(filename, "rb") as in_file:
|
206
254
|
header_size, tonie_header, file_size, audio_size, sha1, opus_head_found, \
|
207
|
-
opus_version, channel_count, sample_rate, bitstream_serial_no = get_header_info(in_file)
|
255
|
+
opus_version, channel_count, sample_rate, bitstream_serial_no, opus_comments = get_header_info(in_file)
|
208
256
|
|
209
257
|
page_count, alignment_okay, page_size_okay, total_time, \
|
210
258
|
chapters = get_audio_info(in_file, sample_rate, tonie_header, header_size)
|
@@ -254,6 +302,20 @@ def check_tonie_file(filename):
|
|
254
302
|
print("")
|
255
303
|
print("[{}] File is {}valid".format("OK" if all_ok else "NOT OK", "" if all_ok else "NOT "))
|
256
304
|
print("")
|
305
|
+
|
306
|
+
# Display Opus comments if available
|
307
|
+
if opus_comments:
|
308
|
+
print("[ii] Opus Comments:")
|
309
|
+
if "vendor" in opus_comments:
|
310
|
+
print(" Vendor: {}".format(opus_comments["vendor"]))
|
311
|
+
# Remove vendor from dict to avoid showing it twice
|
312
|
+
vendor = opus_comments.pop("vendor")
|
313
|
+
|
314
|
+
# Sort remaining comments for consistent display
|
315
|
+
for key in sorted(opus_comments.keys()):
|
316
|
+
print(" {}: {}".format(key, opus_comments[key]))
|
317
|
+
print("")
|
318
|
+
|
257
319
|
print("[ii] Total runtime: {}".format(granule_to_time_string(total_time)))
|
258
320
|
print("[ii] {} Tracks:".format(len(chapters)))
|
259
321
|
for i in range(0, len(chapters)):
|
@@ -19,6 +19,33 @@ from .logger import get_logger
|
|
19
19
|
logger = get_logger('tonie_file')
|
20
20
|
|
21
21
|
|
22
|
+
def toniefile_comment_add(buffer, length, comment_str):
|
23
|
+
"""
|
24
|
+
Add a comment string to an Opus comment packet buffer.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
buffer: Bytearray buffer to add comment to
|
28
|
+
length: Current position in the buffer
|
29
|
+
comment_str: Comment string to add
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
int: New position in the buffer after adding comment
|
33
|
+
"""
|
34
|
+
logger.debug("Adding comment: %s", comment_str)
|
35
|
+
|
36
|
+
# Add 4-byte length prefix
|
37
|
+
str_length = len(comment_str)
|
38
|
+
buffer[length:length+4] = struct.pack("<I", str_length)
|
39
|
+
length += 4
|
40
|
+
|
41
|
+
# Add the actual string
|
42
|
+
buffer[length:length+str_length] = comment_str.encode('utf-8')
|
43
|
+
length += str_length
|
44
|
+
|
45
|
+
logger.trace("Added comment of length %d, new buffer position: %d", str_length, length)
|
46
|
+
return length
|
47
|
+
|
48
|
+
|
22
49
|
def check_identification_header(page):
|
23
50
|
"""
|
24
51
|
Check if a page contains a valid Opus identification header.
|
@@ -52,37 +79,103 @@ def check_identification_header(page):
|
|
52
79
|
logger.debug("Opus identification header is valid")
|
53
80
|
|
54
81
|
|
55
|
-
def prepare_opus_tags(page):
|
82
|
+
def prepare_opus_tags(page, custom_tags=False, bitrate=64, vbr=True, opus_binary=None):
|
56
83
|
"""
|
57
84
|
Prepare standard Opus tags for a Tonie file.
|
58
85
|
|
59
86
|
Args:
|
60
87
|
page: OggPage to modify
|
88
|
+
custom_tags: Whether to use custom TonieToolbox tags instead of default ones
|
89
|
+
bitrate: Actual bitrate used for encoding
|
90
|
+
vbr: Whether variable bitrate was used
|
91
|
+
opus_binary: Path to opusenc binary for version detection
|
61
92
|
|
62
93
|
Returns:
|
63
94
|
OggPage: Modified page with Tonie-compatible Opus tags
|
64
95
|
"""
|
65
96
|
logger.debug("Preparing Opus tags for Tonie compatibility")
|
66
97
|
page.segments.clear()
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
98
|
+
|
99
|
+
if not custom_tags:
|
100
|
+
# Use the default hardcoded tags for backward compatibility
|
101
|
+
segment = OpusPacket(None)
|
102
|
+
segment.size = len(OPUS_TAGS[0])
|
103
|
+
segment.data = bytearray(OPUS_TAGS[0])
|
104
|
+
segment.spanning_packet = True
|
105
|
+
segment.first_packet = True
|
106
|
+
page.segments.append(segment)
|
107
|
+
|
108
|
+
segment = OpusPacket(None)
|
109
|
+
segment.size = len(OPUS_TAGS[1])
|
110
|
+
segment.data = bytearray(OPUS_TAGS[1])
|
111
|
+
segment.spanning_packet = False
|
112
|
+
segment.first_packet = False
|
113
|
+
page.segments.append(segment)
|
114
|
+
else:
|
115
|
+
# Use custom tags for TonieToolbox
|
116
|
+
# Create buffer for opus tags (similar to teddyCloud implementation)
|
117
|
+
logger.debug("Creating custom Opus tags")
|
118
|
+
comment_data = bytearray(0x1B4) # Same size as in teddyCloud
|
119
|
+
|
120
|
+
# OpusTags signature
|
121
|
+
comment_data_pos = 0
|
122
|
+
comment_data[comment_data_pos:comment_data_pos+8] = b"OpusTags"
|
123
|
+
comment_data_pos += 8
|
124
|
+
|
125
|
+
# Vendor string
|
126
|
+
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, "TonieToolbox")
|
127
|
+
|
128
|
+
# Number of comments (3 comments: version, encoder info, and encoder options)
|
129
|
+
comments_count = 3
|
130
|
+
comment_data[comment_data_pos:comment_data_pos+4] = struct.pack("<I", comments_count)
|
131
|
+
comment_data_pos += 4
|
132
|
+
|
133
|
+
# Add version information
|
134
|
+
from . import __version__
|
135
|
+
version_str = f"version={__version__}"
|
136
|
+
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, version_str)
|
137
|
+
|
138
|
+
# Get actual opusenc version
|
139
|
+
from .dependency_manager import get_opus_version
|
140
|
+
encoder_info = get_opus_version(opus_binary)
|
141
|
+
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, f"encoder={encoder_info}")
|
142
|
+
|
143
|
+
# Create encoder options string with actual settings
|
144
|
+
vbr_opt = "--vbr" if vbr else "--cbr"
|
145
|
+
encoder_options = f"encoder_options=--bitrate {bitrate} {vbr_opt}"
|
146
|
+
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, encoder_options)
|
147
|
+
|
148
|
+
# Add padding
|
149
|
+
remain = len(comment_data) - comment_data_pos - 4
|
150
|
+
comment_data[comment_data_pos:comment_data_pos+4] = struct.pack("<I", remain)
|
151
|
+
comment_data_pos += 4
|
152
|
+
comment_data[comment_data_pos:comment_data_pos+4] = b"pad="
|
153
|
+
|
154
|
+
# Create segments - handle data in chunks of 255 bytes maximum
|
155
|
+
comment_data = comment_data[:comment_data_pos + remain] # Trim to actual used size
|
156
|
+
|
157
|
+
# Split large data into smaller segments (each <= 255 bytes)
|
158
|
+
remaining_data = comment_data
|
159
|
+
first_segment = True
|
160
|
+
|
161
|
+
while remaining_data:
|
162
|
+
chunk_size = min(255, len(remaining_data))
|
163
|
+
segment = OpusPacket(None)
|
164
|
+
segment.size = chunk_size
|
165
|
+
segment.data = remaining_data[:chunk_size]
|
166
|
+
segment.spanning_packet = len(remaining_data) > chunk_size # More data follows
|
167
|
+
segment.first_packet = first_segment
|
168
|
+
page.segments.append(segment)
|
169
|
+
|
170
|
+
remaining_data = remaining_data[chunk_size:]
|
171
|
+
first_segment = False
|
172
|
+
|
80
173
|
page.correct_values(0)
|
81
174
|
logger.trace("Opus tags prepared with %d segments", len(page.segments))
|
82
175
|
return page
|
83
176
|
|
84
177
|
|
85
|
-
def copy_first_and_second_page(in_file, out_file, timestamp, sha):
|
178
|
+
def copy_first_and_second_page(in_file, out_file, timestamp, sha, use_custom_tags=True, bitrate=64, vbr=True, opus_binary=None):
|
86
179
|
"""
|
87
180
|
Copy and modify the first two pages of an Opus file for a Tonie file.
|
88
181
|
|
@@ -91,9 +184,10 @@ def copy_first_and_second_page(in_file, out_file, timestamp, sha):
|
|
91
184
|
out_file: Output file handle
|
92
185
|
timestamp: Timestamp to use for the Tonie file
|
93
186
|
sha: SHA1 hash object to update with written data
|
94
|
-
|
95
|
-
|
96
|
-
|
187
|
+
use_custom_tags: Whether to use custom TonieToolbox tags
|
188
|
+
bitrate: Actual bitrate used for encoding
|
189
|
+
vbr: Whether VBR was used
|
190
|
+
opus_binary: Path to opusenc binary
|
97
191
|
"""
|
98
192
|
logger.debug("Copying first and second pages with timestamp %d", timestamp)
|
99
193
|
found = OggPage.seek_to_page_header(in_file)
|
@@ -116,7 +210,7 @@ def copy_first_and_second_page(in_file, out_file, timestamp, sha):
|
|
116
210
|
page = OggPage(in_file)
|
117
211
|
page.serial_no = timestamp
|
118
212
|
page.checksum = page.calc_checksum()
|
119
|
-
page = prepare_opus_tags(page)
|
213
|
+
page = prepare_opus_tags(page, use_custom_tags, bitrate, vbr, opus_binary)
|
120
214
|
page.write_page(out_file, sha)
|
121
215
|
logger.debug("Second page written successfully")
|
122
216
|
|
@@ -277,7 +371,8 @@ def fix_tonie_header(out_file, chapters, timestamp, sha):
|
|
277
371
|
|
278
372
|
|
279
373
|
def create_tonie_file(output_file, input_files, no_tonie_header=False, user_timestamp=None,
|
280
|
-
bitrate=96, vbr=True, ffmpeg_binary=None, opus_binary=None, keep_temp=False, auto_download=False
|
374
|
+
bitrate=96, vbr=True, ffmpeg_binary=None, opus_binary=None, keep_temp=False, auto_download=False,
|
375
|
+
use_custom_tags=True):
|
281
376
|
"""
|
282
377
|
Create a Tonie file from input files.
|
283
378
|
|
@@ -292,6 +387,7 @@ def create_tonie_file(output_file, input_files, no_tonie_header=False, user_time
|
|
292
387
|
opus_binary: Path to opusenc binary
|
293
388
|
keep_temp: Whether to keep temporary opus files for testing
|
294
389
|
auto_download: Whether to automatically download dependencies if not found
|
390
|
+
use_custom_tags: Whether to use dynamic comment tags generated with toniefile_comment_add
|
295
391
|
"""
|
296
392
|
from .audio_conversion import get_opus_tempfile
|
297
393
|
|
@@ -369,7 +465,7 @@ def create_tonie_file(output_file, input_files, no_tonie_header=False, user_time
|
|
369
465
|
try:
|
370
466
|
if next_page_no == 2:
|
371
467
|
logger.debug("Processing first file: copying first and second page")
|
372
|
-
copy_first_and_second_page(handle, out_file, timestamp, sha1)
|
468
|
+
copy_first_and_second_page(handle, out_file, timestamp, sha1, use_custom_tags, bitrate, vbr, opus_binary)
|
373
469
|
else:
|
374
470
|
logger.debug("Processing subsequent file: skipping first and second page")
|
375
471
|
other_size = max_size
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: TonieToolbox
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.2
|
4
4
|
Summary: Convert audio files to Tonie box compatible format
|
5
5
|
Home-page: https://github.com/Quentendo64/TonieToolbox
|
6
6
|
Author: Quentendo64
|
@@ -159,15 +159,14 @@ tonietoolbox --recursive --output-to-source "Music/Albums"
|
|
159
159
|
Run the following command to see all available options:
|
160
160
|
|
161
161
|
```
|
162
|
-
tonietoolbox
|
162
|
+
tonietoolbox -h
|
163
163
|
```
|
164
164
|
|
165
165
|
Output:
|
166
166
|
```
|
167
|
-
usage: TonieToolbox.py [-h] [
|
168
|
-
[
|
169
|
-
[
|
170
|
-
[--compare FILE2] [--detailed-compare] [--debug] [--trace] [--quiet] [--silent]
|
167
|
+
usage: TonieToolbox.py [-h] [-v] [-t TIMESTAMP] [-f FFMPEG] [-o OPUSENC]
|
168
|
+
[-b BITRATE] [-c] [-a TAG] [-n] [-i] [-s] [-r] [-O]
|
169
|
+
[-A] [-k] [-C FILE2] [-D] [-d] [-T] [-q] [-Q]
|
171
170
|
SOURCE [TARGET]
|
172
171
|
|
173
172
|
Create Tonie compatible file from Ogg opus file(s).
|
@@ -178,25 +177,40 @@ positional arguments:
|
|
178
177
|
|
179
178
|
optional arguments:
|
180
179
|
-h, --help show this help message and exit
|
181
|
-
--
|
182
|
-
--
|
183
|
-
|
184
|
-
--
|
185
|
-
--
|
186
|
-
|
187
|
-
|
188
|
-
--
|
189
|
-
|
190
|
-
--
|
191
|
-
|
192
|
-
--
|
193
|
-
|
180
|
+
-v, --version show program version and exit
|
181
|
+
-t, --timestamp TIMESTAMP
|
182
|
+
set custom timestamp / bitstream serial / reference .taf file
|
183
|
+
-f, --ffmpeg FFMPEG specify location of ffmpeg
|
184
|
+
-o, --opusenc OPUSENC specify location of opusenc
|
185
|
+
-b, --bitrate BITRATE set encoding bitrate in kbps (default: 96)
|
186
|
+
-c, --cbr encode in cbr mode
|
187
|
+
-a, --append-tonie-tag TAG
|
188
|
+
append [TAG] to filename (must be an 8-character hex value)
|
189
|
+
-n, --no-tonie-header do not write Tonie header
|
190
|
+
-i, --info Check and display info about Tonie file
|
191
|
+
-s, --split Split Tonie file into opus tracks
|
192
|
+
-r, --recursive Process folders recursively
|
193
|
+
-O, --output-to-source
|
194
|
+
Save output files in the source directory instead of output directory
|
195
|
+
-A, --auto-download Automatically download FFmpeg and opusenc if needed
|
196
|
+
-k, --keep-temp Keep temporary opus files in a temp folder for testing
|
197
|
+
-C, --compare FILE2 Compare input file with another .taf file for debugging
|
198
|
+
-D, --detailed-compare
|
199
|
+
Show detailed OGG page differences when comparing files
|
200
|
+
|
201
|
+
Version Check Options:
|
202
|
+
-S, --skip-update-check
|
203
|
+
Skip checking for updates
|
204
|
+
-F, --force-refresh-cache
|
205
|
+
Force refresh of update information from PyPI
|
206
|
+
-X, --clear-version-cache
|
207
|
+
Clear cached version information
|
194
208
|
|
195
209
|
Logging Options:
|
196
|
-
--debug
|
197
|
-
--trace
|
198
|
-
--quiet
|
199
|
-
--silent
|
210
|
+
-d, --debug Enable debug logging
|
211
|
+
-T, --trace Enable trace logging (very verbose)
|
212
|
+
-q, --quiet Show only warnings and errors
|
213
|
+
-Q, --silent Show only errors
|
200
214
|
```
|
201
215
|
|
202
216
|
### Common Usage Examples
|
@@ -230,9 +244,9 @@ tonietoolbox file1.taf --compare file2.taf --detailed-compare
|
|
230
244
|
#### Custom timestamp options:
|
231
245
|
|
232
246
|
```
|
233
|
-
tonietoolbox input.mp3 --
|
234
|
-
tonietoolbox input.mp3 --
|
235
|
-
tonietoolbox input.mp3 --
|
247
|
+
tonietoolbox input.mp3 --timestamp 1745078762 # UNIX Timestamp
|
248
|
+
tonietoolbox input.mp3 --timestamp 0x6803C9EA # Bitstream time
|
249
|
+
tonietoolbox input.mp3 --timestamp ./reference.taf # Reference TAF for extraction
|
236
250
|
```
|
237
251
|
|
238
252
|
#### Set custom bitrate:
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|