~cdar07/ubuntu/quantal/totem/fix-for-954952

« back to all changes in this revision

Viewing changes to src/plugins/iplayer/iplayer.py

  • Committer: Package Import Robot
  • Author(s): Jeremy Bicha, Rico Tzschichholz, Jeremy Bicha
  • Date: 2012-01-09 16:19:39 UTC
  • mfrom: (1.11.14)
  • Revision ID: package-import@ubuntu.com-20120109161939-wfwd46cy3ytl1qq3
Tags: 3.3.4-0ubuntu1~precise1
[ Rico Tzschichholz ]
* New upstream release

[ Jeremy Bicha]
* debian/watch: Watch for .xz tarballs

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# -*- coding: utf-8 -*-
2
2
 
3
3
import gettext
4
 
from gi.repository import GObject
5
 
from gi.repository import Peas
6
 
from gi.repository import Gtk
7
 
from gi.repository import Totem
 
4
from gi.repository import GObject, Peas, Totem # pylint: disable-msg=E0611
8
5
import iplayer2
9
6
import threading
10
7
 
11
 
gettext.textdomain("totem")
 
8
gettext.textdomain ("totem")
12
9
 
13
10
D_ = gettext.dgettext
14
11
_ = gettext.gettext
15
12
 
16
13
class IplayerPlugin (GObject.Object, Peas.Activatable):
17
 
        __gtype_name__ = 'IplayerPlugin'
18
 
 
19
 
        object = GObject.property(type = GObject.Object)
20
 
 
21
 
        def __init__ (self):
22
 
                GObject.Object.__init__ (self)
23
 
 
24
 
                self.debug = False
25
 
                self.totem = None
26
 
                self.programme_download_lock = threading.Lock ()
27
 
 
28
 
        def do_activate (self):
29
 
                self.totem = self.object
30
 
                # Build the interface
31
 
                builder = Totem.plugin_load_interface ("iplayer", "iplayer.ui", True, self.totem.get_main_window (), self)
32
 
                container = builder.get_object ('iplayer_vbox')
33
 
 
34
 
                self.tv_tree_store = builder.get_object ('iplayer_programme_store')
35
 
                programme_list = builder.get_object ('iplayer_programme_list')
36
 
                programme_list.connect ('row-expanded', self._row_expanded_cb)
37
 
                programme_list.connect ('row-activated', self._row_activated_cb)
38
 
 
39
 
                container.show_all ()
40
 
 
41
 
                self.tv = iplayer2.feed ('tv')
42
 
 
43
 
                # Add the interface to Totem's sidebar
44
 
                self.totem.add_sidebar_page ("iplayer", _(u"BBC iPlayer"), container)
45
 
 
46
 
                # Get the channel category listings
47
 
                self.populate_channel_list (self.tv, self.tv_tree_store)
48
 
 
49
 
        def do_deactivate (self):
50
 
                self.totem.remove_sidebar_page ("iplayer")
51
 
 
52
 
        def populate_channel_list (self, feed, tree_store):
53
 
                if self.debug:
54
 
                        print "Populating channel list…"
55
 
 
56
 
                # Add all the channels as top-level rows in the tree store
57
 
                channels = feed.channels ()
58
 
                for channel_id, title in channels.items ():
59
 
                        parent_iter = tree_store.append (None, (title, channel_id, None))
60
 
 
61
 
                # Add the channels' categories in a thread, since they each require a network request
62
 
                parent_path = tree_store.get_path (parent_iter)
63
 
                thread = PopulateChannelsThread (self, parent_path, feed, tree_store)
64
 
                thread.start ()
65
 
 
66
 
        def _populate_channel_list_cb (self, tree_store, parent_path, values):
67
 
                # Callback from PopulateChannelsThread to add stuff to the tree store
68
 
                if values == None:
69
 
                        self.totem.action_error (_(u'Error listing channel categories'), _(u'There was an unknown error getting the list of television channels available on BBC iPlayer.'))
70
 
                        return False
71
 
 
72
 
                parent_iter = tree_store.get_iter (parent_path)
73
 
                category_iter = tree_store.append (parent_iter, values)
74
 
 
75
 
                # Append a dummy child row so that the expander's visible; we can
76
 
                # then queue off the expander to load the programme listing for this category
77
 
                tree_store.append (category_iter, [_(u'Loading…'), None, None])
78
 
 
79
 
                return False
80
 
 
81
 
        def _row_expanded_cb (self, tree_view, row_iter, path):
82
 
                tree_model = tree_view.get_model ()
