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/main.py ADDED
@@ -0,0 +1,305 @@
1
+ import os
2
+ import sys
3
+ import textwrap
4
+ import warnings
5
+ import deprecation
6
+
7
+ from io import StringIO
8
+ from configparser import ConfigParser
9
+ from configparser import Error as ConfigParserError
10
+
11
+ import eyed3
12
+ import eyed3.utils
13
+ import eyed3.utils.console
14
+ import eyed3.plugins
15
+ import eyed3.__about__
16
+
17
+ from eyed3.utils.log import initLogging
18
+
19
+ DEFAULT_PLUGIN = "classic"
20
+ DEFAULT_CONFIG = os.path.expandvars("${HOME}/.config/eyeD3/config.ini")
21
+ USER_PLUGINS_DIR = os.path.expandvars("${HOME}/.config/eyeD3/plugins")
22
+ DEFAULT_CONFIG_DEPRECATED = os.path.expandvars("${HOME}/.eyeD3/config.ini")
23
+ USER_PLUGINS_DIR_DEPRECATED = os.path.expandvars("${HOME}/.eyeD3/plugins")
24
+
25
+
26
+ def main(args, config):
27
+ if "list_plugins" in args and args.list_plugins:
28
+ _listPlugins(config)
29
+ return 0
30
+
31
+ args.plugin.start(args, config)
32
+
33
+ recursive = False
34
+ if "non_recursive" in args:
35
+ recursive = not args.non_recursive
36
+ elif "recursive" in args:
37
+ recursive = args.recursive
38
+
39
+ # Process paths (files/directories)
40
+ for p in args.paths:
41
+ eyed3.utils.walk(args.plugin, p, excludes=args.excludes, fs_encoding=args.fs_encoding,
42
+ recursive=recursive)
43
+
44
+ retval = args.plugin.handleDone()
45
+
46
+ return retval or 0
47
+
48
+
49
+ def _listPlugins(config):
50
+ from eyed3.utils.console import Fore, Style
51
+
52
+ def header(name):
53
+ is_default = name == DEFAULT_PLUGIN
54
+ return (Style.BRIGHT + (Fore.GREEN if is_default else '') + "* " +
55
+ name + Style.RESET_ALL)
56
+
57
+ all_plugins = eyed3.plugins.load(reload=True, paths=_getPluginPath(config))
58
+ # Create a new dict for sorted display
59
+ plugin_names = []
60
+ for plugin in set(all_plugins.values()):
61
+ plugin_names.append(plugin.NAMES[0])
62
+
63
+ print("\nType 'eyeD3 --plugin=<name> --help' for more help\n")
64
+
65
+ plugin_names.sort()
66
+ for name in plugin_names:
67
+ plugin = all_plugins[name]
68
+
69
+ alt_names = plugin.NAMES[1:]
70
+ alt_names = f" ({', '.join(alt_names)})" if alt_names else ""
71
+
72
+ print(f"{header(name)} {alt_names}:")
73
+ for txt in textwrap.wrap(plugin.SUMMARY, initial_indent=' ' * 2, subsequent_indent=' ' * 2):
74
+ print(f"{Fore.YELLOW}{txt}{Style.RESET_ALL}")
75
+ print("")
76
+
77
+
78
+ @deprecation.deprecated(deprecated_in="0.9a2", removed_in="1.0",
79
+ current_version=eyed3.__about__.__version__,
80
+ details=f"Default eyeD3 config moved to {DEFAULT_CONFIG}")
81
+ def _deprecatedConfigFileCheck(_):
82
+ """This here to add deprecation."""
83
+
84
+
85
+ def _loadConfig(args):
86
+ config_files = []
87
+
88
+ if args.config:
89
+ config_files.append(os.path.abspath(args.config))
90
+
91
+ if args.no_config is False:
92
+ config_files.append(DEFAULT_CONFIG)
93
+ config_files.append(DEFAULT_CONFIG_DEPRECATED)
94
+
95
+ if not config_files:
96
+ return None
97
+
98
+ for config_file in config_files:
99
+ if os.path.isfile(config_file):
100
+ _deprecatedConfigFileCheck(config_file)
101
+
102
+ try:
103
+ config = ConfigParser()
104
+ config.read(config_file)
105
+ except ConfigParserError as ex:
106
+ eyed3.log.warning(f"User config error: {ex}")
107
+ return None
108
+ else:
109
+ return config
110
+ elif config_file != DEFAULT_CONFIG and config_file != DEFAULT_CONFIG_DEPRECATED:
111
+ raise IOError(f"User config not found: {config_file}")
112
+
113
+
114
+ def _getPluginPath(config):
115
+ plugin_path = [USER_PLUGINS_DIR]
116
+
117
+ if config and config.has_option("default", "plugin_path"):
118
+ val = config.get("default", "plugin_path")
119
+ plugin_path += [os.path.expanduser(os.path.expandvars(d)) for d
120
+ in val.split(':') if val]
121
+ return plugin_path
122
+
123
+
124
+ def profileMain(args, config): # pragma: no cover
125
+ """This is the main function for profiling
126
+ http://code.google.com/appengine/kb/commontasks.html#profiling
127
+ """
128
+ import cProfile
129
+ import pstats
130
+
131
+ eyed3.log.debug("driver profileMain")
132
+ prof = cProfile.Profile()
133
+ prof = prof.runctx("main(args)", globals(), locals())
134
+
135
+ stream = StringIO()
136
+ stats = pstats.Stats(prof, stream=stream)
137
+ stats.sort_stats("time") # Or cumulative
138
+ stats.print_stats(100) # 80 = how many to print
139
+
140
+ # The rest is optional.
141
+ stats.print_callees()
142
+ stats.print_callers()
143
+ sys.stderr.write("Profile data:\n%s\n" % stream.getvalue())
144
+
145
+ return 0
146
+
147
+
148
+ def setFileScannerOpts(arg_parser, default_recursive=False, paths_metavar="PATH",
149
+ paths_help="Files or directory paths"):
150
+
151
+ if default_recursive is False:
152
+ arg_parser.add_argument("-r", "--recursive", action="store_true", dest="recursive",
153
+ help="Recurse into subdirectories.")
154
+ else:
155
+ arg_parser.add_argument("-R", "--non-recursive", action="store_true", dest="non_recursive",
156
+ help="Do not recurse into subdirectories.")
157
+
158
+ arg_parser.add_argument("--exclude", action="append", metavar="PATTERN", dest="excludes",
159
+ help="A regular expression for path exclusion. May be specified "
160
+ "multiple times.")
161
+ arg_parser.add_argument("--fs-encoding", action="store", dest="fs_encoding",
162
+ default=eyed3.LOCAL_FS_ENCODING, metavar="ENCODING",
163
+ help="Use the specified file system encoding for filenames. "
164
+ f"Default as it was detected is '{eyed3.LOCAL_FS_ENCODING}' but "
165
+ "this option is still useful when reading from mounted file "
166
+ "systems.")
167
+ arg_parser.add_argument("paths", metavar=paths_metavar, nargs="*", help=paths_help)
168
+
169
+
170
+ def makeCmdLineParser(subparser=None):
171
+ from eyed3.utils import ArgumentParser
172
+
173
+ p = ArgumentParser(prog=eyed3.__about__.__project_name__, add_help=True)\
174
+ if not subparser else subparser
175
+
176
+ setFileScannerOpts(p)
177
+
178
+ p.add_argument("-L", "--plugins", action="store_true", default=False,
179
+ dest="list_plugins", help="List all available plugins")
180
+ p.add_argument("-P", "--plugin", action="store", dest="plugin",
181
+ default=None, metavar="NAME",
182
+ help=f"Specify which plugin to use. The default is '{DEFAULT_PLUGIN}'")
183
+ p.add_argument("-C", "--config", action="store", dest="config",
184
+ default=None, metavar="FILE",
185
+ help="Supply a configuration file. The default is "
186
+ f"'{DEFAULT_CONFIG}', although even that is optional.")
187
+ p.add_argument("--backup", action="store_true", dest="backup",
188
+ help="Plugins should honor this option such that "
189
+ "a backup is made of any file modified. The backup "
190
+ "is made in same directory with a '.orig' "
191
+ "extension added.")
192
+ p.add_argument("-Q", "--quiet", action="store_true", dest="quiet",
193
+ default=False, help="A hint to plugins to output less.")
194
+ p.add_argument("--no-color", action="store_true", dest="no_color",
195
+ help="Suppress color codes in console output. "
196
+ "This will happen automatically if the output is "
197
+ "not a TTY (e.g. when redirecting to a file)")
198
+ p.add_argument("--no-config",
199
+ action="store_true", dest="no_config",
200
+ help=f"Do not load the default user config '{DEFAULT_CONFIG}'. "
201
+ "The -c/--config options are still honored if present.")
202
+
203
+ return p
204
+
205
+
206
+ def parseCommandLine(cmd_line_args=None):
207
+
208
+ cmd_line_args = list(cmd_line_args) if cmd_line_args else list(sys.argv[1:])
209
+
210
+ # Remove any options not related to plugin/config for first parse. These
211
+ # determine the parser for the next stage.
212
+ stage_one_args = []
213
+ idx, auto_append = 0, False
214
+ while idx < len(cmd_line_args):
215
+ opt = cmd_line_args[idx]
216
+ if auto_append:
217
+ stage_one_args.append(opt)
218
+ auto_append = False
219
+
220
+ if opt in ("-C", "--config", "-P", "--plugin", "--no-config"):
221
+ stage_one_args.append(opt)
222
+ if opt != "--no-config":
223
+ auto_append = True
224
+ elif (opt.startswith("-C=") or opt.startswith("--config=") or
225
+ opt.startswith("-P=") or opt.startswith("--plugin=")):
226
+ stage_one_args.append(opt)
227
+ idx += 1
228
+
229
+ parser = makeCmdLineParser()
230
+ args = parser.parse_args(stage_one_args)
231
+
232
+ config = _loadConfig(args)
233
+
234
+ if args.plugin:
235
+ # Plugin on the command line takes precedence over config.
236
+ plugin_name = args.plugin
237
+ elif config and config.has_option("default", "plugin"):
238
+ # Get default plugin from config or use DEFAULT_CONFIG
239
+ plugin_name = config.get("default", "plugin")
240
+ if not plugin_name:
241
+ plugin_name = DEFAULT_PLUGIN
242
+ else:
243
+ plugin_name = DEFAULT_PLUGIN
244
+
245
+ PluginClass = eyed3.plugins.load(plugin_name, paths=_getPluginPath(config))
246
+ if PluginClass is None:
247
+ eyed3.utils.console.printError("Plugin not found: %s" % plugin_name)
248
+ parser.exit(1)
249
+ plugin = PluginClass(parser)
250
+
251
+ if config and config.has_option("default", "options"):
252
+ cmd_line_args.extend(config.get("default", "options").split())
253
+ if config and config.has_option(plugin_name, "options"):
254
+ cmd_line_args.extend(config.get(plugin_name, "options").split())
255
+
256
+ # Re-parse the command line including options from the config.
257
+ args = parser.parse_args(args=cmd_line_args)
258
+
259
+ args.plugin = plugin
260
+ eyed3.log.debug("command line args: %s", args)
261
+ eyed3.log.debug("plugin is: %s", plugin)
262
+
263
+ return args, parser, config
264
+
265
+
266
+ def _main():
267
+ """Entry point"""
268
+ initLogging()
269
+
270
+ args = None
271
+ try:
272
+ args, _, config = parseCommandLine()
273
+
274
+ eyed3.utils.console.AnsiCodes.init(not args.no_color)
275
+
276
+ mainFunc = main if args.debug_profile is False else profileMain
277
+ retval = mainFunc(args, config)
278
+ except KeyboardInterrupt:
279
+ retval = 0
280
+ except (StopIteration, IOError) as ex:
281
+ eyed3.utils.console.printError(str(ex))
282
+ retval = 1
283
+ except Exception as ex:
284
+ eyed3.utils.console.printError(f"Uncaught exception: {ex}\n")
285
+ eyed3.log.exception(ex)
286
+ retval = 1
287
+
288
+ if args.debug_pdb:
289
+ try:
290
+ with warnings.catch_warnings():
291
+ warnings.simplefilter("ignore", PendingDeprecationWarning)
292
+ # Must delay the import of ipdb as say as possible because
293
+ # of https://github.com/gotcha/ipdb/issues/48
294
+ import ipdb as pdb # noqa
295
+ except ImportError:
296
+ import pdb # noqa
297
+
298
+ e, m, tb = sys.exc_info()
299
+ pdb.post_mortem(tb)
300
+
301
+ sys.exit(retval)
302
+
303
+
304
+ if __name__ == "__main__": # pragma: no cover
305
+ _main()
eyed3/mimetype.py ADDED
@@ -0,0 +1,107 @@
1
+ import pathlib
2
+ import filetype
3
+ from io import BytesIO
4
+ from .id3 import ID3_MIME_TYPE, ID3_MIME_TYPE_EXTENSIONS
5
+ from .mp3 import MIME_TYPES as MP3_MIME_TYPES
6
+ from .utils.log import getLogger
7
+ from filetype.utils import _NUM_SIGNATURE_BYTES
8
+
9
+ log = getLogger(__name__)
10
+
11
+
12
+ def guessMimetype(filename):
13
+ """Return the mime-type for `filename`."""
14
+
15
+ path = pathlib.Path(filename) if not isinstance(filename, pathlib.Path) else filename
16
+
17
+ with path.open("rb") as signature:
18
+ # Since filetype only reads 262 of file many mp3s starting with null bytes will not find
19
+ # a header, so ignoring null bytes and using the bytes interface...
20
+ buf = b""
21
+ while not buf:
22
+ data = signature.read(_NUM_SIGNATURE_BYTES)
23
+ if not data:
24
+ break
25
+
26
+ data = data.lstrip(b"\x00")
27
+ if data:
28
+ data_len = len(data)
29
+ if data_len >= _NUM_SIGNATURE_BYTES:
30
+ buf = data[:_NUM_SIGNATURE_BYTES]
31
+ else:
32
+ buf = data + signature.read(_NUM_SIGNATURE_BYTES - data_len)
33
+
34
+ # Special casing .id3/.tag because extended filetype with add_type() prepends, meaning
35
+ # all mp3 would be labeled mimetype id3, while appending would mean each .id3 would be
36
+ # mime mpeg.
37
+ if path.suffix in ID3_MIME_TYPE_EXTENSIONS:
38
+ if Id3Tag().match(buf) or Id3TagExt().match(buf):
39
+ return Id3TagExt.MIME
40
+
41
+ return filetype.guess_mime(buf)
42
+
43
+
44
+ class Mp2x(filetype.Type):
45
+ """Implements the MP2.x audio type matcher."""
46
+ MIME = MP3_MIME_TYPES[0]
47
+ EXTENSION = "mp3"
48
+
49
+ def __init__(self):
50
+ super().__init__(mime=self.__class__.MIME, extension=self.__class__.EXTENSION)
51
+
52
+ def match(self, buf):
53
+ from .mp3.headers import findHeader
54
+
55
+ return (len(buf) > 2 and
56
+ buf[0] == 0xff and buf[1] in (0xf3, 0xe3) and
57
+ findHeader(BytesIO(buf), 0)[1])
58
+
59
+
60
+ class Mp3Invalids(filetype.Type):
61
+ """Implements a MP3 audio type matcher this is odd or/corrupt mp3."""
62
+ MIME = MP3_MIME_TYPES[0]
63
+ EXTENSION = "mp3"
64
+
65
+ def __init__(self):
66
+ super().__init__(mime=self.__class__.MIME, extension=self.__class__.EXTENSION)
67
+
68
+ def match(self, buf):
69
+ from .mp3.headers import findHeader
70
+
71
+ header = findHeader(BytesIO(buf), 0)[1]
72
+ log.debug(f"Mp3Invalid, found: {header}")
73
+ return bool(header)
74
+
75
+
76
+ class Id3Tag(filetype.Type):
77
+ """Implements a MP3 audio type matcher this is odd or/corrupt mp3."""
78
+ MIME = ID3_MIME_TYPE
79
+ EXTENSION = "id3"
80
+
81
+ def __init__(self):
82
+ super().__init__(mime=self.__class__.MIME, extension=self.__class__.EXTENSION)
83
+
84
+ def match(self, buf):
85
+ return buf[:3] in (b"ID3", b"TAG") or len(buf) == 0
86
+
87
+
88
+ class Id3TagExt(Id3Tag):
89
+ EXTENSION = "tag"
90
+
91
+
92
+ class M3u(filetype.Type):
93
+ """Implements the m3u playlist matcher."""
94
+ MIME = "audio/x-mpegurl"
95
+ EXTENSION = "m3u"
96
+
97
+ def __init__(self):
98
+ super().__init__(mime=self.__class__.MIME, extension=self.__class__.EXTENSION)
99
+
100
+ def match(self, buf):
101
+ return len(buf) > 6 and buf.startswith(b"#EXTM3U")
102
+
103
+
104
+ # Not using `add_type()`, to append
105
+ filetype.types.append(Mp2x())
106
+ filetype.types.append(M3u())
107
+ filetype.types.append(Mp3Invalids())
eyed3/mp3/__init__.py ADDED
@@ -0,0 +1,188 @@
1
+ import os
2
+ import re
3
+ import stat
4
+
5
+ from .. import Error
6
+ from .. import id3
7
+ from .. import core
8
+
9
+ from ..utils.log import getLogger
10
+ log = getLogger(__name__)
11
+
12
+
13
+ class Mp3Exception(Error):
14
+ """Used to signal mp3-related errors."""
15
+ pass
16
+
17
+
18
+ NAME = "mpeg"
19
+ # Mime-types that are recognized at MP3
20
+ MIME_TYPES = ["audio/mpeg", "audio/mp3", "audio/x-mp3", "audio/x-mpeg",
21
+ "audio/mpeg3", "audio/x-mpeg3", "audio/mpg", "audio/x-mpg",
22
+ "audio/x-mpegaudio", "audio/mpegapplication/x-tar",
23
+ ]
24
+
25
+ # Mime-types that have been seen to contain mp3 data.
26
+ OTHER_MIME_TYPES = ['application/octet-stream', # ???
27
+ 'audio/x-hx-aac-adts', # ???
28
+ 'audio/x-wav', # RIFF wrapped mp3s
29
+ ]
30
+
31
+ # Valid file extensions.
32
+ EXTENSIONS = [".mp3"]
33
+
34
+
35
+ class Mp3AudioInfo(core.AudioInfo):
36
+ def __init__(self, file_obj, start_offset, tag):
37
+ from . import headers
38
+ from .headers import timePerFrame
39
+
40
+ log.debug("mp3 header search starting @ %x" % start_offset)
41
+
42
+ self.mp3_header = None
43
+ self.xing_header = None
44
+ self.vbri_header = None
45
+ # If not ``None``, the Lame header.
46
+ # See :class:`eyed3.mp3.headers.LameHeader`
47
+ self.lame_tag = None
48
+ # 2-tuple, (vrb?:boolean, bitrate:int)
49
+ self.bit_rate = (None, None)
50
+
51
+ header_pos = 0
52
+ while self.mp3_header is None:
53
+ # Find first mp3 header
54
+ (header_pos,
55
+ header_int,
56
+ header_bytes) = headers.findHeader(file_obj, start_offset)
57
+ if not header_int:
58
+ try:
59
+ fname = file_obj.name
60
+ except AttributeError:
61
+ fname = 'unknown'
62
+ raise headers.Mp3Exception(
63
+ "Unable to find a valid mp3 frame in '%s'" % fname)
64
+
65
+ try:
66
+ self.mp3_header = headers.Mp3Header(header_int)
67
+ log.debug("mp3 header %x found at position: 0x%x" %
68
+ (header_int, header_pos))
69
+ except headers.Mp3Exception as ex:
70
+ log.debug("Invalid mp3 header: %s" % str(ex))
71
+ # keep looking...
72
+ start_offset += 4
73
+
74
+ file_obj.seek(header_pos)
75
+ mp3_frame = file_obj.read(self.mp3_header.frame_length)
76
+ if re.compile(b'Xing|Info').search(mp3_frame):
77
+ # Check for Xing/Info header information.
78
+ self.xing_header = headers.XingHeader()
79
+ if not self.xing_header.decode(mp3_frame):
80
+ log.debug("Ignoring corrupt Xing header")
81
+ self.xing_header = None
82
+ elif mp3_frame.find(b'VBRI') >= 0:
83
+ # Check for VBRI header information.
84
+ self.vbri_header = headers.VbriHeader()
85
+ if not self.vbri_header.decode(mp3_frame):
86
+ log.debug("Ignoring corrupt VBRI header")
87
+ self.vbri_header = None
88
+
89
+ # Check for LAME Tag
90
+ self.lame_tag = headers.LameHeader(mp3_frame)
91
+
92
+ # Set file size
93
+ size_bytes = os.stat(file_obj.name)[stat.ST_SIZE]
94
+
95
+ # Compute track play time.
96
+ if self.xing_header and self.xing_header.vbr:
97
+ tpf = timePerFrame(self.mp3_header, True)
98
+ time_secs = tpf * self.xing_header.numFrames
99
+ elif self.vbri_header and self.vbri_header.version == 1:
100
+ tpf = timePerFrame(self.mp3_header, True)
101
+ time_secs = tpf * self.vbri_header.num_frames
102
+ else:
103
+ tpf = timePerFrame(self.mp3_header, False)
104
+ length = size_bytes
105
+ if tag and tag.isV2():
106
+ length -= tag.header.SIZE + tag.header.tag_size
107
+ # Handle the case where there is a v2 tag and a v1 tag.
108
+ file_obj.seek(-128, 2)
109
+ if file_obj.read(3) == "TAG":
110
+ length -= 128
111
+ elif tag and tag.isV1():
112
+ length -= 128
113
+ time_secs = (length / self.mp3_header.frame_length) * tpf
114
+
115
+ # Compute bitrate
116
+ if (self.xing_header and self.xing_header.vbr and
117
+ self.xing_header.numFrames): # if xing_header.numFrames == 0, ZeroDivisionError
118
+ br = int((self.xing_header.numBytes * 8) /
119
+ (tpf * self.xing_header.numFrames * 1000))
120
+ vbr = True
121
+ else:
122
+ br = self.mp3_header.bit_rate
123
+ vbr = False
124
+ self.bit_rate = (vbr, br)
125
+
126
+ self.sample_freq = self.mp3_header.sample_freq
127
+ self.mode = self.mp3_header.mode
128
+
129
+ super().__init__(time_secs, size_bytes)
130
+
131
+ ##
132
+ # Helper to get the bitrate as a string. The prefix '~' is used to denote
133
+ # variable bit rates.
134
+ @property
135
+ def bit_rate_str(self):
136
+ (vbr, bit_rate) = self.bit_rate
137
+ return f"{'~' if vbr else ''}{bit_rate} kb/s"
138
+
139
+
140
+ class Mp3AudioFile(core.AudioFile):
141
+ """Audio file container for mp3 files."""
142
+
143
+ def __init__(self, path, version=id3.ID3_ANY_VERSION):
144
+ self._tag_version = version
145
+
146
+ super().__init__(path)
147
+ assert self.type == core.AUDIO_MP3
148
+
149
+ def _read(self):
150
+ with open(self.path, "rb") as file_obj:
151
+ self._tag = id3.Tag()
152
+ tag_found = self._tag.parse(file_obj, self._tag_version)
153
+
154
+ # Compute offset for starting mp3 data search
155
+ if tag_found and self._tag.isV1():
156
+ mp3_offset = 0
157
+ elif tag_found and self._tag.isV2():
158
+ mp3_offset = self._tag.header.SIZE + self._tag.header.tag_size
159
+ else:
160
+ mp3_offset = 0
161
+ self._tag = None
162
+
163
+ try:
164
+ self._info = Mp3AudioInfo(file_obj, mp3_offset, self._tag)
165
+ except Mp3Exception as ex:
166
+ # Only logging a warning here since we can still operate on
167
+ # the tag.
168
+ log.warning(ex)
169
+ self._info = None
170
+
171
+ self.type = core.AUDIO_MP3
172
+
173
+ def initTag(self, version=id3.ID3_DEFAULT_VERSION):
174
+ """Add a id3.Tag to the file (removing any existing tag if one exists)."""
175
+ self.tag = id3.Tag()
176
+ self.tag.version = version
177
+ self.tag.file_info = id3.FileInfo(self.path)
178
+ return self.tag
179
+
180
+ @core.AudioFile.tag.setter
181
+ def tag(self, t):
182
+ if t:
183
+ t.file_info = id3.FileInfo(self.path)
184
+ if self._tag and self._tag.file_info:
185
+ t.file_info.tag_size = self._tag.file_info.tag_size
186
+ t.file_info.tag_padding_size = \
187
+ self._tag.file_info.tag_padding_size
188
+ self._tag = t