home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / lib / totem / plugins / jamendo / jamendo.py next >
Encoding:
Python Source  |  2009-04-14  |  25.4 KB  |  694 lines

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (c) 2008 David JL <izimobil@gmail.com>
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a
  6. # copy of this software and associated documentation files (the "Software"),
  7. # to deal in the Software without restriction, including without limitation
  8. # the rights to use, copy, modify, merge, publish, distribute, sublicense,
  9. # and/or sell copies of the Software, and to permit persons to whom the
  10. # Software is furnished to do so, subject to the following conditions:
  11. #
  12. # The above copyright notice and this permission notice shall be included in
  13. # all copies or substantial portions of the Software.
  14. #
  15. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
  18. # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  20. # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  21. # DEALINGS IN THE SOFTWARE.
  22.  
  23. """
  24. Jamendo totem plugin (http://www.jamendo.com).
  25.  
  26. TODO:
  27. - store thumbnails in relevant XDG directories (?)
  28. - cleanup the notebook code
  29. - interface with jamendo write API (not documented yet):
  30.   favorites, comments, etc...
  31. """
  32.  
  33. import os
  34. import totem
  35. import gconf
  36. import gobject
  37. import gtk
  38. import pango
  39. import socket
  40. import threading
  41. import time
  42. import urllib
  43. import urllib2
  44. from xml.sax.saxutils import escape
  45. try:
  46.     import json
  47. except ImportError:
  48.     try:
  49.         import simplejson as json
  50.     except ImportError:
  51.         dlg = gtk.MessageDialog(
  52.             type=gtk.MESSAGE_ERROR,
  53.             buttons=gtk.BUTTONS_OK
  54.         )
  55.         dlg.set_markup(_('You need to install the Python simplejson module.'))
  56.         dlg.run()
  57.         dlg.destroy()
  58.         raise
  59.  
  60. socket.setdefaulttimeout(30)
  61. gobject.threads_init()
  62. gconf_key = '/apps/totem/plugins/jamendo'
  63.  
  64.  
  65. class JamendoPlugin(totem.Plugin):
  66.     """
  67.     Jamendo totem plugin GUI.
  68.     """
  69.     SEARCH_CRITERIA = ['artist_name', 'tag_idstr']
  70.     AUDIO_FORMATS   = ['ogg2', 'mp31']
  71.     TAB_RESULTS     = 0
  72.     TAB_POPULAR     = 1
  73.     TAB_LATEST      = 2
  74.  
  75.     def __init__(self):
  76.         totem.Plugin.__init__(self)
  77.         self.debug = True
  78.         self.gstreamer_plugins_present = True
  79.         self.totem = None
  80.         self.gconf = gconf.client_get_default()
  81.         self.init_settings()
  82.  
  83.     def activate(self, totem_object):
  84.         """
  85.         Plugin activation.
  86.         """
  87.         # Initialise the interface
  88.         self.builder = self.load_interface("jamendo.ui", True,
  89.             totem_object.get_main_window(), self)
  90.         self.config_dialog = self.builder.get_object('config_dialog')
  91.         self.popup = self.builder.get_object('popup_menu')
  92.         container = self.builder.get_object('container')
  93.         self.notebook = self.builder.get_object('notebook')
  94.         self.search_entry = self.builder.get_object('search_entry')
  95.         self.search_combo = self.builder.get_object('search_combo')
  96.         self.search_combo.set_active(0)
  97.         self.album_button = self.builder.get_object('album_button')
  98.         self.previous_button = self.builder.get_object('previous_button')
  99.         self.next_button = self.builder.get_object('next_button')
  100.         self.progressbars = [
  101.             self.builder.get_object('results_progressbar'),
  102.             self.builder.get_object('popular_progressbar'),
  103.             self.builder.get_object('latest_progressbar'),
  104.         ]
  105.         self.treeviews = [
  106.             self.builder.get_object('results_treeview'),
  107.             self.builder.get_object('popular_treeview'),
  108.             self.builder.get_object('latest_treeview'),
  109.         ]
  110.         self.setup_treeviews()
  111.  
  112.         # Set up signals
  113.         self.builder.get_object('search_button').connect('clicked', self.on_search_button_clicked)
  114.         self.search_entry.connect('activate', self.on_search_entry_activate)
  115.         self.notebook.connect('switch-page', self.on_notebook_switch_page)
  116.         self.previous_button.connect('clicked', self.on_previous_button_clicked)
  117.         self.next_button.connect('clicked', self.on_next_button_clicked)
  118.         self.album_button.connect('clicked', self.on_album_button_clicked)
  119.         self.builder.get_object('cancel_button').connect('clicked', self.on_cancel_button_clicked)
  120.         self.builder.get_object('ok_button').connect('clicked', self.on_ok_button_clicked)
  121.         self.builder.get_object('add_to_playlist').connect('activate', self.on_add_to_playlist_activate)
  122.         self.builder.get_object('jamendo_album_page').connect('activate', self.on_open_jamendo_album_page_activate)
  123.  
  124.         self.totem = totem_object
  125.         self.reset()
  126.         container.show_all()
  127.         self.totem.add_sidebar_page("jamendo", _("Jamendo"), container)
  128.  
  129.     def deactivate(self, totem_object):
  130.         """
  131.         Plugin deactivation.
  132.         """
  133.         totem_object.remove_sidebar_page("jamendo")
  134.  
  135.     def create_configure_dialog(self, *args):
  136.         """
  137.         Plugin config dialog.
  138.         """
  139.         format = self.gconf.get_string('%s/format' % gconf_key)
  140.         num_per_page = self.gconf.get_int('%s/num_per_page' % gconf_key)
  141.         combo = self.builder.get_object('preferred_format_combo')
  142.         combo.set_active(self.AUDIO_FORMATS.index(format))
  143.         spinbutton = self.builder.get_object('album_num_spinbutton')
  144.         spinbutton.set_value(num_per_page)
  145.         return self.config_dialog
  146.  
  147.     def reset(self):
  148.         """
  149.         XXX this will be refactored asap.
  150.         """
  151.         self.current_page = {
  152.             self.TAB_RESULTS: 1,
  153.             self.TAB_POPULAR: 1,
  154.             self.TAB_LATEST : 1
  155.         }
  156.         self.running_threads = {
  157.             self.TAB_RESULTS: False,
  158.             self.TAB_POPULAR: False,
  159.             self.TAB_LATEST : False
  160.         }
  161.         self.pages = {
  162.             self.TAB_RESULTS: [],
  163.             self.TAB_POPULAR: [],
  164.             self.TAB_LATEST : []
  165.         }
  166.         self.album_count = [0, 0, 0]
  167.         for tv in self.treeviews:
  168.             tv.get_model().clear()
  169.         self._update_buttons_state()
  170.  
  171.     def init_settings(self):
  172.         """
  173.         Initialize plugin settings.
  174.         """
  175.         format = self.gconf.get_string('%s/format' % gconf_key)
  176.         if not format:
  177.             format = 'ogg2'
  178.             self.gconf.set_string('%s/format' % gconf_key, format)
  179.         num_per_page = self.gconf.get_int('%s/num_per_page' % gconf_key)
  180.         if not num_per_page:
  181.             num_per_page = 10
  182.             self.gconf.set_int('%s/num_per_page' % gconf_key, num_per_page)
  183.         JamendoService.AUDIO_FORMAT = format
  184.         JamendoService.NUM_PER_PAGE = num_per_page
  185.  
  186.     def setup_treeviews(self):
  187.         """
  188.         Setup the 3 treeview: result, popular and latest
  189.         """
  190.         self.current_treeview = self.treeviews[0]
  191.         for w in self.treeviews:
  192.             w.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
  193.  
  194.             # build pixbuf column
  195.             cell = gtk.CellRendererPixbuf()
  196.             col = gtk.TreeViewColumn()
  197.             col.pack_start(cell, True)
  198.             col.set_attributes(cell, pixbuf=1)
  199.             w.append_column(col)
  200.  
  201.             # build description column
  202.             cell = gtk.CellRendererText()
  203.             cell.set_property('wrap-mode', pango.WRAP_WORD)
  204.             cell.set_property('wrap-width', 30)
  205.             col = gtk.TreeViewColumn()
  206.             col.pack_start(cell, True)
  207.             col.set_attributes(cell, markup=2)
  208.             col.set_expand(True)
  209.             w.append_column(col)
  210.             w.connect_after('size-allocate', self.on_treeview_size_allocate, col, cell)
  211.  
  212.             # duration column
  213.             cell = gtk.CellRendererText()
  214.             cell.set_property('xalign', 1.0)
  215.             cell.set_property('size-points', 8)
  216.             col = gtk.TreeViewColumn()
  217.             col.pack_start(cell, True)
  218.             col.set_attributes(cell, markup=3)
  219.             col.set_alignment(1.0)
  220.             w.append_column(col)
  221.  
  222.             # configure the treeview
  223.             w.set_show_expanders(False) # we manage internally expand/collapse
  224.             w.set_tooltip_column(4)     # set the tooltip column
  225.  
  226.             # Connect signals
  227.             w.connect("button-press-event", self.on_treeview_row_clicked)
  228.             w.connect("row-activated", self.on_treeview_row_activated)
  229.  
  230.  
  231.     def add_treeview_item(self, treeview, album):
  232.         if not isinstance(album['image'], gtk.gdk.Pixbuf):
  233.             # album image pixbuf is not yet built
  234.             try:
  235.                 pb = gtk.gdk.pixbuf_new_from_file(album['image'])
  236.                 os.unlink(album['image'])
  237.                 album['image'] = pb
  238.             except:
  239.                 # do not fail for this, just display a dummy pixbuf
  240.                 album['image'] = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True,
  241.                     8, 1, 1)
  242.         # format title
  243.         title  = '<b>%s</b>\n' % self._format_str(album['name'])
  244.         title += _('Artist: %s') % self._format_str(album['artist_name'])
  245.         # format duration
  246.         dur = self._format_duration(album['duration'])
  247.         # format tooltip
  248.         try:
  249.             # Translators: this is the release date of an album in Python strptime format
  250.             release = time.strptime(album['dates']['release'][0:10], _('%Y-%m-%d'))
  251.             # Translators: this is the release time of an album in Python strftime format
  252.             release = time.strftime(_('%x'), release)
  253.         except:
  254.             release = ''
  255.         tip = '\n'.join([
  256.             '<b>%s</b>' % self._format_str(album['name']),
  257.             _('Artist: %s') % self._format_str(album['artist_name']),
  258.             _('Genre: %s') % self._format_str(album['genre']),
  259.             _('Released on: %s') % release,
  260.             _('License: %s') % self._format_str(album['license'][0]),
  261.         ])
  262.         # append album row
  263.         parent = treeview.get_model().append(None,
  264.             [album, album['image'], title, dur, tip]
  265.         )
  266.         # append track rows
  267.         icon = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, 1, 1)
  268.         for i, track in enumerate(album['tracks']):
  269.             # track title
  270.             # Translators: this is the title of a track in Python format
  271.             # (first argument is the track number, second is the track title)
  272.             tt = ('<small>%s</small>' % _('%02d. %s')) % \
  273.                 (i+1, self._format_str(track['name']))
  274.             # track duration
  275.             td = self._format_duration(track['duration'])
  276.             # track tooltip
  277.             tip = '\n'.join([
  278.                 '<b>%s</b>' %  self._format_str(track['name']),
  279.                 _('Album: %s') % self._format_str(album['name']),
  280.                 _('Artist: %s') % self._format_str(album['artist_name']),
  281.                 _('Duration: %s') % td,
  282.             ])
  283.             # append track
  284.             treeview.get_model().append(parent, [track, icon, tt, td, tip])
  285.         # update current album count
  286.         pindex = self.treeviews.index(treeview)
  287.         self.album_count[pindex] += 1
  288.  
  289.     def add_album_to_playlist(self, mode, album):
  290.         """
  291.         Add an album to the playlist, mode can be: replace, enqueue or
  292.         enqueue_and_play.
  293.         """
  294.         for i, track in enumerate(album['tracks']):
  295.             if mode in ('replace', 'enqueue_and_play'):
  296.                 if i == 0:
  297.                     # play first track
  298.                     self.add_track_to_playlist(mode, track)
  299.                 else:
  300.                     # and enqueue other tracks
  301.                     self.add_track_to_playlist('enqueue', track)
  302.             else:
  303.                 self.add_track_to_playlist('enqueue', track)
  304.  
  305.     def add_track_to_playlist(self, mode, t):
  306.         """
  307.         Add a track to the playlist, mode can be: replace, enqueue or
  308.         enqueue_and_play.
  309.         """
  310.         if mode == 'replace':
  311.             self.totem.action_remote(totem.REMOTE_COMMAND_REPLACE, t['stream'])
  312.         elif mode == 'enqueue':
  313.             self.totem.action_remote(totem.REMOTE_COMMAND_ENQUEUE, t['stream'])
  314.  
  315.     def fetch_albums(self, pn=1):
  316.         """
  317.         Initialize the fetch thread.
  318.         """
  319.         tab_index = self.treeviews.index(self.current_treeview)
  320.         if tab_index == self.TAB_POPULAR:
  321.             params = {'order': 'rating_desc'}
  322.         elif tab_index == self.TAB_LATEST:
  323.             params = {'order': 'date_desc'}
  324.         else:
  325.             value = self.search_entry.get_text()
  326.             if not value:
  327.                 return
  328.             prop = self.SEARCH_CRITERIA[self.search_combo.get_active()]
  329.             params = {'order': 'date_desc', prop: value}
  330.         params['pn'] = pn
  331.         self.current_treeview.get_model().clear()
  332.         self.previous_button.set_sensitive(False)
  333.         self.next_button.set_sensitive(False)
  334.         self.album_button.set_sensitive(False)
  335.         self.progressbars[tab_index].show()
  336.         self.progressbars[tab_index].set_fraction(0.0)
  337.         self.progressbars[tab_index].set_text(
  338.             _('Fetching albums, please wait...')
  339.         )
  340.         lcb = (self.on_fetch_albums_loop, self.current_treeview)
  341.         dcb = (self.on_fetch_albums_done, self.current_treeview)
  342.         ecb = (self.on_fetch_albums_error, self.current_treeview)
  343.         thread = JamendoService(params, lcb, dcb, ecb)
  344.         thread.start()
  345.         self.running_threads[tab_index] = True
  346.  
  347.     def on_fetch_albums_loop(self, treeview, album):
  348.         """
  349.         Add an album item and its tracks to the current treeview.
  350.         """
  351.         self.add_treeview_item(treeview, album)
  352.         # pulse progressbar
  353.         pindex = self.treeviews.index(treeview)
  354.         self.progressbars[pindex].set_fraction(
  355.             float(self.album_count[pindex]) / float(JamendoService.NUM_PER_PAGE)
  356.         )
  357.  
  358.     def on_fetch_albums_done(self, treeview, albums, save_state=True):
  359.         """
  360.         Called when the thread finished fetching albums.
  361.         """
  362.         pindex = self.treeviews.index(treeview)
  363.         model = treeview.get_model()
  364.         if save_state and len(albums):
  365.             self.pages[pindex].append(albums)
  366.             self.current_page[pindex] = len(self.pages[pindex])
  367.         self._update_buttons_state()
  368.         self.progressbars[pindex].set_fraction(0.0)
  369.         self.progressbars[pindex].hide()
  370.         self.album_count[pindex] = 0
  371.         self.running_threads[pindex] = False
  372.  
  373.     def on_fetch_albums_error(self, treeview, exc):
  374.         """
  375.         Called when an error occured in the thread.
  376.         """
  377.         self.reset()
  378.         pindex = self.treeviews.index(treeview)
  379.         self.progressbars[pindex].set_fraction(0.0)
  380.         self.progressbars[pindex].hide()
  381.         self.running_threads[pindex] = False
  382.         dlg = gtk.MessageDialog(
  383.             type=gtk.MESSAGE_ERROR,
  384.             buttons=gtk.BUTTONS_OK
  385.         )
  386.         dlg.set_markup(
  387.             '<b>%s</b>' % _('An error occurred while fetching albums.')
  388.         )
  389.         # managing exceptions with urllib is a real PITA... :(
  390.         if hasattr(exc, 'reason'):
  391.             try:
  392.                 reason = exc.reason[1]
  393.             except:
  394.                 try:
  395.                     reason = exc.reason[0]
  396.                 except:
  397.                     reason = str(exc)
  398.             reason = reason.capitalize()
  399.             msg = _('Failed to connect to Jamendo server.\n%s.') % reason
  400.         elif hasattr(exc, 'code'):
  401.             msg = _('The Jamendo server returned code %s.') % exc.code
  402.         else:
  403.             msg = str(exc)
  404.         dlg.format_secondary_text(msg)
  405.         dlg.run()
  406.         dlg.destroy()
  407.  
  408.     def on_search_entry_activate(self, *args):
  409.         """
  410.         Called when the user typed <enter> in the search entry.
  411.         """
  412.         return self.on_search_button_clicked()
  413.  
  414.     def on_search_button_clicked(self, *args):
  415.         """
  416.         Called when the user clicked on the search button.
  417.         """
  418.         if not self.search_entry.get_text():
  419.             return
  420.         if self.current_treeview != self.treeviews[self.TAB_RESULTS]:
  421.             self.current_treeview = self.treeviews[self.TAB_RESULTS]
  422.             self.notebook.set_current_page(self.TAB_RESULTS)
  423.         else:
  424.             self.on_notebook_switch_page(new_search=True)
  425.  
  426.     def on_notebook_switch_page(self, nb=None, tab=None, tab_num=0,
  427.         new_search=False):
  428.         """
  429.         Called when the changed a notebook page.
  430.         """
  431.         self.current_treeview = self.treeviews[int(tab_num)]
  432.         self._update_buttons_state()
  433.         model = self.current_treeview.get_model()
  434.         # fetch popular and latest albums only once
  435.         if self.running_threads[int(tab_num)] == True or \
  436.            (not new_search and len(model)):
  437.             return
  438.         if new_search:
  439.             self.current_page[self.TAB_RESULTS] = 1
  440.             self.pages[self.TAB_RESULTS] = []
  441.             self.album_count[self.TAB_RESULTS] = 0
  442.             self._update_buttons_state()
  443.         model.clear()
  444.         self.fetch_albums()
  445.  
  446.     def on_treeview_row_activated(self, tv, path, column):
  447.         """
  448.         Called when the user double-clicked on a treeview element.
  449.         """
  450.         try:
  451.             item = self._get_selection()[0] # first item selected
  452.         except:
  453.             return
  454.         if len(path) == 1:
  455.             self.add_album_to_playlist('replace', item)
  456.         else:
  457.             self.add_track_to_playlist('replace', item)
  458.  
  459.     def on_treeview_row_clicked(self, tv, evt):
  460.         """
  461.         Called when the user clicked on a treeview element.
  462.         """
  463.         try:
  464.             if evt.button == 3:
  465.                 path = tv.get_path_at_pos(int(evt.x), int(evt.y))
  466.                 sel  = tv.get_selection()
  467.                 rows = sel.get_selected_rows()
  468.                 if path[0] not in rows[1]:
  469.                     sel.unselect_all()
  470.                     sel.select_path(path[0])
  471.                 tv.grab_focus()
  472.                 self.popup.popup(None, None, None, evt.button, evt.time)
  473.                 return True
  474.             coords = evt.get_coords()
  475.             path, c, x, y = tv.get_path_at_pos(int(coords[0]), int(coords[1]))
  476.             if (len(path) == 1):
  477.                 if tv.row_expanded(path):
  478.                     tv.collapse_row(path)
  479.                 else:
  480.                     tv.expand_row(path, False)
  481.             self.album_button.set_sensitive(True)
  482.         except:
  483.             pass
  484.  
  485.     def on_treeview_size_allocate(self, tv, allocation, col, cell):
  486.         """
  487.         Hack to autowrap text of the title colum.
  488.         """
  489.         cols = (c for c in tv.get_columns() if c != col)
  490.         w = allocation.width - sum(c.get_width() for c in cols)
  491.         if cell.props.wrap_width == w or w <= 0:
  492.             return
  493.         cell.props.wrap_width = w
  494.  
  495.     def on_previous_button_clicked(self, *args):
  496.         """
  497.         Called when the user clicked the previous button.
  498.         """
  499.         self._update_buttons_state()
  500.         model = self.current_treeview.get_model()
  501.         model.clear()
  502.         pindex = self.treeviews.index(self.current_treeview)
  503.         self.current_page[pindex] -= 1
  504.         albums = self.pages[pindex][self.current_page[pindex]-1]
  505.         for album in albums:
  506.             self.add_treeview_item(self.current_treeview, album)
  507.         self.on_fetch_albums_done(self.current_treeview, albums, False)
  508.  
  509.     def on_next_button_clicked(self, *args):
  510.         """
  511.         Called when the user clicked the next button.
  512.         """
  513.         self._update_buttons_state()
  514.         model = self.current_treeview.get_model()
  515.         model.clear()
  516.         pindex = self.treeviews.index(self.current_treeview)
  517.         if self.current_page[pindex] == len(self.pages[pindex]):
  518.             self.fetch_albums(self.current_page[pindex]+1)
  519.         else:
  520.             self.current_page[pindex] += 1
  521.             albums = self.pages[pindex][self.current_page[pindex]-1]
  522.             for album in albums:
  523.                 self.add_treeview_item(self.current_treeview, album)
  524.             self.on_fetch_albums_done(self.current_treeview, albums, False)
  525.  
  526.     def on_album_button_clicked(self, *args):
  527.         """
  528.         Called when the user clicked on the album button.
  529.         """
  530.         try:
  531.             url = self._get_selection(True)[0]['url']
  532.             os.spawnlp(os.P_NOWAIT, "xdg-open", "xdg-open", url)
  533.         except:
  534.             pass
  535.  
  536.     def on_cancel_button_clicked(self, *args):
  537.         """
  538.         Called when the user clicked cancel in the config dialog.
  539.         """
  540.         self.config_dialog.hide()
  541.  
  542.     def on_ok_button_clicked(self, *args):
  543.         """
  544.         Called when the user clicked ok in the config dialog.
  545.         """
  546.         combo = self.builder.get_object('preferred_format_combo')
  547.         spinbutton = self.builder.get_object('album_num_spinbutton')
  548.         format = self.AUDIO_FORMATS[combo.get_active()]
  549.         self.gconf.set_string('%s/format' % gconf_key, format)
  550.         num_per_page = int(spinbutton.get_value())
  551.         self.gconf.set_int('%s/num_per_page' % gconf_key, num_per_page)
  552.         self.init_settings()
  553.         self.config_dialog.hide()
  554.         try:
  555.             self.reset()
  556.         except:
  557.             pass
  558.  
  559.     def on_add_to_playlist_activate(self, *args):
  560.         """
  561.         Called when the user clicked on the add to playlist button of the
  562.         popup menu.
  563.         """
  564.         items = self._get_selection()
  565.         for item in items:
  566.             if 'tracks' in item:
  567.                 # we have an album
  568.                 self.add_album_to_playlist('enqueue', item)
  569.             else:
  570.                 # we have a track
  571.                 self.add_track_to_playlist('enqueue', item)
  572.  
  573.     def on_open_jamendo_album_page_activate(self, *args):
  574.         """
  575.         Called when the user clicked on the jamendo album page button of the
  576.         popup menu.
  577.         """
  578.         return self.on_album_button_clicked()
  579.  
  580.     def _get_selection(self, root=False):
  581.         """
  582.         Shortcut method to retrieve the treeview items selected.
  583.         """
  584.         ret = []
  585.         sel = self.current_treeview.get_selection()
  586.         model, rows = sel.get_selected_rows()
  587.         for row in rows:
  588.             if root:
  589.                 it = model.get_iter((row[0],))
  590.             else:
  591.                 it = model.get_iter(row)
  592.             elt = model.get(it, 0)[0]
  593.             if elt not in ret:
  594.                 ret.append(elt)
  595.         return ret
  596.  
  597.     def _update_buttons_state(self):
  598.         """
  599.         Update the state of the previous and next buttons.
  600.         """
  601.         sel = self.current_treeview.get_selection()
  602.         model, rows = sel.get_selected_rows()
  603.         try:
  604.             it = model.get_iter(rows[0])
  605.         except:
  606.             it = None
  607.         pindex = self.treeviews.index(self.current_treeview)
  608.         self.previous_button.set_sensitive(self.current_page[pindex] > 1)
  609.         self.next_button.set_sensitive(len(model)==JamendoService.NUM_PER_PAGE)
  610.         self.album_button.set_sensitive(it is not None)
  611.  
  612.  
  613.     def _format_str(self, st, truncate=False):
  614.         """
  615.         Escape entities for pango markup and force the string to utf-8.
  616.         """
  617.         if not st:
  618.             return ''
  619.         try:
  620.             return escape(st.encode('utf8'))
  621.         except:
  622.             return st
  623.  
  624.     def _format_duration(self, secs):
  625.         """
  626.         Format the given number of seconds to a human readable duration.
  627.         """
  628.         try:
  629.             secs = int(secs)
  630.             if secs >= 3600:
  631.                 # Translators: time formatting (in Python strftime format) for the Jamendo plugin
  632.                 # for times longer than an hour
  633.                 return time.strftime(_('%H:%M:%S'), time.gmtime(secs))
  634.             # Translators: time formatting (in Python strftime format) for the Jamendo plugin
  635.             # for times shorter than an hour
  636.             return time.strftime(_('%M:%S'), time.gmtime(secs))
  637.         except:
  638.             return ''
  639.  
  640.  
  641. class JamendoService(threading.Thread):
  642.     """
  643.     Class that requests the jamendo REST service.
  644.     """
  645.  
  646.     API_URL = 'http://api.jamendo.com/get2'
  647.     AUDIO_FORMAT = 'ogg2'
  648.     NUM_PER_PAGE = 10
  649.  
  650.     def __init__(self, params, loop_cb, done_cb, error_cb):
  651.         self.params = params
  652.         self.loop_cb = loop_cb
  653.         self.done_cb = done_cb
  654.         self.error_cb = error_cb
  655.         self.lock = threading.Lock()
  656.         threading.Thread.__init__(self)
  657.  
  658.     def run(self):
  659.         url = '%s/id+name+duration+image+genre+dates+url+artist_id+' \
  660.               'artist_name+artist_url/album/json/?n=%s&imagesize=50' % \
  661.               (self.API_URL, self.NUM_PER_PAGE)
  662.         if len(self.params):
  663.             url += '&%s' % urllib.urlencode(self.params)
  664.         try:
  665.             self.lock.acquire()
  666.             albums = json.loads(self._request(url))
  667.             ret = []
  668.             for i, album in enumerate(albums):
  669.                 fname, headers = urllib.urlretrieve(album['image'])
  670.                 album['image'] = fname
  671.                 album['tracks'] = json.loads(self._request(
  672.                     '%s/id+name+duration+stream/track/json/?album_id=%s'\
  673.                     '&order=numalbum_asc' % (self.API_URL, album['id'])
  674.                 ))
  675.                 album['license'] = json.loads(self._request(
  676.                     '%s/name/license/json/album_license/?album_id=%s'\
  677.                     % (self.API_URL, album['id'])
  678.                 ))
  679.                 gobject.idle_add(self.loop_cb[0], self.loop_cb[1], album)
  680.             gobject.idle_add(self.done_cb[0], self.done_cb[1], albums)
  681.         except Exception, exc:
  682.             gobject.idle_add(self.error_cb[0], self.error_cb[1], exc)
  683.         finally:
  684.             self.lock.release()
  685.  
  686.     def _request(self, url):
  687.         opener = urllib2.build_opener()
  688.         opener.addheaders = [('User-agent', 'Totem Jamendo plugin')]
  689.         handle = opener.open(url)
  690.         data = handle.read()
  691.         handle.close()
  692.         return data
  693.  
  694.