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/genres.py
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
import math
|
2
|
+
from eyed3 import id3
|
3
|
+
from eyed3.plugins import Plugin
|
4
|
+
|
5
|
+
|
6
|
+
class GenreListPlugin(Plugin):
|
7
|
+
SUMMARY = "Display the full list of standard ID3 genres."
|
8
|
+
DESCRIPTION = "ID3 v1 defined a list of genres and mapped them to "\
|
9
|
+
"to numeric values so they can be stored as a single "\
|
10
|
+
"byte.\nIt is *recommended* that these genres are used "\
|
11
|
+
"although most newer software (including eyeD3) does not "\
|
12
|
+
"care."
|
13
|
+
NAMES = ["genres"]
|
14
|
+
|
15
|
+
def __init__(self, arg_parser):
|
16
|
+
super(GenreListPlugin, self).__init__(arg_parser)
|
17
|
+
self.arg_group.add_argument("-1", "--single-column", action="store_true",
|
18
|
+
help="List on genre per line.")
|
19
|
+
|
20
|
+
def start(self, args, config):
|
21
|
+
self._printGenres(args)
|
22
|
+
|
23
|
+
@staticmethod
|
24
|
+
def _printGenres(args):
|
25
|
+
# Filter out 'Unknown'
|
26
|
+
genre_ids = [i for i in id3.genres
|
27
|
+
if type(i) is int and id3.genres[i] is not None]
|
28
|
+
genre_ids.sort()
|
29
|
+
|
30
|
+
if args.single_column:
|
31
|
+
for gid in genre_ids:
|
32
|
+
print("%3d: %s" % (gid, id3.genres[gid]))
|
33
|
+
else:
|
34
|
+
offset = int(math.ceil(float(len(genre_ids)) / 2))
|
35
|
+
for i in range(offset):
|
36
|
+
if i < len(genre_ids):
|
37
|
+
c1 = "%3d: %s" % (i, id3.genres[i])
|
38
|
+
else:
|
39
|
+
c1 = ""
|
40
|
+
if (i * 2) < len(genre_ids):
|
41
|
+
try:
|
42
|
+
c2 = "%3d: %s" % (i + offset, id3.genres[i + offset])
|
43
|
+
except IndexError:
|
44
|
+
break
|
45
|
+
else:
|
46
|
+
c2 = ""
|
47
|
+
print(c1 + (" " * (40 - len(c1))) + c2)
|
48
|
+
print("")
|
eyed3/plugins/itunes.py
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
from eyed3.plugins import LoaderPlugin
|
2
|
+
from eyed3.id3.apple import PCST, PCST_FID, WFED, WFED_FID
|
3
|
+
|
4
|
+
|
5
|
+
class Podcast(LoaderPlugin):
|
6
|
+
NAMES = ['itunes-podcast']
|
7
|
+
SUMMARY = "Adds (or removes) the tags necessary for Apple iTunes to "\
|
8
|
+
"identify the file as a podcast."
|
9
|
+
|
10
|
+
def __init__(self, arg_parser):
|
11
|
+
super(Podcast, self).__init__(arg_parser)
|
12
|
+
g = self.arg_group
|
13
|
+
g.add_argument("--add", action="store_true",
|
14
|
+
help="Add the podcast frames.")
|
15
|
+
g.add_argument("--remove", action="store_true",
|
16
|
+
help="Remove the podcast frames.")
|
17
|
+
|
18
|
+
def _add(self, tag):
|
19
|
+
save = False
|
20
|
+
if PCST_FID not in tag.frame_set:
|
21
|
+
tag.frame_set[PCST_FID] = PCST()
|
22
|
+
save = True
|
23
|
+
if WFED_FID not in tag.frame_set:
|
24
|
+
tag.frame_set[WFED_FID] = WFED("http://eyeD3.nicfit.net/")
|
25
|
+
save = True
|
26
|
+
|
27
|
+
if save:
|
28
|
+
print("\tAdding...")
|
29
|
+
tag.save(backup=self.args.backup)
|
30
|
+
self._printStatus(tag)
|
31
|
+
|
32
|
+
def _remove(self, tag):
|
33
|
+
save = False
|
34
|
+
for fid in [PCST_FID, WFED_FID]:
|
35
|
+
try:
|
36
|
+
del tag.frame_set[fid]
|
37
|
+
save = True
|
38
|
+
except KeyError:
|
39
|
+
continue
|
40
|
+
|
41
|
+
if save:
|
42
|
+
print("\tRemoving...")
|
43
|
+
tag.save(backup=self.args.backup)
|
44
|
+
self._printStatus(tag)
|
45
|
+
|
46
|
+
def _printStatus(self, tag):
|
47
|
+
status = ":-("
|
48
|
+
if PCST_FID in tag.frame_set:
|
49
|
+
status = ":-/"
|
50
|
+
if WFED_FID in tag.frame_set:
|
51
|
+
status = ":-)"
|
52
|
+
print("\tiTunes podcast? %s" % status)
|
53
|
+
|
54
|
+
def handleFile(self, f):
|
55
|
+
super(Podcast, self).handleFile(f)
|
56
|
+
|
57
|
+
if self.audio_file and self.audio_file.tag:
|
58
|
+
print(f)
|
59
|
+
tag = self.audio_file.tag
|
60
|
+
self._printStatus(tag)
|
61
|
+
if self.args.remove:
|
62
|
+
self._remove(self.audio_file.tag)
|
63
|
+
elif self.args.add:
|
64
|
+
self._add(self.audio_file.tag)
|
eyed3/plugins/jsontag.py
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
import base64
|
2
|
+
import inspect
|
3
|
+
import dataclasses
|
4
|
+
from json import dumps
|
5
|
+
|
6
|
+
import eyed3.core
|
7
|
+
import eyed3.plugins
|
8
|
+
import eyed3.id3.tag
|
9
|
+
import eyed3.id3.headers
|
10
|
+
|
11
|
+
from eyed3.utils.log import getLogger
|
12
|
+
|
13
|
+
log = getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
class JsonTagPlugin(eyed3.plugins.LoaderPlugin):
|
17
|
+
NAMES = ["json"]
|
18
|
+
SUMMARY = "Outputs all tags as JSON."
|
19
|
+
|
20
|
+
def __init__(self, arg_parser):
|
21
|
+
super().__init__(arg_parser, cache_files=True, track_images=False)
|
22
|
+
g = self.arg_group
|
23
|
+
g.add_argument("-c", "--compact", action="store_true",
|
24
|
+
help="Output in compact form, wound new lines or indentation.")
|
25
|
+
g.add_argument("-s", "--sort", action="store_true", help="Output JSON in sorted by key.")
|
26
|
+
|
27
|
+
def handleFile(self, f, *args, **kwargs):
|
28
|
+
super().handleFile(f)
|
29
|
+
if self.audio_file and self.audio_file.info and self.audio_file.tag:
|
30
|
+
json = audioFileToJson(self.audio_file)
|
31
|
+
print(dumps(json, indent=2 if not self.args.compact else None,
|
32
|
+
sort_keys=self.args.sort))
|
33
|
+
|
34
|
+
|
35
|
+
def audioFileToJson(audio_file):
|
36
|
+
tag = audio_file.tag
|
37
|
+
|
38
|
+
tdict = dict(path=audio_file.path, info=dataclasses.asdict(audio_file.info))
|
39
|
+
|
40
|
+
# Tag fields
|
41
|
+
for name in [m for m in sorted(dir(tag)) if not m.startswith("_") and m not in _tag_exclusions]:
|
42
|
+
member = getattr(tag, name)
|
43
|
+
|
44
|
+
if name not in _tag_map:
|
45
|
+
if not inspect.ismethod(member) and not inspect.isfunction(member):
|
46
|
+
log.warning(f"Unhandled Tag member: {name}")
|
47
|
+
continue
|
48
|
+
elif member is None:
|
49
|
+
continue
|
50
|
+
elif member.__class__ is not _tag_map[name]:
|
51
|
+
log.warning(f"Unexpected type for member {name}: {member.__class__}")
|
52
|
+
continue
|
53
|
+
|
54
|
+
if isinstance(member, (str, int, bool)):
|
55
|
+
tdict[name] = member
|
56
|
+
elif isinstance(member, eyed3.core.Date):
|
57
|
+
tdict[name] = str(member)
|
58
|
+
elif isinstance(member, eyed3.id3.Genre):
|
59
|
+
tdict[name] = member.name
|
60
|
+
elif isinstance(member, bytes):
|
61
|
+
tdict[name] = base64.b64encode(member).decode("ascii")
|
62
|
+
elif isinstance(member, eyed3.id3.tag.ArtistOrigin):
|
63
|
+
... # TODO
|
64
|
+
elif isinstance(member, (eyed3.core.CountAndTotalTuple,)):
|
65
|
+
if any(member):
|
66
|
+
tdict[name] = member._asdict()
|
67
|
+
elif isinstance(member, (list, tuple)):
|
68
|
+
... # TODO
|
69
|
+
elif isinstance(member, eyed3.id3.tag.AccessorBase):
|
70
|
+
... # TODO
|
71
|
+
elif isinstance(member, (eyed3.id3.tag.TagHeader, eyed3.id3.tag.ExtendedTagHeader,
|
72
|
+
eyed3.id3.tag.FileInfo, eyed3.id3.frames.FrameSet)):
|
73
|
+
... # TODO
|
74
|
+
else:
|
75
|
+
log.warning(f"Unhandled tag member {name}, type {member.__class__.__name__})")
|
76
|
+
|
77
|
+
tdict["_eyeD3"] = eyed3.__about__.__version__
|
78
|
+
return tdict
|
79
|
+
|
80
|
+
|
81
|
+
_tag_map = {
|
82
|
+
'album': str,
|
83
|
+
'album_artist': str,
|
84
|
+
'album_type': str,
|
85
|
+
'artist': str,
|
86
|
+
'original_artist': str,
|
87
|
+
'artist_origin': list,
|
88
|
+
'artist_url': str,
|
89
|
+
'audio_file_url': str,
|
90
|
+
'audio_source_url': str,
|
91
|
+
'best_release_date': eyed3.core.Date,
|
92
|
+
'bpm': int,
|
93
|
+
'cd_id': bytes,
|
94
|
+
'chapters': eyed3.id3.tag.ChaptersAccessor,
|
95
|
+
'comments': eyed3.id3.tag.CommentsAccessor,
|
96
|
+
'commercial_url': str,
|
97
|
+
'composer': str,
|
98
|
+
'copyright_url': str,
|
99
|
+
'disc_num': eyed3.core.CountAndTotalTuple,
|
100
|
+
'encoding_date': eyed3.core.Date,
|
101
|
+
'extended_header': eyed3.id3.headers.ExtendedTagHeader,
|
102
|
+
'file_info': eyed3.id3.tag.FileInfo,
|
103
|
+
'frame_set': eyed3.id3.frames.FrameSet,
|
104
|
+
'genre': eyed3.id3.Genre,
|
105
|
+
'header': eyed3.id3.headers.TagHeader,
|
106
|
+
'images': eyed3.id3.tag.ImagesAccessor,
|
107
|
+
'internet_radio_url': str,
|
108
|
+
'lyrics': eyed3.id3.tag.LyricsAccessor,
|
109
|
+
'non_std_genre': eyed3.id3.Genre,
|
110
|
+
'objects': eyed3.id3.tag.ObjectsAccessor,
|
111
|
+
'original_release_date': eyed3.core.Date,
|
112
|
+
'payment_url': str,
|
113
|
+
'play_count': int,
|
114
|
+
'popularities': eyed3.id3.tag.PopularitiesAccessor,
|
115
|
+
'privates': eyed3.id3.tag.PrivatesAccessor,
|
116
|
+
'publisher': str,
|
117
|
+
'publisher_url': str,
|
118
|
+
'recording_date': eyed3.core.Date,
|
119
|
+
'release_date': eyed3.core.Date,
|
120
|
+
'table_of_contents': eyed3.id3.tag.TocAccessor,
|
121
|
+
'tagging_date': eyed3.core.Date,
|
122
|
+
'terms_of_use': str,
|
123
|
+
'title': str,
|
124
|
+
'track_num': eyed3.core.CountAndTotalTuple,
|
125
|
+
'unique_file_ids': eyed3.id3.tag.UniqueFileIdAccessor,
|
126
|
+
'user_text_frames': eyed3.id3.tag.UserTextsAccessor,
|
127
|
+
'user_url_frames': eyed3.id3.tag.UserUrlsAccessor,
|
128
|
+
'version': tuple,
|
129
|
+
}
|
130
|
+
|
131
|
+
_tag_exclusions = {
|
132
|
+
"read_only": bool,
|
133
|
+
}
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import math
|
2
|
+
from eyed3.utils import formatSize
|
3
|
+
from eyed3.utils.console import printMsg, getTtySize
|
4
|
+
from eyed3.plugins import LoaderPlugin
|
5
|
+
|
6
|
+
|
7
|
+
class LameInfoPlugin(LoaderPlugin):
|
8
|
+
NAMES = ["lameinfo", "xing"]
|
9
|
+
SUMMARY = "Outputs lame header (if one exists) for file."
|
10
|
+
DESCRIPTION = (
|
11
|
+
"The 'lame' (or xing) header provides extra information about the mp3 "
|
12
|
+
"that is useful to players and encoders but not officially part of "
|
13
|
+
"the mp3 specification. Variable bit rate mp3s, for example, use this "
|
14
|
+
"header.\n\n"
|
15
|
+
"For more details see `here <http://gabriel.mp3-tech.org/mp3infotag.html>`_"
|
16
|
+
)
|
17
|
+
|
18
|
+
def printHeader(self, file_path):
|
19
|
+
w = getTtySize()[1]
|
20
|
+
printMsg(self._getFileHeader(file_path, w))
|
21
|
+
printMsg(self._getHardRule(w))
|
22
|
+
|
23
|
+
def handleFile(self, f, *_, **__):
|
24
|
+
super().handleFile(f)
|
25
|
+
if self.audio_file is None:
|
26
|
+
return
|
27
|
+
|
28
|
+
self.printHeader(f)
|
29
|
+
if (self.audio_file.info is None
|
30
|
+
or not self.audio_file.info.lame_tag):
|
31
|
+
printMsg("No LAME Tag")
|
32
|
+
return
|
33
|
+
|
34
|
+
lt = self.audio_file.info.lame_tag
|
35
|
+
if "infotag_crc" not in lt:
|
36
|
+
try:
|
37
|
+
printMsg(f"Encoder Version: {lt['encoder_version']}")
|
38
|
+
except KeyError:
|
39
|
+
pass
|
40
|
+
return
|
41
|
+
|
42
|
+
values = [("Encoder Version", lt['encoder_version']),
|
43
|
+
("LAME Tag Revision", lt['tag_revision']),
|
44
|
+
("VBR Method", lt['vbr_method']),
|
45
|
+
("Lowpass Filter", lt['lowpass_filter']),
|
46
|
+
]
|
47
|
+
|
48
|
+
if "replaygain" in lt:
|
49
|
+
try:
|
50
|
+
peak = lt["replaygain"]["peak_amplitude"]
|
51
|
+
db = 20 * math.log10(peak)
|
52
|
+
val = "%.8f (%+.1f dB)" % (peak, db)
|
53
|
+
values.append(("Peak Amplitude", val))
|
54
|
+
except KeyError:
|
55
|
+
pass
|
56
|
+
for type_ in ["radio", "audiofile"]:
|
57
|
+
try:
|
58
|
+
gain = lt["replaygain"][type_]
|
59
|
+
name = "%s Replay Gain" % gain['name'].capitalize()
|
60
|
+
val = "%s dB (%s)" % (gain['adjustment'],
|
61
|
+
gain['originator'])
|
62
|
+
values.append((name, val))
|
63
|
+
except KeyError:
|
64
|
+
pass
|
65
|
+
|
66
|
+
values.append(("Encoding Flags", " ".join((lt["encoding_flags"]))))
|
67
|
+
if lt["nogap"]:
|
68
|
+
values.append(("No Gap", " and ".join(lt["nogap"])))
|
69
|
+
values.append(("ATH Type", lt["ath_type"]))
|
70
|
+
values.append(("Bitrate (%s)" % lt["bitrate"][1], lt["bitrate"][0]))
|
71
|
+
values.append(("Encoder Delay", "%s samples" % lt["encoder_delay"]))
|
72
|
+
values.append(("Encoder Padding", "%s samples" % lt["encoder_padding"]))
|
73
|
+
values.append(("Noise Shaping", lt["noise_shaping"]))
|
74
|
+
values.append(("Stereo Mode", lt["stereo_mode"]))
|
75
|
+
values.append(("Unwise Settings", lt["unwise_settings"]))
|
76
|
+
values.append(("Sample Frequency", lt["sample_freq"]))
|
77
|
+
values.append(("MP3 Gain", "%s (%+.1f dB)" % (lt["mp3_gain"],
|
78
|
+
lt["mp3_gain"] * 1.5)))
|
79
|
+
values.append(("Preset", lt["preset"]))
|
80
|
+
values.append(("Surround Info", lt["surround_info"]))
|
81
|
+
values.append(("Music Length", "%s" % formatSize(lt["music_length"])))
|
82
|
+
values.append(("Music CRC-16", "%04X" % lt["music_crc"]))
|
83
|
+
values.append(("LAME Tag CRC-16", "%04X" % lt["infotag_crc"]))
|
84
|
+
|
85
|
+
for v in values:
|
86
|
+
printMsg(f"{v[0]:<20}: {v[1]}")
|
eyed3/plugins/lastfm.py
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
from pylast import SIZE_EXTRA_LARGE, SIZE_LARGE, SIZE_MEDIUM, SIZE_MEGA, SIZE_SMALL
|
2
|
+
from pylast import LastFMNetwork, WSError
|
3
|
+
|
4
|
+
api_k = "a5f0ac61e7db2481b054ba52ff9a654f"
|
5
|
+
api_s = "0c4a52ae5dcdbba1f9e782833a50b623"
|
6
|
+
_network = None
|
7
|
+
|
8
|
+
|
9
|
+
def Client():
|
10
|
+
global _network
|
11
|
+
if not _network:
|
12
|
+
_network = LastFMNetwork(api_key=api_k, api_secret=api_s)
|
13
|
+
_network.enable_rate_limit()
|
14
|
+
return _network
|
15
|
+
|
16
|
+
|
17
|
+
def getArtist(artist):
|
18
|
+
return Client().get_artist(artist)
|
19
|
+
|
20
|
+
|
21
|
+
def getAlbum(artist, title):
|
22
|
+
return Client().get_album(artist, title)
|
23
|
+
|
24
|
+
|
25
|
+
def getAlbumArt(artist, title, size=SIZE_EXTRA_LARGE):
|
26
|
+
return _getArt(getAlbum(artist, title), size=size)
|
27
|
+
|
28
|
+
|
29
|
+
def getArtistArt(artist, size=SIZE_EXTRA_LARGE):
|
30
|
+
return _getArt(getArtist(artist), size=size)
|
31
|
+
|
32
|
+
|
33
|
+
def _getArt(obj, size=SIZE_EXTRA_LARGE):
|
34
|
+
try:
|
35
|
+
return obj.get_cover_image(size)
|
36
|
+
except WSError:
|
37
|
+
raise ValueError("{} not found.".format(obj.__class__.__name__))
|
38
|
+
|
39
|
+
|
40
|
+
if __name__ == "__main__":
|
41
|
+
album = getAlbum("Melvins", "Houdini")
|
42
|
+
for sz in (SIZE_SMALL, SIZE_MEGA, SIZE_MEDIUM, SIZE_LARGE,
|
43
|
+
SIZE_EXTRA_LARGE):
|
44
|
+
print(album.get_cover_image(sz))
|
45
|
+
|
46
|
+
melvins = getArtist("Melvins")
|
47
|
+
print(melvins)
|
48
|
+
for sz in (SIZE_SMALL, SIZE_MEGA, SIZE_MEDIUM, SIZE_LARGE,
|
49
|
+
SIZE_EXTRA_LARGE):
|
50
|
+
print(melvins.get_cover_image(sz))
|
@@ -0,0 +1,93 @@
|
|
1
|
+
import time
|
2
|
+
import pprint
|
3
|
+
import eyed3
|
4
|
+
import eyed3.utils
|
5
|
+
from pathlib import Path
|
6
|
+
from collections import Counter
|
7
|
+
from eyed3.mimetype import guessMimetype
|
8
|
+
from eyed3.utils.log import getLogger
|
9
|
+
|
10
|
+
log = getLogger(__name__)
|
11
|
+
|
12
|
+
# python-magic
|
13
|
+
try:
|
14
|
+
import magic
|
15
|
+
|
16
|
+
class MagicTypes(magic.Magic):
|
17
|
+
def __init__(self):
|
18
|
+
magic.Magic.__init__(self, mime=True, mime_encoding=False, keep_going=True)
|
19
|
+
|
20
|
+
def guess_type(self, filename, all_types=False):
|
21
|
+
try:
|
22
|
+
types = self.from_file(filename)
|
23
|
+
except UnicodeEncodeError:
|
24
|
+
# https://github.com/ahupp/python-magic/pull/144
|
25
|
+
types = self.from_file(filename.encode("utf-8", 'surrogateescape'))
|
26
|
+
|
27
|
+
delim = r"\012- "
|
28
|
+
if all_types:
|
29
|
+
return types.split(delim)
|
30
|
+
else:
|
31
|
+
return types.split(delim)[0]
|
32
|
+
|
33
|
+
_python_magic = MagicTypes()
|
34
|
+
|
35
|
+
except ImportError:
|
36
|
+
_python_magic = None
|
37
|
+
|
38
|
+
|
39
|
+
class MimetypesPlugin(eyed3.plugins.LoaderPlugin):
|
40
|
+
NAMES = ["mimetypes"]
|
41
|
+
|
42
|
+
def __init__(self, arg_parser):
|
43
|
+
self._num_visited = 0
|
44
|
+
super().__init__(arg_parser, cache_files=False, track_images=False)
|
45
|
+
|
46
|
+
g = self.arg_group
|
47
|
+
g.add_argument("--status", action="store_true", help="Print dot status.")
|
48
|
+
g.add_argument("--parse-files", action="store_true", help="Parse each file.")
|
49
|
+
g.add_argument("--hide-notfound", action="store_true")
|
50
|
+
if _python_magic:
|
51
|
+
g.add_argument("-M", "--use-pymagic", action="store_true",
|
52
|
+
help="Use python-magic to determine mimetype.")
|
53
|
+
self.magic = None
|
54
|
+
self.start_t = None
|
55
|
+
self.mime_types = Counter()
|
56
|
+
|
57
|
+
def start(self, args, config):
|
58
|
+
super().start(args, config)
|
59
|
+
self.magic = "pymagic" if self.args.use_pymagic else "filetype"
|
60
|
+
self.start_t = time.time()
|
61
|
+
|
62
|
+
def handleFile(self, f, *args, **kwargs):
|
63
|
+
|
64
|
+
self._num_visited += 1
|
65
|
+
if self.args.parse_files:
|
66
|
+
try:
|
67
|
+
super().handleFile(f)
|
68
|
+
except Exception as ex:
|
69
|
+
log.critical(ex, exc_info=ex)
|
70
|
+
else:
|
71
|
+
self._num_loaded += 1
|
72
|
+
|
73
|
+
if self.magic == "pymagic":
|
74
|
+
mtype = _python_magic.guess_type(f)
|
75
|
+
else:
|
76
|
+
mtype = guessMimetype(f)
|
77
|
+
|
78
|
+
self.mime_types[mtype] += 1
|
79
|
+
if not self.args.hide_notfound:
|
80
|
+
if mtype is None and Path(f).suffix.lower() in (".mp3",):
|
81
|
+
print("None mimetype:", f)
|
82
|
+
|
83
|
+
if self.args.status:
|
84
|
+
print(".", end="", flush=True)
|
85
|
+
|
86
|
+
def handleDone(self):
|
87
|
+
t = time.time() - self.start_t
|
88
|
+
print(f"\nVisited {self._num_visited} files")
|
89
|
+
print(f"Processed {self._num_loaded} files")
|
90
|
+
print(f"magic: {self.magic}")
|
91
|
+
print(f"time: {eyed3.utils.formatTime(t)} seconds")
|
92
|
+
if self.mime_types:
|
93
|
+
pprint.pprint(self.mime_types)
|
eyed3/plugins/nfo.py
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
import time
|
2
|
+
import eyed3
|
3
|
+
from eyed3.utils.console import printMsg
|
4
|
+
from eyed3.utils import formatSize, formatTime
|
5
|
+
from eyed3.id3 import versionToString
|
6
|
+
from eyed3.plugins import LoaderPlugin
|
7
|
+
|
8
|
+
|
9
|
+
class NfoPlugin(LoaderPlugin):
|
10
|
+
NAMES = ["nfo"]
|
11
|
+
SUMMARY = "Create NFO files for each directory scanned."
|
12
|
+
DESCRIPTION = "Each directory scanned is treated as an album and a "\
|
13
|
+
"`NFO <http://en.wikipedia.org/wiki/.nfo>`_ file is "\
|
14
|
+
"written to standard out.\n\n"\
|
15
|
+
"NFO files are often found in music archives."
|
16
|
+
|
17
|
+
def __init__(self, arg_parser):
|
18
|
+
super(NfoPlugin, self).__init__(arg_parser)
|
19
|
+
self.albums = {}
|
20
|
+
|
21
|
+
def handleFile(self, f):
|
22
|
+
super(NfoPlugin, self).handleFile(f)
|
23
|
+
|
24
|
+
if self.audio_file and self.audio_file.tag:
|
25
|
+
tag = self.audio_file.tag
|
26
|
+
album = tag.album
|
27
|
+
if album and album not in self.albums:
|
28
|
+
self.albums[album] = []
|
29
|
+
self.albums[album].append(self.audio_file)
|
30
|
+
elif album:
|
31
|
+
self.albums[album].append(self.audio_file)
|
32
|
+
|
33
|
+
def handleDone(self):
|
34
|
+
if not self.albums:
|
35
|
+
printMsg("No albums found.")
|
36
|
+
return
|
37
|
+
|
38
|
+
for album in self.albums:
|
39
|
+
audio_files = self.albums[album]
|
40
|
+
if not audio_files:
|
41
|
+
continue
|
42
|
+
audio_files.sort(key=lambda af: (af.tag.track_num[0] or 999,
|
43
|
+
af.tag.track_num[1] or 999))
|
44
|
+
|
45
|
+
max_title_len = 0
|
46
|
+
avg_bitrate = 0
|
47
|
+
encoder_info = ''
|
48
|
+
for audio_file in audio_files:
|
49
|
+
tag = audio_file.tag
|
50
|
+
# Compute maximum title length
|
51
|
+
title_len = len(tag.title or "")
|
52
|
+
if title_len > max_title_len:
|
53
|
+
max_title_len = title_len
|
54
|
+
# Compute average bitrate
|
55
|
+
avg_bitrate += audio_file.info.bit_rate[1]
|
56
|
+
# Grab the last lame version in case not all files have one
|
57
|
+
if "encoder_version" in audio_file.info.lame_tag:
|
58
|
+
version = audio_file.info.lame_tag['encoder_version']
|
59
|
+
encoder_info = (version or encoder_info)
|
60
|
+
avg_bitrate = avg_bitrate / len(audio_files)
|
61
|
+
|
62
|
+
printMsg("")
|
63
|
+
printMsg("Artist : %s" % audio_files[0].tag.artist)
|
64
|
+
printMsg("Album : %s" % album)
|
65
|
+
printMsg("Released : %s" %
|
66
|
+
(audio_files[0].tag.original_release_date or
|
67
|
+
audio_files[0].tag.release_date))
|
68
|
+
printMsg("Recorded : %s" % audio_files[0].tag.recording_date)
|
69
|
+
genre = audio_files[0].tag.genre
|
70
|
+
if genre:
|
71
|
+
genre = genre.name
|
72
|
+
else:
|
73
|
+
genre = ""
|
74
|
+
printMsg("Genre : %s" % genre)
|
75
|
+
|
76
|
+
printMsg("")
|
77
|
+
printMsg("Source : ")
|
78
|
+
printMsg("Encoder : %s" % encoder_info)
|
79
|
+
printMsg("Codec : mp3")
|
80
|
+
printMsg("Bitrate : ~%s K/s @ %s Hz, %s" %
|
81
|
+
(avg_bitrate, audio_files[0].info.sample_freq,
|
82
|
+
audio_files[0].info.mode))
|
83
|
+
printMsg("Tag : ID3 %s" %
|
84
|
+
versionToString(audio_files[0].tag.version))
|
85
|
+
|
86
|
+
printMsg("")
|
87
|
+
printMsg("Ripped By: ")
|
88
|
+
|
89
|
+
printMsg("")
|
90
|
+
printMsg("Track Listing")
|
91
|
+
printMsg("-------------")
|
92
|
+
count = 0
|
93
|
+
total_time = 0
|
94
|
+
total_size = 0
|
95
|
+
for audio_file in audio_files:
|
96
|
+
tag = audio_file.tag
|
97
|
+
count += 1
|
98
|
+
|
99
|
+
title = tag.title or ""
|
100
|
+
title_len = len(title)
|
101
|
+
padding = " " * ((max_title_len - title_len) + 3)
|
102
|
+
time_secs = audio_file.info.time_secs
|
103
|
+
total_time += time_secs
|
104
|
+
total_size += audio_file.info.size_bytes
|
105
|
+
|
106
|
+
zero_pad = "0" * (len(str(len(audio_files))) - len(str(count)))
|
107
|
+
printMsg(" %s%d. %s%s(%s)" %
|
108
|
+
(zero_pad, count, title, padding,
|
109
|
+
formatTime(time_secs)))
|
110
|
+
|
111
|
+
printMsg("")
|
112
|
+
printMsg("Total play time : %s" %
|
113
|
+
formatTime(total_time))
|
114
|
+
printMsg("Total size : %s" %
|
115
|
+
formatSize(total_size))
|
116
|
+
|
117
|
+
printMsg("")
|
118
|
+
printMsg("=" * 78)
|
119
|
+
printMsg(".NFO file created with eyeD3 %s on %s" %
|
120
|
+
(eyed3.version, time.asctime()))
|
121
|
+
printMsg("For more information about eyeD3 go to %s" %
|
122
|
+
"http://eyeD3.nicfit.net/")
|
123
|
+
printMsg("=" * 78)
|
eyed3/plugins/pymod.py
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
import os
|
2
|
+
import importlib.machinery
|
3
|
+
from eyed3.plugins import LoaderPlugin
|
4
|
+
|
5
|
+
_DEFAULT_MOD = "eyeD3mod.py"
|
6
|
+
|
7
|
+
|
8
|
+
class PyModulePlugin(LoaderPlugin):
|
9
|
+
SUMMARY = "Imports a Python module file and calls its functions for the "\
|
10
|
+
"the various plugin events."
|
11
|
+
DESCRIPTION = f'''
|
12
|
+
If no module if provided a file named {_DEFAULT_MOD} in the current working directory is
|
13
|
+
imported. If any of the following methods exist they still be invoked:
|
14
|
+
|
15
|
+
def audioFile(audio_file):
|
16
|
+
"""Invoked for every audio file that is encountered. The ``audio_file``
|
17
|
+
is of type ``eyed3.core.AudioFile``; currently this is the concrete type
|
18
|
+
``eyed3.mp3.Mp3AudioFile``."""
|
19
|
+
pass
|
20
|
+
|
21
|
+
def audioDir(d, audio_files, images):
|
22
|
+
"""This function is invoked for any directory (``d``) that contains audio
|
23
|
+
(``audio_files``) or image (``images``) media."""
|
24
|
+
pass
|
25
|
+
|
26
|
+
def done():
|
27
|
+
"""This method is invoke before successful exit."""
|
28
|
+
pass
|
29
|
+
'''
|
30
|
+
NAMES = ["pymod"]
|
31
|
+
|
32
|
+
def __init__(self, arg_parser):
|
33
|
+
super(PyModulePlugin, self).__init__(arg_parser, cache_files=True,
|
34
|
+
track_images=True)
|
35
|
+
self._mod = None
|
36
|
+
self.arg_group.add_argument("-m", "--module", dest="module",
|
37
|
+
help="The Python module module to invoke. "
|
38
|
+
f"The default is ./{_DEFAULT_MOD}")
|
39
|
+
|
40
|
+
def start(self, args, config):
|
41
|
+
mod_file = args.module or _DEFAULT_MOD
|
42
|
+
try:
|
43
|
+
mod_name = os.path.splitext(os.path.basename(mod_file))[0]
|
44
|
+
loader = importlib.machinery.SourceFileLoader(mod_name, mod_file)
|
45
|
+
mod = loader.load_module()
|
46
|
+
self._mod = mod
|
47
|
+
except IOError:
|
48
|
+
raise IOError("Module file not found: %s" % mod_file)
|
49
|
+
except (NameError, ImportError, SyntaxError) as ex:
|
50
|
+
raise IOError("Module load error: %s" % str(ex))
|
51
|
+
|
52
|
+
def handleFile(self, f):
|
53
|
+
super(PyModulePlugin, self).handleFile(f)
|
54
|
+
if not self.audio_file:
|
55
|
+
return
|
56
|
+
|
57
|
+
if "audioFile" in dir(self._mod):
|
58
|
+
self._mod.audioFile(self.audio_file)
|
59
|
+
|
60
|
+
def handleDirectory(self, d, _):
|
61
|
+
if not self._file_cache and not self._dir_images:
|
62
|
+
return
|
63
|
+
|
64
|
+
if "audioDir" in dir(self._mod):
|
65
|
+
self._mod.audioDir(d, self._file_cache, self._dir_images)
|
66
|
+
|
67
|
+
super(PyModulePlugin, self).handleDirectory(d, _)
|
68
|
+
|
69
|
+
def handleDone(self):
|
70
|
+
super(PyModulePlugin, self).handleDone()
|
71
|
+
if "done" in dir(self._mod):
|
72
|
+
self._mod.done()
|