Merge branch 'master' into list_scripting_support

Conflicts:
	picard/script.py
This commit is contained in:
Chad Wilson
2012-01-20 01:28:33 +08:00
10 changed files with 149 additions and 124 deletions

View File

@@ -1,4 +1,5 @@
Version UNRELEASED - 2012-XX-XX
* Add %license% tag
* Made %writer% available to tagger scripts and plugins with contents of songwriter (PICARD-21)
Version 0.16 - 2011-10-23

View File

@@ -7,7 +7,7 @@ PLUGIN_NAME = u"ReplayGain"
PLUGIN_AUTHOR = u"Philipp Wolfer"
PLUGIN_DESCRIPTION = """Calculate ReplayGain for selected files and albums."""
PLUGIN_VERSION = "0.1"
PLUGIN_API_VERSIONS = ["0.10", "0.15"]
PLUGIN_API_VERSIONS = ["0.10", "0.15", "0.16"]
from PyQt4 import QtCore
@@ -17,7 +17,7 @@ from picard.track import Track
from picard.file import File
from picard.util import encode_filename, decode_filename, partial
from picard.ui.options import register_options_page, OptionsPage
from picard.config import BoolOption, IntOption, TextOption
from picard.config import TextOption
from picard.ui.itemviews import (BaseAction, register_file_action,
register_album_action)
from picard.plugins.replaygain.ui_options_replaygain import Ui_ReplayGainOptionsPage
@@ -28,12 +28,13 @@ REPLAYGAIN_COMMANDS = {
"Ogg Vorbis": ("replaygain_vorbisgain_command", "replaygain_vorbisgain_options"),
"MPEG-1 Audio": ("replaygain_mp3gain_command", "replaygain_mp3gain_options"),
"FLAC": ("replaygain_metaflac_command", "replaygain_metaflac_options"),
"WavPack": ("replaygain_wvgain_command", "replaygain_wvgain_options"),
}
def calculate_replay_gain_for_files(files, format, tagger):
"""Calculates the replay gain for a list of files in album mode."""
file_list = ['%s' % encode_filename(f.filename) for f in files]
if REPLAYGAIN_COMMANDS.has_key(format) \
and tagger.config.setting[REPLAYGAIN_COMMANDS[format][0]]:
command = tagger.config.setting[REPLAYGAIN_COMMANDS[format][0]]
@@ -45,7 +46,7 @@ def calculate_replay_gain_for_files(files, format, tagger):
class ReplayGain(BaseAction):
NAME = N_("Calculate replay &gain...")
def _add_file_to_queue(self, file):
self.tagger.other_queue.put((
partial(self._calculate_replaygain, file),
@@ -63,7 +64,7 @@ class ReplayGain(BaseAction):
def _calculate_replaygain(self, file):
self.tagger.window.set_statusbar_message(N_('Calculating replay gain for "%s"...'), file.filename)
calculate_replay_gain_for_files([file], file.NAME, self.tagger)
def _replaygain_callback(self, file, result=None, error=None):
if not error:
self.tagger.window.set_statusbar_message(N_('Replay gain for "%s" successfully calculated.'), file.filename)
@@ -72,7 +73,7 @@ class ReplayGain(BaseAction):
class AlbumGain(BaseAction):
NAME = N_("Calculate album &gain...")
def callback(self, objs):
albums = [o for o in objs if isinstance(o, Album)]
for album in albums:
@@ -80,32 +81,32 @@ class AlbumGain(BaseAction):
partial(self._calculate_albumgain, album),
partial(self._albumgain_callback, album),
QtCore.Qt.NormalEventPriority))
def split_files_by_type(self, files):
"""Split the given files by filetype into separate lists."""
files_by_format = {}
for file in files:
if not files_by_format.has_key(file.NAME):
files_by_format[file.NAME] = [file]
else:
files_by_format[file.NAME].append(file)
return files_by_format
def _calculate_albumgain(self, album):
self.tagger.window.set_statusbar_message(N_('Calculating album gain for "%s"...'), album.metadata["album"])
filelist = [t.linked_files[0] for t in album.tracks if t.is_linked()]
for format, files in self.split_files_by_type(filelist).iteritems():
calculate_replay_gain_for_files(files, format, self.tagger)
def _albumgain_callback(self, album, result=None, error=None):
if not error:
self.tagger.window.set_statusbar_message(N_('Album gain for "%s" successfully calculated.'), album.metadata["album"])
else:
self.tagger.window.set_statusbar_message(N_('Could not calculate album gain for "%s".'), album.metadata["album"])
class ReplayGainOptionsPage(OptionsPage):
NAME = "replaygain"
@@ -119,6 +120,8 @@ class ReplayGainOptionsPage(OptionsPage):
TextOption("setting", "replaygain_mp3gain_options", "-a"),
TextOption("setting", "replaygain_metaflac_command", "metaflac"),
TextOption("setting", "replaygain_metaflac_options", "--add-replay-gain"),
TextOption("setting", "replaygain_wvgain_command", "wvgain"),
TextOption("setting", "replaygain_wvgain_options", "-a")
]
def __init__(self, parent=None):
@@ -130,12 +133,14 @@ class ReplayGainOptionsPage(OptionsPage):
self.ui.vorbisgain_command.setText(self.config.setting["replaygain_vorbisgain_command"])
self.ui.mp3gain_command.setText(self.config.setting["replaygain_mp3gain_command"])
self.ui.metaflac_command.setText(self.config.setting["replaygain_metaflac_command"])
self.ui.wvgain_command.setText(self.config.setting["replaygain_wvgain_command"])
def save(self):
self.config.setting["replaygain_vorbisgain_command"] = unicode(self.ui.vorbisgain_command.text())
self.config.setting["replaygain_mp3gain_command"] = unicode(self.ui.mp3gain_command.text())
self.config.setting["replaygain_metaflac_command"] = unicode(self.ui.metaflac_command.text())
self.config.setting["replaygain_wvgain_command"] = unicode(self.ui.wvgain_command.text())
register_file_action(ReplayGain())
register_album_action(AlbumGain())
register_options_page(ReplayGainOptionsPage)

