[Sugar-devel] [PATCH] Playlist for Jukebox

Manuel Quiñones manuq at laptop.org
Fri Sep 16 01:02:18 EDT 2011


This adds a playlist widget to Jukebox, and the ability to store it in
the journal so it can be restored later.

The activity already has the ability to add several media and going
back and forward through them.  Now this information is shown in a
list.  Also, activating a row starts playing the corresponding media.

In the future, an edit toolbox can be added to reorder rows, remove
rows and clean the list.

Signed-off-by: Manuel Quiñones <manuq at laptop.org>
---
 ControlToolbar.py  |   17 ++++++-
 jukeboxactivity.py |  145 ++++++++++++++++++++++++++++++++++++++-------------
 widgets.py         |   87 +++++++++++++++++++++++++++++++
 3 files changed, 211 insertions(+), 38 deletions(-)
 create mode 100644 widgets.py

diff --git a/ControlToolbar.py b/ControlToolbar.py
index 0943180..8d99bb6 100644
--- a/ControlToolbar.py
+++ b/ControlToolbar.py
@@ -23,6 +23,7 @@ import gobject
 import gtk
 
 from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.toggletoolbutton import ToggleToolButton
 from sugar.graphics.menuitem import MenuItem
 from sugar.graphics import iconentry
 from sugar.activity import activity
@@ -34,7 +35,10 @@ class ViewToolbar(gtk.Toolbar):
     __gsignals__ = {
         'go-fullscreen': (gobject.SIGNAL_RUN_FIRST,
                           gobject.TYPE_NONE,
-                          ([]))
+                          ([])),
+        'toggle-playlist': (gobject.SIGNAL_RUN_FIRST,
+                            gobject.TYPE_NONE,
+                            ([bool]))
     }
 
     def __init__(self):
@@ -46,9 +50,20 @@ class ViewToolbar(gtk.Toolbar):
         self.insert(self._fullscreen, -1)
         self._fullscreen.show()
 
+        self._show_playlist = ToggleToolButton('view-list')
+        self._show_playlist.set_active(True)
+        self._show_playlist.set_tooltip(_('Show Playlist'))
+        self._show_playlist.connect('toggled', self._playlist_toggled_cb)
+        self.insert(self._show_playlist, -1)
+        self._show_playlist.show()
+
     def _fullscreen_cb(self, button):
         self.emit('go-fullscreen')
 
+    def _playlist_toggled_cb(self, button):
+        active = button.get_property('active')
+        self.emit('toggle-playlist', active)
+
 
 class Control(gobject.GObject):
     """Class to create the Control (play) toolbar"""
diff --git a/jukeboxactivity.py b/jukeboxactivity.py
index 0ba3f4c..98acdd4 100644
--- a/jukeboxactivity.py
+++ b/jukeboxactivity.py
@@ -24,6 +24,7 @@
 
 import logging
 from gettext import gettext as _
+import cjson
 import os
 
 from sugar.activity import activity
@@ -62,6 +63,8 @@ from ControlToolbar import Control, ViewToolbar
 from ConfigParser import ConfigParser
 cf = ConfigParser()
 
+from widgets import PlayListWidget
+
 
 class JukeboxActivity(activity.Activity):
     UPDATE_INTERVAL = 500
@@ -84,6 +87,8 @@ class JukeboxActivity(activity.Activity):
             _view_toolbar = ViewToolbar()
             _view_toolbar.connect('go-fullscreen',
                     self.__go_fullscreen_cb)
+            _view_toolbar.connect('toggle-playlist',
+                    self.__toggle_playlist_cb)
             toolbox.add_toolbar(_('View'), _view_toolbar)
             _view_toolbar.show()
 
@@ -112,6 +117,8 @@ class JukeboxActivity(activity.Activity):
             _view_toolbar = ViewToolbar()
             _view_toolbar.connect('go-fullscreen',
                     self.__go_fullscreen_cb)
+            _view_toolbar.connect('toggle-playlist',
+                    self.__toggle_playlist_cb)
             view_toolbar_button = ToolbarButton(
                     page=_view_toolbar,
                     icon_name='toolbar-view')
