Initial playlist support
authorKristoffer Grönlund <kristoffer.gronlund@purplescout.se>
Tue, 5 Jan 2010 00:08:34 +0000 (01:08 +0100)
committerKristoffer Grönlund <kristoffer.gronlund@purplescout.se>
Wed, 6 Jan 2010 13:56:42 +0000 (14:56 +0100)
jamaendo/api.py
jamaui/listbox.py [new file with mode: 0644]
jamaui/playerwindow.py
jamaui/playlists.py [new file with mode: 0644]
jamaui/settings.py
jamaui/showalbum.py
jamaui/ui.py
scripts/jamaendo

index 07a04e1..feeb17d 100644 (file)
@@ -129,6 +129,9 @@ class Artist(LazyQuery):
     def _set_from(self, other):
         return self._set_from_impl(other, 'name', 'image', 'albums')
 
+    def get_data(self):
+        return {'name':self.name, 'image':self.image}
+
 class Album(LazyQuery):
     def __init__(self, ID, json=None):
         self.ID = int(ID)
@@ -151,6 +154,12 @@ class Album(LazyQuery):
     def _set_from(self, other):
         return self._set_from_impl(other, 'name', 'image', 'artist_name', 'artist_id', 'license_url', 'tracks')
 
+    def get_data(self):
+        return {'name':self.name, 'image':self.image,
+                'artist_name':self.artist_name,
+                'artist_id':self.artist_id,
+                'license_url':self.license_url}
+
 class Track(LazyQuery):
     def __init__(self, ID, json=None):
         self.ID = int(ID)
@@ -171,6 +180,16 @@ class Track(LazyQuery):
     def ogg_url(self):
        return _OGGURL%(self.ID)
 
+    def get_data(self):
+        return {'name':self.name,
+                'artist_id':self.artist_id,
+                'artist_name':self.artist_name,
+                'album_image':self.album_image,
+                'album_name':self.album_name,
+                'album_id':self.album_id,
+                'numalbum':self.numalbum,
+                'duration':self.duration}
+
     def _needs_load(self):
         return self._needs_load_impl('name', 'artist_name', 'artist_id', 'album_name', 'album_id', 'numalbum', 'duration')
 
diff --git a/jamaui/listbox.py b/jamaui/listbox.py
new file mode 100644 (file)
index 0000000..edef878
--- /dev/null
@@ -0,0 +1,71 @@
+# what the fuck, GTK
+import gtk
+import hildon
+
+class ListBox(gtk.TreeView):
+    def __init__(self):
+        gtk.TreeView.__init__(self)
+        self.store = gtk.ListStore(str)
+        column = gtk.TreeViewColumn("Text")
+        textRenderer = gtk.CellRendererText()
+        column.pack_start(textRenderer, True)
+        column.set_attributes(textRenderer, text = 0)
+        self.append_column(column)
+        self.set_model(self.store)
+
+    def get_text(self, path):
+        it = self.store.get_iter(path)
+        if not it:
+            return None
+        ret = self.store.get(it, 0)
+        return ret[0]
+
+    def get_selected_text(self):
+        model, it = self.get_selection().get_selected()
+        if not it:
+            return None
+        ret = self.store.get(it, 0)
+        return ret[0]
+
+    def append(self, text):
+        self.store.append([text])
+
+    def on_select(self, callback, *args):
+        def cb(lb, path, col):
+            ret = self.get_text(path)
+            if ret:
+                callback(ret, *args)
+        self.connect('row-activated', cb)
+
+class ListDialog(gtk.Dialog):
+    def __init__(self, title, parent=None):
+        gtk.Dialog.__init__(self, title, parent)
+        self.listbox = ListBox()
+        panarea = hildon.PannableArea()
+        panarea.add(self.listbox)
+        panarea.set_size_request(800, 300)
+        self.vbox.pack_start(panarea, True, True, 0)
+
+        self.selected = None
+
+        def selector(selected, dialog):
+            if selected:
+                self.selected = selected
+                dialog.response(gtk.RESPONSE_OK)
+        self.listbox.on_select(selector, self)
+
+class ButtonListDialog(gtk.Dialog):
+    def __init__(self, title, parent=None):
+        gtk.Dialog.__init__(self, title, parent)
+        self.panarea = hildon.PannableArea()
+        self.panarea.set_size_request(800, 400)
+        self.buttons = gtk.VBox(False, 0)
+        self.panarea.add_with_viewport(self.buttons)
+        self.vbox.pack_start(self.panarea, True, True, 0)
+
+    def add_button(self, label, clickcb, *args):
+        btn = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH|gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
+        btn.set_label(label)
+        btn.connect('clicked', clickcb, *args)
+        self.buttons.pack_end(btn, False, False, 0)
+
index 16bfffe..2bb6598 100644 (file)
@@ -34,7 +34,7 @@ import logging
 import cgi
 
 from songposition import SongPosition