83
 
 
84
 
                if self.debug:
85
 
                        print "_row_expanded_cb called."
86
 
 
87
 
                # We only care about the category level (level 1), and only when
88
 
                # it has the "Loading..." placeholder child row
89
 
                if get_iter_level (tree_model, row_iter) != 1 or tree_model.iter_n_children (row_iter) != 1:
90
 
                        return
91
 
 
92
 
                # Populate it with programmes asynchronously
93
 
                self.populate_programme_list (self.tv, tree_model, row_iter)
94
 
 
95
 
        def _row_activated_cb (self, tree_view, path, view_column):
96
 
                tree_store = tree_view.get_model ()
97
 
                tree_iter = tree_store.get_iter (path)
98
 
                if tree_iter == None:
99
 
                        return
100
 
 
101
 
                mrl = tree_store.get_value (tree_iter, 2)
102
 
 
103
 
                # Only allow programme rows to be activated, not channel or category rows
104
 
                if mrl == None:
105
 
                        return
106
 
 
107
 
                # Add the programme to the playlist and play it
108
 
                self.totem.add_to_playlist_and_play (mrl, tree_store.get_value (tree_iter, 0), True)
109
 
 
110
 
        def populate_programme_list (self, feed, tree_store, category_iter):
111
 
                if self.debug:
112
 
                        print "Populating programme list…"
113
 
 
114
 
                category_path = tree_store.get_path (category_iter)
115
 
                thread = PopulateProgrammesThread (self, feed, tree_store, category_path)
116
 
                thread.start ()
117
 
 
118
 
        def _populate_programme_list_cb (self, tree_store, category_path, values, remove_placeholder):
119
 
                # Callback from PopulateProgrammesThread to add stuff to the tree store
120
 
                if values == None:
121
 
                        # Translators: the "programme feed" is the list of TV shows available to watch online
122
 
                        self.totem.action_error (_(u'Error getting programme feed'), _(u'There was an error getting the list of programmes for this channel and category combination.'))
123
 
                        return False
124
 
 
125
 
                category_iter = tree_store.get_iter (category_path)
126
 
                if category_iter != None:
127
 
                        tree_store.append (category_iter, values)
128
 
 
129
 
                # Remove the placeholder row
130
 
                children = tree_store.iter_children (category_iter)
131
 
                if remove_placeholder and children != None:
132
 
                        tree_store.remove (children)
133
 
 
134
 
                return False
 
14
    __gtype_name__ = 'IplayerPlugin'
 
15
 
 
16
    object = GObject.property (type = GObject.Object)
 
17
 
 
18
    def __init__ (self):
 
19
        GObject.Object.__init__ (self)
 
20
 
 
21
        self.debug = False
 
22
        self.totem = None
 
23
        self.programme_download_lock = threading.Lock ()
 
24
 
 
25
        self.tv_feed = None
 
26
        self.tv_tree_store = None
 
27
 
 
28
    def do_activate (self):
 
29
        self.totem = self.object
 
30
        # Build the interface
 
31
        builder = Totem.plugin_load_interface ("iplayer", "iplayer.ui", True,
 
32
                                               self.totem.get_main_window (),
 
33
                                               self)
 
34
        container = builder.get_object ('iplayer_vbox')
 
35
 
 
36
        self.tv_tree_store = builder.get_object ('iplayer_programme_store')
 
37
        programme_list = builder.get_object ('iplayer_programme_list')
 
38
        programme_list.connect ('row-expanded', self.__row_expanded_cb)
 
39
        programme_list.connect ('row-activated', self.__row_activated_cb)
 
40
 
 
41
        container.show_all ()
 
42
 
 
43
        self.tv_feed = iplayer2.Feed ('tv')
 
44
 
 
45
        # Add the interface to Totem's sidebar
 
46
        self.totem.add_sidebar_page ("iplayer", _(u"BBC iPlayer"), container)
 
47
 
 
48
        # Get the channel category listings
 
49
        self._populate_channel_list (self.tv_feed, self.tv_tree_store)
 
50
 
 
51
    def do_deactivate (self):
 
52
        self.totem.remove_sidebar_page ("iplayer")
 
53
 
 
54
    def _populate_channel_list (self, feed, tree_store):
 
55
        if self.debug:
 
56
            print "Populating channel list…"
 
57
 
 
58
        # Add all the channels as top-level rows in the tree store
 
59
        channels = feed.channels ()
 
60
        for channel_id, title in channels.items ():
 
61
            tree_store.append (None, (title, channel_id, None))
 
