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: Fri, 23 Jun 2006 16:16:18 -0700
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]