-
+from listbox import ListDialog
 log = logging.getLogger(__name__)
 
 class PlayerWindow(hildon.StackableWindow):
@@ -134,6 +134,11 @@ class PlayerWindow(hildon.StackableWindow):
         b.connect("clicked", to_album)
         self.menu.append(b)
 
+        b = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        b.set_label("Add to playlist")
+        b.connect("clicked", self.on_add_to_playlist)
+        self.menu.append(b)
+
         self.menu.show_all()
         self.set_app_menu(self.menu)
 
@@ -203,6 +208,14 @@ class PlayerWindow(hildon.StackableWindow):
         self.artist.set_markup('<span size="large">%s</span>'%(cgi.escape(artist)))
         self.album.set_markup('<span foreground="#aaaaaa">%s</span>'%(cgi.escape(album)))
 
+    def show_banner(self, message, timeout = 2000):
+        banner = hildon.hildon_banner_show_information(self, '', message)
+        banner.set_timeout(2000)
+
+    def on_add_to_playlist(self, button, user_data=None):
+        track = self.player.playlist.current()
+        from playlists import add_to_playlist
+        add_to_playlist(self, track)
 
     def volume_changed_hildon(self, widget):
         settings.volume = widget.get_level()/100.0
diff --git a/jamaui/playlists.py b/jamaui/playlists.py
new file mode 100644 (file)
index 0000000..714ec55
--- /dev/null
@@ -0,0 +1,142 @@
+#!/usr/bin/env python
+#
+# This file is part of Jamaendo.
+# Copyright (c) 2010 Kristoffer Gronlund
+#
+# Jamaendo is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Jamaendo is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Jamaendo.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Player code heavily based on http://thpinfo.com/2008/panucci/:
+#  A resuming media player for Podcasts and Audiobooks
+#  Copyright (c) 2008-05-26 Thomas Perl <thpinfo.com>
+#  (based on http://pygstdocs.berlios.de/pygst-tutorial/seeking.html)
+#
+import gtk
+import hildon
+import jamaendo
+from settings import settings
+import logging
+
+log = logging.getLogger(__name__)
+
+def _alist(l, match):
+    for key, value in l:
+        if key == match:
+            return value
+    return None
+
+def _show_banner(parent, message, timeout = 2000):
+    banner = hildon.hildon_banner_show_information(parent, '', message)
+    banner.set_timeout(2000)
+
+from listbox import ListDialog
+
+def add_to_playlist(wnd, track):
+    if not track:
+        _show_banner(wnd, "Nothing to add")
+        return
+
+    dialog = ListDialog('Add to playlist', wnd)
+    for name,_ in settings.playlists.iteritems():
+        dialog.listbox.append(name)
+    dialog.listbox.append("New...")
+    try:
+        dialog.show_all()
+        if dialog.run() == gtk.RESPONSE_OK:
+            selected_playlist = dialog.selected
+            if selected_playlist == "New...":
+                dialog.hide()
+                selected_playlist = create_new_playlist(wnd)
+            if track and selected_playlist:
+                if isinstance(track, (list, tuple)):
+                    for t in track:
+                        settings.add_to_playlist(selected_playlist, {'id':t.ID, 'data':t.get_data()})
+                else:
+                    settings.add_to_playlist(selected_playlist, {'id':track.ID, 'data':track.get_data()})
+                settings.save()
+                _show_banner(wnd, "Added to playlist '%s'" % (selected_playlist))
+    finally:
+        dialog.destroy()
+
+def create_new_playlist(wnd):
+    dia_name = gtk.Dialog()
+    dia_name.set_title("New playlist")
+    dia_name.add_button( gtk.STOCK_OK, gtk.RESPONSE_OK )
+    entry = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
+    entry.set_placeholder("Enter name")
+    entry.set_max_length(32)
+    entry.connect('activate', lambda entry, dialog: dialog.response(gtk.RESPONSE_OK), dia_name)
+    dia_name.vbox.pack_start(entry, True, True, 0)
+    dia_name.show_all()
+    if dia_name.run() != gtk.RESPONSE_OK:
+        return False
+    selected_playlist = entry.get_text()
+    dia_name.destroy()
+    if selected_playlist == '' or selected_playlist == 'New...':
+        return False
+    elif settings.get_playlist(selected_playlist):
+        _show_banner(wnd, "Playlist '%s' already exists!" % (selected_playlist))
+        return False
+    return selected_playlist
+
+
+class PlaylistsWindow(hildon.StackableWindow):
+    def __init__(self):
+        hildon.StackableWindow.__init__(self)
+        self.set_title("Playlists")
+
+        self.panarea = hildon.PannableArea()
+
+        (self.COL_NAME, self.COL_INFO) = range(2)
+        self.store = gtk.ListStore(str, str)
+        self.treeview = gtk.TreeView()
+        self.treeview.set_model(self.store)
+
+        col = gtk.TreeViewColumn('Name')
+        self.treeview.append_column(col)
+        cell = gtk.CellRendererText()
+        col.pack_start(cell, True)
+        col.add_attribute(cell, 'text', self.COL_NAME)
+        self.treeview.set_search_column(self.COL_NAME)
+        col.set_sort_column_id(self.COL_NAME)
+
+        col = gtk.TreeViewColumn('Info')
+        self.treeview.append_column(col)
+        cell = gtk.CellRendererText()
+        col.pack_start(cell, True)
+        col.add_attribute(cell, 'text', self.COL_INFO)
+
+        self.treeview.connect('row-activated', self.row_activated)
+
+        self.panarea.add(self.treeview)
+
+        self.add(self.panarea)
+
+        def trackcount(lst):
+            ln = len(lst)
+            if ln > 1:
+                return "(%d tracks)"%(ln)
+            elif ln == 1:
+                return "(1 track)"
+            return "(empty)"
+
+        for key, lst in sorted(list(settings.playlists.iteritems())):
+            self.store.append([key, trackcount(lst)])
+
+    def row_activated(self, treeview, path, view_column):
+        name = self.store.get(self.store.get_iter(path), self.COL_NAME)[0]
+        pl = settings.get_playlist(name)
+        if pl:
+            from playerwindow import open_playerwindow
+            wnd = open_playerwindow()
+            wnd.play_tracks(pl)
index 04dce11..a9c86ed 100644 (file)
@@ -23,6 +23,8 @@
 #
 import cPickle, os
 import logging
