mirror of
https://github.com/fergalmoran/picard.git
synced 2026-02-22 23:55:46 +00:00
- 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:
192
picard/album.py
192
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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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']
|
||||
|
||||
Reference in New Issue
Block a user