diff --git a/NEWS.txt b/NEWS.txt index 17d5b17f8..3a03e2b33 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -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 diff --git a/contrib/plugins/replaygain/__init__.py b/contrib/plugins/replaygain/__init__.py index fde44048c..e2b018b56 100644 --- a/contrib/plugins/replaygain/__init__.py +++ b/contrib/plugins/replaygain/__init__.py @@ -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) diff --git a/contrib/plugins/replaygain/options_replaygain.ui b/contrib/plugins/replaygain/options_replaygain.ui index 94a6b7d36..32bfcd62f 100644 --- a/contrib/plugins/replaygain/options_replaygain.ui +++ b/contrib/plugins/replaygain/options_replaygain.ui @@ -1,7 +1,8 @@ - + + ReplayGainOptionsPage - - + + 0 0 @@ -9,82 +10,74 @@ 317 - - + + 6 - - 9 - - - 9 - - - 9 - - + 9 - - + + Replay Gain - - + + 2 - - 9 - - - 9 - - - 9 - - + 9 - - + + Path to VorbisGain: - + - - + + Path to MP3Gain: - + - - + + Path to metaflac: - + + + + + + Path to wvgain: + + + + + - + Qt::Vertical - + 263 21 diff --git a/contrib/plugins/replaygain/ui_options_replaygain.py b/contrib/plugins/replaygain/ui_options_replaygain.py index 7d8767a2a..95df97499 100644 --- a/contrib/plugins/replaygain/ui_options_replaygain.py +++ b/contrib/plugins/replaygain/ui_options_replaygain.py @@ -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)) diff --git a/picard/formats/id3.py b/picard/formats/id3.py index f4b8f2ae9..06bbe4749 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -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) diff --git a/picard/formats/mutagenext/compatid3.py b/picard/formats/mutagenext/compatid3.py index 554b2f32c..8ca98a6bc 100644 --- a/picard/formats/mutagenext/compatid3.py +++ b/picard/formats/mutagenext/compatid3.py @@ -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(): diff --git a/picard/script.py b/picard/script.py index 0f9a542ad..4578d0495 100644 --- a/picard/script.py +++ b/picard/script.py @@ -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 "" % (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") diff --git a/picard/util/__init__.py b/picard/util/__init__.py index 289031cc2..f007b23e8 100644 --- a/picard/util/__init__.py +++ b/picard/util/__init__.py @@ -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: diff --git a/test/test_mbxml.py b/test/test_mbxml.py index 4602af9e0..8b5095ead 100644 --- a/test/test_mbxml.py +++ b/test/test_mbxml.py @@ -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): diff --git a/test/test_utils.py b/test/test_utils.py index 786db8d23..131cf5c1b 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -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):