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/__about__.py
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
from .__regarding__ import * # noqa: F403
|
2
|
+
|
3
|
+
__project_name__ = project_name
|
4
|
+
__version__ = version
|
5
|
+
__version_info__ = version_info
|
6
|
+
__release_name__ = version_info.release_name
|
7
|
+
__years__ = years
|
8
|
+
|
9
|
+
__project_slug__ = "eyed3"
|
10
|
+
__pypi_name__ = "eyeD3"
|
11
|
+
__author__ = author
|
12
|
+
__author_email__ = author_email
|
13
|
+
__url__ = homepage
|
14
|
+
__description__ = description
|
15
|
+
# FIXME: __long_description__ not being used anywhere.
|
16
|
+
__long_description__ = """
|
17
|
+
eyeD3 is a Python module and command line program for processing ID3 tags.
|
18
|
+
Information about mp3 files (i.e bit rate, sample frequency,
|
19
|
+
play time, etc.) is also provided. The formats supported are ID3
|
20
|
+
v1.0/v1.1 and v2.3/v2.4.
|
21
|
+
"""
|
22
|
+
__license__ = "GNU GPL v3.0"
|
23
|
+
__github_url__ = "https://github.com/nicfit/eyeD3",
|
24
|
+
__version_txt__ = """%(__project_name__)s %(__version__)s © Copyright %(__years__)s %(__author__)s
|
25
|
+
This program comes with ABSOLUTELY NO WARRANTY! See LICENSE for details.
|
26
|
+
Run with --help/-h for usage information or read the docs at
|
27
|
+
%(__url__)s""" % (locals())
|
eyed3/__init__.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import sys
|
2
|
+
import codecs
|
3
|
+
import locale
|
4
|
+
from .__about__ import __version__ as version
|
5
|
+
|
6
|
+
_DEFAULT_ENCODING = "latin1"
|
7
|
+
# The local encoding, used when parsing command line options, console output,
|
8
|
+
# etc. The default is always ``latin1`` if it cannot be determined, it is NOT
|
9
|
+
# the value shown.
|
10
|
+
LOCAL_ENCODING = locale.getpreferredencoding(do_setlocale=True)
|
11
|
+
if not LOCAL_ENCODING or LOCAL_ENCODING == "ANSI_X3.4-1968": # pragma: no cover
|
12
|
+
LOCAL_ENCODING = _DEFAULT_ENCODING
|
13
|
+
|
14
|
+
# The local file system encoding, the default is ``latin1`` if it cannot be determined.
|
15
|
+
LOCAL_FS_ENCODING = sys.getfilesystemencoding()
|
16
|
+
if not LOCAL_FS_ENCODING: # pragma: no cover
|
17
|
+
LOCAL_FS_ENCODING = _DEFAULT_ENCODING
|
18
|
+
|
19
|
+
|
20
|
+
class Error(Exception):
|
21
|
+
"""Base exception type for all eyed3 errors."""
|
22
|
+
def __init__(self, *args):
|
23
|
+
super().__init__(*args)
|
24
|
+
if args:
|
25
|
+
# The base class will do exactly this if len(args) == 1,
|
26
|
+
# but not when > 1. Note, the 2.7 base class will, 3 will not.
|
27
|
+
# Make it so.
|
28
|
+
self.message = args[0]
|
29
|
+
|
30
|
+
|
31
|
+
from .utils.log import log # noqa: E402
|
32
|
+
from .core import load, AudioFile # noqa: E402
|
33
|
+
|
34
|
+
del sys
|
35
|
+
del codecs
|
36
|
+
del locale
|
37
|
+
|
38
|
+
__all__ = ["AudioFile", "load", "log", "version", "LOCAL_ENCODING", "LOCAL_FS_ENCODING", "Error"]
|
eyed3/__regarding__.py
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
"""
|
2
|
+
~~~~~~~~~~ DO NOT EDIT THIS FILE! Autogenerated by `regarding` ~~~~~~~~~~
|
3
|
+
https://github.com/nicfit/regarding
|
4
|
+
"""
|
5
|
+
import dataclasses
|
6
|
+
from typing import Optional
|
7
|
+
|
8
|
+
|
9
|
+
__all__ = ["project_name", "version", "version_info",
|
10
|
+
"author", "author_email", "years", "description", "homepage"]
|
11
|
+
|
12
|
+
|
13
|
+
@dataclasses.dataclass
|
14
|
+
class Version:
|
15
|
+
major: int
|
16
|
+
minor: int
|
17
|
+
micro: int
|
18
|
+
dev: Optional[int]
|
19
|
+
pre: Optional[tuple[str, int]]
|
20
|
+
post: Optional[int]
|
21
|
+
release_name: Optional[str]
|
22
|
+
|
23
|
+
|
24
|
+
project_name = "eyeD3"
|
25
|
+
version = "0.9.8a1"
|
26
|
+
version_info = Version(
|
27
|
+
0, 9, 8,
|
28
|
+
None,
|
29
|
+
('a', 1),
|
30
|
+
None,
|
31
|
+
"Refuse/Resist",
|
32
|
+
)
|
33
|
+
|
34
|
+
author = "Travis Shirk"
|
35
|
+
author_email = "travis@pobox.com"
|
36
|
+
years = "2002-2025"
|
37
|
+
description = "Python audio data toolkit (ID3 and MP3)"
|
38
|
+
homepage = "https://eyeD3.nicfit.net/"
|
39
|
+
|
40
|
+
|
41
|
+
def versionBanner() -> str:
|
42
|
+
"""Return the version string including release name."""
|
43
|
+
v = ""
|
44
|
+
if version:
|
45
|
+
v = version
|
46
|
+
if version_info is not None and version_info.release_name:
|
47
|
+
v = "%s (%s)" % (v, version_info.release_name)
|
48
|
+
return v
|
eyed3/core.py
ADDED
@@ -0,0 +1,457 @@
|
|
1
|
+
"""Basic core types and utilities."""
|
2
|
+
import os
|
3
|
+
import time
|
4
|
+
import functools
|
5
|
+
import pathlib
|
6
|
+
import dataclasses
|
7
|
+
from collections import namedtuple
|
8
|
+
from typing import Optional
|
9
|
+
|
10
|
+
from . import LOCAL_FS_ENCODING
|
11
|
+
from .utils.log import getLogger
|
12
|
+
log = getLogger(__name__)
|
13
|
+
|
14
|
+
# Audio type selector for no audio.
|
15
|
+
AUDIO_NONE = 0
|
16
|
+
# Audio type selector for MPEG (mp3) audio.
|
17
|
+
AUDIO_MP3 = 1
|
18
|
+
|
19
|
+
AUDIO_TYPES = (AUDIO_NONE, AUDIO_MP3)
|
20
|
+
|
21
|
+
LP_TYPE = "lp"
|
22
|
+
EP_TYPE = "ep"
|
23
|
+
EP_MAX_SIZE_HINT = 6
|
24
|
+
COMP_TYPE = "compilation"
|
25
|
+
LIVE_TYPE = "live"
|
26
|
+
VARIOUS_TYPE = "various"
|
27
|
+
DEMO_TYPE = "demo"
|
28
|
+
SINGLE_TYPE = "single"
|
29
|
+
ALBUM_TYPE_IDS = [LP_TYPE, EP_TYPE, COMP_TYPE, LIVE_TYPE, VARIOUS_TYPE,
|
30
|
+
DEMO_TYPE, SINGLE_TYPE]
|
31
|
+
VARIOUS_ARTISTS = "Various Artists"
|
32
|
+
|
33
|
+
# A key that can be used in a TXXX frame to specify the type of collection
|
34
|
+
# (or album) a file belongs. See :class:`eyed3.core.ALBUM_TYPE_IDS`.
|
35
|
+
TXXX_ALBUM_TYPE = "eyeD3#album_type"
|
36
|
+
|
37
|
+
# A key that can be used in a TXXX frame to specify the origin of an
|
38
|
+
# artist/band. i.e. where they are from.
|
39
|
+
# The format is: city<tab>state<tab>country
|
40
|
+
TXXX_ARTIST_ORIGIN = "eyeD3#artist_origin"
|
41
|
+
|
42
|
+
# A 2-tuple for count and a total count. e.g. track 3 of 10, count of total.
|
43
|
+
CountAndTotalTuple = namedtuple("CountAndTotalTuple", "count, total")
|
44
|
+
|
45
|
+
|
46
|
+
@dataclasses.dataclass
|
47
|
+
class ArtistOrigin:
|
48
|
+
city: str
|
49
|
+
state: str
|
50
|
+
country: str
|
51
|
+
|
52
|
+
def __bool__(self):
|
53
|
+
return bool(self.city or self.state or self.country)
|
54
|
+
|
55
|
+
def id3Encode(self):
|
56
|
+
return "\t".join([(o if o else "") for o in dataclasses.astuple(self)])
|
57
|
+
|
58
|
+
|
59
|
+
@dataclasses.dataclass
|
60
|
+
class AudioInfo:
|
61
|
+
"""A base container for common audio details."""
|
62
|
+
|
63
|
+
# The number of seconds of audio data (i.e., the playtime)
|
64
|
+
time_secs: float
|
65
|
+
# The number of bytes of audio data.
|
66
|
+
size_bytes: int
|
67
|
+
|
68
|
+
def __post_init__(self):
|
69
|
+
self.time_secs = int(self.time_secs * 100.0) / 100.0
|
70
|
+
|
71
|
+
|
72
|
+
class Tag:
|
73
|
+
"""An abstract interface for audio tag (meta) data (e.g. artist, title,
|
74
|
+
etc.)
|
75
|
+
"""
|
76
|
+
|
77
|
+
read_only: bool = False
|
78
|
+
|
79
|
+
def _setArtist(self, val):
|
80
|
+
raise NotImplementedError() # pragma: nocover
|
81
|
+
|
82
|
+
def _getArtist(self):
|
83
|
+
raise NotImplementedError() # pragma: nocover
|
84
|
+
|
85
|
+
def _getAlbumArtist(self):
|
86
|
+
raise NotImplementedError() # pragma: nocover
|
87
|
+
|
88
|
+
def _setAlbumArtist(self, val):
|
89
|
+
raise NotImplementedError() # pragma: nocover
|
90
|
+
|
91
|
+
def _setAlbum(self, val):
|
92
|
+
raise NotImplementedError() # pragma: nocover
|
93
|
+
|
94
|
+
def _getAlbum(self):
|
95
|
+
raise NotImplementedError() # pragma: nocover
|
96
|
+
|
97
|
+
def _setTitle(self, val):
|
98
|
+
raise NotImplementedError() # pragma: nocover
|
99
|
+
|
100
|
+
def _getTitle(self):
|
101
|
+
raise NotImplementedError() # pragma: nocover
|
102
|
+
|
103
|
+
def _setTrackNum(self, val):
|
104
|
+
raise NotImplementedError() # pragma: nocover
|
105
|
+
|
106
|
+
def _getTrackNum(self) -> CountAndTotalTuple:
|
107
|
+
raise NotImplementedError() # pragma: nocover
|
108
|
+
|
109
|
+
@property
|
110
|
+
def artist(self):
|
111
|
+
return self._getArtist()
|
112
|
+
|
113
|
+
@artist.setter
|
114
|
+
def artist(self, v):
|
115
|
+
self._setArtist(v)
|
116
|
+
|
117
|
+
@property
|
118
|
+
def album_artist(self):
|
119
|
+
return self._getAlbumArtist()
|
120
|
+
|
121
|
+
@album_artist.setter
|
122
|
+
def album_artist(self, v):
|
123
|
+
self._setAlbumArtist(v)
|
124
|
+
|
125
|
+
@property
|
126
|
+
def album(self):
|
127
|
+
return self._getAlbum()
|
128
|
+
|
129
|
+
@album.setter
|
130
|
+
def album(self, v):
|
131
|
+
self._setAlbum(v)
|
132
|
+
|
133
|
+
@property
|
134
|
+
def title(self):
|
135
|
+
return self._getTitle()
|
136
|
+
|
137
|
+
@title.setter
|
138
|
+
def title(self, v):
|
139
|
+
self._setTitle(v)
|
140
|
+
|
141
|
+
@property
|
142
|
+
def track_num(self) -> CountAndTotalTuple:
|
143
|
+
"""Track number property.
|
144
|
+
Must return a 2-tuple of (track-number, total-number-of-tracks).
|
145
|
+
Either tuple value may be ``None``.
|
146
|
+
"""
|
147
|
+
return self._getTrackNum()
|
148
|
+
|
149
|
+
@track_num.setter
|
150
|
+
def track_num(self, v):
|
151
|
+
self._setTrackNum(v)
|
152
|
+
|
153
|
+
def __init__(self, title=None, artist=None, album=None, album_artist=None, track_num=None):
|
154
|
+
self.title = title
|
155
|
+
self.artist = artist
|
156
|
+
self.album = album
|
157
|
+
self.album_artist = album_artist
|
158
|
+
self.track_num = track_num
|
159
|
+
|
160
|
+
|
161
|
+
class AudioFile:
|
162
|
+
"""Abstract base class for audio file types (AudioInfo + Tag)"""
|
163
|
+
_tag: Tag = None
|
164
|
+
_info: AudioInfo = None
|
165
|
+
|
166
|
+
def _read(self):
|
167
|
+
"""Subclasses MUST override this method and set ``self._info``,
|
168
|
+
``self._tag`` and ``self.type``.
|
169
|
+
"""
|
170
|
+
raise NotImplementedError()
|
171
|
+
|
172
|
+
def initTag(self, version=None):
|
173
|
+
raise NotImplementedError()
|
174
|
+
|
175
|
+
def rename(self, name, fsencoding=LOCAL_FS_ENCODING,
|
176
|
+
preserve_file_time=False):
|
177
|
+
"""Rename the file to ``name``.
|
178
|
+
The encoding used for the file name is :attr:`eyed3.LOCAL_FS_ENCODING`
|
179
|
+
unless overridden by ``fsencoding``. Note, if the target file already
|
180
|
+
exists, or the full path contains non-existent directories the
|
181
|
+
operation will fail with :class:`IOError`.
|
182
|
+
File times are not modified when ``preserve_file_time`` is ``True``,
|
183
|
+
``False`` is the default.
|
184
|
+
"""
|
185
|
+
curr_path = pathlib.Path(self.path)
|
186
|
+
ext = curr_path.suffix
|
187
|
+
|
188
|
+
new_path = curr_path.parent / "{name}{ext}".format(**locals())
|
189
|
+
if new_path.exists():
|
190
|
+
raise IOError(f"File '{new_path}' exists, will not overwrite")
|
191
|
+
elif not new_path.parent.exists():
|
192
|
+
raise IOError("Target directory '%s' does not exists, will not "
|
193
|
+
"create" % new_path.parent)
|
194
|
+
|
195
|
+
os.rename(self.path, str(new_path))
|
196
|
+
if self.tag:
|
197
|
+
self.tag.file_info.name = str(new_path)
|
198
|
+
if preserve_file_time:
|
199
|
+
self.tag.file_info.touch((self.tag.file_info.atime,
|
200
|
+
self.tag.file_info.mtime))
|
201
|
+
|
202
|
+
self.path = str(new_path)
|
203
|
+
|
204
|
+
@property
|
205
|
+
def path(self):
|
206
|
+
"""The absolute path of this file."""
|
207
|
+
return self._path
|
208
|
+
|
209
|
+
@path.setter
|
210
|
+
def path(self, path):
|
211
|
+
"""Set the path"""
|
212
|
+
if isinstance(path, pathlib.Path):
|
213
|
+
path = str(path)
|
214
|
+
self._path = path
|
215
|
+
|
216
|
+
@property
|
217
|
+
def info(self) -> AudioInfo:
|
218
|
+
"""Returns a concrete implementation of :class:`eyed3.core.AudioInfo`"""
|
219
|
+
return self._info
|
220
|
+
|
221
|
+
@property
|
222
|
+
def tag(self):
|
223
|
+
"""Returns a concrete implementation of :class:`eyed3.core.Tag`"""
|
224
|
+
return self._tag
|
225
|
+
|
226
|
+
@tag.setter
|
227
|
+
def tag(self, t):
|
228
|
+
self._tag = t
|
229
|
+
|
230
|
+
def __init__(self, path):
|
231
|
+
"""Construct with a path and invoke ``_read``.
|
232
|
+
All other members are set to None."""
|
233
|
+
if isinstance(path, pathlib.Path):
|
234
|
+
path = str(path)
|
235
|
+
self.path = path
|
236
|
+
|
237
|
+
self.type = None
|
238
|
+
self._info = None
|
239
|
+
self._tag = None
|
240
|
+
self._read()
|
241
|
+
|
242
|
+
def __str__(self):
|
243
|
+
return str(self.path)
|
244
|
+
|
245
|
+
|
246
|
+
@functools.total_ordering
|
247
|
+
class Date:
|
248
|
+
"""
|
249
|
+
A class for representing a date and time (optional). This class differs
|
250
|
+
from ``datetime.datetime`` in that the default values for month, day,
|
251
|
+
hour, minute, and second is ``None`` and not 'January 1, 00:00:00'.
|
252
|
+
This allows for an object that is simply 1987, and not January 1 12AM,
|
253
|
+
for example. But when more resolution is required those vales can be set
|
254
|
+
as well.
|
255
|
+
"""
|
256
|
+
|
257
|
+
TIME_STAMP_FORMATS = ["%Y",
|
258
|
+
"%Y-%m",
|
259
|
+
"%Y-%m-%d",
|
260
|
+
"%Y-%m-%dT%H",
|
261
|
+
"%Y-%m-%dT%H:%M",
|
262
|
+
"%Y-%m-%dT%H:%M:%S",
|
263
|
+
# The following end with 'Z' signally time is UTC
|
264
|
+
"%Y-%m-%dT%HZ",
|
265
|
+
"%Y-%m-%dT%H:%MZ",
|
266
|
+
"%Y-%m-%dT%H:%M:%SZ",
|
267
|
+
# The following are wrong per the specs, but ...
|
268
|
+
"%Y-%m-%d %H:%M:%S",
|
269
|
+
"%Y-00-00",
|
270
|
+
"%Y%m%d",
|
271
|
+
]
|
272
|
+
"""Valid time stamp formats per ISO 8601 and used by `strptime`."""
|
273
|
+
|
274
|
+
def __init__(self, year, month=None, day=None,
|
275
|
+
hour=None, minute=None, second=None):
|
276
|
+
# Validate with datetime
|
277
|
+
from datetime import datetime
|
278
|
+
_ = datetime(year, month if month is not None else 1,
|
279
|
+
day if day is not None else 1,
|
280
|
+
hour if hour is not None else 0,
|
281
|
+
minute if minute is not None else 0,
|
282
|
+
second if second is not None else 0)
|
283
|
+
|
284
|
+
self._year = year
|
285
|
+
self._month = month
|
286
|
+
self._day = day
|
287
|
+
self._hour = hour
|
288
|
+
self._minute = minute
|
289
|
+
self._second = second
|
290
|
+
|
291
|
+
# Python's date classes do a lot more date validation than does not
|
292
|
+
# need to be duplicated here. Validate it
|
293
|
+
_ = Date._validateFormat(str(self)) # noqa
|
294
|
+
|
295
|
+
@property
|
296
|
+
def year(self):
|
297
|
+
return self._year
|
298
|
+
|
299
|
+
@property
|
300
|
+
def month(self):
|
301
|
+
return self._month
|
302
|
+
|
303
|
+
@property
|
304
|
+
def day(self):
|
305
|
+
return self._day
|
306
|
+
|
307
|
+
@property
|
308
|
+
def hour(self):
|
309
|
+
return self._hour
|
310
|
+
|
311
|
+
@property
|
312
|
+
def minute(self):
|
313
|
+
return self._minute
|
314
|
+
|
315
|
+
@property
|
316
|
+
def second(self):
|
317
|
+
return self._second
|
318
|
+
|
319
|
+
def __eq__(self, rhs):
|
320
|
+
if not rhs:
|
321
|
+
return False
|
322
|
+
|
323
|
+
return (self.year == rhs.year and
|
324
|
+
self.month == rhs.month and
|
325
|
+
self.day == rhs.day and
|
326
|
+
self.hour == rhs.hour and
|
327
|
+
self.minute == rhs.minute and
|
328
|
+
self.second == rhs.second)
|
329
|
+
|
330
|
+
def __ne__(self, rhs):
|
331
|
+
return not (self == rhs)
|
332
|
+
|
333
|
+
def __lt__(self, rhs):
|
334
|
+
if not rhs:
|
335
|
+
return False
|
336
|
+
|
337
|
+
for left, right in ((self.year, rhs.year),
|
338
|
+
(self.month, rhs.month),
|
339
|
+
(self.day, rhs.day),
|
340
|
+
(self.hour, rhs.hour),
|
341
|
+
(self.minute, rhs.minute),
|
342
|
+
(self.second, rhs.second)):
|
343
|
+
|
344
|
+
left = left if left is not None else -1
|
345
|
+
right = right if right is not None else -1
|
346
|
+
|
347
|
+
if left < right:
|
348
|
+
return True
|
349
|
+
elif left > right:
|
350
|
+
return False
|
351
|
+
|
352
|
+
return False
|
353
|
+
|
354
|
+
def __hash__(self):
|
355
|
+
return hash(str(self))
|
356
|
+
|
357
|
+
@staticmethod
|
358
|
+
def _validateFormat(s):
|
359
|
+
pdate, fmt = None, None
|
360
|
+
for fmt in Date.TIME_STAMP_FORMATS:
|
361
|
+
try:
|
362
|
+
pdate = time.strptime(s, fmt)
|
363
|
+
break
|
364
|
+
except ValueError:
|
365
|
+
# date string did not match format.
|
366
|
+
continue
|
367
|
+
|
368
|
+
if pdate is None:
|
369
|
+
raise ValueError(f"Invalid date string: {s}")
|
370
|
+
|
371
|
+
assert pdate
|
372
|
+
return pdate, fmt
|
373
|
+
|
374
|
+
@staticmethod
|
375
|
+
def parse(s):
|
376
|
+
"""Parses date strings that conform to ISO-8601."""
|
377
|
+
if not isinstance(s, str):
|
378
|
+
s = s.decode("ascii")
|
379
|
+
s = s.strip('\x00')
|
380
|
+
|
381
|
+
pdate, fmt = Date._validateFormat(s)
|
382
|
+
|
383
|
+
# Here is the difference with Python date/datetime objects, some
|
384
|
+
# of the members can be None
|
385
|
+
kwargs = {}
|
386
|
+
if "%m" in fmt:
|
387
|
+
kwargs["month"] = pdate.tm_mon
|
388
|
+
if "%d" in fmt:
|
389
|
+
kwargs["day"] = pdate.tm_mday
|
390
|
+
if "%H" in fmt:
|
391
|
+
kwargs["hour"] = pdate.tm_hour
|
392
|
+
if "%M" in fmt:
|
393
|
+
kwargs["minute"] = pdate.tm_min
|
394
|
+
if "%S" in fmt:
|
395
|
+
kwargs["second"] = pdate.tm_sec
|
396
|
+
|
397
|
+
return Date(pdate.tm_year, **kwargs)
|
398
|
+
|
399
|
+
def __str__(self):
|
400
|
+
"""Returns date strings that conform to ISO-8601.
|
401
|
+
The returned string will be no larger than 17 characters."""
|
402
|
+
s = "%d" % self.year
|
403
|
+
if self.month:
|
404
|
+
s += "-%s" % str(self.month).rjust(2, '0')
|
405
|
+
if self.day:
|
406
|
+
s += "-%s" % str(self.day).rjust(2, '0')
|
407
|
+
if self.hour is not None:
|
408
|
+
s += "T%s" % str(self.hour).rjust(2, '0')
|
409
|
+
if self.minute is not None:
|
410
|
+
s += ":%s" % str(self.minute).rjust(2, '0')
|
411
|
+
if self.second is not None:
|
412
|
+
s += ":%s" % str(self.second).rjust(2, '0')
|
413
|
+
return s
|
414
|
+
|
415
|
+
|
416
|
+
def parseError(ex) -> None:
|
417
|
+
"""A function that is invoked when non-fatal parse, format, etc. errors
|
418
|
+
occur. In most cases the invalid values will be ignored or possibly fixed.
|
419
|
+
This function simply logs the error."""
|
420
|
+
log.warning(ex)
|
421
|
+
|
422
|
+
|
423
|
+
def load(path, tag_version=None) -> Optional[AudioFile]:
|
424
|
+
"""Loads the file identified by ``path`` and returns a concrete type of
|
425
|
+
:class:`eyed3.core.AudioFile`. If ``path`` is not a file an ``IOError`` is
|
426
|
+
raised. ``None`` is returned when the file type (i.e. mime-type) is not
|
427
|
+
recognized.
|
428
|
+
The following AudioFile types are supported:
|
429
|
+
|
430
|
+
* :class:`eyed3.mp3.Mp3AudioFile` - For mp3 audio files.
|
431
|
+
* :class:`eyed3.id3.TagFile` - For raw ID3 data files.
|
432
|
+
|
433
|
+
If ``tag_version`` is not None (the default) only a specific version of
|
434
|
+
metadata is loaded. This value must be a version constant specific to the
|
435
|
+
eventual format of the metadata.
|
436
|
+
"""
|
437
|
+
from . import mimetype, mp3, id3
|
438
|
+
|
439
|
+
if not isinstance(path, pathlib.Path):
|
440
|
+
path = pathlib.Path(path)
|
441
|
+
log.debug(f"Loading file: {path}")
|
442
|
+
|
443
|
+
if path.exists():
|
444
|
+
if not path.is_file():
|
445
|
+
raise IOError(f"not a file: {path}")
|
446
|
+
else:
|
447
|
+
raise IOError(f"file not found: {path}")
|
448
|
+
|
449
|
+
mtype = mimetype.guessMimetype(path)
|
450
|
+
log.debug(f"File mime-type: {mtype}")
|
451
|
+
|
452
|
+
if mtype in mp3.MIME_TYPES:
|
453
|
+
return mp3.Mp3AudioFile(path, tag_version)
|
454
|
+
elif mtype == id3.ID3_MIME_TYPE:
|
455
|
+
return id3.TagFile(path, tag_version)
|
456
|
+
else:
|
457
|
+
return None
|