From: Kristoffer Grönlund Date: Sat, 2 Jan 2010 04:16:03 +0000 (+0100) Subject: Again the hack is too great to mention.. but the amount of covers are now limited. X-Git-Url: http://git.maemo.org/git/?p=jamaendo;a=commitdiff_plain;h=38cb45f4c0c4231bda9a0466ad29e5804ced0f0a;ds=sidebyside Again the hack is too great to mention.. but the amount of covers are now limited. --- diff --git a/jamaendo/api.py b/jamaendo/api.py index 4c94cac..cd3d454 100644 --- a/jamaendo/api.py +++ b/jamaendo/api.py @@ -28,7 +28,8 @@ # 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 @@ -37,21 +38,15 @@ _MP3URL = _GET2+'stream/track/redirect/?id=%d&streamencoding=mp31' _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, @@ -104,7 +99,7 @@ class LazyQuery(object): 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() @@ -200,6 +195,8 @@ _CACHED_ARTISTS = 100 _CACHED_ALBUMS = 200 _CACHED_TRACKS = 500 _CACHED_RADIOS = 10 +# cache sizes, persistant +_CACHED_COVERS = 2048 # TODO: cache queries? @@ -218,7 +215,7 @@ class Query(object): pass def _geturl(self, url): - print "*** %s" % (url) + log.info("%s", url) Query._ratelimit() try: f = urllib.urlopen(url) @@ -234,8 +231,6 @@ class Query(object): def execute(self): raise NotImplemented -import threading - class CoverFetcher(threading.Thread): def __init__(self): threading.Thread.__init__(self) @@ -289,25 +284,46 @@ class CoverCache(object): """ 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) @@ -324,6 +340,24 @@ class CoverCache(object): _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) @@ -393,11 +427,7 @@ class GetQuery(Query): '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) @@ -471,6 +501,14 @@ def _update_cache(cache, new_items): 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""" diff --git a/jamaui/__init__.py b/jamaui/__init__.py index 505bc11..fd73b72 100644 --- a/jamaui/__init__.py +++ b/jamaui/__init__.py @@ -26,8 +26,8 @@ import logging 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) diff --git a/jamaui/albumlist.py b/jamaui/albumlist.py index ce1671a..5c82637 100644 --- a/jamaui/albumlist.py +++ b/jamaui/albumlist.py @@ -7,6 +7,12 @@ import logging 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) @@ -89,8 +95,8 @@ class RadioList(gtk.TreeView): 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: diff --git a/jamaui/player.py b/jamaui/player.py index 65cc549..dd51a92 100644 --- a/jamaui/player.py +++ b/jamaui/player.py @@ -122,7 +122,6 @@ class GStreamer(_Player): def play(self): if self.player: - log.debug("playing") self.player.set_state(gst.STATE_PLAYING) def pause(self): @@ -183,17 +182,18 @@ class GStreamer(_Player): # 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 ) @@ -289,6 +289,9 @@ PlayerBackend = GStreamer 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: @@ -327,6 +330,11 @@ class Playlist(object): 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 @@ -334,13 +342,14 @@ class Playlist(object): 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() @@ -379,8 +388,15 @@ class Player(object): 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(): @@ -391,7 +407,6 @@ class Player(object): postoffice.notify('prev', entry) def _on_eos(self): - log.debug("EOS!") self.next() the_player = Player() # the player instance diff --git a/jamaui/playerwindow.py b/jamaui/playerwindow.py index ce13494..2c441ec 100644 --- a/jamaui/playerwindow.py +++ b/jamaui/playerwindow.py @@ -26,6 +26,7 @@ import gobject import hildon import util import pango +import jamaendo from settings import settings from postoffice import postoffice from player import Playlist, the_player @@ -60,10 +61,10 @@ class PlayerWindow(hildon.StackableWindow): 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) @@ -104,6 +105,7 @@ class PlayerWindow(hildon.StackableWindow): 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) @@ -164,7 +166,7 @@ class PlayerWindow(hildon.StackableWindow): 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) @@ -199,7 +201,11 @@ class PlayerWindow(hildon.StackableWindow): self.playbtn.set_data('state', 'play') def set_labels(self, track, artist, album, playlist_pos, playlist_size): - self.playlist_pos.set_markup('Track %s of %s'%(int(playlist_pos)+1, playlist_size)) + if self.playlist.radio_mode: + ppstr = 'Radio: %s'%(cgi.escape(self.playlist.radio_name)) + else: + ppstr = 'Track %s of %s'%(int(playlist_pos)+1, playlist_size) + self.playlist_pos.set_markup(ppstr) self.track.set_markup('%s'%(cgi.escape(track))) self.artist.set_markup('%s'%(cgi.escape(artist))) self.album.set_markup('%s'%(cgi.escape(album))) @@ -258,9 +264,37 @@ class PlayerWindow(hildon.StackableWindow): 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) diff --git a/jamaui/radios.py b/jamaui/radios.py index 2068b82..dc3109c 100644 --- a/jamaui/radios.py +++ b/jamaui/radios.py @@ -59,14 +59,6 @@ class RadiosWindow(hildon.StackableWindow): 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) diff --git a/jamaui/showalbum.py b/jamaui/showalbum.py index 4325622..688022d 100644 --- a/jamaui/showalbum.py +++ b/jamaui/showalbum.py @@ -25,6 +25,7 @@ import gtk import cgi import hildon import jamaendo +from player import Playlist from playerwindow import open_playerwindow from settings import settings from postoffice import postoffice @@ -55,10 +56,6 @@ class ShowAlbum(hildon.StackableWindow): 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() @@ -68,13 +65,13 @@ class ShowAlbum(hildon.StackableWindow): 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) @@ -149,8 +146,10 @@ class ShowAlbum(hildon.StackableWindow): 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):