- Nuke all traces of "release events" in the code. The release context menu now has an "Other versions" submenu where you can switch to a different release from the release group. It queries the RG in the background to construct this list.

- Fix cluster lookups.
- Only display the disc numbers for a release if there's more than one medium.
- preferred_release_country support needs to be fixed.
This commit is contained in:
Michael Wiencek
2011-05-23 00:57:30 -05:00
parent 50e2c1de73
commit fa9d455e70
7 changed files with 102 additions and 201 deletions

View File

@@ -41,86 +41,19 @@ _TRANSLATE_TAGS = {
}
class ReleaseEvent(object):
ATTRS = ['date', 'releasecountry', 'label', 'barcode', 'catalognumber', 'media']
def __init__(self):
for attr in self.ATTRS:
setattr(self, attr, None)
def to_metadata(self, m):
for attr in self.ATTRS:
val = getattr(self, attr)
if val is not None:
m[attr] = val.strip()
else:
try: del m[attr]
except KeyError: pass
def from_metadata(self, m):
for attr in self.ATTRS:
setattr(self, attr, m[attr])
def copy(self):
new_event = ReleaseEvent()
for attr in self.ATTRS:
setattr(new_event, attr, getattr(self, attr))
return new_event
def similarity(self, m):
sim = 0.0
if not m:
return sim
for attr in self.ATTRS:
val = getattr(self, attr)
mval = getattr(m, attr)
if val and mval:
if attr == 'date':
dsim = 0.0
sdate = val.split('-')
mdate = mval.split('-')
for i in range(min(len(sdate),len(mdate))):
if sdate[i] == mdate[i]:
dsim+=1.0
else:
break
dsim/=max(len(mdate),len(sdate))
sim+=dsim
else:
if mval == val:
sim+=1.0
sim/=len(self.ATTRS)
return sim
def __cmp__(self, other):
if other == None:
return -1
elif self.date == other.date:
return cmp([self.releasecountry, self.label, self.catalognumber, self.media, self.barcode],
[other.releasecountry, other.label, other.catalognumber, other.media, other.barcode])
elif self.date == None:
return 1
elif other.date == None:
return -1
else:
return cmp(self.date, other.date)
class Album(DataObject, Item):
def __init__(self, id, catalognumber=None, discid=None):
def __init__(self, id, discid=None):
DataObject.__init__(self, id)
self.metadata = Metadata()
self.tracks = []
self.loaded = False
self.rgloaded = False
self._files = 0
self._requests = 0
self._catalognumber = catalognumber
self._discid = discid
self._after_load_callbacks = queue.Queue()
self.current_release_event = None
self.release_events = []
self.other_versions = []
self.unmatched_files = Cluster(_("Unmatched Files"), special=True, related_album=self, hide_if_empty=True)
def __repr__(self):
@@ -182,30 +115,14 @@ class Album(DataObject, Item):
# Get release metadata
m = self._new_metadata
m.length = 0
self.release_events = []
release_to_metadata(release_node, m, config=self.config, album=self)
self.release_events.sort()
# Add empty release event
self.add_release_event()
if self._discid:
m['musicbrainz_discid'] = self._discid
self.current_release_event = None
for rel in self.release_events:
if self._catalognumber and rel.catalognumber == self._catalognumber:
self.current_release_event = rel
break
else:
if self.release_events:
preferred_events = [rel for rel in self.release_events
if rel.releasecountry == self.config.setting["preferred_release_country"]]
if preferred_events:
self.current_release_event = preferred_events[0]
else:
self.current_release_event = self.release_events[0]
if self.current_release_event:
self.current_release_event.to_metadata(m)
if not self.rgloaded:
releasegroupid = release_node.release_group[0].id
self.tagger.xmlws.get_release_group_by_id(releasegroupid, self._release_group_request_finished)
# 'Translate' artist name
if self.config.setting['translate_artist_names']:
@@ -299,6 +216,26 @@ class Album(DataObject, Item):
return True
def _parse_release_group(self, document):
releases = document.metadata[0].release_group[0].release_list[0].release
for release in releases:
version = {}
version["mbid"] = release.id
if "date" in release.children:
version["date"] = release.date[0].text
if "country" in release.children:
version["country"] = release.country[0].text
formats = {}
for medium in release.medium_list[0].medium:
if "format" in medium.children:
f = medium.format[0].text
if f in formats: formats[f] += 1
else: formats[f] = 1
if formats:
version["media"] = " + ".join(["%s%s" % (str(j)+"x" if j>1 else "", i)
for i, j in formats.items()])
self.other_versions.append(version)
def _release_request_finished(self, document, http, error):
parsed = False
try:
@@ -315,6 +252,19 @@ class Album(DataObject, Item):
if parsed:
self._finalize_loading(error)
def _release_group_request_finished(self, document, http, error):
try:
if error:
self.log.error("%r", unicode(http.errorString()))
else:
try:
self._parse_release_group(document)
except:
error = True
self.log.error(traceback.format_exc())
finally:
self.rgloaded = True
def _finalize_loading(self, error):
if error:
self.metadata.clear()
@@ -324,11 +274,8 @@ class Album(DataObject, Item):
self.update()
else:
if not self._requests:
for old_track, new_track in zip(self.tracks, self._new_tracks):
for file in old_track.linked_files:
file.move(new_track)
for track in self.tracks[len(self._new_tracks):]:
for file in track.linked_files:
for track in self.tracks:
for file in list(track.linked_files):
file.move(self.unmatched_files)
self.metadata = self._new_metadata
self.tracks = self._new_tracks
@@ -336,11 +283,6 @@ class Album(DataObject, Item):
del self._new_tracks
self.loaded = True
self.match_files(self.unmatched_files.files)
for track in self.tracks:
for file in track.linked_files:
if file.orig_metadata:
self.match_release_event(file.orig_metadata)
break
self.update()
self.tagger.window.set_statusbar_message('Album %s loaded', self.id, timeout=3000)
while self._after_load_callbacks.qsize() > 0:
@@ -360,7 +302,7 @@ class Album(DataObject, Item):
self._new_tracks = []
self._requests = 1
require_authentication = False
inc = ['recordings', 'puids', 'artist-credits', 'labels', 'isrcs']
inc = ['release-groups', 'recordings', 'puids', 'artist-credits', 'labels', 'isrcs']
if self.config.setting['release_ars'] or self.config.setting['track_ars']:
inc += ['artist-rels', 'release-rels', 'url-rels', 'recording-rels']
if self.config.setting['track_ars']:
@@ -507,50 +449,6 @@ class Album(DataObject, Item):
else:
return ''
def set_current_release_event(self, rel):
self.current_release_event = rel
if self.current_release_event:
self.current_release_event.to_metadata(self.metadata)
self.update(update_tracks=False)
for track in self.tracks:
self.current_release_event.to_metadata(track.metadata)
for file in track.linked_files:
self.current_release_event.to_metadata(file.metadata)
file.update()
if len(track.linked_files) <> 1:
track.update()
def add_release_event(self, date=None, releasecountry=None, label=None, barcode=None, catalognumber=None, media=None):
rel = ReleaseEvent()
rel.date = date
rel.releasecountry = releasecountry
rel.label = label
rel.barcode = barcode
rel.catalognumber = catalognumber
rel.media = media
self.release_events.append(rel)
return rel
def match_release_event(self, obj):
rel = ReleaseEvent()
if isinstance(obj, ReleaseEvent):
rel = obj.copy()
elif isinstance(obj, Metadata):
rel.from_metadata(obj)
elif isinstance(obj, File):
if obj.metadata:
rel.from_metadata(obj.metadata)
else:
self.log.error("Unsupported type given")
return
if rel.releasecountry is None:
rel.releasecountry = self.config.setting["preferred_release_country"]
matches = []
if self.release_events:
for rrel in self.release_events:
sim = rrel.similarity(rel)
matches.append((sim, rrel))
matches.sort(reverse=True)
if matches[0] and matches[0][0] > 0: self.set_current_release_event(matches[0][1])
def switch_release_version(self, version):
self.id = version["mbid"]
self.load()