62
 
 
63
        # Add the channels' categories in a thread, since they each require a
 
64
        # network request
 
65
        thread = PopulateChannelsThread (self.__populate_channel_list_cb,
 
66
                                         feed, tree_store)
 
67
        thread.start ()
 
68
 
 
69
    def __populate_channel_list_cb (self, tree_store, parent_path, values):
 
70
        # Callback from PopulateChannelsThread to add stuff to the tree store
 
71
        if values == None:
 
72
            self.totem.action_error (_(u'Error listing channel categories'),
 
73
                                     _(u'There was an unknown error getting '\
 
74
                                        'the list of television channels '\
 
75
                                        'available on BBC iPlayer.'))
 
76
            return False
 
77
 
 
78
        parent_iter = tree_store.get_iter (parent_path)
 
79
        category_iter = tree_store.append (parent_iter, values)
 
80
 
 
81
        # Append a dummy child row so that the expander's visible; we can
 
82
        # then queue off the expander to load the programme listing for this
 
83
        # category
 
84
        tree_store.append (category_iter, [_(u'Loading…'), None, None])
 
85
 
 
86
        return False
 
87
 
 
88
    def __row_expanded_cb (self, tree_view, row_iter, path):
 
89
        tree_model = tree_view.get_model ()
 
90
 
 
91
        if self.debug:
 
92
            print "__row_expanded_cb called."
 
93
 
 
94
        # We only care about the category level (level 1), and only when
 
95
        # it has the "Loading..." placeholder child row
 
96
        if (get_iter_level (tree_model, row_iter) != 1 or
 
97
            tree_model.iter_n_children (row_iter) != 1):
 
98
            return
 
99
 
 
100
        # Populate it with programmes asynchronously
 
101
        self._populate_programme_list (self.tv_feed, tree_model, row_iter)
 
102
 
 
103
    def __row_activated_cb (self, tree_view, path, view_column):
 
104
        tree_store = tree_view.get_model ()
 
105
        tree_iter = tree_store.get_iter (path)
 
106
        if tree_iter == None:
 
107
            return
 
108
 
 
109
        mrl = tree_store.get_value (tree_iter, 2)
 
110
 
 
111
        # Only allow programme rows to be activated, not channel or category
 
112
        # rows
 
113
        if mrl == None:
 
114
            return
 
115
 
 
116
        # Add the programme to the playlist and play it
 
117
        title = tree_store.get_value (tree_iter, 0)
 
118
        self.totem.add_to_playlist_and_play (mrl, title, True)
 
119
 
 
120
    def _populate_programme_list (self, feed, tree_store, category_iter):
 
121
        if self.debug:
 
122
            print "Populating programme list…"
 
123
 
 
124
        category_path = tree_store.get_path (category_iter)
 
125
        thread = PopulateProgrammesThread (self.programme_download_lock,
 
126
                                           self.__populate_programme_list_cb,
 
127
                                           feed, (tree_store, category_path))
 
128
        thread.start ()
 
129
 
 
130
    def __populate_programme_list_cb (self, tree_store, category_path, values,
 
131
                                     remove_placeholder):
 
132
        # Callback from PopulateProgrammesThread to add stuff to the tree store
 
133
        if values == None:
 
134
            # Translators: the "programme feed" is the list of TV shows
 
135
            # available to watch online
 
136
            self.totem.action_error (_(u'Error getting programme feed'),
 
137
                                     _(u'There was an error getting the list '\
 
138
                                        'of programmes for this channel and '\
 
139
                                        'category combination.'))
 
140
            return False
 
141
 
 
142
        category_iter = tree_store.get_iter (category_path)
 
143
        if category_iter != None:
 
144
            tree_store.append (category_iter, values)
 
145
 
 
146
        # Remove the placeholder row
 
147
        children = tree_store.iter_children (category_iter)
 
148
        if remove_placeholder and children != None:
 
149
            tree_store.remove (children)
 
150
 
 
151
        return False
135
152
 
136
153
def get_iter_level (tree_model, tree_iter):
137
 
        i = 0;
138
 
        while True:
139
 
                tree_iter = tree_model.iter_parent (tree_iter)
140
 
                if tree_iter == None:
141
 
                        break
142
 
                i += 1
143
 
        return i
 
154
    i = 0
 
155
    while True:
 
156
        tree_iter = tree_model.iter_parent (tree_iter)
 
157
        if tree_iter == None:
 
158
            break
 
159
        i += 1
 
160
    return i
144
161
 
