~walkerlee/totem/pre-interview

« back to all changes in this revision

Viewing changes to debian/patches/70_bbc_plugin.patch

  • Committer: Package Import Robot
  • Author(s): Jeremy Bicha
  • Date: 2013-05-26 00:07:51 UTC
  • mfrom: (1.6.1) (24.1.4 experimental)
  • Revision ID: package-import@ubuntu.com-20130526000751-kv8ap3x1di4qq8j2
Tags: 3.8.2-0ubuntu1
* Sync with Debian. Remaining changes: 
* debian/control.in:
  - Drop build-depends on libepc-ui-dev and libgrilo-0.2-dev (in universe)
  - Drop libxtst-dev build-depends so that the (redundant) fake key presses
    for inhibiting the screensaver are disabled (LP: #1007438)
  - Build-depend on libzeitgeist-dev
  - Suggest rather than recommend gstreamer components in universe
  - Add totem-plugins-extra
  - Add XB-Npp-Description and XB-Npp-Filename header to the 
    totem-mozilla package to improve ubufox/ubuntu plugin db integration 
  - Refer to Firefox in totem-mozilla description instead of Iceweasel
  - Don't have totem-mozilla recommend any particular browser
  - Drop obsolete python library dependencies since iplayer is no longer
    included
* debian/totem-common.install, debian/source_totem.py:
  - Install Ubuntu apport debugging hook
* debian/totem-plugins-extra.install:
  - Universe plugins split out of totem-plugins (currently only gromit)
* debian/totem-plugins.install:    
  - Skip the plugins split to -extra and add the zeitgeist plugin
* debian/rules:
  - Build with --fail-missing, to ensure we install everything. 
    + Ignore libtotem.{,l}a since we delibrately don't install these.
  - Re-enable hardening, make sure both PIE and BINDNOW are used
    by setting hardening=+all. (LP: #1039604)
* debian/patches/91_quicklist_entries.patch:
  - Add static quicklist
* debian/patches/92_gst-plugins-good.patch:
  - Build without unnecessary gstreamer1.0-bad dependency
* debian/patches/93_grilo_optional.patch:
  - Allow building without grilo while grilo MIR is still pending
* debian/patches/correct_desktop_mimetypes.patch:
  - Don't list the mimetypes after the unity lists
* debian/patches/revert_shell_menu.patch: 
  - revert the use of a shell menu until indicator-appmenu can handle
    the mixed shell/traditional menus itself
* New upstream release

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
Index: b/bindings/python/totem.defs
2
 
===================================================================
3
 
--- a/bindings/python/totem.defs
4
 
+++ b/bindings/python/totem.defs
5
 
@@ -216,6 +216,16 @@
6
 
   (return-type "none")
7
 
 )
8
 
 
9
 
+(define-method action_set_mrl_and_play
10
 
+  (of-object "TotemObject")
11
 
+  (c-name "totem_action_set_mrl_and_play")
12
 
+  (return-type "none")
13
 
+  (parameters
14
 
+    '("const-char*" "mrl")
15
 
+    '("const-char*" "subtitle_mrl" (null-ok) (default "NULL"))
16
 
+  )
17
 
+)
18
 
+
19
 
 (define-method action_stop
20
 
   (of-object "TotemObject")
21
 
   (c-name "totem_action_stop")
22
 
Index: b/configure.in
23
 
===================================================================
24
 
--- a/configure.in
25
 
+++ b/configure.in
26
 
@@ -52,7 +52,7 @@
27
 
 AC_SUBST(TOTEM_VERSION_MICRO)
28
 
 
29
 
 # The full list of plugins
30
 
-allowed_plugins="thumbnail screensaver ontop galago gromit lirc media-player-keys mythtv properties sidebar-test skipto sample-python sample-vala bemused youtube publish tracker pythonconsole jamendo opensubtitles screenshot brasero-disc-recorder coherence_upnp dbus-service iplayer"
31
 
+allowed_plugins="thumbnail screensaver ontop galago gromit lirc media-player-keys mythtv properties sidebar-test skipto sample-python sample-vala bemused youtube publish tracker pythonconsole jamendo opensubtitles screenshot brasero-disc-recorder coherence_upnp dbus-service iplayer bbc"
32
 
 
33
 
 PLUGINDIR='${libdir}/totem/plugins'
34
 
 AC_SUBST(PLUGINDIR)
35
 
@@ -512,6 +512,32 @@
36
 
                                add_plugin="0"
37
 
                        fi
38
 
                ;;
39
 
+               bbc)
40
 
+                       # pygobject >= 2.15.3 for gio support
41
 
+                       # pygtk >= 2.13.0 for gtk_tree_view_get_tooltip_context support
42
 
+                       PKG_CHECK_MODULES([BBC],
43
 
+                               [ pygobject-2.0 >= 2.15.3 gst-python-0.10 >= 0.10.11 pygtk-2.0 >= 2.13.0 ],
44
 
+                               [ HAVE_BBCSTUFF=yes ], [ HAVE_BBCSTUFF=no ])
45
 
+                       if test "x$HAVE_BBCSTUFF" != "xyes"; then
46
 
+                               plugin_error_or_ignore "you need pygobject-2.0 >= 2.15.3 and gst-python-0.10 >= 0.10.11 and pygtk-2.0 >= 2.13.0 installed for the BBC plugin"
47
 
+                               add_plugin="0"
48
 
+                       fi
49
 
+                       # only require python apt and gdbm where we would actually be using it, ie. on ubuntu
50
 
+                       bbc_py_pkg_reqs=''
51
 
+                       if test -f /etc/debian_version ; then
52
 
+                               bbc_distro_id=`lsb_release -s -i 2>/dev/null`
53
 
+                               if test x$bbc_distro_id = xUbuntu; then
54
 
+                                       bbc_py_pkg_reqs="apt gdbm"
55
 
+                               fi 
56
 
+                       fi
57
 
+                       for pymodule in rdflib.Graph xdg $bbc_py_pkg_reqs
58
 
+                       do
59
 
+                               if ! $PYTHON -c "import $pymodule" 2>/dev/null >/dev/null; then
60
 
+                                       plugin_error_or_ignore "you need the python $pymodule module installed for the BBC plugin"
61
 
+                                       add_plugin="0"
62
 
+                               fi
63
 
+                       done
64
 
+               ;;
65
 
                bemused)
66
 
                        PKG_CHECK_MODULES(BEMUSED, bluez, [HAVE_BLUEZ=yes], [HAVE_BLUEZ=no])
67
 
                        if test "${HAVE_BLUEZ}" != "yes" ; then
68
 
@@ -814,6 +840,7 @@
69
 
 lib/Makefile
70
 
 src/Makefile
71
 
 src/plugins/Makefile
72
 
+src/plugins/bbc/Makefile
73
 
 src/plugins/bemused/Makefile
74
 
 src/plugins/coherence_upnp/Makefile
75
 
 src/plugins/dbus-service/Makefile
76
 
Index: b/src/plugins/bbc/Makefile.am
77
 
===================================================================
78
 
--- /dev/null
79
 
+++ b/src/plugins/bbc/Makefile.am
80
 
@@ -0,0 +1,26 @@
81
 
+plugindir = $(PLUGINDIR)/bbc
82
 
+plugin_PYTHON = bbc.py contentview.py genres.py installablecodecs.py
83
 
+
84
 
+plugin_in_files = bbc.totem-plugin.in
85
 
+
86
 
+%.totem-plugin: %.totem-plugin.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*po) ; $(INTLTOOL_MERGE) $(top_srcdir)/po $< $@ -d -u -c $(top_builddir)/po/.intltool-merge-cache
87
 
+
88
 
+plugin_DATA = $(plugin_in_files:.totem-plugin.in=.totem-plugin)
89
 
+
90
 
+EXTRA_DIST = $(plugin_in_files) bbc.py contentview.py genres.py installablecodecs.py
91
 
+
92
 
+CLEANFILES = $(plugin_DATA)
93
 
+DISTCLEANFILES = $(plugin_DATA)
94
 
+
95
 
+
96
 
+pychecker:
97
 
+       PYTHONPATH=$(top_srcdir)/src/plugins/bbc:$$PYTHONPATH \
98
 
