TonieToolbox 0.5.0a1__py3-none-any.whl → 0.6.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 +2 -1
- TonieToolbox/__main__.py +303 -141
- TonieToolbox/artwork.py +59 -10
- TonieToolbox/audio_conversion.py +106 -34
- TonieToolbox/constants.py +133 -10
- TonieToolbox/dependency_manager.py +679 -184
- TonieToolbox/filename_generator.py +57 -10
- TonieToolbox/integration.py +73 -0
- TonieToolbox/integration_macos.py +613 -0
- TonieToolbox/integration_ubuntu.py +2 -0
- TonieToolbox/integration_windows.py +445 -0
- TonieToolbox/logger.py +9 -10
- TonieToolbox/media_tags.py +24 -104
- TonieToolbox/ogg_page.py +41 -41
- TonieToolbox/opus_packet.py +15 -15
- TonieToolbox/recursive_processor.py +34 -34
- TonieToolbox/tags.py +4 -5
- TonieToolbox/teddycloud.py +164 -51
- TonieToolbox/tonie_analysis.py +26 -24
- TonieToolbox/tonie_file.py +88 -72
- TonieToolbox/tonies_json.py +830 -37
- TonieToolbox/version_handler.py +14 -20
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0.dist-info}/METADATA +257 -177
- tonietoolbox-0.6.0.dist-info/RECORD +30 -0
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0.dist-info}/WHEEL +1 -1
- tonietoolbox-0.5.0a1.dist-info/RECORD +0 -26
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.5.0a1.dist-info → tonietoolbox-0.6.0.dist-info}/top_level.txt +0 -0
TonieToolbox/media_tags.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
"""
|
2
3
|
Media tag processing functionality for the TonieToolbox package
|
3
4
|
|
@@ -13,8 +14,9 @@ import base64
|
|
13
14
|
from mutagen.flac import Picture
|
14
15
|
from .logger import get_logger
|
15
16
|
from .dependency_manager import is_mutagen_available, ensure_mutagen
|
17
|
+
from .constants import ARTWORK_NAMES, ARTWORK_EXTENSIONS, TAG_VALUE_REPLACEMENTS, TAG_MAPPINGS
|
18
|
+
logger = get_logger(__name__)
|
16
19
|
|
17
|
-
# Global variables to track dependency state and store module references
|
18
20
|
MUTAGEN_AVAILABLE = False
|
19
21
|
mutagen = None
|
20
22
|
ID3 = None
|
@@ -53,85 +55,9 @@ def _import_mutagen():
|
|
53
55
|
MUTAGEN_AVAILABLE = False
|
54
56
|
return False
|
55
57
|
|
56
|
-
# Try to import mutagen if it's available
|
57
58
|
if is_mutagen_available():
|
58
59
|
_import_mutagen()
|
59
60
|
|
60
|
-
logger = get_logger('media_tags')
|
61
|
-
|
62
|
-
# Define tag mapping for different formats to standardized names
|
63
|
-
# This helps normalize tags across different audio formats
|
64
|
-
TAG_MAPPING = {
|
65
|
-
# ID3 (MP3) tags
|
66
|
-
'TIT2': 'title',
|
67
|
-
'TALB': 'album',
|
68
|
-
'TPE1': 'artist',
|
69
|
-
'TPE2': 'albumartist',
|
70
|
-
'TCOM': 'composer',
|
71
|
-
'TRCK': 'tracknumber',
|
72
|
-
'TPOS': 'discnumber',
|
73
|
-
'TDRC': 'date',
|
74
|
-
'TCON': 'genre',
|
75
|
-
'TPUB': 'publisher',
|
76
|
-
'TCOP': 'copyright',
|
77
|
-
'COMM': 'comment',
|
78
|
-
|
79
|
-
# Vorbis tags (FLAC, OGG)
|
80
|
-
'title': 'title',
|
81
|
-
'album': 'album',
|
82
|
-
'artist': 'artist',
|
83
|
-
'albumartist': 'albumartist',
|
84
|
-
'composer': 'composer',
|
85
|
-
'tracknumber': 'tracknumber',
|
86
|
-
'discnumber': 'discnumber',
|
87
|
-
'date': 'date',
|
88
|
-
'genre': 'genre',
|
89
|
-
'publisher': 'publisher',
|
90
|
-
'copyright': 'copyright',
|
91
|
-
'comment': 'comment',
|
92
|
-
|
93
|
-
# MP4 (M4A, AAC) tags
|
94
|
-
'©nam': 'title',
|
95
|
-
'©alb': 'album',
|
96
|
-
'©ART': 'artist',
|
97
|
-
'aART': 'albumartist',
|
98
|
-
'©wrt': 'composer',
|
99
|
-
'trkn': 'tracknumber',
|
100
|
-
'disk': 'discnumber',
|
101
|
-
'©day': 'date',
|
102
|
-
'©gen': 'genre',
|
103
|
-
'©pub': 'publisher',
|
104
|
-
'cprt': 'copyright',
|
105
|
-
'©cmt': 'comment',
|
106
|
-
|
107
|
-
# Additional tags some files might have
|
108
|
-
'album_artist': 'albumartist',
|
109
|
-
'track': 'tracknumber',
|
110
|
-
'track_number': 'tracknumber',
|
111
|
-
'disc': 'discnumber',
|
112
|
-
'disc_number': 'discnumber',
|
113
|
-
'year': 'date',
|
114
|
-
'albuminterpret': 'albumartist', # German tag name
|
115
|
-
'interpret': 'artist', # German tag name
|
116
|
-
}
|
117
|
-
|
118
|
-
# Define replacements for special tag values
|
119
|
-
TAG_VALUE_REPLACEMENTS = {
|
120
|
-
"Die drei ???": "Die drei Fragezeichen",
|
121
|
-
"Die Drei ???": "Die drei Fragezeichen",
|
122
|
-
"DIE DREI ???": "Die drei Fragezeichen",
|
123
|
-
"Die drei !!!": "Die drei Ausrufezeichen",
|
124
|
-
"Die Drei !!!": "Die drei Ausrufezeichen",
|
125
|
-
"DIE DREI !!!": "Die drei Ausrufezeichen",
|
126
|
-
"TKKG™": "TKKG",
|
127
|
-
"Die drei ??? Kids": "Die drei Fragezeichen Kids",
|
128
|
-
"Die Drei ??? Kids": "Die drei Fragezeichen Kids",
|
129
|
-
"Bibi & Tina": "Bibi und Tina",
|
130
|
-
"Benjamin Blümchen™": "Benjamin Blümchen",
|
131
|
-
"???": "Fragezeichen",
|
132
|
-
"!!!": "Ausrufezeichen",
|
133
|
-
}
|
134
|
-
|
135
61
|
def normalize_tag_value(value: str) -> str:
|
136
62
|
"""
|
137
63
|
Normalize tag values by replacing special characters or known patterns
|
@@ -146,7 +72,6 @@ def normalize_tag_value(value: str) -> str:
|
|
146
72
|
if not value:
|
147
73
|
return value
|
148
74
|
|
149
|
-
# Check for direct replacements first
|
150
75
|
if value in TAG_VALUE_REPLACEMENTS:
|
151
76
|
logger.debug("Direct tag replacement: '%s' -> '%s'", value, TAG_VALUE_REPLACEMENTS[value])
|
152
77
|
return TAG_VALUE_REPLACEMENTS[value]
|
@@ -158,10 +83,7 @@ def normalize_tag_value(value: str) -> str:
|
|
158
83
|
original = result
|
159
84
|
result = result.replace(pattern, replacement)
|
160
85
|
logger.debug("Partial tag replacement: '%s' -> '%s'", original, result)
|
161
|
-
|
162
|
-
# Special case for "Die drei ???" type patterns that might have been missed
|
163
|
-
result = result.replace("???", "Fragezeichen")
|
164
|
-
|
86
|
+
|
165
87
|
return result
|
166
88
|
|
167
89
|
def is_available() -> bool:
|
@@ -213,60 +135,61 @@ def get_file_tags(file_path: str) -> Dict[str, Any]:
|
|
213
135
|
id3 = audio if isinstance(audio, ID3) else audio.ID3
|
214
136
|
for tag_key, tag_value in id3.items():
|
215
137
|
tag_name = tag_key.split(':')[0] # Handle ID3 tags with colons
|
216
|
-
if tag_name in
|
138
|
+
if tag_name in TAG_MAPPINGS:
|
217
139
|
tag_value_str = str(tag_value)
|
218
|
-
tags[
|
140
|
+
tags[TAG_MAPPINGS[tag_name]] = normalize_tag_value(tag_value_str)
|
219
141
|
except (AttributeError, TypeError) as e:
|
220
142
|
logger.debug("Error accessing ID3 tags: %s", e)
|
221
143
|
# Try alternative approach for ID3 tags
|
222
144
|
try:
|
223
145
|
if hasattr(audio, 'tags') and audio.tags:
|
224
146
|
for tag_key in audio.tags.keys():
|
225
|
-
if tag_key in
|
147
|
+
if tag_key in TAG_MAPPINGS:
|
226
148
|
tag_value = audio.tags[tag_key]
|
227
149
|
if hasattr(tag_value, 'text'):
|
228
150
|
tag_value_str = str(tag_value.text[0]) if tag_value.text else ''
|
229
151
|
else:
|
230
152
|
tag_value_str = str(tag_value)
|
231
|
-
tags[
|
153
|
+
tags[TAG_MAPPINGS[tag_key]] = normalize_tag_value(tag_value_str)
|
232
154
|
except Exception as e:
|
233
155
|
logger.debug("Alternative ID3 tag reading failed: %s", e)
|
234
156
|
elif isinstance(audio, (FLAC, OggOpus, OggVorbis)):
|
235
157
|
# FLAC and OGG files
|
236
158
|
for tag_key, tag_values in audio.items():
|
237
159
|
tag_key_lower = tag_key.lower()
|
238
|
-
if tag_key_lower in
|
160
|
+
if tag_key_lower in TAG_MAPPINGS:
|
239
161
|
# Some tags might have multiple values, we'll take the first one
|
240
162
|
tag_value = tag_values[0] if tag_values else ''
|
241
|
-
tags[
|
163
|
+
tags[TAG_MAPPINGS[tag_key_lower]] = normalize_tag_value(tag_value)
|
242
164
|
elif isinstance(audio, MP4):
|
243
165
|
# MP4 files
|
244
166
|
for tag_key, tag_value in audio.items():
|
245
|
-
if tag_key in
|
167
|
+
if tag_key in TAG_MAPPINGS:
|
246
168
|
if isinstance(tag_value, list):
|
247
169
|
if tag_key in ('trkn', 'disk'):
|
248
170
|
# Handle track and disc number tuples
|
249
171
|
if tag_value and isinstance(tag_value[0], tuple) and len(tag_value[0]) >= 1:
|
250
|
-
tags[
|
172
|
+
tags[TAG_MAPPINGS[tag_key]] = str(tag_value[0][0])
|
251
173
|
else:
|
252
174
|
tag_value_str = str(tag_value[0]) if tag_value else ''
|
253
|
-
tags[
|
175
|
+
tags[TAG_MAPPINGS[tag_key]] = normalize_tag_value(tag_value_str)
|
254
176
|
else:
|
255
177
|
tag_value_str = str(tag_value)
|
256
|
-
tags[
|
178
|
+
tags[TAG_MAPPINGS[tag_key]] = normalize_tag_value(tag_value_str)
|
257
179
|
else:
|
258
180
|
# Generic audio file - try to read any available tags
|
259
181
|
for tag_key, tag_value in audio.items():
|
260
182
|
tag_key_lower = tag_key.lower()
|
261
|
-
if tag_key_lower in
|
183
|
+
if tag_key_lower in TAG_MAPPINGS:
|
262
184
|
if isinstance(tag_value, list):
|
263
185
|
tag_value_str = str(tag_value[0]) if tag_value else ''
|
264
|
-
tags[
|
186
|
+
tags[TAG_MAPPINGS[tag_key_lower]] = normalize_tag_value(tag_value_str)
|
265
187
|
else:
|
266
188
|
tag_value_str = str(tag_value)
|
267
|
-
tags[
|
189
|
+
tags[TAG_MAPPINGS[tag_key_lower]] = normalize_tag_value(tag_value_str)
|
268
190
|
|
269
191
|
logger.debug("Successfully read %d tags from file", len(tags))
|
192
|
+
logger.debug("Tags: %s", str(tags))
|
270
193
|
return tags
|
271
194
|
except Exception as e:
|
272
195
|
logger.error("Error reading tags from file %s: %s", file_path, str(e))
|
@@ -330,12 +253,12 @@ def extract_album_info(folder_path: str) -> Dict[str, str]:
|
|
330
253
|
if not all_tags:
|
331
254
|
logger.debug("Could not read tags from any files in folder")
|
332
255
|
return {}
|
333
|
-
|
334
|
-
# Try to find consistent album information
|
335
256
|
result = {}
|
336
|
-
|
257
|
+
all_tag_names = set()
|
258
|
+
for tags in all_tags:
|
259
|
+
all_tag_names.update(tags.keys())
|
337
260
|
|
338
|
-
for tag_name in
|
261
|
+
for tag_name in all_tag_names:
|
339
262
|
# Count occurrences of each value
|
340
263
|
value_counts = {}
|
341
264
|
for tags in all_tags:
|
@@ -614,13 +537,10 @@ def find_cover_image(source_dir):
|
|
614
537
|
return None
|
615
538
|
|
616
539
|
# Common cover image file names
|
617
|
-
cover_names =
|
618
|
-
'cover', 'folder', 'album', 'front', 'artwork', 'image',
|
619
|
-
'albumart', 'albumartwork', 'booklet'
|
620
|
-
]
|
540
|
+
cover_names = ARTWORK_NAMES
|
621
541
|
|
622
542
|
# Common image extensions
|
623
|
-
image_extensions =
|
543
|
+
image_extensions = ARTWORK_EXTENSIONS
|
624
544
|
|
625
545
|
# Try different variations
|
626
546
|
for name in cover_names:
|
TonieToolbox/ogg_page.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
"""
|
2
3
|
Classes and functions for handling OGG container pages
|
3
4
|
"""
|
@@ -14,16 +15,15 @@ from .constants import (
|
|
14
15
|
)
|
15
16
|
from .logger import get_logger
|
16
17
|
|
17
|
-
|
18
|
-
logger = get_logger('ogg_page')
|
18
|
+
logger = get_logger(__name__)
|
19
19
|
|
20
20
|
|
21
|
-
def create_crc_table():
|
21
|
+
def create_crc_table() -> list[int]:
|
22
22
|
"""
|
23
23
|
Create a CRC lookup table for OGG page checksums.
|
24
24
|
|
25
25
|
Returns:
|
26
|
-
list: CRC32 lookup table for OGG pages
|
26
|
+
list[int]: CRC32 lookup table for OGG pages
|
27
27
|
"""
|
28
28
|
logger.debug("Creating CRC table for OGG page checksums")
|
29
29
|
table = []
|
@@ -39,12 +39,12 @@ def create_crc_table():
|
|
39
39
|
CRC_TABLE = create_crc_table()
|
40
40
|
|
41
41
|
|
42
|
-
def crc32(bytestream):
|
42
|
+
def crc32(bytestream: bytes) -> int:
|
43
43
|
"""
|
44
44
|
Calculate a CRC32 checksum for the given bytestream.
|
45
45
|
|
46
46
|
Args:
|
47
|
-
bytestream: Bytes to calculate the CRC for
|
47
|
+
bytestream (bytes): Bytes to calculate the CRC for
|
48
48
|
|
49
49
|
Returns:
|
50
50
|
int: CRC32 checksum
|
@@ -64,7 +64,7 @@ class OggPage:
|
|
64
64
|
with particular focus on features needed for Tonie compatibility.
|
65
65
|
"""
|
66
66
|
|
67
|
-
def __init__(self, filehandle):
|
67
|
+
def __init__(self, filehandle) -> None:
|
68
68
|
"""
|
69
69
|
Initialize a new OggPage.
|
70
70
|
|
@@ -88,7 +88,7 @@ class OggPage:
|
|
88
88
|
self.parse_header(filehandle)
|
89
89
|
self.parse_segments(filehandle)
|
90
90
|
|
91
|
-
def parse_header(self, filehandle):
|
91
|
+
def parse_header(self, filehandle) -> None:
|
92
92
|
"""
|
93
93
|
Parse the OGG page header.
|
94
94
|
|
@@ -108,7 +108,7 @@ class OggPage:
|
|
108
108
|
logger.trace("Parsed OGG header - Page #%d, Type: %d, Granule: %d, Serial: %d, Segments: %d",
|
109
109
|
self.page_no, self.page_type, self.granule_position, self.serial_no, self.segment_count)
|
110
110
|
|
111
|
-
def parse_segments(self, filehandle):
|
111
|
+
def parse_segments(self, filehandle) -> None:
|
112
112
|
"""
|
113
113
|
Parse the segments in this OGG page.
|
114
114
|
|
@@ -133,12 +133,12 @@ class OggPage:
|
|
133
133
|
logger.error("Found an opus packet spanning OGG pages, which is not supported")
|
134
134
|
raise RuntimeError("Found an opus packet spanning ogg pages. This is not supported yet.")
|
135
135
|
|
136
|
-
def correct_values(self, last_granule):
|
136
|
+
def correct_values(self, last_granule: int) -> None:
|
137
137
|
"""
|
138
138
|
Correct the granule position and checksum for this page.
|
139
139
|
|
140
140
|
Args:
|
141
|
-
last_granule: Last granule position
|
141
|
+
last_granule (int): Last granule position
|
142
142
|
|
143
143
|
Raises:
|
144
144
|
RuntimeError: If there are too many segments in the page
|
@@ -160,7 +160,7 @@ class OggPage:
|
|
160
160
|
logger.trace("Corrected OGG page values: Page #%d, Segments: %d, Granule: %d",
|
161
161
|
self.page_no, self.segment_count, self.granule_position)
|
162
162
|
|
163
|
-
def calc_checksum(self):
|
163
|
+
def calc_checksum(self) -> int:
|
164
164
|
"""
|
165
165
|
Calculate the checksum for this page.
|
166
166
|
|
@@ -178,7 +178,7 @@ class OggPage:
|
|
178
178
|
logger.trace("Calculated checksum for page #%d: 0x%X", self.page_no, checksum)
|
179
179
|
return checksum
|
180
180
|
|
181
|
-
def get_page_size(self):
|
181
|
+
def get_page_size(self) -> int:
|
182
182
|
"""
|
183
183
|
Get the total size of this page in bytes.
|
184
184
|
|
@@ -190,7 +190,7 @@ class OggPage:
|
|
190
190
|
size = size + len(segment.data)
|
191
191
|
return size
|
192
192
|
|
193
|
-
def get_size_of_first_opus_packet(self):
|
193
|
+
def get_size_of_first_opus_packet(self) -> int:
|
194
194
|
"""
|
195
195
|
Get the size of the first opus packet in bytes.
|
196
196
|
|
@@ -208,7 +208,7 @@ class OggPage:
|
|
208
208
|
i = i + 1
|
209
209
|
return size
|
210
210
|
|
211
|
-
def get_segment_count_of_first_opus_packet(self):
|
211
|
+
def get_segment_count_of_first_opus_packet(self) -> int:
|
212
212
|
"""
|
213
213
|
Get the number of segments in the first opus packet.
|
214
214
|
|
@@ -224,14 +224,14 @@ class OggPage:
|
|
224
224
|
count = count + 1
|
225
225
|
return count
|
226
226
|
|
227
|
-
def insert_empty_segment(self, index_after, spanning_packet=False, first_packet=False):
|
227
|
+
def insert_empty_segment(self, index_after: int, spanning_packet: bool = False, first_packet: bool = False) -> None:
|
228
228
|
"""
|
229
229
|
Insert an empty segment after the specified index.
|
230
230
|
|
231
231
|
Args:
|
232
|
-
index_after: Index to insert the segment after
|
233
|
-
spanning_packet: Whether this segment belongs to a packet that spans pages
|
234
|
-
first_packet: Whether this is the first segment of a packet
|
232
|
+
index_after (int): Index to insert the segment after
|
233
|
+
spanning_packet (bool): Whether this segment belongs to a packet that spans pages
|
234
|
+
first_packet (bool): Whether this is the first segment of a packet
|
235
235
|
"""
|
236
236
|
logger.trace("Inserting empty segment after index %d (spanning: %s, first: %s)",
|
237
237
|
index_after, spanning_packet, first_packet)
|
@@ -242,12 +242,12 @@ class OggPage:
|
|
242
242
|
segment.data = bytes()
|
243
243
|
self.segments.insert(index_after + 1, segment)
|
244
244
|
|
245
|
-
def get_opus_packet_size(self, seg_start):
|
245
|
+
def get_opus_packet_size(self, seg_start: int) -> int:
|
246
246
|
"""
|
247
247
|
Get the size of the opus packet starting at the specified segment index.
|
248
248
|
|
249
249
|
Args:
|
250
|
-
seg_start: Starting segment index
|
250
|
+
seg_start (int): Starting segment index
|
251
251
|
|
252
252
|
Returns:
|
253
253
|
int: Size of the opus packet in bytes
|
@@ -259,12 +259,12 @@ class OggPage:
|
|
259
259
|
seg_start = seg_start + 1
|
260
260
|
return size
|
261
261
|
|
262
|
-
def get_segment_count_of_packet_at(self, seg_start):
|
262
|
+
def get_segment_count_of_packet_at(self, seg_start: int) -> int:
|
263
263
|
"""
|
264
264
|
Get the number of segments in the packet starting at the specified segment index.
|
265
265
|
|
266
266
|
Args:
|
267
|
-
seg_start: Starting segment index
|
267
|
+
seg_start (int): Starting segment index
|
268
268
|
|
269
269
|
Returns:
|
270
270
|
int: Number of segments
|
@@ -274,13 +274,13 @@ class OggPage:
|
|
274
274
|
seg_end = seg_end + 1
|
275
275
|
return seg_end - seg_start
|
276
276
|
|
277
|
-
def redistribute_packet_data_at(self, seg_start, pad_count):
|
277
|
+
def redistribute_packet_data_at(self, seg_start: int, pad_count: int) -> None:
|
278
278
|
"""
|
279
279
|
Redistribute packet data starting at the specified segment index.
|
280
280
|
|
281
281
|
Args:
|
282
|
-
seg_start: Starting segment index
|
283
|
-
pad_count: Number of padding bytes to add
|
282
|
+
seg_start (int): Starting segment index
|
283
|
+
pad_count (int): Number of padding bytes to add
|
284
284
|
"""
|
285
285
|
logger.trace("Redistributing packet data at segment %d with %d padding bytes",
|
286
286
|
seg_start, pad_count)
|
@@ -316,14 +316,14 @@ class OggPage:
|
|
316
316
|
logger.trace("Redistribution complete, %d segments used", seg_count)
|
317
317
|
assert len(full_data) == 0
|
318
318
|
|
319
|
-
def convert_packet_to_framepacking_three_and_pad(self, seg_start, pad=False, count=0):
|
319
|
+
def convert_packet_to_framepacking_three_and_pad(self, seg_start: int, pad: bool = False, count: int = 0) -> None:
|
320
320
|
"""
|
321
321
|
Convert the packet to framepacking three mode and add padding if required.
|
322
322
|
|
323
323
|
Args:
|
324
|
-
seg_start: Starting segment index
|
325
|
-
pad: Whether to add padding
|
326
|
-
count: Number of padding bytes to add
|
324
|
+
seg_start (int): Starting segment index
|
325
|
+
pad (bool): Whether to add padding
|
326
|
+
count (int): Number of padding bytes to add
|
327
327
|
|
328
328
|
Raises:
|
329
329
|
AssertionError: If the segment is not the first packet
|
@@ -336,13 +336,13 @@ class OggPage:
|
|
336
336
|
self.segments[seg_start].set_pad_count(count)
|
337
337
|
self.redistribute_packet_data_at(seg_start, count)
|
338
338
|
|
339
|
-
def calc_actual_padding_value(self, seg_start, bytes_needed):
|
339
|
+
def calc_actual_padding_value(self, seg_start: int, bytes_needed: int) -> int:
|
340
340
|
"""
|
341
341
|
Calculate the actual padding value needed for the packet.
|
342
342
|
|
343
343
|
Args:
|
344
|
-
seg_start: Starting segment index
|
345
|
-
bytes_needed: Number of bytes needed for padding
|
344
|
+
seg_start (int): Starting segment index
|
345
|
+
bytes_needed (int): Number of bytes needed for padding
|
346
346
|
|
347
347
|
Returns:
|
348
348
|
int: Actual padding value or a special return code
|
@@ -426,13 +426,13 @@ class OggPage:
|
|
426
426
|
logger.trace("Calculated actual padding value: %d", result)
|
427
427
|
return result
|
428
428
|
|
429
|
-
def pad(self, pad_to, idx_offset
|
429
|
+
def pad(self, pad_to: int, idx_offset: int = -1) -> None:
|
430
430
|
"""
|
431
431
|
Pad the page to the specified size.
|
432
432
|
|
433
433
|
Args:
|
434
|
-
pad_to: Target size to pad to
|
435
|
-
idx_offset: Index offset to start from, defaults to last segment
|
434
|
+
pad_to (int): Target size to pad to
|
435
|
+
idx_offset (int): Index offset to start from, defaults to last segment
|
436
436
|
|
437
437
|
Raises:
|
438
438
|
RuntimeError: If beginning of last packet cannot be found
|
@@ -490,7 +490,7 @@ class OggPage:
|
|
490
490
|
final_size, pad_to)
|
491
491
|
assert final_size == pad_to
|
492
492
|
|
493
|
-
def pad_one_byte(self):
|
493
|
+
def pad_one_byte(self) -> None:
|
494
494
|
"""
|
495
495
|
Add one byte of padding to the page.
|
496
496
|
|
@@ -515,7 +515,7 @@ class OggPage:
|
|
515
515
|
logger.trace("Converting packet to framepacking 3")
|
516
516
|
self.convert_packet_to_framepacking_three_and_pad(i)
|
517
517
|
|
518
|
-
def write_page(self, filehandle, sha1=None):
|
518
|
+
def write_page(self, filehandle, sha1=None) -> None:
|
519
519
|
"""
|
520
520
|
Write the page to a file handle.
|
521
521
|
|
@@ -537,12 +537,12 @@ class OggPage:
|
|
537
537
|
segment.write(filehandle)
|
538
538
|
|
539
539
|
@staticmethod
|
540
|
-
def from_page(other_page):
|
540
|
+
def from_page(other_page: 'OggPage') -> 'OggPage':
|
541
541
|
"""
|
542
542
|
Create a new OggPage based on another page.
|
543
543
|
|
544
544
|
Args:
|
545
|
-
other_page: Source page to copy from
|
545
|
+
other_page (OggPage): Source page to copy from
|
546
546
|
|
547
547
|
Returns:
|
548
548
|
OggPage: New page with copied properties
|
@@ -560,7 +560,7 @@ class OggPage:
|
|
560
560
|
return new_page
|
561
561
|
|
562
562
|
@staticmethod
|
563
|
-
def seek_to_page_header(filehandle):
|
563
|
+
def seek_to_page_header(filehandle) -> bool:
|
564
564
|
"""
|
565
565
|
Seek to the next OGG page header in a file.
|
566
566
|
|
TonieToolbox/opus_packet.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/python3
|
1
2
|
"""
|
2
3
|
Classes and functions for handling Opus audio packets
|
3
4
|
"""
|
@@ -6,8 +7,7 @@ import struct
|
|
6
7
|
from .constants import SAMPLE_RATE_KHZ
|
7
8
|
from .logger import get_logger
|
8
9
|
|
9
|
-
|
10
|
-
logger = get_logger('opus_packet')
|
10
|
+
logger = get_logger(__name__)
|
11
11
|
|
12
12
|
|
13
13
|
class OpusPacket:
|
@@ -18,15 +18,15 @@ class OpusPacket:
|
|
18
18
|
with particular focus on features needed for Tonie compatibility.
|
19
19
|
"""
|
20
20
|
|
21
|
-
def __init__(self, filehandle, size
|
21
|
+
def __init__(self, filehandle, size: int = -1, last_size: int = -1, dont_parse_info: bool = False) -> None:
|
22
22
|
"""
|
23
23
|
Initialize a new OpusPacket.
|
24
24
|
|
25
25
|
Args:
|
26
26
|
filehandle: File handle to read the packet data from, or None to create an empty packet
|
27
|
-
size: Size of the packet in bytes
|
28
|
-
last_size: Size of the previous packet in bytes
|
29
|
-
dont_parse_info: If True, don't parse the packet information even if this is a first packet
|
27
|
+
size (int): Size of the packet in bytes
|
28
|
+
last_size (int): Size of the previous packet in bytes
|
29
|
+
dont_parse_info (bool): If True, don't parse the packet information even if this is a first packet
|
30
30
|
"""
|
31
31
|
self.config_value = None
|
32
32
|
self.stereo = None
|
@@ -51,7 +51,7 @@ class OpusPacket:
|
|
51
51
|
if self.first_packet and not dont_parse_info:
|
52
52
|
self.parse_segment_info()
|
53
53
|
|
54
|
-
def get_frame_count(self):
|
54
|
+
def get_frame_count(self) -> int:
|
55
55
|
"""
|
56
56
|
Get the number of frames in this packet based on its framepacking.
|
57
57
|
|
@@ -68,7 +68,7 @@ class OpusPacket:
|
|
68
68
|
unpacked = struct.unpack("<B", self.data[1:2])
|
69
69
|
return unpacked[0] & 63
|
70
70
|
|
71
|
-
def get_padding(self):
|
71
|
+
def get_padding(self) -> int:
|
72
72
|
"""
|
73
73
|
Get the padding count for this packet.
|
74
74
|
|
@@ -94,7 +94,7 @@ class OpusPacket:
|
|
94
94
|
logger.trace("Packet has %d bytes of padding", total_padding)
|
95
95
|
return total_padding
|
96
96
|
|
97
|
-
def get_frame_size(self):
|
97
|
+
def get_frame_size(self) -> float:
|
98
98
|
"""
|
99
99
|
Get the frame size in milliseconds based on the config value.
|
100
100
|
|
@@ -121,7 +121,7 @@ class OpusPacket:
|
|
121
121
|
logger.error(error_msg)
|
122
122
|
raise RuntimeError(error_msg)
|
123
123
|
|
124
|
-
def calc_granule(self):
|
124
|
+
def calc_granule(self) -> float:
|
125
125
|
"""
|
126
126
|
Calculate the granule position for this packet.
|
127
127
|
|
@@ -133,7 +133,7 @@ class OpusPacket:
|
|
133
133
|
granule, self.frame_size, self.frame_count)
|
134
134
|
return granule
|
135
135
|
|
136
|
-
def parse_segment_info(self):
|
136
|
+
def parse_segment_info(self) -> None:
|
137
137
|
"""Parse the segment information from the packet data."""
|
138
138
|
logger.trace("Parsing segment info from packet data")
|
139
139
|
byte = struct.unpack("<B", self.data[0:1])[0]
|
@@ -148,7 +148,7 @@ class OpusPacket:
|
|
148
148
|
logger.trace("Packet info: config=%d, stereo=%d, framepacking=%d, frame_count=%d, frame_size=%f",
|
149
149
|
self.config_value, self.stereo, self.framepacking, self.frame_count, self.frame_size)
|
150
150
|
|
151
|
-
def write(self, filehandle):
|
151
|
+
def write(self, filehandle) -> None:
|
152
152
|
"""
|
153
153
|
Write the packet data to a file.
|
154
154
|
|
@@ -159,7 +159,7 @@ class OpusPacket:
|
|
159
159
|
logger.trace("Writing %d bytes of packet data to file", len(self.data))
|
160
160
|
filehandle.write(self.data)
|
161
161
|
|
162
|
-
def convert_to_framepacking_three(self):
|
162
|
+
def convert_to_framepacking_three(self) -> None:
|
163
163
|
"""
|
164
164
|
Convert the packet to use framepacking mode 3.
|
165
165
|
|
@@ -182,12 +182,12 @@ class OpusPacket:
|
|
182
182
|
self.framepacking = 3
|
183
183
|
logger.debug("Packet successfully converted to framepacking mode 3")
|
184
184
|
|
185
|
-
def set_pad_count(self, count):
|
185
|
+
def set_pad_count(self, count: int) -> None:
|
186
186
|
"""
|
187
187
|
Set the padding count for this packet.
|
188
188
|
|
189
189
|
Args:
|
190
|
-
count: Number of padding bytes to add
|
190
|
+
count (int): Number of padding bytes to add
|
191
191
|
|
192
192
|
Raises:
|
193
193
|
AssertionError: If the packet is not using framepacking mode 3 or is already padded
|