TonieToolbox 0.4.2__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- TonieToolbox/__init__.py +1 -1
- TonieToolbox/__main__.py +269 -349
- TonieToolbox/artwork.py +105 -0
- TonieToolbox/audio_conversion.py +48 -5
- TonieToolbox/media_tags.py +5 -4
- TonieToolbox/recursive_processor.py +24 -19
- TonieToolbox/tags.py +74 -0
- TonieToolbox/teddycloud.py +250 -593
- TonieToolbox/tonie_analysis.py +173 -13
- TonieToolbox/tonie_file.py +17 -29
- TonieToolbox/tonies_json.py +1036 -170
- TonieToolbox/version_handler.py +26 -22
- {tonietoolbox-0.4.2.dist-info → tonietoolbox-0.5.0.dist-info}/METADATA +147 -99
- tonietoolbox-0.5.0.dist-info/RECORD +26 -0
- {tonietoolbox-0.4.2.dist-info → tonietoolbox-0.5.0.dist-info}/WHEEL +1 -1
- tonietoolbox-0.4.2.dist-info/RECORD +0 -24
- {tonietoolbox-0.4.2.dist-info → tonietoolbox-0.5.0.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.4.2.dist-info → tonietoolbox-0.5.0.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.4.2.dist-info → tonietoolbox-0.5.0.dist-info}/top_level.txt +0 -0
TonieToolbox/tonie_analysis.py
CHANGED
@@ -6,18 +6,11 @@ import datetime
|
|
6
6
|
import hashlib
|
7
7
|
import struct
|
8
8
|
import os
|
9
|
-
import difflib
|
10
|
-
from collections import defaultdict
|
11
|
-
|
12
9
|
from . import tonie_header_pb2
|
13
|
-
|
14
10
|
from .ogg_page import OggPage
|
15
11
|
from .logger import get_logger
|
16
12
|
|
17
|
-
# Setup logging
|
18
13
|
logger = get_logger('tonie_analysis')
|
19
|
-
|
20
|
-
|
21
14
|
def format_time(ts):
|
22
15
|
"""
|
23
16
|
Format a timestamp as a human-readable date and time string.
|
@@ -28,7 +21,7 @@ def format_time(ts):
|
|
28
21
|
Returns:
|
29
22
|
str: Formatted date and time string
|
30
23
|
"""
|
31
|
-
return datetime.datetime.
|
24
|
+
return datetime.datetime.fromtimestamp(ts, datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
|
32
25
|
|
33
26
|
|
34
27
|
def format_hex(data):
|
@@ -112,8 +105,6 @@ def get_header_info(in_file):
|
|
112
105
|
|
113
106
|
logger.debug("Opus header found: %s, Version: %d, Channels: %d, Sample rate: %d Hz, Serial: %d",
|
114
107
|
opus_head_found, opus_version, channel_count, sample_rate, bitstream_serial_no)
|
115
|
-
|
116
|
-
# Read and parse Opus comments from the second page
|
117
108
|
opus_comments = {}
|
118
109
|
found = OggPage.seek_to_page_header(in_file)
|
119
110
|
if not found:
|
@@ -124,7 +115,6 @@ def get_header_info(in_file):
|
|
124
115
|
logger.debug("Read second OGG page")
|
125
116
|
|
126
117
|
try:
|
127
|
-
# Combine all segments data for the second page
|
128
118
|
comment_data = bytearray()
|
129
119
|
for segment in second_page.segments:
|
130
120
|
comment_data.extend(segment.data)
|
@@ -461,7 +451,6 @@ def compare_taf_files(file1, file2, detailed=False):
|
|
461
451
|
|
462
452
|
differences = []
|
463
453
|
|
464
|
-
# Compare headers and extract key information
|
465
454
|
with open(file1, "rb") as f1, open(file2, "rb") as f2:
|
466
455
|
# Read and compare header sizes
|
467
456
|
header_size1 = struct.unpack(">L", f1.read(4))[0]
|
@@ -581,4 +570,175 @@ def compare_taf_files(file1, file2, detailed=False):
|
|
581
570
|
else:
|
582
571
|
print("\nFiles are equivalent")
|
583
572
|
logger.info("Files comparison result: Equivalent")
|
584
|
-
return True
|
573
|
+
return True
|
574
|
+
|
575
|
+
def get_header_info_cli(in_file):
|
576
|
+
"""
|
577
|
+
Get header information from a Tonie file.
|
578
|
+
|
579
|
+
Args:
|
580
|
+
in_file: Input file handle
|
581
|
+
|
582
|
+
Returns:
|
583
|
+
tuple: Header size, Tonie header object, file size, audio size, SHA1 sum,
|
584
|
+
Opus header found flag, Opus version, channel count, sample rate, bitstream serial number,
|
585
|
+
Opus comments dictionary, valid flag
|
586
|
+
|
587
|
+
Note:
|
588
|
+
Instead of raising exceptions, this function returns default values and a valid flag
|
589
|
+
"""
|
590
|
+
logger.debug("Reading Tonie header information")
|
591
|
+
|
592
|
+
try:
|
593
|
+
tonie_header = tonie_header_pb2.TonieHeader()
|
594
|
+
header_size = struct.unpack(">L", in_file.read(4))[0]
|
595
|
+
logger.debug("Header size: %d bytes", header_size)
|
596
|
+
|
597
|
+
tonie_header = tonie_header.FromString(in_file.read(header_size))
|
598
|
+
logger.debug("Read Tonie header with %d chapter pages", len(tonie_header.chapterPages))
|
599
|
+
|
600
|
+
sha1sum = hashlib.sha1(in_file.read())
|
601
|
+
logger.debug("Calculated SHA1: %s", sha1sum.hexdigest())
|
602
|
+
|
603
|
+
file_size = in_file.tell()
|
604
|
+
in_file.seek(4 + header_size)
|
605
|
+
audio_size = file_size - in_file.tell()
|
606
|
+
logger.debug("File size: %d bytes, Audio size: %d bytes", file_size, audio_size)
|
607
|
+
|
608
|
+
found = OggPage.seek_to_page_header(in_file)
|
609
|
+
if not found:
|
610
|
+
logger.error("First OGG page not found")
|
611
|
+
return (header_size, tonie_header, file_size, audio_size, sha1sum,
|
612
|
+
False, 0, 0, 0, 0, {}, False)
|
613
|
+
|
614
|
+
first_page = OggPage(in_file)
|
615
|
+
logger.debug("Read first OGG page")
|
616
|
+
|
617
|
+
unpacked = struct.unpack("<8sBBHLH", first_page.segments[0].data[0:18])
|
618
|
+
opus_head_found = unpacked[0] == b"OpusHead"
|
619
|
+
opus_version = unpacked[1]
|
620
|
+
channel_count = unpacked[2]
|
621
|
+
sample_rate = unpacked[4]
|
622
|
+
bitstream_serial_no = first_page.serial_no
|
623
|
+
|
624
|
+
logger.debug("Opus header found: %s, Version: %d, Channels: %d, Sample rate: %d Hz, Serial: %d",
|
625
|
+
opus_head_found, opus_version, channel_count, sample_rate, bitstream_serial_no)
|
626
|
+
opus_comments = {}
|
627
|
+
found = OggPage.seek_to_page_header(in_file)
|
628
|
+
if not found:
|
629
|
+
logger.error("Second OGG page not found")
|
630
|
+
return (header_size, tonie_header, file_size, audio_size, sha1sum,
|
631
|
+
opus_head_found, opus_version, channel_count, sample_rate, bitstream_serial_no, {}, False)
|
632
|
+
|
633
|
+
second_page = OggPage(in_file)
|
634
|
+
logger.debug("Read second OGG page")
|
635
|
+
|
636
|
+
try:
|
637
|
+
comment_data = bytearray()
|
638
|
+
for segment in second_page.segments:
|
639
|
+
comment_data.extend(segment.data)
|
640
|
+
|
641
|
+
if comment_data.startswith(b"OpusTags"):
|
642
|
+
pos = 8 # Skip "OpusTags"
|
643
|
+
# Extract vendor string
|
644
|
+
if pos + 4 <= len(comment_data):
|
645
|
+
vendor_length = struct.unpack("<I", comment_data[pos:pos+4])[0]
|
646
|
+
pos += 4
|
647
|
+
if pos + vendor_length <= len(comment_data):
|
648
|
+
vendor = comment_data[pos:pos+vendor_length].decode('utf-8', errors='replace')
|
649
|
+
opus_comments["vendor"] = vendor
|
650
|
+
pos += vendor_length
|
651
|
+
|
652
|
+
# Extract comments count
|
653
|
+
if pos + 4 <= len(comment_data):
|
654
|
+
comments_count = struct.unpack("<I", comment_data[pos:pos+4])[0]
|
655
|
+
pos += 4
|
656
|
+
|
657
|
+
# Extract individual comments
|
658
|
+
for i in range(comments_count):
|
659
|
+
if pos + 4 <= len(comment_data):
|
660
|
+
comment_length = struct.unpack("<I", comment_data[pos:pos+4])[0]
|
661
|
+
pos += 4
|
662
|
+
if pos + comment_length <= len(comment_data):
|
663
|
+
comment = comment_data[pos:pos+comment_length].decode('utf-8', errors='replace')
|
664
|
+
pos += comment_length
|
665
|
+
|
666
|
+
# Split comment into key/value if possible
|
667
|
+
if "=" in comment:
|
668
|
+
key, value = comment.split("=", 1)
|
669
|
+
opus_comments[key] = value
|
670
|
+
else:
|
671
|
+
opus_comments[f"comment_{i}"] = comment
|
672
|
+
else:
|
673
|
+
break
|
674
|
+
else:
|
675
|
+
break
|
676
|
+
except Exception as e:
|
677
|
+
logger.error("Failed to parse Opus comments: %s", str(e))
|
678
|
+
|
679
|
+
return (
|
680
|
+
header_size, tonie_header, file_size, audio_size, sha1sum,
|
681
|
+
opus_head_found, opus_version, channel_count, sample_rate, bitstream_serial_no,
|
682
|
+
opus_comments, True
|
683
|
+
)
|
684
|
+
except Exception as e:
|
685
|
+
logger.error("Error processing Tonie file: %s", str(e))
|
686
|
+
# Return default values with valid=False
|
687
|
+
return (0, tonie_header_pb2.TonieHeader(), 0, 0, None, False, 0, 0, 0, 0, {}, False)
|
688
|
+
|
689
|
+
|
690
|
+
def check_tonie_file_cli(filename):
|
691
|
+
"""
|
692
|
+
Check if a file is a valid Tonie file
|
693
|
+
|
694
|
+
Args:
|
695
|
+
filename: Path to the file to check
|
696
|
+
|
697
|
+
Returns:
|
698
|
+
bool: True if the file is valid, False otherwise
|
699
|
+
"""
|
700
|
+
logger.info("Checking Tonie file: %s", filename)
|
701
|
+
|
702
|
+
try:
|
703
|
+
with open(filename, "rb") as in_file:
|
704
|
+
header_size, tonie_header, file_size, audio_size, sha1, opus_head_found, \
|
705
|
+
opus_version, channel_count, sample_rate, bitstream_serial_no, opus_comments, valid = get_header_info_cli(in_file)
|
706
|
+
|
707
|
+
if not valid:
|
708
|
+
logger.error("Invalid Tonie file: %s", filename)
|
709
|
+
return False
|
710
|
+
|
711
|
+
try:
|
712
|
+
page_count, alignment_okay, page_size_okay, total_time, \
|
713
|
+
chapters = get_audio_info(in_file, sample_rate, tonie_header, header_size)
|
714
|
+
except Exception as e:
|
715
|
+
logger.error("Error analyzing audio data: %s", str(e))
|
716
|
+
return False
|
717
|
+
|
718
|
+
hash_ok = tonie_header.dataHash == sha1.digest()
|
719
|
+
timestamp_ok = tonie_header.timestamp == bitstream_serial_no
|
720
|
+
audio_size_ok = tonie_header.dataLength == audio_size
|
721
|
+
opus_ok = opus_head_found and \
|
722
|
+
opus_version == 1 and \
|
723
|
+
(sample_rate == 48000 or sample_rate == 44100) and \
|
724
|
+
channel_count == 2
|
725
|
+
|
726
|
+
all_ok = hash_ok and \
|
727
|
+
timestamp_ok and \
|
728
|
+
opus_ok and \
|
729
|
+
alignment_okay and \
|
730
|
+
page_size_okay
|
731
|
+
|
732
|
+
logger.debug("Validation results:")
|
733
|
+
logger.debug(" Hash OK: %s", hash_ok)
|
734
|
+
logger.debug(" Timestamp OK: %s", timestamp_ok)
|
735
|
+
logger.debug(" Audio size OK: %s", audio_size_ok)
|
736
|
+
logger.debug(" Opus OK: %s", opus_ok)
|
737
|
+
logger.debug(" Alignment OK: %s", alignment_okay)
|
738
|
+
logger.debug(" Page size OK: %s", page_size_okay)
|
739
|
+
logger.debug(" All OK: %s", all_ok)
|
740
|
+
|
741
|
+
return all_ok
|
742
|
+
except Exception as e:
|
743
|
+
logger.error("Error checking Tonie file: %s", str(e))
|
744
|
+
return False
|
TonieToolbox/tonie_file.py
CHANGED
@@ -15,7 +15,6 @@ from .ogg_page import OggPage
|
|
15
15
|
from .constants import OPUS_TAGS, SAMPLE_RATE_KHZ, TIMESTAMP_DEDUCT
|
16
16
|
from .logger import get_logger
|
17
17
|
|
18
|
-
# Setup logging
|
19
18
|
logger = get_logger('tonie_file')
|
20
19
|
|
21
20
|
|
@@ -38,7 +37,6 @@ def toniefile_comment_add(buffer, length, comment_str):
|
|
38
37
|
buffer[length:length+4] = struct.pack("<I", str_length)
|
39
38
|
length += 4
|
40
39
|
|
41
|
-
# Add the actual string
|
42
40
|
buffer[length:length+str_length] = comment_str.encode('utf-8')
|
43
41
|
length += str_length
|
44
42
|
|
@@ -115,49 +113,38 @@ def prepare_opus_tags(page, custom_tags=False, bitrate=64, vbr=True, opus_binary
|
|
115
113
|
# Use custom tags for TonieToolbox
|
116
114
|
# Create buffer for opus tags (similar to teddyCloud implementation)
|
117
115
|
logger.debug("Creating custom Opus tags")
|
118
|
-
comment_data = bytearray(0x1B4)
|
119
|
-
|
120
|
-
# OpusTags signature
|
116
|
+
comment_data = bytearray(0x1B4)
|
121
117
|
comment_data_pos = 0
|
122
118
|
comment_data[comment_data_pos:comment_data_pos+8] = b"OpusTags"
|
123
|
-
comment_data_pos += 8
|
124
|
-
|
119
|
+
comment_data_pos += 8
|
125
120
|
# Vendor string
|
126
|
-
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, "TonieToolbox")
|
127
|
-
|
121
|
+
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, "TonieToolbox")
|
128
122
|
# Number of comments (3 comments: version, encoder info, and encoder options)
|
129
123
|
comments_count = 3
|
130
124
|
comment_data[comment_data_pos:comment_data_pos+4] = struct.pack("<I", comments_count)
|
131
|
-
comment_data_pos += 4
|
132
|
-
|
125
|
+
comment_data_pos += 4
|
133
126
|
# Add version information
|
134
127
|
from . import __version__
|
135
128
|
version_str = f"version={__version__}"
|
136
|
-
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, version_str)
|
137
|
-
|
129
|
+
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, version_str)
|
138
130
|
# Get actual opusenc version
|
139
131
|
from .dependency_manager import get_opus_version
|
140
132
|
encoder_info = get_opus_version(opus_binary)
|
141
|
-
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, f"encoder={encoder_info}")
|
142
|
-
|
133
|
+
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, f"encoder={encoder_info}")
|
143
134
|
# Create encoder options string with actual settings
|
144
135
|
vbr_opt = "--vbr" if vbr else "--cbr"
|
145
136
|
encoder_options = f"encoder_options=--bitrate {bitrate} {vbr_opt}"
|
146
|
-
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, encoder_options)
|
147
|
-
|
137
|
+
comment_data_pos = toniefile_comment_add(comment_data, comment_data_pos, encoder_options)
|
148
138
|
# Add padding
|
149
139
|
remain = len(comment_data) - comment_data_pos - 4
|
150
140
|
comment_data[comment_data_pos:comment_data_pos+4] = struct.pack("<I", remain)
|
151
141
|
comment_data_pos += 4
|
152
|
-
comment_data[comment_data_pos:comment_data_pos+4] = b"pad="
|
153
|
-
|
142
|
+
comment_data[comment_data_pos:comment_data_pos+4] = b"pad="
|
154
143
|
# 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
|
-
|
144
|
+
comment_data = comment_data[:comment_data_pos + remain] # Trim to actual used size
|
157
145
|
# Split large data into smaller segments (each <= 255 bytes)
|
158
146
|
remaining_data = comment_data
|
159
|
-
first_segment = True
|
160
|
-
|
147
|
+
first_segment = True
|
161
148
|
while remaining_data:
|
162
149
|
chunk_size = min(255, len(remaining_data))
|
163
150
|
segment = OpusPacket(None)
|
@@ -377,7 +364,7 @@ def fix_tonie_header(out_file, chapters, timestamp, sha):
|
|
377
364
|
|
378
365
|
def create_tonie_file(output_file, input_files, no_tonie_header=False, user_timestamp=None,
|
379
366
|
bitrate=96, vbr=True, ffmpeg_binary=None, opus_binary=None, keep_temp=False, auto_download=False,
|
380
|
-
use_custom_tags=True):
|
367
|
+
use_custom_tags=True, no_mono_conversion=False):
|
381
368
|
"""
|
382
369
|
Create a Tonie file from input files.
|
383
370
|
|
@@ -393,13 +380,14 @@ def create_tonie_file(output_file, input_files, no_tonie_header=False, user_time
|
|
393
380
|
keep_temp: Whether to keep temporary opus files for testing
|
394
381
|
auto_download: Whether to automatically download dependencies if not found
|
395
382
|
use_custom_tags: Whether to use dynamic comment tags generated with toniefile_comment_add
|
383
|
+
no_mono_conversion: Whether to skip mono conversion during audio processing
|
396
384
|
"""
|
397
385
|
from .audio_conversion import get_opus_tempfile
|
398
386
|
|
399
387
|
logger.trace("Entering create_tonie_file(output_file=%s, input_files=%s, no_tonie_header=%s, user_timestamp=%s, "
|
400
|
-
"bitrate=%d, vbr=%s, ffmpeg_binary=%s, opus_binary=%s, keep_temp=%s, auto_download=%s, use_custom_tags=%s)",
|
388
|
+
"bitrate=%d, vbr=%s, ffmpeg_binary=%s, opus_binary=%s, keep_temp=%s, auto_download=%s, use_custom_tags=%s, no_mono_conversion=%s)",
|
401
389
|
output_file, input_files, no_tonie_header, user_timestamp, bitrate, vbr, ffmpeg_binary,
|
402
|
-
opus_binary, keep_temp, auto_download, use_custom_tags)
|
390
|
+
opus_binary, keep_temp, auto_download, use_custom_tags, no_mono_conversion)
|
403
391
|
|
404
392
|
logger.info("Creating Tonie file from %d input files", len(input_files))
|
405
393
|
logger.debug("Output file: %s, Bitrate: %d kbps, VBR: %s, No header: %s",
|
@@ -465,9 +453,9 @@ def create_tonie_file(output_file, input_files, no_tonie_header=False, user_time
|
|
465
453
|
handle = open(fname, "rb")
|
466
454
|
temp_file_path = None
|
467
455
|
else:
|
468
|
-
logger.debug("Converting %s to Opus format (bitrate: %d kbps, VBR: %s)",
|
469
|
-
fname, bitrate, vbr)
|
470
|
-
handle, temp_file_path = get_opus_tempfile(ffmpeg_binary, opus_binary, fname, bitrate, vbr, keep_temp, auto_download)
|
456
|
+
logger.debug("Converting %s to Opus format (bitrate: %d kbps, VBR: %s, no_mono_conversion: %s)",
|
457
|
+
fname, bitrate, vbr, no_mono_conversion)
|
458
|
+
handle, temp_file_path = get_opus_tempfile(ffmpeg_binary, opus_binary, fname, bitrate, vbr, keep_temp, auto_download, no_mono_conversion=no_mono_conversion)
|
471
459
|
if temp_file_path:
|
472
460
|
temp_files.append(temp_file_path)
|
473
461
|
logger.debug("Temporary opus file saved to: %s", temp_file_path)
|