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.
@@ -0,0 +1,1173 @@
1
+ import os
2
+ import re
3
+ import dataclasses
4
+ from functools import partial
5
+ from argparse import ArgumentTypeError
6
+
7
+ from eyed3.plugins import LoaderPlugin
8
+ from eyed3 import core, id3, mp3
9
+ from eyed3.utils import makeUniqueFileName, b, formatTime
10
+ from eyed3.utils.console import (
11
+ printMsg, printError, printWarning, boldText, getTtySize,
12
+ )
13
+ from eyed3.id3.frames import ImageFrame
14
+ from eyed3.mimetype import guessMimetype
15
+
16
+ from eyed3.utils.log import getLogger
17
+ log = getLogger(__name__)
18
+
19
+ FIELD_DELIM = ':'
20
+
21
+ DEFAULT_MAX_PADDING = 64 * 1024
22
+
23
+
24
+ class ClassicPlugin(LoaderPlugin):
25
+ SUMMARY = "Classic eyeD3 interface for viewing and editing tags."
26
+ DESCRIPTION = """
27
+ All PATH arguments are parsed and displayed. Directory paths are searched
28
+ recursively. Any editing options (--artist, --title) are applied to each file
29
+ read.
30
+
31
+ All date options (-Y, --release-year excepted) follow ISO 8601 format. This is
32
+ ``yyyy-mm-ddThh:mm:ss``. The year is required, and each component thereafter is
33
+ optional. For example, 2012-03 is valid, 2012--12 is not.
34
+ """
35
+ NAMES = ["classic"]
36
+
37
+ def __init__(self, arg_parser):
38
+ super(ClassicPlugin, self).__init__(arg_parser)
39
+ g = self.arg_group
40
+
41
+ def PositiveIntArg(i):
42
+ i = int(i)
43
+ if i < 0:
44
+ raise ArgumentTypeError("positive number required")
45
+ return i
46
+
47
+ # Common options
48
+ g.add_argument("-a", "--artist", dest="artist",
49
+ metavar="STRING", help=ARGS_HELP["--artist"])
50
+ g.add_argument("-A", "--album", dest="album",
51
+ metavar="STRING", help=ARGS_HELP["--album"])
52
+ g.add_argument("-b", "--album-artist",
53
+ dest="album_artist", metavar="STRING",
54
+ help=ARGS_HELP["--album-artist"])
55
+ g.add_argument("-t", "--title", dest="title",
56
+ metavar="STRING", help=ARGS_HELP["--title"])
57
+ g.add_argument("-n", "--track", type=PositiveIntArg, dest="track",
58
+ metavar="NUM", help=ARGS_HELP["--track"])
59
+ g.add_argument("-N", "--track-total", type=PositiveIntArg,
60
+ dest="track_total", metavar="NUM",
61
+ help=ARGS_HELP["--track-total"])
62
+
63
+ g.add_argument("--track-offset", type=int, dest="track_offset",
64
+ metavar="N", help=ARGS_HELP["--track-offset"])
65
+
66
+ g.add_argument("--composer", dest="composer",
67
+ metavar="STRING", help=ARGS_HELP["--composer"])
68
+ g.add_argument("--orig-artist", dest="orig_artist",
69
+ metavar="STRING", help=ARGS_HELP["--orig-artist"])
70
+ g.add_argument("-d", "--disc-num", type=PositiveIntArg, dest="disc_num",
71
+ metavar="NUM", help=ARGS_HELP["--disc-num"])
72
+ g.add_argument("-D", "--disc-total", type=PositiveIntArg,
73
+ dest="disc_total", metavar="NUM",
74
+ help=ARGS_HELP["--disc-total"])
75
+ g.add_argument("-G", "--genre", dest="genre",
76
+ metavar="GENRE", help=ARGS_HELP["--genre"])
77
+ g.add_argument("--non-std-genres", dest="non_std_genres",
78
+ action="store_true", help=ARGS_HELP["--non-std-genres"])
79
+ g.add_argument("-Y", "--release-year", type=PositiveIntArg,
80
+ dest="release_year", metavar="YEAR",
81
+ help=ARGS_HELP["--release-year"])
82
+ g.add_argument("-c", "--comment", dest="simple_comment",
83
+ metavar="STRING",
84
+ help=ARGS_HELP["--comment"])
85
+ g.add_argument("--artist-city", metavar="STRING",
86
+ help="The artist's city of origin. "
87
+ f"Stored as a user text frame `{core.TXXX_ARTIST_ORIGIN}`")
88
+ g.add_argument("--artist-state", metavar="STRING",
89
+ help="The artist's state of origin. "
90
+ f"Stored as a user text frame `{core.TXXX_ARTIST_ORIGIN}`")
91
+ g.add_argument("--artist-country", metavar="STRING",
92
+ help="The artist's country of origin. "
93
+ f"Stored as a user text frame `{core.TXXX_ARTIST_ORIGIN}`")
94
+ g.add_argument("--rename", dest="rename_pattern", metavar="PATTERN",
95
+ help=ARGS_HELP["--rename"])
96
+
97
+ gid3 = arg_parser.add_argument_group("ID3 options")
98
+
99
+ def _splitArgs(arg, maxsplit=None):
100
+ NEW_DELIM = "#DELIM#"
101
+ arg = re.sub(r"\\%s" % FIELD_DELIM, NEW_DELIM, arg)
102
+ t = tuple(re.sub(NEW_DELIM, FIELD_DELIM, s)
103
+ for s in arg.split(FIELD_DELIM))
104
+ if maxsplit is not None and maxsplit < 2:
105
+ raise ValueError("Invalid maxsplit value: {}".format(maxsplit))
106
+ elif maxsplit and len(t) > maxsplit:
107
+ t = t[:maxsplit - 1] + (FIELD_DELIM.join(t[maxsplit - 1:]),)
108
+ assert len(t) <= maxsplit
109
+ return t
110
+
111
+ def DescLangArg(arg):
112
+ """DESCRIPTION[:LANG]"""
113
+ vals = _splitArgs(arg, 2)
114
+ desc = vals[0]
115
+ lang = vals[1] if len(vals) > 1 else id3.DEFAULT_LANG
116
+ return desc, b(lang)[:3] or id3.DEFAULT_LANG
117
+
118
+ def DescTextArg(arg):
119
+ """DESCRIPTION:TEXT"""
120
+ vals = _splitArgs(arg, 2)
121
+ desc = vals[0].strip()
122
+ text = FIELD_DELIM.join(vals[1:] if len(vals) > 1 else [])
123
+ return desc or "", text or ""
124
+ KeyValueArg = DescTextArg
125
+
126
+ def DescUrlArg(arg):
127
+ desc, url = DescTextArg(arg)
128
+ return desc, url.encode("latin1")
129
+
130
+ def FidArg(arg):
131
+ fid = arg.strip().encode("ascii")
132
+ if not fid:
133
+ raise ArgumentTypeError("No frame ID")
134
+ return fid
135
+
136
+ def TextFrameArg(arg):
137
+ """FID:TEXT"""
138
+ vals = _splitArgs(arg, 2)
139
+ fid = vals[0].strip().encode("ascii")
140
+ if not fid:
141
+ raise ArgumentTypeError("No frame ID")
142
+ text = vals[1] if len(vals) > 1 else ""
143
+ return fid, text
144
+
145
+ def UrlFrameArg(arg):
146
+ """FID:TEXT"""
147
+ fid, url = TextFrameArg(arg)
148
+ return fid, url.encode("latin1")
149
+
150
+ def DateArg(date_str):
151
+ return core.Date.parse(date_str) if date_str else ""
152
+
153
+ def CommentArg(arg):
154
+ """
155
+ COMMENT[:DESCRIPTION[:LANG]
156
+ """
157
+ vals = _splitArgs(arg, 3)
158
+ text = vals[0]
159
+ if not text:
160
+ raise ArgumentTypeError("text required")
161
+ desc = vals[1] if len(vals) > 1 else ""
162
+ lang = vals[2] if len(vals) > 2 else id3.DEFAULT_LANG
163
+ return text, desc, b(lang)[:3]
164
+
165
+ def LyricsArg(arg):
166
+ text, desc, lang = CommentArg(arg)
167
+ try:
168
+ with open(text, "r") as fp:
169
+ data = fp.read()
170
+ except Exception: # noqa: B901
171
+ raise ArgumentTypeError("Unable to read file")
172
+ return data, desc, lang
173
+
174
+ def PlayCountArg(pc):
175
+ if not pc:
176
+ raise ArgumentTypeError("value required")
177
+ increment = False
178
+ if pc[0] == "+":
179
+ pc = int(pc[1:])
180
+ increment = True
181
+ else:
182
+ pc = int(pc)
183
+ if pc < 0:
184
+ raise ArgumentTypeError("out of range")
185
+ return increment, pc
186
+
187
+ def BpmArg(bpm):
188
+ bpm = int(float(bpm) + 0.5)
189
+ if bpm <= 0:
190
+ raise ArgumentTypeError("out of range")
191
+ return bpm
192
+
193
+ def DirArg(d):
194
+ if not d or not os.path.isdir(d):
195
+ raise ArgumentTypeError("invalid directory: %s" % d)
196
+ return d
197
+
198
+ def ImageArg(s):
199
+ """PATH:TYPE[:DESCRIPTION]
200
+ Returns (path, type_id, mime_type, description)
201
+ """
202
+ args = _splitArgs(s, 3)
203
+ if len(args) < 2:
204
+ raise ArgumentTypeError("Format is: PATH:TYPE[:DESCRIPTION]")
205
+
206
+ path, type_str = args[:2]
207
+ desc = args[2] if len(args) > 2 else ""
208
+
209
+ try:
210
+ type_id = id3.frames.ImageFrame.stringToPicType(type_str)
211
+ except Exception: # noqa: B901
212
+ raise ArgumentTypeError("invalid pic type: {}".format(type_str))
213
+
214
+ if not path:
215
+ raise ArgumentTypeError("path required")
216
+ elif True in [path.startswith(prefix)
217
+ for prefix in ["http://", "https://"]]:
218
+ mt = ImageFrame.URL_MIME_TYPE
219
+ else:
220
+ if not os.path.isfile(path):
221
+ raise ArgumentTypeError("file does not exist")
222
+ mt = guessMimetype(path)
223
+ if mt is None:
224
+ raise ArgumentTypeError("Cannot determine mime-type")
225
+
226
+ return path, type_id, mt, desc
227
+
228
+ def ObjectArg(s):
229
+ """OBJ_PATH:MIME-TYPE[:DESCRIPTION[:FILENAME]],
230
+ Returns (path, mime_type, description, filename)
231
+ """
232
+ args = _splitArgs(s, 4)
233
+ if len(args) < 2:
234
+ raise ArgumentTypeError("too few parts")
235
+
236
+ path = args[0]
237
+ if path:
238
+ mt = args[1]
239
+ desc = args[2] if len(args) > 2 else ""
240
+ filename = args[3] \
241
+ if len(args) > 3 \
242
+ else os.path.basename(path)
243
+ if not os.path.isfile(path):
244
+ raise ArgumentTypeError("file does not exist")
245
+ if not mt:
246
+ raise ArgumentTypeError("mime-type required")
247
+ else:
248
+ raise ArgumentTypeError("path required")
249
+ return (path, mt, desc, filename)
250
+
251
+ def UniqFileIdArg(arg):
252
+ owner_id, id = KeyValueArg(arg)
253
+ if not owner_id:
254
+ raise ArgumentTypeError("owner_id required")
255
+ id = id.encode("latin1") # don't want to pass unicode
256
+ if len(id) > 64:
257
+ raise ArgumentTypeError("id must be <= 64 bytes")
258
+ return (owner_id, id)
259
+
260
+ def PopularityArg(arg):
261
+ """EMAIL:RATING[:PLAY_COUNT]
262
+ Returns (email, rating, play_count)
263
+ """
264
+ args = _splitArgs(arg, 3)
265
+ if len(args) < 2:
266
+ raise ArgumentTypeError("Incorrect number of argument components")
267
+ email = args[0]
268
+ rating = int(float(args[1]))
269
+ if rating < 0 or rating > 255:
270
+ raise ArgumentTypeError("Rating out-of-range")
271
+ play_count = 0
272
+ if len(args) > 2:
273
+ play_count = int(args[2])
274
+ if play_count < 0:
275
+ raise ArgumentTypeError("Play count out-of-range")
276
+ return (email, rating, play_count)
277
+
278
+ # Tag versions
279
+ gid3.add_argument("-1", "--v1", action="store_const", const=id3.ID3_V1,
280
+ dest="tag_version", default=id3.ID3_ANY_VERSION,
281
+ help=ARGS_HELP["--v1"])
282
+ gid3.add_argument("-2", "--v2", action="store_const", const=id3.ID3_V2,
283
+ dest="tag_version", default=id3.ID3_ANY_VERSION,
284
+ help=ARGS_HELP["--v2"])
285
+ gid3.add_argument("--to-v1.1", action="store_const", const=id3.ID3_V1_1,
286
+ dest="convert_version", help=ARGS_HELP["--to-v1.1"])
287
+ gid3.add_argument("--to-v2.3", action="store_const", const=id3.ID3_V2_3,
288
+ dest="convert_version", help=ARGS_HELP["--to-v2.3"])
289
+ gid3.add_argument("--to-v2.4", action="store_const", const=id3.ID3_V2_4,
290
+ dest="convert_version", help=ARGS_HELP["--to-v2.4"])
291
+
292
+ # Dates
293
+ gid3.add_argument("--release-date", type=DateArg, dest="release_date",
294
+ metavar="DATE",
295
+ help=ARGS_HELP["--release-date"])
296
+ gid3.add_argument("--orig-release-date", type=DateArg,
297
+ dest="orig_release_date", metavar="DATE",
298
+ help=ARGS_HELP["--orig-release-date"])
299
+ gid3.add_argument("--recording-date", type=DateArg,
300
+ dest="recording_date", metavar="DATE",
301
+ help=ARGS_HELP["--recording-date"])
302
+ gid3.add_argument("--encoding-date", type=DateArg, dest="encoding_date",
303
+ metavar="DATE", help=ARGS_HELP["--encoding-date"])
304
+ gid3.add_argument("--tagging-date", type=DateArg, dest="tagging_date",
305
+ metavar="DATE", help=ARGS_HELP["--tagging-date"])
306
+
307
+ # Misc
308
+ gid3.add_argument("--publisher", action="store",
309
+ dest="publisher", metavar="STRING",
310
+ help=ARGS_HELP["--publisher"])
311
+ gid3.add_argument("--play-count", type=PlayCountArg, dest="play_count",
312
+ metavar="<+>N", default=None,
313
+ help=ARGS_HELP["--play-count"])
314
+ gid3.add_argument("--bpm", type=BpmArg, dest="bpm", metavar="N",
315
+ default=None, help=ARGS_HELP["--bpm"])
316
+ gid3.add_argument("--unique-file-id", action="append",
317
+ type=UniqFileIdArg, dest="unique_file_ids",
318
+ metavar="OWNER_ID:ID", default=[],
319
+ help=ARGS_HELP["--unique-file-id"])
320
+
321
+ # Comments
322
+ gid3.add_argument("--add-comment", action="append", dest="comments",
323
+ metavar="COMMENT[:DESCRIPTION[:LANG]]", default=[],
324
+ type=CommentArg, help=ARGS_HELP["--add-comment"])
325
+ gid3.add_argument("--remove-comment", action="append", type=DescLangArg,
326
+ dest="remove_comment", default=[],
327
+ metavar="DESCRIPTION[:LANG]",
328
+ help=ARGS_HELP["--remove-comment"])
329
+ gid3.add_argument("--remove-all-comments", action="store_true",
330
+ dest="remove_all_comments",
331
+ help=ARGS_HELP["--remove-all-comments"])
332
+
333
+ gid3.add_argument("--remove-all-unknown", action="store_true",
334
+ dest="remove_all_unknown",
335
+ help=ARGS_HELP["--remove-all-unknown"])
336
+
337
+ gid3.add_argument("--add-lyrics", action="append", type=LyricsArg,
338
+ dest="lyrics", default=[],
339
+ metavar="LYRICS_FILE[:DESCRIPTION[:LANG]]",
340
+ help=ARGS_HELP["--add-lyrics"])
341
+ gid3.add_argument("--remove-lyrics", action="append", type=DescLangArg,
342
+ dest="remove_lyrics", default=[],
343
+ metavar="DESCRIPTION[:LANG]",
344
+ help=ARGS_HELP["--remove-lyrics"])
345
+ gid3.add_argument("--remove-all-lyrics", action="store_true",
346
+ dest="remove_all_lyrics",
347
+ help=ARGS_HELP["--remove-all-lyrics"])
348
+
349
+ gid3.add_argument("--text-frame", action="append", type=TextFrameArg,
350
+ dest="text_frames", metavar="FID:TEXT", default=[],
351
+ help=ARGS_HELP["--text-frame"])
352
+ gid3.add_argument("--user-text-frame", action="append",
353
+ type=DescTextArg,
354
+ dest="user_text_frames", metavar="DESC:TEXT",
355
+ default=[], help=ARGS_HELP["--user-text-frame"])
356
+
357
+ gid3.add_argument("--url-frame", action="append", type=UrlFrameArg,
358
+ dest="url_frames", metavar="FID:URL", default=[],
359
+ help=ARGS_HELP["--url-frame"])
360
+ gid3.add_argument("--user-url-frame", action="append", type=DescUrlArg,
361
+ dest="user_url_frames", metavar="DESCRIPTION:URL",
362
+ default=[], help=ARGS_HELP["--user-url-frame"])
363
+
364
+ gid3.add_argument("--add-image", action="append", type=ImageArg,
365
+ dest="images", metavar="IMG_PATH:TYPE[:DESCRIPTION]",
366
+ default=[], help=ARGS_HELP["--add-image"])
367
+ gid3.add_argument("--remove-image", action="append",
368
+ dest="remove_image", default=[],
369
+ metavar="DESCRIPTION",
370
+ help=ARGS_HELP["--remove-image"])
371
+ gid3.add_argument("--remove-all-images", action="store_true",
372
+ dest="remove_all_images",
373
+ help=ARGS_HELP["--remove-all-images"])
374
+ gid3.add_argument("--write-images", dest="write_images_dir",
375
+ metavar="DIR", type=DirArg,
376
+ help=ARGS_HELP["--write-images"])
377
+
378
+ gid3.add_argument("--add-object", action="append", type=ObjectArg,
379
+ dest="objects", default=[],
380
+ metavar="OBJ_PATH:MIME-TYPE[:DESCRIPTION[:FILENAME]]",
381
+ help=ARGS_HELP["--add-object"])
382
+ gid3.add_argument("--remove-object", action="append",
383
+ dest="remove_object", default=[],
384
+ metavar="DESCRIPTION",
385
+ help=ARGS_HELP["--remove-object"])
386
+ gid3.add_argument("--write-objects", action="store",
387
+ dest="write_objects_dir", metavar="DIR", default=None,
388
+ help=ARGS_HELP["--write-objects"])
389
+ gid3.add_argument("--remove-all-objects", action="store_true",
390
+ dest="remove_all_objects",
391
+ help=ARGS_HELP["--remove-all-objects"])
392
+
393
+ gid3.add_argument("--add-popularity", action="append",
394
+ type=PopularityArg, dest="popularities", default=[],
395
+ metavar="EMAIL:RATING[:PLAY_COUNT]",
396
+ help=ARGS_HELP["--add-popularity"])
397
+ gid3.add_argument("--remove-popularity", action="append", type=str,
398
+ dest="remove_popularity", default=[],
399
+ metavar="EMAIL",
400
+ help=ARGS_HELP["--remove-popularity"])
401
+
402
+ gid3.add_argument("--remove-v1", action="store_true", dest="remove_v1",
403
+ default=False, help=ARGS_HELP["--remove-v1"])
404
+ gid3.add_argument("--remove-v2", action="store_true", dest="remove_v2",
405
+ default=False, help=ARGS_HELP["--remove-v2"])
406
+ gid3.add_argument("--remove-all", action="store_true", default=False,
407
+ dest="remove_all", help=ARGS_HELP["--remove-all"])
408
+ gid3.add_argument("--remove-frame", action="append", default=[],
409
+ dest="remove_fids", metavar="FID", type=FidArg,
410
+ help=ARGS_HELP["--remove-frame"])
411
+
412
+ # 'True' means 'apply default max_padding, but only if saving anyhow'
413
+ gid3.add_argument("--max-padding", type=int, dest="max_padding",
414
+ default=True, metavar="NUM_BYTES",
415
+ help=ARGS_HELP["--max-padding"])
416
+ gid3.add_argument("--no-max-padding", dest="max_padding",
417
+ action="store_const", const=None,
418
+ help=ARGS_HELP["--no-max-padding"])
419
+
420
+ _encodings = ["latin1", "utf8", "utf16", "utf16-be"]
421
+ gid3.add_argument("--encoding", dest="text_encoding", default=None,
422
+ choices=_encodings, metavar='|'.join(_encodings),
423
+ help=ARGS_HELP["--encoding"])
424
+
425
+ # Misc options
426
+ gid4 = arg_parser.add_argument_group("Misc options")
427
+ gid4.add_argument("--force-update", action="store_true", default=False,
428
+ dest="force_update", help=ARGS_HELP["--force-update"])
429
+ gid4.add_argument("-v", "--verbose", action="store_true",
430
+ dest="verbose", help=ARGS_HELP["--verbose"])
431
+ gid4.add_argument("--preserve-file-times", action="store_true",
432
+ dest="preserve_file_time",
433
+ help=ARGS_HELP["--preserve-file-times"])
434
+
435
+ def handleFile(self, f):
436
+ parse_version = self.args.tag_version
437
+
438
+ try:
439
+ super().handleFile(f, tag_version=parse_version)
440
+ except id3.TagException as tag_ex:
441
+ printError(str(tag_ex))
442
+ return
443
+
444
+ if not self.audio_file:
445
+ return
446
+
447
+ self.terminal_width = getTtySize()[1]
448
+ self.printHeader(f)
449
+
450
+ if self.audio_file.tag and self.handleRemoves(self.audio_file.tag):
451
+ # Reload after removal
452
+ super(ClassicPlugin, self).handleFile(f, tag_version=parse_version)
453
+ if not self.audio_file:
454
+ return
455
+
456
+ new_tag = False
457
+ if not self.audio_file.tag:
458
+ self.audio_file.initTag(version=parse_version)
459
+ new_tag = True
460
+
461
+ try:
462
+ save_tag = (self.handleEdits(self.audio_file.tag) or
463
+ self.handlePadding(self.audio_file.tag) or
464
+ self.args.force_update or self.args.convert_version)
465
+ except ValueError as ex:
466
+ printError(str(ex))
467
+ return
468
+
469
+ self.printAudioInfo(self.audio_file.info)
470
+
471
+ if not save_tag and new_tag:
472
+ printError(f"No ID3 {id3.versionToString(self.args.tag_version)} tag found!")
473
+ return
474
+
475
+ self.printTag(self.audio_file.tag)
476
+
477
+ if self.args.write_images_dir:
478
+ for img in self.audio_file.tag.images:
479
+ if img.mime_type not in ImageFrame.URL_MIME_TYPE_VALUES:
480
+ img_path = "%s%s" % (self.args.write_images_dir,
481
+ os.path.sep)
482
+ if not os.path.isdir(img_path):
483
+ raise IOError("Directory does not exist: %s" % img_path)
484
+ img_file = makeUniqueFileName(
485
+ os.path.join(img_path, img.makeFileName()))
486
+ printWarning("Writing %s..." % img_file)
487
+ with open(img_file, "wb") as fp:
488
+ fp.write(img.image_data)
489
+
490
+ if save_tag:
491
+ # Use current tag version unless a convert was supplied
492
+ version = (self.args.convert_version or
493
+ self.audio_file.tag.version)
494
+ printWarning("Writing ID3 version %s" %
495
+ id3.versionToString(version))
496
+
497
+ # DEFAULT_MAX_PADDING is not set up as argument default,
498
+ # because we don't want to rewrite the file if the user
499
+ # did not trigger that explicitly:
500
+ max_padding = self.args.max_padding
501
+ if max_padding is True:
502
+ max_padding = DEFAULT_MAX_PADDING
503
+
504
+ self.audio_file.tag.save(
505
+ version=version, encoding=self.args.text_encoding,
506
+ backup=self.args.backup,
507
+ preserve_file_time=self.args.preserve_file_time,
508
+ max_padding=max_padding)
509
+
510
+ if self.args.rename_pattern:
511
+ # Handle file renaming.
512
+ from eyed3.id3.tag import TagTemplate
513
+ template = TagTemplate(self.args.rename_pattern)
514
+ name = template.substitute(self.audio_file.tag, zeropad=True)
515
+ orig = self.audio_file.path
516
+ try:
517
+ self.audio_file.rename(name)
518
+ printWarning(f"Renamed '{orig}' to '{self.audio_file.path}'")
519
+ except IOError as ex:
520
+ printError(str(ex))
521
+
522
+ printMsg(self._getHardRule(self.terminal_width))
523
+
524
+ def printHeader(self, file_path):
525
+ printMsg(self._getFileHeader(file_path, self.terminal_width))
526
+ printMsg(self._getHardRule(self.terminal_width))
527
+
528
+ def printAudioInfo(self, info):
529
+ if isinstance(info, mp3.Mp3AudioInfo):
530
+ printMsg(boldText("Time: ") +
531
+ "%s\tMPEG%d, Layer %s\t[ %s @ %s Hz - %s ]" %
532
+ (formatTime(info.time_secs),
533
+ info.mp3_header.version,
534
+ "I" * info.mp3_header.layer,
535
+ info.bit_rate_str,
536
+ info.mp3_header.sample_freq, info.mp3_header.mode))
537
+ printMsg(self._getHardRule(self.terminal_width))
538
+
539
+ @staticmethod
540
+ def _getDefaultNameForObject(obj_frame, suffix=""):
541
+ if obj_frame.filename:
542
+ name_str = obj_frame.filename
543
+ else:
544
+ name_str = obj_frame.description
545
+ name_str += ".%s" % obj_frame.mime_type.split("/")[1]
546
+ if suffix:
547
+ name_str += suffix
548
+ return name_str
549
+
550
+ def printTag(self, tag):
551
+ if isinstance(tag, id3.Tag):
552
+ if self.args.quiet:
553
+ printMsg(f"ID3 {id3.versionToString(tag.version)}: {len(tag.frame_set)} frames")
554
+ return
555
+ printMsg(f"ID3 {id3.versionToString(tag.version)}:")
556
+
557
+ artist = tag.artist if tag.artist else ""
558
+ title = tag.title if tag.title else ""
559
+ album = tag.album if tag.album else ""
560
+ printMsg("%s: %s" % (boldText("title"), title))
561
+ printMsg("%s: %s" % (boldText("artist"), artist))
562
+ printMsg("%s: %s" % (boldText("album"), album))
563
+ if tag.album_artist:
564
+ printMsg("%s: %s" % (boldText("album artist"),
565
+ tag.album_artist))
566
+ if tag.composer:
567
+ printMsg("%s: %s" % (boldText("composer"), tag.composer))
568
+ if tag.original_artist:
569
+ printMsg("%s: %s" % (boldText("original artist"), tag.original_artist))
570
+
571
+ for date, date_label in [
572
+ (tag.release_date, "release date"),
573
+ (tag.original_release_date, "original release date"),
574
+ (tag.recording_date, "recording date"),
575
+ (tag.encoding_date, "encoding date"),
576
+ (tag.tagging_date, "tagging date"),
577
+ ]:
578
+ if date:
579
+ printMsg("%s: %s" % (boldText(date_label), str(date)))
580
+
581
+ track_str = ""
582
+ (track_num, track_total) = tag.track_num
583
+ if track_num is not None:
584
+ track_str = str(track_num)
585
+ if track_total:
586
+ track_str += "/%d" % track_total
587
+
588
+ genre = tag.genre if not self.args.non_std_genres else tag.non_std_genre
589
+ genre_str = f"{boldText('genre')}: {genre.name} (id {genre.id})" if genre else ""
590
+ printMsg(f"{boldText('track')}: {track_str}\t\t{genre_str}")
591
+
592
+ (num, total) = tag.disc_num
593
+ if num is not None:
594
+ disc_str = str(num)
595
+ if total:
596
+ disc_str += "/%d" % total
597
+ printMsg("%s: %s" % (boldText("disc"), disc_str))
598
+
599
+ # PCNT
600
+ play_count = tag.play_count
601
+ if tag.play_count is not None:
602
+ printMsg("%s %d" % (boldText("Play Count:"), play_count))
603
+
604
+ # POPM
605
+ for popm in tag.popularities:
606
+ printMsg("%s [email: %s] [rating: %d] [play count: %d]" %
607
+ (boldText("Popularity:"), popm.email, popm.rating,
608
+ popm.count))
609
+
610
+ # TBPM
611
+ bpm = tag.bpm
612
+ if bpm is not None:
613
+ printMsg("%s %d" % (boldText("BPM:"), bpm))
614
+
615
+ # TPUB
616
+ pub = tag.publisher
617
+ if pub is not None:
618
+ printMsg("%s %s" % (boldText("Publisher/label:"), pub))
619
+
620
+ # UFID
621
+ for ufid in tag.unique_file_ids:
622
+ printMsg("%s [%s] : %s" %
623
+ (boldText("Unique File ID:"), ufid.owner_id,
624
+ ufid.uniq_id.decode("unicode_escape")))
625
+
626
+ # COMM
627
+ for c in tag.comments:
628
+ printMsg("%s: [Description: %s] [Lang: %s]\n%s" %
629
+ (boldText("Comment"), c.description or "",
630
+ c.lang.decode("ascii") or "", c.text or ""))
631
+
632
+ # USLT
633
+ for l in tag.lyrics:
634
+ printMsg("%s: [Description: %s] [Lang: %s]\n%s" %
635
+ (boldText("Lyrics"), l.description or "",
636
+ l.lang.decode("ascii") or "", l.text))
637
+
638
+ # TXXX
639
+ for f in tag.user_text_frames:
640
+ printMsg("%s: [Description: %s]\n%s" %
641
+ (boldText("UserTextFrame"), f.description, f.text))
642
+
643
+ # URL frames
644
+ for desc, url in (("Artist URL", tag.artist_url),
645
+ ("Audio source URL", tag.audio_source_url),
646
+ ("Audio file URL", tag.audio_file_url),
647
+ ("Internet radio URL", tag.internet_radio_url),
648
+ ("Commercial URL", tag.commercial_url),
649
+ ("Payment URL", tag.payment_url),
650
+ ("Publisher URL", tag.publisher_url),
651
+ ("Copyright URL", tag.copyright_url),
652
+ ):
653
+ if url:
654
+ printMsg("%s: %s" % (boldText(desc), url))
655
+
656
+ # user url frames
657
+ for u in tag.user_url_frames:
658
+ printMsg("%s [Description: %s]: %s" % (u.id, u.description,
659
+ u.url))
660
+
661
+ # APIC
662
+ for img in tag.images:
663
+ if img.mime_type not in ImageFrame.URL_MIME_TYPE_VALUES:
664
+ printMsg("%s: [Size: %d bytes] [Type: %s]" %
665
+ (boldText(img.picTypeToString(img.picture_type) +
666
+ " Image"),
667
+ len(img.image_data),
668
+ img.mime_type))
669
+ printMsg("Description: %s" % img.description)
670
+ printMsg("")
671
+ else:
672
+ printMsg("%s: [Type: %s] [URL: %s]" %
673
+ (boldText(img.picTypeToString(img.picture_type) +
674
+ " Image"),
675
+ img.mime_type, img.image_url))
676
+ printMsg("Description: %s" % img.description)
677
+ printMsg("")
678
+
679
+ # GOBJ
680
+ for obj in tag.objects:
681
+ printMsg("%s: [Size: %d bytes] [Type: %s]" %
682
+ (boldText("GEOB"), len(obj.object_data),
683
+ obj.mime_type))
684
+ printMsg("Description: %s" % obj.description)
685
+ printMsg("Filename: %s" % obj.filename)
686
+ printMsg("\n")
687
+ if self.args.write_objects_dir:
688
+ obj_path = "%s%s" % (self.args.write_objects_dir, os.sep)
689
+ if not os.path.isdir(obj_path):
690
+ raise IOError("Directory does not exist: %s" % obj_path)
691
+ obj_file = self._getDefaultNameForObject(obj)
692
+ count = 1
693
+ while os.path.exists(os.path.join(obj_path, obj_file)):
694
+ obj_file = self._getDefaultNameForObject(obj,
695
+ str(count))
696
+ count += 1
697
+ printWarning("Writing %s..." % os.path.join(obj_path,
698
+ obj_file))
699
+ with open(os.path.join(obj_path, obj_file), "wb") as fp:
700
+ fp.write(obj.object_data)
701
+
702
+ # PRIV
703
+ for p in tag.privates:
704
+ printMsg("%s: [Data: %d bytes]" % (boldText("PRIV"),
705
+ len(p.data)))
706
+ printMsg("Owner Id: %s" % p.owner_id.decode("ascii"))
707
+
708
+ # MCDI
709
+ if tag.cd_id:
710
+ printMsg("\n%s: [Data: %d bytes]" % (boldText("MCDI"),
711
+ len(tag.cd_id)))
712
+
713
+ # USER
714
+ if tag.terms_of_use:
715
+ printMsg("\nTerms of Use (%s): %s" % (boldText("USER"),
716
+ tag.terms_of_use))
717
+
718
+ # --verbose
719
+ if self.args.verbose:
720
+ printMsg(self._getHardRule(self.terminal_width))
721
+ printMsg("%d ID3 Frames:" % len(tag.frame_set))
722
+ for fid in tag.frame_set:
723
+ frames = tag.frame_set[fid]
724
+ num_frames = len(frames)
725
+ count = " x %d" % num_frames if num_frames > 1 else ""
726
+ if not tag.isV1():
727
+ total_bytes = sum(
728
+ tuple(frame.header.data_size + frame.header.size
729
+ for frame in frames if frame.header))
730
+ else:
731
+ total_bytes = 30
732
+ if total_bytes:
733
+ printMsg("%s%s (%d bytes)" % (fid.decode("ascii"),
734
+ count, total_bytes))
735
+ printMsg("%d bytes unused (padding)" %
736
+ (tag.file_info.tag_padding_size, ))
737
+ else:
738
+ raise TypeError("Unknown tag type: " + str(type(tag)))
739
+
740
+ def handleRemoves(self, tag):
741
+ remove_version = 0
742
+ status = False
743
+ rm_str = ""
744
+ if self.args.remove_all:
745
+ remove_version = id3.ID3_ANY_VERSION
746
+ rm_str = "v1.x and/or v2.x"
747
+ elif self.args.remove_v1:
748
+ remove_version = id3.ID3_V1
749
+ rm_str = "v1.x"
750
+ elif self.args.remove_v2:
751
+ remove_version = id3.ID3_V2
752
+ rm_str = "v2.x"
753
+
754
+ if remove_version:
755
+ status = id3.Tag.remove(tag.file_info.name, remove_version,
756
+ preserve_file_time=self.args.preserve_file_time)
757
+ printWarning(f"Removing ID3 {rm_str} tag: {'SUCCESS' if status else 'FAIL'}")
758
+
759
+ return status
760
+
761
+ def handlePadding(self, tag):
762
+ max_padding = self.args.max_padding
763
+ if max_padding is None or max_padding is True:
764
+ return False
765
+ padding = tag.file_info.tag_padding_size
766
+ needs_change = padding > max_padding
767
+ return needs_change
768
+
769
+ def handleEdits(self, tag):
770
+ retval = False
771
+
772
+ # --remove-all-*, Handling removes first means later options are still
773
+ # applied
774
+ for what, arg, fid in (("comments", self.args.remove_all_comments,
775
+ id3.frames.COMMENT_FID),
776
+ ("lyrics", self.args.remove_all_lyrics,
777
+ id3.frames.LYRICS_FID),
778
+ ("images", self.args.remove_all_images,
779
+ id3.frames.IMAGE_FID),
780
+ ("objects", self.args.remove_all_objects,
781
+ id3.frames.OBJECT_FID),
782
+ ):
783
+ if arg and tag.frame_set[fid]:
784
+ printWarning("Removing all %s..." % what)
785
+ del tag.frame_set[fid]
786
+ retval = True
787
+
788
+ if self.args.remove_all_unknown:
789
+ for fid in tag.unknown_frame_ids:
790
+ printWarning("Removing unknown (%s)..." % fid)
791
+ del tag.frame_set[fid]
792
+ retval = True
793
+
794
+ # --artist, --title, etc. All common/simple text frames.
795
+ for (what, setFunc) in (
796
+ ("artist", partial(tag._setArtist, self.args.artist)),
797
+ ("album", partial(tag._setAlbum, self.args.album)),
798
+ ("album artist", partial(tag._setAlbumArtist,
799
+ self.args.album_artist)),
800
+ ("title", partial(tag._setTitle, self.args.title)),
801
+ ("genre", partial(tag._setGenre, self.args.genre,
802
+ id3_std=not self.args.non_std_genres)),
803
+ ("release date", partial(tag._setReleaseDate,
804
+ self.args.release_date)),
805
+ ("original release date", partial(tag._setOrigReleaseDate,
806
+ self.args.orig_release_date)),
807
+ ("recording date", partial(tag._setRecordingDate,
808
+ self.args.recording_date)),
809
+ ("encoding date", partial(tag._setEncodingDate,
810
+ self.args.encoding_date)),
811
+ ("tagging date", partial(tag._setTaggingDate,
812
+ self.args.tagging_date)),
813
+ ("beats per minute", partial(tag._setBpm, self.args.bpm)),
814
+ ("publisher", partial(tag._setPublisher, self.args.publisher)),
815
+ ("composer", partial(tag._setComposer, self.args.composer)),
816
+ ("orig-artist", partial(tag._setOrigArtist, self.args.orig_artist)),
817
+ ):
818
+ if setFunc.args[0] is not None:
819
+ printWarning("Setting %s: %s" % (what, setFunc.args[0]))
820
+ setFunc()
821
+ retval = True
822
+
823
+ def _checkNumberedArgTuples(curr, new):
824
+ n = None
825
+ if new not in [(None, None), curr]:
826
+ n = [None] * 2
827
+ for i in (0, 1):
828
+ if new[i] == 0:
829
+ n[i] = None
830
+ else:
831
+ n[i] = new[i] or curr[i]
832
+ n = tuple(n)
833
+ # Returning None means do nothing, (None, None) would clear both vals
834
+ return n
835
+
836
+ # --artist-{city,state,country}
837
+ origin = core.ArtistOrigin(self.args.artist_city,
838
+ self.args.artist_state,
839
+ self.args.artist_country)
840
+ if origin or (dataclasses.astuple(origin) != (None, None, None) and tag.artist_origin):
841
+ printWarning(f"Setting artist origin: {origin}")
842
+ tag.artist_origin = origin
843
+ retval = True
844
+
845
+ # --track, --track-total
846
+ track_info = _checkNumberedArgTuples(tag.track_num,
847
+ (self.args.track,
848
+ self.args.track_total))
849
+ if track_info is not None:
850
+ printWarning("Setting track info: %s" % str(track_info))
851
+ tag.track_num = track_info
852
+ retval = True
853
+
854
+ # --track-offset
855
+ if self.args.track_offset:
856
+ offset = self.args.track_offset
857
+ tag.track_num = (tag.track_num[0] + offset, tag.track_num[1])
858
+ printWarning("%s track info by %d: %d" %
859
+ ("Incrementing" if offset > 0 else "Decrementing",
860
+ offset, tag.track_num[0]))
861
+ retval = True
862
+
863
+ # --disc-num, --disc-total
864
+ disc_info = _checkNumberedArgTuples(tag.disc_num,
865
+ (self.args.disc_num,
866
+ self.args.disc_total))
867
+ if disc_info is not None:
868
+ printWarning("Setting disc info: %s" % str(disc_info))
869
+ tag.disc_num = disc_info
870
+ retval = True
871
+
872
+ # -Y, --release-year
873
+ if self.args.release_year is not None:
874
+ # empty string means clean, None means not given
875
+ year = self.args.release_year
876
+ printWarning(f"Setting release year: {year}")
877
+ tag.release_date = int(year) if year else None
878
+ retval = True
879
+
880
+ # -c , simple comment
881
+ if self.args.simple_comment:
882
+ # Just add it as if it came in --add-comment
883
+ self.args.comments.append((self.args.simple_comment, "",
884
+ id3.DEFAULT_LANG))
885
+
886
+ # --remove-comment, remove-lyrics, --remove-image, --remove-object
887
+ for what, arg, accessor in (("comment", self.args.remove_comment,
888
+ tag.comments),
889
+ ("lyrics", self.args.remove_lyrics,
890
+ tag.lyrics),
891
+ ("image", self.args.remove_image,
892
+ tag.images),
893
+ ("object", self.args.remove_object,
894
+ tag.objects),
895
+ ):
896
+ for vals in arg:
897
+ if type(vals) is str:
898
+ frame = accessor.remove(vals)
899
+ else:
900
+ frame = accessor.remove(*vals)
901
+ if frame:
902
+ printWarning("Removed %s %s" % (what, str(vals)))
903
+ retval = True
904
+ else:
905
+ printError("Removing %s failed, %s not found" %
906
+ (what, str(vals)))
907
+
908
+ # --add-comment, --add-lyrics
909
+ for what, arg, accessor in (("comment", self.args.comments,
910
+ tag.comments),
911
+ ("lyrics", self.args.lyrics, tag.lyrics),
912
+ ):
913
+ for text, desc, lang in arg:
914
+ printWarning("Setting %s: %s/%s" %
915
+ (what, desc, str(lang, "ascii")))
916
+ accessor.set(text, desc, b(lang))
917
+ retval = True
918
+
919
+ # --play-count
920
+ playcount_arg = self.args.play_count
921
+ if playcount_arg:
922
+ increment, pc = playcount_arg
923
+ if increment:
924
+ printWarning("Increment play count by %d" % pc)
925
+ tag.play_count += pc
926
+ else:
927
+ printWarning("Setting play count to %d" % pc)
928
+ tag.play_count = pc
929
+ retval = True
930
+
931
+ # --add-popularity
932
+ for email, rating, play_count in self.args.popularities:
933
+ tag.popularities.set(email.encode("latin1"), rating, play_count)
934
+ retval = True
935
+
936
+ # --remove-popularity
937
+ for email in self.args.remove_popularity:
938
+ popm = tag.popularities.remove(email.encode("latin1"))
939
+ if popm:
940
+ retval = True
941
+
942
+ # --text-frame, --url-frame
943
+ for what, arg, setter in (
944
+ ("text frame", self.args.text_frames, tag.setTextFrame),
945
+ ("url frame", self.args.url_frames, tag._setUrlFrame),
946
+ ):
947
+ for fid, text in arg:
948
+ if text:
949
+ printWarning("Setting %s %s to '%s'" % (fid, what, text))
950
+ else:
951
+ printWarning("Removing %s %s" % (fid, what))
952
+ setter(fid, text)
953
+ retval = True
954
+
955
+ # --user-text-frame, --user-url-frame
956
+ for what, arg, accessor in (
957
+ ("user text frame", self.args.user_text_frames,
958
+ tag.user_text_frames),
959
+ ("user url frame", self.args.user_url_frames,
960
+ tag.user_url_frames),
961
+ ):
962
+ for desc, text in arg:
963
+ if text:
964
+ printWarning(f"Setting '{desc}' {what} to '{text}'")
965
+ accessor.set(text, desc)
966
+ else:
967
+ printWarning(f"Removing '{desc}' {what}")
968
+ accessor.remove(desc)
969
+ retval = True
970
+
971
+ # --add-image
972
+ for img_path, img_type, img_mt, img_desc in self.args.images:
973
+ assert img_path
974
+ printWarning("Adding image %s" % img_path)
975
+ if img_mt not in ImageFrame.URL_MIME_TYPE_VALUES:
976
+ with open(img_path, "rb") as img_fp:
977
+ tag.images.set(img_type, img_fp.read(), img_mt, img_desc)
978
+ else:
979
+ tag.images.set(img_type, None, None, img_desc, img_url=img_path)
980
+ retval = True
981
+
982
+ # --add-object
983
+ for obj_path, obj_mt, obj_desc, obj_fname in self.args.objects or []:
984
+ assert obj_path
985
+ printWarning("Adding object %s" % obj_path)
986
+ with open(obj_path, "rb") as obj_fp:
987
+ tag.objects.set(obj_fp.read(), obj_mt, obj_desc, obj_fname)
988
+ retval = True
989
+
990
+ # --unique-file-id
991
+ for arg in self.args.unique_file_ids:
992
+ owner_id, id = arg
993
+ if not id:
994
+ if tag.unique_file_ids.remove(owner_id):
995
+ printWarning("Removed unique file ID '%s'" % owner_id)
996
+ retval = True
997
+ else:
998
+ printWarning("Unique file ID '%s' not found" % owner_id)
999
+ else:
1000
+ tag.unique_file_ids.set(id, owner_id.encode("latin1"))
1001
+ printWarning("Setting unique file ID '%s' to %s" %
1002
+ (owner_id, id))
1003
+ retval = True
1004
+
1005
+ # --remove-frame
1006
+ for fid in self.args.remove_fids:
1007
+ assert isinstance(fid, bytes)
1008
+ if fid in tag.frame_set:
1009
+ del tag.frame_set[fid]
1010
+ retval = True
1011
+
1012
+ return retval
1013
+
1014
+
1015
+ def _getTemplateKeys():
1016
+ keys = list(id3.TagTemplate("")._makeMapping(None, False).keys())
1017
+ keys.sort()
1018
+ return ", ".join(["$%s" % v for v in keys])
1019
+
1020
+
1021
+ ARGS_HELP = {
1022
+ "--artist": "Set the artist name.",
1023
+ "--album": "Set the album name.",
1024
+ "--album-artist": "Set the album artist name. '%s', for example. "
1025
+ "Another example is collaborations when the "
1026
+ "track artist might be 'Eminem featuring Proof' "
1027
+ "the album artist would be 'Eminem'." %
1028
+ core.VARIOUS_ARTISTS,
1029
+ "--title": "Set the track title.",
1030
+ "--track": "Set the track number. Use 0 to clear.",
1031
+ "--track-total": "Set total number of tracks. Use 0 to clear.",
1032
+ "--disc-num": "Set the disc number. Use 0 to clear.",
1033
+ "--disc-total": "Set total number of discs in set. Use 0 to clear.",
1034
+ "--genre": "Set the genre. If the argument is a standard ID3 genre "
1035
+ "name or number both will be set. Otherwise, any string "
1036
+ "can be used. Run 'eyeD3 --plugin=genres' for a list of "
1037
+ "standard ID3 genre names/ids.",
1038
+ "--non-std-genres": "Disables certain ID3 genre standards, such as the "
1039
+ "mapping of numeric value to genre names. For example, "
1040
+ "genre=1 is taken literally, not mapped to 'Classic Rock'.",
1041
+ "--release-year": "Set the year the track was released. Use the date "
1042
+ "options for more precise values or dates other "
1043
+ "than release.",
1044
+
1045
+ "--v1": "Only read and write ID3 v1.x tags. By default, v1.x tags are "
1046
+ "only read or written if there is not a v2 tag in the file.",
1047
+ "--v2": "Only read/write ID3 v2.x tags. This is the default unless "
1048
+ "the file only contains a v1 tag.",
1049
+
1050
+ "--to-v1.1": "Convert the file's tag to ID3 v1.1 (Or 1.0 if there is "
1051
+ "no track number)",
1052
+ "--to-v2.3": "Convert the file's tag to ID3 v2.3",
1053
+ "--to-v2.4": "Convert the file's tag to ID3 v2.4",
1054
+
1055
+ "--release-date": "Set the date the track/album was released",
1056
+ "--orig-release-date": "Set the original date the track/album was "
1057
+ "released",
1058
+ "--recording-date": "Set the date the track/album was recorded",
1059
+ "--encoding-date": "Set the date the file was encoded",
1060
+ "--tagging-date": "Set the date the file was tagged",
1061
+
1062
+ "--comment": "Set a comment. In ID3 tags this is the comment with "
1063
+ "an empty description. See --add-comment to add multiple "
1064
+ "comment frames.",
1065
+ "--add-comment":
1066
+ "Add or replace a comment. There may be more than one comment in a "
1067
+ "tag, as long as the DESCRIPTION and LANG values are unique. The "
1068
+ "default DESCRIPTION is '' and the default language code is '%s'." %
1069
+ str(id3.DEFAULT_LANG, "ascii"),
1070
+ "--remove-comment": "Remove comment matching DESCRIPTION and LANG. "
1071
+ "The default language code is '%s'." %
1072
+ str(id3.DEFAULT_LANG, "ascii"),
1073
+ "--remove-all-comments": "Remove all comments from the tag.",
1074
+
1075
+ "--remove-all-unknown": "Remove all unknown frames from the tag.",
1076
+
1077
+ "--add-lyrics":
1078
+ "Add or replace a lyrics. There may be more than one set of lyrics "
1079
+ "in a tag, as long as the DESCRIPTION and LANG values are unique. "
1080
+ "The default DESCRIPTION is '' and the default language code is "
1081
+ "'%s'." % str(id3.DEFAULT_LANG, "ascii"),
1082
+ "--remove-lyrics": "Remove lyrics matching DESCRIPTION and LANG. "
1083
+ "The default language code is '%s'." %
1084
+ str(id3.DEFAULT_LANG, "ascii"),
1085
+ "--remove-all-lyrics": "Remove all lyrics from the tag.",
1086
+
1087
+ "--publisher": "Set the publisher/label name",
1088
+ "--play-count": "Set the number of times played counter. If the "
1089
+ "argument value begins with '+' the tag's play count "
1090
+ "is incremented by N, otherwise the value is set to "
1091
+ "exactly N.",
1092
+ "--bpm": "Set the beats per minute value.",
1093
+
1094
+ "--text-frame": "Set the value of a text frame. To remove the "
1095
+ "frame, specify an empty value. For example, "
1096
+ "--text-frame='TDRC:'",
1097
+ "--user-text-frame": "Set the value of a user text frame (i.e., TXXX). "
1098
+ "To remove the frame, specify an empty value. "
1099
+ "e.g., --user-text-frame='SomeDesc:'",
1100
+ "--url-frame": "Set the value of a URL frame. To remove the frame, "
1101
+ "specify an empty value. e.g., --url-frame='WCOM:'",
1102
+ "--user-url-frame": "Set the value of a user URL frame (i.e., WXXX). "
1103
+ "To remove the frame, specify an empty value. "
1104
+ "e.g., --user-url-frame='SomeDesc:'",
1105
+
1106
+ "--add-image": "Add or replace an image. There may be more than one "
1107
+ "image in a tag, as long as the DESCRIPTION values are "
1108
+ "unique. The default DESCRIPTION is ''. If PATH begins "
1109
+ "with 'http[s]://' then it is interpreted as a URL "
1110
+ "instead of a file containing image data. The TYPE must "
1111
+ "be one of the following: %s."
1112
+ % (", ".join([ImageFrame.picTypeToString(t)
1113
+ for t in range(ImageFrame.MIN_TYPE,
1114
+ ImageFrame.MAX_TYPE + 1)]),
1115
+ ),
1116
+ "--remove-image": "Remove image matching DESCRIPTION.",
1117
+ "--remove-all-images": "Remove all images from the tag",
1118
+ "--write-images": "Causes all attached images (APIC frames) to be "
1119
+ "written to the specified directory.",
1120
+
1121
+ "--add-object": "Add or replace an object. There may be more than one "
1122
+ "object in a tag, as long as the DESCRIPTION values "
1123
+ "are unique. The default DESCRIPTION is ''.",
1124
+ "--remove-object": "Remove object matching DESCRIPTION.",
1125
+ "--remove-all-objects": "Remove all objects from the tag",
1126
+ "--write-objects": "Causes all attached objects (GEOB frames) to be "
1127
+ "written to the specified directory.",
1128
+
1129
+ "--add-popularity": "Adds a pupularity metric. There may be multiples "
1130
+ "popularity values, but each must have a unique "
1131
+ "email address component. The rating is a number "
1132
+ "between 0 (worst) and 255 (best). The play count "
1133
+ "is optional, and defaults to 0, since there is "
1134
+ "already a dedicated play count frame.",
1135
+ "--remove-popularity": "Removes the popularity frame with the "
1136
+ "specified email key.",
1137
+
1138
+ "--remove-v1": "Remove ID3 v1.x tag.",
1139
+ "--remove-v2": "Remove ID3 v2.x tag.",
1140
+ "--remove-all": "Remove ID3 v1.x and v2.x tags.",
1141
+
1142
+ "--remove-frame": "Remove all frames with the given ID. This option "
1143
+ "may be specified multiple times.",
1144
+
1145
+ "--max-padding": "Shrink file if tag padding (unused space) exceeds "
1146
+ "the given number of bytes. "
1147
+ "(Useful e.g. after removal of large cover art.) "
1148
+ "Default is 64 KiB, file will be rewritten with "
1149
+ "default padding (1 KiB) or max padding, whichever "
1150
+ "is smaller.",
1151
+ "--no-max-padding": "Disable --max-padding altogether.",
1152
+
1153
+ "--force-update": "Rewrite the tag despite there being no edit "
1154
+ "options.",
1155
+ "--verbose": "Show all available tag data",
1156
+ "--unique-file-id": "Add a unique file ID frame. If the ID arg is "
1157
+ "empty the frame is removed. An OWNER_ID is "
1158
+ "required. The ID may be no more than 64 bytes.",
1159
+ "--encoding": "Set the encoding that is used for all text frames. "
1160
+ "This option is only applied if the tag is updated "
1161
+ "as the result of an edit option (e.g. --artist, "
1162
+ "--title, etc.) or --force-update is specified.",
1163
+ "--rename": "Rename file (the extension is not affected) "
1164
+ "based on data in the tag using substitution "
1165
+ "variables: " + _getTemplateKeys(),
1166
+ "--preserve-file-times": "When writing, do not update file "
1167
+ "modification times.",
1168
+ "--track-offset": "Increment/decrement the track number by [-]N. "
1169
+ "This option is applied after --track=N is set.",
1170
+ "--composer": "Set the composer's name.",
1171
+ "--orig-artist": "Set the orignal artist's name. For example, a cover song can include "
1172
+ "the orignal author of the track.",
1173
+ }