145
162
def category_name_to_id (category_name):
146
 
        return category_name.lower ().replace (' ', '_').replace ('&', 'and')
 
163
    return category_name.lower ().replace (' ', '_').replace ('&', 'and')
147
164
 
148
165
class PopulateChannelsThread (threading.Thread):
149
 
        # Class to populate the channel list from the Internet
150
 
        def __init__ (self, plugin, parent_path, feed, tree_model):
151
 
                self.plugin = plugin
152
 
                self.feed = feed
153
 
                self.tree_model = tree_model
154
 
                threading.Thread.__init__ (self)
155
 
 
156
 
        def run (self):
157
 
                shown_error = False
158
 
                tree_iter = self.tree_model.get_iter_first ()
159
 
                while (tree_iter != None):
160
 
                        channel_id = self.tree_model.get_value (tree_iter, 1)
161
 
                        parent_path = self.tree_model.get_path (tree_iter)
162
 
 
163
 
                        try:
164
 
                                # Add this channel's categories as sub-rows
165
 
                                # We have to pass a path because the model could theoretically be modified
166
 
                                # while the idle function is waiting in the queue, invalidating an iter
167
 
                                for name, count in self.feed.get (channel_id).categories ():
168
 
                                        category_id = category_name_to_id (name)
169
 
                                        GObject.idle_add (self.plugin._populate_channel_list_cb, self.tree_model, parent_path, [name, category_id, None])
170
 
                        except:
171
 
                                # Only show the error once, rather than for each channel (it gets a bit grating)
172
 
                                if not shown_error:
173
 
                                        GObject.idle_add (self.plugin._populate_channel_list_cb, self.tree_model, parent_path, None)
174
 
                                        shown_error = True
175
 
 
176
 
                        tree_iter = self.tree_model.iter_next (tree_iter)
 
166
    # Class to populate the channel list from the Internet
 
167
    def __init__ (self, callback, feed, tree_model):
 
168
        self.callback = callback
 
169
        self.feed = feed
 
170
        self.tree_model = tree_model
 
171
        threading.Thread.__init__ (self)
 
172
 
 
173
    def run (self):
 
174
        shown_error = False
 
175
        tree_iter = self.tree_model.get_iter_first ()
 
176
        while (tree_iter != None):
 
177
            channel_id = self.tree_model.get_value (tree_iter, 1)
 
178
            parent_path = self.tree_model.get_path (tree_iter)
 
179
 
 
180
            try:
 
181
                # Add this channel's categories as sub-rows
 
182
                # We have to pass a path because the model could theoretically
 
183
                # be modified while the idle function is waiting in the queue,
 
184
                # invalidating an iter
 
185
                for name, _count in self.feed.get (channel_id).categories ():
 
186
                    category_id = category_name_to_id (name)
 
187
                    GObject.idle_add (self.callback,
 
188
                                      self.tree_model, parent_path,
 
189
                                      [name, category_id, None])
 
190
            except StandardError:
 
191
                # Only show the error once, rather than for each channel
 
192
                # (it gets a bit grating)
 
193
                if not shown_error:
 
194
                    GObject.idle_add (self.callback,
 
195
                                      self.tree_model, parent_path, None)
 
196
                    shown_error = True
 
197
 
 
198
            tree_iter = self.tree_model.iter_next (tree_iter)
177
199
 
178
200
class PopulateProgrammesThread (threading.Thread):
179
 
        # Class to populate the programme list for a channel/category combination from the Internet
180
 
        def __init__ (self, plugin, feed, tree_model, category_path):
181
 
                self.plugin = plugin
182
 
                self.feed = feed
183
 
                self.tree_model = tree_model
184
 
                self.category_path = category_path
185
 
                threading.Thread.__init__ (self)
186
 
 
187
 
        def run (self):
188
 
                self.plugin.programme_download_lock.acquire ()
189
 
 
190
 
                category_iter = self.tree_model.get_iter (self.category_path)
191
 
                if category_iter == None:
192
 
                        GObject.idle_add (self.plugin._populate_programme_list_cb, self.tree_model, self.category_path, None, False)
193
 
                        self.plugin.programme_download_lock.release ()
194
 
                        return
195
 
 
196
 
                category_id = self.tree_model.get_value (category_iter, 1)
197
 
                parent_iter = self.tree_model.iter_parent (category_iter)
198
 
                channel_id = self.tree_model.get_value (parent_iter, 1)
199
 
 
200
 
                # Retrieve the programmes and return them
