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/plugins/stats.py
ADDED
@@ -0,0 +1,479 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
import operator
|
4
|
+
from collections import Counter
|
5
|
+
|
6
|
+
from eyed3 import id3, mp3
|
7
|
+
from eyed3.core import AUDIO_MP3
|
8
|
+
from eyed3.mimetype import guessMimetype
|
9
|
+
from eyed3.utils.console import Fore, Style, printMsg
|
10
|
+
from eyed3.plugins import LoaderPlugin
|
11
|
+
from eyed3.id3 import frames
|
12
|
+
|
13
|
+
ID3_VERSIONS = [id3.ID3_V1_0, id3.ID3_V1_1,
|
14
|
+
id3.ID3_V2_2, id3.ID3_V2_3, id3.ID3_V2_4]
|
15
|
+
|
16
|
+
_OP_STRINGS = {operator.le: "<=",
|
17
|
+
operator.lt: "< ",
|
18
|
+
operator.ge: ">=",
|
19
|
+
operator.gt: "> ",
|
20
|
+
operator.eq: "= ",
|
21
|
+
operator.ne: "!=",
|
22
|
+
}
|
23
|
+
|
24
|
+
|
25
|
+
class Rule:
|
26
|
+
def test(self, path, audio_file):
|
27
|
+
raise NotImplementedError()
|
28
|
+
|
29
|
+
|
30
|
+
PREFERRED_ID3_VERSIONS = [id3.ID3_V2_3,
|
31
|
+
id3.ID3_V2_4,
|
32
|
+
]
|
33
|
+
|
34
|
+
|
35
|
+
class Id3TagRules(Rule):
|
36
|
+
def test(self, path, audio_file):
|
37
|
+
scores = []
|
38
|
+
|
39
|
+
if audio_file is None:
|
40
|
+
return None
|
41
|
+
|
42
|
+
if not audio_file.tag:
|
43
|
+
return [(-75, "Missing ID3 tag")]
|
44
|
+
|
45
|
+
tag = audio_file.tag
|
46
|
+
if tag.version not in PREFERRED_ID3_VERSIONS:
|
47
|
+
scores.append((-30, "ID3 version not in %s" %
|
48
|
+
PREFERRED_ID3_VERSIONS))
|
49
|
+
if not tag.title:
|
50
|
+
scores.append((-30, "Tag missing title"))
|
51
|
+
if not tag.artist:
|
52
|
+
scores.append((-28, "Tag missing artist"))
|
53
|
+
if not tag.album:
|
54
|
+
scores.append((-26, "Tag missing album"))
|
55
|
+
if not tag.track_num[0]:
|
56
|
+
scores.append((-24, "Tag missing track number"))
|
57
|
+
if not tag.track_num[1]:
|
58
|
+
scores.append((-22, "Tag missing total # of tracks"))
|
59
|
+
|
60
|
+
if not tag.getBestDate():
|
61
|
+
scores.append((-30, "Tag missing any useful dates"))
|
62
|
+
else:
|
63
|
+
if not tag.original_release_date:
|
64
|
+
# Original release date is so rarely used but is almost always
|
65
|
+
# what I mean or wanna know.
|
66
|
+
scores.append((-10, "No original release date"))
|
67
|
+
elif not tag.release_date:
|
68
|
+
scores.append((-5, "No release date"))
|
69
|
+
|
70
|
+
# TLEN, best gotten from audio_file.info.time_secs but having it in
|
71
|
+
# the tag is good, I guess.
|
72
|
+
if b"TLEN" not in tag.frame_set:
|
73
|
+
scores.append((-5, "No TLEN frame"))
|
74
|
+
|
75
|
+
return scores
|
76
|
+
|
77
|
+
|
78
|
+
class BitrateRule(Rule):
|
79
|
+
BITRATE_DEDUCTIONS = [(128, -20), (192, -10)]
|
80
|
+
|
81
|
+
def test(self, path, audio_file):
|
82
|
+
scores = []
|
83
|
+
|
84
|
+
if not audio_file:
|
85
|
+
return None
|
86
|
+
|
87
|
+
if not audio_file.info:
|
88
|
+
# Detected as an audio file but not real audio data found.
|
89
|
+
return [(-90, "No audio data found")]
|
90
|
+
|
91
|
+
is_vbr, bitrate = audio_file.info.bit_rate
|
92
|
+
for threshold, score in self.BITRATE_DEDUCTIONS:
|
93
|
+
if bitrate < threshold:
|
94
|
+
scores.append((score, "Bit rate < %d" % threshold))
|
95
|
+
break
|
96
|
+
|
97
|
+
return scores
|
98
|
+
|
99
|
+
|
100
|
+
VALID_MIME_TYPES = mp3.MIME_TYPES + ["image/png",
|
101
|
+
"image/gif",
|
102
|
+
"image/jpeg",
|
103
|
+
]
|
104
|
+
|
105
|
+
|
106
|
+
class FileRule(Rule):
|
107
|
+
def test(self, path, audio_file):
|
108
|
+
mt = guessMimetype(path)
|
109
|
+
|
110
|
+
for name in os.path.split(path):
|
111
|
+
if name.startswith('.'):
|
112
|
+
return [(-100, "Hidden file type")]
|
113
|
+
|
114
|
+
if mt not in VALID_MIME_TYPES:
|
115
|
+
return [(-100, "Unsupported file type: %s" % mt)]
|
116
|
+
return None
|
117
|
+
|
118
|
+
|
119
|
+
VALID_ARTWORK_NAMES = ("cover", "cover-front", "cover-back")
|
120
|
+
|
121
|
+
|
122
|
+
class ArtworkRule(Rule):
|
123
|
+
def test(self, path, audio_file):
|
124
|
+
mt = guessMimetype(path)
|
125
|
+
if mt and mt.startswith("image/"):
|
126
|
+
name, ext = os.path.splitext(os.path.basename(path))
|
127
|
+
if name not in VALID_ARTWORK_NAMES:
|
128
|
+
return [(-10, "Artwork file not in %s" %
|
129
|
+
str(VALID_ARTWORK_NAMES))]
|
130
|
+
|
131
|
+
return None
|
132
|
+
|
133
|
+
|
134
|
+
BAD_FRAMES = [frames.PRIVATE_FID, frames.OBJECT_FID]
|
135
|
+
|
136
|
+
|
137
|
+
class Id3FrameRules(Rule):
|
138
|
+
def test(self, path, audio_file):
|
139
|
+
scores = []
|
140
|
+
if not audio_file or not audio_file.tag:
|
141
|
+
return
|
142
|
+
|
143
|
+
tag = audio_file.tag
|
144
|
+
for fid in tag.frame_set:
|
145
|
+
if fid[0] == 'T' and fid != "TXXX" and len(tag.frame_set[fid]) > 1:
|
146
|
+
scores.append((-10, "Multiple %s frames" % fid.decode('ascii')))
|
147
|
+
elif fid in BAD_FRAMES:
|
148
|
+
scores.append((-13, "%s frames are bad, mmmkay?" %
|
149
|
+
fid.decode('ascii')))
|
150
|
+
|
151
|
+
return scores
|
152
|
+
|
153
|
+
|
154
|
+
class Stat(Counter):
|
155
|
+
TOTAL = "total"
|
156
|
+
|
157
|
+
def __init__(self, *args, **kwargs):
|
158
|
+
super(Stat, self).__init__(*args, **kwargs)
|
159
|
+
self[self.TOTAL] = 0
|
160
|
+
self._key_names = {}
|
161
|
+
|
162
|
+
def compute(self, file, audio_file):
|
163
|
+
self[self.TOTAL] += 1
|
164
|
+
self._compute(file, audio_file)
|
165
|
+
|
166
|
+
def _compute(self, file, audio_file):
|
167
|
+
pass
|
168
|
+
|
169
|
+
def report(self):
|
170
|
+
self._report()
|
171
|
+
|
172
|
+
def _sortedKeys(self, most_common=False):
|
173
|
+
def keyDisplayName(k):
|
174
|
+
return self._key_names[k] if k in self._key_names else k
|
175
|
+
|
176
|
+
key_map = {}
|
177
|
+
for k in list(self.keys()):
|
178
|
+
key_map[keyDisplayName(k)] = k
|
179
|
+
|
180
|
+
if not most_common:
|
181
|
+
sorted_names = [k for k in key_map.keys() if k]
|
182
|
+
sorted_names.remove(self.TOTAL)
|
183
|
+
sorted_names.sort()
|
184
|
+
sorted_names.append(self.TOTAL)
|
185
|
+
else:
|
186
|
+
most_common = self.most_common()
|
187
|
+
sorted_names = []
|
188
|
+
remainder_names = []
|
189
|
+
for k, v in most_common:
|
190
|
+
if k != self.TOTAL and v > 0:
|
191
|
+
sorted_names.append(keyDisplayName(k))
|
192
|
+
elif k != self.TOTAL:
|
193
|
+
remainder_names.append(keyDisplayName(k))
|
194
|
+
|
195
|
+
remainder_names.sort()
|
196
|
+
sorted_names = sorted_names + remainder_names
|
197
|
+
sorted_names.append(self.TOTAL)
|
198
|
+
|
199
|
+
return [key_map[name] for name in sorted_names]
|
200
|
+
|
201
|
+
def _report(self, most_common=False):
|
202
|
+
keys = self._sortedKeys(most_common=most_common)
|
203
|
+
|
204
|
+
key_col_width = 0
|
205
|
+
val_col_width = 0
|
206
|
+
for key in keys:
|
207
|
+
key = self._key_names[key] if key in self._key_names else key
|
208
|
+
key_col_width = max(key_col_width, len(str(key)))
|
209
|
+
val_col_width = max(val_col_width, len(str(self[key])))
|
210
|
+
key_col_width += 1
|
211
|
+
val_col_width += 1
|
212
|
+
|
213
|
+
for k in keys:
|
214
|
+
key_name = self._key_names[k] if k in self._key_names else k
|
215
|
+
if type(key_name) is bytes:
|
216
|
+
key_name = key_name.decode("latin1")
|
217
|
+
value = self[k]
|
218
|
+
percent = self.percent(k) if value and k != "total" else ""
|
219
|
+
print("{padding}{key}:{value}{percent}".format(
|
220
|
+
padding=' ' * 4,
|
221
|
+
key=str(key_name).ljust(key_col_width),
|
222
|
+
value=str(value).rjust(val_col_width),
|
223
|
+
percent=" ( %s%.2f%%%s )" % (Fore.GREEN, percent, Fore.RESET)
|
224
|
+
if percent else "",
|
225
|
+
))
|
226
|
+
|
227
|
+
def percent(self, key):
|
228
|
+
return (float(self[key]) / float(self["total"])) * 100
|
229
|
+
|
230
|
+
|
231
|
+
class AudioStat(Stat):
|
232
|
+
def compute(self, audio_file):
|
233
|
+
self["total"] += 1
|
234
|
+
self._compute(audio_file)
|
235
|
+
|
236
|
+
def _compute(self, audio_file):
|
237
|
+
pass
|
238
|
+
|
239
|
+
|
240
|
+
class FileCounterStat(Stat):
|
241
|
+
SUPPORTED_AUDIO = "audio"
|
242
|
+
UNSUPPORTED_AUDIO = "audio (unsupported)"
|
243
|
+
HIDDEN_FILES = "hidden"
|
244
|
+
OTHER_FILES = "other"
|
245
|
+
|
246
|
+
def __init__(self):
|
247
|
+
super(FileCounterStat, self).__init__()
|
248
|
+
for k in ("audio", "hidden", "audio (unsupported)"):
|
249
|
+
self[k] = 0
|
250
|
+
|
251
|
+
def _compute(self, file, audio_file):
|
252
|
+
mt = guessMimetype(file)
|
253
|
+
|
254
|
+
if audio_file:
|
255
|
+
self[self.SUPPORTED_AUDIO] += 1
|
256
|
+
elif mt and mt.startswith("audio/"):
|
257
|
+
self[self.UNSUPPORTED_AUDIO] += 1
|
258
|
+
elif os.path.basename(file).startswith('.'):
|
259
|
+
self[self.HIDDEN_FILES] += 1
|
260
|
+
else:
|
261
|
+
self[self.OTHER_FILES] += 1
|
262
|
+
|
263
|
+
def _report(self):
|
264
|
+
print(Style.BRIGHT + Fore.YELLOW + "Files:" + Style.RESET_ALL)
|
265
|
+
super(FileCounterStat, self)._report()
|
266
|
+
|
267
|
+
|
268
|
+
class MimeTypeStat(Stat):
|
269
|
+
def _compute(self, file, audio_file):
|
270
|
+
mt = guessMimetype(file)
|
271
|
+
self[mt] += 1
|
272
|
+
|
273
|
+
def _report(self):
|
274
|
+
print(Style.BRIGHT + Fore.YELLOW + "Mime-Types:" + Style.RESET_ALL)
|
275
|
+
super(MimeTypeStat, self)._report(most_common=True)
|
276
|
+
|
277
|
+
|
278
|
+
class Id3VersionCounter(AudioStat):
|
279
|
+
def __init__(self):
|
280
|
+
super(Id3VersionCounter, self).__init__()
|
281
|
+
for v in ID3_VERSIONS:
|
282
|
+
self[v] = 0
|
283
|
+
self._key_names[v] = id3.versionToString(v)
|
284
|
+
|
285
|
+
def _compute(self, audio_file):
|
286
|
+
if audio_file.tag:
|
287
|
+
self[audio_file.tag.version] += 1
|
288
|
+
else:
|
289
|
+
self[None] += 1
|
290
|
+
|
291
|
+
def _report(self):
|
292
|
+
print(Style.BRIGHT + Fore.YELLOW + "ID3 versions:" + Style.RESET_ALL)
|
293
|
+
super(Id3VersionCounter, self)._report()
|
294
|
+
|
295
|
+
|
296
|
+
class Id3FrameCounter(AudioStat):
|
297
|
+
def _compute(self, audio_file):
|
298
|
+
if audio_file.tag:
|
299
|
+
for frame_id in audio_file.tag.frame_set:
|
300
|
+
self[frame_id] += len(audio_file.tag.frame_set[frame_id])
|
301
|
+
|
302
|
+
def _report(self):
|
303
|
+
print(Style.BRIGHT + Fore.YELLOW + "ID3 frames:" + Style.RESET_ALL)
|
304
|
+
super(Id3FrameCounter, self)._report(most_common=True)
|
305
|
+
|
306
|
+
|
307
|
+
class BitrateCounter(AudioStat):
|
308
|
+
def __init__(self):
|
309
|
+
super(BitrateCounter, self).__init__()
|
310
|
+
self["cbr"] = 0
|
311
|
+
self["vbr"] = 0
|
312
|
+
self.bitrate_keys = [(operator.le, 96),
|
313
|
+
(operator.le, 112),
|
314
|
+
(operator.le, 128),
|
315
|
+
(operator.le, 160),
|
316
|
+
(operator.le, 192),
|
317
|
+
(operator.le, 256),
|
318
|
+
(operator.le, 320),
|
319
|
+
(operator.gt, 320),
|
320
|
+
]
|
321
|
+
for k in self.bitrate_keys:
|
322
|
+
self[k] = 0
|
323
|
+
op, bitrate = k
|
324
|
+
self._key_names[k] = "%s %d" % (_OP_STRINGS[op], bitrate)
|
325
|
+
|
326
|
+
def _compute(self, audio_file):
|
327
|
+
if audio_file.type != AUDIO_MP3 or audio_file.info is None:
|
328
|
+
self["total"] -= 1
|
329
|
+
return
|
330
|
+
|
331
|
+
vbr, br = audio_file.info.bit_rate
|
332
|
+
if vbr:
|
333
|
+
self["vbr"] += 1
|
334
|
+
else:
|
335
|
+
self["cbr"] += 1
|
336
|
+
|
337
|
+
for key in self.bitrate_keys:
|
338
|
+
key_op, key_br = key
|
339
|
+
if key_op(br, key_br):
|
340
|
+
self[key] += 1
|
341
|
+
break
|
342
|
+
|
343
|
+
def _report(self):
|
344
|
+
print(Style.BRIGHT + Fore.YELLOW + "MP3 bitrates:" + Style.RESET_ALL)
|
345
|
+
super(BitrateCounter, self)._report(most_common=True)
|
346
|
+
|
347
|
+
def _sortedKeys(self, most_common=False):
|
348
|
+
keys = super(BitrateCounter, self)._sortedKeys(most_common=most_common)
|
349
|
+
keys.remove("cbr")
|
350
|
+
keys.remove("vbr")
|
351
|
+
keys.insert(0, "cbr")
|
352
|
+
keys.insert(1, "vbr")
|
353
|
+
return keys
|
354
|
+
|
355
|
+
|
356
|
+
class RuleViolationStat(Stat):
|
357
|
+
def _report(self):
|
358
|
+
print(Style.BRIGHT + Fore.YELLOW + "Rule Violations:" + Style.RESET_ALL)
|
359
|
+
super(RuleViolationStat, self)._report(most_common=True)
|
360
|
+
|
361
|
+
|
362
|
+
class Id3ImageTypeCounter(AudioStat):
|
363
|
+
def __init__(self):
|
364
|
+
super(Id3ImageTypeCounter, self).__init__()
|
365
|
+
|
366
|
+
self._key_names = {}
|
367
|
+
for attr in dir(frames.ImageFrame):
|
368
|
+
val = getattr(frames.ImageFrame, attr)
|
369
|
+
if isinstance(val, int) and not attr.endswith("_TYPE") and not attr.startswith("_"):
|
370
|
+
self._key_names[val] = attr
|
371
|
+
|
372
|
+
for v in self._key_names:
|
373
|
+
self[v] = 0
|
374
|
+
|
375
|
+
def _compute(self, audio_file):
|
376
|
+
if audio_file.tag:
|
377
|
+
for img in audio_file.tag.images:
|
378
|
+
self[img.picture_type] += 1
|
379
|
+
|
380
|
+
def _report(self):
|
381
|
+
print(Style.BRIGHT + Fore.YELLOW + "APIC image types:" + Style.RESET_ALL)
|
382
|
+
super(Id3ImageTypeCounter, self)._report()
|
383
|
+
|
384
|
+
|
385
|
+
class StatisticsPlugin(LoaderPlugin):
|
386
|
+
NAMES = ['stats']
|
387
|
+
SUMMARY = "Computes statistics for all audio files scanned."
|
388
|
+
|
389
|
+
def __init__(self, arg_parser):
|
390
|
+
super(StatisticsPlugin, self).__init__(arg_parser)
|
391
|
+
|
392
|
+
self.arg_group.add_argument(
|
393
|
+
"--verbose", action="store_true", default=False,
|
394
|
+
help="Show details for each file with rule violations.")
|
395
|
+
|
396
|
+
self._stats = []
|
397
|
+
self._rules_stat = RuleViolationStat()
|
398
|
+
|
399
|
+
self._stats.append(FileCounterStat())
|
400
|
+
self._stats.append(MimeTypeStat())
|
401
|
+
self._stats.append(Id3VersionCounter())
|
402
|
+
self._stats.append(Id3FrameCounter())
|
403
|
+
self._stats.append(Id3ImageTypeCounter())
|
404
|
+
self._stats.append(BitrateCounter())
|
405
|
+
|
406
|
+
self._score_sum = 0
|
407
|
+
self._score_count = 0
|
408
|
+
self._rules_log = {}
|
409
|
+
self._rules = [Id3TagRules(),
|
410
|
+
FileRule(),
|
411
|
+
ArtworkRule(),
|
412
|
+
BitrateRule(),
|
413
|
+
Id3FrameRules(),
|
414
|
+
]
|
415
|
+
|
416
|
+
def handleFile(self, path):
|
417
|
+
super(StatisticsPlugin, self).handleFile(path)
|
418
|
+
if not self.args.quiet:
|
419
|
+
sys.stdout.write('.')
|
420
|
+
sys.stdout.flush()
|
421
|
+
|
422
|
+
for stat in self._stats:
|
423
|
+
if isinstance(stat, AudioStat):
|
424
|
+
if self.audio_file:
|
425
|
+
stat.compute(self.audio_file)
|
426
|
+
else:
|
427
|
+
stat.compute(path, self.audio_file)
|
428
|
+
|
429
|
+
self._score_count += 1
|
430
|
+
total_score = 100
|
431
|
+
for rule in self._rules:
|
432
|
+
scores = rule.test(path, self.audio_file) or []
|
433
|
+
if scores:
|
434
|
+
if path not in self._rules_log:
|
435
|
+
self._rules_log[path] = []
|
436
|
+
|
437
|
+
for score, text in scores:
|
438
|
+
self._rules_stat[text] += 1
|
439
|
+
self._rules_log[path].append((score, text))
|
440
|
+
# += because negative values are returned
|
441
|
+
total_score += score
|
442
|
+
|
443
|
+
if total_score != 100:
|
444
|
+
self._rules_stat[Stat.TOTAL] += 1
|
445
|
+
|
446
|
+
self._score_sum += total_score
|
447
|
+
|
448
|
+
def handleDone(self):
|
449
|
+
if self._num_loaded == 0:
|
450
|
+
super(StatisticsPlugin, self).handleDone()
|
451
|
+
return
|
452
|
+
|
453
|
+
print()
|
454
|
+
for stat in self._stats + [self._rules_stat]:
|
455
|
+
stat.report()
|
456
|
+
print()
|
457
|
+
|
458
|
+
# Detailed rule violations
|
459
|
+
if self.args.verbose:
|
460
|
+
for path in self._rules_log:
|
461
|
+
printMsg(path) # does the right thing for unicode
|
462
|
+
for score, text in self._rules_log[path]:
|
463
|
+
print(f"\t{Fore.RED}{str(score).center(3)}{Fore.RESET} ({text})")
|
464
|
+
|
465
|
+
def prettyScore():
|
466
|
+
s = float(self._score_sum) / float(self._score_count)
|
467
|
+
if s > 80:
|
468
|
+
c = Fore.GREEN
|
469
|
+
elif s > 70:
|
470
|
+
c = Fore.YELLOW
|
471
|
+
else:
|
472
|
+
c = Fore.RED
|
473
|
+
return s, c
|
474
|
+
|
475
|
+
score, color = prettyScore()
|
476
|
+
print(f"{Style.BRIGHT}Score{Style.RESET_BRIGHT} = {color}{score}%%{Fore.RESET}")
|
477
|
+
if not self.args.verbose:
|
478
|
+
print("Run with --verbose to see files and their rule violations")
|
479
|
+
print()
|
eyed3/plugins/xep_118.py
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from xml.sax.saxutils import escape
|
3
|
+
|
4
|
+
from eyed3.plugins import LoaderPlugin
|
5
|
+
from eyed3.utils.console import printMsg
|
6
|
+
|
7
|
+
|
8
|
+
class Xep118Plugin(LoaderPlugin):
|
9
|
+
NAMES = ["xep-118"]
|
10
|
+
SUMMARY = "Outputs all tags in XEP-118 XML format. "\
|
11
|
+
"(see: http://xmpp.org/extensions/xep-0118.html)"
|
12
|
+
|
13
|
+
def __init__(self, arg_parser):
|
14
|
+
super().__init__(arg_parser, cache_files=True, track_images=False)
|
15
|
+
g = self.arg_group
|
16
|
+
g.add_argument("--no-pretty-print", action="store_true",
|
17
|
+
help="Output without new lines or indentation.")
|
18
|
+
|
19
|
+
def handleFile(self, f, *args, **kwargs):
|
20
|
+
super().handleFile(f)
|
21
|
+
|
22
|
+
if self.audio_file and self.audio_file.tag:
|
23
|
+
xml = self.getXML(self.audio_file)
|
24
|
+
printMsg(xml)
|
25
|
+
|
26
|
+
def getXML(self, audio_file):
|
27
|
+
tag = audio_file.tag
|
28
|
+
|
29
|
+
pprint = not self.args.no_pretty_print
|
30
|
+
nl = "\n" if pprint else ""
|
31
|
+
indent = (" " * 2) if pprint else ""
|
32
|
+
|
33
|
+
xml = f"<tune xmlns='http://jabber.org/protocol/tune'>{nl}"
|
34
|
+
if tag.artist:
|
35
|
+
xml += f"{indent}<artist>{escape(tag.artist)}</artist>{nl}"
|
36
|
+
if tag.title:
|
37
|
+
xml += f"{indent}<title>{escape(tag.title)}</title>{nl}"
|
38
|
+
if tag.album:
|
39
|
+
xml += f"{indent}<source>{escape(tag.album)}</source>{nl}"
|
40
|
+
xml += f"{indent}<track>file://{escape(str(Path(audio_file.path).absolute()))}</track>{nl}"
|
41
|
+
if audio_file.info:
|
42
|
+
xml += f"{indent}<length>{audio_file.info.time_secs:.2f}</length>{nl}"
|
43
|
+
xml += "</tune>"
|
44
|
+
|
45
|
+
return xml
|
eyed3/plugins/yamltag.py
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
import eyed3.plugins
|
2
|
+
from eyed3 import log
|
3
|
+
from eyed3.plugins.jsontag import audioFileToJson
|
4
|
+
|
5
|
+
try:
|
6
|
+
import yaml as _yaml
|
7
|
+
except ImportError:
|
8
|
+
_yaml = None
|
9
|
+
log.info("yaml plugin: Install `PyYAML` for YAML plugin support.")
|
10
|
+
|
11
|
+
|
12
|
+
if _yaml:
|
13
|
+
class YamlTagPlugin(eyed3.plugins.LoaderPlugin):
|
14
|
+
NAMES = ["yaml"]
|
15
|
+
SUMMARY = "Outputs all tags as YAML."
|
16
|
+
|
17
|
+
def __init__(self, arg_parser):
|
18
|
+
super().__init__(arg_parser, cache_files=True, track_images=False)
|
19
|
+
|
20
|
+
def handleFile(self, f, *args, **kwargs):
|
21
|
+
super().handleFile(f)
|
22
|
+
if self.audio_file and self.audio_file.info and self.audio_file.tag:
|
23
|
+
print(_yaml.safe_dump(audioFileToJson(self.audio_file),
|
24
|
+
indent=2, default_flow_style=False,
|
25
|
+
explicit_start=True))
|