View File

@@ -51,7 +51,7 @@ class BrowserIntegration(QtNetwork.QTcpServer):
args = [a.split("=", 1) for a in args.split("&")]
args = dict((a, unicode(QtCore.QUrl.fromPercentEncoding(b))) for (a, b) in args)
if action == "/openalbum":
self.tagger.load_album(args["id"], catalognumber=args.get("catno"))
self.tagger.load_album(args["id"])
else:
self.log.error("Unknown browser integration request: %r", action)

View File

@@ -25,6 +25,7 @@ from picard.metadata import Metadata
from picard.similarity import similarity2, similarity
from picard.ui.item import Item
from picard.util import format_time
from picard.mbxml import artist_credit_from_node
class Cluster(QtCore.QObject, Item):
@@ -132,7 +133,6 @@ class Cluster(QtCore.QObject, Item):
* number of tracks = 5
TODO:
* use release events
* prioritize official albums over compilations (optional?)
"""
total = 0.0
@@ -142,11 +142,11 @@ class Cluster(QtCore.QObject, Item):
total += similarity2(a, b) * self.comparison_weights['title']
a = self.metadata['artist']
b = release.artist[0].name[0].text
b = artist_credit_from_node(release.artist_credit[0])[0]
total += similarity2(a, b) * self.comparison_weights['artist']
a = len(self.files)
b = int(release.track_list[0].count)
b = int(release.medium_list[0].track_count[0].text)
if a > b:
score = 0.0
elif a < b:
@@ -211,7 +211,7 @@ class Cluster(QtCore.QObject, Item):
artist_cluster = artist_cluster_engine.cluster(threshold)
album_cluster_engine = ClusterEngine(albumDict)
album_cluster = album_cluster_engine.cluster(threshold)
album_cluster = album_cluster_engine.cluster(threshold)
# Arrange tracks into albums
albums = {}
@@ -284,7 +284,7 @@ class ClusterList(list, Item):
class ClusterDict(object):
def __init__(self):
# word -> id index
self.words = {}
@@ -306,12 +306,12 @@ class ClusterDict(object):
does exist, increment the count. Return the index of the word
in the dictionary or -1 is the word is empty.
"""
if word == u'':
if word == u'':
return -1
token = self.tokenize(word)
if token == u'':
if token == u'':
return -1
try:
@@ -349,7 +349,7 @@ class ClusterDict(object):
index, count = self.words[word]
except KeyError:
pass
return word, count
return word, count
class ClusterEngine(object):
@@ -368,7 +368,7 @@ class ClusterEngine(object):
return self.idClusterIndex.get(id)
def printCluster(self, cluster):
if cluster < 0:
if cluster < 0:
print "[no such cluster]"
return
@@ -377,10 +377,10 @@ class ClusterEngine(object):
def getClusterTitle(self, cluster):
if cluster < 0:
if cluster < 0:
return ""
max = 0
max = 0
maxWord = u''
for id in self.clusterBins[cluster]:
word, count = self.clusterDict.getWordAndCount(id)
@@ -398,10 +398,10 @@ class ClusterEngine(object):
for y in xrange(self.clusterDict.getSize()):
for x in xrange(y):
if x != y:
c = similarity(self.clusterDict.getToken(x).lower(),
c = similarity(self.clusterDict.getToken(x).lower(),
self.clusterDict.getToken(y).lower())
#print "'%s' - '%s' = %f" % (
# self.clusterDict.getToken(x).encode('utf-8', 'replace').lower(),
# self.clusterDict.getToken(x).encode('utf-8', 'replace').lower(),
# self.clusterDict.getToken(y).encode('utf-8', 'replace').lower(), c)
if c >= threshold:
@@ -421,12 +421,12 @@ class ClusterEngine(object):
c, pair = heappop(heap)
c = 1.0 - c
try:
try:
match0 = self.idClusterIndex[pair[0]]
except:
match0 = -1
try:
try:
match1 = self.idClusterIndex[pair[1]]
except:
match1 = -1
@@ -443,15 +443,15 @@ class ClusterEngine(object):
# If cluster0 is in a bin, stick the other match into that bin
if match0 >= 0 and match1 < 0:
self.clusterBins[match0].append(pair[1])
self.clusterBins[match0].append(pair[1])
self.idClusterIndex[pair[1]] = match0
#print "add '%s' to cluster " % (self.clusterDict.getWord(pair[0])),
#print "add '%s' to cluster " % (self.clusterDict.getWord(pair[0])),
#self.printCluster(match0)
continue
# If cluster1 is in a bin, stick the other match into that bin
if match1 >= 0 and match0 < 0:
self.clusterBins[match1].append(pair[0])
self.clusterBins[match1].append(pair[0])
self.idClusterIndex[pair[0]] = match1
#print "add '%s' to cluster " % (self.clusterDict.getWord(pair[1])),
#self.printCluster(match0)
@@ -466,7 +466,7 @@ class ClusterEngine(object):
#self.printCluster(match0)
del self.clusterBins[match1]
return self.clusterBins
return self.clusterBins
def can_refresh(self):
return False

View File

@@ -427,13 +427,13 @@ class Tagger(QtGui.QApplication):
for file in files:
file.save(self._file_saved, self.tagger.config.setting)
def load_album(self, id, catalognumber=None, discid=None):
def load_album(self, id, discid=None):
if id in self.albumids:
id = self.albumids[id]
album = self.get_album_by_id(id)
if album:
return album
album = Album(id, catalognumber=catalognumber, discid=discid)
album = Album(id, discid=discid)
self.albums.append(album)
self.emit(QtCore.SIGNAL("album_added"), album)
album.load()

View File

@@ -106,7 +106,8 @@ class Track(DataObject):
else:
similarity = 1
if column == 'title':
return u"%s-%s %s" % (self.metadata['discnumber'], self.metadata['tracknumber'].zfill(2), self.metadata['title']), similarity
prefix = "%s-" % self.metadata['discnumber'] if self.metadata['totaldiscs'] != "1" else ""
return u"%s%s %s" % (prefix, self.metadata['tracknumber'].zfill(2), self.metadata['title']), similarity
elif column == '~length':
return format_time(self.metadata.length), similarity
else:

View File

@@ -175,7 +175,8 @@ class MainPanel(QtGui.QSplitter):
if oldobj != obj:
self._object_to_item[obj] = item
self._item_to_object[item] = obj
del self._object_to_item[oldobj]
if oldobj in self._object_to_item:
del self._object_to_item[oldobj]
def unregister_object(self, obj=None, item=None):
if obj is None and item is not None:
@@ -319,9 +320,9 @@ class BaseTreeView(QtGui.QTreeWidget):
self.connect(self, QtCore.SIGNAL("doubleClicked(QModelIndex)"), self.activate_item)
def set_current_release_event(self, album, checked):
def switch_release_version(self, album):
index = self.sender().data().toInt()[0]
album.set_current_release_event(album.release_events[index])
album.switch_release_version(album.other_versions[index])
def contextMenuEvent(self, event):
item = self.itemAt(event.pos())
@@ -355,30 +356,27 @@ class BaseTreeView(QtGui.QTreeWidget):
menu.addAction(self.window.remove_action)
if isinstance(obj, Album):
releases_menu = QtGui.QMenu(_("&Releases"), menu)
#releases_menu.addActions(list(plugin_actions))
self._set_current_release_event = partial(self.set_current_release_event, obj)
for i, rel in enumerate(obj.release_events):
releases_menu = QtGui.QMenu(_("&Other versions"), menu)
self._switch_release_version = partial(self.switch_release_version, obj)
for i, version in enumerate(obj.other_versions):
if obj.id == version["mbid"]:
continue
name = []
if rel.date:
name.append(rel.date)
if rel.releasecountry:
try: name.append(RELEASE_COUNTRIES[rel.releasecountry])
except KeyError: name.append(rel.releasecountry)
if rel.label:
name.append(rel.label)
if rel.catalognumber:
name.append(rel.catalognumber)
if rel.media:
try: name.append(RELEASE_FORMATS[rel.media])
except KeyError: name.append(rel.media)
event_name = " / ".join(name).replace('&', '&&')
action = releases_menu.addAction(event_name or _('No release event'))
if "date" in version:
name.append(version["date"])
if "country" in version:
try: name.append(RELEASE_COUNTRIES[version["country"]])
except KeyError: name.append(version["country"])
if "media" in version:
name.append(version["media"])
version_name = " / ".join(name).replace('&', '&&')
action = releases_menu.addAction(version_name or _('[no release info]'))
action.setData(QtCore.QVariant(i))
action.setCheckable(True)
self.connect(action, QtCore.SIGNAL("triggered(bool)"), self._set_current_release_event)
if obj.current_release_event == rel:
action.setChecked(True)
self.connect(action, QtCore.SIGNAL("triggered(bool)"), self._switch_release_version)
if releases_menu.isEmpty():
text = _('No other versions') if obj.rgloaded else _('Loading...')
action = releases_menu.addAction(text)
action.setEnabled(False)
menu.addSeparator()
menu.addMenu(releases_menu)
@@ -640,7 +638,7 @@ class AlbumTreeView(BaseTreeView):
file = track.linked_files[i]
self.panel.register_object(file, file_item)
self.panel.update_file(file, file_item)
self.expandItem (item)
self.expandItem(item)
item.setIcon(0, icon)
for i, column in enumerate(self.columns):
text, similarity = track.column(column[1])

View File

@@ -274,11 +274,15 @@ class XmlWebService(QtCore.QObject):
if entitytype == "discid": path += "&cdstubs=no"
self.get(host, port, path, handler, mblogin=mblogin)
def get_release_group_by_id(self, releasegroupid, handler):
inc = ['releases', 'media']
self._get_by_id('release-group', releasegroupid, handler, inc)
def get_release_by_id(self, releaseid, handler, inc=[], mblogin=False):
self._get_by_id('release', releaseid, handler, inc, mblogin=mblogin)
def get_track_by_id(self, releaseid, handler, inc=[]):
self._get_by_id('track', releaseid, handler, inc)
def get_track_by_id(self, trackid, handler, inc=[]):
self._get_by_id('recording', trackid, handler, inc)
def lookup_puid(self, puid, handler):
inc = ['releases', 'release-groups', 'media', 'artist-credits']