+import jamaendo
+import datetime
 
 from postoffice import postoffice
 
@@ -33,7 +35,8 @@ class Settings(object):
     defaults = {
         'volume':0.1,
         'user':None,
-        'favorites':set([]) # local favorites - until we can sync back
+        'favorites':set([]), # local favorites - until we can sync back
+        'playlists':{},
         }
 
     def __init__(self):
@@ -52,6 +55,27 @@ class Settings(object):
     def favorite(self, album):
         self.favorites.add(('album', album.ID))
         self.save()
+        postoffice.notify('settings-changed', 'favorites', self.favorites)
+
+    def get_playlist(self, playlist, get_track_objects=True):
+        entry = self.playlists.get(playlist)
+        if entry:
+            if get_track_objects:
+                return [jamaendo.Track(item['id'], item['data']) for item in entry]
+            return entry
+        return None
+
+    def add_to_playlist(self, playlist, track):
+        if isinstance(track, jamaendo.Track):
+            track = {'id':track.ID, 'data':track.get_data()}
+        assert(isinstance(track, dict))
+        lst = self.playlists.get(playlist)
+        if not lst:
+            lst = []
+            self.playlists[playlist] = lst
+        lst.append(track)
+        postoffice.notify('settings-changed', 'playlists', self.playlists)
+        log.debug("playlists is now %s", self.playlists)
 
     def load(self):
         if not os.path.isfile(self.__savename):
@@ -67,7 +91,10 @@ class Settings(object):
 
             for k in self.defaults.keys():
                 if k in settings:
+                    if k == 'playlists' and not isinstance(k, dict):
+                        continue
                     setattr(self, k, settings[k])
+            print settings
         except Exception, e:
             log.exception('failed to load settings')
 
@@ -81,6 +108,7 @@ class Settings(object):
             f = open(self.__savename, 'w')
             cPickle.dump(settings, f)
             f.close()
+            print settings
         except Exception, e:
             log.exception('failed to save settings')
 
index 688022d..a13ffe8 100644 (file)
@@ -32,6 +32,8 @@ from postoffice import postoffice
 import util
 import logging
 from albumlist import TrackList
