Re: [Rhythmbox-devel] Magnatune catalog/purchasing plugin



Sweet! Why didn't I come across this[1] blog post sooner? The reading
code is now fully non-blocking thanks to gobject.add_idle, except for
check_info() which doesn't seem to take very long (using
gnomevfs.get_file_info instead of httplib helped).

I'm not sure whether I want to keep it so that the source is added to as
song_info.xml is being downloaded. It seems like it takes a long time to
add all the tracks (though it may just be my perception as to whether
it's slower than adding after downloading), but on the other hand, the
source doesn't sit empty for 2 minutes.

The amounts read in each of the idle methods (64KB while downloading,
128KB when loading from disk) can probably be tweaked as well, to give
the best balance between a fully responsive UI and being efficient when
doing I/O.

I'm not quite certain how to associate an action with a popup menu. I
can see that I need to add some glade bits to rhythmbox-ui.xml and then
call source.show_popup("mygladestuff") in the show_popup callback,
right? So how do I associate a method call with that popup menu item?
And how do I pass arguments to it? Is it like gobject.add_idle, where I
just add arguments after the method?

Thanks so much.

-Adam

PS: does anyone know what the int and bool passed to the show_popup
callback are?

[1]
http://gnomerocksmyworld.blogspot.com/2006/05/getting-off-my-lazy-arse.html

--
Adam Zimmerman <adam_zimmerman sfu ca>

CREATIVITY  - http://mirrors.creativecommons.org/movingimages/Building_on_the_Past.mpg
ALWAYS      - http://www.musiccreators.ca/
BUILDS      - http://www.ubuntu.com/
ON THE PAST - http://www.theopencd.org/
--

 In Pocatello, Idaho, a law passed in 1912 provided that "The carrying
of concealed weapons is forbidden, unless same are exhibited to public view."
import rhythmdb, rb
import gobject, gtk, gconf, gnomevfs, gnome
from gettext import gettext as _

import xml.sax, xml.sax.handler
import urllib
import datetime
import zipfile

magnatune_partner_id = "zimmerman"

user_dir = gnome.user_dir_get()
magnatune_dir = user_dir + "rhythmbox/magnatune/"
magnatune_dir_uri = gnomevfs.URI(magnatune_dir)
magnatune_song_info_uri = gnomevfs.URI("http://magnatune.com/info/song_info.xml";)
local_song_info_uri = gnomevfs.URI(magnatune_dir + "song_info.xml")
lc_uri = gnomevfs.URI(magnatune_dir + "info_last_changed")

################################################
# Class to add Magnatune catalog to the source #
################################################

class TrackListHandler(xml.sax.handler.ContentHandler):
	
	def __init__(self, db, entry_type):
		xml.sax.handler.ContentHandler.__init__(self)
		self._track = {} # temporary dictionary for track info
		self._db = db
		self._entry_type = entry_type
	
	def startElement(self, name, attrs):
		self._text = ""
	
	def endElement(self, name):
		if name == "Track":
			try:
				# add the track to the source
				entry = self._db.entry_new(self._entry_type, self._track['url'])
				date = datetime.date(int(self._track['launchdate'][0:4]), 1, 1).toordinal() # year is sometimes 0, so we use launchdate
				
				self._db.entry_set_uninserted(entry, rhythmdb.PROP_ARTIST, self._track['artist'])
				self._db.entry_set_uninserted(entry, rhythmdb.PROP_ALBUM, self._track['albumname'])
				self._db.entry_set_uninserted(entry, rhythmdb.PROP_TITLE, self._track['trackname'])
				self._db.entry_set_uninserted(entry, rhythmdb.PROP_TRACK_NUMBER, int(self._track['tracknum']))
				self._db.entry_set_uninserted(entry, rhythmdb.PROP_DATE, date)
				self._db.entry_set_uninserted(entry, rhythmdb.PROP_GENRE, self._track['mp3genre'])
				self._db.entry_set_uninserted(entry, rhythmdb.PROP_DURATION, int(self._track['seconds']))
				# entry.data['sku'] = self._track['albumsku']
				
				self._db.commit()
			except Exception,e: # This happens on duplicate uris being added
				print _("Couldn't add %s - %s") % (self._track['artist'], self._track['trackname']) # TODO: This should be printed to debug
				print e
			
			self._track = {}
		elif name == "AllSongs":
			pass # end of the file
		else:
			self._track[name] = self._text
	
	def characters(self, content):
		self._text = self._text + content


################################################
# Main Magnatune Plugin Class                  #
################################################

class Magnatune(rb.Plugin):
	
	info_file = None
	remote_info_file = None
	
	#
	# Core methods
	#
	
	def __init__(self):
		rb.Plugin.__init__(self)
		
	def activate(self, shell):
		self.db = shell.get_property("db")
		self.entry_type = rhythmdb.entry_register_type("MagnatuneEntryType")
		self.source = gobject.new (MagnatuneSource, shell=shell, name=_("Magnatune"), entry_type=self.entry_type)
		shell.register_entry_type_for_source(self.source, self.entry_type)
		
		icon = gtk.gdk.pixbuf_new_from_xpm_data(magnatune_logo_xpm) # Include a flashy Magnatune logo for the source
		self.source.set_property("icon", icon)
		ev = self.source.get_entry_view()
		ev.connect_object("show_popup", self.show_popup_cb, self.source, 0)
		shell.append_source(self.source, None) # Add the source to the list
		
		self.parser = xml.sax.make_parser()
		self.parser.setContentHandler(TrackListHandler(self.db, self.entry_type))
		if check_info():
			gobject.idle_add(self.idle_download_info)
		else:
			gobject.idle_add(self.idle_load_info)
		gobject.timeout_add(6 * 60 * 60 * 1000, self.check_info_updates) # every 6 hours.
	
	def deactivate(self, shell):
		self.db.entry_delete_by_type(self.entry_type)
		self.db.commit()
		self.source.delete_thyself()
		self.source = None
	
	
	#
	# Callback/helper functions
	#
	
	def show_popup_cb(self, source, some_int, some_bool): # FIXME: find out what the int and bool are
		entry_view = source.get_entry_view()
		client = gconf.client_get_default()
		cc = {}
		cc['number'] = client.get_string("/apps/rhythmbox/plugins/magnatune/cc")
		cc['year'] = client.get_string("/apps/rhythmbox/plugins/magnatune/yy")
		cc['month'] = client.get_string("/apps/rhythmbox/plugins/magnatune/mm")
		name = client.get_string("/apps/rhythmbox/plugins/magnatune/name")
		email = client.get_string("/apps/rhythmbox/plugins/magnatune/email")
		#sku = entry_view.get_selected_entries()[0].data['sku'] # just use the sku for the first track selected.
		#attach action: buy_track(sku, amount, cc, name, email, format)
		#source.show_popup("/MagnatuneSourcePopup")
	
	def check_info_updates(self): # TODO: if possible, make it so that the updated file is downloaded first, then the entries are switched
		if check_info():
			self.db.entry_delete_by_type(self.entry_type)
			self.db.commit()
			gobject.idle_add(self.idle_download_info)
		return True
	
	def idle_load_info(self):
		if self.info_file == None:
			self.info_file = gnomevfs.open(local_song_info_uri)
		try:
			data = self.info_file.read(128 * 1024)
			self.parser.feed(data)
			return True
		except gnomevfs.EOFError:
			self.info_file.close()
			self.info_file = None
			return False
	
	def idle_download_info(self):
		if self.info_file == None:
			self.remote_info_file = gnomevfs.open(magnatune_song_info_uri)
			self.info_file = gnomevfs.create(local_song_info_uri, open_mode=gnomevfs.OPEN_WRITE)
		try:
			data = self.remote_info_file.read(64 * 1024)
			self.parser.feed(data)
			self.info_file.write(data)
			return True
		except gnomevfs.EOFError:
			self.remote_info_file.close()
			self.info_file.close()
			self.remote_info_file = None
			self.info_file = None
			return False


class MagnatuneSource(rb.BrowserSource):
	def __init__(self):
		rb.Source.__init__(self)

gobject.type_register(MagnatuneSource)

################################################
# Methods for downloading the song info        #
################################################

def check_info():
# returns whether or not info has changed
	if not gnomevfs.exists(magnatune_dir_uri):
		gnomevfs.make_directory(magnatune_dir_uri, 0755)
	if not gnomevfs.exists(lc_uri):
		t = gnomevfs.create(lc_uri, open_mode=gnomevfs.OPEN_WRITE)
		t.write("never") # there needs to be something in the file, otherwise it throws an exception when read from
		t.close()
	modified = str(gnomevfs.get_file_info(magnatune_song_info_uri).mtime)
	lc_file = gnomevfs.open(lc_uri)
	last_changed = lc_file.read(100) # file should be less than 100 chars
	lc_file.close()
	if not last_changed.strip() == modified.strip():
		lc_file = gnomevfs.open(lc_uri, open_mode=gnomevfs.OPEN_WRITE)
		lc_file.write(modified)
		lc_file.close()
		return True
	return False

################################################
# Purchasing code.                             #
################################################

class BuyAlbumHandler(xml.sax.handler.ContentHandler): # Class to download the track, etc.
	
	format_map =	{
			'ogg'		:	'URL_OGGZIP',
			'flac'		:	'URL_FLACZIP',
			'wav'		:	'URL_WAVZIP',
			'mp3-cbr'	:	'URL_128KMP3ZIP',
			'mp3-vbr'	:	'URL_VBRZIP'
			}
	
	def __init__(self, format):
		xml.sax.handler.ContentHandler.__init__(self)
		self._format_tag = format_map[format] # format of audio to download
	
	def startElement(self, name, attrs):
		self._text = ""
	
	def endElement(self, name):
		if name == "ERROR": # Something went wrong. Display error message to user.
			raise MagnatuneError(self._text)
		elif name == "DL_USERNAME":
			self.username = self._text
		elif name == "DL_PASSWORD":
			self.password = self._text
		elif name == self._format_tag:
			self.url = self._text
	
	def characters(self, content):
		self._text = self._text + content

def buy_track(sku, amount, cc, name, email, format): # http://magnatune.com/info/api#purchase
	client = gconf.client_get_default()
	url = "https://magnatune.com/buy/buy_dl_cc_xml?";
	url = url + urllib.urlencode({
					'id':	magnatune_partner_id,
					'sku':	sku,
					'amount': amount,
					'cc':	cc['number'],
					'yy':	cc['year'],
					'mm':	cc['month'],
					'name': name,
					'email':email
				})
	
	buy_album_handler = BuyAlbumHandler(format) # so we can get the url and auth info
	xml.sax.parse(url, buy_album_handler)
	audio_dl_uri = gnomevfs.URI(buy_album_handler.url.replace(" ", "%20")) # some parts of the returned url are escaped, some aren't. TODO: Properly quote just the filename part of the path
	audio_dl_uri.user_name = buy_album_handler.username
	audio_dl_uri.password = buy_album_handler.password
	
	# Download the album and unzip it into the library
	library_location = client.get_list("/apps/rhythmbox/library_locations")[0] # Just use the first library location
	to_file = gnomevfs.URI(library_location + "/" + audio_dl_uri.short_name)
	out_file = to_file.__str__()
	gnomevfs.xfer_uri(audio_dl_uri, to_file, xfer_options=gnomevfs.XFER_DEFAULT,
		error_mode=gnomevfs.XFER_ERROR_MODE_ABORT, overwrite_mode=gnomevfs.XFER_OVERWRITE_MODE_ABORT,
		progress_callback=progress_info_cb, data=0x1234) # this will take a LONG time.
	
	album = zipfile.ZipFile(out_file)
	for track in album.namelist():
		out = gnomevfs.open(gnomevfs.URI(library_location + "/" + track), open_mode=gnomevfs.OPEN_MODE_WRITE) # FIXME: directories will need to be created first
		out.write(album.read(track))
		out.close()
	album.close()
	gnomevfs.unlink(to_file)

class MagnatuneError(Exception):
	pass

def progress_info_cb(info, data):
	assert data == 0x1234
	try:
		print "%s: %f %%\r" % (info.target_name,
			info.bytes_copied/float(info.bytes_total)*100),
	except Exception, ex: # Sometimes the method throws an exception, for no apparent reason
		pass
	return True

################################################
# Magnatune Logo.                              #
################################################

# (converted from http://www.magnatune.com/favicon.ico)

magnatune_logo_xpm = [
"32 32 4 1",
" 	c None", #Original colours:
".	c None", #FFFFFF
"+	c #303030", #C0C0C0 
"@	c #000000", #808080
"................................",
"................................",
"................................",
"................................",
"................................",
"................................",
"............++@@@@++............",
"..........+@@@@@@@@@@+..........",
".........+@@@+....+@@@+.........",
"........+@@+...++...+@@+........",
".......+@@+....@@....+@@+.......",
".......@@+.....@@.....+@@.......",
"......+@@......@@......@@+......",
"      + +      @@      + +      ",
"......@@...@@..@@..@@...@@......",
"......@@...@@+.@@.+@@...@@......",
"......@@...@@+.@@.+@@...@@......",
"......@@...@@+.@@.+@@...@@......",
"      + +  @@+.@@.+@@  + +      ",
"......+@@..@@+.@@.+@@..@@+......",
".......@@+.@@+.@@.+@@.+@@.......",
".......+@@+.+..+...+.+@@+.......",
"........+@@+........+@@+........",
".........+@@@+....+@@@+.........",
"..........+@@@@@@@@@@+..........",
"............++@@@@++............",
"................................",
"................................",
"................................",
"................................",
"................................",
"................................"
]

###
### preferences, ugly and gross. Someone else who knows what they're doing should probably fix this. Should probably be glade too.
###

#	def create_configure_dialog(self): # return a gtk dialog with configure options
#		if self._preferences == None:
#			client = gconf.client_get_default()
#			self._preferences = gtk.Dialog(title=_("Magnatune Preferences"), flags=gtk.DIALOG_MODAL)
#			
#			label = gtk.Label("<b>Purchase Information</b>")
#			self._preferences.vbox.pack_start(label, False, False, 0)
#			label.show()
#			
#			hbox = gtk.HBox()
#			label = gtk.Label(_("Name"))
#			entry = gtk.Entry()
#			self.setup_entry(entry, "name")
#			hbox.pack_start(label, False, False, 0)
#			hbox.pack_start(entry, False, False, 0)
#			label.show()
#			entry.show()
#			self._preferences.vbox.pack_start(hbox, True, True, 0)
#			hbox.show()
#			
#			hbox = gtk.HBox()
#			label = gtk.Label(_("E-mail Address"))
#			entry = gtk.Entry()
#			self.setup_entry(entry, "email")
#			hbox.pack_start(label, False, False, 0)
#			hbox.pack_start(entry, False, False, 0)
#			label.show()
#			entry.show()
#			self._preferences.vbox.pack_start(hbox, True, True, 0)
#			hbox.show()
#			
#			button = gtk.CheckButton(_("Remember Credit Card Information"))
#			credit_entry = gtk.Entry(max=16)
#			month_entry = gtk.Entry(max=2)
#			year_entry = gtk.Entry(max=4)
#			button.connect("toggled", self.check_toggle, (credit_entry, month_entry, year_entry))
#			set = client.get_bool("/apps/rhythmbox/plugins/magnatune/forget")
#			if set is not None:
#				button.set_active(set)
#			self._preferences.vbox.pack_start(button, False, False, 0)
#			button.show()
#			
#			hbox = gtk.HBox()
#			label = gtk.Label(_("Credit Card Number"))
#			# entry has already been created
#			self.setup_entry(credit_entry, "cc_num")
#			hbox.pack_start(label, False, False, 0)
#			hbox.pack_start(credit_entry, False, False, 0)
#			label.show()
#			credit_entry.show()
#			self._preferences.vbox.pack_start(hbox, True, True, 0)
#			hbox.show()
#			
#			hbox = gtk.HBox()
#			label = gtk.Label(_("Expiration: mm/yy "))
#			# entries already created
#			sep = gtk.Label(" / ")
#			self.setup_entry(month_entry, "cc_mm")
#			self.setup_entry(year_entry, "cc_yy")
#			hbox.pack_start(label, False, False, 0)
#			hbox.pack_start(month_entry, False, False, 0)
#			hbox.pack_start(sep, False, False, 0)
#			hbox.pack_start(year_entry, False, False, 0)
#			label.show()
#			month_entry.show()
#			sep.show()
#			year_entry.show()
#			self._preferences.vbox.pack_start(hbox, True, True, 0)
#			hbox.show()
#			
#			hbox = gtk.HBox()
#			button = gtk.Button(stock=gtk.STOCK_CLOSE)
#			button.connect("clicked", self.close_clicked, None)
#			self._preferences.action_area.pack_end(button, True, True, 0)
#			button.show()
#			
#		self._preferences.show()
#		return self._preferences
#
#	def check_toggle(self, widget, data=None):
#		active = not widget.get_active() # this method gets called before the widget changes
#		client = gconf.client_get_default()
#		client.set_bool("/apps/rhythmbox/plugins/magnatune/forget", active)
#		if active:
#			for entry in data:
#				entry.set_text("")
#				entry.set_sensitive(False)
#			for field in ('cc_num', 'cc_mm', 'cc_yy'):
#				client.unset("/apps/rhythmbox/plugins/magnatune/" + field)
#		else:
#			for entry in data:
#				entry.set_sensitive(True)
#	
#	def pref_changed(self, widget, gdk_event, data=None):
#		client = gconf.client_get_default()
#		client.set_string("/apps/rhythmbox/plugins/magnatune/" + data, widget.get_text())
#	
#	def close_clicked(self, widget, data=None):
#		self._preferences.hide()
#	
#	def setup_entry(self, entry, data):
#		client = gconf.client_get_default()
#		text = client.get_string("/apps/rhythmbox/plugins/magnatune/" + data)
#		if text is not None:
#			entry.set_text(text)
#		entry.connect("focus-out-event", self.pref_changed, data)


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]