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.
- eyed3/__about__.py +27 -0
- eyed3/__init__.py +38 -0
- eyed3/__regarding__.py +48 -0
- eyed3/core.py +457 -0
- eyed3/id3/__init__.py +544 -0
- eyed3/id3/apple.py +58 -0
- eyed3/id3/frames.py +2261 -0
- eyed3/id3/headers.py +696 -0
- eyed3/id3/tag.py +2047 -0
- eyed3/main.py +305 -0
- eyed3/mimetype.py +107 -0
- eyed3/mp3/__init__.py +188 -0
- eyed3/mp3/headers.py +866 -0
- eyed3/plugins/__init__.py +200 -0
- eyed3/plugins/art.py +266 -0
- eyed3/plugins/classic.py +1173 -0
- eyed3/plugins/extract.py +61 -0
- eyed3/plugins/fixup.py +631 -0
- eyed3/plugins/genres.py +48 -0
- eyed3/plugins/itunes.py +64 -0
- eyed3/plugins/jsontag.py +133 -0
- eyed3/plugins/lameinfo.py +86 -0
- eyed3/plugins/lastfm.py +50 -0
- eyed3/plugins/mimetype.py +93 -0
- eyed3/plugins/nfo.py +123 -0
- eyed3/plugins/pymod.py +72 -0
- eyed3/plugins/stats.py +479 -0
- eyed3/plugins/xep_118.py +45 -0
- eyed3/plugins/yamltag.py +25 -0
- eyed3/utils/__init__.py +443 -0
- eyed3/utils/art.py +79 -0
- eyed3/utils/binfuncs.py +153 -0
- eyed3/utils/console.py +553 -0
- eyed3/utils/log.py +59 -0
- eyed3/utils/prompt.py +90 -0
- eyed3-0.9.8a1.dist-info/METADATA +163 -0
- eyed3-0.9.8a1.dist-info/RECORD +42 -0
- eyed3-0.9.8a1.dist-info/WHEEL +5 -0
- eyed3-0.9.8a1.dist-info/entry_points.txt +2 -0
- eyed3-0.9.8a1.dist-info/licenses/AUTHORS.rst +39 -0
- eyed3-0.9.8a1.dist-info/licenses/LICENSE +675 -0
- eyed3-0.9.8a1.dist-info/top_level.txt +1 -0
eyed3/utils/__init__.py
ADDED
@@ -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
|
eyed3/utils/binfuncs.py
ADDED
@@ -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}")
|