View File

@@ -1,7 +1,8 @@
<ui version="4.0" >
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ReplayGainOptionsPage</class>
<widget class="QWidget" name="ReplayGainOptionsPage" >
<property name="geometry" >
<widget class="QWidget" name="ReplayGainOptionsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
@@ -9,82 +10,74 @@
<height>317</height>
</rect>
</property>
<layout class="QVBoxLayout" >
<property name="spacing" >
<layout class="QVBoxLayout">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin" >
<number>9</number>
</property>
<property name="topMargin" >
<number>9</number>
</property>
<property name="rightMargin" >
<number>9</number>
</property>
<property name="bottomMargin" >
<property name="margin">
<number>9</number>
</property>
<item>
<widget class="QGroupBox" name="replay_gain" >
<property name="title" >
<widget class="QGroupBox" name="replay_gain">
<property name="title">
<string>Replay Gain</string>
</property>
<layout class="QVBoxLayout" >
<property name="spacing" >
<layout class="QVBoxLayout">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin" >
<number>9</number>
</property>
<property name="topMargin" >
<number>9</number>
</property>
<property name="rightMargin" >
<number>9</number>
</property>
<property name="bottomMargin" >
<property name="margin">
<number>9</number>
</property>
<item>
<widget class="QLabel" name="label" >
<property name="text" >
<widget class="QLabel" name="label">
<property name="text">
<string>Path to VorbisGain:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="vorbisgain_command" />
<widget class="QLineEdit" name="vorbisgain_command"/>
</item>
<item>
<widget class="QLabel" name="label_2" >
<property name="text" >
<widget class="QLabel" name="label_2">
<property name="text">
<string>Path to MP3Gain:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="mp3gain_command" />
<widget class="QLineEdit" name="mp3gain_command"/>
</item>
<item>
<widget class="QLabel" name="label_3" >
<property name="text" >
<widget class="QLabel" name="label_3">
<property name="text">
<string>Path to metaflac:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="metaflac_command" />
<widget class="QLineEdit" name="metaflac_command"/>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Path to wvgain:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="wvgain_command"/>
</item>
</layout>
</widget>
</item>
<item>
<spacer>
<property name="orientation" >
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" >
<property name="sizeHint" stdset="0">
<size>
<width>263</width>
<height>21</height>

View File

