eyeD3 0.9.8a1__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.
@@ -0,0 +1,443 @@
1
+ import os
2
+ import re
3
+ import math
4
+ import pathlib
5
+ import logging
6
+ import argparse
7
+ import warnings
8
+ import functools
9
+
10
+ import deprecation
11
+
12
+ from ..utils.log import getLogger
13
+ from .. import LOCAL_FS_ENCODING
14
+ from ..__about__ import __version__, __release_name__, __version_txt__
15
+
16
+ if hasattr(os, "fwalk"):
17
+ os_walk = functools.partial(os.fwalk, follow_symlinks=True)
18
+
19
+ def os_walk_unpack(w):
20
+ return w[0:3]
21
+
22
+ else:
23
+ os_walk = functools.partial(os.walk, followlinks=True)
24
+
25
+ def os_walk_unpack(w):
26
+ return w
27
+
28
+ log = getLogger(__name__)
29
+
30
+
31
+ @deprecation.deprecated(deprecated_in="0.9a2", removed_in="1.0", current_version=__version__,
32
+ details="Use eyed3.mimetype.guessMimetype() instead.")
33
+ def guessMimetype(filename, with_encoding=False):
34
+ from .. import mimetype
35
+
36
+ retval = mimetype.guessMimetype(filename)
37
+
38
+ if not with_encoding:
39
+ return retval
40
+ else:
41
+ warnings.warn("File character encoding no longer returned, value is None",
42
+ UserWarning, stacklevel=2)
43
+ return retval, None
44
+
45
+
46
+ def walk(handler, path, excludes=None, fs_encoding=LOCAL_FS_ENCODING, recursive=False):
47
+ """A wrapper around os.walk which handles exclusion patterns and multiple
48
+ path types (str, pathlib.Path, bytes).
49
+ """
50
+ if isinstance(path, pathlib.Path):
51
+ path = str(path)
52
+ else:
53
+ path = str(path, fs_encoding) if type(path) is not str else path
54
+
55
+ excludes = excludes if excludes else []
56
+ excludes_re = []
57
+ for e in excludes:
58
+ excludes_re.append(re.compile(e))
59
+
60
+ def _isExcluded(_p):
61
+ for ex in excludes_re:
62
+ match = ex.match(_p)
63
+ if match:
64
+ return True
65
+ return False
66
+
67
+ if not os.path.exists(path):
68
+ raise IOError(f"file not found: {path}")
69
+ elif os.path.isfile(path) and not _isExcluded(path):
70
+ # If not given a directory, invoke the handler and return
71
+ handler.handleFile(os.path.abspath(path))
72
+ return
73
+
74
+ for root, dirs, files in [os_walk_unpack(w) for w in os_walk(path)]:
75
+ root = root if type(root) is str else str(root, fs_encoding)
76
+ dirs.sort()
77
+ files.sort()
78
+ for f in list(files):
79
+ f_key = f
80
+ f = f if type(f) is str else str(f, fs_encoding)
81
+ f = os.path.abspath(os.path.join(root, f))
82
+
83
+ if not os.path.isfile(f) or _isExcluded(f):
84
+ files.remove(f_key)
85
+ continue
86
+
87
+ try:
88
+ handler.handleFile(f)
89
+ except StopIteration:
90
+ return
91
+
92
+ if files:
93
+ handler.handleDirectory(root, files)
94
+
95
+ if not recursive:
96
+ break
97
+
98
+
99
+ class FileHandler(object):
100
+ """A handler interface for :func:`eyed3.utils.walk` callbacks."""
101
+
102
+ def handleFile(self, f):
103
+ """Called for each file walked. The file ``f`` is the full path and
104
+ the return value is ignored. If the walk should abort the method should
105
+ raise a ``StopIteration`` exception."""
106
+ pass
107
+
108
+ def handleDirectory(self, d, files):
109
+ """Called for each directory ``d`` **after** ``handleFile`` has been
110
+ called for each file in ``files``. ``StopIteration`` may be raised to
111
+ halt iteration."""
112
+ pass
113
+
114
+ def handleDone(self):
115
+ """Called when there are no more files to handle."""
116
+ pass
117
+
118
+
119
+ def _requireArgType(arg_type, *args):
120
+ arg_indices = []
121
+ kwarg_names = []
122
+ for a in args:
123
+ if type(a) is int:
124
+ arg_indices.append(a)
125
+ else:
126
+ kwarg_names.append(a)
127
+ assert arg_indices or kwarg_names
128
+
129
+ def wrapper(fn):
130
+ def wrapped_fn(*args, **kwargs):
131
+ for i in arg_indices:
132
+ if i >= len(args):
133
+ # The ith argument is not there, as in optional arguments
134
+ break
135
+ if args[i] is not None and not isinstance(args[i], arg_type):
136
+ raise TypeError("%s(argument %d) must be %s" %
137
+ (fn.__name__, i, str(arg_type)))
138
+ for name in kwarg_names:
139
+ if (name in kwargs and kwargs[name] is not None and
140
+ not isinstance(kwargs[name], arg_type)):
141
+ raise TypeError("%s(argument %s) must be %s" %
142
+ (fn.__name__, name, str(arg_type)))
143
+ return fn(*args, **kwargs)
144
+ return wrapped_fn
145
+ return wrapper
146
+
147
+
148
+ def requireUnicode(*args):
149
+ """Function decorator to enforce str/unicode argument types.
150
+ ``None`` is a valid argument value, in all cases, regardless of not being
151
+ unicode. ``*args`` Positional arguments may be numeric argument index
152
+ values (requireUnicode(1, 3) - requires argument 1 and 3 are unicode)
153
+ or keyword argument names (requireUnicode("title")) or a combination
154
+ thereof.
155
+ """
156
+ return _requireArgType(str, *args)
157
+
158
+
159
+ def requireBytes(*args):
160
+ """Function decorator to enforce byte string argument types.
161
+ """
162
+ return _requireArgType(bytes, *args)
163
+
164
+
165
+ def formatTime(seconds, total=None, short=False):
166
+ """
167
+ Format ``seconds`` (number of seconds) as a string representation.
168
+ When ``short`` is False (the default) the format is:
169
+
170
+ HH:MM:SS.
171
+
172
+ Otherwise, the format is exacly 6 characters long and of the form:
173
+
174
+ 1w 3d
175
+ 2d 4h
176
+ 1h 5m
177
+ 1m 4s
178
+ 15s
179
+
180
+ If ``total`` is not None it will also be formatted and
181
+ appended to the result seperated by ' / '.
182
+ """
183
+ seconds = round(seconds)
184
+
185
+ def time_tuple(ts):
186
+ if ts is None or ts < 0:
187
+ ts = 0
188
+ hours = ts / 3600
189
+ mins = (ts % 3600) / 60
190
+ secs = (ts % 3600) % 60
191
+ tstr = '%02d:%02d' % (mins, secs)
192
+ if int(hours):
193
+ tstr = '%02d:%s' % (hours, tstr)
194
+ return (int(hours), int(mins), int(secs), tstr)
195
+
196
+ if not short:
197
+ hours, mins, secs, curr_str = time_tuple(seconds)
198
+ retval = curr_str
199
+ if total:
200
+ hours, mins, secs, total_str = time_tuple(total)
201
+ retval += ' / %s' % total_str
202
+ return retval
203
+ else:
204
+ units = [
205
+ ('y', 60 * 60 * 24 * 7 * 52),
206
+ ('w', 60 * 60 * 24 * 7),
207
+ ('d', 60 * 60 * 24),
208
+ ('h', 60 * 60),
209
+ ('m', 60),
210
+ ('s', 1),
211
+ ]
212
+
213
+ seconds = int(seconds)
214
+
215
+ if seconds < 60:
216
+ return ' {0:02d}s'.format(seconds)
217
+ for i in range(len(units) - 1):
218
+ unit1, limit1 = units[i]
219
+ unit2, limit2 = units[i + 1]
220
+ if seconds >= limit1:
221
+ return '{0:02d}{1}{2:02d}{3}'.format(
222
+ seconds // limit1, unit1,
223
+ (seconds % limit1) // limit2, unit2)
224
+ return ' ~inf'
225
+
226
+
227
+ # Number of bytes per KB (2^10)
228
+ KB_BYTES = 1024
229
+ # Number of bytes per MB (2^20)
230
+ MB_BYTES = 1048576
231
+ # Number of bytes per GB (2^30)
232
+ GB_BYTES = 1073741824
233
+ # Kilobytes abbreviation
234
+ KB_UNIT = "KB"
235
+ # Megabytes abbreviation
236
+ MB_UNIT = "MB"
237
+ # Gigabytes abbreviation
238
+ GB_UNIT = "GB"
239
+
240
+
241
+ def formatSize(size, short=False):
242
+ """Format ``size`` (number of bytes) into string format doing KB, MB, or GB
243
+ conversion where necessary.
244
+
245
+ When ``short`` is False (the default) the format is smallest unit of
246
+ bytes and largest gigabytes; '234 GB'.
247
+ The short version is 2-4 characters long and of the form
248
+
249
+ 256b
250
+ 64k
251
+ 1.1G
252
+ """
253
+ if not short:
254
+ unit = "Bytes"
255
+ if size >= GB_BYTES:
256
+ size = float(size) / float(GB_BYTES)
257
+ unit = GB_UNIT
258
+ elif size >= MB_BYTES:
259
+ size = float(size) / float(MB_BYTES)
260
+ unit = MB_UNIT
261
+ elif size >= KB_BYTES:
262
+ size = float(size) / float(KB_BYTES)
263
+ unit = KB_UNIT
264
+ return "%.2f %s" % (size, unit)
265
+ else:
266
+ suffixes = ' kMGTPEH'
267
+ if size == 0:
268
+ num_scale = 0
269
+ else:
270
+ num_scale = int(math.floor(math.log(size) / math.log(1000)))
271
+ if num_scale > 7:
272
+ suffix = '?'
273
+ else:
274
+ suffix = suffixes[num_scale]
275
+ num_scale = int(math.pow(1000, num_scale))
276
+ value = size / num_scale
277
+ str_value = str(value)
278
+ if len(str_value) >= 3 and str_value[2] == '.':
279
+ str_value = str_value[:2]
280
+ else:
281
+ str_value = str_value[:3]
282
+ return "{0:>3s}{1}".format(str_value, suffix)
283
+
284
+
285
+ def formatTimeDelta(td):
286
+ """Format a timedelta object ``td`` into a string. """
287
+ days = td.days
288
+ hours = td.seconds / 3600
289
+ mins = (td.seconds % 3600) / 60
290
+ secs = (td.seconds % 3600) % 60
291
+ tstr = "%02d:%02d:%02d" % (hours, mins, secs)
292
+ if days:
293
+ tstr = "%d days %s" % (days, tstr)
294
+ return tstr
295
+
296
+
297
+ def chunkCopy(src_fp, dest_fp, chunk_sz=(1024 * 512)):
298
+ """Copy ``src_fp`` to ``dest_fp`` in ``chunk_sz`` byte increments."""
299
+ done = False
300
+ while not done:
301
+ data = src_fp.read(chunk_sz)
302
+ if data:
303
+ dest_fp.write(data)
304
+ else:
305
+ done = True
306
+ del data
307
+
308
+
309
+ class ArgumentParser(argparse.ArgumentParser):
310
+ """Subclass of argparse.ArgumentParser that adds version and log level
311
+ options."""
312
+
313
+ def __init__(self, *args, **kwargs):
314
+ from eyed3 import version as VERSION
315
+ from eyed3.utils.log import LEVELS
316
+ from eyed3.utils.log import MAIN_LOGGER
317
+
318
+ def pop_kwarg(name, default):
319
+ if name in kwargs:
320
+ value = kwargs.pop(name) or default
321
+ else:
322
+ value = default
323
+ return value
324
+ main_logger = pop_kwarg("main_logger", MAIN_LOGGER)
325
+ version = pop_kwarg("version", VERSION)
326
+
327
+ self.log_levels = [logging.getLevelName(l).lower() for l in LEVELS]
328
+
329
+ formatter = argparse.RawDescriptionHelpFormatter
330
+ super(ArgumentParser, self).__init__(*args, formatter_class=formatter,
331
+ **kwargs)
332
+
333
+ self.add_argument("--version", action="version", version=version,
334
+ help="Display version information and exit")
335
+ self.add_argument("--about", action="store_true", dest="about_eyed3",
336
+ help="Display full version, release name, additional info, and exit")
337
+
338
+ debug_group = self.add_argument_group("Debugging")
339
+ debug_group.add_argument(
340
+ "-l", "--log-level", metavar="LEVEL[:LOGGER]",
341
+ action=LoggingAction, main_logger=main_logger,
342
+ help="Set a log level. This option may be specified multiple "
343
+ "times. If a logger name is specified than the level "
344
+ "applies only to that logger, otherwise the level is set "
345
+ "on the top-level logger. Acceptable levels are %s. " %
346
+ (", ".join("'%s'" % l for l in self.log_levels)))
347
+ debug_group.add_argument("--profile", action="store_true",
348
+ default=False, dest="debug_profile",
349
+ help="Run using python profiler.")
350
+ debug_group.add_argument("--pdb", action="store_true", dest="debug_pdb",
351
+ help="Drop into 'pdb' when errors occur.")
352
+
353
+ def parse_args(self, *args, **kwargs):
354
+ args = super().parse_args(*args, **kwargs)
355
+ if "about_eyed3" in args and args.about_eyed3:
356
+ action = [a for a in self._actions if isinstance(a, argparse._VersionAction)][0]
357
+ version = action.version
358
+ release_name = f" {__release_name__}" if __release_name__ else ""
359
+ print(f"{version}{release_name}\n\n{__version_txt__}")
360
+ self.exit()
361
+ else:
362
+ return args
363
+
364
+
365
+ class LoggingAction(argparse._AppendAction):
366
+ def __init__(self, *args, **kwargs):
367
+ self.main_logger = kwargs.pop("main_logger")
368
+ super(LoggingAction, self).__init__(*args, **kwargs)
369
+
370
+ def __call__(self, parser, namespace, values, option_string=None):
371
+ values = values.split(':')
372
+ level, logger = values if len(values) > 1 else (values[0],
373
+ self.main_logger)
374
+
375
+ logger = logging.getLogger(logger)
376
+ try:
377
+ logger.setLevel(logging._nameToLevel[level.upper()])
378
+ except KeyError:
379
+ msg = f"invalid level choice: {level} (choose from {parser.log_levels})"
380
+ raise argparse.ArgumentError(self, msg)
381
+
382
+ super(LoggingAction, self).__call__(parser, namespace, values, option_string)
383
+
384
+
385
+ def datePicker(thing, prefer_recording_date=False):
386
+ """This function returns a date of some sort, amongst all the possible
387
+ dates (members called release_date, original_release_date,
388
+ and recording_date of type eyed3.core.Date).
389
+
390
+ The order of preference is:
391
+ 1) date of original release
392
+ 2) date of this versions release
393
+ 3) the recording date.
394
+
395
+ Unless ``prefer_recording_date`` is ``True`` in which case the order is
396
+ 3, 1, 2.
397
+
398
+ ``None`` will be returned if no dates are available."""
399
+ if not prefer_recording_date:
400
+ return (thing.original_release_date or
401
+ thing.release_date or
402
+ thing.recording_date)
403
+ else:
404
+ return (thing.recording_date or
405
+ thing.original_release_date or
406
+ thing.release_date)
407
+
408
+
409
+ def makeUniqueFileName(file_path, uniq=''):
410
+ """The ``file_path`` is the desired file name, and it is returned if the
411
+ file does not exist. In the case that it already exists the path is
412
+ adjusted to be unique. First, the ``uniq`` string is added, and then
413
+ a counter is used to find a unique name."""
414
+
415
+ path = os.path.dirname(file_path)
416
+ file = os.path.basename(file_path)
417
+ name, ext = os.path.splitext(file)
418
+ count = 1
419
+ while os.path.exists(os.path.join(path, file)):
420
+ if uniq:
421
+ name = "%s_%s" % (name, uniq)
422
+ file = "".join([name, ext])
423
+ uniq = ''
424
+ else:
425
+ file = "".join(["%s_%s" % (name, count), ext])
426
+ count += 1
427
+ return os.path.join(path, file)
428
+
429
+
430
+ def b(x, encoder=None):
431
+ """Converts `x` to a bytes string if not already.
432
+
433
+ :param x: The string.
434
+ :param encoder: Optional codec encoder to perform the conversion. The default is
435
+ `codecs.latin_1_encode`.
436
+ :return: The byte string if conversion was needed.
437
+ """
438
+ if isinstance(x, bytes):
439
+ return x
440
+ else:
441
+ import codecs
442
+ encoder = encoder or codecs.latin_1_encode
443
+ return encoder(x)[0]
eyed3/utils/art.py ADDED
@@ -0,0 +1,79 @@
1
+ from os.path import basename, splitext
2
+ from fnmatch import fnmatch
3
+ from ..id3.frames import ImageFrame
4
+
5
+
6
+ FRONT_COVER = "FRONT_COVER"
7
+ """Album front cover."""
8
+ BACK_COVER = "BACK_COVER"
9
+ """Album back cover."""
10
+ MISC_COVER = "MISC_COVER"
11
+ """Other part of the album cover; liner notes, gate-fold, etc."""
12
+ LOGO = "LOGO"
13
+ """Artist/band logo."""
14
+ ARTIST = "ARTIST"
15
+ """Artist/band images."""
16
+ LIVE = "LIVE"
17
+ """Artist/band images."""
18
+
19
+ FILENAMES = {
20
+ FRONT_COVER: ["cover-front", "cover-alternate*", "cover",
21
+ "folder", "front", "cover-front_*", "flier"],
22
+ BACK_COVER: ["cover-back", "back", "cover-back_*"],
23
+ MISC_COVER: ["cover-insert*", "cover-liner*", "cover-disc",
24
+ "cover-media*"],
25
+ LOGO: ["logo*"],
26
+ ARTIST: ["artist*"],
27
+ LIVE: ["live*"],
28
+ }
29
+ """A mapping of art types to lists of filename patterns (excluding file
30
+ extension): type -> [file_pattern, ..]."""
31
+
32
+ TO_ID3_ART_TYPES = {
33
+ FRONT_COVER: [ImageFrame.FRONT_COVER, ImageFrame.OTHER, ImageFrame.ICON,
34
+ ImageFrame.LEAFLET],
35
+ BACK_COVER: [ImageFrame.BACK_COVER],
36
+ MISC_COVER: [ImageFrame.MEDIA],
37
+ LOGO: [ImageFrame.BAND_LOGO],
38
+ ARTIST: [ImageFrame.LEAD_ARTIST, ImageFrame.ARTIST, ImageFrame.BAND],
39
+ LIVE: [ImageFrame.DURING_PERFORMANCE, ImageFrame.DURING_RECORDING]
40
+ }
41
+ """A mapping of art types to ID3 APIC (image) types: type -> [apic_type, ..]"""
42
+ # ID3 image types not mapped above:
43
+ # OTHER_ICON = 0x02
44
+ # CONDUCTOR = 0x09
45
+ # COMPOSER = 0x0B
46
+ # LYRICIST = 0x0C
47
+ # RECORDING_LOCATION = 0x0D
48
+ # VIDEO = 0x10
49
+ # BRIGHT_COLORED_FISH = 0x11
50
+ # ILLUSTRATION = 0x12
51
+ # PUBLISHER_LOGO = 0x14
52
+
53
+ FROM_ID3_ART_TYPES = {}
54
+ """A mapping of ID3 art types to eyeD3 art types; the opposite of
55
+ TO_ID3_ART_TYPES."""
56
+ for _type in TO_ID3_ART_TYPES:
57
+ for _id3_type in TO_ID3_ART_TYPES[_type]:
58
+ FROM_ID3_ART_TYPES[_id3_type] = _type
59
+
60
+
61
+ def matchArtFile(filename):
62
+ """Compares ``filename`` (case insensitive) with lists of common art file
63
+ names and returns the type of art that was matched, or None if no types
64
+ were matched."""
65
+ base = splitext(basename(filename))[0]
66
+ for type_ in FILENAMES.keys():
67
+ if True in [fnmatch(base.lower(), fname) for fname in FILENAMES[type_]]:
68
+ return type_
69
+ return None
70
+
71
+
72
+ def getArtFromTag(tag, type_=None):
73
+ """Returns a list of eyed3.id3.frames.ImageFrame objects matching ``type_``,
74
+ all if ``type_`` is None, or empty if tag does not contain art."""
75
+ art = []
76
+ for img in tag.images:
77
+ if not type_ or type_ == img.picture_type:
78
+ art.append(img)
79
+ return art
@@ -0,0 +1,153 @@
1
+ ################################################################################
2
+ # Copyright (C) 2001 Ryan Finne <ryan@finnie.org>
3
+ # Copyright (C) 2002-2011 Travis Shirk <travis@pobox.com>
4
+ #
5
+ # This program is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 2 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program; if not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+ ################################################################################
19
+ import struct
20
+
21
+ MAX_INT16 = (2 ** 16) // 2
22
+ MIN_INT16 = -(MAX_INT16 - 1)
23
+
24
+
25
+ def bytes2bin(bites, sz=8):
26
+ """Accepts a string of ``bytes`` (chars) and returns an array of bits
27
+ representing the bytes in big endian byte order. An optional max ``sz`` for
28
+ each byte (default 8 bits/byte) which can be used to mask out higher
29
+ bits."""
30
+ if sz < 1 or sz > 8:
31
+ raise ValueError(f"Invalid sz value: {sz}")
32
+
33
+ retval = []
34
+ for b in [bytes([b]) for b in bites]:
35
+ bits = []
36
+ b = ord(b)
37
+ while b > 0:
38
+ bits.append(b & 1)
39
+ b >>= 1
40
+
41
+ if len(bits) < sz:
42
+ bits.extend([0] * (sz - len(bits)))
43
+ elif len(bits) > sz:
44
+ bits = bits[:sz]
45
+
46
+ # Big endian byte order.
47
+ bits.reverse()
48
+ retval.extend(bits)
49
+
50
+ return retval
51
+
52
+
53
+ def bin2bytes(x):
54
+ """Convert an array of bits (MSB first) into a string of characters."""
55
+ bits = []
56
+ bits.extend(x)
57
+ bits.reverse()
58
+
59
+ i = 0
60
+ out = b''
61
+ multi = 1
62
+ ttl = 0
63
+ for b in bits:
64
+ i += 1
65
+ ttl += b * multi
66
+ multi *= 2
67
+ if i == 8:
68
+ i = 0
69
+ out += bytes([ttl])
70
+ multi = 1
71
+ ttl = 0
72
+
73
+ if multi > 1:
74
+ out += bytes([ttl])
75
+
76
+ out = bytearray(out)
77
+ out.reverse()
78
+ out = bytes(out)
79
+ return out
80
+
81
+
82
+ def bin2dec(x):
83
+ """Convert ``x``, an array of "bits" (MSB first), to it's decimal value."""
84
+ bits = []
85
+ bits.extend(x)
86
+ bits.reverse() # MSB
87
+
88
+ multi = 1
89
+ value = 0
90
+ for b in bits:
91
+ value += b * multi
92
+ multi *= 2
93
+ return value
94
+
95
+
96
+ def bytes2dec(bites, sz=8):
97
+ return bin2dec(bytes2bin(bites, sz))
98
+
99
+
100
+ def dec2bin(n, p=1):
101
+ """Convert a decimal value ``n`` to an array of bits (MSB first).
102
+ Optionally, pad the overall size to ``p`` bits."""
103
+ assert n >= 0
104
+ if type(n) is not int:
105
+ n = int(n)
106
+ retval = []
107
+
108
+ while n > 0:
109
+ retval.append(n & 1)
110
+ n >>= 1
111
+
112
+ if p > 0:
113
+ retval.extend([0] * (p - len(retval)))
114
+ retval.reverse()
115
+ return retval
116
+
117
+
118
+ def dec2bytes(n, p=1):
119
+ return bin2bytes(dec2bin(n, p))
120
+
121
+
122
+ def bin2synchsafe(x):
123
+ """Convert ``x``, a list of bits (MSB first), to a synch safe list of bits.
124
+ (section 6.2 of the ID3 2.4 spec)."""
125
+ n = bin2dec(x)
126
+ if len(x) > 32 or n > 268435456: # 2^28
127
+ raise ValueError("Invalid value: %s" % str(x))
128
+ elif len(x) < 8:
129
+ return x
130
+
131
+ bites = bytes([(n >> 21) & 0x7f,
132
+ (n >> 14) & 0x7f,
133
+ (n >> 7) & 0x7f,
134
+ (n >> 0) & 0x7f,
135
+ ])
136
+ bits = bytes2bin(bites)
137
+ assert len(bits) == 32
138
+
139
+ return bits
140
+
141
+
142
+ def bytes2signedInt16(bites: bytes):
143
+ if len(bites) != 2:
144
+ raise ValueError("Signed 16 bit integer MUST be 2 bytes.")
145
+ i = struct.unpack(">h", bites)
146
+ return i[0]
147
+
148
+
149
+ def signedInt162bytes(n: int):
150
+ n = int(n)
151
+ if MIN_INT16 <= n <= MAX_INT16:
152
+ return struct.pack(">h", n)
153
+ raise ValueError(f"Signed int16 out of range: {n}")