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.
@@ -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.utcfromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')
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