@@ -2,57 +2,58 @@
# Form implementation generated from reading ui file 'options_replaygain.ui'
#
# Created: Thu Mar 13 23:07:48 2008
# by: PyQt4 UI code generator 4.3
# Created: Sun Jan 8 13:42:44 2012
# by: PyQt4 UI code generator 4.9
#
# WARNING! All changes made in this file will be lost!
from PyQt4 import QtCore, QtGui
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
_fromUtf8 = lambda s: s
class Ui_ReplayGainOptionsPage(object):
def setupUi(self, ReplayGainOptionsPage):
ReplayGainOptionsPage.setObjectName("ReplayGainOptionsPage")
ReplayGainOptionsPage.resize(QtCore.QSize(QtCore.QRect(0,0,305,317).size()).expandedTo(ReplayGainOptionsPage.minimumSizeHint()))
ReplayGainOptionsPage.setObjectName(_fromUtf8("ReplayGainOptionsPage"))
ReplayGainOptionsPage.resize(305, 317)
self.vboxlayout = QtGui.QVBoxLayout(ReplayGainOptionsPage)
self.vboxlayout.setSpacing(6)
self.vboxlayout.setMargin(9)
self.vboxlayout.setObjectName("vboxlayout")
self.vboxlayout.setObjectName(_fromUtf8("vboxlayout"))
self.replay_gain = QtGui.QGroupBox(ReplayGainOptionsPage)
self.replay_gain.setObjectName("replay_gain")
self.replay_gain.setObjectName(_fromUtf8("replay_gain"))
self.vboxlayout1 = QtGui.QVBoxLayout(self.replay_gain)
self.vboxlayout1.setSpacing(2)
self.vboxlayout1.setMargin(9)
self.vboxlayout1.setObjectName("vboxlayout1")
self.vboxlayout1.setObjectName(_fromUtf8("vboxlayout1"))
self.label = QtGui.QLabel(self.replay_gain)
self.label.setObjectName("label")
self.label.setObjectName(_fromUtf8("label"))
self.vboxlayout1.addWidget(self.label)
self.vorbisgain_command = QtGui.QLineEdit(self.replay_gain)
self.vorbisgain_command.setObjectName("vorbisgain_command")
self.vorbisgain_command.setObjectName(_fromUtf8("vorbisgain_command"))
self.vboxlayout1.addWidget(self.vorbisgain_command)
self.label_2 = QtGui.QLabel(self.replay_gain)
self.label_2.setObjectName("label_2")
self.label_2.setObjectName(_fromUtf8("label_2"))
self.vboxlayout1.addWidget(self.label_2)
self.mp3gain_command = QtGui.QLineEdit(self.replay_gain)
self.mp3gain_command.setObjectName("mp3gain_command")
self.mp3gain_command.setObjectName(_fromUtf8("mp3gain_command"))
self.vboxlayout1.addWidget(self.mp3gain_command)
self.label_3 = QtGui.QLabel(self.replay_gain)
self.label_3.setObjectName("label_3")
self.label_3.setObjectName(_fromUtf8("label_3"))
self.vboxlayout1.addWidget(self.label_3)
self.metaflac_command = QtGui.QLineEdit(self.replay_gain)
self.metaflac_command.setObjectName("metaflac_command")
self.metaflac_command.setObjectName(_fromUtf8("metaflac_command"))
self.vboxlayout1.addWidget(self.metaflac_command)
self.label_4 = QtGui.QLabel(self.replay_gain)
self.label_4.setObjectName(_fromUtf8("label_4"))
self.vboxlayout1.addWidget(self.label_4)
self.wvgain_command = QtGui.QLineEdit(self.replay_gain)
self.wvgain_command.setObjectName(_fromUtf8("wvgain_command"))
self.vboxlayout1.addWidget(self.wvgain_command)
self.vboxlayout.addWidget(self.replay_gain)
spacerItem = QtGui.QSpacerItem(263,21,QtGui.QSizePolicy.Minimum,QtGui.QSizePolicy.Expanding)
spacerItem = QtGui.QSpacerItem(263, 21, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
self.vboxlayout.addItem(spacerItem)
self.retranslateUi(ReplayGainOptionsPage)
@@ -63,4 +64,5 @@ class Ui_ReplayGainOptionsPage(object):
self.label.setText(QtGui.QApplication.translate("ReplayGainOptionsPage", "Path to VorbisGain:", None, QtGui.QApplication.UnicodeUTF8))
self.label_2.setText(QtGui.QApplication.translate("ReplayGainOptionsPage", "Path to MP3Gain:", None, QtGui.QApplication.UnicodeUTF8))
self.label_3.setText(QtGui.QApplication.translate("ReplayGainOptionsPage", "Path to metaflac:", None, QtGui.QApplication.UnicodeUTF8))
self.label_4.setText(QtGui.QApplication.translate("ReplayGainOptionsPage", "Path to wvgain:", None, QtGui.QApplication.UnicodeUTF8))

View File

@@ -180,7 +180,7 @@ class ID3File(File):
elif frameid == 'USLT':
name = 'lyrics'
if frame.desc:
name += frame.desc
name += ':%s' % frame.desc
metadata.add(name, unicode(frame.text))
elif frameid == 'UFID' and frame.owner == 'http://musicbrainz.org':
metadata['musicbrainz_trackid'] = unicode(frame.data)

View File

@@ -221,7 +221,7 @@ class CompatID3(ID3):
# New frames added in v2.4.
for key in ["ASPI", "EQU2", "RVA2", "SEEK", "SIGN", "TDRL", "TDTG",
"TMOO", "TPRO", "TSST"]:
"TMOO", "TPRO"]:
if key in self: del(self[key])
for frame in self.values():

