# -*- coding: utf-8 -*-
################################################################################
# Copyright (C) 2007-2016 Travis Shirk <travis@pobox.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
#
################################################################################
from __future__ import print_function
import os
import re
from functools import partial
from argparse import ArgumentTypeError
from eyed3 import LOCAL_ENCODING
from eyed3.plugins import LoaderPlugin
from eyed3 import core, id3, mp3, utils, compat
from eyed3.utils import makeUniqueFileName
from eyed3.utils.console import (printMsg, printError, printWarning, boldText,
HEADER_COLOR, Fore, getTtySize)
from eyed3.id3.frames import ImageFrame
from eyed3.utils.log import getLogger
log = getLogger(__name__)
FIELD_DELIM = ':'
DEFAULT_MAX_PADDING = 64 * 1024
[docs]class ClassicPlugin(LoaderPlugin):
SUMMARY = u"Classic eyeD3 interface for viewing and editing tags."
DESCRIPTION = u"""
All PATH arguments are parsed and displayed. Directory paths are searched
recursively. Any editing options (--artist, --title) are applied to each file
read.
All date options (-Y, --release-year excepted) follow ISO 8601 format. This is
``yyyy-mm-ddThh:mm:ss``. The year is required, and each component thereafter is
optional. For example, 2012-03 is valid, 2012--12 is not.
"""
NAMES = ["classic"]
def __init__(self, arg_parser):
super(ClassicPlugin, self).__init__(arg_parser)
g = self.arg_group
def UnicodeArg(arg):
return _unicodeArgValue(arg)
def PositiveIntArg(i):
i = int(i)
if i < 0:
raise ArgumentTypeError("positive number required")
return i
# Common options
g.add_argument("-a", "--artist", type=UnicodeArg, dest="artist",
metavar="STRING", help=ARGS_HELP["--artist"])
g.add_argument("-A", "--album", type=UnicodeArg, dest="album",
metavar="STRING", help=ARGS_HELP["--album"])
g.add_argument("-b", "--album-artist", type=UnicodeArg,
dest="album_artist", metavar="STRING",
help=ARGS_HELP["--album-artist"])
g.add_argument("-t", "--title", type=UnicodeArg, dest="title",
metavar="STRING", help=ARGS_HELP["--title"])
g.add_argument("-n", "--track", type=PositiveIntArg, dest="track",
metavar="NUM", help=ARGS_HELP["--track"])
g.add_argument("-N", "--track-total", type=PositiveIntArg,
dest="track_total", metavar="NUM",
help=ARGS_HELP["--track-total"])
g.add_argument("--track-offset", type=int, dest="track_offset",
metavar="N", help=ARGS_HELP["--track-offset"])
g.add_argument("--composer", type=UnicodeArg, dest="composer",
metavar="STRING", help=ARGS_HELP["--composer"])
g.add_argument("-d", "--disc-num", type=PositiveIntArg, dest="disc_num",
metavar="NUM", help=ARGS_HELP["--disc-num"])
g.add_argument("-D", "--disc-total", type=PositiveIntArg,
dest="disc_total", metavar="NUM",
help=ARGS_HELP["--disc-total"])
g.add_argument("-G", "--genre", type=UnicodeArg, dest="genre",
metavar="GENRE", help=ARGS_HELP["--genre"])
g.add_argument("--non-std-genres", dest="non_std_genres",
action="store_true", help=ARGS_HELP["--non-std-genres"])
g.add_argument("-Y", "--release-year", type=PositiveIntArg,
dest="release_year", metavar="YEAR",
help=ARGS_HELP["--release-year"])
g.add_argument("-c", "--comment", dest="simple_comment",
type=UnicodeArg, metavar="STRING",
help=ARGS_HELP["--comment"])
g.add_argument("--rename", dest="rename_pattern", metavar="PATTERN",
help=ARGS_HELP["--rename"])
gid3 = arg_parser.add_argument_group("ID3 options")
def _splitArgs(arg, maxsplit=None):
NEW_DELIM = "#DELIM#"
arg = re.sub(r"\\%s" % FIELD_DELIM, NEW_DELIM, arg)
t = tuple(re.sub(NEW_DELIM, FIELD_DELIM, s)
for s in arg.split(FIELD_DELIM))
if maxsplit is not None and maxsplit < 2:
raise ValueError("Invalid maxsplit value: {}".format(maxsplit))
elif maxsplit and len(t) > maxsplit:
t = t[:maxsplit - 1] + (FIELD_DELIM.join(t[maxsplit - 1:]),)
assert len(t) <= maxsplit
return t
def _unicodeArgValue(arg):
if not isinstance(arg, compat.UnicodeType):
return compat.unicode(arg, LOCAL_ENCODING)
else:
return arg
def DescLangArg(arg):
"""DESCRIPTION[:LANG]"""
arg = _unicodeArgValue(arg)
vals = _splitArgs(arg, 2)
desc = vals[0]
lang = vals[1] if len(vals) > 1 else id3.DEFAULT_LANG
return (desc, compat.b(lang)[:3] or id3.DEFAULT_LANG)
def DescTextArg(arg):
"""DESCRIPTION:TEXT"""
arg = _unicodeArgValue(arg)
vals = _splitArgs(arg, 2)
desc = vals[0].strip()
text = FIELD_DELIM.join(vals[1:] if len(vals) > 1 else [])
return (desc or u"", text or u"")
KeyValueArg = DescTextArg
def DescUrlArg(arg):
desc, url = DescTextArg(arg)
return (desc, url.encode("latin1"))
def FidArg(arg):
arg = _unicodeArgValue(arg)
fid = arg.strip().encode("ascii")
if not fid:
raise ArgumentTypeError("No frame ID")
return fid
def TextFrameArg(arg):
"""FID:TEXT"""
arg = _unicodeArgValue(arg)
vals = _splitArgs(arg, 2)
fid = vals[0].strip().encode("ascii")
if not fid:
raise ArgumentTypeError("No frame ID")
text = vals[1] if len(vals) > 1 else u""
return (fid, text)
def UrlFrameArg(arg):
"""FID:TEXT"""
fid, url = TextFrameArg(arg)
return (fid, url.encode("latin1"))
def DateArg(date_str):
return core.Date.parse(date_str) if date_str else ""
def CommentArg(arg):
"""
COMMENT[:DESCRIPTION[:LANG]
"""
arg = _unicodeArgValue(arg)
vals = _splitArgs(arg, 3)
text = vals[0]
if not text:
raise ArgumentTypeError("text required")
desc = vals[1] if len(vals) > 1 else u""
lang = vals[2] if len(vals) > 2 else id3.DEFAULT_LANG
return (text, desc, compat.b(lang)[:3])
def LyricsArg(arg):
text, desc, lang = CommentArg(arg)
try:
with open(text, "rb") as fp:
data = fp.read()
except Exception: # noqa: B901
raise ArgumentTypeError("Unable to read file")
return (_unicodeArgValue(data), desc, lang)
def PlayCountArg(pc):
if not pc:
raise ArgumentTypeError("value required")
increment = False
if pc[0] == "+":
pc = int(pc[1:])
increment = True
else:
pc = int(pc)
if pc < 0:
raise ArgumentTypeError("out of range")
return (increment, pc)
def BpmArg(bpm):
bpm = int(float(bpm) + 0.5)
if bpm <= 0:
raise ArgumentTypeError("out of range")
return bpm
def DirArg(d):
if not d or not os.path.isdir(d):
raise ArgumentTypeError("invalid directory: %s" % d)
return d
def ImageArg(s):
"""PATH:TYPE[:DESCRIPTION]
Returns (path, type_id, mime_type, description)
"""
args = _splitArgs(s, 3)
if len(args) < 2:
raise ArgumentTypeError("Format is: PATH:TYPE[:DESCRIPTION]")
path, type_str = args[:2]
desc = UnicodeArg(args[2]) if len(args) > 2 else u""
mt = None
try:
type_id = id3.frames.ImageFrame.stringToPicType(type_str)
except: # noqa: B901
raise ArgumentTypeError("invalid pic type: {}".format(type_str))
if not path:
raise ArgumentTypeError("path required")
elif True in [path.startswith(prefix)
for prefix in ["http://", "https://"]]:
mt = ImageFrame.URL_MIME_TYPE
else:
if not os.path.isfile(path):
raise ArgumentTypeError("file does not exist")
mt = utils.guessMimetype(path)
if mt is None:
raise ArgumentTypeError("Cannot determine mime-type")
return (path, type_id, mt, desc)
def ObjectArg(s):
"""OBJ_PATH:MIME-TYPE[:DESCRIPTION[:FILENAME]],
Returns (path, mime_type, description, filename)
"""
args = _splitArgs(s, 4)
if len(args) < 2:
raise ArgumentTypeError("too few parts")
path = args[0]
mt = None
desc = None
filename = None
if path:
mt = args[1]
desc = UnicodeArg(args[2]) if len(args) > 2 else u""
filename = UnicodeArg(args[3]) \
if len(args) > 3 \
else UnicodeArg(os.path.basename(path))
if not os.path.isfile(path):
raise ArgumentTypeError("file does not exist")
if not mt:
raise ArgumentTypeError("mime-type required")
else:
raise ArgumentTypeError("path required")
return (path, mt, desc, filename)
def UniqFileIdArg(arg):
owner_id, id = KeyValueArg(arg)
if not owner_id:
raise ArgumentTypeError("owner_id required")
id = id.encode("latin1") # don't want to pass unicode
if len(id) > 64:
raise ArgumentTypeError("id must be <= 64 bytes")
return (owner_id, id)
def PopularityArg(arg):
"""EMAIL:RATING[:PLAY_COUNT]
Returns (email, rating, play_count)
"""
args = _splitArgs(arg, 3)
if len(args) < 2:
raise ArgumentTypeError("Incorrect number of argument "
"components")
email = args[0]
rating = int(float(args[1]))
if rating < 0 or rating > 255:
raise ArgumentTypeError("Rating out-of-range")
play_count = 0
if len(args) > 2:
play_count = int(args[2])
if play_count < 0:
raise ArgumentTypeError("Play count out-of-range")
return (email, rating, play_count)
# Tag versions
gid3.add_argument("-1", "--v1", action="store_const", const=id3.ID3_V1,
dest="tag_version", default=id3.ID3_ANY_VERSION,
help=ARGS_HELP["--v1"])
gid3.add_argument("-2", "--v2", action="store_const", const=id3.ID3_V2,
dest="tag_version", default=id3.ID3_ANY_VERSION,
help=ARGS_HELP["--v2"])
gid3.add_argument("--to-v1.1", action="store_const", const=id3.ID3_V1_1,
dest="convert_version", help=ARGS_HELP["--to-v1.1"])
gid3.add_argument("--to-v2.3", action="store_const", const=id3.ID3_V2_3,
dest="convert_version", help=ARGS_HELP["--to-v2.3"])
gid3.add_argument("--to-v2.4", action="store_const", const=id3.ID3_V2_4,
dest="convert_version", help=ARGS_HELP["--to-v2.4"])
# Dates
gid3.add_argument("--release-date", type=DateArg, dest="release_date",
metavar="DATE",
help=ARGS_HELP["--release-date"])
gid3.add_argument("--orig-release-date", type=DateArg,
dest="orig_release_date", metavar="DATE",
help=ARGS_HELP["--orig-release-date"])
gid3.add_argument("--recording-date", type=DateArg,
dest="recording_date", metavar="DATE",
help=ARGS_HELP["--recording-date"])
gid3.add_argument("--encoding-date", type=DateArg, dest="encoding_date",
metavar="DATE", help=ARGS_HELP["--encoding-date"])
gid3.add_argument("--tagging-date", type=DateArg, dest="tagging_date",
metavar="DATE", help=ARGS_HELP["--tagging-date"])
# Misc
gid3.add_argument("--publisher", action="store", type=UnicodeArg,
dest="publisher", metavar="STRING",
help=ARGS_HELP["--publisher"])
gid3.add_argument("--play-count", type=PlayCountArg, dest="play_count",
metavar="<+>N", default=None,
help=ARGS_HELP["--play-count"])
gid3.add_argument("--bpm", type=BpmArg, dest="bpm", metavar="N",
default=None, help=ARGS_HELP["--bpm"])
gid3.add_argument("--unique-file-id", action="append",
type=UniqFileIdArg, dest="unique_file_ids",
metavar="OWNER_ID:ID", default=[],
help=ARGS_HELP["--unique-file-id"])
# Comments
gid3.add_argument("--add-comment", action="append", dest="comments",
metavar="COMMENT[:DESCRIPTION[:LANG]]", default=[],
type=CommentArg, help=ARGS_HELP["--add-comment"])
gid3.add_argument("--remove-comment", action="append", type=DescLangArg,
dest="remove_comment", default=[],
metavar="DESCRIPTION[:LANG]",
help=ARGS_HELP["--remove-comment"])
gid3.add_argument("--remove-all-comments", action="store_true",
dest="remove_all_comments",
help=ARGS_HELP["--remove-all-comments"])
gid3.add_argument("--add-lyrics", action="append", type=LyricsArg,
dest="lyrics", default=[],
metavar="LYRICS_FILE[:DESCRIPTION[:LANG]]",
help=ARGS_HELP["--add-lyrics"])
gid3.add_argument("--remove-lyrics", action="append", type=DescLangArg,
dest="remove_lyrics", default=[],
metavar="DESCRIPTION[:LANG]",
help=ARGS_HELP["--remove-lyrics"])
gid3.add_argument("--remove-all-lyrics", action="store_true",
dest="remove_all_lyrics",
help=ARGS_HELP["--remove-all-lyrics"])
gid3.add_argument("--text-frame", action="append", type=TextFrameArg,
dest="text_frames", metavar="FID:TEXT", default=[],
help=ARGS_HELP["--text-frame"])
gid3.add_argument("--user-text-frame", action="append",
type=DescTextArg,
dest="user_text_frames", metavar="DESC:TEXT",
default=[], help=ARGS_HELP["--user-text-frame"])
gid3.add_argument("--url-frame", action="append", type=UrlFrameArg,
dest="url_frames", metavar="FID:URL", default=[],
help=ARGS_HELP["--url-frame"])
gid3.add_argument("--user-url-frame", action="append", type=DescUrlArg,
dest="user_url_frames", metavar="DESCRIPTION:URL",
default=[], help=ARGS_HELP["--user-url-frame"])
gid3.add_argument("--add-image", action="append", type=ImageArg,
dest="images", metavar="IMG_PATH:TYPE[:DESCRIPTION]",
default=[], help=ARGS_HELP["--add-image"])
gid3.add_argument("--remove-image", action="append", type=UnicodeArg,
dest="remove_image", default=[],
metavar="DESCRIPTION",
help=ARGS_HELP["--remove-image"])
gid3.add_argument("--remove-all-images", action="store_true",
dest="remove_all_images",
help=ARGS_HELP["--remove-all-images"])
gid3.add_argument("--write-images", dest="write_images_dir",
metavar="DIR", type=DirArg,
help=ARGS_HELP["--write-images"])
gid3.add_argument("--add-object", action="append", type=ObjectArg,
dest="objects", default=[],
metavar="OBJ_PATH:MIME-TYPE[:DESCRIPTION[:FILENAME]]",
help=ARGS_HELP["--add-object"])
gid3.add_argument("--remove-object", action="append", type=UnicodeArg,
dest="remove_object", default=[],
metavar="DESCRIPTION",
help=ARGS_HELP["--remove-object"])
gid3.add_argument("--write-objects", action="store",
dest="write_objects_dir", metavar="DIR", default=None,
help=ARGS_HELP["--write-objects"])
gid3.add_argument("--remove-all-objects", action="store_true",
dest="remove_all_objects",
help=ARGS_HELP["--remove-all-objects"])
gid3.add_argument("--add-popularity", action="append",
type=PopularityArg, dest="popularities", default=[],
metavar="EMAIL:RATING[:PLAY_COUNT]",
help=ARGS_HELP["--add-popularty"])
gid3.add_argument("--remove-popularity", action="append", type=str,
dest="remove_popularity", default=[],
metavar="EMAIL",
help=ARGS_HELP["--remove-popularity"])
gid3.add_argument("--remove-v1", action="store_true", dest="remove_v1",
default=False, help=ARGS_HELP["--remove-v1"])
gid3.add_argument("--remove-v2", action="store_true", dest="remove_v2",
default=False, help=ARGS_HELP["--remove-v2"])
gid3.add_argument("--remove-all", action="store_true", default=False,
dest="remove_all", help=ARGS_HELP["--remove-all"])
gid3.add_argument("--remove-frame", action="append", default=[],
dest="remove_fids", metavar="FID", type=FidArg,
help=ARGS_HELP["--remove-frame"])
# 'True' means 'apply default max_padding, but only if saving anyhow'
gid3.add_argument("--max-padding", type=int, dest="max_padding",
default=True, metavar="NUM_BYTES",
help=ARGS_HELP["--max-padding"])
gid3.add_argument("--no-max-padding", dest="max_padding",
action="store_const", const=None,
help=ARGS_HELP["--no-max-padding"])
_encodings = ["latin1", "utf8", "utf16", "utf16-be"]
gid3.add_argument("--encoding", dest="text_encoding", default=None,
choices=_encodings, metavar='|'.join(_encodings),
help=ARGS_HELP["--encoding"])
# Misc options
gid4 = arg_parser.add_argument_group("Misc options")
gid4.add_argument("--force-update", action="store_true", default=False,
dest="force_update", help=ARGS_HELP["--force-update"])
gid4.add_argument("-v", "--verbose", action="store_true",
dest="verbose", help=ARGS_HELP["--verbose"])
gid4.add_argument("--preserve-file-times", action="store_true",
dest="preserve_file_time",
help=ARGS_HELP["--preserve-file-times"])
[docs] def handleFile(self, f):
parse_version = self.args.tag_version
super(ClassicPlugin, self).handleFile(f, tag_version=parse_version)
if not self.audio_file:
return
self.terminal_width = getTtySize()[1]
self.printHeader(f)
printMsg("-" * self.terminal_width)
if self.audio_file.tag and self.handleRemoves(self.audio_file.tag):
# Reload after removal
super(ClassicPlugin, self).handleFile(f, tag_version=parse_version)
if not self.audio_file:
return
new_tag = False
if not self.audio_file.tag:
self.audio_file.initTag(version=parse_version)
new_tag = True
try:
save_tag = (self.handleEdits(self.audio_file.tag) or
self.handlePadding(self.audio_file.tag) or
self.args.force_update or self.args.convert_version)
except ValueError as ex:
printError(str(ex))
return
self.printAudioInfo(self.audio_file.info)
if not save_tag and new_tag:
printError("No ID3 %s tag found!" %
id3.versionToString(self.args.tag_version))
return
self.printTag(self.audio_file.tag)
if self.args.write_images_dir:
for img in self.audio_file.tag.images:
if img.mime_type not in ImageFrame.URL_MIME_TYPE_VALUES:
img_path = "%s%s" % (self.args.write_images_dir,
os.path.sep)
if not os.path.isdir(img_path):
raise IOError("Directory does not exist: %s" % img_path)
img_file = makeUniqueFileName(
os.path.join(img_path, img.makeFileName()))
printWarning("Writing %s..." % img_file)
with open(img_file, "wb") as fp:
fp.write(img.image_data)
if save_tag:
# Use current tag version unless a convert was supplied
version = (self.args.convert_version or
self.audio_file.tag.version)
printWarning("Writing ID3 version %s" %
id3.versionToString(version))
# DEFAULT_MAX_PADDING is not set up as argument default,
# because we don't want to rewrite the file if the user
# did not trigger that explicitly:
max_padding = self.args.max_padding
if max_padding is True:
max_padding = DEFAULT_MAX_PADDING
self.audio_file.tag.save(
version=version, encoding=self.args.text_encoding,
backup=self.args.backup,
preserve_file_time=self.args.preserve_file_time,
max_padding=max_padding)
if self.args.rename_pattern:
# Handle file renaming.
from eyed3.id3.tag import TagTemplate
template = TagTemplate(self.args.rename_pattern)
name = template.substitute(self.audio_file.tag, zeropad=True)
orig = self.audio_file.path
try:
self.audio_file.rename(name)
printWarning("Renamed '%s' to '%s'" %
(orig, self.audio_file.path))
except IOError as ex:
printError(str(ex))
printMsg("-" * self.terminal_width)
[docs] def printAudioInfo(self, info):
if isinstance(info, mp3.Mp3AudioInfo):
printMsg(boldText("Time: ") +
"%s\tMPEG%d, Layer %s\t[ %s @ %s Hz - %s ]" %
(utils.formatTime(info.time_secs),
info.mp3_header.version,
"I" * info.mp3_header.layer,
info.bit_rate_str,
info.mp3_header.sample_freq, info.mp3_header.mode))
printMsg("-" * self.terminal_width)
@staticmethod
def _getDefaultNameForObject(obj_frame, suffix=""):
if obj_frame.filename:
name_str = obj_frame.filename
else:
name_str = obj_frame.description
name_str += ".%s" % obj_frame.mime_type.split("/")[1]
if suffix:
name_str += suffix
return name_str
[docs] def printTag(self, tag):
if isinstance(tag, id3.Tag):
if self.args.quiet:
printMsg("ID3 %s: %d frames" %
(id3.versionToString(tag.version),
len(tag.frame_set)))
return
printMsg("ID3 %s:" % id3.versionToString(tag.version))
artist = tag.artist if tag.artist else u""
title = tag.title if tag.title else u""
album = tag.album if tag.album else u""
printMsg("%s: %s" % (boldText("title"), title))
printMsg("%s: %s" % (boldText("artist"), artist))
printMsg("%s: %s" % (boldText("album"), album))
if tag.album_artist:
printMsg("%s: %s" % (boldText("album artist"),
tag.album_artist))
if tag.composer:
printMsg("%s: %s" % (boldText("composer"), tag.composer))
for date, date_label in [
(tag.release_date, "release date"),
(tag.original_release_date, "original release date"),
(tag.recording_date, "recording date"),
(tag.encoding_date, "encoding date"),
(tag.tagging_date, "tagging date"),
]:
if date:
printMsg("%s: %s" % (boldText(date_label), str(date)))
track_str = ""
(track_num, track_total) = tag.track_num
if track_num is not None:
track_str = str(track_num)
if track_total:
track_str += "/%d" % track_total
genre = tag._getGenre(id3_std=not self.args.non_std_genres)
genre_str = "%s: %s (id %s)" % (boldText("genre"),
genre.name,
str(genre.id)) if genre else u""
printMsg("%s: %s\t\t%s" % (boldText("track"), track_str, genre_str))
(num, total) = tag.disc_num
if num is not None:
disc_str = str(num)
if total:
disc_str += "/%d" % total
printMsg("%s: %s" % (boldText("disc"), disc_str))
# PCNT
play_count = tag.play_count
if tag.play_count is not None:
printMsg("%s %d" % (boldText("Play Count:"), play_count))
# POPM
for popm in tag.popularities:
printMsg("%s [email: %s] [rating: %d] [play count: %d]" %
(boldText("Popularity:"), popm.email, popm.rating,
popm.count))
# TBPM
bpm = tag.bpm
if bpm is not None:
printMsg("%s %d" % (boldText("BPM:"), bpm))
# TPUB
pub = tag.publisher
if pub is not None:
printMsg("%s %s" % (boldText("Publisher/label:"), pub))
# UFID
for ufid in tag.unique_file_ids:
printMsg("%s [%s] : %s" %
(boldText("Unique File ID:"), ufid.owner_id,
ufid.uniq_id.decode("unicode_escape")))
# COMM
for c in tag.comments:
printMsg("%s: [Description: %s] [Lang: %s]\n%s" %
(boldText("Comment"), c.description or "",
c.lang.decode("ascii") or "", c.text or ""))
# USLT
for l in tag.lyrics:
printMsg("%s: [Description: %s] [Lang: %s]\n%s" %
(boldText("Lyrics"), l.description or u"",
l.lang.decode("ascii") or "", l.text))
# TXXX
for f in tag.user_text_frames:
printMsg("%s: [Description: %s]\n%s" %
(boldText("UserTextFrame"), f.description, f.text))
# URL frames
for desc, url in (("Artist URL", tag.artist_url),
("Audio source URL", tag.audio_source_url),
("Audio file URL", tag.audio_file_url),
("Internet radio URL", tag.internet_radio_url),
("Commercial URL", tag.commercial_url),
("Payment URL", tag.payment_url),
("Publisher URL", tag.publisher_url),
("Copyright URL", tag.copyright_url),
):
if url:
printMsg("%s: %s" % (boldText(desc), url))
# user url frames
for u in tag.user_url_frames:
printMsg("%s [Description: %s]: %s" % (u.id, u.description,
u.url))
# APIC
for img in tag.images:
if img.mime_type not in ImageFrame.URL_MIME_TYPE_VALUES:
printMsg("%s: [Size: %d bytes] [Type: %s]" %
(boldText(img.picTypeToString(img.picture_type) +
" Image"),
len(img.image_data),
img.mime_type))
printMsg("Description: %s" % img.description)
printMsg("")
else:
printMsg("%s: [Type: %s] [URL: %s]" %
(boldText(img.picTypeToString(img.picture_type) +
" Image"),
img.mime_type, img.image_url))
printMsg("Description: %s" % img.description)
printMsg("")
# GOBJ
for obj in tag.objects:
printMsg("%s: [Size: %d bytes] [Type: %s]" %
(boldText("GEOB"), len(obj.object_data),
obj.mime_type))
printMsg("Description: %s" % obj.description)
printMsg("Filename: %s" % obj.filename)
printMsg("\n")
if self.args.write_objects_dir:
obj_path = "%s%s" % (self.args.write_objects_dir, os.sep)
if not os.path.isdir(obj_path):
raise IOError("Directory does not exist: %s" % obj_path)
obj_file = self._getDefaultNameForObject(obj)
count = 1
while os.path.exists(os.path.join(obj_path, obj_file)):
obj_file = self._getDefaultNameForObject(obj,
str(count))
count += 1
printWarning("Writing %s..." % os.path.join(obj_path,
obj_file))
with open(os.path.join(obj_path, obj_file), "wb") as fp:
fp.write(obj.object_data)
# PRIV
for p in tag.privates:
printMsg("%s: [Data: %d bytes]" % (boldText("PRIV"),
len(p.data)))
printMsg("Owner Id: %s" % p.owner_id.decode("ascii"))
# MCDI
if tag.cd_id:
printMsg("\n%s: [Data: %d bytes]" % (boldText("MCDI"),
len(tag.cd_id)))
# USER
if tag.terms_of_use:
printMsg("\nTerms of Use (%s): %s" % (boldText("USER"),
tag.terms_of_use))
# --verbose
if self.args.verbose:
printMsg("-" * self.terminal_width)
printMsg("%d ID3 Frames:" % len(tag.frame_set))
for fid in tag.frame_set:
frames = tag.frame_set[fid]
num_frames = len(frames)
count = " x %d" % num_frames if num_frames > 1 else ""
if not tag.isV1():
total_bytes = sum(
tuple(frame.header.data_size + frame.header.size
for frame in frames if frame.header))
else:
total_bytes = 30
if total_bytes:
printMsg("%s%s (%d bytes)" % (fid.decode("ascii"),
count, total_bytes))
printMsg("%d bytes unused (padding)" %
(tag.file_info.tag_padding_size, ))
else:
raise TypeError("Unknown tag type: " + str(type(tag)))
[docs] def handleRemoves(self, tag):
remove_version = 0
status = False
rm_str = ""
if self.args.remove_all:
remove_version = id3.ID3_ANY_VERSION
rm_str = "v1.x and/or v2.x"
elif self.args.remove_v1:
remove_version = id3.ID3_V1
rm_str = "v1.x"
elif self.args.remove_v2:
remove_version = id3.ID3_V2
rm_str = "v2.x"
if remove_version:
status = id3.Tag.remove(
tag.file_info.name, remove_version,
preserve_file_time=self.args.preserve_file_time)
printWarning("Removing ID3 %s tag: %s" %
(rm_str, "SUCCESS" if status else "FAIL"))
return status
[docs] def handlePadding(self, tag):
max_padding = self.args.max_padding
if max_padding is None or max_padding is True:
return False
padding = tag.file_info.tag_padding_size
needs_change = padding > max_padding
return needs_change
[docs] def handleEdits(self, tag):
retval = False
# --remove-all-*, Handling removes first means later options are still
# applied
for what, arg, fid in (("comments", self.args.remove_all_comments,
id3.frames.COMMENT_FID),
("lyrics", self.args.remove_all_lyrics,
id3.frames.LYRICS_FID),
("images", self.args.remove_all_images,
id3.frames.IMAGE_FID),
("objects", self.args.remove_all_objects,
id3.frames.OBJECT_FID),
):
if arg and tag.frame_set[fid]:
printWarning("Removing all %s..." % what)
del tag.frame_set[fid]
retval = True
# --artist, --title, etc. All common/simple text frames.
for (what, setFunc) in (
("artist", partial(tag._setArtist, self.args.artist)),
("album", partial(tag._setAlbum, self.args.album)),
("album artist", partial(tag._setAlbumArtist,
self.args.album_artist)),
("title", partial(tag._setTitle, self.args.title)),
("genre", partial(tag._setGenre, self.args.genre,
id3_std=not self.args.non_std_genres)),
("release date", partial(tag._setReleaseDate,
self.args.release_date)),
("original release date", partial(tag._setOrigReleaseDate,
self.args.orig_release_date)),
("recording date", partial(tag._setRecordingDate,
self.args.recording_date)),
("encoding date", partial(tag._setEncodingDate,
self.args.encoding_date)),
("tagging date", partial(tag._setTaggingDate,
self.args.tagging_date)),
("beats per minute", partial(tag._setBpm, self.args.bpm)),
("publisher", partial(tag._setPublisher, self.args.publisher)),
("composer", partial(tag._setComposer, self.args.composer)),
):
if setFunc.args[0] is not None:
printWarning("Setting %s: %s" % (what, setFunc.args[0]))
setFunc()
retval = True
def _checkNumberedArgTuples(curr, new):
n = None
if new not in [(None, None), curr]:
n = [None] * 2
for i in (0, 1):
if new[i] == 0:
n[i] = None
else:
n[i] = new[i] or curr[i]
n = tuple(n)
# Returing None means do nothing, (None, None) would clear both vals
return n
# --track, --track-total
track_info = _checkNumberedArgTuples(tag.track_num,
(self.args.track,
self.args.track_total))
if track_info is not None:
printWarning("Setting track info: %s" % str(track_info))
tag.track_num = track_info
retval = True
# --track-offset
if self.args.track_offset:
offset = self.args.track_offset
tag.track_num = (tag.track_num[0] + offset, tag.track_num[1])
printWarning("%s track info by %d: %d" %
("Incrementing" if offset > 0 else "Decrementing",
offset, tag.track_num[0]))
retval = True
# --disc-num, --disc-total
disc_info = _checkNumberedArgTuples(tag.disc_num,
(self.args.disc_num,
self.args.disc_total))
if disc_info is not None:
printWarning("Setting disc info: %s" % str(disc_info))
tag.disc_num = disc_info
retval = True
# -Y, --release-year
if self.args.release_year is not None:
# empty string means clean, None means not given
year = self.args.release_year
printWarning("Setting release year: %s" % year)
tag.release_date = int(year) if year else None
retval = True
# -c , simple comment
if self.args.simple_comment:
# Just add it as if it came in --add-comment
self.args.comments.append((self.args.simple_comment, u"",
id3.DEFAULT_LANG))
# --remove-comment, remove-lyrics, --remove-image, --remove-object
for what, arg, accessor in (("comment", self.args.remove_comment,
tag.comments),
("lyrics", self.args.remove_lyrics,
tag.lyrics),
("image", self.args.remove_image,
tag.images),
("object", self.args.remove_object,
tag.objects),
):
for vals in arg:
if type(vals) in compat.StringTypes:
frame = accessor.remove(vals)
else:
frame = accessor.remove(*vals)
if frame:
printWarning("Removed %s %s" % (what, str(vals)))
retval = True
else:
printError("Removing %s failed, %s not found" %
(what, str(vals)))
# --add-comment, --add-lyrics
for what, arg, accessor in (("comment", self.args.comments,
tag.comments),
("lyrics", self.args.lyrics, tag.lyrics),
):
for text, desc, lang in arg:
printWarning("Setting %s: %s/%s" %
(what, desc, compat.unicode(lang, "ascii")))
accessor.set(text, desc, compat.b(lang))
retval = True
# --play-count
playcount_arg = self.args.play_count
if playcount_arg:
increment, pc = playcount_arg
if increment:
printWarning("Increment play count by %d" % pc)
tag.play_count += pc
else:
printWarning("Setting play count to %d" % pc)
tag.play_count = pc
retval = True
# --add-popularty
for email, rating, play_count in self.args.popularities:
tag.popularities.set(email.encode("latin1"), rating, play_count)
retval = True
# --remove-popularity
for email in self.args.remove_popularity:
popm = tag.popularities.remove(email.encode("latin1"))
if popm:
retval = True
# --text-frame, --url-frame
for what, arg, setter in (
("text frame", self.args.text_frames, tag.setTextFrame),
("url frame", self.args.url_frames, tag._setUrlFrame),
):
for fid, text in arg:
if text:
printWarning("Setting %s %s to '%s'" % (fid, what, text))
else:
printWarning("Removing %s %s" % (fid, what))
setter(fid, text)
retval = True
# --user-text-frame, --user-url-frame
for what, arg, accessor in (
("user text frame", self.args.user_text_frames,
tag.user_text_frames),
("user url frame", self.args.user_url_frames,
tag.user_url_frames),
):
for desc, text in arg:
if text:
printWarning("Setting '%s' %s to '%s'" % (desc, what, text))
accessor.set(text, desc)
else:
printWarning("Removing '%s' %s" % (desc, what))
accessor.remove(desc)
retval = True
# --add-image
for img_path, img_type, img_mt, img_desc in self.args.images:
assert(img_path)
printWarning("Adding image %s" % img_path)
if img_mt not in ImageFrame.URL_MIME_TYPE_VALUES:
with open(img_path, "rb") as img_fp:
tag.images.set(img_type, img_fp.read(), img_mt, img_desc)
else:
tag.images.set(img_type, None, None, img_desc, img_url=img_path)
retval = True
# --add-object
for obj_path, obj_mt, obj_desc, obj_fname in self.args.objects or []:
assert(obj_path)
printWarning("Adding object %s" % obj_path)
with open(obj_path, "rb") as obj_fp:
tag.objects.set(obj_fp.read(), obj_mt, obj_desc, obj_fname)
retval = True
# --unique-file-id
for arg in self.args.unique_file_ids:
owner_id, id = arg
if not id:
if tag.unique_file_ids.remove(owner_id):
printWarning("Removed unique file ID '%s'" % owner_id)
retval = True
else:
printWarning("Unique file ID '%s' not found" % owner_id)
else:
tag.unique_file_ids.set(id, owner_id.encode("latin1"))
printWarning("Setting unique file ID '%s' to %s" %
(owner_id, id))
retval = True
# --remove-frame
for fid in self.args.remove_fids:
assert(isinstance(fid, compat.BytesType))
if fid in tag.frame_set:
del tag.frame_set[fid]
retval = True
return retval
def _getTemplateKeys():
keys = list(id3.TagTemplate("")._makeMapping(None, False).keys())
keys.sort()
return ", ".join(["$%s" % v for v in keys])
ARGS_HELP = {
"--artist": "Set the artist name.",
"--album": "Set the album name.",
"--album-artist": u"Set the album artist name. '%s', for example. "
"Another example is collaborations when the "
"track artist might be 'Eminem featuring Proof' "
"the album artist would be 'Eminem'." %
core.VARIOUS_ARTISTS,
"--title": "Set the track title.",
"--track": "Set the track number. Use 0 to clear.",
"--track-total": "Set total number of tracks. Use 0 to clear.",
"--disc-num": "Set the disc number. Use 0 to clear.",
"--disc-total": "Set total number of discs in set. Use 0 to clear.",
"--genre": "Set the genre. If the argument is a standard ID3 genre "
"name or number both will be set. Otherwise, any string "
"can be used. Run 'eyeD3 --plugin=genres' for a list of "
"standard ID3 genre names/ids.",
"--non-std-genres": "Disables certain ID3 genre standards, such as the "
"mapping of numeric value to genre names.",
"--release-year": "Set the year the track was released. Use the date "
"options for more precise values or dates other "
"than release.",
"--v1": "Only read and write ID3 v1.x tags. By default, v1.x tags are "
"only read or written if there is not a v2 tag in the file.",
"--v2": "Only read/write ID3 v2.x tags. This is the default unless "
"the file only contains a v1 tag.",
"--to-v1.1": "Convert the file's tag to ID3 v1.1 (Or 1.0 if there is "
"no track number)",
"--to-v2.3": "Convert the file's tag to ID3 v2.3",
"--to-v2.4": "Convert the file's tag to ID3 v2.4",
"--release-date": "Set the date the track/album was released",
"--orig-release-date": "Set the original date the track/album was "
"released",
"--recording-date": "Set the date the track/album was recorded",
"--encoding-date": "Set the date the file was encoded",
"--tagging-date": "Set the date the file was tagged",
"--comment": "Set a comment. In ID3 tags this is the comment with "
"an empty description. See --add-comment to add multiple "
"comment frames.",
"--add-comment":
"Add or replace a comment. There may be more than one comment in a "
"tag, as long as the DESCRIPTION and LANG values are unique. The "
"default DESCRIPTION is '' and the default language code is '%s'." %
compat.unicode(id3.DEFAULT_LANG, "ascii"),
"--remove-comment": "Remove comment matching DESCRIPTION and LANG. "
"The default language code is '%s'." %
compat.unicode(id3.DEFAULT_LANG, "ascii"),
"--remove-all-comments": "Remove all comments from the tag.",
"--add-lyrics":
"Add or replace a lyrics. There may be more than one set of lyrics "
"in a tag, as long as the DESCRIPTION and LANG values are unique. "
"The default DESCRIPTION is '' and the default language code is "
"'%s'." % compat.unicode(id3.DEFAULT_LANG, "ascii"),
"--remove-lyrics": "Remove lyrics matching DESCRIPTION and LANG. "
"The default language code is '%s'." %
compat.unicode(id3.DEFAULT_LANG, "ascii"),
"--remove-all-lyrics": "Remove all lyrics from the tag.",
"--publisher": "Set the publisher/label name",
"--play-count": "Set the number of times played counter. If the "
"argument value begins with '+' the tag's play count "
"is incremented by N, otherwise the value is set to "
"exactly N.",
"--bpm": "Set the beats per minute value.",
"--text-frame": "Set the value of a text frame. To remove the "
"frame, specify an empty value. For example, "
"--text-frame='TDRC:'",
"--user-text-frame": "Set the value of a user text frame (i.e., TXXX). "
"To remove the frame, specify an empty value. "
"e.g., --user-text-frame='SomeDesc:'",
"--url-frame": "Set the value of a URL frame. To remove the frame, "
"specify an empty value. e.g., --url-frame='WCOM:'",
"--user-url-frame": "Set the value of a user URL frame (i.e., WXXX). "
"To remove the frame, specify an empty value. "
"e.g., --user-url-frame='SomeDesc:'",
"--add-image": "Add or replace an image. There may be more than one "
"image in a tag, as long as the DESCRIPTION values are "
"unique. The default DESCRIPTION is ''. If PATH begins "
"with 'http[s]://' then it is interpreted as a URL "
"instead of a file containing image data. The TYPE must "
"be one of the following: %s."
% (", ".join([ImageFrame.picTypeToString(t)
for t in range(ImageFrame.MIN_TYPE,
ImageFrame.MAX_TYPE + 1)]),
),
"--remove-image": "Remove image matching DESCRIPTION.",
"--remove-all-images": "Remove all images from the tag",
"--write-images": "Causes all attached images (APIC frames) to be "
"written to the specified directory.",
"--add-object": "Add or replace an object. There may be more than one "
"object in a tag, as long as the DESCRIPTION values "
"are unique. The default DESCRIPTION is ''.",
"--remove-object": "Remove object matching DESCRIPTION.",
"--remove-all-objects": "Remove all objects from the tag",
"--write-objects": "Causes all attached objects (GEOB frames) to be "
"written to the specified directory.",
"--add-popularty": "Adds a pupularity metric. There may be multiples "
"popularity values, but each must have a unique "
"email address component. The rating is a number "
"between 0 (worst) and 255 (best). The play count "
"is optional, and defaults to 0, since there is "
"already a dedicated play count frame.",
"--remove-popularity": "Removes the popularity frame with the "
"specified email key.",
"--remove-v1": "Remove ID3 v1.x tag.",
"--remove-v2": "Remove ID3 v2.x tag.",
"--remove-all": "Remove ID3 v1.x and v2.x tags.",
"--remove-frame": "Remove all frames with the given ID. This option "
"may be specified multiple times.",
"--max-padding": "Shrink file if tag padding (unused space) exceeds "
"the given number of bytes. "
"(Useful e.g. after removal of large cover art.) "
"Default is 64 KiB, file will be rewritten with "
"default padding (1 KiB) or max padding, whichever "
"is smaller.",
"--no-max-padding": "Disable --max-padding altogether.",
"--force-update": "Rewrite the tag despite there being no edit "
"options.",
"--verbose": "Show all available tag data",
"--unique-file-id": "Add a unique file ID frame. If the ID arg is "
"empty the frame is removed. An OWNER_ID is "
"required. The ID may be no more than 64 bytes.",
"--encoding": "Set the encoding that is used for all text frames. "
"This option is only applied if the tag is updated "
"as the result of an edit option (e.g. --artist, "
"--title, etc.) or --force-update is specified.",
"--rename": "Rename file (the extension is not affected) "
"based on data in the tag using substitution "
"variables: " + _getTemplateKeys(),
"--preserve-file-times": "When writing, do not update file "
"modification times.",
"--track-offset": "Increment/decrement the track number by [-]N. "
"This option is applied after --track=N is set.",
"--composer": "Set the composer's name.",
}