1
1
# -*- coding: utf-8 -*-
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
11
gettext.textdomain("totem")
8
gettext.textdomain ("totem")
13
10
D_ = gettext.dgettext
14
11
_ = gettext.gettext
16
13
class IplayerPlugin (GObject.Object, Peas.Activatable):
17
__gtype_name__ = 'IplayerPlugin'
19
object = GObject.property(type = GObject.Object)
22
GObject.Object.__init__ (self)
26
self.programme_download_lock = threading.Lock ()
28
def do_activate (self):
29
self.totem = self.object
31
builder = Totem.plugin_load_interface ("iplayer", "iplayer.ui", True, self.totem.get_main_window (), self)
32
container = builder.get_object ('iplayer_vbox')
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)
41
self.tv = iplayer2.feed ('tv')
43
# Add the interface to Totem's sidebar
44
self.totem.add_sidebar_page ("iplayer", _(u"BBC iPlayer"), container)
46
# Get the channel category listings
47
self.populate_channel_list (self.tv, self.tv_tree_store)
49
def do_deactivate (self):
50
self.totem.remove_sidebar_page ("iplayer")
52
def populate_channel_list (self, feed, tree_store):
54
print "Populating channel list…"
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))
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)
66
def _populate_channel_list_cb (self, tree_store, parent_path, values):
67
# Callback from PopulateChannelsThread to add stuff to the tree store
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.'))
72
parent_iter = tree_store.get_iter (parent_path)
73
category_iter = tree_store.append (parent_iter, values)
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])
81
def _row_expanded_cb (self, tree_view, row_iter, path):
82
tree_model = tree_view.get_model ()
85
print "_row_expanded_cb called."
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:
92
# Populate it with programmes asynchronously
93
self.populate_programme_list (self.tv, tree_model, row_iter)
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)
101
mrl = tree_store.get_value (tree_iter, 2)
103
# Only allow programme rows to be activated, not channel or category rows
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)
110
def populate_programme_list (self, feed, tree_store, category_iter):
112
print "Populating programme list…"
114
category_path = tree_store.get_path (category_iter)
115
thread = PopulateProgrammesThread (self, feed, tree_store, category_path)
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
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.'))
125
category_iter = tree_store.get_iter (category_path)
126
if category_iter != None:
127
tree_store.append (category_iter, values)
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)
14
__gtype_name__ = 'IplayerPlugin'
16
object = GObject.property (type = GObject.Object)
19
GObject.Object.__init__ (self)
23
self.programme_download_lock = threading.Lock ()
26
self.tv_tree_store = None
28
def do_activate (self):
29
self.totem = self.object
31
builder = Totem.plugin_load_interface ("iplayer", "iplayer.ui", True,
32
self.totem.get_main_window (),
34
container = builder.get_object ('iplayer_vbox')
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)
43
self.tv_feed = iplayer2.Feed ('tv')
45
# Add the interface to Totem's sidebar
46
self.totem.add_sidebar_page ("iplayer", _(u"BBC iPlayer"), container)
48
# Get the channel category listings
49
self._populate_channel_list (self.tv_feed, self.tv_tree_store)
51
def do_deactivate (self):
52
self.totem.remove_sidebar_page ("iplayer")
54
def _populate_channel_list (self, feed, tree_store):
56
print "Populating channel list…"
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))
63
# Add the channels' categories in a thread, since they each require a
65
thread = PopulateChannelsThread (self.__populate_channel_list_cb,
69
def __populate_channel_list_cb (self, tree_store, parent_path, values):
70
# Callback from PopulateChannelsThread to add stuff to the tree store
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.'))
78
parent_iter = tree_store.get_iter (parent_path)
79
category_iter = tree_store.append (parent_iter, values)
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
84
tree_store.append (category_iter, [_(u'Loading…'), None, None])
88
def __row_expanded_cb (self, tree_view, row_iter, path):
89
tree_model = tree_view.get_model ()
92
print "__row_expanded_cb called."
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):
100
# Populate it with programmes asynchronously
101
self._populate_programme_list (self.tv_feed, tree_model, row_iter)
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:
109
mrl = tree_store.get_value (tree_iter, 2)
111
# Only allow programme rows to be activated, not channel or category
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)
120
def _populate_programme_list (self, feed, tree_store, category_iter):
122
print "Populating programme list…"
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))
130
def __populate_programme_list_cb (self, tree_store, category_path, values,
132
# Callback from PopulateProgrammesThread to add stuff to the tree store
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.'))
142
category_iter = tree_store.get_iter (category_path)
143
if category_iter != None:
144
tree_store.append (category_iter, values)
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)
136
153
def get_iter_level (tree_model, tree_iter):
139
tree_iter = tree_model.iter_parent (tree_iter)
140
if tree_iter == None:
156
tree_iter = tree_model.iter_parent (tree_iter)
157
if tree_iter == None:
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')
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):
153
self.tree_model = tree_model
154
threading.Thread.__init__ (self)
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)
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])
171
# Only show the error once, rather than for each channel (it gets a bit grating)
173
GObject.idle_add (self.plugin._populate_channel_list_cb, self.tree_model, parent_path, None)
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
170
self.tree_model = tree_model
171
threading.Thread.__init__ (self)
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)
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)
194
GObject.idle_add (self.callback,
195
self.tree_model, parent_path, None)
198
tree_iter = self.tree_model.iter_next (tree_iter)
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):
183
self.tree_model = tree_model
184
self.category_path = category_path
185
threading.Thread.__init__ (self)
188
self.plugin.programme_download_lock.acquire ()
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 ()
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)
200
# Retrieve the programmes and return them
201
feed = self.feed.get (channel_id).get (category_id)
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 ()
209
programmes = feed.list ()
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 ()
215
# Add the programmes to the tree store
216
remove_placeholder = True
217
for programme in programmes:
218
programme_item = programme.programme
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')
226
# Not worth displaying an error in the interface for this
227
print "Programme has no HTTP streams"
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],
233
remove_placeholder = False
235
self.plugin.programme_download_lock.release ()
201
# Class to populate the programme list for a channel/category combination
203
def __init__ (self, download_lock, callback, feed,
204
(tree_model, category_path)):
205
self.download_lock = download_lock
206
self.callback = callback
208
self.tree_model = tree_model
209
self.category_path = category_path
210
threading.Thread.__init__ (self)
213
self.download_lock.acquire ()
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 ()
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)
226
# Retrieve the programmes and return them
227
feed = self.feed.get (channel_id).get (category_id)
229
GObject.idle_add (self.callback,
230
self.tree_model, self.category_path, None, False)
231
self.download_lock.release ()
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 ()
243
# Add the programmes to the tree store
244
remove_placeholder = True
245
for programme in programmes:
246
programme_item = programme.programme
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')
255
# Not worth displaying an error in the interface for this
256
print "Programme has no HTTP streams"
259
GObject.idle_add (self.callback,
260
self.tree_model, self.category_path,
261
[programme.get_title (), programme.get_summary (),
264
remove_placeholder = False
266
self.download_lock.release ()