View File

@@ -25,6 +25,7 @@ import unicodedata
from picard.metadata import Metadata
from picard.metadata import MULTI_VALUED_JOINER
from picard.plugin import ExtensionPoint
from inspect import getargspec
class ScriptError(Exception): pass
class ParseError(ScriptError): pass
@@ -55,7 +56,23 @@ class ScriptVariable(object):
class ScriptFunction(object):
def __init__(self, name, args):
def __init__(self, name, args, parser):
try:
expected_args = parser.functions[name][2]
if expected_args and (len(args) not in expected_args):
raise ScriptError(
"Wrong number of arguments for $%s: Expected %s, got %i at position %i, line %i"
% (name,
str(expected_args[0])
if len(expected_args) == 1
else
"%i - %i" % (min(expected_args), max(expected_args)),
len(args),
parser._x,
parser._y))
except KeyError:
raise UnknownFunction("Unknown function '%s'" % name)
self.name = name
self.args = args
@@ -63,15 +80,12 @@ class ScriptFunction(object):
return "<ScriptFunction $%s(%r)>" % (self.name, self.args)
def eval(self, parser):
try:
function, eval_args = parser.functions[self.name]
if eval_args:
args = [arg.eval(parser) for arg in self.args]
else:
args = self.args
return function(parser, *args)
except KeyError:
raise UnknownFunction("Unknown function '%s'" % self.name)
function, eval_args, num_args = parser.functions[self.name]
if eval_args:
args = [arg.eval(parser) for arg in self.args]
else:
args = self.args
return function(parser, *args)
class ScriptExpression(list):
@@ -150,7 +164,7 @@ Grammar:
name = self._text[start:self._pos-1]
if name not in self.functions:
raise UnknownFunction("Unknown function '%s'" % name)
return ScriptFunction(name, self.parse_arguments())
return ScriptFunction(name, self.parse_arguments(), self)
elif ch is None:
self.__raise_eof()
elif not isidentif(ch):
@@ -214,8 +228,8 @@ Grammar:
def load_functions(self):
self.functions = {}
for name, function, eval_args in ScriptParser._function_registry:
self.functions[name] = (function, eval_args)
for name, function, eval_args, num_args in ScriptParser._function_registry:
self.functions[name] = (function, eval_args, num_args)
def parse(self, script, functions=False):
"""Parse the script."""
@@ -239,22 +253,32 @@ Grammar:
return ScriptParser._cache[key].eval(self)
def register_script_function(function, name=None, eval_args=True):
def register_script_function(function, name=None, eval_args=True,
check_argcount=True):
"""Registers a script function. If ``name`` is ``None``,
``function.__name__`` will be used.
If ``eval_args`` is ``False``, the arguments will not be evaluated before being
passed to ``function``.
If ``check_argcount`` is ``False`` the number of arguments passed to the
function will not be verified."""
argspec = getargspec(function)
argcount = (len(argspec.args) - 1,) # -1 for the parser
if argspec.defaults is not None:
argcount = range(argcount[0] - len(argspec.defaults), argcount[0] + 1)
if name is None:
name = function.__name__
ScriptParser._function_registry.register(function.__module__, (name, function, eval_args))
ScriptParser._function_registry.register(function.__module__,
(name, function, eval_args,
argcount if argcount and check_argcount else False)
)
def func_if(parser, *args):
"""If ``if`` is not empty, it returns ``then``, otherwise it returns
``else``."""
nargs = len(args)
if nargs > 1:
if args[0].eval(parser):
return args[1].eval(parser)
if nargs == 3:
return args[2].eval(parser)
return ''
def func_if(parser, _if, _then, _else=None):
"""If ``_if`` is not empty, it returns ``_then``, otherwise it returns
``_else``."""
return _then if _if else _else if _else else ''
def func_if2(parser, *args):
"""Returns first non empty argument."""
@@ -528,9 +552,9 @@ def func_truncate(parser, text, length):
length = None
return text[:length].rstrip()
register_script_function(func_if, "if", eval_args=False)
register_script_function(func_if2, "if2", eval_args=False)
register_script_function(func_noop, "noop", eval_args=False)
register_script_function(func_if, "if")
register_script_function(func_if2, "if2", eval_args=False, check_argcount=False)
register_script_function(func_noop, "noop", eval_args=False, check_argcount=False)
register_script_function(func_left, "left")
register_script_function(func_right, "right")
register_script_function(func_lower, "lower")

