TonieToolbox 0.1.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 +5 -0
- TonieToolbox/__main__.py +145 -0
- TonieToolbox/audio_conversion.py +194 -0
- TonieToolbox/constants.py +14 -0
- TonieToolbox/dependency_manager.py +378 -0
- TonieToolbox/filename_generator.py +94 -0
- TonieToolbox/logger.py +57 -0
- TonieToolbox/ogg_page.py +588 -0
- TonieToolbox/opus_packet.py +219 -0
- TonieToolbox/tonie_analysis.py +522 -0
- TonieToolbox/tonie_file.py +411 -0
- TonieToolbox/tonie_header.proto +11 -0
- TonieToolbox/tonie_header_pb2.py +99 -0
- tonietoolbox-0.1.0.dist-info/METADATA +301 -0
- tonietoolbox-0.1.0.dist-info/RECORD +19 -0
- tonietoolbox-0.1.0.dist-info/WHEEL +5 -0
- tonietoolbox-0.1.0.dist-info/entry_points.txt +2 -0
- tonietoolbox-0.1.0.dist-info/licenses/LICENSE.md +674 -0
- tonietoolbox-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,522 @@
|
|
1
|
+
"""
|
2
|
+
Functions for analyzing Tonie files
|
3
|
+
"""
|
4
|
+
|
5
|
+
import datetime
|
6
|
+
import hashlib
|
7
|
+
import struct
|
8
|
+
import os
|
9
|
+
import difflib
|
10
|
+
from collections import defaultdict
|
11
|
+
|
12
|
+
from . import tonie_header_pb2
|
13
|
+
|
14
|
+
from .ogg_page import OggPage
|
15
|
+
from .logger import get_logger
|
16
|
+
|
17
|
+
# Setup logging
|
18
|
+
logger = get_logger('tonie_analysis')
|
19
|
+
|
20
|
+
|
21
|
+
def format_time(ts):
|
22
|
+
"""
|
23
|
+
Format a timestamp as a human-readable date and time string.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
ts: Timestamp to format
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
str: Formatted date and time string
|
30
|
+
"""
|
31
|
+
return datetime.datetime.utcfromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S')
|
32
|
+
|
33
|
+
|
34
|
+
def format_hex(data):
|
35
|
+
"""
|
36
|
+
Format binary data as a hex string.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
data: Binary data to format
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
str: Formatted hex string
|
43
|
+
"""
|
44
|
+
return "".join(format(x, "02X") for x in data)
|
45
|
+
|
46
|
+
|
47
|
+
def granule_to_time_string(granule, sample_rate=1):
|
48
|
+
"""
|
49
|
+
Convert a granule position to a time string.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
granule: Granule position
|
53
|
+
sample_rate: Sample rate in Hz
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
str: Formatted time string (HH:MM:SS.FF)
|
57
|
+
"""
|
58
|
+
total_seconds = granule / sample_rate
|
59
|
+
hours = int(total_seconds / 3600)
|
60
|
+
minutes = int((total_seconds - (hours * 3600)) / 60)
|
61
|
+
seconds = int(total_seconds - (hours * 3600) - (minutes * 60))
|
62
|
+
fraction = int((total_seconds * 100) % 100)
|
63
|
+
return "{:02d}:{:02d}:{:02d}.{:02d}".format(hours, minutes, seconds, fraction)
|
64
|
+
|
65
|
+
|
66
|
+
def get_header_info(in_file):
|
67
|
+
"""
|
68
|
+
Get header information from a Tonie file.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
in_file: Input file handle
|
72
|
+
|
73
|
+
Returns:
|
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
|
76
|
+
|
77
|
+
Raises:
|
78
|
+
RuntimeError: If OGG pages cannot be found
|
79
|
+
"""
|
80
|
+
logger.debug("Reading Tonie header information")
|
81
|
+
|
82
|
+
tonie_header = tonie_header_pb2.TonieHeader()
|
83
|
+
header_size = struct.unpack(">L", in_file.read(4))[0]
|
84
|
+
logger.debug("Header size: %d bytes", header_size)
|
85
|
+
|
86
|
+
tonie_header = tonie_header.FromString(in_file.read(header_size))
|
87
|
+
logger.debug("Read Tonie header with %d chapter pages", len(tonie_header.chapterPages))
|
88
|
+
|
89
|
+
sha1sum = hashlib.sha1(in_file.read())
|
90
|
+
logger.debug("Calculated SHA1: %s", sha1sum.hexdigest())
|
91
|
+
|
92
|
+
file_size = in_file.tell()
|
93
|
+
in_file.seek(4 + header_size)
|
94
|
+
audio_size = file_size - in_file.tell()
|
95
|
+
logger.debug("File size: %d bytes, Audio size: %d bytes", file_size, audio_size)
|
96
|
+
|
97
|
+
found = OggPage.seek_to_page_header(in_file)
|
98
|
+
if not found:
|
99
|
+
logger.error("First OGG page not found")
|
100
|
+
raise RuntimeError("First ogg page not found")
|
101
|
+
|
102
|
+
first_page = OggPage(in_file)
|
103
|
+
logger.debug("Read first OGG page")
|
104
|
+
|
105
|
+
unpacked = struct.unpack("<8sBBHLH", first_page.segments[0].data[0:18])
|
106
|
+
opus_head_found = unpacked[0] == b"OpusHead"
|
107
|
+
opus_version = unpacked[1]
|
108
|
+
channel_count = unpacked[2]
|
109
|
+
sample_rate = unpacked[4]
|
110
|
+
bitstream_serial_no = first_page.serial_no
|
111
|
+
|
112
|
+
logger.debug("Opus header found: %s, Version: %d, Channels: %d, Sample rate: %d Hz, Serial: %d",
|
113
|
+
opus_head_found, opus_version, channel_count, sample_rate, bitstream_serial_no)
|
114
|
+
|
115
|
+
found = OggPage.seek_to_page_header(in_file)
|
116
|
+
if not found:
|
117
|
+
logger.error("Second OGG page not found")
|
118
|
+
raise RuntimeError("Second ogg page not found")
|
119
|
+
|
120
|
+
OggPage(in_file)
|
121
|
+
logger.debug("Read second OGG page")
|
122
|
+
|
123
|
+
return (
|
124
|
+
header_size, tonie_header, file_size, audio_size, sha1sum,
|
125
|
+
opus_head_found, opus_version, channel_count, sample_rate, bitstream_serial_no
|
126
|
+
)
|
127
|
+
|
128
|
+
|
129
|
+
def get_audio_info(in_file, sample_rate, tonie_header, header_size):
|
130
|
+
"""
|
131
|
+
Get audio information from a Tonie file.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
in_file: Input file handle
|
135
|
+
sample_rate: Sample rate in Hz
|
136
|
+
tonie_header: Tonie header object
|
137
|
+
header_size: Header size in bytes
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
tuple: Page count, alignment OK flag, page size OK flag, total time, chapter times
|
141
|
+
"""
|
142
|
+
logger.debug("Reading audio information")
|
143
|
+
|
144
|
+
chapter_granules = []
|
145
|
+
if 0 in tonie_header.chapterPages:
|
146
|
+
chapter_granules.append(0)
|
147
|
+
logger.trace("Added chapter at granule position 0")
|
148
|
+
|
149
|
+
alignment_okay = in_file.tell() == (512 + 4 + header_size)
|
150
|
+
logger.debug("Initial alignment OK: %s (position: %d)", alignment_okay, in_file.tell())
|
151
|
+
|
152
|
+
page_size_okay = True
|
153
|
+
page_count = 2
|
154
|
+
|
155
|
+
page = None
|
156
|
+
found = OggPage.seek_to_page_header(in_file)
|
157
|
+
while found:
|
158
|
+
page_count = page_count + 1
|
159
|
+
page = OggPage(in_file)
|
160
|
+
logger.trace("Read page #%d with granule position %d", page.page_no, page.granule_position)
|
161
|
+
|
162
|
+
found = OggPage.seek_to_page_header(in_file)
|
163
|
+
if found and in_file.tell() % 0x1000 != 0:
|
164
|
+
alignment_okay = False
|
165
|
+
logger.debug("Page alignment not OK at position %d", in_file.tell())
|
166
|
+
|
167
|
+
if page_size_okay and page_count > 3 and page.get_page_size() != 0x1000 and found:
|
168
|
+
page_size_okay = False
|
169
|
+
logger.debug("Page size not OK for page #%d (size: %d)", page.page_no, page.get_page_size())
|
170
|
+
|
171
|
+
if page.page_no in tonie_header.chapterPages:
|
172
|
+
chapter_granules.append(page.granule_position)
|
173
|
+
logger.trace("Added chapter at granule position %d from page #%d",
|
174
|
+
page.granule_position, page.page_no)
|
175
|
+
|
176
|
+
chapter_granules.append(page.granule_position)
|
177
|
+
logger.debug("Found %d chapters", len(chapter_granules) - 1)
|
178
|
+
|
179
|
+
chapter_times = []
|
180
|
+
for i in range(1, len(chapter_granules)):
|
181
|
+
length = chapter_granules[i] - chapter_granules[i - 1]
|
182
|
+
time_str = granule_to_time_string(length, sample_rate)
|
183
|
+
chapter_times.append(time_str)
|
184
|
+
logger.debug("Chapter %d duration: %s", i, time_str)
|
185
|
+
|
186
|
+
total_time = page.granule_position / sample_rate
|
187
|
+
logger.debug("Total time: %f seconds (%s)", total_time,
|
188
|
+
granule_to_time_string(page.granule_position, sample_rate))
|
189
|
+
|
190
|
+
return page_count, alignment_okay, page_size_okay, total_time, chapter_times
|
191
|
+
|
192
|
+
|
193
|
+
def check_tonie_file(filename):
|
194
|
+
"""
|
195
|
+
Check if a file is a valid Tonie file and display information about it.
|
196
|
+
|
197
|
+
Args:
|
198
|
+
filename: Path to the file to check
|
199
|
+
|
200
|
+
Returns:
|
201
|
+
bool: True if the file is valid, False otherwise
|
202
|
+
"""
|
203
|
+
logger.info("Checking Tonie file: %s", filename)
|
204
|
+
|
205
|
+
with open(filename, "rb") as in_file:
|
206
|
+
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)
|
208
|
+
|
209
|
+
page_count, alignment_okay, page_size_okay, total_time, \
|
210
|
+
chapters = get_audio_info(in_file, sample_rate, tonie_header, header_size)
|
211
|
+
|
212
|
+
hash_ok = tonie_header.dataHash == sha1.digest()
|
213
|
+
timestamp_ok = tonie_header.timestamp == bitstream_serial_no
|
214
|
+
audio_size_ok = tonie_header.dataLength == audio_size
|
215
|
+
opus_ok = opus_head_found and \
|
216
|
+
opus_version == 1 and \
|
217
|
+
(sample_rate == 48000 or sample_rate == 44100) and \
|
218
|
+
channel_count == 2
|
219
|
+
|
220
|
+
all_ok = hash_ok and \
|
221
|
+
timestamp_ok and \
|
222
|
+
opus_ok and \
|
223
|
+
alignment_okay and \
|
224
|
+
page_size_okay
|
225
|
+
|
226
|
+
logger.debug("Validation results:")
|
227
|
+
logger.debug(" Hash OK: %s", hash_ok)
|
228
|
+
logger.debug(" Timestamp OK: %s", timestamp_ok)
|
229
|
+
logger.debug(" Audio size OK: %s", audio_size_ok)
|
230
|
+
logger.debug(" Opus OK: %s", opus_ok)
|
231
|
+
logger.debug(" Alignment OK: %s", alignment_okay)
|
232
|
+
logger.debug(" Page size OK: %s", page_size_okay)
|
233
|
+
logger.debug(" All OK: %s", all_ok)
|
234
|
+
|
235
|
+
print("[{}] SHA1 hash: 0x{}".format("OK" if hash_ok else "NOT OK", format_hex(tonie_header.dataHash)))
|
236
|
+
if not hash_ok:
|
237
|
+
print(" actual: 0x{}".format(sha1.hexdigest().upper()))
|
238
|
+
print("[{}] Timestamp: [0x{:X}] {}".format("OK" if timestamp_ok else "NOT OK", tonie_header.timestamp,
|
239
|
+
format_time(tonie_header.timestamp)))
|
240
|
+
if not timestamp_ok:
|
241
|
+
print(" bitstream serial: 0x{:X}".format(bitstream_serial_no))
|
242
|
+
print("[{}] Opus data length: {} bytes (~{:2.0f} kbps)".format("OK" if audio_size_ok else "NOT OK",
|
243
|
+
tonie_header.dataLength,
|
244
|
+
(audio_size * 8) / 1024 / total_time))
|
245
|
+
if not audio_size_ok:
|
246
|
+
print(" actual: {} bytes".format(audio_size))
|
247
|
+
|
248
|
+
print("[{}] Opus header {}OK || {} channels || {:2.1f} kHz || {} Ogg pages"
|
249
|
+
.format("OK" if opus_ok else "NOT OK", "" if opus_head_found and opus_version == 1 else "NOT ",
|
250
|
+
channel_count, sample_rate / 1000, page_count))
|
251
|
+
print("[{}] Page alignment {}OK and size {}OK"
|
252
|
+
.format("OK" if alignment_okay and page_size_okay else "NOT OK", "" if alignment_okay else "NOT ",
|
253
|
+
"" if page_size_okay else "NOT "))
|
254
|
+
print("")
|
255
|
+
print("[{}] File is {}valid".format("OK" if all_ok else "NOT OK", "" if all_ok else "NOT "))
|
256
|
+
print("")
|
257
|
+
print("[ii] Total runtime: {}".format(granule_to_time_string(total_time)))
|
258
|
+
print("[ii] {} Tracks:".format(len(chapters)))
|
259
|
+
for i in range(0, len(chapters)):
|
260
|
+
print(" Track {:02d}: {}".format(i + 1, chapters[i]))
|
261
|
+
|
262
|
+
logger.info("File validation complete. Result: %s", "Valid" if all_ok else "Invalid")
|
263
|
+
return all_ok
|
264
|
+
|
265
|
+
|
266
|
+
def split_to_opus_files(filename, output=None):
|
267
|
+
"""
|
268
|
+
Split a Tonie file into individual Opus files.
|
269
|
+
|
270
|
+
Args:
|
271
|
+
filename: Path to the Tonie file
|
272
|
+
output: Output directory path (optional)
|
273
|
+
"""
|
274
|
+
logger.info("Splitting Tonie file into individual Opus tracks: %s", filename)
|
275
|
+
|
276
|
+
with open(filename, "rb") as in_file:
|
277
|
+
tonie_header = tonie_header_pb2.TonieHeader()
|
278
|
+
header_size = struct.unpack(">L", in_file.read(4))[0]
|
279
|
+
logger.debug("Header size: %d bytes", header_size)
|
280
|
+
|
281
|
+
tonie_header = tonie_header.FromString(in_file.read(header_size))
|
282
|
+
logger.debug("Read Tonie header with %d chapter pages", len(tonie_header.chapterPages))
|
283
|
+
|
284
|
+
abs_path = os.path.abspath(filename)
|
285
|
+
if output:
|
286
|
+
if not os.path.exists(output):
|
287
|
+
logger.debug("Creating output directory: %s", output)
|
288
|
+
os.makedirs(output)
|
289
|
+
path = output
|
290
|
+
else:
|
291
|
+
path = os.path.dirname(abs_path)
|
292
|
+
|
293
|
+
logger.debug("Output path: %s", path)
|
294
|
+
|
295
|
+
name = os.path.basename(abs_path)
|
296
|
+
pos = name.rfind('.')
|
297
|
+
if pos == -1:
|
298
|
+
name = name + ".opus"
|
299
|
+
else:
|
300
|
+
name = name[:pos] + ".opus"
|
301
|
+
|
302
|
+
filename_template = "{{:02d}}_{}".format(name)
|
303
|
+
out_path = "{}{}".format(path, os.path.sep)
|
304
|
+
logger.debug("Output filename template: %s", out_path + filename_template)
|
305
|
+
|
306
|
+
found = OggPage.seek_to_page_header(in_file)
|
307
|
+
if not found:
|
308
|
+
logger.error("First OGG page not found")
|
309
|
+
raise RuntimeError("First ogg page not found")
|
310
|
+
|
311
|
+
first_page = OggPage(in_file)
|
312
|
+
logger.debug("Read first OGG page")
|
313
|
+
|
314
|
+
found = OggPage.seek_to_page_header(in_file)
|
315
|
+
if not found:
|
316
|
+
logger.error("Second OGG page not found")
|
317
|
+
raise RuntimeError("Second ogg page not found")
|
318
|
+
|
319
|
+
second_page = OggPage(in_file)
|
320
|
+
logger.debug("Read second OGG page")
|
321
|
+
|
322
|
+
found = OggPage.seek_to_page_header(in_file)
|
323
|
+
page = OggPage(in_file)
|
324
|
+
logger.debug("Read third OGG page")
|
325
|
+
|
326
|
+
import math
|
327
|
+
|
328
|
+
pad_len = math.ceil(math.log(len(tonie_header.chapterPages) + 1, 10))
|
329
|
+
format_string = "[{{:0{}d}}/{:0{}d}] {{}}".format(pad_len, len(tonie_header.chapterPages), pad_len)
|
330
|
+
|
331
|
+
for i in range(0, len(tonie_header.chapterPages)):
|
332
|
+
if (i + 1) < len(tonie_header.chapterPages):
|
333
|
+
end_page = tonie_header.chapterPages[i + 1]
|
334
|
+
else:
|
335
|
+
end_page = 0
|
336
|
+
|
337
|
+
granule = 0
|
338
|
+
output_filename = filename_template.format(i + 1)
|
339
|
+
print(format_string.format(i + 1, output_filename))
|
340
|
+
logger.info("Creating track %d: %s (end page: %d)", i + 1, out_path + output_filename, end_page)
|
341
|
+
|
342
|
+
with open("{}{}".format(out_path, output_filename), "wb") as out_file:
|
343
|
+
first_page.write_page(out_file)
|
344
|
+
second_page.write_page(out_file)
|
345
|
+
page_count = 0
|
346
|
+
|
347
|
+
while found and ((page.page_no < end_page) or (end_page == 0)):
|
348
|
+
page.correct_values(granule)
|
349
|
+
granule = page.granule_position
|
350
|
+
page.write_page(out_file)
|
351
|
+
page_count += 1
|
352
|
+
|
353
|
+
found = OggPage.seek_to_page_header(in_file)
|
354
|
+
if found:
|
355
|
+
page = OggPage(in_file)
|
356
|
+
|
357
|
+
logger.debug("Track %d: Wrote %d pages, final granule position: %d",
|
358
|
+
i + 1, page_count, granule)
|
359
|
+
|
360
|
+
logger.info("Successfully split Tonie file into %d individual tracks", len(tonie_header.chapterPages))
|
361
|
+
|
362
|
+
|
363
|
+
def compare_taf_files(file1, file2, detailed=False):
|
364
|
+
"""
|
365
|
+
Compare two .taf files for debugging purposes.
|
366
|
+
|
367
|
+
Args:
|
368
|
+
file1: Path to the first .taf file
|
369
|
+
file2: Path to the second .taf file
|
370
|
+
detailed: Whether to show detailed comparison results
|
371
|
+
|
372
|
+
Returns:
|
373
|
+
bool: True if files are equivalent, False otherwise
|
374
|
+
"""
|
375
|
+
logger.info("Comparing .taf files:")
|
376
|
+
logger.info(" File 1: %s", file1)
|
377
|
+
logger.info(" File 2: %s", file2)
|
378
|
+
|
379
|
+
if not os.path.exists(file1):
|
380
|
+
logger.error("File 1 does not exist: %s", file1)
|
381
|
+
return False
|
382
|
+
|
383
|
+
if not os.path.exists(file2):
|
384
|
+
logger.error("File 2 does not exist: %s", file2)
|
385
|
+
return False
|
386
|
+
|
387
|
+
# Compare file sizes
|
388
|
+
size1 = os.path.getsize(file1)
|
389
|
+
size2 = os.path.getsize(file2)
|
390
|
+
|
391
|
+
if size1 != size2:
|
392
|
+
logger.info("Files have different sizes: %d vs %d bytes", size1, size2)
|
393
|
+
print("Files have different sizes:")
|
394
|
+
print(f" File 1: {size1} bytes")
|
395
|
+
print(f" File 2: {size2} bytes")
|
396
|
+
else:
|
397
|
+
logger.info("Files have the same size: %d bytes", size1)
|
398
|
+
print(f"Files have the same size: {size1} bytes")
|
399
|
+
|
400
|
+
differences = []
|
401
|
+
|
402
|
+
# Compare headers and extract key information
|
403
|
+
with open(file1, "rb") as f1, open(file2, "rb") as f2:
|
404
|
+
# Read and compare header sizes
|
405
|
+
header_size1 = struct.unpack(">L", f1.read(4))[0]
|
406
|
+
header_size2 = struct.unpack(">L", f2.read(4))[0]
|
407
|
+
|
408
|
+
if header_size1 != header_size2:
|
409
|
+
differences.append(f"Header sizes differ: {header_size1} vs {header_size2} bytes")
|
410
|
+
logger.info("Header sizes differ: %d vs %d bytes", header_size1, header_size2)
|
411
|
+
|
412
|
+
# Read and parse headers
|
413
|
+
tonie_header1 = tonie_header_pb2.TonieHeader()
|
414
|
+
tonie_header2 = tonie_header_pb2.TonieHeader()
|
415
|
+
|
416
|
+
tonie_header1 = tonie_header1.FromString(f1.read(header_size1))
|
417
|
+
tonie_header2 = tonie_header2.FromString(f2.read(header_size2))
|
418
|
+
|
419
|
+
# Compare timestamps
|
420
|
+
if tonie_header1.timestamp != tonie_header2.timestamp:
|
421
|
+
differences.append(f"Timestamps differ: {tonie_header1.timestamp} vs {tonie_header2.timestamp}")
|
422
|
+
logger.info("Timestamps differ: %d vs %d", tonie_header1.timestamp, tonie_header2.timestamp)
|
423
|
+
|
424
|
+
# Compare data lengths
|
425
|
+
if tonie_header1.dataLength != tonie_header2.dataLength:
|
426
|
+
differences.append(f"Data lengths differ: {tonie_header1.dataLength} vs {tonie_header2.dataLength} bytes")
|
427
|
+
logger.info("Data lengths differ: %d vs %d bytes", tonie_header1.dataLength, tonie_header2.dataLength)
|
428
|
+
|
429
|
+
# Compare data hashes
|
430
|
+
hash1_hex = format_hex(tonie_header1.dataHash)
|
431
|
+
hash2_hex = format_hex(tonie_header2.dataHash)
|
432
|
+
if tonie_header1.dataHash != tonie_header2.dataHash:
|
433
|
+
differences.append(f"Data hashes differ: 0x{hash1_hex} vs 0x{hash2_hex}")
|
434
|
+
logger.info("Data hashes differ: 0x%s vs 0x%s", hash1_hex, hash2_hex)
|
435
|
+
|
436
|
+
# Compare chapter pages
|
437
|
+
ch1 = list(tonie_header1.chapterPages)
|
438
|
+
ch2 = list(tonie_header2.chapterPages)
|
439
|
+
|
440
|
+
if ch1 != ch2:
|
441
|
+
differences.append(f"Chapter pages differ: {ch1} vs {ch2}")
|
442
|
+
logger.info("Chapter pages differ: %s vs %s", ch1, ch2)
|
443
|
+
|
444
|
+
if len(ch1) != len(ch2):
|
445
|
+
differences.append(f"Number of chapters differ: {len(ch1)} vs {len(ch2)}")
|
446
|
+
logger.info("Number of chapters differ: %d vs %d", len(ch1), len(ch2))
|
447
|
+
|
448
|
+
# Compare audio content
|
449
|
+
# Reset file positions to after headers
|
450
|
+
f1.seek(4 + header_size1)
|
451
|
+
f2.seek(4 + header_size2)
|
452
|
+
|
453
|
+
# Compare Ogg pages
|
454
|
+
ogg_differences = []
|
455
|
+
page_count = 0
|
456
|
+
|
457
|
+
# Find first Ogg page in each file
|
458
|
+
found1 = OggPage.seek_to_page_header(f1)
|
459
|
+
found2 = OggPage.seek_to_page_header(f2)
|
460
|
+
|
461
|
+
if not found1 or not found2:
|
462
|
+
if not found1:
|
463
|
+
differences.append("First file: First OGG page not found")
|
464
|
+
if not found2:
|
465
|
+
differences.append("Second file: First OGG page not found")
|
466
|
+
else:
|
467
|
+
# Compare Ogg pages
|
468
|
+
while found1 and found2:
|
469
|
+
page_count += 1
|
470
|
+
page1 = OggPage(f1)
|
471
|
+
page2 = OggPage(f2)
|
472
|
+
|
473
|
+
# Compare key page attributes
|
474
|
+
if page1.serial_no != page2.serial_no:
|
475
|
+
ogg_differences.append(f"Page {page_count}: Serial numbers differ: {page1.serial_no} vs {page2.serial_no}")
|
476
|
+
|
477
|
+
if page1.page_no != page2.page_no:
|
478
|
+
ogg_differences.append(f"Page {page_count}: Page numbers differ: {page1.page_no} vs {page2.page_no}")
|
479
|
+
|
480
|
+
if page1.granule_position != page2.granule_position:
|
481
|
+
ogg_differences.append(f"Page {page_count}: Granule positions differ: {page1.granule_position} vs {page2.granule_position}")
|
482
|
+
|
483
|
+
if page1.get_page_size() != page2.get_page_size():
|
484
|
+
ogg_differences.append(f"Page {page_count}: Page sizes differ: {page1.get_page_size()} vs {page2.get_page_size()}")
|
485
|
+
|
486
|
+
# Check for more pages
|
487
|
+
found1 = OggPage.seek_to_page_header(f1)
|
488
|
+
found2 = OggPage.seek_to_page_header(f2)
|
489
|
+
|
490
|
+
# Check if one file has more pages than the other
|
491
|
+
if found1 and not found2:
|
492
|
+
extra_pages1 = 1
|
493
|
+
while OggPage.seek_to_page_header(f1):
|
494
|
+
OggPage(f1)
|
495
|
+
extra_pages1 += 1
|
496
|
+
ogg_differences.append(f"File 1 has {extra_pages1} more pages than File 2")
|
497
|
+
|
498
|
+
elif found2 and not found1:
|
499
|
+
extra_pages2 = 1
|
500
|
+
while OggPage.seek_to_page_header(f2):
|
501
|
+
OggPage(f2)
|
502
|
+
extra_pages2 += 1
|
503
|
+
ogg_differences.append(f"File 2 has {extra_pages2} more pages than File 1")
|
504
|
+
|
505
|
+
# Add Ogg differences to main differences list if detailed flag is set
|
506
|
+
if detailed and ogg_differences:
|
507
|
+
differences.extend(ogg_differences)
|
508
|
+
elif ogg_differences:
|
509
|
+
differences.append(f"Found {len(ogg_differences)} differences in Ogg pages")
|
510
|
+
logger.info("Found %d differences in Ogg pages", len(ogg_differences))
|
511
|
+
|
512
|
+
# Print summary
|
513
|
+
if differences:
|
514
|
+
print("\nFiles are different:")
|
515
|
+
for diff in differences:
|
516
|
+
print(f" - {diff}")
|
517
|
+
logger.info("Files comparison result: Different (%d differences found)", len(differences))
|
518
|
+
return False
|
519
|
+
else:
|
520
|
+
print("\nFiles are equivalent")
|
521
|
+
logger.info("Files comparison result: Equivalent")
|
522
|
+
return True
|