mirror of
https://github.com/fergalmoran/picard.git
synced 2026-02-25 09:03:59 +00:00
Merge branch 'master' of ssh://git.musicbrainz.org:10015/picard into tagdiff
This commit is contained in:
4
NEWS.txt
4
NEWS.txt
@@ -1,3 +1,7 @@
|
||||
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
|
||||
* Added AcoustID support.
|
||||
* Fixed track metadata plugins.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from picard.metadata import Metadata
|
||||
from picard.file import File
|
||||
from picard.formats.mutagenext import compatid3
|
||||
from picard.util import encode_filename, sanitize_date
|
||||
from urlparse import urlparse
|
||||
|
||||
|
||||
# Ugly, but... I need to save the text in ISO-8859-1 even if it contains
|
||||
@@ -57,6 +58,10 @@ id3.MultiSpec._write_orig = id3.MultiSpec.write
|
||||
id3.MultiSpec.write = patched_MultiSpec_write
|
||||
|
||||
|
||||
id3.TCMP = compatid3.TCMP
|
||||
id3.TSO2 = compatid3.TSO2
|
||||
|
||||
|
||||
class ID3File(File):
|
||||
"""Generic ID3-based file."""
|
||||
_File = None
|
||||
@@ -64,7 +69,7 @@ class ID3File(File):
|
||||
|
||||
__upgrade = {
|
||||
'XSOP': 'TSOP',
|
||||
'XDOR': 'TDRC',
|
||||
'TXXX:ALBUMARTISTSORT': 'TSO2',
|
||||
}
|
||||
|
||||
__translate = {
|
||||
@@ -88,10 +93,12 @@ class ID3File(File):
|
||||
'TMED': 'media',
|
||||
'TBPM': 'bpm',
|
||||
'WOAR': 'website',
|
||||
'WCOP': 'license',
|
||||
'TSRC': 'isrc',
|
||||
'TENC': 'encodedby',
|
||||
'TCOP': 'copyright',
|
||||
'TSOA': 'albumsort',
|
||||
'TSO2': 'albumartistsort',
|
||||
'TSOP': 'artistsort',
|
||||
'TSOT': 'titlesort',
|
||||
'TPUB': 'label',
|
||||
@@ -114,7 +121,7 @@ class ID3File(File):
|
||||
'Acoustid Fingerprint': 'acoustid_fingerprint',
|
||||
'Acoustid Id': 'acoustid_id',
|
||||
'SCRIPT': 'script',
|
||||
'ALBUMARTISTSORT': 'albumartistsort',
|
||||
'LICENSE': 'license',
|
||||
'CATALOGNUMBER': 'catalognumber',
|
||||
'BARCODE': 'barcode',
|
||||
'ASIN': 'asin',
|
||||
@@ -173,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)
|
||||
@@ -243,7 +250,6 @@ class ID3File(File):
|
||||
tmcl = mutagen.id3.TMCL(encoding=encoding, people=[])
|
||||
tipl = mutagen.id3.TIPL(encoding=encoding, people=[])
|
||||
|
||||
id3.TCMP = compatid3.TCMP
|
||||
tags.delall('TCMP')
|
||||
for name, values in metadata.rawitems():
|
||||
if name.startswith('performer:'):
|
||||
@@ -284,9 +290,19 @@ class ID3File(File):
|
||||
elif name in self.__rtranslate:
|
||||
frameid = self.__rtranslate[name]
|
||||
if frameid.startswith('W'):
|
||||
tags.add(getattr(id3, frameid)(url=values[0]))
|
||||
# Only add WCOP if there is only one license URL, otherwise use TXXX:LICENSE
|
||||
if frameid == 'WCOP' and len(values) == 1 and all(urlparse(values[0])[:2]):
|
||||
tags.add(getattr(id3, frameid)(url=values[0]))
|
||||
else:
|
||||
tags.add(id3.TXXX(encoding=encoding, desc=self.__rtranslate_freetext[name], text=values))
|
||||
elif frameid.startswith('T'):
|
||||
tags.add(getattr(id3, frameid)(encoding=encoding, text=values))
|
||||
if frameid == 'TSOA':
|
||||
tags.delall('XSOA')
|
||||
elif frameid == 'TSOP':
|
||||
tags.delall('XSOP')
|
||||
elif frameid == 'TSO2':
|
||||
tags.delall('TXXX:ALBUMARTISTSORT')
|
||||
elif name in self.__rtranslate_freetext:
|
||||
tags.add(id3.TXXX(encoding=encoding, desc=self.__rtranslate_freetext[name], text=values))
|
||||
elif name.startswith('~id3:'):
|
||||
@@ -307,9 +323,6 @@ class ID3File(File):
|
||||
tags.update_to_v23()
|
||||
tags.save(encode_filename(filename), v2=3, v1=v1)
|
||||
else:
|
||||
# remove all custom 2.3 frames
|
||||
for old in self.__upgrade.keys():
|
||||
tags.delall(old)
|
||||
tags.update_to_v24()
|
||||
tags.save(encode_filename(filename), v2=4, v1=v1)
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ class MP4File(File):
|
||||
"----:com.apple.iTunes:ISRC": "isrc",
|
||||
"----:com.apple.iTunes:MEDIA": "media",
|
||||
"----:com.apple.iTunes:LABEL": "label",
|
||||
"----:com.apple.iTunes:LICENSE": "license",
|
||||
"----:com.apple.iTunes:CATALOGNUMBER": "catalognumber",
|
||||
"----:com.apple.iTunes:SUBTITLE": "subtitle",
|
||||
"----:com.apple.iTunes:DISCSUBTITLE": "discsubtitle",
|
||||
|
||||
@@ -28,6 +28,9 @@ from mutagen.id3 import ID3, Frame, Frames, Frames_2_2, TextFrame, TORY, \
|
||||
class TCMP(TextFrame):
|
||||
pass
|
||||
|
||||
class TSO2(TextFrame):
|
||||
pass
|
||||
|
||||
class XDOR(TextFrame):
|
||||
pass
|
||||
|
||||
@@ -49,6 +52,7 @@ class CompatID3(ID3):
|
||||
known_frames = dict(Frames)
|
||||
known_frames.update(dict(Frames_2_2))
|
||||
known_frames["TCMP"] = TCMP
|
||||
known_frames["TSO2"] = TSO2
|
||||
known_frames["XDOR"] = XDOR
|
||||
known_frames["XSOP"] = XSOP
|
||||
kwargs["known_frames"] = known_frames
|
||||
@@ -212,13 +216,12 @@ class CompatID3(ID3):
|
||||
# ID3v2.2 LNK frames are just way too different to upgrade.
|
||||
self.delall("LINK")
|
||||
|
||||
if "TSOP" in self:
|
||||
f = self.pop("TSOP")
|
||||
self.add(XSOP(encoding=f.encoding, text=f.text))
|
||||
# leave TSOP, TSOA and TSOT even though they are officially defined
|
||||
# only in ID3v2.4, because most applications use them also in ID3v2.3
|
||||
|
||||
# New frames added in v2.4.
|
||||
for key in ["ASPI", "EQU2", "RVA2", "SEEK", "SIGN", "TDRL", "TDTG",
|
||||
"TMOO", "TPRO", "TSOA", "TSOT", "TSST"]:
|
||||
"TMOO", "TPRO"]:
|
||||
if key in self: del(self[key])
|
||||
|
||||
for frame in self.values():
|
||||
|
||||
@@ -27,6 +27,7 @@ AMAZON_ASIN_URL_REGEX = re.compile(r'^http://(?:www.)?(.*?)(?:\:[0-9]+)?/.*/([0-
|
||||
|
||||
_artist_rel_types = {
|
||||
"composer": "composer",
|
||||
"writer": "writer",
|
||||
"conductor": "conductor",
|
||||
"chorus master": "conductor",
|
||||
"performing orchestra": "performer:orchestra",
|
||||
@@ -109,6 +110,9 @@ def _relations_to_metadata(relation_lists, m, config):
|
||||
match = AMAZON_ASIN_URL_REGEX.match(url)
|
||||
if match is not None and 'asin' not in m:
|
||||
m['asin'] = match.group(2)
|
||||
if relation.type == 'license':
|
||||
url = relation.target[0].text
|
||||
m.add('license', url)
|
||||
|
||||
|
||||
def _translate_artist_node(node, config=None):
|
||||
|
||||
@@ -21,8 +21,9 @@
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
from picard.plugin import ExtensionPoint
|
||||
from inspect import getargspec
|
||||
|
||||
class ScriptError(Exception): pass
|
||||
class ParseError(ScriptError): pass
|
||||
@@ -53,7 +54,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
|
||||
|
||||
@@ -61,15 +78,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):
|
||||
@@ -148,7 +162,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):
|
||||
@@ -212,8 +226,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."""
|
||||
@@ -237,22 +251,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."""
|
||||
@@ -510,9 +534,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")
|
||||
|
||||
@@ -87,6 +87,8 @@ class RenamingOptionsPage(OptionsPage):
|
||||
parser = ScriptParser()
|
||||
parser.eval(script, file.metadata)
|
||||
filename = file._make_filename(file.filename, file.metadata, settings)
|
||||
if not settings["move_files"]:
|
||||
return os.path.basename(filename)
|
||||
return filename
|
||||
except SyntaxError, e: return ""
|
||||
except TypeError, e: return ""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -39,7 +39,9 @@ TAG_NAMES = {
|
||||
'mood': N_('Mood'),
|
||||
'bpm': N_('BPM'),
|
||||
'copyright': N_('Copyright'),
|
||||
'license': N_('License'),
|
||||
'composer': N_('Composer'),
|
||||
'writer': N_('Writer'),
|
||||
'conductor': N_('Conductor'),
|
||||
'lyricist': N_('Lyricist'),
|
||||
'arranger': N_('Arranger'),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user