@@ -146,7 +153,7 @@ class JukeboxActivity(activity.Activity):
         self.seek_timeout_id = -1
         self.player = None
         self.uri = None
-        self.playlist = []
+        self.playlist = [] # {'url': 'file://.../media.ogg', 'title': 'My song'}
         self.jobjectlist = []
         self.playpath = None
         self.currentplaying = None
@@ -159,10 +166,17 @@ class JukeboxActivity(activity.Activity):
         self.p_duration = gst.CLOCK_TIME_NONE
 
         self.bin = gtk.HBox()
+        self.bin.show()
+        self.playlist_widget = PlayListWidget(self.play)
+        self.playlist_widget.update(self.playlist)
+        self.playlist_widget.show()
+        self.bin.pack_start(self.playlist_widget, expand=False)
         self.videowidget = VideoWidget()
-        self.bin.add(self.videowidget)
+        self.videowidget.show()
+        self.bin.pack_start(self.videowidget)
         self.set_canvas(self.bin)
-        self.show_all()
+        self.bin.connect('size-allocate', self.__size_allocate_cb)
+
         #From ImageViewer Activity
         self._want_document = True
         if self._object_id is None:
@@ -171,8 +185,12 @@ class JukeboxActivity(activity.Activity):
 
         if handle.uri:
             self.uri = handle.uri
-            gobject.idle_add(self._start, self.uri)
+            gobject.idle_add(self._start, self.uri, handle.title)
             
+    def __size_allocate_cb(self, widget, allocation):
+        canvas_size = self.bin.get_allocation()
+        self.playlist_widget.set_size_request(canvas_size.width/3, 0)
+
     def open_button_clicked_cb(self, widget):
         """ To open the dialog to select a new file"""
         #self.player.seek(0L)
@@ -211,35 +229,32 @@ class JukeboxActivity(activity.Activity):
         #    return
         self.player.seek(0L)
         if direction == "prev" and self.currentplaying  > 0:
-            self.currentplaying -= 1
-            self.player.stop()
-            self.player = GstPlayer(self.videowidget)
-            self.player.connect("error", self._player_error_cb)
-            self.player.connect("tag", self._player_new_tag_cb)
-            self.player.connect("stream-info", self._player_stream_info_cb)
-            self.player.set_uri(self.playlist[self.currentplaying])
-            logging.info("prev: " + self.playlist[self.currentplaying])
+            self.play(self.currentplaying - 1)
+            logging.info("prev: " + self.playlist[self.currentplaying]['url'])
             #self.playflag = True
-            self.play_toggled()
-            self.player.connect("eos", self._player_eos_cb)
         elif direction == "next" and self.currentplaying  < len(self.playlist) - 1:
-            self.currentplaying += 1
-            self.player.stop()
-            self.player = GstPlayer(self.videowidget)
-            self.player.connect("error", self._player_error_cb)
-            self.player.connect("tag", self._player_new_tag_cb)
-            self.player.connect("stream-info", self._player_stream_info_cb)
-            self.player.set_uri(self.playlist[self.currentplaying])
-            logging.info("NExt: " + self.playlist[self.currentplaying])
+            self.play(self.currentplaying + 1)
+            logging.info("next: " + self.playlist[self.currentplaying]['url'])
             #self.playflag = True
-            self.play_toggled()
-            self.player.connect("eos", self._player_eos_cb)
         else:
             self.play_toggled()
             self.player.stop()
             self.player.set_uri(None)
-        self.check_if_next_prev()
+            self.check_if_next_prev()
 
+    def play(self, media_index):
+        self.currentplaying = media_index
+        self.player.stop()
+        self.player = GstPlayer(self.videowidget)
+        self.player.connect("eos", self._player_eos_cb)
+        self.player.connect("error", self._player_error_cb)
+        self.player.connect("tag", self._player_new_tag_cb)
+        self.player.connect("stream-info", self._player_stream_info_cb)
+        self.player.set_uri(self.playlist[self.currentplaying]['url'])
+
+        self.play_toggled()
+        self.check_if_next_prev()
+        self.playlist_widget.set_cursor(self.currentplaying)
 
     def _player_eos_cb(self, widget):
         self.songchange('next')