201
 
                feed = self.feed.get (channel_id).get (category_id)
202
 
                if feed == None:
203
 
                        GObject.idle_add (self.plugin._populate_programme_list_cb, self.tree_model, self.category_path, None, False)
204
 
                        self.plugin.programme_download_lock.release ()
205
 
                        return
206
 
 
207
 
                # Get the programmes
208
 
                try:
209
 
                        programmes = feed.list ()
210
 
                except:
211
 
                        GObject.idle_add (self.plugin._populate_programme_list_cb, self.tree_model, self.category_path, None, False)
212
 
                        self.plugin.programme_download_lock.release ()
213
 
                        return
214
 
 
215
 
                # Add the programmes to the tree store
216
 
                remove_placeholder = True
217
 
                for programme in programmes:
218
 
                        programme_item = programme.programme
219
 
 
220
 
                        # Get the media, which gives the stream URI.
221
 
                        # We go for mobile quality, since the higher-quality streams are RTMP-only
222
 
                        # which isn't currently supported by GStreamer or xine
223
 
                        # TODO: Use higher-quality streams once http://bugzilla.gnome.org/show_bug.cgi?id=566604 is fixed
224
 
                        media = programme_item.get_media_for ('mobile')
225
 
                        if media == None:
226
 
                                # Not worth displaying an error in the interface for this
227
 
                                print "Programme has no HTTP streams"
228
 
                                continue
229
 
 
230
 
                        GObject.idle_add (self.plugin._populate_programme_list_cb, self.tree_model, self.category_path,
231
 
                                          [programme.get_title (), programme.get_summary (), media.url],
232
 
                                          remove_placeholder)
233
 
                        remove_placeholder = False
234
 
 
235
 
                self.plugin.programme_download_lock.release ()
 
201
    # Class to populate the programme list for a channel/category combination
 
202
    # from the Internet
 
203
    def __init__ (self, download_lock, callback, feed,
 
204
                  (tree_model, category_path)):
 
205
        self.download_lock = download_lock
 
206
        self.callback = callback
 
207
        self.feed = feed
 
208
        self.tree_model = tree_model
 
209
        self.category_path = category_path
 
210
        threading.Thread.__init__ (self)
 
211
 
 
212
    def run (self):
 
213
        self.download_lock.acquire ()
 
214
 
 
215
        category_iter = self.tree_model.get_iter (self.category_path)
 
216
        if category_iter == None:
 
217
            GObject.idle_add (self.callback,
 
218
                              self.tree_model, self.category_path, None, False)
 
219
            self.download_lock.release ()
 
220
            return
 
221
 
 
222
        category_id = self.tree_model.get_value (category_iter, 1)
 
223
        parent_iter = self.tree_model.iter_parent (category_iter)
 
224
        channel_id = self.tree_model.get_value (parent_iter, 1)
 
225
 
 
226
        # Retrieve the programmes and return them
 
227
        feed = self.feed.get (channel_id).get (category_id)
 
228
        if feed == None:
 
229
            GObject.idle_add (self.callback,
 
230
                              self.tree_model, self.category_path, None, False)
 
231
            self.download_lock.release ()
 
232
            return
 
233
 
 
234
        # Get the programmes
 
235
        try:
 
236
            programmes = feed.list ()
 
237
        except StandardError:
 
238
            GObject.idle_add (self.callback,
 
239
                              self.tree_model, self.category_path, None, False)
 
240
            self.download_lock.release ()
 
241
            return
 
242
 
 
243
        # Add the programmes to the tree store
 
244
        remove_placeholder = True
 
245
        for programme in programmes:
 
246
            programme_item = programme.programme
 
247
 
 
248
            # Get the media, which gives the stream URI.
 
249
            # We go for mobile quality, since the higher-quality streams are
 
250
            # RTMP-only which isn't currently supported by GStreamer or xine
 
251
            # TODO: Use higher-quality streams once
 
252
            # http://bugzilla.gnome.org/show_bug.cgi?id=566604 is fixed
 
253
            media = programme_item.get_media_for ('mobile')
 
254
            if media == None:
 
255
                # Not worth displaying an error in the interface for this
 
256
                print "Programme has no HTTP streams"
 
257
                continue
 
258
 
 
259
            GObject.idle_add (self.callback,
 
260
                              self.tree_model, self.category_path,
 
261
                              [programme.get_title (), programme.get_summary (),
 
262
                               media.url],
 
263
                              remove_placeholder)
 
264
            remove_placeholder = False
 
265
 
 
266
        self.download_lock.release ()