From fa9d455e702a8b57ece9ea10b7bb6d933e54a262 Mon Sep 17 00:00:00 2001 From: Michael Wiencek Date: Mon, 23 May 2011 00:57:30 -0500 Subject: [PATCH] - 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. --- picard/album.py | 192 +++++++++----------------------------- picard/browser/browser.py | 2 +- picard/cluster.py | 44 ++++----- picard/tagger.py | 4 +- picard/track.py | 3 +- picard/ui/itemviews.py | 50 +++++----- picard/webservice.py | 8 +- 7 files changed, 102 insertions(+), 201 deletions(-) diff --git a/picard/album.py b/picard/album.py index d1fd77782..4192cb813 100644 --- a/picard/album.py +++ b/picard/album.py @@ -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() diff --git a/picard/browser/browser.py b/picard/browser/browser.py index 094527bf5..c16443a9e 100644 --- a/picard/browser/browser.py +++ b/picard/browser/browser.py @@ -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) diff --git a/picard/cluster.py b/picard/cluster.py index ee002cc78..3da000c91 100644 --- a/picard/cluster.py +++ b/picard/cluster.py @@ -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 diff --git a/picard/tagger.py b/picard/tagger.py index fbec53aab..5ba1dc8d5 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -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() diff --git a/picard/track.py b/picard/track.py index 9273fe51f..2e8d52387 100644 --- a/picard/track.py +++ b/picard/track.py @@ -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: diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index 354854b65..cf99c525f 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -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]) diff --git a/picard/webservice.py b/picard/webservice.py index f24697987..d7cbe9b5d 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -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']