@@ -310,17 +325,57 @@ class JukeboxActivity(activity.Activity):
                 jobject = chooser.get_selected_object()
                 if jobject and jobject.file_path:
                     self.jobjectlist.append(jobject)
-                    self._start(jobject.file_path)
+                    title = jobject.metadata.get('title', None)
+                    self._start(jobject.file_path, title)
         finally:
             #chooser.destroy()
             #del chooser
             pass
 
+    def _get_data_from_file_path(self, file_path):
+        fd = open(file_path, 'r')
+        try:
+            data = fd.read()
+        finally:
+            fd.close()
+        return data
+
     def read_file(self, file_path):
-        self.uri = os.path.abspath(file_path)
-        if os.path.islink(self.uri):
-            self.uri = os.path.realpath(self.uri)
-        gobject.idle_add(self._start, self.uri)
+        def deserialize_playlist(data):
+            return cjson.decode(data)
+
+        def get_uri_title(file_path):
+            """Return the media URI and the title to show in the playlist."""
+            uri = os.path.abspath(file_path)
+            title = os.path.basename(file_path)
+            if os.path.islink(uri):
+                uri = os.path.realpath(uri)
+            return uri, title
+
+        if self.metadata['mime_type'] == 'text/plain':
+            logging.debug("reading playlist from file...")
+            data = self._get_data_from_file_path(file_path)
+            playlist = deserialize_playlist(data)
+            for elem in playlist:
+                gobject.idle_add(self._start, elem['url'], elem['title'])
+
+        else:
+            logging.debug("reading one media from file...")
+            self.uri, title = get_uri_title(file_path)
+            gobject.idle_add(self._start, self.uri, title)
+
+    def write_file(self, file_path):
+        def serialize_playlist(playlist):
+            return cjson.encode(playlist)
+
+        logging.debug("writing playlist to file...")
+        self.metadata['mime_type'] = 'text/plain'
+        playlist_str = serialize_playlist(self.playlist)
+        f = open(file_path, 'w')
+        try:
+            f.write(playlist_str)
+        finally:
+            f.close()
 
     def getplaylist(self, links):
         result = []
@@ -333,26 +388,34 @@ class JukeboxActivity(activity.Activity):
                 result.append('file://' + urllib.quote(os.path.join(self.playpath,x)))
         return result
 
-    def _start(self, uri=None):
+    def _start(self, uri=None, title=None):
         self._want_document = False
         self.playpath = os.path.dirname(uri)
         if not uri:
             return False
         # FIXME: parse m3u files and extract actual URL
         if uri.endswith(".m3u") or uri.endswith(".m3u8"):
-            self.playlist.extend(self.getplaylist([line.strip() for line in open(uri).readlines()]))
+            for line in open(uri).readlines():
+                url = line.strip()
+                self.playlist.extend({'url': url, 'title': title})
         elif uri.endswith('.pls'):
             try:
                 cf.readfp(open(uri))
                 x = 1
                 while True:
-                    self.playlist.append(cf.get("playlist",'File'+str(x)))
+                    self.playlist.append({'url': cf.get("playlist",'File'+str(x)),
+                                          'title': title})
                     x += 1
             except:
                 #read complete
                 pass
+
+        elif uri.startswith("file://"):
+            self.playlist.append({'url': uri, 'title': title})
+
         else:
-            self.playlist.append("file://" + urllib.quote(os.path.abspath(uri)))
+            self.playlist.append({'url': "file://" + urllib.quote(os.path.abspath(uri)),
+                                  'title': title})
         if not self.player:
             # lazy init the player so that videowidget is realized
             # and has a valid widget allocation
@@ -362,10 +425,12 @@ class JukeboxActivity(activity.Activity):
             self.player.connect("tag", self._player_new_tag_cb)
             self.player.connect("stream-info", self._player_stream_info_cb)
 
+        self.playlist_widget.update(self.playlist)
+
         try:
             if not self.currentplaying:
-                logging.info("Playing: " + self.playlist[0])
-                self.player.set_uri(self.playlist[0])
+                logging.info("Playing: " + self.playlist[0]['url'])
+                self.player.set_uri(self.playlist[0]['url'])
                 self.currentplaying = 0
                 self.play_toggled()
                 self.show_all()
@@ -452,6 +517,12 @@ class JukeboxActivity(activity.Activity):
     def __go_fullscreen_cb(self, toolbar):
         self.fullscreen()
 
+    def __toggle_playlist_cb(self, toolbar, active):
+        if active:
+            self.playlist_widget.show_all()
+        else:
+            self.playlist_widget.hide()
+        self.bin.queue_draw()
 
 class GstPlayer(gobject.GObject):
     __gsignals__ = {
@@ -617,7 +688,7 @@ class GstPlayer(gobject.GObject):
 
     def is_playing(self):
         return self.playing
-    
+
 class VideoWidget(gtk.DrawingArea):
     def __init__(self):
         gtk.DrawingArea.__init__(self)
diff --git a/widgets.py b/widgets.py
new file mode 100644
index 0000000..6606c39
--- /dev/null
+++ b/widgets.py
@@ -0,0 +1,87 @@
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+import logging
+from gettext import gettext as _
+
+import pygtk
+pygtk.require('2.0')
+
+import gobject
+import gtk
+import pango
+
+
+COLUMNS_NAME = ('index', 'media')
+COLUMNS = dict((name, i) for i, name in enumerate(COLUMNS_NAME))
+
+
+class PlayListWidget(gtk.ScrolledWindow):
+    def __init__(self, play_callback):
+        self._playlist = None
+        self._play_callback = play_callback
+
+        gtk.ScrolledWindow.__init__(self, hadjustment=None,
+                                    vadjustment=None)
+        self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+        self.listview = gtk.TreeView()
+        self.treemodel = gtk.ListStore(int, object)
+        self.listview.set_model(self.treemodel)
+        selection = self.listview.get_selection()
+        selection.set_mode(gtk.SELECTION_SINGLE)
+
+        renderer_idx = gtk.CellRendererText()
+        treecol_idx = gtk.TreeViewColumn(_('No.'))
+        treecol_idx.pack_start(renderer_idx, True)
+        treecol_idx.set_cell_data_func(renderer_idx, self._set_number)
+        self.listview.append_column(treecol_idx)
+
+        renderer_title = gtk.CellRendererText()
+        renderer_title.set_property('ellipsize', pango.ELLIPSIZE_END)
+        treecol_title = gtk.TreeViewColumn(_('Play List'))
+        treecol_title.pack_start(renderer_title, True)
+        treecol_title.set_cell_data_func(renderer_title, self._set_title)
+        self.listview.append_column(treecol_title)
+
+        self.listview.connect('row-activated', self.__on_row_activated)
+
+        self.add(self.listview)
+
+    def __on_row_activated(self, treeview, path, col):
+        model = treeview.get_model()
+        media_idx = path[COLUMNS['index']]
+        self._play_callback(media_idx)
+
+    def _set_number(self, column, cell, model, it):
+        idx = model.get_value(it, COLUMNS['index'])
+        cell.set_property('text', idx+1)
+
+    def _set_title(self, column, cell, model, it):
+        playlist_item = model.get_value(it, COLUMNS['media'])
+        cell.set_property('text', playlist_item['title'])
+
+    def update(self, playlist):
+        self.treemodel.clear()
+        self._playlist = playlist
+        pl = list(enumerate(playlist))
+        for i, media in pl:
+            self.treemodel.append((i, media))
+        self.set_cursor(0)
+
+    def set_cursor(self, index):
+        self.listview.set_cursor((index,))
+
+    def play_cb(self):
+        pass
-- 
1.7.4.4



More information about the Sugar-devel mailing list