View File

@@ -299,7 +299,7 @@ def translate_from_sortname(name, sortname):
parts = [sortname]
separator = ""
return separator.join(map(_reverse_sortname, parts))
return None
return name
try:

View File

@@ -1,13 +1,13 @@
import unittest
from picard.metadata import Metadata
from picard.mbxml import track_to_metadata, release_to_metadata
from picard.webservice import XmlNode
class config:
setting = {
"standardize_tracks": False,
"standardize_artists": False,
"standardize_releases": False
"standardize_releases": False,
"translate_artist_names": False
}
class XmlNode(object):

View File

@@ -85,23 +85,23 @@ class ShortFilenameTest(unittest.TestCase):
class TranslateArtistTest(unittest.TestCase):
def test_latin(self):
self.failUnlessEqual(u"Jean Michel Jarre", util.translate_artist(u"Jean Michel Jarre", u"Jarre, Jean Michel"))
self.failIfEqual(u"Jarre, Jean Michel", util.translate_artist(u"Jean Michel Jarre", u"Jarre, Jean Michel"))
self.failUnlessEqual(u"Jean Michel Jarre", util.translate_from_sortname(u"Jean Michel Jarre", u"Jarre, Jean Michel"))
self.failIfEqual(u"Jarre, Jean Michel", util.translate_from_sortname(u"Jean Michel Jarre", u"Jarre, Jean Michel"))
def test_kanji(self):
self.failUnlessEqual(u"Tetsuya Komuro", util.translate_artist(u"小室哲哉", u"Komuro, Tetsuya"))
self.failIfEqual(u"Komuro, Tetsuya", util.translate_artist(u"小室哲哉", u"Komuro, Tetsuya"))
self.failIfEqual(u"小室哲哉", util.translate_artist(u"小室哲哉", u"Komuro, Tetsuya"))
self.failUnlessEqual(u"Tetsuya Komuro", util.translate_from_sortname(u"小室哲哉", u"Komuro, Tetsuya"))
self.failIfEqual(u"Komuro, Tetsuya", util.translate_from_sortname(u"小室哲哉", u"Komuro, Tetsuya"))
self.failIfEqual(u"小室哲哉", util.translate_from_sortname(u"小室哲哉", u"Komuro, Tetsuya"))
def test_kanji2(self):
self.failUnlessEqual(u"Ayumi Hamasaki & Keiko", util.translate_artist(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko"))
self.failIfEqual(u"浜崎あゆみ & KEIKO", util.translate_artist(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko"))
self.failIfEqual(u"Hamasaki, Ayumi & Keiko", util.translate_artist(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko"))
self.failUnlessEqual(u"Ayumi Hamasaki & Keiko", util.translate_from_sortname(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko"))
self.failIfEqual(u"浜崎あゆみ & KEIKO", util.translate_from_sortname(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko"))
self.failIfEqual(u"Hamasaki, Ayumi & Keiko", util.translate_from_sortname(u"浜崎あゆみ & KEIKO", u"Hamasaki, Ayumi & Keiko"))
def test_cyrillic(self):
self.failUnlessEqual(U"Pyotr Ilyich Tchaikovsky", util.translate_artist(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich"))
self.failIfEqual(u"Tchaikovsky, Pyotr Ilyich", util.translate_artist(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich"))
self.failIfEqual(u"Пётр Ильич Чайковский", util.translate_artist(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich"))
self.failUnlessEqual(U"Pyotr Ilyich Tchaikovsky", util.translate_from_sortname(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich"))
self.failIfEqual(u"Tchaikovsky, Pyotr Ilyich", util.translate_from_sortname(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich"))
self.failIfEqual(u"Пётр Ильич Чайковский", util.translate_from_sortname(u"Пётр Ильич Чайковский", u"Tchaikovsky, Pyotr Ilyich"))
class FormatTimeTest(unittest.TestCase):