All pastes #62670 Raw Edit

Something

public text v1 · immutable
#62670 ·published 2006-06-06 19:38 UTC
rendered paste body
# (C) 2005 Joshua Kwan <joshk@triplehelix.org>
# 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,
				["<i>%s</i>" % 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. <b>%s</b>" %
						  (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 = [
			"<b>%s:</b> %s" % (tag("artist"), escape(album_artist)),
			"<b>%s:</b> %s" % (tag("album"), escape(candidate.tracklist[0]['album'])),
			"\n<u>%s</u>" % _("Track List")
		]
			
		for i in range(0, len(album)):
			if candidate.various:
				message.append("<b>%d.</b> %s - %s" % (i + 1,
					escape(candidate.tracklist[i]['artist']),
					escape(candidate.tracklist[i]['title'])))
			else:
				message.append("<b>%d.</b> %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)