+from playlists import add_to_playlist
+
 import webbrowser
 
 log = logging.getLogger(__name__)
@@ -97,6 +99,10 @@ class ShowAlbum(hildon.StackableWindow):
         player.set_label("Open player")
         player.connect("clicked", on_player)
         self.menu.append(player)
+        player = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        player.set_label("Add to playlist")
+        player.connect("clicked", self.on_add_to_playlist)
+        self.menu.append(player)
         self.menu.show_all()
         self.set_app_menu(self.menu)
 
@@ -107,6 +113,10 @@ class ShowAlbum(hildon.StackableWindow):
         if albumid == self.album.ID and size == 300:
             self.cover.set_from_file(cover)
 
+
+    def on_add_to_playlist(self, button, user_data=None):
+        add_to_playlist(self, self.tracklist)
+
     def make_imagebutton(self, name, cb):
         btn = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
         btn.set_relief(gtk.RELIEF_NONE)
index 2c0ad65..2eb3261 100644 (file)
@@ -62,16 +62,8 @@ from search import SearchWindow
 from featured import FeaturedWindow
 from radios import RadiosWindow
 from favorites import FavoritesWindow
-
-class PlaylistsWindow(hildon.StackableWindow):
-    def __init__(self):
-        hildon.StackableWindow.__init__(self)
-        self.set_title("Playlists")
-
-        label = gtk.Label("Playlists")
-        vbox = gtk.VBox(False, 0)
-        vbox.pack_start(label, True, True, 0)
-        self.add(vbox)
+from playlists import PlaylistsWindow
+from listbox import ButtonListDialog
 
 class Jamaui(object):
     def __init__(self):
@@ -98,9 +90,6 @@ class Jamaui(object):
         postoffice.connect('request-images', self, self.on_request_images)
         log.debug("Created main window.")
 
-    def save_settings(self):
-        settings.save()
-
     def create_menu(self):
         self.menu = hildon.AppMenu()
 
@@ -119,10 +108,10 @@ class Jamaui(object):
         player.connect("clicked", self.on_favorites)
         self.menu.append(player)
 
-        #player = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-        #player.set_label("Playlists")
-        #player.connect("clicked", self.on_playlists)
-        #self.menu.append(player)
+        player = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        player.set_label("Playlists")
+        player.connect("clicked", self.on_playlists)
+        self.menu.append(player)
 
         player = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
         player.set_label("Settings")
@@ -201,7 +190,7 @@ class Jamaui(object):
 
     def destroy(self, widget):
         postoffice.disconnect(['request-album-cover', 'request-images'], self)
-        self.save_settings()
+        settings.save()
         from player import the_player
         if the_player:
             the_player.stop()
@@ -267,17 +256,15 @@ JAMENDO is an online platform that distributes musical works under Creative Comm
     #    dialog.hide()
 
     def on_featured(self, button):
-        dialog = hildon.PickerDialog(self.window)
-        sel = hildon.TouchSelectorEntry(text=True)
-        for feature, _ in FeaturedWindow.features:
-            sel.append_text(feature)
-        dialog.set_selector(sel)
-        dialog.set_title("Featured")
-        sel.unselect_all(0)
-        if dialog.run() == gtk.RESPONSE_OK:
-            txt = sel.get_current_text()
-            self.featuredwnd = FeaturedWindow(txt)
+        dialog = ButtonListDialog('Featured', self.window)
+        def fn(btn, feature):
+            self.featuredwnd = FeaturedWindow(feature)
             self.featuredwnd.show_all()
+            dialog.response(gtk.RESPONSE_OK)
+        for feature, _ in FeaturedWindow.features:
+            dialog.add_button(feature, fn, feature)
+        dialog.show_all()
+        dialog.run()
         dialog.destroy()
 
     def on_radios(self, button):
@@ -313,7 +300,7 @@ JAMENDO is an online platform that distributes musical works under Creative Comm
         if val and result == gtk.RESPONSE_OK:
             #print "new user name:", val
             settings.user = val
-            self.save_settings()
+            settings.save()
 
 
     def on_favorites(self, button):
index 063064e..debdf19 100755 (executable)
@@ -11,7 +11,7 @@
 import os, sys
 local_module_dir = os.path.join(os.path.dirname(sys.argv[0]), '..')
 if os.path.isdir(local_module_dir):
-    sys.path.append(local_module_dir)
+    sys.path = [local_module_dir] + sys.path
 
 def main():
     from jamaui.ui import Jamaui