diff --git a/NEWS.txt b/NEWS.txt
index 57092f871..c4b61761d 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -2,6 +2,7 @@ Version 0.14 - IN DEVELOPMENT
* Fixed a problem with network operations hanging after a network error
(#5794, #5884)
* ID3v2.3 with UTF-16 is now the default ID3 version
+ * Option to set preferred release types for improved album matching
Version 0.13 - 2011-03-06
* Changed Picard icon license to CC by-sa
diff --git a/picard/file.py b/picard/file.py
index ac4a55587..f98dc4bb4 100644
--- a/picard/file.py
+++ b/picard/file.py
@@ -43,6 +43,7 @@ from picard.util import (
LockableObject,
pathcmp,
mimetype,
+ load_release_type_scores,
)
@@ -433,10 +434,11 @@ class File(LockableObject, Item):
Weigths:
* title = 13
- * artist name = 3
+ * artist name = 4
* release name = 5
* length = 10
- * number of tracks = 3
+ * number of tracks = 4
+ * album type = 20
"""
total = 0.0
@@ -467,7 +469,8 @@ class File(LockableObject, Item):
parts.append((score, 10))
total += 10
- track_list = track.release_list[0].release[0].track_list[0]
+ first_release = track.release_list[0].release[0]
+ track_list = first_release.track_list[0]
if 'totaltracks' in self.metadata and 'count' in track_list.attribs:
try:
a = int(self.metadata['totaltracks'])
@@ -483,6 +486,16 @@ class File(LockableObject, Item):
except ValueError:
pass
+ type_scores = load_release_type_scores(self.config.setting["release_type_scores"])
+ if 'type' in first_release.attribs:
+ release_type = first_release.type
+ score = type_scores.get(release_type, type_scores.get('Other', 0.5))
+ print release_type, score
+ else:
+ score = 0.0
+ parts.append((score, 20))
+ total += 20
+
return reduce(lambda x, y: x + y[0] * y[1] / total, parts, 0.0)
def _lookup_finished(self, lookuptype, document, http, error):
diff --git a/picard/ui/options/matching.py b/picard/ui/options/matching.py
index 673239a35..d5ef2ce3b 100644
--- a/picard/ui/options/matching.py
+++ b/picard/ui/options/matching.py
@@ -17,8 +17,9 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-from PyQt4 import QtGui
-from picard.config import FloatOption
+from PyQt4 import QtCore, QtGui
+from picard.config import FloatOption, TextOption
+from picard.util import load_release_type_scores, save_release_type_scores
from picard.ui.options import OptionsPage, OptionsCheckError, register_options_page
from picard.ui.ui_options_matching import Ui_MatchingOptionsPage
@@ -35,22 +36,48 @@ class MatchingOptionsPage(OptionsPage):
FloatOption("setting", "file_lookup_threshold", 0.7),
FloatOption("setting", "cluster_lookup_threshold", 0.8),
FloatOption("setting", "track_matching_threshold", 0.4),
+ TextOption("setting", "release_type_scores", "Album 0.2 Single 0.2 EP 0.2 Compilation 0.2 Soundtrack 0.2 Spokenword 0.2 Interview 0.2 Audiobook 0.2 Live 0.2 Remix 0.2 Other 0.5"),
]
+ _release_type_sliders = {}
+
def __init__(self, parent=None):
super(MatchingOptionsPage, self).__init__(parent)
self.ui = Ui_MatchingOptionsPage()
self.ui.setupUi(self)
+ self.connect(self.ui.reset_preferred_types_btn, QtCore.SIGNAL("clicked()"), self.reset_preferred_types)
+ self._release_type_sliders["Album"] = self.ui.prefer_album_score
+ self._release_type_sliders["Single"] = self.ui.prefer_single_score
+ self._release_type_sliders["EP"] = self.ui.prefer_ep_score
+ self._release_type_sliders["Compilation"] = self.ui.prefer_compilation_score
+ self._release_type_sliders["Soundtrack"] = self.ui.prefer_soundtrack_score
+ self._release_type_sliders["Spokenword"] = self.ui.prefer_spokenword_score
+ self._release_type_sliders["Interview"] = self.ui.prefer_interview_score
+ self._release_type_sliders["Audiobook"] = self.ui.prefer_audiobook_score
+ self._release_type_sliders["Live"] = self.ui.prefer_live_score
+ self._release_type_sliders["Remix"] = self.ui.prefer_remix_score
+ self._release_type_sliders["Other"] = self.ui.prefer_other_score
def load(self):
self.ui.file_lookup_threshold.setValue(int(self.config.setting["file_lookup_threshold"] * 100))
self.ui.cluster_lookup_threshold.setValue(int(self.config.setting["cluster_lookup_threshold"] * 100))
self.ui.track_matching_threshold.setValue(int(self.config.setting["track_matching_threshold"] * 100))
+ scores = load_release_type_scores(self.config.setting["release_type_scores"])
+ for (release_type, release_type_slider) in self._release_type_sliders.iteritems():
+ release_type_slider.setValue(int(scores.get(release_type, 0.5) * 100))
def save(self):
self.config.setting["file_lookup_threshold"] = float(self.ui.file_lookup_threshold.value()) / 100.0
self.config.setting["cluster_lookup_threshold"] = float(self.ui.cluster_lookup_threshold.value()) / 100.0
self.config.setting["track_matching_threshold"] = float(self.ui.track_matching_threshold.value()) / 100.0
+ scores = {}
+ for (release_type, release_type_slider) in self._release_type_sliders.iteritems():
+ scores[release_type] = float(release_type_slider.value()) / 100.0
+ self.config.setting["release_type_scores"] = save_release_type_scores(scores)
+
+ def reset_preferred_types(self):
+ for release_type_slider in self._release_type_sliders.values():
+ release_type_slider.setValue(50)
register_options_page(MatchingOptionsPage)
diff --git a/picard/ui/ui_options_matching.py b/picard/ui/ui_options_matching.py
index 3ce995b19..7601f999f 100644
--- a/picard/ui/ui_options_matching.py
+++ b/picard/ui/ui_options_matching.py
@@ -2,8 +2,8 @@
# Form implementation generated from reading ui file 'ui/options_matching.ui'
#
-# Created: Thu Sep 3 00:39:04 2009
-# by: PyQt4 UI code generator 4.4.4
+# Created: Thu May 12 23:52:13 2011
+# by: PyQt4 UI code generator 4.7.2
#
# WARNING! All changes made in this file will be lost!
@@ -12,7 +12,7 @@ from PyQt4 import QtCore, QtGui
class Ui_MatchingOptionsPage(object):
def setupUi(self, MatchingOptionsPage):
MatchingOptionsPage.setObjectName("MatchingOptionsPage")
- MatchingOptionsPage.resize(383, 313)
+ MatchingOptionsPage.resize(382, 498)
self.vboxlayout = QtGui.QVBoxLayout(MatchingOptionsPage)
self.vboxlayout.setSpacing(6)
self.vboxlayout.setMargin(9)
@@ -60,8 +60,125 @@ class Ui_MatchingOptionsPage(object):
self.label_5.setObjectName("label_5")
self.gridlayout.addWidget(self.label_5, 1, 0, 1, 1)
self.vboxlayout.addWidget(self.rename_files)
- spacerItem = QtGui.QSpacerItem(20, 41, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
- self.vboxlayout.addItem(spacerItem)
+ self.groupBox = QtGui.QGroupBox(MatchingOptionsPage)
+ self.groupBox.setObjectName("groupBox")
+ self.gridLayout = QtGui.QGridLayout(self.groupBox)
+ self.gridLayout.setObjectName("gridLayout")
+ self.prefer_album_score = QtGui.QSlider(self.groupBox)
+ self.prefer_album_score.setMaximum(100)
+ self.prefer_album_score.setProperty("value", 50)
+ self.prefer_album_score.setOrientation(QtCore.Qt.Horizontal)
+ self.prefer_album_score.setObjectName("prefer_album_score")
+ self.gridLayout.addWidget(self.prefer_album_score, 0, 2, 1, 1)
+ self.prefer_single_score = QtGui.QSlider(self.groupBox)
+ self.prefer_single_score.setMaximum(100)
+ self.prefer_single_score.setProperty("value", 50)
+ self.prefer_single_score.setOrientation(QtCore.Qt.Horizontal)
+ self.prefer_single_score.setObjectName("prefer_single_score")
+ self.gridLayout.addWidget(self.prefer_single_score, 1, 2, 1, 1)
+ self.label = QtGui.QLabel(self.groupBox)
+ self.label.setObjectName("label")
+ self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
+ self.label_2 = QtGui.QLabel(self.groupBox)
+ self.label_2.setObjectName("label_2")
+ self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1)
+ self.prefer_ep_score = QtGui.QSlider(self.groupBox)
+ self.prefer_ep_score.setMaximum(100)
+ self.prefer_ep_score.setProperty("value", 50)
+ self.prefer_ep_score.setOrientation(QtCore.Qt.Horizontal)
+ self.prefer_ep_score.setObjectName("prefer_ep_score")
+ self.gridLayout.addWidget(self.prefer_ep_score, 2, 2, 1, 1)
+ self.prefer_compilation_score = QtGui.QSlider(self.groupBox)
+ self.prefer_compilation_score.setMaximum(100)
+ self.prefer_compilation_score.setProperty("value", 50)
+ self.prefer_compilation_score.setOrientation(QtCore.Qt.Horizontal)
+ self.prefer_compilation_score.setObjectName("prefer_compilation_score")
+ self.gridLayout.addWidget(self.prefer_compilation_score, 3, 2, 1, 1)
+ self.prefer_soundtrack_score = QtGui.QSlider(self.groupBox)
+ self.prefer_soundtrack_score.setMaximum(100)
+ self.prefer_soundtrack_score.setProperty("value", 50)
+ self.prefer_soundtrack_score.setOrientation(QtCore.Qt.Horizontal)
+ self.prefer_soundtrack_score.setObjectName("prefer_soundtrack_score")
+ self.gridLayout.addWidget(self.prefer_soundtrack_score, 4, 2, 1, 1)
+ self.prefer_spokenword_score = QtGui.QSlider(self.groupBox)
+ self.prefer_spokenword_score.setMaximum(100)
+ self.prefer_spokenword_score.setProperty("value", 50)
+ self.prefer_spokenword_score.setOrientation(QtCore.Qt.Horizontal)
+ self.prefer_spokenword_score.setObjectName("prefer_spokenword_score")
+ self.gridLayout.addWidget(self.prefer_spokenword_score, 5, 2, 1, 1)
+ self.label_3 = QtGui.QLabel(self.groupBox)
+ self.label_3.setObjectName("label_3")
+ self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1)
+ self.label_7 = QtGui.QLabel(self.groupBox)
+ self.label_7.setObjectName("label_7")
+ self.gridLayout.addWidget(self.label_7, 3, 0, 1, 1)
+ self.label_8 = QtGui.QLabel(self.groupBox)
+ self.label_8.setObjectName("label_8")
+ self.gridLayout.addWidget(self.label_8, 4, 0, 1, 1)
+ self.label_9 = QtGui.QLabel(self.groupBox)
+ self.label_9.setObjectName("label_9")
+ self.gridLayout.addWidget(self.label_9, 5, 0, 1, 1)
+ self.prefer_interview_score = QtGui.QSlider(self.groupBox)
+ self.prefer_interview_score.setMaximum(100)
+ self.prefer_interview_score.setProperty("value", 50)
+ self.prefer_interview_score.setOrientation(QtCore.Qt.Horizontal)
+ self.prefer_interview_score.setObjectName("prefer_interview_score")
+ self.gridLayout.addWidget(self.prefer_interview_score, 6, 2, 1, 1)
+ self.prefer_audiobook_score = QtGui.QSlider(self.groupBox)
+ self.prefer_audiobook_score.setMaximum(100)
+ self.prefer_audiobook_score.setProperty("value", 50)
+ self.prefer_audiobook_score.setOrientation(QtCore.Qt.Horizontal)
+ self.prefer_audiobook_score.setObjectName("prefer_audiobook_score")
+ self.gridLayout.addWidget(self.prefer_audiobook_score, 7, 2, 1, 1)
+ self.prefer_live_score = QtGui.QSlider(self.groupBox)
+ self.prefer_live_score.setMaximum(100)
+ self.prefer_live_score.setProperty("value", 50)
+ self.prefer_live_score.setOrientation(QtCore.Qt.Horizontal)
+ self.prefer_live_score.setObjectName("prefer_live_score")
+ self.gridLayout.addWidget(self.prefer_live_score, 8, 2, 1, 1)
+ self.label_10 = QtGui.QLabel(self.groupBox)
+ self.label_10.setObjectName("label_10")
+ self.gridLayout.addWidget(self.label_10, 6, 0, 1, 1)
+ self.label_11 = QtGui.QLabel(self.groupBox)
+ self.label_11.setObjectName("label_11")
+ self.gridLayout.addWidget(self.label_11, 7, 0, 1, 1)
+ self.label_12 = QtGui.QLabel(self.groupBox)
+ self.label_12.setObjectName("label_12")
+ self.gridLayout.addWidget(self.label_12, 8, 0, 1, 1)
+ self.prefer_remix_score = QtGui.QSlider(self.groupBox)
+ self.prefer_remix_score.setMaximum(100)
+ self.prefer_remix_score.setProperty("value", 50)
+ self.prefer_remix_score.setOrientation(QtCore.Qt.Horizontal)
+ self.prefer_remix_score.setObjectName("prefer_remix_score")
+ self.gridLayout.addWidget(self.prefer_remix_score, 9, 2, 1, 1)
+ self.label_13 = QtGui.QLabel(self.groupBox)
+ self.label_13.setObjectName("label_13")
+ self.gridLayout.addWidget(self.label_13, 9, 0, 1, 1)
+ self.prefer_other_score = QtGui.QSlider(self.groupBox)
+ self.prefer_other_score.setMaximum(100)
+ self.prefer_other_score.setSliderPosition(50)
+ self.prefer_other_score.setOrientation(QtCore.Qt.Horizontal)
+ self.prefer_other_score.setObjectName("prefer_other_score")
+ self.gridLayout.addWidget(self.prefer_other_score, 10, 2, 1, 1)
+ self.label_14 = QtGui.QLabel(self.groupBox)
+ self.label_14.setObjectName("label_14")
+ self.gridLayout.addWidget(self.label_14, 10, 0, 1, 1)
+ self.horizontalLayout = QtGui.QHBoxLayout()
+ self.horizontalLayout.setObjectName("horizontalLayout")
+ spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
+ self.horizontalLayout.addItem(spacerItem)
+ self.reset_preferred_types_btn = QtGui.QPushButton(self.groupBox)
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.reset_preferred_types_btn.sizePolicy().hasHeightForWidth())
+ self.reset_preferred_types_btn.setSizePolicy(sizePolicy)
+ self.reset_preferred_types_btn.setObjectName("reset_preferred_types_btn")
+ self.horizontalLayout.addWidget(self.reset_preferred_types_btn)
+ self.gridLayout.addLayout(self.horizontalLayout, 12, 2, 1, 1)
+ self.vboxlayout.addWidget(self.groupBox)
+ spacerItem1 = QtGui.QSpacerItem(20, 41, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
+ self.vboxlayout.addItem(spacerItem1)
self.label_6.setBuddy(self.file_lookup_threshold)
self.label_4.setBuddy(self.file_lookup_threshold)
self.label_5.setBuddy(self.file_lookup_threshold)
@@ -79,4 +196,17 @@ class Ui_MatchingOptionsPage(object):
self.file_lookup_threshold.setSuffix(_(" %"))
self.label_4.setText(_("Minimal similarity for file lookups:"))
self.label_5.setText(_("Minimal similarity for cluster lookups:"))
+ self.groupBox.setTitle(_("Preferred release types"))
+ self.label.setText(_("Album"))
+ self.label_2.setText(_("Single"))
+ self.label_3.setText(_("EP"))
+ self.label_7.setText(_("Compilation"))
+ self.label_8.setText(_("Soundtrack"))
+ self.label_9.setText(_("Spokenword"))
+ self.label_10.setText(_("Interview"))
+ self.label_11.setText(_("Audiobook"))
+ self.label_12.setText(_("Live"))
+ self.label_13.setText(_("Remix"))
+ self.label_14.setText(_("Other"))
+ self.reset_preferred_types_btn.setText(_("Reset all"))
diff --git a/picard/util/__init__.py b/picard/util/__init__.py
index 9169688cb..a5bbfef1f 100644
--- a/picard/util/__init__.py
+++ b/picard/util/__init__.py
@@ -352,3 +352,15 @@ def mbid_validate(string):
def rot13(input):
return u''.join(unichr(rot_13.encoding_map.get(ord(c), ord(c))) for c in input)
+
+
+def load_release_type_scores(setting):
+ scores = {}
+ values = setting.split()
+ for i in range(0, len(values), 2):
+ scores[values[i]] = float(values[i+1]) if i+1 < len(values) else 0.0
+ return scores
+
+
+def save_release_type_scores(scores):
+ return " ".join(["%s %.2f" % v for v in scores.iteritems()])
diff --git a/test/test_utils.py b/test/test_utils.py
index 55667b242..786db8d23 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -99,7 +99,7 @@ class TranslateArtistTest(unittest.TestCase):
self.failIfEqual(u"Hamasaki, Ayumi & Keiko", util.translate_artist(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.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"))
@@ -112,3 +112,33 @@ class FormatTimeTest(unittest.TestCase):
self.failUnlessEqual("3:00", util.format_time(179500))
self.failUnlessEqual("2:59", util.format_time(179499))
+
+class LoadReleaseTypeScoresTest(unittest.TestCase):
+
+ def test_valid(self):
+ release_type_score_config = "Album 1.0 Single 0.5 EP 0.5 Compilation 0.5 Soundtrack 0.5 Spokenword 0.5 Interview 0.2 Audiobook 0.0 Live 0.5 Remix 0.4 Other 0.0"
+ release_type_scores = util.load_release_type_scores(release_type_score_config)
+ self.assertEqual(1.0, release_type_scores["Album"])
+ self.assertEqual(0.5, release_type_scores["Single"])
+ self.assertEqual(0.2, release_type_scores["Interview"])
+ self.assertEqual(0.0, release_type_scores["Audiobook"])
+ self.assertEqual(0.4, release_type_scores["Remix"])
+
+ def test_invalid(self):
+ release_type_score_config = "Album 1.0 Other"
+ release_type_scores = util.load_release_type_scores(release_type_score_config)
+ self.assertEqual(1.0, release_type_scores["Album"])
+ self.assertEqual(0.0, release_type_scores["Other"])
+
+
+class SaveReleaseTypeScoresTest(unittest.TestCase):
+
+ def test(self):
+ expected = "Album 1.00 Single 0.50 Other 0.00"
+ scores = {"Album": 1.0, "Single": 0.5, "Other": 0.0}
+ saved_scores = util.save_release_type_scores(scores)
+ self.assertTrue("Album 1.00" in saved_scores)
+ self.assertTrue("Single 0.50" in saved_scores)
+ self.assertTrue("Other 0.00" in saved_scores)
+ self.assertEqual(6, len(saved_scores.split()))
+
diff --git a/ui/options_matching.ui b/ui/options_matching.ui
index 695c8cd91..92cbf35b2 100644
--- a/ui/options_matching.ui
+++ b/ui/options_matching.ui
@@ -6,8 +6,8 @@
0
0
- 383
- 313
+ 382
+ 498
@@ -110,6 +110,265 @@
+ -
+
+
+ Preferred release types
+
+
+
-
+
+
+ 100
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ 100
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Album
+
+
+
+ -
+
+
+ Single
+
+
+
+ -
+
+
+ 100
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ 100
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ 100
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ 100
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ EP
+
+
+
+ -
+
+
+ Compilation
+
+
+
+ -
+
+
+ Soundtrack
+
+
+
+ -
+
+
+ Spokenword
+
+
+
+ -
+
+
+ 100
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ 100
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ 100
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Interview
+
+
+
+ -
+
+
+ Audiobook
+
+
+
+ -
+
+
+ Live
+
+
+
+ -
+
+
+ 100
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Remix
+
+
+
+ -
+
+
+ 100
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Other
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Reset all
+
+
+
+
+
+
+
+
-