# (C) 2005 Joshua Kwan # redistributable under the terms of the GNU GPL, version 2 or later import musicbrainz, os, gtk from musicbrainz.queries import * from qltk import GetStringDialog, ErrorMessage, ConfirmAction, Message from util import tag, escape __all__ = ["QLBrainz"] class AlbumCandidate(object): various = False tracklist = [] trmlist = [] id = "" def __init__(self): self.various = False self.tracklist = [] self.trmlist = [] self.id = "" # Shamelessly stolen from cddb.py class AskAction(ConfirmAction): """A message dialog that asks a yes/no question.""" def __init__(self, *args, **kwargs): kwargs["buttons"] = gtk.BUTTONS_YES_NO Message.__init__(self, gtk.MESSAGE_QUESTION, *args, **kwargs) class AlbumChooser(gtk.Dialog): active_candidate = None candidates = {} first = True def __title_match(self, a, b): c = filter(lambda x: x.isalnum(), a.lower()) d = filter(lambda x: x.isalnum(), b.lower()) return (c == d or c.startswith(d) or c.endswith(d) or d.startswith(c) or d.endswith(c)) def __cursor_changed(self, view): selection = view.get_selection() selection.set_mode(gtk.SELECTION_SINGLE) model, iter = view.get_selection().get_selected() # This MAY be the parent node or not. while model.iter_parent(iter) != None: iter = model.iter_parent(iter) selection.unselect_all() view.collapse_all() view.expand_row(model.get_path(iter), False) selection.set_mode(gtk.SELECTION_MULTIPLE) selection.select_iter(iter) for i in range(0, model.iter_n_children(iter)): view.get_selection().select_iter(model.iter_nth_child(iter, i)) self.active_candidate = model[iter][2] return def run(self): self.show_all() resp = gtk.Dialog.run(self) if resp == gtk.RESPONSE_OK: value = self.active_candidate else: value = None self.destroy() return value def __init__(self, brainz, album, candidates): gtk.Dialog.__init__(self, "Album selection") self.add_buttons(gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) self.candidates = candidates label = gtk.Label(_("Multiple albums found, please select one. (Bold entries denote a track title match with the tags this album already has.)")) label.set_line_wrap(True) # Initialize the TreeStore and seed it with data from candidates treestore = gtk.TreeStore(str, str, str) for candidate in candidates.values(): iter = treestore.append(None, ["%s" % escape(candidate.tracklist[0]['album']), "", candidate.id]) i = 1 for track in candidate.tracklist: if 'title' in album[i - 1] and self.__title_match(album[i - 1]['title'], track['title']): treestore.append(iter, [escape(track['artist']), "%d. %s" % (i, escape(track['title'])), ""]) else: treestore.append(iter, [escape(track['artist']), "%d. %s" % (i, escape(track['title'])), ""]) i = i + 1 view = gtk.TreeView(treestore) i = 0 def pango_format(column, cell, model, iter, arg): cell.set_property('markup', model[iter][arg]) for column in ["Album / Artist", "Title"]: renderer = gtk.CellRendererText() tvcolumn = gtk.TreeViewColumn(column, renderer, text=i) if column is "Title": tvcolumn.set_cell_data_func(renderer, pango_format, 1) else: tvcolumn.set_cell_data_func(renderer, pango_format, 0) tvcolumn.set_clickable(False) tvcolumn.set_resizable(True) view.append_column(tvcolumn) i = i + 1 self.set_size_request(400, 300) swin = gtk.ScrolledWindow() swin.add(view) swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) self.vbox.pack_start(label, expand=False, fill=True) self.vbox.pack_start(swin) view.connect('cursor-changed', self.__cursor_changed) class QLBrainz(object): PLUGIN_NAME = 'MusicBrainz lookup' PLUGIN_ICON = 'gtk-cdrom' PLUGIN_DESC = 'Retag an album based on a MusicBrainz search.' PLUGIN_VERSION = '0.1' VARIOUS_ARTISTS_ARTISTID = '89ad4ac3-39f7-470e-963a-56509c546377' mb = None def __init__(self): # Prepare the MusicBrainz client class self.mb = musicbrainz.mb() self.mb.SetDepth(4) """ A function that assumes you just 'select'ed an album. Returns an AlbumCandidate with cached track list and list of TRMs for each track. """ def __cache_this_album(self, tracks): tracklist = [] this_numtracks = self.mb.GetResultInt(MBE_AlbumGetNumTracks) this_title = self.mb.GetResultData(MBE_AlbumGetAlbumName) this_artistid = self.mb.GetIDFromURL(self.mb.GetResultData(MBE_AlbumGetAlbumArtistId)) # print "Album has %d tracks" % this_numtracks if this_numtracks == tracks: new_candidate = AlbumCandidate() new_candidate.id = self.mb.GetIDFromURL(self.mb.GetResultData(MBE_AlbumGetAlbumId)) if this_artistid == self.VARIOUS_ARTISTS_ARTISTID: new_candidate.various = True # Now cache EVERYTHING for all tracks # If this tracklist is used, its dict will be merged into the # target song's dict, so use the proper keys. for j in range(1, tracks + 1): track_data = {} track_data['musicbrainz_trackid'] = self.mb.GetIDFromURL(self.mb.GetResultData1(MBE_AlbumGetTrackId, j)) # VA album is possible, just obliquely cover all cases track_data['artist'] = self.mb.GetResultData1(MBE_AlbumGetArtistName, j) track_data['title'] = self.mb.GetResultData1(MBE_AlbumGetTrackName, j) track_data['album'] = this_title track_data['tracknumber'] = str(j) new_candidate.tracklist.append(track_data) # self.mb.Select1(MBS_SelectTrack, j - 1) # for k in range(0, self.mb.GetResultInt(MBE_GetNumTrmids, j)): # self.mb.Select(MBS_SelectTrmid, k) # self.mb.Select(MBS_Back) # print "Album: %s" % new_candidate.tracklist[0]['album'] # k = 1 # for track in new_candidate.tracklist: # print "%d. %s - %s (%s)" % (k, track['artist'], track['title'], # track['musicbrainz_trackid']) # k = k + 1 return new_candidate return None def __lookup_by_album_name(self, album, tracks): candidates = {} self.mb.QueryWithArgs(MBQ_FindAlbumByName, [album]) n_albums = self.mb.GetResultInt(MBE_GetNumAlbums) print "Found %d albums" % n_albums for i in range(1, n_albums + 1): self.mb.Select(MBS_Rewind) self.mb.Select1(MBS_SelectAlbum, i) candidate = self.__cache_this_album(tracks) if candidate != None: candidates[candidate.id] = candidate return candidates def do_tag(self, album, candidate): i = 0 album_artist = "" if candidate.various: album_artist = "Various Artists" else: album_artist = candidate.tracklist[0]['artist'] message = [ "%s: %s" % (tag("artist"), escape(album_artist)), "%s: %s" % (tag("album"), escape(candidate.tracklist[0]['album'])), "\n%s" % _("Track List") ] for i in range(0, len(album)): if candidate.various: message.append("%d. %s - %s" % (i + 1, escape(candidate.tracklist[i]['artist']), escape(candidate.tracklist[i]['title']))) else: message.append("%d. %s" % (i + 1, escape(candidate.tracklist[i]['title']))) if AskAction(None, _("Save the following information?"), "\n".join(message)).run(): for i in range(0, len(album)): for key in ['artist', 'title', 'album', 'musicbrainz_trackid', 'tracknumber']: if key not in album[i] or album[i][key] != candidate.tracklist[i][key]: album[i][key] = candidate.tracklist[i][key] def __get_album_trm(self, album): trm_this_album = [] for track in album: i, o = None, None try: i, o = os.popen2(['trm', track('~filename')]) except: raise TRMError #lame try: trm_this_album.append(o.readlines()[0].rstrip()) except: raise TRMError return trm_this_album def __choose_album(self, album, candidates): ret = AlbumChooser(self, album, candidates).run() if ret is None: return else: self.do_tag(album, candidates[ret]) def plugin_album(self, album, override=None): # If there is already an album name. When plugin_album is called, # all of the 'album' entries are guaranteed to be the same. mb_album = None # Test for user error. if 'tracknumber' in album[0] and int(album[0]('tracknumber').split("/")[0]) != 1: ErrorMessage(None, "", _("Please select the entire album (starting from track 1!)")).run() elif override is not None or 'album' in album[0]: album_name = "" if override is not None: album_name = override else: album_name = album[0]('album') candidates = self.__lookup_by_album_name(album_name, len(album)) if len(candidates) > 1: self.__choose_album(album, candidates) elif len(candidates) == 0: name = GetStringDialog( None, _("Couldn't locate album by name"), _("Couldn't find an album with the name \"%s\". To retry " "with another possible album name, enter it here.") % album_name, [], gtk.STOCK_OK).run() # recursion. well... if name: self.plugin_album(album, name) else: self.do_tag(album, candidates[candidates.keys()[0]]) elif 'album' not in album[0]: # and override is None name = GetStringDialog( None, _("Not enough information to locate album"), _("Please enter an album name to match this one to."), [], gtk.STOCK_OK).run() if name: self.plugin_album(album, name)