Re: [Rhythmbox-devel] Magnatune catalog/purchasing plugin
- From: Adam Zimmerman <adam_zimmerman sfu ca>
- To: rhythmbox-devel gnome org
- Subject: Re: [Rhythmbox-devel] Magnatune catalog/purchasing plugin
- Date: Wed, 21 Jun 2006 11:03:34 -0700
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]