# An improved, structured jamendo API wrapper for the N900 with cacheing
# Image / cover downloads.. and more?
-import urllib, threading, os, gzip, time, simplejson, re
+import urllib, threading, os, time, simplejson, re
+import logging
_CACHEDIR = None
_COVERDIR = None
_OGGURL = _GET2+'stream/track/redirect/?id=%d&streamencoding=ogg2'
_TORRENTURL = _GET2+'bittorrent/file/redirect/?album_id=%d&type=archive&class=mp32'
-def set_cache_dir(cachedir):
- global _CACHEDIR
- global _COVERDIR
- _CACHEDIR = cachedir
- _COVERDIR = os.path.join(_CACHEDIR, 'covers')
-
- try:
- os.makedirs(_CACHEDIR)
- except OSError:
- pass
-
- try:
- os.makedirs(_COVERDIR)
- except OSError:
- pass
+try:
+ log = logging.getLogger(__name__)
+except:
+ class StdoutLogger(object):
+ def info(self, s, *args):
+ print s % (args)
+ def debug(self, s, *args):
+ pass#print s % (args)
+ log = StdoutLogger()
# These classes can be partially constructed,
# and if asked for a property they don't know,
def __repr__(self):
try:
return u"%s(%s)"%(self.__class__.__name__,
- u", ".join(repr(v) for k,v in self.__dict__.iteritems() if not k.startswith('_')))
+ u", ".join(("%s:%s"%(k,repr(v))) for k,v in self.__dict__.iteritems() if not k.startswith('_')))
except UnicodeEncodeError:
#import traceback
#traceback.print_exc()
_CACHED_ALBUMS = 200
_CACHED_TRACKS = 500
_CACHED_RADIOS = 10
+# cache sizes, persistant
+_CACHED_COVERS = 2048
# TODO: cache queries?
pass
def _geturl(self, url):
- print "*** %s" % (url)
+ log.info("%s", url)
Query._ratelimit()
try:
f = urllib.urlopen(url)
def execute(self):
raise NotImplemented
-import threading
-
class CoverFetcher(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
"""
def __init__(self):
self._covers = {} # (albumid, size) -> file
- coverdir = _COVERDIR if _COVERDIR else '/tmp'
- if os.path.isdir(coverdir):
- covermatch = re.compile(r'(\d+)\-(\d+)\.jpg')
- for fil in os.listdir(coverdir):
- fl = os.path.join(coverdir, fil)
- m = covermatch.match(fil)
- if m and os.path.isfile(fl):
- self._covers[(int(m.group(1)), int(m.group(2)))] = fl
self._fetcher = CoverFetcher()
self._fetcher.start()
+ if _COVERDIR and os.path.isdir(_COVERDIR):
+ self.prime_cache()
+
+ def prime_cache(self):
+ coverdir = _COVERDIR
+ covermatch = re.compile(r'(\d+)\-(\d+)\.jpg')
+
+ prev_covers = os.listdir(coverdir)
+
+ if len(prev_covers) > _CACHED_COVERS:
+ import random
+ dropn = len(prev_covers) - _CACHED_COVERS
+ todrop = random.sample(prev_covers, dropn)
+ log.warning("Deleting from cache: %s", todrop)
+ for d in todrop:
+ m = covermatch.match(d)
+ if m:
+ try:
+ os.unlink(os.path.join(coverdir, d))
+ except OSError, e:
+ log.exception('unlinking failed')
+
+ for fil in os.listdir(coverdir):
+ fl = os.path.join(coverdir, fil)
+ m = covermatch.match(fil)
+ if m and os.path.isfile(fl):
+ self._covers[(int(m.group(1)), int(m.group(2)))] = fl
def fetch_cover(self, albumid, size):
- coverdir = _COVERDIR if _COVERDIR else '/tmp'
- to = os.path.join(coverdir, '%d-%d.jpg'%(albumid, size))
- if not os.path.isfile(to):
- url = _GET2+'image/album/redirect/?id=%d&imagesize=%d'%(albumid, size)
- urllib.urlretrieve(url, to)
- self._covers[(albumid, size)] = to
- return to
+ coverdir = _COVERDIR
+ if coverdir:
+ to = os.path.join(coverdir, '%d-%d.jpg'%(albumid, size))
+ if not os.path.isfile(to):
+ url = _GET2+'image/album/redirect/?id=%d&imagesize=%d'%(albumid, size)
+ urllib.urlretrieve(url, to)
+ self._covers[(albumid, size)] = to
+ return to
+ return None
def get_cover(self, albumid, size):
cover = self._covers.get((albumid, size), None)
_cover_cache = CoverCache()
+def set_cache_dir(cachedir):
+ global _CACHEDIR
+ global _COVERDIR
+ _CACHEDIR = cachedir
+ _COVERDIR = os.path.join(_CACHEDIR, 'covers')
+
+ try:
+ os.makedirs(_CACHEDIR)
+ except OSError:
+ pass
+
+ try:
+ os.makedirs(_COVERDIR)
+ except OSError:
+ pass
+
+ _cover_cache.prime_cache()
+
def get_album_cover(albumid, size=100):
return _cover_cache.get_cover(albumid, size)
'params' : 'user_idstr=%s',
'constructor' : [Album]
},
- #http://api.jamendo.com/get2/id+name+url+image+artist_name/album/jsonpretty/album_user_starred/?user_idstr=sylvinus&n=all
- #q = SearchQuery('album', user_idstr=user)
-
}
-#http://api.jamendo.com/get2/id+name+image+artist_name+album_name+album_id+numalbum+duration/track/json/radio_track_inradioplaylist+track_album+album_artist/?order=numradio_asc&radio_id=283
def __init__(self, what, ID):
Query.__init__(self)
elif isinstance(item, Album) and item.tracks:
for track in item.tracks:
_update_cache(_tracks, track)
+ # enforce cache limits here!
+ # also, TODO: save/load cache between sessions
+ # that will require storing a timestamp with
+ # each item, though..
+ # perhaps,
+ # artists: 1 day - changes often
+ # albums: 2-5 days - changes less often (?)
+ # tracks: 1 week - changes rarely, queried often
def get_artist(artist_id):
"""Returns: Artist"""
import sys
LOG_FILENAME = '/tmp/jamaendo.log'
-LOG_LEVEL = logging.DEBUG
-
-#
-logging.basicConfig(filename=LOG_FILENAME, level=LOG_LEVEL, format="%(name)-15s: [%(lineno)4d] %(levelname)-8s %(message)s")
+LOG_LEVEL = logging.INFO
+#LOG_FORMAT = "%(asctime)s %(name)-19s %(levelname)-5s - %(message)s"
+LOG_FORMAT = "%(asctime)s %(name)-10s: [%(lineno)4d] %(levelname)-5s %(message)s"
+logging.basicConfig(filename=LOG_FILENAME, level=LOG_LEVEL, format=LOG_FORMAT)
log = logging.getLogger(__name__)
+class ImageDownloader(object):
+ """
+ TODO: background downloader of images
+ for album lists, track lists, etc
+ """
+
class AlbumList(gtk.TreeView):
def __init__(self):
gtk.TreeView.__init__(self)
def get_radio_id(self, path):
treeiter = self.__store.get_iter(path)
- _, _id = self.__store.get(treeiter, 0, 1)
- return _id
+ name, _id = self.__store.get(treeiter, 0, 1)
+ return name, _id
def radio_name(self, radio):
if radio.idstr:
def play(self):
if self.player:
- log.debug("playing")
self.player.set_state(gst.STATE_PLAYING)
def pause(self):
# Sets the right property depending on the platform of self.filesrc
if self.player is not None:
self.filesrc.set_property(self.filesrc_property, uri)
+ log.info("%s", uri)
def _on_message(self, bus, message):
t = message.type
if t == gst.MESSAGE_EOS:
- log.info("End of stream")
+ log.debug("Gstreamer: End of stream")
self.eos_callback()
elif t == gst.MESSAGE_STATE_CHANGED:
if (message.src == self.player and
message.structure['new-state'] == gst.STATE_PLAYING):
- log.info("State changed to playing")
+ log.debug("gstreamer: state -> playing")
elif t == gst.MESSAGE_ERROR:
err, debug = message.parse_error()
log.critical( 'Error: %s %s', err, debug )
class Playlist(object):
def __init__(self, items = []):
+ self.radio_mode = False
+ self.radio_id = None
+ self.radio_name = None
if items is None:
items = []
for item in items:
return self.items[self._current]
return None
+ def jump_to(self, item_id):
+ for c, i in enumerate(self.items):
+ if i.ID == item_id:
+ self._current = c
+
def current_index(self):
return self._current
return len(self.items)
def __repr__(self):
- return "Playlist(%s)"%(", ".join([str(item.ID) for item in self.items]))
+ return "Playlist(%d of %s)"%(self._current, ", ".join([str(item.ID) for item in self.items]))
class Player(object):
def __init__(self):
self.backend = PlayerBackend()
self.backend.set_eos_callback(self._on_eos)
self.playlist = Playlist()
+ self.__in_end_notify = False # ugly...
def get_position_duration(self):
return self.backend.get_position_duration()
self.backend.play_url('mp3', entry.mp3_url())
log.debug("playing %s", entry)
postoffice.notify('next', entry)
- else:
- self.stop()
+ elif not self.__in_end_notify:
+ self.__in_end_notify = True
+ postoffice.notify('playlist-end', self.playlist)
+ self.__in_end_notify = False
+ # if the notification refills the playlist,
+ # we do nothing after this point so we don't
+ # mess things up
+ if not self.playlist.has_next():
+ self.stop()
def prev(self):
if self.playlist.has_prev():
postoffice.notify('prev', entry)
def _on_eos(self):
- log.debug("EOS!")
self.next()
the_player = Player() # the player instance
import hildon
import util
import pango
+import jamaendo
from settings import settings
from postoffice import postoffice
from player import Playlist, the_player
self.playlist_pos = gtk.Label()
self.playlist_pos.set_alignment(1.0,0)
self.track = gtk.Label()
- self.track.set_alignment(0,0)
+ self.track.set_alignment(0,1)
self.track.set_ellipsize(pango.ELLIPSIZE_END)
self.artist = gtk.Label()
- self.artist.set_alignment(0,0)
+ self.artist.set_alignment(0,0.5)
self.artist.set_ellipsize(pango.ELLIPSIZE_END)
self.album = gtk.Label()
self.album.set_alignment(0,0)
self.add(vbox)
postoffice.connect('album-cover', self, self.set_album_cover)
+ postoffice.connect('playlist-end', self, self.on_playlist_end)
postoffice.connect(['next', 'prev', 'play', 'pause', 'stop'], self, self.on_state_changed)
#print "Created player window, playlist: %s" % (self.playlist)
def on_destroy(self, wnd):
self.stop_position_timer()
- postoffice.disconnect(['album-cover', 'next', 'prev', 'play', 'stop'], self)
+ postoffice.disconnect(['album-cover', 'playlist-end', 'next', 'prev', 'play', 'stop'], self)
def add_stock_button(self, btns, stock, cb):
btn = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
self.playbtn.set_data('state', 'play')
def set_labels(self, track, artist, album, playlist_pos, playlist_size):
- self.playlist_pos.set_markup('<span size="small">Track %s of %s</span>'%(int(playlist_pos)+1, playlist_size))
+ if self.playlist.radio_mode:
+ ppstr = '<span size="small">Radio: %s</span>'%(cgi.escape(self.playlist.radio_name))
+ else:
+ ppstr = '<span size="small">Track %s of %s</span>'%(int(playlist_pos)+1, playlist_size)
+ self.playlist_pos.set_markup(ppstr)
self.track.set_markup('<span size="x-large">%s</span>'%(cgi.escape(track)))
self.artist.set_markup('<span size="large">%s</span>'%(cgi.escape(artist)))
self.album.set_markup('<span foreground="#aaaaaa">%s</span>'%(cgi.escape(album)))
if playing and albumid and (int(playing) == int(albumid)):
self.cover.set_from_file(cover)
+ def play_radio(self, radio_name, radio_id):
+ playlist = Playlist()
+ playlist.radio_mode = True
+ playlist.radio_name = radio_name
+ playlist.radio_id = radio_id
+ log.debug("Playing radio: %s", playlist)
+ self.refill_radio(playlist)
+
+ def refill_radio(self, playlist):
+ if playlist.radio_mode:
+ playlist.add(jamaendo.get_radio_tracks(playlist.radio_id))
+ log.debug("Refilling radio %s", playlist)
+ self.player.playlist = playlist
+ self.playlist = playlist
+ self.player.next()
+ log.debug("Playlist current: %s, playing? %s", playlist.current_index(),
+ self.player.playing())
+
+ def on_playlist_end(self, playlist):
+ if playlist.radio_mode:
+ self.refill_radio(playlist)
+
def play_tracks(self, tracks):
+ self.__play_tracks(tracks)
+
+ def __play_tracks(self, tracks):
self.clear_position()
- self.playlist = Playlist(tracks)
+ if isinstance(tracks, Playlist):
+ self.playlist = tracks
+ else:
+ self.playlist = Playlist(tracks)
self.player.stop()
self.player.play(self.playlist)
return button
def row_activated(self, treeview, path, view_column):
- _id = self.radiolist.get_radio_id(path)
- item = self.radios[_id]
- self.open_item(item)
-
- def open_item(self, item):
- hildon.hildon_gtk_window_set_progress_indicator(self, 1)
- tracks = jamaendo.get_radio_tracks(item.ID)
- hildon.hildon_gtk_window_set_progress_indicator(self, 0)
- if tracks:
- wnd = open_playerwindow()
- wnd.play_tracks(tracks)
+ name, _id = self.radiolist.get_radio_id(path)
+ wnd = open_playerwindow()
+ wnd.play_radio(name, _id)
import cgi
import hildon
import jamaendo
+from player import Playlist
from playerwindow import open_playerwindow
from settings import settings
from postoffice import postoffice
self.download = self.make_imagebutton('download', self.on_download)
self.favorite = self.make_imagebutton('favorite', self.on_favorite)
self.license = self.make_imagebutton('license', self.on_license)
- self.playbtn = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
- self.playbtn.set_label("Play album")
- self.playbtn.set_border_width(0)
- self.playbtn.connect('clicked', self.on_play)
vbox2 = gtk.VBox()
self.albumname = gtk.Label()
self.tracks = TrackList(numbers=True)
self.tracks.connect('row-activated', self.row_activated)
- for track in jamaendo.get_tracks(album.ID):
+ self.tracklist = jamaendo.get_tracks(album.ID)
+ for track in self.tracklist:
self.tracks.add_track(track)
top_hbox.pack_start(vbox1, False)
top_hbox.pack_start(vbox2, True)
vbox1.pack_start(self.cover, True)
- vbox1.pack_start(self.playbtn, False)
vbox1.pack_start(self.bbox, False)
self.bbox.add(self.goto_artist)
self.bbox.add(self.download)
def row_activated(self, treeview, path, view_column):
_id = self.tracks.get_track_id(path)
- track = jamaendo.get_track(_id)
- self.open_item(track)
+ playlist = Playlist(self.tracklist)
+ playlist.jump_to(_id)
+ wnd = open_playerwindow()
+ wnd.play_tracks(playlist)
def open_item(self, item):
if isinstance(item, jamaendo.Album):