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



Alright, status update.

On Tue, 2006-20-06 at 09:03 -0700, Adam Zimmerman wrote: 
> On Tue, 2006-20-06 at 11:23 +1000, James "Doc" Livingston wrote:
> > It might be worth asking if there is a compressed version of the .xml
> > file available too, for example a gzipped version is ~270kb instead of
> > 5.8Mb - and that would save a lot of bandwidth. If there is anything he
> > needs to know about Rhythmbox which you don't feel up to answering, feel
> > free to pass my address on.
> 
> I'll do that, because that would definitely speed things up a bit.

Done. John says he'll post a zip version sometime soon.

> 
> > On a related note, we can probably do some other things to reduce the
> > bandwidth. The most obvious would be caching the xml file as
> > ~/.gnome2/rhythmbox/magnatune/song_info.xml (or whatever) and only
> > downloading it every now and then, and in the background. We could
> > probably send the HTTP magic needed to get the "it hasn't changed"
> > response, so we know not to update.
> 
> That's also a good idea anyway, in case someone manages to leave
> rhythmbox open for a month or something, and doesn't get any new
> artists. I'll work on that today.

OK, I've written the code that checks this and downloads the file if it
has changed. It just does a basic string comparison on the last-modified
header gotten from a HTTP HEAD request, which seems to work. I've added
a call to gobject.timeout_add to set a timer, which also seems to work.

The albums are stored in zip files, so track-transfer isn't really an
option. Instead, I'm using gnomevfs.xfer_uri (maybe switched to async if
it doesn't crash) to download the zip file and extract it (untested, not
even hooked up to ui, almost certainly doesn't work yet, as it doesn't
create the directories).

> > Rhythmbox supports attaching extra data, but it's not currently exposed
> > to Python.
> > 
> > One method of doing it (which would be fairly simple) would be to give
> > each entry a dictionary, accessable via "entry.data" or something - how
> > does that sound?
> 
> That sounds perfect. I assume the entry gets passed to whatever handler
> I have for the entry view's show-popup signal.

well, it seems the source does, which is good enough, since I can get
the entry view, and then the selected entries from that.

-- 
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/
--

 "Engineering without management is art."
		-- Jeff Johnson
import rhythmdb, rb
import gobject, gtk, gconf, gnomevfs, gnome
from gettext import gettext as _

import xml.sax, xml.sax.handler
import urllib, httplib
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):
	
	_preferences = 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))
		check_info()
		gobject.timeout_add(60 * 60 * 1000, self.check_info_updates) # every hour.
		###gnomevfs.async.open(user_dir + "rhythmbox/magnatune/song_info.xml", self.open_callback)
		self.parser.parse(user_dir + "rhythmbox/magnatune/song_info.xml")
	
	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/do
		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):
		if check_info(): # FIXME: is there a better way of doing this?
			self.db.entry_delete_by_type(self.entry_type)
			self.db.commit()
			self.parser.parse(user_dir + "rhythmbox/magnatune/song_info.xml")
		return True # keep running the method every hour


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

gobject.type_register(MagnatuneSource)

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

def download_info():
	gnomevfs.xfer_uri(magnatune_song_info_uri, local_song_info_uri, xfer_options=gnomevfs.XFER_DEFAULT, 
		error_mode=gnomevfs.XFER_ERROR_MODE_ABORT, overwrite_mode=gnomevfs.XFER_OVERWRITE_MODE_REPLACE,
		progress_callback=progress_info_cb, data=0x1234)

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


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()
	
	conn = httplib.HTTPConnection("magnatune.com")
	conn.request("HEAD", "/info/song_info.xml")
	resp = conn.getresponse()
	headers = resp.getheaders()
	resp.close()
	conn.close()
	for header in headers:
		if header[0] == "last-modified":
			modified_header = header[1]
	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_header.strip():
		download_info()
		lc_file = gnomevfs.open(lc_uri, open_mode=gnomevfs.OPEN_WRITE)
		lc_file.write(modified_header)
		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

################################################
# 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
"................................",
"................................",
"................................",
"................................",
"................................",
"................................",
"............++@@@@++............",
"..........+@@@@@@@@@@+..........",
".........+@@@+....+@@@+.........",
"........+@@+...++...+@@+........",
".......+@@+....@@....+@@+.......",
".......@@+.....@@.....+@@.......",
"......+@@......@@......@@+......",
"      + +      @@      + +      ",
"......@@...@@..@@..@@...@@......",
"......@@...@@+.@@.+@@...@@......",
"......@@...@@+.@@.+@@...@@......",
"......@@...@@+.@@.+@@...@@......",
"      + +  @@+.@@.+@@  + +      ",
"......+@@..@@+.@@.+@@..@@+......",
".......@@+.@@+.@@.+@@.+@@.......",
".......+@@+.+..+...+.+@@+.......",
"........+@@+........+@@+........",
".........+@@@+....+@@@+.........",
"..........+@@@@@@@@@@+..........",
"............++@@@@++............",
"................................",
"................................",
"................................",
"................................",
"................................",
"................................"
]

###
### Async callbacks
###

#	def open_callback(self, handle, exc_type):
#		times = 0
#		if not exc_type:
#			try:
#				while True:
#					handle.read(512*1024, self.read_callback) # file is about 5MB
#			except EOFError:
#				handle.close(lambda *args: None)
#		else:
#			handle.close(lambda *args: None)
#	
#	def read_callback(self, handle, buf, exc_type, bytes_requested):
#		self.parser.feed(buf)

###
### 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]