+       pychecker $(wildcard $(top_srcdir)/src/plugins/bbc/*py)
99
 
+
100
 
+pyflakes:
101
 
+       pyflakes $(wildcard $(top_srcdir)/src/plugins/bbc/*py)
102
 
+
103
 
+check: pychecker pyflakes
104
 
+       echo
105
 
+
106
 
+
107
 
Index: b/src/plugins/bbc/bbc.py
108
 
===================================================================
109
 
--- /dev/null
110
 
+++ b/src/plugins/bbc/bbc.py
111
 
@@ -0,0 +1,95 @@
112
 
+#!/usr/bin/python
113
 
+# coding=UTF-8
114
 
+#
115
 
+# Copyright (C) 2008 Tim-Philipp Müller <tim.muller@collabora.co.uk>
116
 
+# Copyright (C) 2008 Canonical Ltd.
117
 
+#
118
 
+# This program is free software; you can redistribute it and/or modify
119
 
+# it under the terms of the GNU General Public License as published by
120
 
+# the Free Software Foundation; either version 2 of the License, or
121
 
+# (at your option) any later version.
122
 
+#
123
 
+# This program is distributed in the hope that it will be useful,
124
 
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
125
 
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
126
 
+# GNU General Public License for more details.
127
 
+#
128
 
+# You should have received a copy of the GNU General Public License
129
 
+# along with this program; if not, write to the Free Software
130
 
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
131
 
+#
132
 
+# The Totem project hereby grant permission for non-gpl compatible GStreamer
133
 
+# plugins to be used and distributed together with GStreamer and Totem. This
134
 
+# permission are above and beyond the permissions granted by the GPL license
135
 
+# Totem is covered by.
136
 
+#
137
 
+# See license_change file for details.
138
 
+
139
 
+import gobject
140
 
+gobject.threads_init()
141
 
+import pygst
142
 
+pygst.require ("0.10")
143
 
+import gst
144
 
+
145
 
+import totem
146
 
+import gtk
147
 
+import gconf
148
 
+import time
149
 
+import os
150
 
+from contentview import ContentView
151
 
+
152
 
+class BBCViewer(totem.Plugin):
153
 
+       def __init__ (self):
154
 
+               totem.Plugin.__init__ (self)
155
 
+               self.loaded_content = False
156
 
+
157
 
+       def mapped (self, contentview):
158
 
+               gst.log('mapped')
159
 
+               if not self.loaded_content:
160
 
+                 self.view.load()
161
 
+                 self.loaded_content = True
162
 
+
163
 
+       def activate (self, totem_object):
164
 
+               self.gconf_client = gconf.client_get_default ()
165
 
+               self.totem = totem_object
166
 
+                self.view = ContentView()
167
 
+               self.view.connect('play-episode', self.playEpisode)
168
 
+               vbox = gtk.VBox()
169
 
+                scrollwin = gtk.ScrolledWindow()
170
 
+                scrollwin.add(self.view)
171
 
+                scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
172
 
+                scrollwin.set_shadow_type(gtk.SHADOW_ETCHED_IN)
173
 
+                vbox.pack_start(scrollwin, True, True)
174
 
+               vbox.show_all ()
175
 
+               totem_object.add_sidebar_page ("bbc", _("BBC"), vbox)
176
 
+               # connect to 'map' only after adding the sidebar page
177
 
+               self.view.connect('map', self.mapped)
178
 
+               gst.log('activated')
179
 
+
180
 
+       def deactivate (self, totem_object):
181
 
+               totem_object.remove_sidebar_page ("bbc")
182
 
+               self.loaded_content = False
183
 
+
184
 
+       def getConnectionSpeed(self):
185
 
+               speed_map = [ 14400, 19200, 28800, 33600, 34400,
186
 
+                             56000, 112000, 256000, 384000, 512000,
187
 
+                             1536000, 10752000 ]
188
 
+               speed_enum = self.gconf_client.get_int("/apps/totem/connection_speed")
189
 
+               if speed_enum >= 0 and speed_enum < len(speed_map):
190
 
+                 speed_kbps = speed_map[speed_enum] / 1000
191
 
+               else:
192
 
+                 speed_kbps = 0
193
 
+               gst.log('Configured connection speed #%d: %d kbit/s' % (speed_enum, speed_kbps))
194
 
+               return speed_kbps
195
 
+
196
 
+       def playEpisode (self, view, episode):
197
 
+               gst.info('Playing episode ' + episode.title)
198
 
+               mrl = episode.getUri(self.getConnectionSpeed())
199
 
+               if mrl:
200
 
+                 gst.log('Playing uri ' + mrl)
201
 
+                 self.totem.action_set_mrl_and_play(mrl, None)
202
 
+                 #self.totem.action_remote(totem.REMOTE_COMMAND_ENQUEUE, mrl)
203
 
+                 #self.totem.action_remote(totem.REMOTE_COMMAND_PLAY, mrl)
204
 
+               else:
205
 
+                 gst.error('No uri for episode ' + episode.title)
206
 
+
207
 
Index: b/src/plugins/bbc/bbc.totem-plugin.in
208
 
===================================================================
209
 
--- /dev/null
210
 
+++ b/src/plugins/bbc/bbc.totem-plugin.in
211
 
@@ -0,0 +1,9 @@
212
 
+[Totem Plugin]
213
 
+Loader=python
214
 
+Module=bbc
215
 
+IAge=1
216
 
+_Name=BBC content viewer
217
 
+_Description=Watch or listen to selected video and audio content made available by the British Broadcasting Corporation (BBC)
218
 
+Authors=Tim-Philipp Müller <tim.muller@collabora.co.uk>
219
 
+Copyright=Copyright © 2008 Tim-Philipp Müller and Canonical Ltd. 
220
 
+Website=http://www.gnome.org/projects/totem/
221
 
Index: b/src/plugins/bbc/contentview.py
222
 
===================================================================
223
 
--- /dev/null
224
 
+++ b/src/plugins/bbc/contentview.py
225
 
@@ -0,0 +1,1019 @@
226
 
+#!/usr/bin/python
227
 
+# coding=UTF-8
228
 
+#
229
 
+# Copyright (C) 2008 Tim-Philipp Müller <tim.muller@collabora.co.uk>
230
 
+# Copyright (C) 2008 Canonical Ltd.
231
 
+#
232
 
+# This program is free software; you can redistribute it and/or modify
233
 
+# it under the terms of the GNU General Public License as published by
234
 
+# the Free Software Foundation; either version 2 of the License, or
235
 
+# (at your option) any later version.
236
 
+#
237
 
+# This program is distributed in the hope that it will be useful,
238
 
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
239
 
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
240
 
+# GNU General Public License for more details.
241
 
+#
242
 
+# You should have received a copy of the GNU General Public License
243
 
+# along with this program; if not, write to the Free Software
244
 
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
245
 
+#
246
 
+# The Totem project hereby grant permission for non-gpl compatible GStreamer
247
 
+# plugins to be used and distributed together with GStreamer and Totem. This
248
 
+# permission are above and beyond the permissions granted by the GPL license
249
 
+# Totem is covered by.
250
 
+#
251
 
+# See license_change file for details.
252
 
+#
253
 
+# TODO:
254
 
+#  - clean up code: mixed studlyCaps and foo_bar; mixed callbacks and signals
255
 
+
256
 
+import gobject
257
 
+gobject.threads_init()
258
 
+import glib
259
 
+import gio
260
 
+import pygst
261
 
+pygst.require ("0.10")
262
 
+import gst
263
 
+import gtk
264
 
+import pango
265
 
+
266
 
+import os
267
 
+import dircache
268
 
+import errno
269
 
+import random
270
 
+import time
271
 
+import thread
272
 
+
273
 
+from rdflib.Graph import ConjunctiveGraph
274
 
+from rdflib import Namespace
275
 
+from rdflib import RDF
276
 
+
277
 
+from xdg import BaseDirectory
278
 
+
279
 
+import installablecodecs
280
 
+import genres
281
 
+
282
 
+'''
283
 
+Define namespaces we will be using globally
284
 
+'''
285
 
+DC = Namespace('http://purl.org/dc/elements/1.1/')
286
 
+PO = Namespace('http://purl.org/ontology/po/')
287
 
+OWL = Namespace('http://www.w3.org/2002/07/owl#')
288
 
+FOAF = Namespace('http://xmlns.com/foaf/0.1/')
289
 
+PLAY = Namespace('http://uriplay.org/elements/')
290
 
+
291
 
+
292
 
+'''
293
 
+Global codec cache singleton
294
 
+'''
295
 
+codec_cache = None
296
 
+
297
 
+'''
298
 
+Container/Audio/Video codec mappings - global for readability
299
 
+'''
300
 
+
301
 
+# FIXME: Real Media formats
302
 
+container_map = { 'application/ogg' : 'application/ogg',
303
 
+                  'audio/ogg' : 'application/ogg',
304
 
+                  'video/ogg' : 'application/ogg',
305
 
+                  'video/x-ms-asf' : 'video/x-ms-asf',
306
 
+                  'audio/x-ms-asf' : 'video/x-ms-asf',
307
 
+                  'audio/mp3' : 'audio/mpeg, mpegversion=(int)1, layer=(int)3',
308
 
+                  'audio/mp4' : 'audio/x-m4a',
309
 
+                  'audio/mpeg' : 'audio/mpeg, mpegversion=(int)1',
310
 
+                  'video/x-flv' : 'video/x-flv',
311
 
+                  'video/3gpp' : 'application/x-3gp',
312
 
+                  'application/x-3gp' : 'application/x-3gp',
313
 
+                  'audio/x-matroska' : 'video/x-matroska',
314
 
+                  'video/x-matroska' : 'video/x-matroska',
315
 
+                  'video/mp4' : 'video/quicktime',
316
 
+                  'video/mpeg' : 'video/mpeg, mpegversion=(int)1, ' +
317
 
+                                 ' systemstream=(boolean)true;   ' + 
318
 
+                                 'video/mpeg, mpegversion=(int)2, ' +
319
 
+                                  ' systemstream=(boolean)true',
320
 
+                  'video/mpeg2' : 'video/mpeg, mpegversion=(int)2,' +
321
 
+                                  ' systemstream=(boolean)false',
322
 
+                  'video/mp2t' : 'video/mpegts',
323
 
+                  'video/mpegts' : 'video/mpegts' }
324
 
+
325
 
+# FIXME: do we need both parsed=true and parsed=false for mp3?
326
 
+audio_map = { 'audio/mp1' : 'audio/mpeg, mpegversion=(int)1, layer=(int)1',
327
 
+              'audio/mp2' : 'audio/mpeg, mpegversion=(int)1, layer=(int)2',
328
 
+              'audio/mp3' : 'audio/mpeg, mpegversion=(int)1, layer=(int)3',
329
 
+              'audio/mp4' : 'audio/mpeg, mpegversion=(int)2; ' +
330
 
+                            'audio/mpeg, mpegversion=(int)4',
331
 
+              'audio/mpeg' : 'audio/mpeg, mpegversion=(int)1, layer=(int)3',
332
 
+              'audio/x-wma' : 'audio/x-wma, wmaversion=(int)1; ' +
333
 
+                              'audio/x-wma, wmaversion=(int)2',
334
 
+              'audio/x-wmv' : 'audio/x-wma, wmaversion=(int)1; ' +
335
 
+                              'audio/x-wma, wmaversion=(int)2',
336
 
+              'audio/x-ms-wma' : 'audio/x-wma, wmaversion=(int)1; ' +
337
 
+                                 'audio/x-wma, wmaversion=(int)2',
338
 
+              'audio/x-ms-wmv' : 'audio/x-wma, wmaversion=(int)1; ' +
339
 
+                                 'audio/x-wma, wmaversion=(int)2',
340
 
+              'audio/vorbis' : 'audio/x-vorbis' }
341
 
+
342
 
+# FIXME: video/x-ms-wmv: ask if this refers to a particular wmv version or if it can be any version/profile
343
 
+# BBC regard video/x-svq as equivalent to video/x-flash-video, so we just
344
 
+# treat them all the same here and require all of them in this case
345
 
+video_map = { 'video/x-vp6' : 'video/x-vp6',
346
 
+              'video/x-flash-video' : 'video/x-svq, svqversion=(int)1; ' +
347
 
+                                      'video/x-svq, svqversion=(int)3; ' +
348
 
+                                      'video/x-flash-video, flvversion=(int)1',
349
 
+              'video/H263-200' : 'video/x-svq, svqversion=(int)1; ' +
350
 
+                                 'video/x-flash-video, flvversion=(int)1',
351
 
+              'video/x-svq' : 'video/x-svq, svqversion=(int)1; ' +
352
 
+                              'video/x-svq, svqversion=(int)3; ' +
353
 
+                              'video/x-flash-video, flvversion=(int)1',
354
 
+              'video/H264' : 'video/x-h264',
355
 
+              'video/mpeg' : 'video/mpeg, mpegversion=(int)1, ' +
356
 
+                             ' systemstream=(boolean)false;   ' + 
357
 
+                             'video/mpeg, mpegversion=(int)2, ' +
358
 
+                              ' systemstream=(boolean)false',
359
 
+              'video/mpeg1' : 'video/mpeg, mpegversion=(int)1, ' +
360
 
+                              ' systemstream=(boolean)false',
361
 
+              'video/mpeg2' : 'video/mpeg, mpegversion=(int)2, ' +
362
 
+                              ' systemstream=(boolean)false',
363
 
+              'video/x-dirac' : 'video/x-dirac',
364
 
+              'video/x-wmv' : 'video/x-wmv, wmvversion=(int)1; ' +
365
 
+                              'video/x-wmv, wmvversion=(int)2; ' +
366
 
+                              'video/x-wmv, wmvversion=(int)3',
367
 
+              'video/x-ms-wmv' : 'video/x-wmv, wmvversion=(int)1; ' +
368
 
+                                 'video/x-wmv, wmvversion=(int)2; ' +
369
 
+                                 'video/x-wmv, wmvversion=(int)3' }
370
 
+
371
 
+###############################################################################
372
 
+
373
 
+'''
374
 
+CodecCache: keeps track of what is currently installed and what we might be
375
 
+            able to install; caches things internally in a dict so we don't
376
 
+            have to do expensive checks more often than necessary; do not
377
 
+            cache results elsewhere and make sure to listen to the 'loaded'
378
 
+            signal and refilter any content once the database of installable
379
 
+            and installed codecs is loaded (methods may just return False if
380
 
+            the database hasn't been loaded yet)
381
 
+'''
382
 
+class CodecCache(gobject.GObject):
383
 
+    __slots__ = [ 'codec_cache', 'installable_codecs' ]
384
 
+
385
 
+    __gsignals__ = dict(loaded=(gobject.SIGNAL_RUN_LAST, None, ()))
386
 
+
387
 
+    def __init__(self):
388
 
+        gobject.GObject.__init__ (self)
389
 
+        self.codec_cache = { }
390
 
+        self.installable_codecs = None
391
 
+
392
 
+    def reload_async(self):
393
 
+        gst.log('starting codec cache loading')
394
 
+        thread.start_new_thread(self._loading_thread, ())
395
 
+
396
 
+    def _loading_thread(self):
397
 
+        ''' idle callback to marshal result back into the main thread '''
398
 
+        def _loading_done_idle_cb(res):
399
 
+            gst.log('codec cache loaded (%d elements)' % (len(res)))
400
 
+            self.installable_codecs = res
401
 
+            self.emit('loaded')
402
 
+            return False # don't want to be called again
403
 
+
404
 
+        gst.log('in codec cache loading thread')
405
 
+        # the following can take quite a few seconds on machines with very
406
 
+        # low cpu power (culprit: apt.Cache()), so we do this in a separate
407
 
+        # thread and then trigger a refiltering of the treeview when done
408
 
+        res = installablecodecs.getInstallableCodecs()
409
 
+        gst.log('codec cache loading done, marshalling result into main thread')
410
 
+        gobject.idle_add(_loading_done_idle_cb, res)
411
 
412
 
+    def haveDecoderForCaps(self, decoder_caps):
413
 
+        caps_string = decoder_caps.to_string()
414
 
+
415
 
+        if caps_string in self.codec_cache:
416
 
+          return self.codec_cache[caps_string]
417
 
+
418
 
+        registry = gst.registry_get_default()
419
 
+        features = registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY)
420
 
+
421
 
+        for feature in features:
422
 
+          # only take into account elements playbin will use
423
 
+          if feature.get_rank() < gst.RANK_MARGINAL:
424
 
+            continue
425
 
+          klass = feature.get_klass()
426
 
+          # ignore Depayloaders for now
427
 
+          if klass.find('Demux') >= 0 or \
428
 
+             klass.find('Decoder') >= 0 or \
429
 
+             klass.find('Parse') >= 0:
430
 
+            for pad_template in feature.get_static_pad_templates():
431
 
+              if pad_template.direction == gst.PAD_SINK:
432
 
+                if not pad_template.get_caps().intersect(decoder_caps).is_empty():
433
 
+                  self.codec_cache[caps_string] = True
434
 
+                  gst.debug('%s can handle %s' % (feature.get_name(), caps_string))
435
 
+                  return True
436
 
+        self.codec_cache[caps_string] = False
437
 
+        gst.debug('no element found that can handle ' + caps_string)
438
 
+        return False
439
 
+
440
 
+    ''' do not cache the result of this function '''
441
 
+    def isInstalledOrInstallable(self, caps_needed):
442
 
+        if not caps_needed or caps_needed.is_empty() or caps_needed.is_any():
443
 
+          return False
444
 
+
445
 
+        if self.installable_codecs is None:
446
 
+          gst.log('database of installable codecs not loaded yet')
447
 
+          return False
448
 
+
449
 
+        for s in caps_needed:
450
 
+          if not self.haveDecoderForCaps(gst.Caps(s)):
451
 
+            gst.debug('no decoder for %s installed' % (s.to_string()))
452
 
+            if not s.get_name() in self.installable_codecs:
453
 
+              gst.debug('%s not installable either'  % (s.to_string()))
454
 
+              return False
455
 
+
456
 
+        return True
457
 
+
458
 
+###############################################################################
459
 
+
460
 
+'''
461
 
+UriPlayObject: base class for Brand, Episode, Encoding, Location etc.
462
 
+'''
463
 
+class UriPlayObject(object):
464
 
+    __slots__ = [ 'rdf_attribute_mapping' ]
465
 
+
466
 
+    def __init__(self):
467
 
+        self.rdf_attribute_mapping = []
468
 
+
469
 
+    def parseProperties(self, conjunctive_graph, graph_obj):
470
 
+        for rdf_tag, prop_name in self.rdf_attribute_mapping:
471
 
+          self.__setattr__(prop_name, None)
472
 
+        for rdf_tag, prop_name in self.rdf_attribute_mapping:
473
 
+          for match in conjunctive_graph.objects(graph_obj, rdf_tag):
474
 
+            self.__setattr__(prop_name, match.encode('utf-8'))
475
 
+            break # we can handle only one value for each property name
476
 
+
477
 
+###############################################################################
478
 
+
479
 
+'''
480
 
+Brand: a show/series/group of episodes
481
 
+'''
482
 
+class Brand(UriPlayObject):
483
 
+    __slots__ = [ 'title', 'description', 'episodes', 'genres' ]
484
 
+
485
 
+    def __init__(self):
486
 
+        self.episodes = []
487
 
+        self.genres = []
488
 
+        self.rdf_attribute_mapping = [ ( DC['title'], 'title' ),
489
 
+                                       ( DC['description'], 'description' ) ]
490
 
+
491
 
+    def parseBrand(self, conjunctive_graph, graph_brand):
492
 
+        self.parseProperties(conjunctive_graph, graph_brand)
493
 
+
494
 
+        self.episodes = []
495
 
+        for e in conjunctive_graph.objects(graph_brand, PO['episode']):
496
 
+          episode = Episode()
497
 
+          episode.parseEpisode(conjunctive_graph, e)
498
 
+          self.episodes.append(episode)
499
 
+
500
 
+        self.genres = []
501
 
+        for match in conjunctive_graph.objects(graph_brand, PO['genre']):
502
 
+          genre_utf8 = match.encode('utf-8')
503
 
+          pos = genre_utf8.find('/genres/')
504
 
+          if pos > 0:
505
 
+            pos += len('/genres/')
506
 
+            genre = genre_utf8[pos:]
507
 
+          else:
508
 
+            gst.warning('Unexpected genre identifier: ' + genre_utf8)
509
 
+            genre = 'other'
510
 
+          if genre not in self.genres:
511
 
+            self.genres.append(genre)
512
 
+
513
 
+    def hasUsableEpisodes(self):
514
 
+        for episode in self.episodes:
515
 
+          if episode.hasUsableEncodings():
516
 
+            return True
517
 
+        return False
518
 
+
519
 
+    def getUsableEpisodes(self):
520
 
+        usable_episodes = []
521
 
+        for episode in self.episodes:
522
 
+          if episode.hasUsableEncodings():
523
 
+            usable_episodes.append(episode)
524
 
+        return usable_episodes
525
 
+
526
 
+###############################################################################
527
 
+
528
 
+'''
529
 
+Episode: a single episode of a Brand (even though we parse the different
530
 
+         versions, for now we'll just pretend there is only one version and
531
 
+         map the encodings attribute to the encodings of the first version
532
 
+         we find, to make things easier)
533
 
+'''
534
 
+class Episode(UriPlayObject):
535
 
+    __slots__ = [ 'title', 'description', 'versions', 'encodings' ]
536
 
+
537
 
+    def __init__(self):
538
 
+        self.encodings = []
539
 
+        self.rdf_attribute_mapping = [ ( DC['title'], 'title' ),
540
 
+                                       ( DC['description'], 'description' ) ]
541
 
+
542
 
+    def parseEpisode(self, conjunctive_graph, graph_episode):
543
 
+        self.parseProperties(conjunctive_graph, graph_episode)
544
 
+        self.versions = []
545
 
+        for v in conjunctive_graph.objects(graph_episode, PO['version']):
546
 
+          version = EpisodeVersion()
547
 
+          version.parseVersion(conjunctive_graph, v)
548
 
+          self.versions.append(version)
549
 
+          # encodings of episode = encodings of first version of episode
550
 
+          if not self.encodings:
551
 
+            self.encodings = version.encodings
552
 
+
553
 
+    def hasUsableEncodings(self):
554
 
+        for encoding in self.encodings:
555
 
+          if encoding.isUsable():
556
 
+            return True
557
 
+        return False
558
 
+
559
 
+    # TODO: this does not take into account codec quality, highest bitrate wins
560
 
+    def getBestEncoding(self, connection_speed=0):
561
 
+        gst.log('connection speed: %d kbit/s' % (connection_speed))
562
 
+        best_encoding = None
563
 
+        for encoding in self.encodings:
564
 
+          if not encoding.isUsable():
565
 
+            continue
566
 
+          gst.log('have encoding with bitrate: %d kbit/s' % (encoding.getBitrate()))
567
 
+          if best_encoding:
568
 
+            if encoding.getBitrate() > best_encoding.getBitrate():
569
 
+              if connection_speed <= 0 or encoding.getBitrate() <= connection_speed:
570
 
+                best_encoding = encoding
571
 
+          else:
572
 
+            best_encoding = encoding
573
 
+        if best_encoding:
574
 
+          gst.log('best encoding has bitrate of %d kbit/s' % (best_encoding.getBitrate()))  
575
 
+        return best_encoding
576
 
+
577
 
+    def getUri(self, connection_speed=0):
578
 
+        encoding = self.getBestEncoding(connection_speed)
579
 
+        if encoding:
580
 
+          location = encoding.getBestLocation()
581
 
+          if location:
582
 
+            return location.uri
583
 
+        return None
584
 
+
585
 
+###############################################################################
586
 
+
587
 
+'''
588
 
+EpisodeVersion: a version of an Episode (e.g. UK vs. US or pg-13 vs. 18)
589
 
+'''
590
 
+class EpisodeVersion(UriPlayObject):
591
 
+    __slots__ = [ 'encodings' ]
592
 
+
593
 
+    def __init__(self):
594
 
+        self.encodings = []
595
 
+        self.rdf_attribute_mapping = []
596
 
+
597
 
+    def parseVersion(self, conjunctive_graph, graph_version):
598
 
+        self.parseProperties(conjunctive_graph, graph_version)
599
 
+        self.encodings = []
600
 
+        for e in conjunctive_graph.objects(graph_version, PLAY['manifestedAs']):
601
 
+          encoding = Encoding()
602
 
+          encoding.parseEncoding(conjunctive_graph, e)
603
 
+          self.encodings.append(encoding)
604
 
+
605
 
+###############################################################################
606
 
+
607
 
+'''
608
 
+Encoding: a specific encoding of an Episode (format/bitrate/size etc.)
609
 
+'''
610
 
+class Encoding(UriPlayObject):
611
 
+    __slots__ = [ 'container_format', 'bitrate', 'size', 'video_codec',
612
 
+                  'video_bitrate', 'video_fps', 'video_height', 'video_width',
613
 
+                  'audio_codec', 'audio_bitrate', 'audio_channels',
614
 
+                  'locations', 'required_caps' ]
615
 
+
616
 
+    def __init__(self):
617
 
+        self.required_caps = None
618
 
+        self.rdf_attribute_mapping = [
619
 
+            ( PLAY['dataContainerFormat'], 'container_format' ),
620
 
+            ( PLAY['bitRate'], 'bitrate' ),
621
 
+            ( PLAY['dataSize'], 'size' ),
622
 
+            ( PLAY['videoCoding'], 'video_codec' ),
623
 
+            ( PLAY['videoBitrate'], 'video_bitrate' ),
624
 
+            ( PLAY['videoFrameRate'], 'video_fps' ),
625
 
+            ( PLAY['videoVerticalSize'], 'video_height' ),
626
 
+            ( PLAY['videoHorizontalSize'], 'video_width' ),
627
 
+            ( PLAY['audioCoding'], 'audio_codec' ),
628
 
+            ( PLAY['audioBitrate'], 'audio_bitrate' ),
629
 
+            ( PLAY['audioChannels'], 'audio_channels' )]
630
 
+
631
 
+    def parseEncoding(self, conjunctive_graph, graph_encoding):
632
 
+        self.parseProperties(conjunctive_graph, graph_encoding)
633
 
+        self.locations = []
634
 
+        for l in conjunctive_graph.objects(graph_encoding, PLAY['availableAt']):
635
 
+          location = Location()
636
 
+          location.parseLocation(conjunctive_graph, l)
637
 
+          self.locations.append(location)
638
 
+          self.required_caps = self.postProcessCodecs()
639
 
+
640
 
+    def postProcessCodecs(self):
641
 
+        required_caps = gst.Caps()
642
 
+        if self.video_codec:
643
 
+          self.video_codec = self.video_codec.lower()
644
 
+          if self.video_codec in video_map:
645
 
+            required_caps.append(gst.Caps(video_map[self.video_codec]))
646
 
+          else:
647
 
+            gst.warning('unmapped video codec ' + self.video_codec)
648
 
+            return None
649
 
+        if self.audio_codec:
650
 
+          self.audio_codec = self.audio_codec.lower()
651
 
+          if self.audio_codec in audio_map:
652
 
+            required_caps.append(gst.Caps(audio_map[self.audio_codec]))
653
 
+          else:
654
 
+            gst.warning('unmapped audio codec ' + self.audio_codec)
655
 
+            return None
656
 
+        if self.container_format:
657
 
+          self.container_format = self.container_format.lower()
658
 
+          if self.container_format in container_map:
659
 
+            required_caps.append(gst.Caps(container_map[self.container_format]))
660
 
+          else:
661
 
+            gst.warning('unmapped container format ' + self.container_format)
662
 
+            return None
663
 
+
664
 
+        if not required_caps.is_empty():
665
 
+          return required_caps
666
 
+        else:
667
 
+          return None
668
 
+
669
 
+    def isUsable(self):
670
 
+        global codec_cache
671
 
+
672
 
+        if self.required_caps:
673
 
+          return codec_cache.isInstalledOrInstallable(self.required_caps)
674
 
+        else:
675
 
+          return False
676
 
+
677
 
+    def getBitrate(self):
678
 
+      if not self.bitrate:
679
 
+        return 0
680
 
+      return eval(self.bitrate)
681
 
+
682
 
+    def getBestLocation(self):
683
 
+        locations = self.locations
684
 
+        random.shuffle(locations)
685
 
+        for loc in locations:
686
 
+          if loc.isUsable():
687
 
+            return loc
688
 
+        return None
689
 
+
690
 
+###############################################################################
691
 
+
692
 
+'''
693
 
+Location: location (URI) of a specific encoding
694
 
+'''
695
 
+class Location(UriPlayObject):
696
 
+    __slots__ = [ 'uri', 'type', 'sub_type', 'is_live' ]
697
 
+
698
 
+    # Note: type, subType and isLive are more often not available than available
699
 
+    def __init__(self):
700
 
+        self.rdf_attribute_mapping = [
701
 
+            ( PLAY['uri'], 'uri' ),
702
 
+            ( PLAY['transportType'], 'type' ),
703
 
+            ( PLAY['transportSubType'], 'sub_type' ),
704
 
+            ( PLAY['transportIsLive'], 'is_live' )]
705
 
+
706
 
+    def parseLocation(self, conjunctive_graph, graph_location):
707
 
+        self.parseProperties(conjunctive_graph, graph_location)
708
 
+
709
 
+    def isUsable(self):
710
 
+        if self.uri and self.uri.startswith('http'):
711
 
+          return True
712
 
+        return False
713
 
+
714
 
+###############################################################################
715
 
+
716
 
+'''
717
 
+ContentPool: downloads rdf file with available content and caches it locally,
718
 
+             then parses the file and announces new brands and brands where
719
 
+             the episode listing has changed. The cached file is saved with
720
 
+             the ETag from the server/gio as part of the filename, so we can
721
 
+             easily compare the tag to the server's later to check if we have
722
 
+             to update the file or not (not that ETag here means what we get
723
 
+             from the gio.FileInfo on the remote uri, and never refers to a
724
 
+             gio-generated ETag for the local cache file, since those two
725
 
+             are not comparable)
726
 
+'''
727
 
+# TODO:
728
 
+#  - maybe derive from list store or filtermodel directly?
729
 
+#  - aggregate codec-cache-loaded and loading-done into loading-done internally,
730
 
+#    so caller doesn't have to worry about that
731
 
+class ContentPool(gobject.GObject):
732
 
+    __slots__ = [ 'cache_dir', 'brands' ]
733
 
+
734
 
+    __gsignals__ = dict(codec_cache_loaded=(gobject.SIGNAL_RUN_LAST, None, ()),
735
 
+                        progress_message=(gobject.SIGNAL_RUN_LAST, None, (str, )),
736
 
+                        loading_error=(gobject.SIGNAL_RUN_LAST, None, (str, )),
737
 
+                        loading_done=(gobject.SIGNAL_RUN_LAST, None, ()))
738
 
+
739
 
+    CACHE_FILE_PREFIX = 'content-'
740
 
+    CACHE_FILE_SUFFIX = '.cache'
741
 
+    AVAILABLE_CONTENT_URI = 'http://open.bbc.co.uk/rad/uriplay/availablecontent'
742
 
+    MAX_CACHE_FILE_AGE = 2*3600  # 2 hours
743
 
+
744
 
+    def __init__(self):
745
 
+        gobject.GObject.__init__ (self)
746
 
+
747
 
+        self.brands = []
748
 
+        self.cache_dir = os.path.join(BaseDirectory.xdg_cache_home, 'totem',
749
 
+                                      'plugins', 'bbc')
750
 
+        try:
751
 
+          os.makedirs(self.cache_dir)
752
 
+          gst.log('created cache directory ' + self.cache_dir)
753
 
+        except OSError, err:
754
 
+          if err.errno == errno.EEXIST:
755
 
+            gst.log('cache directory ' + self.cache_dir + ' already exists')
756
 
+          else:
757
 
+            gst.error('failed to create cache directory ' + self.cache_dir +
758
 
+                      ': ' + err.strerror)
759
 
+            self.cache_dir = None
760
 
+
761
 
+    def _on_codec_cache_loaded(self, pool):
762
 
+        self.emit('codec-cache-loaded')
763
 
+
764
 
+    ''' returns True if the given filename refers to one of our cache files '''
765
 
+    def isCacheFileName(self, filename):
766
 
+      if not filename.startswith(self.CACHE_FILE_PREFIX):
767
 
+        return False
768
 
+      if not filename.endswith(self.CACHE_FILE_SUFFIX):
769
 
+        return False
770
 
+      return True
771
 
+
772
 
+    ''' removes all cache files that don't relate to the given etag '''
773
 
+    def deleteStaleCacheFiles(self, except_etag=None):
774
 
+        try:
775
 
+          for fn in dircache.listdir(self.cache_dir):
776
 
+            if self.isCacheFileName(fn):
777
 
+              if except_etag == None or fn.find(except_etag) < 0:
778
 
+                try:
779
 
+                  gst.log('deleting stale cache file ' + fn)
780
 
+                  os.remove(os.path.join(self.cache_dir,fn))
781
 
+                except OSError:
782
 
+                  pass
783
 
+        except OSError:
784
 
+          pass
785
 
+
786
 
+    ''' finds the most recent cache file and returns its file name or None'''
787
 
+    def findMostRecentCacheFile(self):
788
 
+        best_mtime = 0
789
 
+        best_name = None
790
 
+        try:
791
 
+          gst.log('Looking for cache files in ' + self.cache_dir)
792
 
+          for fn in dircache.listdir(self.cache_dir):
793
 
+            if self.isCacheFileName(fn):
794
 
+              mtime = os.stat(os.path.join(self.cache_dir,fn)).st_mtime
795
 
+              gst.log('Found cache file %s, mtime %ld' % (fn, long(mtime)))
796
 
+              if mtime > best_mtime:
797
 
+                best_name = fn
798
 
+                best_mtime = mtime
799
 
+        except OSError, err:
800
 
+          gst.debug("couldn't inspect cache directory %s: %s" % (self.cache_dir, err.strerror))
801
 
+          return None
802
 
+
803
 
+        if not best_name:
804
 
+          gst.log('No cache file found')
805
 
+          return None
806
 
+
807
 
+        return best_name
808
 
+
809
 
+    ''' gets the ETag for the most recent cache file, or None '''
810
 
+    def getCacheETag(self):
811
 
+        etag = self.findMostRecentCacheFile()
812
 
+        if not etag:
813
 
+          return None
814
 
+        prefix_len = len(self.CACHE_FILE_PREFIX)
815
 
+        suffix_len = len(self.CACHE_FILE_SUFFIX)
816
 
+        etag = etag[prefix_len:-suffix_len]
817
 
+        gst.log('ETag: ' + etag)
818
 
+        return etag
819
 
+
820
 
+    ''' makes a full filename from an ETag '''
821
 
+    def createCacheFileName(self, etag):
822
 
+        if not etag:
823
 
+          gst.debug('No ETag, using dummy ETag as fallback')
824
 
+          etag = '000000-00000-00000000'
825
 
+        fn = self.CACHE_FILE_PREFIX + etag +  self.CACHE_FILE_SUFFIX
826
 
+        return os.path.join(self.cache_dir, fn)
827
 
+
828
 
+    def parse_async(self, cache_fn):
829
 
+        self.emit('progress-message', 'Parsing available content list ...')
830
 
+        thread.start_new_thread(self._parsing_thread, (cache_fn, ))
831
 
+
832
 
+    def _parsing_thread(self, cache_fn):
833
 
+        def _parse_idle_cb(err_msg, brands):
834
 
+            self.brands = brands
835
 
+            gst.info('Parsing done: %d brands' % (len(self.brands)))
836
 
+            if err_msg:
837
 
+              self.emit('loading-error', err_msg)
838
 
+            else:
839
 
+              self.emit('loading-done')
840
 
+            return False
841
 
+
842
 
+        err_msg = None
843
 
+        brands = []
844
 
+        gst.debug('Loading ' + cache_fn)
845
 
+        store = ConjunctiveGraph()
846
 
+        try:
847
 
+          gst.debug('Reading RDF file ...')
848
 
+          store.load(cache_fn)
849
 
+          gst.debug('Parsing ' + cache_fn)
850
 
+          brands = self.parseBrands(store)
851
 
+        except:
852
 
+          gst.warning('Problem parsing RDF')
853
 
+          err_msg = 'Could not parse available content list'
854
 
+        finally:
855
 
+          gst.debug('Parsing done, marshalling result into main thread')
856
 
+          gobject.idle_add(_parse_idle_cb, err_msg, brands)
857
 
+
858
 
+    def _format_size_for_display(self, size):
859
 
+        if size < 1024:
860
 
+          return '%d bytes' % size
861
 
+        if size < 1024*1024:
862
 
+          return '%.1f kB' % (size / 1024.0)
863
 
+        else:
864
 
+          return '%.1f MB' % (size / (1024.0*1024.0))
865
 
+
866
 
+    def load_async(self):
867
 
+        def _query_done_cb(remote_file, result):
868
 
+            # mutable container so subfunctions can share access
869
 
+            # chunks, total_len
870
 
+            pdata = [ [], 0 ] 
871
 
+
872
 
+            def _read_async_cb(instream, result):
873
 
+                try:
874
 
+                  partial_data = instream.read_finish(result)
875
 
+                  gst.log('Read partial chunk of %d bytes' % (len(partial_data)))
876
 
+                  chunks = pdata[0]
877
 
+                  bytes_read = pdata[1]
878
 
+                  if len(partial_data) == 0:                  
879
 
+                    instream.close()
880
 
+                    outstream = cache_file.create(gio.FILE_CREATE_NONE)
881
 
+                    for chunk in chunks:
882
 
+                      outstream.write(chunk)
883
 
+                    outsize = outstream.query_info('*').get_size()
884
 
+                    outstream.close()
885
 
+                    gst.info('Wrote %ld bytes' % (outsize))
886
 
+                    self.parse_async(cache_fn)
887
 
+                  else:
888
 
+                    chunks.append(partial_data)
889
 
+                    bytes_read += len(partial_data)
890
 
+                    pdata[0] = chunks
891
 
+                    pdata[1] = bytes_read
892
 
+                    instream.read_async(10240, _read_async_cb, io_priority=glib.PRIORITY_LOW-1)
893
 
+                    self.emit('progress-message',
894
 
+                              'Downloading available content list ... ' + '(' +
895
 
+                              self._format_size_for_display(bytes_read) + ')')
896
 
+                except IOError, e:
897
 
+                  gst.warning('Error downloading ' + self.AVAILABLE_CONTENT_URI)
898
 
+                  instream.close()
899
 
+                  try:
900
 
+                    cache_file.delete()
901
 
+                  finally:
902
 
+                    self.emit('loading-error', 'Error downloading available content list')
903
 
+
904
 
+            # _query_done_cb start:
905
 
+            gst.log('Query done')
906
 
+            try:
907
 
+              remote_info = remote_file.query_info_finish(result)
908
 
+            except Exception, e:
909
 
+              # bail out if we can't query, not much point trying to download
910
 
+              gst.warning('Could not query %s: %s' % (self.AVAILABLE_CONTENT_URI, e.message))
911
 
+              self.emit('loading-error', 'Could not connect to server')
912
 
+              return
913
 
+
914
 
+            gst.log('Got info, querying etag')
915
 
+            remote_etag = remote_info.get_etag()
916
 
+            if remote_etag:
917
 
+              remote_etag = remote_etag.strip('"')
918
 
+              gst.log('Remote etag: ' + remote_etag)
919
 
+
920
 
+            cache_fn = self.createCacheFileName(remote_etag)
921
 
+            cache_file = gio.File(cache_fn)
922
 
+
923
 
+            # if file already exists, get size to double-check against server's
924
 
+            try:
925
 
+              cache_size = cache_file.query_info('standard::size').get_size()
926
 
+            except:
927
 
+              cache_size = 0
928
 
+            finally:
929
 
+              if etag and remote_etag and etag == remote_etag:
930
 
+                remote_size = remote_info.get_size()
931
 
+                if remote_size <= 0 or cache_size == remote_size:
932
 
+                  gst.log('Cache file is up-to-date, nothing to do')
933
 
+                  self.parse_async(cache_fn)
934
 
+                  return
935
 
+
936
 
+            # delete old cache file if it exists
937
 
+            try:
938
 
+              cache_file.delete()
939
 
+            except:
940
 
+              pass
941
 
+
942
 
+            # FIXME: use gio.File.copy_async() once it's wrapped
943
 
+            remote_file.read().read_async(10240, _read_async_cb, io_priority=glib.PRIORITY_LOW-1)
944
 
+            gst.info('copying ' + self.AVAILABLE_CONTENT_URI + ' -> ' + cache_fn)
945
 
+            self.emit('progress-message', 'Downloading available content list ...')
946
 
+            return
947
 
+
948
 
+        # load_async start:
949
 
+        gst.log('starting loading')
950
 
+
951
 
+        # init global singleton variable codec_cache, if needed
952
 
+        global codec_cache
953
 
+
954
 
+        if not codec_cache:
955
 
+          codec_cache = CodecCache()
956
 
+          codec_cache.connect('loaded', self._on_codec_cache_loaded)
957
 
+          codec_cache.reload_async()
958
 
+
959
 
+        etag = self.getCacheETag()
960
 
+        if etag:
961
 
+          gst.log('Cached etag: ' + etag)
962
 
+          self.deleteStaleCacheFiles(etag)
963
 
+          existing_cache_fn = self.createCacheFileName(etag)
964
 
+          existing_cache_file = gio.File(existing_cache_fn)
965
 
+          existing_cache_info = existing_cache_file.query_info('time::modified')
966
 
+          existing_cache_mtime = existing_cache_info.get_modification_time()
967
 
+          # if the cache file is not older than N minutes/hours/days, then
968
 
+          # we'll just go ahead and use it instead of downloading a new one,
969
 
+          # even if it's not perfectly up-to-date.
970
 
+          # FIXME: UI should have a way to force an update
971
 
+          secs_since_update = time.time() - existing_cache_mtime
972
 
+          if secs_since_update >= 0 and secs_since_update < self.MAX_CACHE_FILE_AGE:
973
 
+            gst.log('Cache file is fairly recent, last updated %f secs ago' % (secs_since_update))
974
 
+            self.parse_async(existing_cache_fn)
975
 
+            return
976
 
+        else:
977
 
+          gst.log('Cached etag: None')
978
 
+
979
 
+        # CHECKME: what happens if http is not available as protocol?
980
 
+        remote_file = gio.File(self.AVAILABLE_CONTENT_URI)
981
 
+        gst.log('Contacting server ' + self.AVAILABLE_CONTENT_URI)
982
 
+        self.emit('progress-message', 'Connecting to server ...')
983
 
+        remote_file.query_info_async(_query_done_cb, '*')
984
 
+
985
 
+    def parseBrands(self, graph):
986
 
+        brands = []
987
 
+        for b in graph.subjects(RDF.type, PO['Brand']):
988
 
+          brand = Brand()
989
 
+          brand.parseBrand(graph, b)
990
 
+          brands.append(brand)
991
 
+          gst.log('[%3d eps] %s %s' % (len(brand.episodes), brand.title, brand.genres))
992
 
+        return brands
993
 
+
994
 
+    ''' returns array of brands which can potentially be played '''
995
 
+    def getUsableBrands(self):
996
 
+        usable_brands = []
997
 
+        for brand in self.brands:
998
 
+          if brand.hasUsableEpisodes():
999
 
+            usable_brands.append(brand)
1000
 
+        return usable_brands
1001
 
+
1002
 
+
1003
 
+###############################################################################
1004
 
+
1005
 
+class ContentView(gtk.TreeView):
1006
 
+    __slots__ = [ 'pool', 'content_pool_loaded', 'codec_cache_loaded', 'genre_pool' ]
1007
 
+    __gsignals__ = dict(play_episode=
1008
 
+                        (gobject.SIGNAL_RUN_LAST, None,
1009
 
+                         (object,))) # Episode
1010
 
+
1011
 
+    SORT_ID_1 = 0
1012
 
+
1013
 
+    def __init__(self):
1014
 
+        gtk.TreeView.__init__ (self)
1015
 
+        self.setupModel()
1016
 
+
1017
 
+        self.set_headers_visible(False)
1018
 
+
1019
 
+        self.connect('row-activated', self.onRowActivated)
1020
 
+
1021
 
+       self.set_property('has-tooltip', True)
1022
 
+       self.connect('query-tooltip', self.onQueryTooltip)
1023
 
+
1024
 
+        self.set_message('Loading ...')
1025
 
+
1026
 
+        self.pool = ContentPool()
1027
 
+        self.pool.connect('codec-cache-loaded', self._on_codec_cache_loaded)
1028
 
+        self.pool.connect('progress-message', self._on_content_pool_message)
1029
 
+        self.pool.connect('loading-error', self._on_content_pool_error)
1030
 
+        self.pool.connect('loading-done', self._on_content_pool_loading_done)
1031
 
+        self.codec_cache_loaded = False
1032
 
+        self.content_pool_loaded = False
1033
 
+        self.genre_pool = genres.GenrePool()
1034
 
+
1035
 
+    def load(self):
1036
 
+        self.pool.load_async()
1037
 
+        gst.log('started loading')
1038
 
+
1039
 
+    def _on_content_pool_message(self, content_pool, msg):
1040
 
+        self.set_message(msg)
1041
 
+
1042
 
+    def _on_content_pool_error(self, content_pool, err_msg):
1043
 
+        gst.warning('Failed to load available content: ' + err_msg)
1044
 
+        self.set_message(err_msg)
1045
 
+
1046
 
+    def _on_content_pool_loading_done(self, content_pool):
1047
 
+        gst.log('content pool loaded')
1048
 
+        self.content_pool_loaded = True
1049
 
+        if self.codec_cache_loaded:
1050
 
+          self.populate()
1051
 
+
1052
 
+    def _on_codec_cache_loaded(self, content_pool):
1053
 
+        gst.log('codec cache loaded, refilter')
1054
 
+        self.codec_cache_loaded = True
1055
 
+        #self.filter.refilter() FIXME: we don't filter at the moment
1056
 
+        if self.content_pool_loaded:
1057
 
+          self.populate()
1058
 
+
1059
 
+    def populate_add_genre(self, genre, parent_iter):
1060
 
+        _iter = self.store.append(parent_iter, [None, None, None, genre])
1061
 
+        for child_genre in genre.children:
1062
 
+          self.populate_add_genre(child_genre, _iter)
1063
 
+        for brand in genre.brands:
1064
 
+          brand_iter = self.store.append(_iter, [brand, None, None, None])
1065
 
+          for ep in brand.episodes:
1066
 
+            self.store.append(brand_iter, [brand, ep, None, None])
1067
 
+        return _iter
1068
 
+
1069
 
+    def populate(self):
1070
 
+        gst.log('populating treeview')
1071
 
+
1072
 
+        brands = self.pool.getUsableBrands()
1073
 
+        gst.info('%d brands with usable episodes/encodings' % (len(brands)))
1074
 
+
1075
 
+        # build genre tree in memory and add brands to genre objects
1076
 
+        self.genre_pool.clear()
1077
 
+        for brand in brands:
1078
 
+          for genre_shortref in brand.genres:
1079
 
+            genre = self.genre_pool.get_genre(genre_shortref)
1080
 
+            genre.add_brand(brand)
1081
 
+
1082
 
+        # add everything to the list store
1083
 
+        self.store.clear()
1084
 
+        toplevel_iters = []
1085
 
+        for toplevel_genre in self.genre_pool.get_toplevel_genres():
1086
 
+          _iter = self.populate_add_genre(toplevel_genre, None)          
1087
 
+          toplevel_iters.append(_iter)
1088
 
+          
1089
 
+        # now make all this visible (view might be showing model with message)
1090
 
+        self.set_model(self.filter)
1091
 
+
1092
 
+        # expand top-level categories
1093
 
+        for _iter in toplevel_iters:
1094
 
+          path = self.store.get_path(_iter)
1095
 
+          self.expand_row(path, False)
1096
 
+
1097
 
+    def get_brand_tooltip(self, brand):
1098
 
+      if not brand or not brand.description:
1099
 
+        return None
1100
 
+      return '<b>%s</b>\n<i>%s</i>' % (gobject.markup_escape_text(brand.title),
1101
 
+                                        gobject.markup_escape_text(brand.description))
1102
 
+
1103
 
+    def get_episode_tooltip(self, brand, episode):
1104
 
+      if not episode or not episode.description:
1105
 
+        return None
1106
 
+      return '<b>%s</b>\n<b><small>%s</small></b>\n<i>%s</i>' % (gobject.markup_escape_text(brand.title),
1107
 
+                                                                 gobject.markup_escape_text(episode.title),
1108
 
+                                                                 gobject.markup_escape_text(episode.description))
1109
 
+
1110
 
+    def onQueryTooltip(self, view, x, y, keyboard_tip, tip):
1111
 
+      try:
1112
 
+        model, path, _iter = self.get_tooltip_context(x, y, keyboard_tip)
1113
 
+      except:
1114
 
+        return False # probably no content yet
1115
 
+
1116
 
+      brand, episode, msg, genre = model.get(_iter, 0, 1, 2, 3)
1117
 
+      if msg or genre:
1118
 
+        return False
1119
 
+      if brand and not episode:
1120
 
+        markup = self.get_brand_tooltip(brand)
1121
 
+      elif brand and episode:
1122
 
+        markup = self.get_episode_tooltip(brand, episode)
1123
 
+      else:
1124
 
+        markup = None
1125
 
+      if markup:
1126
 
+        tip.set_markup(markup)
1127
 
+      else:
1128
 
+        tip.set_text('No details available') # FIXME: translate
1129
 
+      return True
1130
 
+
1131
 
+    def onRowActivated(self, view, path, col):
1132
 
+        model = self.get_model()
1133
 
+        if model:
1134
 
+          _iter = model.get_iter(path)
1135
 
+          brand, episode = self.get_model().get(_iter, 0, 1)
1136
 
+          if episode:
1137
 
+            self.emit('play-episode', episode)
1138
 
+
1139
 
+    def renderGenreCell(self, column, renderer, model, _iter, genre):
1140
 
+        markup = '<b>%s</b>' % \
1141
 
+                 (gobject.markup_escape_text(genre.label))
1142
 
+        renderer.set_property('markup', markup)
1143
 
+
1144
 
+    def renderBrandCell(self, column, renderer, model, _iter, brand):
1145
 
+        markup = '<b><small>%s <span color="LightGray">(%d)</span></small></b>' % \
1146
 
+                 (gobject.markup_escape_text(brand.title), len(brand.episodes))
1147
 
+        renderer.set_property('markup', markup)
1148
 
+
1149
 
+    def renderEpisodeCell(self, column, renderer, model, _iter, brand, episode):
1150
 
+        markup = '<span><small>%s</small></span>' % (gobject.markup_escape_text(episode.title))
1151
 
+        renderer.set_property('markup', markup)
1152
 
+
1153
 
+    def renderMessageCell(self, column, renderer, model, _iter, msg):
1154
 
+        markup = '<i>%s</i>' % (gobject.markup_escape_text(msg))
1155
 
+        renderer.set_property('markup', markup)
1156
 
+        
1157
 
+    def renderCell(self, column, renderer, model, _iter):
1158
 
+        brand, episode, msg, genre = model.get(_iter, 0, 1, 2, 3)
1159
 
+        if msg:
1160
 
+          self.renderMessageCell(column, renderer, model, _iter, msg)
1161
 
+        elif genre:
1162
 
+          self.renderGenreCell(column, renderer, model, _iter, genre)
1163
 
+        elif not episode:
1164
 
+          self.renderBrandCell(column, renderer, model, _iter, brand)
1165
 
+        else:
1166
 
+          self.renderEpisodeCell(column, renderer, model, _iter, brand, episode)
1167
 
+
1168
 
+    # there must be a more elegant way to do this in python
1169
 
+    def sortFunc(self, model, iter1, iter2):
1170
 
+        brand1, episode1, genre1 = model.get(iter1, 0, 1, 3)
1171
 
+        brand2, episode2, genre2 = model.get(iter2, 0, 1, 3)
1172
 
+
1173
 
+        # genres are sorted by genre.sort_rank
1174
 
+        if genre1 and genre2:
1175
 
+          if genre1.sort_rank != genre2.sort_rank:
1176
 
+            return genre1.sort_rank - genre2.sort_rank
1177
 
+          else:
1178
 
+            s1 = genre1.label
1179
 
+            s2 = genre2.label
1180
 
+
1181
 
+        # genre always comes before any other siblings (like brands or episodes)
1182
 
+        elif genre1:
1183
 
+          return -1
1184
 
+        elif genre2:
1185
 
+          return 1
1186
 
+
1187
 
+        # brands and episodes are sorted alphabetically by title
1188
 
+        elif not episode1 or not episode2:
1189
 
+          s1 = brand1.title
1190
 
+          s2 = brand2.title
1191
 
+        elif episode1 and episode2:
1192
 
+          s1 = episode1.title
1193
 
+          s2 = episode2.title
1194
 
+        else:
1195
 
+          gst.warning('should not be reached (should be genre label comparison)')
1196
 
+
1197
 
+        # string comparison
1198
 
+        if s1 == s2:
1199
 
+          return 0
1200
 
+        elif s1 > s2:
1201
 
+          return 1
1202
 
+        else:
1203
 
+          return -1
1204
 
+
1205
 
+    def set_message(self, msg):
1206
 
+        self.msg_store.clear()
1207
 
+        self.msg_store.append(None, [None, None, msg, None])
1208
 
+        self.set_model(self.msg_store)
1209
 
+        gst.log('set message "' + msg + '"')
1210
 
+
1211
 
+    def setupModel(self):
1212
 
+        # columns: Brand, Episode, message string, Genre
1213
 
+        self.msg_store = gtk.TreeStore(object, object, str, object)
1214
 
+        self.store = gtk.TreeStore(object, object, str, object)
1215
 
+        self.filter = self.store.filter_new()
1216
 
+
1217
 
+        column = gtk.TreeViewColumn()
1218
 
+        renderer = gtk.CellRendererText()
1219
 
+        renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
1220
 
+        column.pack_start(renderer, expand=True)
1221
 
+        column.set_cell_data_func(renderer, self.renderCell)
1222
 
+        self.append_column(column)
1223
 
+        self.store.set_sort_func(self.SORT_ID_1, self.sortFunc)
1224
 
+        self.store.set_sort_column_id(self.SORT_ID_1, gtk.SORT_ASCENDING)
1225
 
+
1226
 
+if __name__ == "__main__":
1227
 
+    # ensure the caps strings in the container/video/audio map are parsable
1228
 
+    for cs in video_map:
1229
 
+      caps = gst.Caps(video_map[cs])
1230
 
+    for cs in audio_map:
1231
 
+      caps = gst.Caps(audio_map[cs])
1232
 
+    for cs in container_map:
1233
 
+      caps = gst.Caps(container_map[cs])
1234
 
+    # test window
1235
 
+    window = gtk.Window()
1236
 
+    scrollwin = gtk.ScrolledWindow()
1237
 
+    scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
1238
 
+    window.add(scrollwin)
1239
 
+    view = ContentView()
1240
 
+    view.load()
1241
 
+    scrollwin.add(view)
1242
 
+    window.show_all()
1243
 
+    gtk.main()
1244
 
+
1245
 
Index: b/src/plugins/bbc/genres.py
1246
 
===================================================================
1247
 
--- /dev/null
1248
 
+++ b/src/plugins/bbc/genres.py
1249
 
@@ -0,0 +1,251 @@
1250
 
+#!/usr/bin/python
1251
 
+# coding=UTF-8
1252
 
+#
1253
 
+# Copyright (C) 2008 Tim-Philipp Müller <tim.muller@collabora.co.uk>
1254
 
+# Copyright (C) 2008 Canonical Ltd.
1255
 
+#
1256
 
+# This program is free software; you can redistribute it and/or modify
1257
 
+# it under the terms of the GNU General Public License as published by
1258
 
+# the Free Software Foundation; either version 2 of the License, or
1259
 
+# (at your option) any later version.
1260
 
+#
1261
 
+# This program is distributed in the hope that it will be useful,
1262
 
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1263
 
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
1264
 
+# GNU General Public License for more details.
1265
 
+#
1266
 
+# You should have received a copy of the GNU General Public License
1267
 
+# along with this program; if not, write to the Free Software
1268
 
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
1269
 
+#
1270
 
+# The Totem project hereby grant permission for non-gpl compatible GStreamer
1271
 
+# plugins to be used and distributed together with GStreamer and Totem. This
1272
 
+# permission are above and beyond the permissions granted by the GPL license
1273
 
+# Totem is covered by.
1274
 
+#
1275
 
+# See license_change file for details.
1276
 
+
1277
 
+import gobject
1278
 
+gobject.threads_init()
1279
 
+import pygst
1280
 
+pygst.require ("0.10")
1281
 
+import gst
1282
 
+
1283
 
+shortref_to_label_map = {
1284
 
+  "childrens": "Children's",
1285
 
+  "childrens/activities": "Activities",
1286
 
+  "childrens/drama": "Drama",
1287
 
+  "childrens/entertainmentandcomedy": "Entertainment & Comedy",
1288
 
+  "childrens/factual": "Factual",
1289
 
+  "childrens/music": "Music",
1290
 
+  "childrens/news": "News",
1291
 
+  "childrens/sport": "Sport",
1292
 
+  "drama": "Drama",
1293
 
+  "drama/actionandadventure": "Action & Adventure",
1294
 
+  "drama/biographical": "Biographical",
1295
 
+  "drama/classicandperiod": "Classic & Period",
1296
 
+  "drama/crime": "Crime",
1297
 
+  "drama/historical": "Historical",
1298
 
+  "drama/horrorandsupernatural": "Horror & Supernatural",
1299
 
+  "drama/legalandcourtroom": "Legal & Courtroom",
1300
 
+  "drama/medical": "Medical",
1301
 
+  "drama/musical": "Musical",
1302
 
+  "drama/political": "Political",
1303
 
+  "drama/psychological": "Psychological",
1304
 
+  "drama/relationshipsandromance": "Relationships & Romance",
1305
 
+  "drama/scifiandfantasy": "SciFi & Fantasy",
1306
 
+  "drama/soaps": "Soaps",
1307
 
+  "drama/spiritual": "Spiritual",
1308
 
+  "drama/thriller": "Thriller",
1309
 
+  "drama/waranddisaster": "War & Disaster",
1310
 
+  "drama/western": "Western",
1311
 
+  "entertainmentandcomedy": "Entertainment & Comedy",
1312
 
+  "entertainmentandcomedy/impressionists": "Impressionists",
1313
 
+  "entertainmentandcomedy/satire": "Satire",
1314
 
+  "entertainmentandcomedy/sitcoms": "Sitcoms",
1315
 
+  "entertainmentandcomedy/sketch": "Sketch",
1316
 
+  "entertainmentandcomedy/spoof": "Spoof",
1317
 
+  "entertainmentandcomedy/standup": "Standup",
1318
 
+  "entertainmentandcomedy/varietyshows": "Variety Shows",
1319
 
+  "factual": "Factual",
1320
 
+  "factual/antiques": "Antiques",
1321
 
+  "factual/artscultureandthemedia": "Arts, Culture & the Media",
1322
 
+  "factual/beautyandstyle": "Beauty & Style",
1323
 
+  "factual/carsandmotors": "Cars & Motors",
1324
 
+  "factual/cinema": "Cinema",
1325
 
+  "factual/consumer": "Consumer",
1326
 
+  "factual/crimeandjustice": "Crime & Justice",
1327
 
+  "factual/disability": "Disability",
1328
 
+  "factual/familiesandrelationships": "Families & Relationships",
1329
 
+  "factual/foodanddrink": "Food & Drink",
1330
 
+  "factual/healthandwellbeing": "Health & Wellbeing",
1331
 
+  "factual/history": "History",
1332
 
+  "factual/homesandgardens": "Homes & Gardens",
1333
 
+  "factual/lifestories": "Life Stories",
1334
 
+  "factual/money": "Money",
1335
 
+  "factual/petsandanimals": "Pets & Animals",
1336
 
+  "factual/politics": "Politics",
1337
 
+  "factual/sciencenatureandenvironment": "Science, Nature & Environment",
1338
 
+  "factual/travel": "Travel",
1339
 
+  "learning": "Learning",
1340
 
+  "learning/1119": "Age 11-19",
1341
 
+  "learning/511": "Age 5-11",
1342
 
+  "learning/adults": "Adults",
1343
 
+  "learning/preschool": "Pre-School",
1344
 
+  "music": "Music",
1345
 
+  "music/classicpopandrock": "Classic Pop & Rock",
1346
 
+  "music/classical": "Classical",
1347
 
+  "music/country": "Country",
1348
 
+  "music/danceandelectronica": "Dance & Electronica",
1349
 
+  "music/desi": "Desi",
1350
 
+  "music/easylisteningsoundtracksandmusicals": "Easy Listening, Soundtracks & Musicals",
1351
 
+  "music/folk": "Folk",
1352
 
+  "music/hiphoprnbanddancehall": "Hip Hop, RnB & Dancehall",
1353
 
+  "music/jazzandblues": "Jazz & Blues",
1354
 
+  "music/popandchart": "Pop & Chart",
1355
 
+  "music/rockandindie": "Rock & Indie",
1356
 
+  "music/soulandreggae": "Soul & Reggae",
1357
 
+  "music/world": "World",
1358
 
+  "news": "News",
1359
 
+  "religionandethics": "Religion & Ethics",
1360
 
+  "sport": "Sport",
1361
 
+  "sport/archery": "Archery",
1362
 
+  "sport/athletics": "Athletics",
1363
 
+  "sport/badminton": "Badminton",
1364
 
+  "sport/baseball": "Baseball",
1365
 
+  "sport/basketball": "Basketball",
1366
 
+  "sport/bowls": "Bowls",
1367
 
+  "sport/boxing": "Boxing",
1368
 
+  "sport/canoeing": "Canoeing",
1369
 
+  "sport/cricket": "Cricket",
1370
 
+  "sport/cycling": "Cycling",
1371
 
+  "sport/darts": "Darts",
1372
 
+  "sport/disabilitysport": "Disability Sport",
1373
 
+  "sport/diving": "Diving",
1374
 
+  "sport/equestrian": "Equestrian",
1375
 
+  "sport/fencing": "Fencing",
1376
 
+  "sport/football": "Football",
1377
 
+  "sport/gaelicgames": "Gaelic Games",
1378
 
+  "sport/golf": "Golf",
1379
 
+  "sport/gymnastics": "Gymnastics",
1380
 
+  "sport/handball": "Handball",
1381
 
+  "sport/hockey": "Hockey",
1382
 
+  "sport/horseracing": "Horse Racing",
1383
 
+  "sport/judo": "Judo",
1384
 
+  "sport/modernpentathlon": "Modern Pentathlon",
1385
 
+  "sport/motorsport": "Motorsport",
1386
 
+  "sport/olympics": "Olympics",
1387
 
+  "sport/rowing": "Rowing",
1388
 
+  "sport/rugbyleague": "Rugby League",
1389
 
+  "sport/rugbyunion": "Rugby Union",
1390
 
+  "sport/sailing": "Sailing",
1391
 
+  "sport/shinty": "Shinty",
1392
 
+  "sport/shooting": "Shooting",
1393
 
+  "sport/snooker": "Snooker",
1394
 
+  "sport/softball": "Softball",
1395
 
+  "sport/swimming": "Swimming",
1396
 
+  "sport/tabletennis": "Table Tennis",
1397
 
+  "sport/taekwondo": "Taekwondo",
1398
 
+  "sport/tennis": "Tennis",
1399
 
+  "sport/triathlon": "Triathlon",
1400
 
+  "sport/volleyball": "Volleyball",
1401
 
+  "sport/waterpolo": "Water Polo",
1402
 
+  "sport/weightlifting": "Weightlifting",
1403
 
+  "sport/wintersports": "Winter Sports",
1404
 
+  "sport/wrestling": "Wrestling",
1405
 
+  "weather": "Weather"
1406
 
+}
1407
 
+
1408
 
+# lowest = at the top
1409
 
+shortref_to_sortrank_map = {
1410
 
+  "news": 1,
1411
 
+  "childrens": 2,
1412
 
+  "drama": 3,
1413
 
+  "entertainmentandcomedy": 4,
1414
 
+  "factual": 5,
1415
 
+  "learning": 6,
1416
 
+  "music": 7,
1417
 
+  "religionandethics": 8,
1418
 
+  "sport": 9,
1419
 
+  "weather": 10
1420
 
+}
1421
 
+
1422
 
+'''
1423
 
+GenrePool: keeps track of the already-created genres, mainly so we can easily
1424
 
+           find already-existing parents for to-be-created genres
1425
 
+''' 
1426
 
+class GenrePool(object):
1427
 
+    __slots__ = [ 'genres', 'toplevel_genres' ]
1428
 
+
1429
 
+    def __init__(self):
1430
 
+        self.clear()
1431
 
+
1432
 
+    def clear(self):
1433
 
+        self.genres = { } # maps short_ref => genre object
1434
 
+
1435
 
+    def get_genre(self, short_ref):
1436
 
+        # check if genre already exists
1437
 
+        if short_ref in self.genres:
1438
 
+          return self.genres[short_ref]
1439
 
+
1440
 
+        # if not, create genre (and any parents which don't exist yet)
1441
 
+        lastslash_pos = short_ref.rfind('/')
1442
 
+        if lastslash_pos > 0:
1443
 
+          parent_ref = short_ref[0:lastslash_pos]
1444
 
+          gst.log('genre: ' + short_ref + ', parent_genre: ' + parent_ref)
1445
 
+          parent = self.get_genre(parent_ref)
1446
 
+        else:
1447
 
+          parent = None
1448
 
+
1449
 
+        genre = Genre(short_ref, parent)
1450
 
+        self.genres[short_ref] = genre
1451
 
+
1452
 
+        return genre
1453
 
+
1454
 
+    def get_toplevel_genres(self):
1455
 
+        toplevel_genres = []
1456
 
+        for genre in self.genres.values():
1457
 
+          if not genre.parent:
1458
 
+            toplevel_genres.append(genre)
1459
 
+        return toplevel_genres
1460
 
+
1461
 
+'''
1462
 
+Genre: represents a genre
1463
 
+'''
1464
 
+class Genre(object):
1465
 
+    __slots__ = [ 'short_ref', 'label', 'sort_rank', 'parent',  'children', 'brands' ]
1466
 
+
1467
 
+    def __init__(self, short_ref, parent_genre):
1468
 
+        self.short_ref = short_ref
1469
 
+
1470
 
+        if short_ref in shortref_to_label_map:
1471
 
+          self.label = shortref_to_label_map[short_ref]
1472
 
+        else:
1473
 
+          self.label = 'Unknown: ' + short_ref
1474
 
+
1475
 
+        if short_ref in shortref_to_sortrank_map:
1476
 
+          self.sort_rank = shortref_to_sortrank_map[short_ref]
1477
 
+        else:
1478
 
+          self.sort_rank = 99999
1479
 
+
1480
 
+        self.parent = parent_genre
1481
 
+        self.children = []
1482
 
+        self.brands = []
1483
 
+
1484
 
+        if parent_genre is not None:
1485
 
+          parent_genre.add_child(self)
1486
 
+
1487
 
+        gst.log('created genre ' + short_ref + ' = ' + self.label)
1488
 
+
1489
 
+    def add_child(self, child_genre):
1490
 
+        if child_genre not in self.children:
1491
 
+          self.children.append(child_genre)
1492
 
+
1493
 
+    def add_brand(self, brand):
1494
 
+        if brand not in self.brands:
1495
 
+            self.brands.append(brand)
1496
 
+            gst.log(self.short_ref + ': adding show ' + brand.title)
1497
 
+
1498
 
+if __name__ == "__main__":
1499
 
+  pass
1500
 
+
1501
 
Index: b/src/plugins/bbc/installablecodecs.py
1502
 
===================================================================
1503
 
--- /dev/null
1504
 
+++ b/src/plugins/bbc/installablecodecs.py
1505
 
@@ -0,0 +1,112 @@
1506
 
+#!/usr/bin/python
1507
 
+# coding=UTF-8
1508
 
+#
1509
 
+# Copyright (C) 2008 Tim-Philipp Müller <tim.muller@collabora.co.uk>
1510
 
+# Copyright (C) 2008 Canonical Ltd.
1511
 
+#
1512
 
+# This program is free software; you can redistribute it and/or modify
1513
 
+# it under the terms of the GNU General Public License as published by
1514
 
+# the Free Software Foundation; either version 2 of the License, or
1515
 
+# (at your option) any later version.
1516
 
+#
1517
 
+# This program is distributed in the hope that it will be useful,
1518
 
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1519
 
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
1520
 
+# GNU General Public License for more details.
1521
 
+#
1522
 
+# You should have received a copy of the GNU General Public License
1523
 
+# along with this program; if not, write to the Free Software
1524
 
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
1525
 
+#
1526
 
+# The Totem project hereby grant permission for non-gpl compatible GStreamer
1527
 
+# plugins to be used and distributed together with GStreamer and Totem. This
1528
 
+# permission are above and beyond the permissions granted by the GPL license
1529
 
+# Totem is covered by.
1530
 
+#
1531
 
+# See license_change file for details.
1532
 
+
1533
 
+import gobject
1534
 
+gobject.threads_init()
1535
 
+import pygst
1536
 
+pygst.require ("0.10")
1537
 
+import gst
1538
 
+
1539
 
+import os
1540
 
+
1541
 
+def allPackagesAvailableApt(package_descs, codec_name, apt_cache):
1542
 
+  toinstall = ''
1543
 
+  allinstalled = True
1544
 
+  for pkg_desc in package_descs:
1545
 
+    (component, pkg_name) = pkg_desc.split('/')
1546
 
+    if apt_cache.has_key(pkg_name):
1547
 
+      package = apt_cache[pkg_name]
1548
 
+      # we only care about installable ones here, not installed, since
1549
 
+      # the installed ones we can better check via the registry (also
1550
 
+      # taking into account element factory ranks etc.)
1551
 
+      gst.log('%s: package %s: installed=%d, downloadable=%d' % (codec_name, pkg_name,
1552
 
+                                                                 package.isInstalled,
1553
 
+                                                                 package.candidateDownloadable))
1554
 
+      if not package.isInstalled:
1555
 
+        allinstalled = False
1556
 
+        if not package.candidateDownloadable:
1557
 
+          gst.log('%s: not available (package %s not downloadable)' % (codec_name, pkg_name))
1558
 
+          return False
1559
 
+        elif toinstall.find(pkg_name) < 0:
1560
 
+           toinstall += pkg_name + ' '
1561
 
+
1562
 
+  # if all candidat packages are already installed, things should
1563
 
+  # be checked via the registry and there's nothing to be gained
1564
 
+  # here, so pretend the codec is not available for installation
1565
 
+  # in this case
1566
 
+  if allinstalled:
1567
 
+    gst.debug('%s: not available (all already installed)' % (codec_name))
1568
 
+    return False
1569
 
+  else:
1570
 
+    gst.debug('%s: available (install %s)' % (codec_name, toinstall[:-1]))
1571
 
+    return True
1572
 
+
1573
 
+def getInstallableCodecsUbuntu():
1574
 
+  import warnings
1575
 
+  warnings.filterwarnings("ignore", "apt API not stable yet", FutureWarning)
1576
 
+  import apt
1577
 
+  import gdbm
1578
 
+
1579
 
+  codecs = []
1580
 
+
1581
 
+  try:
1582
 
+    apt_cache = apt.Cache()
1583
 
+  except:
1584
 
+    gst.warning('Failed to read APT cache')
1585
 
+    return []
1586
 
+
1587
 
+  try:
1588
 
+    codec_map = gdbm.open('/var/cache/app-install/gai-codec-map.gdbm')
1589
 
+    for key in codec_map.keys():
1590
 
+      if not key.startswith('0.10:decoder-'):
1591
 
+        continue
1592
 
+      codec_name = key[13:]
1593
 
+      if allPackagesAvailableApt(codec_map[key].split(), codec_name, apt_cache):
1594
 
+        codecs.append(codec_name)
1595
 
+  except gdbm.error:
1596
 
+    gst.warning('gdbm error')
1597
 
+    return []
1598
 
+
1599
 
+  return codecs
1600
 
+
1601
 
+
1602
 
+def getInstallableCodecs():
1603
 
+  codecs = []
1604
 
+  if os.access('/var/cache/app-install/gai-codec-map.gdbm', os.R_OK):
1605
 
+    codecs = getInstallableCodecsUbuntu()
1606
 
+  return codecs
1607
 
+
1608
 
+
1609
 
+if __name__ == "__main__":
1610
 
+  codecs = getInstallableCodecs()
1611
 
+  if len(codecs) > 0:
1612
 
+    for codec in codecs:
1613
 
+      print "installable: %s" % (codec)
1614
 
+  else:
1615
 
+    print 'No codecs known to be installable'
1616
 
+
1617
 
+