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.
@@ -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
@@ -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) # Same size as in teddyCloud
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)