Source code for eyed3.plugins.classic

# -*- 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 printHeader(self, file_path): file_len = len(file_path) from stat import ST_SIZE file_size = os.stat(file_path)[ST_SIZE] size_str = utils.formatSize(file_size) size_len = len(size_str) + 5 if file_len + size_len >= self.terminal_width: file_path = "..." + file_path[-(75 - size_len):] file_len = len(file_path) pat_len = self.terminal_width - file_len - size_len printMsg("%s%s%s[ %s ]%s" % (boldText(file_path, c=HEADER_COLOR()), HEADER_COLOR(), " " * pat_len, size_str, Fore.RESET))
[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.", }