Skip to content

Instantly share code, notes, and snippets.

@woowee
Last active December 11, 2015 22:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save woowee/4670510 to your computer and use it in GitHub Desktop.
Save woowee/4670510 to your computer and use it in GitHub Desktop.
just idea/memo. no checking...
#!/usr/bin/env python
# Pretend to be /usr/bin/id3v2 from id3lib, sort of.
# Copyright 2005 Joe Wreschnig
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
import os
import sys
import locale
from optparse import OptionParser, SUPPRESS_HELP
try:
import mutagen, mutagen.id3
except ImportError:
# Run as ./mid3v2 out of tools/
sys.path.append(os.path.abspath("../"))
import mutagen, mutagen.id3
VERSION = (1, 3)
global verbose
verbose = True
class ID3OptionParser(OptionParser):
def __init__(self):
mutagen_version = ".".join(map(str, mutagen.version))
my_version = ".".join(map(str, VERSION))
version = "mid3v2 %s\nUses Mutagen %s" % (my_version, mutagen_version)
self.edits = []
OptionParser.__init__(
self, version=version,
usage="%prog [OPTION] [FILE]...",
description="Mutagen-based replacement for id3lib's id3v2.")
def format_help(self, *args, **kwargs):
text = OptionParser.format_help(self, *args, **kwargs)
return text + """\
You can set the value for any ID3v2 frame by using '--' and then a frame ID.
For example:
mid3v2 --TIT3 "Monkey!" file.mp3
would set the "Subtitle/Description" frame to "Monkey!".
Any editing operation will cause the ID3 tag to be upgraded to ID3v2.4.
"""
def list_frames(option, opt, value, parser):
items = mutagen.id3.Frames.items()
items.sort()
for name, frame in items:
print " --%s %s" % (name, frame.__doc__.split("\n")[0])
raise SystemExit
def list_frames_2_2(option, opt, value, parser):
items = mutagen.id3.Frames_2_2.items()
items.sort()
for name, frame in items:
print " --%s %s" % (name, frame.__doc__.split("\n")[0])
raise SystemExit
def list_genres(option, opt, value, parser):
for i, genre in enumerate(mutagen.id3.TCON.GENRES):
print "%3d: %s" % (i, genre)
raise SystemExit
def delete_tags(filenames, v1, v2):
for filename in filenames:
if verbose:
print "deleting ID3 tag info in %s" % filename
mutagen.id3.delete(filename, v1, v2)
def delete_frames(deletes, filenames):
frames = deletes.split(",")
for filename in filenames:
if verbose:
print "deleting %s from %s" % (deletes, filename)
try:
id3 = mutagen.id3.ID3(filename)
except mutagen.id3.ID3NoHeaderError:
if verbose:
print "No ID3 header found; skipping."
except StandardError, err:
print str(err)
else:
map(id3.delall, frames)
id3.save()
def write_files(edits, filenames):
enc = locale.getpreferredencoding()
edits = [(frame[2:], value.decode(enc)) for (frame, value) in edits]
# preprocess:
# for all [frame,value] pairs in the edits list
# gather values for identical frames into a list
tmp = {}
for frame, value in edits:
if tmp.has_key(frame):
tmp[frame].append(value)
else:
tmp[frame] = [value]
# edits is now a dictionary of frame -> [list of values]
edits = tmp
for filename in filenames:
if verbose:
print "Writing", filename
try:
id3 = mutagen.id3.ID3(filename)
except mutagen.id3.ID3NoHeaderError:
if verbose:
print "No ID3 header found; creating a new tag"
id3 = mutagen.id3.ID3()
except StandardError, err:
print str(err)
continue
for (frame, vlist) in edits.items():
if frame == "COMM":
for value in vlist:
values = value.split(":")
if len(values) == 1:
value, desc, lang = values[0], "", "eng"
elif len(values) == 2:
desc, value, lang = values[0], values[1], "eng"
else:
value = ":".join(values[1:-1])
desc, lang = values[0], values[-1]
frame = mutagen.id3.COMM(
encoding=3, text=value, lang=lang, desc=desc)
elif frame == "APIC":
fileext = value.split(".")[-1].lower()
mimetype = {
'jpeg':"image/jpeg",
"jpg":"image/jpeg",
"png":"image/png",
"gif":"image/gif"}.get(fileext,None)
if not mimetype: continue
fin = open(value)
image = fin.read()
frame = mutagen.id3.APIC(3, mimetype, 3, 'Front cover', image)
fin.close()
elif issubclass(mutagen.id3.Frames[frame], mutagen.id3.UrlFrame):
frame = mutagen.id3.Frames[frame](encoding=3, url=vlist)
else:
frame = mutagen.id3.Frames[frame](encoding=3, text=vlist)
id3.add(frame)
id3.save(filename)
def list_tags(filenames):
enc = locale.getpreferredencoding()
for filename in filenames:
print "IDv2 tag info for %s:" % filename
try:
id3 = mutagen.id3.ID3(filename, translate=False)
except StandardError, err:
print str(err)
else:
print id3.pprint().encode(enc, "replace")
def list_tags_raw(filenames):
for filename in filenames:
print "Raw IDv2 tag info for %s:" % filename
try:
id3 = mutagen.id3.ID3(filename, translate=False)
except StandardError, err:
print str(err)
else:
for frame in id3.values():
print repr(frame)
def main(argv):
parser = ID3OptionParser()
parser.add_option(
"-v", "--verbose", action="store_true", dest="verbose", default=False,
help="be verbose")
parser.add_option(
"-q", "--quiet", action="store_false", dest="verbose",
help="be quiet (the default)")
parser.add_option(
"-f", "--list-frames", action="callback", callback=list_frames,
help="Display all possible frames for ID3v2.3 / ID3v2.4")
parser.add_option(
"--list-frames-v2.2", action="callback", callback=list_frames_2_2,
help="Display all possible frames for ID3v2.2")
parser.add_option(
"-L", "--list-genres", action="callback", callback=list_genres,
help="Lists all ID3v1 genres")
parser.add_option(
"-l", "--list", action="store_const", dest="action", const="list",
help="Lists the tag(s) on the file(s)")
parser.add_option(
"--list-raw", action="store_const", dest="action", const="list-raw",
help="Lists the tag(s) on the file(s) in Python format")
parser.add_option(
"-d", "--delete-v2", action="store_const", dest="action",
const="delete-v2", help="Deletes ID3v2 tags")
parser.add_option(
"-s", "--delete-v1", action="store_const", dest="action",
const="delete-v1", help="Deletes ID3v1 tags")
parser.add_option(
"-D", "--delete-all", action="store_const", dest="action",
const="delete-v1-v2", help="Deletes ID3v1 and ID3v2 tags")
parser.add_option(
'--delete-frames', metavar='FID1,FID2,...', action='store',
dest='deletes', default='', help="Delete the given frames")
parser.add_option(
"-C", "--convert", action="store_const", dest="action",
const="convert",
help="Convert tags to ID3v2.4 (any editing will do this)")
parser.add_option(
"-a", "--artist", metavar='"ARTIST"', action="callback",
help="Set the artist information", type="string",
callback=lambda *args: args[3].edits.append(("--TPE1", args[2])))
parser.add_option(
"-A", "--album", metavar='"ALBUM"', action="callback",
help="Set the album title information", type="string",
callback=lambda *args: args[3].edits.append(("--TALB", args[2])))
parser.add_option(
"-t", "--song", metavar='"SONG"', action="callback",
help="Set the song title information", type="string",
callback=lambda *args: args[3].edits.append(("--TIT2", args[2])))
parser.add_option(
"-c", "--comment", metavar='"DESCRIPTION":"COMMENT":"LANGUAGE"',
action="callback", help="Set the comment information", type="string",
callback=lambda *args: args[3].edits.append(("--COMM", args[2])))
parser.add_option(
"-g", "--genre", metavar='"GENRE"', action="callback",
help="Set the genre or genre number", type="string",
callback=lambda *args: args[3].edits.append(("--TCON", args[2])))
parser.add_option(
"-y", "--year", "--date", metavar='YYYY[-MM-DD]', action="callback",
help="Set the year/date", type="string",
callback=lambda *args: args[3].edits.append(("--TDRC", args[2])))
parser.add_option(
"-T", "--track", metavar='"num/num"', action="callback",
help="Set the track number/(optional) total tracks", type="string",
callback=lambda *args: args[3].edits.append(("--TRCK", args[2])))
parser.add_option(
"-p", "--picture", metavar='"filename"', action="callback",
help="Set cover art to image in file (.jpg, .png or .gif)", type="string",
callback=lambda *args: args[3].edits.append(("--APIC", args[2])))
for frame in mutagen.id3.Frames:
if issubclass(mutagen.id3.Frames[frame], mutagen.id3.TextFrame)
or issubclass(mutagen.id3.Frames[frame], mutagen.id3.UrlFrame) \
or frame=='APIC':
parser.add_option(
"--" + frame, action="callback", help=SUPPRESS_HELP,
type='string', metavar="value", # optparse blows up with this
callback=lambda *args: args[3].edits.append(args[1:3]))
(options, args) = parser.parse_args(argv[1:])
global verbose
verbose = options.verbose
if args:
if parser.edits or options.deletes:
if options.deletes:
delete_frames(options.deletes, args)
if parser.edits:
write_files(parser.edits, args)
elif options.action in [None, 'list']:
list_tags(args)
elif options.action == "list-raw":
list_tags_raw(args)
elif options.action == "convert":
write_files([], args)
elif options.action.startswith("delete"):
delete_tags(args, "v1" in options.action, "v2" in options.action)
else:
parser.print_help()
else:
parser.print_help()
if __name__ == "__main__":
main(sys.argv)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment