Ticket #3714: 0002-Code-improved-for-better-performance-and-robustness.patch

File 0002-Code-improved-for-better-performance-and-robustness.patch, 49.6 KB (added by humitos, 11 years ago)
  • activity.py

    From 34f257acc510495ecb05a42b8e7669191d6a3d3f Mon Sep 17 00:00:00 2001
    From: Manuel Kaufmann <humitos@gmail.com>
    Date: Tue, 15 Jan 2013 17:55:47 -0300
    Subject: [PATCH Jukebox 2/2] Code improved for better performance and
     robustness
    
     - Visualization on Audio streams removed
    
       This cause a 100% of CPU usage on XO-4 and XO-1.75. An empty widget
       is shown when an Audio stream is played and the Video stream is
       shown if a Video stream is played.
    
     - Better control over the playlist
    
       Use signals ('play-index' for example) to communicate between the
       main window and the playlist.
    
       Save and Load playlists and Add track methods are reduced in
       complexity.
    
     - Pause Video when lost focus
    
       If the user is playing a Video stream, this is paused if he changes
       the focus to another Activity.
    
     - Controls properly setted
    
       Show the correct icon (Play / Pause) depending on the status of the
       stream.
    
     - Old code removed
    
       Removed code to show some old notifications about missing tracks.
    
     - Put Gst.Pipeline in Gst.State.NULL at exit
    
       This is required by gstreamer to be able to clean up the Player and
       free its memory. JukeboxActivity.can_close method is used for this.
    
     - Do not initialize video sink (videoscale, videoconvert,
       autovideosink, etc)
    
       This was causing some problems on performance to reproduce .ogv
       Video streams on XO-4.
    
    Signed-off-by: Manuel Kaufmann <humitos@gmail.com>
    ---
     activity.py | 553 +++++++++++++-----------------------------------------------
     controls.py | 174 +++++++++++++++++--
     player.py   |  77 ++-------
     playlist.py | 136 ++++++++++++---
     4 files changed, 401 insertions(+), 539 deletions(-)
    
    diff --git a/activity.py b/activity.py
    index 14fcbe9..54d4fa3 100644
    a b  
    2121
    2222import sys
    2323import logging
    24 import tempfile
    2524from gettext import gettext as _
    26 import os
    2725
    2826from sugar3.activity import activity
    29 from sugar3.graphics.objectchooser import ObjectChooser
    3027from sugar3 import mime
    3128from sugar3.datastore import datastore
    3229
    gi.require_version('Gst', '1.0') 
    4441from gi.repository import GObject
    4542from gi.repository import Gdk
    4643from gi.repository import Gtk
    47 from gi.repository import Gst
    4844from gi.repository import Gio
    4945
    50 import urllib
    5146from viewtoolbar import ViewToolbar
    5247from controls import Controls
    5348from player import GstPlayer
    PLAYLIST_WIDTH_PROP = 1.0 / 3 
    5853
    5954
    6055class JukeboxActivity(activity.Activity):
    61     UPDATE_INTERVAL = 500
     56
     57    __gsignals__ = {
     58        'no-stream': (GObject.SignalFlags.RUN_FIRST, None, []),
     59        }
    6260
    6361    def __init__(self, handle):
    6462        activity.Activity.__init__(self, handle)
    65         self._object_id = handle.object_id
    66         self.set_title(_('Jukebox Activity'))
     63
    6764        self.player = None
    68         self.max_participants = 1
    6965        self._playlist_jobject = None
    7066
     67        self.set_title(_('Jukebox Activity'))
     68        self.max_participants = 1
     69
    7170        toolbar_box = ToolbarBox()
    7271        activity_button = ActivityToolbarButton(self)
    7372        activity_toolbar = activity_button.page
    7473        toolbar_box.toolbar.insert(activity_button, 0)
    7574        self.title_entry = activity_toolbar.title
    7675
    77         # FIXME: I don't know what is the mission of this line
    78         # activity_toolbar.stop.hide()
    79 
    80         self.volume_monitor = Gio.VolumeMonitor.get()
    81         self.volume_monitor.connect('mount-added', self._mount_added_cb)
    82         self.volume_monitor.connect('mount-removed', self._mount_removed_cb)
    83 
    84         _view_toolbar = ViewToolbar()
    85         _view_toolbar.connect('go-fullscreen',
     76        view_toolbar = ViewToolbar()
     77        view_toolbar.connect('go-fullscreen',
    8678                              self.__go_fullscreen_cb)
    87         _view_toolbar.connect('toggle-playlist',
     79        view_toolbar.connect('toggle-playlist',
    8880                              self.__toggle_playlist_cb)
    8981        view_toolbar_button = ToolbarButton(
    90             page=_view_toolbar,
     82            page=view_toolbar,
    9183            icon_name='toolbar-view')
    92         _view_toolbar.show()
     84        view_toolbar.show()
    9385        toolbar_box.toolbar.insert(view_toolbar_button, -1)
    9486        view_toolbar_button.show()
    9587
    96         self.control = Controls(toolbar_box.toolbar, self)
     88        self.canvas = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
     89
     90        self.playlist_widget = PlayList()
     91        self.playlist_widget.connect('play-index', self.__play_index)
     92        self.playlist_widget.show()
     93        self.canvas.pack_start(self.playlist_widget, False, True, 0)
     94
     95        # Create the player just once
     96        self.player = GstPlayer()
     97        self.player.connect('eos', self.__player_eos_cb)
     98        self.player.connect('error', self.__player_error_cb)
     99        self.player.connect('play', self.__player_play_cb)
     100
     101        self.control = Controls(self, toolbar_box.toolbar)
    97102
    98103        toolbar_box.toolbar.insert(StopButton(self), -1)
    99104
    100105        self.set_toolbar_box(toolbar_box)
    101106        toolbar_box.show_all()
    102107
    103         self.connect("key_press_event", self._key_press_event_cb)
     108        self.connect('key_press_event', self.__key_press_event_cb)
    104109
    105110        # We want to be notified when the activity gets the focus or
    106         # loses it.  When it is not active, we don't need to keep
     111        # loses it. When it is not active, we don't need to keep
    107112        # reproducing the video
    108         self.connect("notify::active", self._notify_active_cb)
    109 
    110         # FIXME: this is related with shared activity and it doesn't work
    111         # if handle.uri:
    112         #     pass
    113         # elif self._shared_activity:
    114         #     if self.get_shared():
    115         #         pass
    116         #     else:
    117         #         # Wait for a successful join before trying to get the document
    118         #         self.connect("joined", self._joined_cb)
    119 
    120         self.update_id = -1
    121         self.changed_id = -1
    122         self.seek_timeout_id = -1
    123         self.player = None
    124         self.uri = None
    125 
    126         # {'url': 'file://.../media.ogg', 'title': 'My song', object_id: '..'}
    127         self.playlist = []
    128 
    129         self.jobjectlist = []
    130         self.playpath = None
    131         self.currentplaying = None
    132         self.playflag = False
    133         self._not_found_files = 0
    134 
    135         # README: I changed this because I was getting an error when I
    136         # tried to modify self.bin with something different than
    137         # Gtk.Bin
     113        self.connect('notify::active', self.__notify_active_cb)
    138114
    139         # self.bin = Gtk.HBox()
    140         # self.bin.show()
     115        self._empty_widget = Gtk.Label(label='')
    141116
    142         self.canvas = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
    143         self._alert = None
    144 
    145         self.playlist_widget = PlayList(self.play)
    146         self.playlist_widget.update(self.playlist)
    147         self.playlist_widget.show()
    148         self.canvas.pack_start(self.playlist_widget, False, True, 0)
    149         self._empty_widget = Gtk.Label(label="")
    150117        self._empty_widget.show()
    151118        self.videowidget = VideoWidget()
    152119        self.set_canvas(self.canvas)
    class JukeboxActivity(activity.Activity): 
    154121        self.show_all()
    155122        self.canvas.connect('size-allocate', self.__size_allocate_cb)
    156123
    157         #From ImageViewer Activity
    158         self._want_document = True
    159         if self._object_id is None:
    160             self._show_object_picker = GObject.timeout_add(1000, \
    161             self._show_picker_cb)
     124        self.player.init_view_area(self.videowidget)
    162125
     126        # Check if the activity was launched from a Journal Entry
    163127        if handle.uri:
    164             self.uri = handle.uri
    165             GObject.idle_add(self._start, self.uri, handle.title)
     128            self._start(handle.uri, handle.title)
    166129
    167         # Create the player just once
    168         logging.debug('Instantiating GstPlayer')
    169         self.player = GstPlayer(self.videowidget)
    170         self.player.connect("eos", self._player_eos_cb)
    171         self.player.connect("error", self._player_error_cb)
    172         self.p_position = Gst.CLOCK_TIME_NONE
    173         self.p_duration = Gst.CLOCK_TIME_NONE
    174 
    175     def _notify_active_cb(self, widget, event):
     130        self._volume_monitor = Gio.VolumeMonitor.get()
     131        self._volume_monitor.connect('mount-added', self._mount_added_cb)
     132        self._volume_monitor.connect('mount-removed', self._mount_removed_cb)
     133
     134    def __notify_active_cb(self, widget, event):
    176135        """Sugar notify us that the activity is becoming active or inactive.
    177136        When we are inactive, we stop the player if it is reproducing
    178137        a video.
    179138        """
    180         if self.player.player.props.uri is not None:
     139
     140        logging.debug('JukeboxActivity notify::active signal received')
     141
     142        if self.player.player.props.current_uri is not None and \
     143                self.player.playing_video():
    181144            if not self.player.is_playing() and self.props.active:
    182145                self.player.play()
    183146            if self.player.is_playing() and not self.props.active:
    class JukeboxActivity(activity.Activity): 
    213176        playlist_width = int(canvas_size.width * PLAYLIST_WIDTH_PROP)
    214177        self.playlist_widget.set_size_request(playlist_width, 0)
    215178
    216     def open_button_clicked_cb(self, widget):
    217         """ To open the dialog to select a new file"""
    218         #self.player.seek(0L)
    219         #self.player.stop()
    220         #self.playlist = []
    221         #self.playpath = None
    222         #self.currentplaying = None
    223         #self.playflag = False
    224         self._want_document = True
    225         self._show_object_picker = GObject.timeout_add(1, self._show_picker_cb)
    226 
    227     def _key_press_event_cb(self, widget, event):
     179    def __key_press_event_cb(self, widget, event):
    228180        keyname = Gdk.keyval_name(event.keyval)
    229         logging.info("Keyname Press: %s, time: %s", keyname, event.time)
     181
    230182        if self.title_entry.has_focus():
    231183            return False
    232184
    233         if keyname == "space":
    234             self.play_toggled()
     185        if keyname == 'space':
     186            self.control._button_clicked_cb(None)
    235187            return True
    236188
    237     def check_if_next_prev(self):
    238         if self.currentplaying == 0:
    239             self.control.prev_button.set_sensitive(False)
    240         else:
    241             self.control.prev_button.set_sensitive(True)
    242         if self.currentplaying == len(self.playlist) - 1:
    243             self.control.next_button.set_sensitive(False)
    244         else:
    245             self.control.next_button.set_sensitive(True)
    246 
    247189    def songchange(self, direction):
    248         #if self.playflag:
    249         #    self.playflag = False
    250         #    return
    251         self.player.seek(0L)
    252         if direction == "prev" and self.currentplaying > 0:
    253             self.play(self.currentplaying - 1)
    254             logging.info("prev: " + self.playlist[self.currentplaying]['url'])
    255             #self.playflag = True
    256         elif direction == "next" and \
    257                 self.currentplaying < len(self.playlist) - 1:
    258             self.play(self.currentplaying + 1)
    259             logging.info("next: " + self.playlist[self.currentplaying]['url'])
    260             #self.playflag = True
     190        current_playing = self.playlist_widget._current_playing
     191        if direction == 'prev' and  current_playing > 0:
     192            self.play_index(current_playing - 1)
     193        elif direction == 'next' and \
     194                self.playlist_widget._current_playing < len(self.playlist_widget._items) - 1:
     195            self.play_index(self.playlist_widget._current_playing + 1)
    261196        else:
    262             self.play_toggled()
    263             self.player.stop()
    264197            self._switch_canvas(show_video=False)
    265             self.player.set_uri(None)
    266             self.check_if_next_prev()
     198            self.emit('no-stream')
    267199
    268     def play(self, media_index):
     200    def play_index(self, index):
    269201        self._switch_canvas(show_video=True)
    270         self.currentplaying = media_index
    271         url = self.playlist[self.currentplaying]['url']
    272         error = None
    273         if url.startswith('journal://'):
    274             try:
    275                 jobject = datastore.get(url[len("journal://"):])
    276                 url = 'file://' + jobject.file_path
    277             except:
    278                 path = url[len("journal://"):]
    279                 error = _('The file %s was not found') % path
    280 
    281         self.check_if_next_prev()
    282 
    283         if error is None:
    284             self.player.set_uri(url)
    285             self.player.play()
    286         else:
    287             self.control.set_disabled()
    288             self._show_error_alert(error)
     202        self.playlist_widget._current_playing = index
    289203
    290         self.playlist_widget.set_cursor(self.currentplaying)
     204        path = self.playlist_widget._items[index]['path']
     205        self.control.check_if_next_prev()
    291206
    292     def _player_eos_cb(self, widget):
    293         self.songchange('next')
     207        self.player.set_uri(path)
     208        self.player.play()
    294209
    295     def _show_error_alert(self, title, msg=None):
    296         self._alert = ErrorAlert()
    297         self._alert.props.title = title
    298         if msg is not None:
    299             self._alert.props.msg = msg
    300         self.add_alert(self._alert)
    301         self._alert.connect('response', self._alert_cancel_cb)
    302         self._alert.show()
     210    def __play_index(self, widget, index, path):
     211        self._switch_canvas(show_video=True)
     212        self.playlist_widget._current_playing = index
     213
     214        self.control.check_if_next_prev()
     215
     216        self.player.set_uri(path)
     217        self.player.play()
     218
     219    def __player_eos_cb(self, widget):
     220        self.songchange('next')
    303221
    304222    def _mount_added_cb(self, volume_monitor, device):
     223        logging.debug('Mountpoint added. Checking...')
    305224        self.view_area.set_current_page(0)
    306         self.remove_alert(self._alert)
    307         self.playlist_widget.update(self.playlist)
     225        self.playlist_widget.update()
    308226
    309227    def _mount_removed_cb(self, volume_monitor, device):
     228        logging.debug('Mountpoint removed. Checking...')
    310229        self.view_area.set_current_page(0)
    311         self.remove_alert(self._alert)
    312         self.playlist_widget.update(self.playlist)
     230        self.playlist_widget.update()
    313231
    314     def _show_missing_tracks_alert(self, nro):
    315         self._alert = Alert()
    316         title = _('%s tracks not found.') % nro
     232    def _show_error_alert(self, title, msg=None):
     233        self._alert = ErrorAlert()
    317234        self._alert.props.title = title
    318         self._alert.add_button(Gtk.ResponseType.APPLY, _('Details'))
     235        if msg is not None:
     236            self._alert.props.msg = msg
    319237        self.add_alert(self._alert)
    320         self._alert.connect('response',
    321                 self.__missing_tracks_alert_response_cb)
    322 
    323     def __missing_tracks_alert_response_cb(self, alert, response_id):
    324         vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    325         vbox.props.valign = Gtk.Align.CENTER
    326         label = Gtk.Label(label='')
    327         label.set_markup(_('<b>Missing tracks</b>'))
    328         vbox.pack_start(label, False, False, 15)
    329 
    330         for track in self.playlist_widget.get_missing_tracks():
    331             path = track['url'].replace('journal://', '')\
    332                 .replace('file://', '')
    333             label = Gtk.Label(label=path)
    334             vbox.add(label)
     238        self._alert.connect('response', self._alert_cancel_cb)
     239        self._alert.show()
    335240
    336         _missing_tracks = Gtk.ScrolledWindow()
    337         _missing_tracks.add_with_viewport(vbox)
    338         _missing_tracks.show_all()
     241    def _alert_cancel_cb(self, alert, response_id):
     242        self.remove_alert(alert)
    339243
    340         self.view_area.append_page(_missing_tracks, None)
     244    def __player_play_cb(self, widget):
     245        # Do not show the visualization widget if we are playing just
     246        # an audio stream
    341247
    342         self.view_area.set_current_page(2)
    343         self.remove_alert(alert)
     248        def callback():
     249            if self.player.playing_video():
     250                self._switch_canvas(True)
     251            else:
     252                self._switch_canvas(False)
     253            return False
    344254
    345     def _alert_cancel_cb(self, alert, response_id):
    346         self.remove_alert(alert)
     255        # HACK: we need a timeout here because gstreamer returns
     256        # n-video = 0 if we call it immediately
     257        GObject.timeout_add(1000, callback)
    347258
    348     def _player_error_cb(self, widget, message, detail):
     259    def __player_error_cb(self, widget, message, detail):
    349260        self.player.stop()
    350         self.player.set_uri(None)
    351261        self.control.set_disabled()
    352262
    353         file_path = self.playlist[self.currentplaying]['url']\
    354             .replace('journal://', 'file://')
     263        logging.error('ERROR MESSAGE: %s', message)
     264        logging.error('ERROR DETAIL: %s', detail)
     265
     266        file_path = self.playlist_widget._items[self.playlist_widget._current_playing]['path']
    355267        mimetype = mime.get_for_file(file_path)
    356268
    357269        title = _('Error')
    class JukeboxActivity(activity.Activity): 
    359271        self._switch_canvas(False)
    360272        self._show_error_alert(title, msg)
    361273
    362     def _joined_cb(self, activity):
    363         logging.debug("someone joined")
    364         pass
    365 
    366     def _shared_cb(self, activity):
    367         logging.debug("shared start")
    368         pass
    369 
    370     def _show_picker_cb(self):
    371         #From ImageViewer Activity
    372         if not self._want_document:
    373             return
    374 
    375         # README: some arguments are deprecated so I avoid them
    376 
    377         # chooser = ObjectChooser(_('Choose document'), self,
    378         #     Gtk.DialogFlags.MODAL |
    379         #     Gtk.DialogFlags.DESTROY_WITH_PARENT,
    380         #     what_filter=mime.GENERIC_TYPE_AUDIO)
    381 
    382         chooser = ObjectChooser(self, what_filter=mime.GENERIC_TYPE_AUDIO)
    383 
    384         try:
    385             result = chooser.run()
    386             if result == Gtk.ResponseType.ACCEPT:
    387                 jobject = chooser.get_selected_object()
    388                 if jobject and jobject.file_path:
    389                     logging.error('Adding %s', jobject.file_path)
    390                     title = jobject.metadata.get('title', None)
    391                     self._load_file(jobject.file_path, title,
    392                             jobject.object_id)
    393         finally:
    394             #chooser.destroy()
    395             #del chooser
    396             pass
     274    def can_close(self):
     275        # We need to put the Gst.State in NULL so gstreamer can
     276        # cleanup the pipeline
     277        self.player.stop()
     278        return True
    397279
    398280    def read_file(self, file_path):
    399281        """Load a file from the datastore on activity start."""
    400282        logging.debug('JukeBoxAtivity.read_file: %s', file_path)
    401         title = self.metadata.get('title', None)
    402         self._load_file(file_path, title, self._object_id)
    403 
    404     def _load_file(self, file_path, title, object_id):
    405         self.uri = os.path.abspath(file_path)
    406         if os.path.islink(self.uri):
    407             self.uri = os.path.realpath(self.uri)
    408         mimetype = mime.get_for_file('file://' + file_path)
    409         logging.error('read_file mime %s', mimetype)
    410         if mimetype == 'audio/x-mpegurl':
    411             # is a M3U playlist:
    412             for uri in self._read_m3u_playlist(file_path):
    413                 if not self.playlist_widget.check_available_media(uri['url']):
    414                     self._not_found_files += 1
    415 
    416                 GObject.idle_add(self._start, uri['url'], uri['title'],
    417                         uri['object_id'])
    418         else:
    419             # is another media file:
    420             GObject.idle_add(self._start, self.uri, title, object_id)
    421 
    422         if self._not_found_files > 0:
    423             self._show_missing_tracks_alert(self._not_found_files)
    424 
    425     def _create_playlist_jobject(self):
    426         """Create an object in the Journal to store the playlist.
    427 
    428         This is needed if the activity was not started from a playlist
    429         or from scratch.
    430 
    431         """
    432         jobject = datastore.create()
    433         jobject.metadata['mime_type'] = "audio/x-mpegurl"
    434         jobject.metadata['title'] = _('Jukebox playlist')
    435 
    436         temp_path = os.path.join(activity.get_activity_root(),
    437                                  'instance')
    438         if not os.path.exists(temp_path):
    439             os.makedirs(temp_path)
    440 
    441         jobject.file_path = tempfile.mkstemp(dir=temp_path)[1]
    442         self._playlist_jobject = jobject
     283        title = self.metadata['title']
     284        self.playlist_widget.load_file(file_path, title)
    443285
    444286    def write_file(self, file_path):
    445287
    class JukeboxActivity(activity.Activity): 
    447289            """Open the file at file_path and write the playlist.
    448290
    449291            It is saved in audio/x-mpegurl format.
    450 
    451292            """
     293
    452294            list_file = open(file_path, 'w')
    453             for uri in self.playlist:
    454                 list_file.write('#EXTINF: %s\n' % uri['title'])
    455                 list_file.write('%s\n' % uri['url'])
     295            for uri in self.playlist_widget._items:
     296                list_file.write('#EXTINF:%s\n' % uri['title'])
     297                list_file.write('%s\n' % uri['path'])
    456298            list_file.close()
    457299
    458300        if not self.metadata['mime_type']:
    class JukeboxActivity(activity.Activity): 
    463305
    464306        else:
    465307            if self._playlist_jobject is None:
    466                 self._create_playlist_jobject()
     308                self._playlist_jobject = self.playlist_widget.create_playlist_jobject()
    467309
    468310            # Add the playlist to the playlist jobject description.
    469311            # This is only done if the activity was not started from a
    470312            # playlist or from scratch:
    471313            description = ''
    472             for uri in self.playlist:
     314            for uri in self.playlist_widget._items:
    473315                description += '%s\n' % uri['title']
    474316            self._playlist_jobject.metadata['description'] = description
    475317
    476318            write_playlist_to_file(self._playlist_jobject.file_path)
    477319            datastore.write(self._playlist_jobject)
    478320
    479     def _read_m3u_playlist(self, file_path):
    480         urls = []
    481         title = ''
    482         for line in open(file_path).readlines():
    483             line = line.strip()
    484             if line != '':
    485                 if line.startswith('#EXTINF:'):
    486                     # line with data
    487                     #EXTINF: title
    488                     title = line[len('#EXTINF:'):]
    489                 else:
    490                     uri = {}
    491                     uri['url'] = line.strip()
    492                     uri['title'] = title
    493                     if uri['url'].startswith('journal://'):
    494                         uri['object_id'] = uri['url'][len('journal://'):]
    495                     else:
    496                         uri['object_id'] = None
    497                     urls.append(uri)
    498                     title = ''
    499         return urls
    500 
    501     def _start(self, uri=None, title=None, object_id=None):
    502         self._want_document = False
    503         self.playpath = os.path.dirname(uri)
    504         if not uri:
    505             return False
    506 
    507         if title is not None:
    508             title = title.strip()
    509         if object_id is not None:
    510             self.playlist.append({'url': 'journal://' + object_id,
    511                     'title': title})
    512         else:
    513             if uri.startswith("file://"):
    514                 self.playlist.append({'url': uri, 'title': title})
    515             else:
    516                 uri = "file://" + urllib.quote(os.path.abspath(uri))
    517                 self.playlist.append({'url': uri, 'title': title})
    518 
    519         self.playlist_widget.update(self.playlist)
    520 
    521         try:
    522             if self.currentplaying is None:
    523                 logging.info("Playing: " + self.playlist[0]['url'])
    524                 url = self.playlist[0]['url']
    525                 if url.startswith('journal://'):
    526                     jobject = datastore.get(url[len("journal://"):])
    527                     url = 'file://' + jobject.file_path
    528 
    529                 self.player.set_uri(url)
    530                 self.player.play()
    531                 self.currentplaying = 0
    532                 self.play_toggled()
    533                 self.show_all()
    534             else:
    535                 pass
    536                 #self.player.seek(0L)
    537                 #self.player.stop()
    538                 #self.currentplaying += 1
    539                 #self.player.set_uri(self.playlist[self.currentplaying])
    540                 #self.play_toggled()
    541         except:
    542             pass
    543         self.check_if_next_prev()
    544         return False
    545 
    546     def play_toggled(self):
    547         self.control.set_enabled()
    548 
    549         if self.player.is_playing():
    550             self.player.pause()
    551             self.control.set_button_play()
    552         else:
    553             if self.player.error:
    554                 self.control.set_disabled()
    555             else:
    556                 self.player.play()
    557                 if self.update_id == -1:
    558                     self.update_id = GObject.timeout_add(self.UPDATE_INTERVAL,
    559                                                          self.update_scale_cb)
    560                 self.control.set_button_pause()
    561 
    562     def volume_changed_cb(self, widget, value):
    563         if self.player:
    564             self.player.player.set_property('volume', value)
    565 
    566     def scale_button_press_cb(self, widget, event):
    567         self.control.button.set_sensitive(False)
    568         self.was_playing = self.player.is_playing()
    569         if self.was_playing:
    570             self.player.pause()
    571 
    572         # don't timeout-update position during seek
    573         if self.update_id != -1:
    574             GObject.source_remove(self.update_id)
    575             self.update_id = -1
    576 
    577         # make sure we get changed notifies
    578         if self.changed_id == -1:
    579             self.changed_id = self.control.hscale.connect('value-changed',
    580                 self.scale_value_changed_cb)
    581 
    582     def scale_value_changed_cb(self, scale):
    583         # see seek.c:seek_cb
    584         real = long(scale.get_value() * self.p_duration / 100)  # in ns
    585         self.player.seek(real)
    586         # allow for a preroll
    587         self.player.get_state(timeout=50 * Gst.MSECOND)  # 50 ms
    588 
    589     def scale_button_release_cb(self, widget, event):
    590         # see seek.cstop_seek
    591         widget.disconnect(self.changed_id)
    592         self.changed_id = -1
    593 
    594         self.control.button.set_sensitive(True)
    595         if self.seek_timeout_id != -1:
    596             GObject.source_remove(self.seek_timeout_id)
    597             self.seek_timeout_id = -1
    598         else:
    599             if self.was_playing:
    600                 self.player.play()
    601 
    602         if self.update_id != -1:
    603             self.error('Had a previous update timeout id')
    604         else:
    605             self.update_id = GObject.timeout_add(self.UPDATE_INTERVAL,
    606                 self.update_scale_cb)
    607 
    608     def update_scale_cb(self):
    609         success, self.p_position, self.p_duration = \
    610             self.player.query_position()
    611 
    612         if not success:
    613             return True
    614 
    615         if self.p_position != Gst.CLOCK_TIME_NONE:
    616             value = self.p_position * 100.0 / self.p_duration
    617             self.control.adjustment.set_value(value)
    618 
    619             # Update the current time
    620             seconds = self.p_position * 10 ** -9
    621             time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60))
    622             self.control.current_time_label.set_text(time)
    623 
    624         # FIXME: this should be updated just once when the file starts
    625         # the first time
    626         if self.p_duration != Gst.CLOCK_TIME_NONE:
    627             seconds = self.p_duration * 10 ** -9
    628             time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60))
    629             self.control.total_time_label.set_text(time)
    630 
    631         return True
    632 
    633     def _erase_playlist_entry_clicked_cb(self, widget):
    634         self.playlist_widget.delete_selected_items()
    635 
    636321    def __go_fullscreen_cb(self, toolbar):
    637322        self.fullscreen()
    638323
    class JukeboxActivity(activity.Activity): 
    647332class VideoWidget(Gtk.DrawingArea):
    648333    def __init__(self):
    649334        GObject.GObject.__init__(self)
    650         self.set_events(Gdk.EventMask.POINTER_MOTION_MASK |
    651                         Gdk.EventMask.POINTER_MOTION_HINT_MASK |
    652                         Gdk.EventMask.EXPOSURE_MASK |
    653                         Gdk.EventMask.KEY_PRESS_MASK |
    654                         Gdk.EventMask.KEY_RELEASE_MASK)
     335        # self.set_events(Gdk.EventMask.POINTER_MOTION_MASK |
     336        #                 Gdk.EventMask.POINTER_MOTION_HINT_MASK |
     337        #                 Gdk.EventMask.EXPOSURE_MASK |
     338        #                 Gdk.EventMask.KEY_PRESS_MASK |
     339        #                 Gdk.EventMask.KEY_RELEASE_MASK)
    655340
    656341        self.set_app_paintable(True)
    657342        self.set_double_buffered(False)
  • controls.py

    diff --git a/controls.py b/controls.py
    index b84a55a..a5922e8 100644
    a b  
    1818import logging
    1919
    2020from gi.repository import Gtk
     21from gi.repository import Gst
    2122from gi.repository import GObject
    2223
    2324from gettext import gettext as _
    2425
     26from sugar3 import mime
    2527from sugar3.graphics.toolbutton import ToolButton
     28from sugar3.graphics.objectchooser import ObjectChooser
    2629
    2730
    2831class Controls(GObject.GObject):
    2932    """Class to create the Control (play, back, forward,
    3033    add, remove, etc) toolbar"""
    3134
    32     def __init__(self, toolbar, jukebox):
     35    SCALE_UPDATE_INTERVAL = 1000
     36
     37    def __init__(self, activity, toolbar):
    3338        GObject.GObject.__init__(self)
    3439
     40        self.activity = activity
    3541        self.toolbar = toolbar
    36         self.jukebox = jukebox
     42
     43        self._scale_update_id = -1
     44        self._scale_changed_id = -1
     45        self._seek_timeout_id = -1
    3746
    3847        self.open_button = ToolButton('list-add')
    3948        self.open_button.set_tooltip(_('Add track'))
    4049        self.open_button.show()
    41         self.open_button.connect('clicked', jukebox.open_button_clicked_cb)
     50        self.open_button.connect('clicked', self.__open_button_clicked_cb)
    4251        self.toolbar.insert(self.open_button, -1)
    4352
    4453        erase_playlist_entry_btn = ToolButton(icon_name='list-remove')
    4554        erase_playlist_entry_btn.set_tooltip(_('Remove track'))
    4655        erase_playlist_entry_btn.connect('clicked',
    47                  jukebox._erase_playlist_entry_clicked_cb)
     56                 self.__erase_playlist_entry_clicked_cb)
    4857        self.toolbar.insert(erase_playlist_entry_btn, -1)
    4958
    5059        spacer = Gtk.SeparatorToolItem()
    class Controls(GObject.GObject): 
    5463        self.prev_button = ToolButton('player_rew')
    5564        self.prev_button.set_tooltip(_('Previous'))
    5665        self.prev_button.show()
    57         self.prev_button.connect('clicked', self.prev_button_clicked_cb)
     66        self.prev_button.connect('clicked', self.__prev_button_clicked_cb)
    5867        self.toolbar.insert(self.prev_button, -1)
    5968
    6069        self.pause_image = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PAUSE,
    class Controls(GObject.GObject): 
    7584        self.next_button = ToolButton('player_fwd')
    7685        self.next_button.set_tooltip(_('Next'))
    7786        self.next_button.show()
    78         self.next_button.connect('clicked', self.next_button_clicked_cb)
     87        self.next_button.connect('clicked', self.__next_button_clicked_cb)
    7988        self.toolbar.insert(self.next_button, -1)
    8089
    8190        current_time = Gtk.ToolItem()
    8291        self.current_time_label = Gtk.Label(label='')
    8392        current_time.add(self.current_time_label)
    8493        current_time.show()
    85         toolbar.insert(current_time, -1)
     94        self.toolbar.insert(current_time, -1)
    8695
    8796        self.adjustment = Gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0)
    8897        self.hscale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL,
    class Controls(GObject.GObject): 
    93102        logging.debug("FIXME: AttributeError: 'Scale' object has no "
    94103                      "attribute 'set_update_policy'")
    95104        self.hscale.connect('button-press-event',
    96                 jukebox.scale_button_press_cb)
     105                self.__scale_button_press_cb)
    97106        self.hscale.connect('button-release-event',
    98                 jukebox.scale_button_release_cb)
     107                self.__scale_button_release_cb)
    99108
    100109        self.scale_item = Gtk.ToolItem()
    101110        self.scale_item.set_expand(True)
    class Controls(GObject.GObject): 
    106115        self.total_time_label = Gtk.Label(label='')
    107116        total_time.add(self.total_time_label)
    108117        total_time.show()
    109         toolbar.insert(total_time, -1)
     118        self.toolbar.insert(total_time, -1)
     119
     120        self.activity.connect('no-stream', self.__no_stream_cb)
     121        self.activity.player.connect('play', self.__player_play)
     122
     123    def __player_play(self, widget):
     124        if self._scale_update_id == -1:
     125            self._scale_update_id = GObject.timeout_add(
     126                self.SCALE_UPDATE_INTERVAL, self.__update_scale_cb)
     127        self.set_enabled()
     128        self.set_button_pause()
     129
     130    def __open_button_clicked_cb(self, widget):
     131        self.__show_picker_cb()
     132
     133    def __erase_playlist_entry_clicked_cb(self, widget):
     134        self.activity.playlist_widget.delete_selected_items()
     135
     136    def __show_picker_cb(self):
     137        jobject = None
     138        chooser = ObjectChooser(self.activity,
     139                                what_filter=mime.GENERIC_TYPE_AUDIO)
    110140
    111     def prev_button_clicked_cb(self, widget):
    112         self.jukebox.songchange('prev')
     141        try:
     142            result = chooser.run()
     143            if result == Gtk.ResponseType.ACCEPT:
     144                jobject = chooser.get_selected_object()
     145                if jobject and jobject.file_path:
     146                    logging.info('Adding %s', jobject.file_path)
     147                    self.activity.playlist_widget.load_file(jobject)
     148                    self.check_if_next_prev()
     149        finally:
     150            if jobject is not None:
     151                jobject.destroy()
    113152
    114     def next_button_clicked_cb(self, widget):
    115         self.jukebox.songchange('next')
     153    def __prev_button_clicked_cb(self, widget):
     154        self.activity.songchange('prev')
     155
     156    def __next_button_clicked_cb(self, widget):
     157        self.activity.songchange('next')
     158
     159    def check_if_next_prev(self):
     160        current_playing = self.activity.playlist_widget._current_playing
     161        if current_playing == 0:
     162            self.prev_button.set_sensitive(False)
     163        else:
     164            self.prev_button.set_sensitive(True)
     165
     166        if current_playing == len(self.activity.playlist_widget._items) - 1:
     167            self.next_button.set_sensitive(False)
     168        else:
     169            self.next_button.set_sensitive(True)
    116170
    117171    def _button_clicked_cb(self, widget):
    118         self.jukebox.play_toggled()
     172        self.set_enabled()
     173
     174        if self.activity.player.is_playing():
     175            self.activity.player.pause()
     176            self.set_button_play()
     177            GObject.source_remove(self._scale_update_id)
     178        else:
     179            if self.activity.player.error:
     180                self.set_disabled()
     181            else:
     182                self.activity.player.play()
     183                self.activity._switch_canvas(True)
     184                self._scale_update_id = GObject.timeout_add(
     185                    self.SCALE_UPDATE_INTERVAL, self.__update_scale_cb)
    119186
    120187    def set_button_play(self):
    121188        self.button.set_icon_widget(self.play_image)
    class Controls(GObject.GObject): 
    132199        self.button.set_sensitive(True)
    133200        self.scale_item.set_sensitive(True)
    134201        self.hscale.set_sensitive(True)
     202
     203    def __scale_button_press_cb(self, widget, event):
     204        self.button.set_sensitive(False)
     205        self.was_playing = self.activity.player.is_playing()
     206        if self.was_playing:
     207            self.activity.player.pause()
     208
     209        # don't timeout-update position during seek
     210        if self._scale_update_id != -1:
     211            GObject.source_remove(self._scale_update_id)
     212            self._scale_update_id = -1
     213
     214        # make sure we get changed notifies
     215        if self._scale_changed_id == -1:
     216            self._scale_changed_id = self.hscale.connect('value-changed',
     217                self.__scale_value_changed_cb)
     218
     219    def __scale_value_changed_cb(self, scale):
     220        # see seek.c:seek_cb
     221        real = long(scale.get_value() * self.p_duration / 100)  # in ns
     222        self.activity.player.seek(real)
     223        # allow for a preroll
     224        self.activity.player.get_state(timeout=50 * Gst.MSECOND)  # 50 ms
     225
     226    def __scale_button_release_cb(self, widget, event):
     227        # see seek.cstop_seek
     228        widget.disconnect(self._scale_changed_id)
     229        self._scale_changed_id = -1
     230
     231        self.button.set_sensitive(True)
     232        if self._seek_timeout_id != -1:
     233            GObject.source_remove(self._seek_timeout_id)
     234            self._seek_timeout_id = -1
     235        else:
     236            if self.was_playing:
     237                self.activity.player.play()
     238
     239        if self._scale_update_id != -1:
     240            # self.error('Had a previous update timeout id')
     241            pass
     242        else:
     243            self._scale_update_id = GObject.timeout_add(
     244                self.SCALE_UPDATE_INTERVAL, self.__update_scale_cb)
     245
     246    def __update_scale_cb(self):
     247        success, self.p_position, self.p_duration = \
     248            self.activity.player.query_position()
     249
     250        if not success:
     251            return True
     252
     253        if self.p_position != Gst.CLOCK_TIME_NONE:
     254            value = self.p_position * 100.0 / self.p_duration
     255            self.adjustment.set_value(value)
     256
     257            # Update the current time
     258            seconds = self.p_position * 10 ** -9
     259            time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60))
     260            self.current_time_label.set_text(time)
     261
     262        # FIXME: this should be updated just once when the file starts
     263        # the first time
     264        if self.p_duration != Gst.CLOCK_TIME_NONE:
     265            seconds = self.p_duration * 10 ** -9
     266            time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60))
     267            self.total_time_label.set_text(time)
     268
     269        return True
     270
     271    def __no_stream_cb(self, widget):
     272        self.activity.player.stop()
     273        self.set_button_play()
     274        self.check_if_next_prev()
     275
     276        self.adjustment.set_value(0)
     277        self.current_time_label.set_text('')
     278        self.total_time_label.set_text('')
  • player.py

    diff --git a/player.py b/player.py
    index b5b03da..7ddaeaa 100644
    a b class GstPlayer(GObject.GObject): 
    3737    __gsignals__ = {
    3838        'error': (GObject.SignalFlags.RUN_FIRST, None, [str, str]),
    3939        'eos': (GObject.SignalFlags.RUN_FIRST, None, []),
     40        'play': (GObject.SignalFlags.RUN_FIRST, None, []),
    4041    }
    4142
    42     def __init__(self, videowidget):
     43    def __init__(self):
    4344        GObject.GObject.__init__(self)
    4445
    4546        self.playing = False
    class GstPlayer(GObject.GObject): 
    6263        self.player = Gst.ElementFactory.make('playbin', None)
    6364        self.pipeline.add(self.player)
    6465
    65         # Set the proper flags to render the vis-plugin
    66         GST_PLAY_FLAG_VIS = 1 << 3
    67         GST_PLAY_FLAG_TEXT = 1 << 2
    68         self.player.props.flags |= GST_PLAY_FLAG_VIS
    69         self.player.props.flags |= GST_PLAY_FLAG_TEXT
    70 
    71         r = Gst.Registry.get()
    72         l = [x for x in r.get_feature_list(Gst.ElementFactory)
    73              if (x.get_metadata('klass') == "Visualization")]
    74         if len(l):
    75             e = l.pop()  # take latest plugin in the list
    76             vis_plug = Gst.ElementFactory.make(e.get_name(), e.get_name())
    77             self.player.set_property('vis-plugin', vis_plug)
    78 
    79         self.overlay = None
     66    def init_view_area(self, videowidget):
    8067        videowidget.realize()
    8168        self.videowidget = videowidget
    8269        self.videowidget_xid = videowidget.get_window().get_xid()
    83         self._init_video_sink()
    8470
    8571    def __on_error_message(self, bus, msg):
    8672        self.stop()
    class GstPlayer(GObject.GObject): 
    10086
    10187    def set_uri(self, uri):
    10288        self.pipeline.set_state(Gst.State.READY)
    103         logging.debug('### Setting URI: %s', uri)
     89        # gstreamer needs the 'file://' prefix
     90        uri = 'file://' + uri
     91        logging.debug('URI: %s', uri)
    10492        self.player.set_property('uri', uri)
    10593
    106     def _init_video_sink(self):
    107         self.bin = Gst.Bin()
    108         videoscale = Gst.ElementFactory.make('videoscale', 'videoscale')
    109         self.bin.add(videoscale)
    110         pad = videoscale.get_static_pad("sink")
    111         ghostpad = Gst.GhostPad.new("sink", pad)
    112         self.bin.add_pad(ghostpad)
    113         videoscale.set_property("method", 0)
    114 
    115         textoverlay = Gst.ElementFactory.make('textoverlay', 'textoverlay')
    116         self.overlay = textoverlay
    117         self.bin.add(textoverlay)
    118         conv = Gst.ElementFactory.make("videoconvert", "conv")
    119         self.bin.add(conv)
    120         videosink = Gst.ElementFactory.make('autovideosink', 'autovideosink')
    121         self.bin.add(videosink)
    122 
    123         videoscale.link(textoverlay)
    124         textoverlay.link(conv)
    125         conv.link(videosink)
    126 
    127         self.player.set_property("video-sink", self.bin)
    128 
    129     def set_overlay(self, title, artist, album):
    130         text = "%s\n%s" % (title, artist)
    131         if album and len(album):
    132             text += "\n%s" % album
    133         self.overlay.set_property("text", text)
    134         self.overlay.set_property("font-desc", "sans bold 14")
    135         self.overlay.set_property("halignment", "right")
    136         self.overlay.set_property("valignment", "bottom")
    137         try:
    138             # Only in OLPC versions of gstreamer-plugins-base for now
    139             self.overlay.set_property("line-align", "left")
    140         except:
    141             pass
    142 
    14394    def query_position(self):
    14495        "Returns a (position, duration) tuple"
    14596
    class GstPlayer(GObject.GObject): 
    149100        return (p_success and d_success, position, duration)
    150101
    151102    def seek(self, location):
    152         """
    153         @param location: time to seek to, in nanoseconds
    154         """
     103        # Time to seek to, in nanoseconds
    155104
    156         logging.debug('Seek: %s ns', location)
     105        logging.debug('SEEK: %s ns', location)
    157106
    158107        self.pipeline.seek_simple(Gst.Format.TIME,
    159108                                  Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
    160109                                  location)
    161110
    162111    def pause(self):
    163         logging.debug("pausing player")
     112        logging.debug('Pausing player')
    164113        self.pipeline.set_state(Gst.State.PAUSED)
    165114        self.playing = False
    166115
    167116    def play(self):
    168         logging.debug("playing player")
     117        logging.debug('Playing player')
    169118        self.pipeline.set_state(Gst.State.PLAYING)
    170119        self.playing = True
    171120        self.error = False
     121        self.emit('play')
    172122
    173123    def stop(self):
    174124        self.playing = False
    175125        self.pipeline.set_state(Gst.State.NULL)
    176         logging.debug("stopped player")
     126        logging.debug('Stopped player')
    177127
    178128    def get_state(self, timeout=1):
    179129        return self.player.get_state(timeout=timeout)
    180130
    181131    def is_playing(self):
    182132        return self.playing
     133
     134    def playing_video(self):
     135        return self.player.props.n_video > 0
  • playlist.py

    diff --git a/playlist.py b/playlist.py
    index 4563d47..0587ab2 100644
    a b  
    1313# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
    1414# USA
    1515
    16 import logging
    1716import os
     17import logging
     18import tempfile
    1819from gettext import gettext as _
    1920
    20 import gi
    21 gi.require_version('Gtk', '3.0')
    22 
    2321from gi.repository import GObject
    2422from gi.repository import Gtk
    2523from gi.repository import Pango
    2624
     25from sugar3 import mime
     26from sugar3.datastore import datastore
     27from sugar3.activity import activity
    2728from sugar3.graphics.icon import CellRendererIcon
    2829
    2930
    30 COLUMNS_NAME = ('index', 'media', 'available')
     31COLUMNS_NAME = ('index', 'title', 'available')
    3132COLUMNS = dict((name, i) for i, name in enumerate(COLUMNS_NAME))
    3233
    3334
    3435class PlayList(Gtk.ScrolledWindow):
    35     def __init__(self, play_callback):
    36         self._playlist = None
    37         self._play_callback = play_callback
     36    __gsignals__ = {
     37        'play-index': (GObject.SignalFlags.RUN_FIRST, None, [int, str]),
     38        }
     39
     40    def __init__(self):
     41        self._not_found_files = 0
     42        self._current_playing = None
     43        self._items = []
    3844
    3945        GObject.GObject.__init__(self, hadjustment=None,
    40                                     vadjustment=None)
     46                                 vadjustment=None)
    4147        self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
    4248        self.listview = Gtk.TreeView()
    4349        self.treemodel = Gtk.ListStore(int, object, bool)
    class PlayList(Gtk.ScrolledWindow): 
    6369
    6470        renderer_title = Gtk.CellRendererText()
    6571        renderer_title.set_property('ellipsize', Pango.EllipsizeMode.END)
    66         treecol_title = Gtk.TreeViewColumn(_('Play List'))
     72        treecol_title = Gtk.TreeViewColumn(_('Track'))
    6773        treecol_title.pack_start(renderer_title, True)
    6874        treecol_title.set_cell_data_func(renderer_title, self._set_title)
    6975        self.listview.append_column(treecol_title)
    class PlayList(Gtk.ScrolledWindow): 
    7985        model = treeview.get_model()
    8086
    8187        treeiter = model.get_iter(path)
    82         media_idx = model.get_value(treeiter, COLUMNS['index'])
    83         self._play_callback(media_idx)
     88        index = model.get_value(treeiter, COLUMNS['index'])
     89        # TODO: put the path inside the ListStore
     90        path = self._items[index]['path']
     91        # TODO: check if the stream is available before emitting the signal
     92        self._current_playing = index
     93        self.set_cursor(index)
     94        self.emit('play-index', index, path)
    8495
    8596    def _set_number(self, column, cell, model, it, data):
    8697        idx = model.get_value(it, COLUMNS['index'])
    8798        cell.set_property('text', idx + 1)
    8899
    89100    def _set_title(self, column, cell, model, it, data):
    90         playlist_item = model.get_value(it, COLUMNS['media'])
     101        title = model.get_value(it, COLUMNS['title'])
    91102        available = model.get_value(it, COLUMNS['available'])
    92103
    93         cell.set_property('text', playlist_item['title'])
     104        cell.set_property('text', title)
    94105        sensitive = True
    95106        if not available:
    96107            sensitive = False
    class PlayList(Gtk.ScrolledWindow): 
    100111        available = model.get_value(it, COLUMNS['available'])
    101112        cell.set_property('visible', not available)
    102113
    103     def update(self, playlist):
    104         self.treemodel.clear()
    105         self._playlist = playlist
    106         pl = list(enumerate(playlist))
    107         for i, media in pl:
    108             available = self.check_available_media(media['url'])
    109             media['available'] = available
    110             self.treemodel.append((i, media, available))
    111         #self.set_cursor(0)
    112 
    113114    def set_cursor(self, index):
    114115        self.listview.set_cursor((index,))
    115116
    class PlayList(Gtk.ScrolledWindow): 
    118119        sel_model, sel_rows = self.listview.get_selection().get_selected_rows()
    119120        for row in sel_rows:
    120121            index = sel_model.get_value(sel_model.get_iter(row), 0)
    121             self._playlist.pop(index)
     122            self._items.pop(index)
    122123            self.treemodel.remove(self.treemodel.get_iter(row))
    123         self.update(self._playlist)
    124124
    125     def check_available_media(self, uri):
    126         path = uri.replace('journal://', '').replace('file://', '')
     125    def check_available_media(self, path):
    127126        if os.path.exists(path):
    128127            return True
    129128        else:
    class PlayList(Gtk.ScrolledWindow): 
    135134            if not track['available']:
    136135                missing_tracks.append(track)
    137136        return missing_tracks
     137
     138    def _load_m3u_playlist(self, file_path):
     139        for uri in self._read_m3u_playlist(file_path):
     140            if not self.check_available_media(uri['path']):
     141                self._not_found_files += 1
     142
     143            self._add_track(uri['path'], uri['title'])
     144
     145    def _load_stream(self, file_path, title=None):
     146        # TODO: read id3 here
     147        if os.path.islink(file_path):
     148            file_path = os.path.realpath(file_path)
     149        self._add_track(file_path, title)
     150
     151    def load_file(self, jobject, title=None):
     152        logging.debug('#### jobject: %s', type(jobject))
     153        if isinstance(jobject, datastore.RawObject) or \
     154           isinstance(jobject, datastore.DSObject):
     155            file_path = jobject.file_path
     156            title = jobject.metadata['title']
     157        else:
     158            file_path = jobject
     159
     160        mimetype = mime.get_for_file('file://' + file_path)
     161        logging.info('read_file mime %s', mimetype)
     162        if mimetype == 'audio/x-mpegurl':
     163            # is a M3U playlist:
     164            self._load_m3u_playlist(file_path)
     165        else:
     166            # is not a M3U playlist
     167            self._load_stream(file_path, title)
     168
     169    def update(self):
     170        for tree_item, playlist_item in zip(self.treemodel, self._items):
     171            tree_item[2] = self.check_available_media(playlist_item['path'])
     172
     173    def _add_track(self, file_path, title):
     174        available = self.check_available_media(file_path)
     175        item = {'path': file_path,
     176                'title': title,
     177                'available': available}
     178        self._items.append(item)
     179        index = len(self._items) - 1
     180        self.treemodel.append((index, item['title'], available))
     181
     182    def _read_m3u_playlist(self, file_path):
     183        urls = []
     184        title = ''
     185        for line in open(file_path).readlines():
     186            line = line.strip()
     187            if line != '':
     188                if line.startswith('#EXTINF:'):
     189                    # line with data
     190                    #EXTINF: title
     191                    title = line[len('#EXTINF:'):]
     192                else:
     193                    uri = {}
     194                    uri['path'] = line.strip()
     195                    uri['title'] = title
     196                    urls.append(uri)
     197                    title = ''
     198        return urls
     199
     200    def create_playlist_jobject(self):
     201        """Create an object in the Journal to store the playlist.
     202
     203        This is needed if the activity was not started from a playlist
     204        or from scratch.
     205        """
     206
     207        jobject = datastore.create()
     208        jobject.metadata['mime_type'] = "audio/x-mpegurl"
     209        jobject.metadata['title'] = _('Jukebox playlist')
     210
     211        temp_path = os.path.join(activity.get_activity_root(),
     212                                 'instance')
     213        if not os.path.exists(temp_path):
     214            os.makedirs(temp_path)
     215
     216        jobject.file_path = tempfile.mkstemp(dir=temp_path)[1]
     217        return jobject