TonieToolbox 0.4.1__py3-none-any.whl → 0.5.0a1__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 +227 -324
- TonieToolbox/artwork.py +105 -0
- TonieToolbox/recursive_processor.py +14 -8
- TonieToolbox/tags.py +74 -0
- TonieToolbox/teddycloud.py +250 -593
- TonieToolbox/tonie_analysis.py +173 -13
- TonieToolbox/tonies_json.py +251 -174
- TonieToolbox/version_handler.py +26 -22
- {tonietoolbox-0.4.1.dist-info → tonietoolbox-0.5.0a1.dist-info}/METADATA +7 -2
- tonietoolbox-0.5.0a1.dist-info/RECORD +26 -0
- {tonietoolbox-0.4.1.dist-info → tonietoolbox-0.5.0a1.dist-info}/WHEEL +1 -1
- tonietoolbox-0.4.1.dist-info/RECORD +0 -24
- {tonietoolbox-0.4.1.dist-info → tonietoolbox-0.5.0a1.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.4.1.dist-info → tonietoolbox-0.5.0a1.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.4.1.dist-info → tonietoolbox-0.5.0a1.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
|