eyeD3 0.9.8__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/id3/__init__.py ADDED
@@ -0,0 +1,544 @@
1
+ import re
2
+ import functools
3
+
4
+ from .. import core
5
+ from .. import Error
6
+ from ..utils.log import getLogger
7
+
8
+ log = getLogger(__name__)
9
+
10
+ # Version 1, 1.0 or 1.1
11
+ ID3_V1 = (1, None, None)
12
+ # Version 1.0, specifically
13
+ ID3_V1_0 = (1, 0, 0)
14
+ # Version 1.1, specifically
15
+ ID3_V1_1 = (1, 1, 0)
16
+ # Version 2, 2.2, 2.3 or 2.4
17
+ ID3_V2 = (2, None, None)
18
+ # Version 2.2, specifically
19
+ ID3_V2_2 = (2, 2, 0)
20
+ # Version 2.3, specifically
21
+ ID3_V2_3 = (2, 3, 0)
22
+ # Version 2.4, specifically
23
+ ID3_V2_4 = (2, 4, 0)
24
+ # The default version for eyeD3 tags and save operations.
25
+ ID3_DEFAULT_VERSION = ID3_V2_4
26
+ # Useful for operations where any version will suffice.
27
+ ID3_ANY_VERSION = (ID3_V1[0] | ID3_V2[0], None, None)
28
+
29
+ # Byte code for latin1
30
+ LATIN1_ENCODING = b"\x00"
31
+ # Byte code for UTF-16
32
+ UTF_16_ENCODING = b"\x01"
33
+ # Byte code for UTF-16 (big endian)
34
+ UTF_16BE_ENCODING = b"\x02"
35
+ # Byte code for UTF-8 (Not supported in ID3 versions < 2.4)
36
+ UTF_8_ENCODING = b"\x03"
37
+
38
+ # Default language code for frames that contain a language portion.
39
+ DEFAULT_LANG = b"eng"
40
+
41
+ ID3_MIME_TYPE = "application/x-id3"
42
+ ID3_MIME_TYPE_EXTENSIONS = (".id3", ".tag")
43
+
44
+
45
+ def isValidVersion(v, fully_qualified=False):
46
+ """Check the tuple ``v`` against the list of valid ID3 version constants.
47
+ If ``fully_qualified`` is ``True`` it is enforced that there are 3
48
+ components to the version in ``v``. Returns ``True`` when valid and
49
+ ``False`` otherwise."""
50
+ valid = v in [ID3_V1, ID3_V1_0, ID3_V1_1,
51
+ ID3_V2, ID3_V2_2, ID3_V2_3, ID3_V2_4,
52
+ ID3_ANY_VERSION]
53
+ if not valid:
54
+ return False
55
+
56
+ if fully_qualified:
57
+ return None not in (v[0], v[1], v[2])
58
+ else:
59
+ return True
60
+
61
+
62
+ def normalizeVersion(v):
63
+ """If version tuple ``v`` is of the non-specific type (v1 or v2, any, etc.)
64
+ a fully qualified version is returned."""
65
+ if v == ID3_V1:
66
+ v = ID3_V1_1
67
+ elif v == ID3_V2:
68
+ assert ID3_DEFAULT_VERSION[0] & ID3_V2[0]
69
+ v = ID3_DEFAULT_VERSION
70
+ elif v == ID3_ANY_VERSION:
71
+ v = ID3_DEFAULT_VERSION
72
+
73
+ # Now, correct bogus version as seen in the wild
74
+ if v[:2] == (2, 2) and v[2] != 0:
75
+ v = (2, 2, 0)
76
+
77
+ return v
78
+
79
+
80
+ # Convert an ID3 version constant to a display string
81
+ def versionToString(v):
82
+ """Conversion version tuple ``v`` to a string description."""
83
+ if v == ID3_ANY_VERSION:
84
+ return "v1.x/v2.x"
85
+ elif v[0] == 1:
86
+ if v == ID3_V1_0:
87
+ return "v1.0"
88
+ elif v == ID3_V1_1:
89
+ return "v1.1"
90
+ elif v == ID3_V1:
91
+ return "v1.x"
92
+ elif v[0] == 2:
93
+ if v == ID3_V2_2:
94
+ return "v2.2"
95
+ elif v == ID3_V2_3:
96
+ return "v2.3"
97
+ elif v == ID3_V2_4:
98
+ return "v2.4"
99
+ elif v == ID3_V2:
100
+ return "v2.x"
101
+ raise ValueError("Invalid ID3 version constant: %s" % str(v))
102
+
103
+
104
+ class GenreException(Error):
105
+ """Excpetion type for exceptions related to genres."""
106
+
107
+
108
+ @functools.total_ordering
109
+ class Genre:
110
+ """A genre in terms of a ``name`` and and ``id``. Only when ``name`` is
111
+ a "standard" genre (as defined by ID3 v1) will ``id`` be a value other
112
+ than ``None``."""
113
+
114
+ def __init__(self, name=None, id: int=None, genre_map=None):
115
+ """Constructor takes an optional name and ID. If `id` is
116
+ provided the `name`, regardless of value, is set to the string the
117
+ id maps to. Likewise, if `name` is passed and is a standard genre the
118
+ id is set to the correct value. Any invalid id values cause a
119
+ `ValueError` to be raised. Genre names that are not in the standard
120
+ list are still accepted but the `id` value is set to `None`."""
121
+ self._id, self._name = None, None
122
+ self._genre_map = genre_map or genres
123
+ if not name and id is None:
124
+ return
125
+
126
+ # An ID always takes precedence
127
+ if id is not None:
128
+ try:
129
+ self.id = id
130
+ # valid id will set name
131
+ if name and name != self.name:
132
+ log.warning(f"Genre ID takes precedence and remapped '{name}' to '{self.name}'")
133
+ except ValueError as ex:
134
+ log.warning(f"Invalid numeric genre ID: {id}")
135
+ if not name:
136
+ # Gave an invalid ID and no name to fallback on
137
+ raise GenreException from ex
138
+ self.name = name
139
+ self.id = None
140
+ else:
141
+ # All we have is a name
142
+ self.name = name
143
+
144
+ assert self.id or self.name
145
+
146
+ @property
147
+ def id(self):
148
+ """The Genre's id property.
149
+ When setting the value is strictly enforced and if the value is not
150
+ a valid genre code a ``ValueError`` is raised. Otherwise the id is
151
+ set **and** the ``name`` property is updated to the code's string
152
+ name.
153
+ """
154
+ return self._id
155
+
156
+ @id.setter
157
+ def id(self, val):
158
+ if val is None:
159
+ self._id = None
160
+ return
161
+
162
+ val = int(val)
163
+ if val not in self._genre_map.keys() or not self._genre_map[val]:
164
+ raise ValueError(f"Unknown genre ID: {val}")
165
+
166
+ name = self._genre_map[val]
167
+ self._id = val
168
+ self._name = name
169
+
170
+ @property
171
+ def name(self):
172
+ """The Genre's name property.
173
+ When setting the value the name is looked up in the standard genre
174
+ map and if found the ``id`` ppropery is set to the numeric valud **and**
175
+ the name is normalized to the sting found in the map. Non standard
176
+ genres are set (with a warning log) and the ``id`` is set to ``None``.
177
+ It is valid to set the value to ``None``.
178
+ """
179
+ return self._name
180
+
181
+ @name.setter
182
+ def name(self, val):
183
+ if val is None:
184
+ self._name = None
185
+ return
186
+
187
+ if val.lower() in list(self._genre_map.keys()):
188
+ self._id = self._genre_map[val]
189
+ # normalize the name
190
+ self._name = self._genre_map[self._id]
191
+ else:
192
+ log.warning(f"Non standard genre name: {val}")
193
+ self._id = None
194
+ self._name = val
195
+
196
+ @staticmethod
197
+ def parse(g_str, id3_std=True):
198
+ """Parses genre information from `genre_str`.
199
+ The following formats are supported:
200
+ 01, 2, 23, 125 - ID3 v1.x style.
201
+ (01), (2), (129)Hardcore, (9)Metal, Indie - ID3v2 style with and without refinement.
202
+ Raises GenreException when an invalid string is passed.
203
+ """
204
+
205
+ g_str = g_str.strip()
206
+ if not g_str:
207
+ return None
208
+
209
+ def strip0Padding(s):
210
+ if len(s) > 1:
211
+ return s.lstrip("0")
212
+ else:
213
+ return s
214
+
215
+ if id3_std:
216
+ # ID3 v1 style.
217
+ # Match 03, 34, 129.
218
+ if re.compile(r"[0-9][0-9]*$").match(g_str):
219
+ return Genre(id=int(strip0Padding(g_str)))
220
+
221
+ # ID3 v2 style.
222
+ # Match (03), (0)Blues, (15) Rap
223
+ v23_match = re.compile(r"\(([0-9][0-9]*)\)(.*)$").match(g_str)
224
+ if v23_match:
225
+ (gid, name) = v23_match.groups()
226
+
227
+ gid = int(strip0Padding(gid))
228
+ if gid and name:
229
+ gid = gid
230
+ name = name.strip()
231
+ else:
232
+ gid = gid
233
+ name = None
234
+
235
+ return Genre(id=gid, name=name)
236
+
237
+ return Genre(id=None, name=g_str)
238
+
239
+ def __str__(self):
240
+ s = ""
241
+ if self.id is not None:
242
+ s += f"({self.id:d})"
243
+ if self.name:
244
+ s += self.name
245
+ return s
246
+
247
+ def __eq__(self, rhs):
248
+ if not rhs:
249
+ return False
250
+ elif type(rhs) is str:
251
+ return self.name == rhs
252
+ else:
253
+ return self.id == rhs.id and self.name == rhs.name
254
+
255
+ def __lt__(self, rhs):
256
+ if not rhs:
257
+ return False
258
+ elif type(rhs) is str:
259
+ return self.name == rhs
260
+ else:
261
+ return self.name < rhs.name
262
+
263
+
264
+ class GenreMap(dict):
265
+ """Classic genres defined around ID3 v1 but suitable anywhere. This class
266
+ is used primarily as a way to map numeric genre values to a string name.
267
+ Genre strings on the other hand are not required to exist in this list.
268
+ """
269
+ GENRE_MIN = 0
270
+ GENRE_MAX = None
271
+ ID3_GENRE_MIN = 0
272
+ ID3_GENRE_MAX = 79
273
+ WINAMP_GENRE_MIN = 80
274
+ WINAMP_GENRE_MAX = 191
275
+ GENRE_ID3V1_MAX = 255
276
+
277
+ def __init__(self, *args):
278
+ """The optional ``*args`` are passed directly to the ``dict``
279
+ constructor."""
280
+ super().__init__(*args)
281
+
282
+ # ID3 genres as defined by the v1.1 spec with WinAmp extensions.
283
+ for i, g in enumerate(ID3_GENRES):
284
+ self[i] = g
285
+ self[g.lower() if g else None] = i
286
+
287
+ GenreMap.GENRE_MAX = len(ID3_GENRES) - 1
288
+ # Pad up to 255
289
+ for i in range(GenreMap.GENRE_MAX + 1, 255 + 1):
290
+ self[i] = None
291
+ self[None] = 255
292
+
293
+ def get(self, key):
294
+ if type(key) is int:
295
+ name, gid = self[key], key
296
+ else:
297
+ gid = self[key]
298
+ name = self[gid]
299
+ return Genre(name, id=gid, genre_map=self)
300
+
301
+ def __getitem__(self, key):
302
+ if key and type(key) is not int:
303
+ key = key.lower()
304
+ return super().__getitem__(key)
305
+
306
+ @property
307
+ def ids(self):
308
+ return list(sorted([k for k in self.keys() if type(k) is int and self[k]]))
309
+
310
+ def iter(self):
311
+ for gid in self.ids:
312
+ g = self[gid]
313
+ if g:
314
+ yield Genre(g, id=gid)
315
+
316
+
317
+ class TagFile(core.AudioFile):
318
+ """
319
+ A shim class for dealing with files that contain only ID3 data, no audio.
320
+ """
321
+ def __init__(self, path, version=ID3_ANY_VERSION):
322
+ self._tag_version = version
323
+ core.AudioFile.__init__(self, path)
324
+ assert self.type == core.AUDIO_NONE
325
+
326
+ def _read(self):
327
+
328
+ with open(self.path, 'rb') as file_obj:
329
+ tag = Tag()
330
+ tag_found = tag.parse(file_obj, self._tag_version)
331
+ self._tag = tag if tag_found else None
332
+
333
+ self.type = core.AUDIO_NONE
334
+
335
+ def initTag(self, version=ID3_DEFAULT_VERSION):
336
+ """Add a id3.Tag to the file (removing any existing tag if one exists).
337
+ """
338
+ self.tag = Tag()
339
+ self.tag.version = version
340
+ self.tag.file_info = FileInfo(self.path)
341
+
342
+
343
+ # ID3 genres, as defined in ID3 v1. The position in the list is the genre's numeric byte value.
344
+ ID3_GENRES = [
345
+ 'Blues',
346
+ 'Classic Rock',
347
+ 'Country',
348
+ 'Dance',
349
+ 'Disco',
350
+ 'Funk',
351
+ 'Grunge',
352
+ 'Hip-Hop',
353
+ 'Jazz',
354
+ 'Metal',
355
+ 'New Age',
356
+ 'Oldies',
357
+ 'Other',
358
+ 'Pop',
359
+ 'R&B',
360
+ 'Rap',
361
+ 'Reggae',
362
+ 'Rock',
363
+ 'Techno',
364
+ 'Industrial',
365
+ 'Alternative',
366
+ 'Ska',
367
+ 'Death Metal',
368
+ 'Pranks',
369
+ 'Soundtrack',
370
+ 'Euro-Techno',
371
+ 'Ambient',
372
+ 'Trip-Hop',
373
+ 'Vocal',
374
+ 'Jazz+Funk',
375
+ 'Fusion',
376
+ 'Trance',
377
+ 'Classical',
378
+ 'Instrumental',
379
+ 'Acid',
380
+ 'House',
381
+ 'Game',
382
+ 'Sound Clip',
383
+ 'Gospel',
384
+ 'Noise',
385
+ 'AlternRock',
386
+ 'Bass',
387
+ 'Soul',
388
+ 'Punk',
389
+ 'Space',
390
+ 'Meditative',
391
+ 'Instrumental Pop',
392
+ 'Instrumental Rock',
393
+ 'Ethnic',
394
+ 'Gothic',
395
+ 'Darkwave',
396
+ 'Techno-Industrial',
397
+ 'Electronic',
398
+ 'Pop-Folk',
399
+ 'Eurodance',
400
+ 'Dream',
401
+ 'Southern Rock',
402
+ 'Comedy',
403
+ 'Cult',
404
+ 'Gangsta Rap',
405
+ 'Top 40',
406
+ 'Christian Rap',
407
+ 'Pop / Funk',
408
+ 'Jungle',
409
+ 'Native American',
410
+ 'Cabaret',
411
+ 'New Wave',
412
+ 'Psychedelic',
413
+ 'Rave',
414
+ 'Showtunes',
415
+ 'Trailer',
416
+ 'Lo-Fi',
417
+ 'Tribal',
418
+ 'Acid Punk',
419
+ 'Acid Jazz',
420
+ 'Polka',
421
+ 'Retro',
422
+ 'Musical',
423
+ 'Rock & Roll',
424
+ 'Hard Rock',
425
+ 'Folk',
426
+ 'Folk-Rock',
427
+ 'National Folk',
428
+ 'Swing',
429
+ 'Fast Fusion',
430
+ 'Bebob',
431
+ 'Latin',
432
+ 'Revival',
433
+ 'Celtic',
434
+ 'Bluegrass',
435
+ 'Avantgarde',
436
+ 'Gothic Rock',
437
+ 'Progressive Rock',
438
+ 'Psychedelic Rock',
439
+ 'Symphonic Rock',
440
+ 'Slow Rock',
441
+ 'Big Band',
442
+ 'Chorus',
443
+ 'Easy Listening',
444
+ 'Acoustic',
445
+ 'Humour',
446
+ 'Speech',
447
+ 'Chanson',
448
+ 'Opera',
449
+ 'Chamber Music',
450
+ 'Sonata',
451
+ 'Symphony',
452
+ 'Booty Bass',
453
+ 'Primus',
454
+ 'Porn Groove',
455
+ 'Satire',
456
+ 'Slow Jam',
457
+ 'Club',
458
+ 'Tango',
459
+ 'Samba',
460
+ 'Folklore',
461
+ 'Ballad',
462
+ 'Power Ballad',
463
+ 'Rhythmic Soul',
464
+ 'Freestyle',
465
+ 'Duet',
466
+ 'Punk Rock',
467
+ 'Drum Solo',
468
+ 'A Cappella',
469
+ 'Euro-House',
470
+ 'Dance Hall',
471
+ 'Goa',
472
+ 'Drum & Bass',
473
+ 'Club-House',
474
+ 'Hardcore',
475
+ 'Terror',
476
+ 'Indie',
477
+ 'BritPop',
478
+ 'Negerpunk',
479
+ 'Polsk Punk',
480
+ 'Beat',
481
+ 'Christian Gangsta Rap',
482
+ 'Heavy Metal',
483
+ 'Black Metal',
484
+ 'Crossover',
485
+ 'Contemporary Christian',
486
+ 'Christian Rock',
487
+ 'Merengue',
488
+ 'Salsa',
489
+ 'Thrash Metal',
490
+ 'Anime',
491
+ 'JPop',
492
+ 'Synthpop',
493
+ # https://de.wikipedia.org/wiki/Liste_der_ID3v1-Genres
494
+ 'Abstract',
495
+ 'Art Rock',
496
+ 'Baroque',
497
+ 'Bhangra',
498
+ 'Big Beat',
499
+ 'Breakbeat',
500
+ 'Chillout',
501
+ 'Downtempo',
502
+ 'Dub',
503
+ 'EBM',
504
+ 'Eclectic',
505
+ 'Electro',
506
+ 'Electroclash',
507
+ 'Emo',
508
+ 'Experimental',
509
+ 'Garage',
510
+ 'Global',
511
+ 'IDM',
512
+ 'Illbient',
513
+ 'Industro-Goth',
514
+ 'Jam Band',
515
+ 'Krautrock',
516
+ 'Leftfield',
517
+ 'Lounge',
518
+ 'Math Rock',
519
+ 'New Romantic',
520
+ 'Nu-Breakz',
521
+ 'Post-Punk',
522
+ 'Post-Rock',
523
+ 'Psytrance',
524
+ 'Shoegaze',
525
+ 'Space Rock',
526
+ 'Trop Rock',
527
+ 'World Music',
528
+ 'Neoclassical',
529
+ 'Audiobook',
530
+ 'Audio Theatre',
531
+ 'Neue Deutsche Welle',
532
+ 'Podcast',
533
+ 'Indie Rock',
534
+ 'G-Funk',
535
+ 'Dubstep',
536
+ 'Garage Rock',
537
+ 'Psybient',
538
+ ]
539
+
540
+ # A map of standard genre names and IDs per the ID3 v1 genre definition.
541
+ genres = GenreMap()
542
+
543
+ from . import frames # noqa
544
+ from .tag import Tag, TagException, TagTemplate, FileInfo # noqa
eyed3/id3/apple.py ADDED
@@ -0,0 +1,74 @@
1
+ """
2
+ Here lies Apple frames, all of which are non-standard. All of these would have
3
+ been standard user text frames by anyone not being a bastard, on purpose.
4
+ """
5
+ from .frames import Frame, TextFrame
6
+
7
+ PCST_FID = b"PCST"
8
+ WFED_FID = b"WFED"
9
+ TKWD_FID = b"TKWD"
10
+ TDES_FID = b"TDES"
11
+ TGID_FID = b"TGID"
12
+ GRP1_FID = b"GRP1"
13
+ MVNM_FID = b"MVNM"
14
+ MVIN_FID = b"MVIN"
15
+
16
+
17
+ class PCST(Frame):
18
+ """Indicates a podcast. The 4 bytes of data is undefined, and is typically all 0."""
19
+
20
+ def __init__(self, _=None):
21
+ super().__init__(PCST_FID)
22
+
23
+ def render(self):
24
+ self.data = b"\x00" * 4
25
+ return super(PCST, self).render()
26
+
27
+
28
+ class TKWD(TextFrame):
29
+ """Podcast keywords."""
30
+
31
+ def __init__(self, _=None, **kwargs):
32
+ super().__init__(TKWD_FID, **kwargs)
33
+
34
+
35
+ class TDES(TextFrame):
36
+ """Podcast description. One encoding byte followed by text per encoding."""
37
+
38
+ def __init__(self, _=None, **kwargs):
39
+ super().__init__(TDES_FID, **kwargs)
40
+
41
+
42
+ class TGID(TextFrame):
43
+ """Podcast URL of the audio file. This should be a W frame!"""
44
+
45
+ def __init__(self, _=None, **kwargs):
46
+ super().__init__(TGID_FID, **kwargs)
47
+
48
+
49
+ class WFED(TextFrame):
50
+ """Another podcast URL, the feed URL it is said."""
51
+
52
+ def __init__(self, _=None, url=""):
53
+ super().__init__(WFED_FID, url)
54
+
55
+
56
+ class GRP1(TextFrame):
57
+ """Apple grouping, could be a TIT1 conversion."""
58
+
59
+ def __init__(self, _=None, **kwargs):
60
+ super().__init__(GRP1_FID, **kwargs)
61
+
62
+
63
+ class MVNM(TextFrame):
64
+ """Movement name. An Apple extension for classical music."""
65
+
66
+ def __init__(self, _=None, **kwargs):
67
+ super().__init__(MVNM_FID, **kwargs)
68
+
69
+
70
+ class MVIN(TextFrame):
71
+ """Movement index (e.g. "3/9"). An Apple extension for classical music."""
72
+
73
+ def __init__(self, _=None, **kwargs):
74
+ super().__init__(MVIN_FID, **kwargs)