1
Index: b/bindings/python/totem.defs
2
===================================================================
3
--- a/bindings/python/totem.defs
4
+++ b/bindings/python/totem.defs
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")
14
+ '("const-char*" "mrl")
15
+ '("const-char*" "subtitle_mrl" (null-ok) (default "NULL"))
19
(define-method action_stop
20
(of-object "TotemObject")
21
(c-name "totem_action_stop")
23
===================================================================
27
AC_SUBST(TOTEM_VERSION_MICRO)
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"
33
PLUGINDIR='${libdir}/totem/plugins'
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"
49
+ # only require python apt and gdbm where we would actually be using it, ie. on ubuntu
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"
57
+ for pymodule in rdflib.Graph xdg $bbc_py_pkg_reqs
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"
66
PKG_CHECK_MODULES(BEMUSED, bluez, [HAVE_BLUEZ=yes], [HAVE_BLUEZ=no])
67
if test "${HAVE_BLUEZ}" != "yes" ; then
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
===================================================================
79
+++ b/src/plugins/bbc/Makefile.am
81
+plugindir = $(PLUGINDIR)/bbc
82
+plugin_PYTHON = bbc.py contentview.py genres.py installablecodecs.py
84
+plugin_in_files = bbc.totem-plugin.in
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
88
+plugin_DATA = $(plugin_in_files:.totem-plugin.in=.totem-plugin)
90
+EXTRA_DIST = $(plugin_in_files) bbc.py contentview.py genres.py installablecodecs.py
92
+CLEANFILES = $(plugin_DATA)
93
+DISTCLEANFILES = $(plugin_DATA)
97
+ PYTHONPATH=$(top_srcdir)/src/plugins/bbc:$$PYTHONPATH \
98
+ pychecker $(wildcard $(top_srcdir)/src/plugins/bbc/*py)
101
+ pyflakes $(wildcard $(top_srcdir)/src/plugins/bbc/*py)
103
+check: pychecker pyflakes
107
Index: b/src/plugins/bbc/bbc.py
108
===================================================================
110
+++ b/src/plugins/bbc/bbc.py
115
+# Copyright (C) 2008 Tim-Philipp Müller <tim.muller@collabora.co.uk>
116
+# Copyright (C) 2008 Canonical Ltd.
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.
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.
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.
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.
137
+# See license_change file for details.
140
+gobject.threads_init()
142
+pygst.require ("0.10")
150
+from contentview import ContentView
152
+class BBCViewer(totem.Plugin):
153
+ def __init__ (self):
154
+ totem.Plugin.__init__ (self)
155
+ self.loaded_content = False
157
+ def mapped (self, contentview):
159
+ if not self.loaded_content:
161
+ self.loaded_content = True
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)
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)
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')
180
+ def deactivate (self, totem_object):
181
+ totem_object.remove_sidebar_page ("bbc")
182
+ self.loaded_content = False
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
193
+ gst.log('Configured connection speed #%d: %d kbit/s' % (speed_enum, speed_kbps))
196
+ def playEpisode (self, view, episode):
197
+ gst.info('Playing episode ' + episode.title)
198
+ mrl = episode.getUri(self.getConnectionSpeed())
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)
205
+ gst.error('No uri for episode ' + episode.title)
207
Index: b/src/plugins/bbc/bbc.totem-plugin.in
208
===================================================================
210
+++ b/src/plugins/bbc/bbc.totem-plugin.in
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
===================================================================
224
+++ b/src/plugins/bbc/contentview.py
229
+# Copyright (C) 2008 Tim-Philipp Müller <tim.muller@collabora.co.uk>
230
+# Copyright (C) 2008 Canonical Ltd.
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.
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.
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.
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.
251
+# See license_change file for details.
254
+# - clean up code: mixed studlyCaps and foo_bar; mixed callbacks and signals
257
+gobject.threads_init()
261
+pygst.require ("0.10")
273
+from rdflib.Graph import ConjunctiveGraph
274
+from rdflib import Namespace
275
+from rdflib import RDF
277
+from xdg import BaseDirectory
279
+import installablecodecs
283
+Define namespaces we will be using globally
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/')
293
+Global codec cache singleton
298
+Container/Audio/Video codec mappings - global for readability
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' }
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' }
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' }
371
+###############################################################################
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)
382
+class CodecCache(gobject.GObject):
383
+ __slots__ = [ 'codec_cache', 'installable_codecs' ]
385
+ __gsignals__ = dict(loaded=(gobject.SIGNAL_RUN_LAST, None, ()))
387
+ def __init__(self):
388
+ gobject.GObject.__init__ (self)
389
+ self.codec_cache = { }
390
+ self.installable_codecs = None
392
+ def reload_async(self):
393
+ gst.log('starting codec cache loading')
394
+ thread.start_new_thread(self._loading_thread, ())
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
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)
412
+ def haveDecoderForCaps(self, decoder_caps):
413
+ caps_string = decoder_caps.to_string()
415
+ if caps_string in self.codec_cache:
416
+ return self.codec_cache[caps_string]
418
+ registry = gst.registry_get_default()
419
+ features = registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY)
421
+ for feature in features:
422
+ # only take into account elements playbin will use
423
+ if feature.get_rank() < gst.RANK_MARGINAL:
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))
436
+ self.codec_cache[caps_string] = False
437
+ gst.debug('no element found that can handle ' + caps_string)
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():
445
+ if self.installable_codecs is None:
446
+ gst.log('database of installable codecs not loaded yet')
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()))
458
+###############################################################################
461
+UriPlayObject: base class for Brand, Episode, Encoding, Location etc.
463
+class UriPlayObject(object):
464
+ __slots__ = [ 'rdf_attribute_mapping' ]
466
+ def __init__(self):
467
+ self.rdf_attribute_mapping = []
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
477
+###############################################################################
480
+Brand: a show/series/group of episodes
482
+class Brand(UriPlayObject):
483
+ __slots__ = [ 'title', 'description', 'episodes', 'genres' ]
485
+ def __init__(self):
488
+ self.rdf_attribute_mapping = [ ( DC['title'], 'title' ),
489
+ ( DC['description'], 'description' ) ]
491
+ def parseBrand(self, conjunctive_graph, graph_brand):
492
+ self.parseProperties(conjunctive_graph, graph_brand)
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)
501
+ for match in conjunctive_graph.objects(graph_brand, PO['genre']):
502
+ genre_utf8 = match.encode('utf-8')
503
+ pos = genre_utf8.find('/genres/')
505
+ pos += len('/genres/')
506
+ genre = genre_utf8[pos:]
508
+ gst.warning('Unexpected genre identifier: ' + genre_utf8)
510
+ if genre not in self.genres:
511
+ self.genres.append(genre)
513
+ def hasUsableEpisodes(self):
514
+ for episode in self.episodes:
515
+ if episode.hasUsableEncodings():
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
526
+###############################################################################
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)
534
+class Episode(UriPlayObject):
535
+ __slots__ = [ 'title', 'description', 'versions', 'encodings' ]
537
+ def __init__(self):
538
+ self.encodings = []
539
+ self.rdf_attribute_mapping = [ ( DC['title'], 'title' ),
540
+ ( DC['description'], 'description' ) ]
542
+ def parseEpisode(self, conjunctive_graph, graph_episode):
543
+ self.parseProperties(conjunctive_graph, graph_episode)
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
553
+ def hasUsableEncodings(self):
554
+ for encoding in self.encodings:
555
+ if encoding.isUsable():
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():
566
+ gst.log('have encoding with bitrate: %d kbit/s' % (encoding.getBitrate()))
568
+ if encoding.getBitrate() > best_encoding.getBitrate():
569
+ if connection_speed <= 0 or encoding.getBitrate() <= connection_speed:
570
+ best_encoding = encoding
572
+ best_encoding = encoding
574
+ gst.log('best encoding has bitrate of %d kbit/s' % (best_encoding.getBitrate()))
575
+ return best_encoding
577
+ def getUri(self, connection_speed=0):
578
+ encoding = self.getBestEncoding(connection_speed)
580
+ location = encoding.getBestLocation()
582
+ return location.uri
585
+###############################################################################
588
+EpisodeVersion: a version of an Episode (e.g. UK vs. US or pg-13 vs. 18)
590
+class EpisodeVersion(UriPlayObject):
591
+ __slots__ = [ 'encodings' ]
593
+ def __init__(self):
594
+ self.encodings = []
595
+ self.rdf_attribute_mapping = []
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)
605
+###############################################################################
608
+Encoding: a specific encoding of an Episode (format/bitrate/size etc.)
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' ]
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' )]
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()
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]))
647
+ gst.warning('unmapped video codec ' + self.video_codec)
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]))
654
+ gst.warning('unmapped audio codec ' + self.audio_codec)
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]))
661
+ gst.warning('unmapped container format ' + self.container_format)
664
+ if not required_caps.is_empty():
665
+ return required_caps
669
+ def isUsable(self):
672
+ if self.required_caps:
673
+ return codec_cache.isInstalledOrInstallable(self.required_caps)
677
+ def getBitrate(self):
678
+ if not self.bitrate:
680
+ return eval(self.bitrate)
682
+ def getBestLocation(self):
683
+ locations = self.locations
684
+ random.shuffle(locations)
685
+ for loc in locations:
690
+###############################################################################
693
+Location: location (URI) of a specific encoding
695
+class Location(UriPlayObject):
696
+ __slots__ = [ 'uri', 'type', 'sub_type', 'is_live' ]
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' )]
706
+ def parseLocation(self, conjunctive_graph, graph_location):
707
+ self.parseProperties(conjunctive_graph, graph_location)
709
+ def isUsable(self):
710
+ if self.uri and self.uri.startswith('http'):
714
+###############################################################################
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)
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' ]
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, ()))
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
744
+ def __init__(self):
745
+ gobject.GObject.__init__ (self)
748
+ self.cache_dir = os.path.join(BaseDirectory.xdg_cache_home, 'totem',
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')
757
+ gst.error('failed to create cache directory ' + self.cache_dir +
758
+ ': ' + err.strerror)
759
+ self.cache_dir = None
761
+ def _on_codec_cache_loaded(self, pool):
762
+ self.emit('codec-cache-loaded')
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):
768
+ if not filename.endswith(self.CACHE_FILE_SUFFIX):
772
+ ''' removes all cache files that don't relate to the given etag '''
773
+ def deleteStaleCacheFiles(self, except_etag=None):
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:
779
+ gst.log('deleting stale cache file ' + fn)
780
+ os.remove(os.path.join(self.cache_dir,fn))
786
+ ''' finds the most recent cache file and returns its file name or None'''
787
+ def findMostRecentCacheFile(self):
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:
799
+ except OSError, err:
800
+ gst.debug("couldn't inspect cache directory %s: %s" % (self.cache_dir, err.strerror))
804
+ gst.log('No cache file found')
809
+ ''' gets the ETag for the most recent cache file, or None '''
810
+ def getCacheETag(self):
811
+ etag = self.findMostRecentCacheFile()
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)
820
+ ''' makes a full filename from an ETag '''
821
+ def createCacheFileName(self, 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)
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, ))
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)))
837
+ self.emit('loading-error', err_msg)
839
+ self.emit('loading-done')
844
+ gst.debug('Loading ' + cache_fn)
845
+ store = ConjunctiveGraph()
847
+ gst.debug('Reading RDF file ...')
848
+ store.load(cache_fn)
849
+ gst.debug('Parsing ' + cache_fn)
850
+ brands = self.parseBrands(store)
852
+ gst.warning('Problem parsing RDF')
853
+ err_msg = 'Could not parse available content list'
855
+ gst.debug('Parsing done, marshalling result into main thread')
856
+ gobject.idle_add(_parse_idle_cb, err_msg, brands)
858
+ def _format_size_for_display(self, size):
860
+ return '%d bytes' % size
861
+ if size < 1024*1024:
862
+ return '%.1f kB' % (size / 1024.0)
864
+ return '%.1f MB' % (size / (1024.0*1024.0))
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
872
+ def _read_async_cb(instream, result):
874
+ partial_data = instream.read_finish(result)
875
+ gst.log('Read partial chunk of %d bytes' % (len(partial_data)))
877
+ bytes_read = pdata[1]
878
+ if len(partial_data) == 0:
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()
885
+ gst.info('Wrote %ld bytes' % (outsize))
886
+ self.parse_async(cache_fn)
888
+ chunks.append(partial_data)
889
+ bytes_read += len(partial_data)
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) + ')')
897
+ gst.warning('Error downloading ' + self.AVAILABLE_CONTENT_URI)
900
+ cache_file.delete()
902
+ self.emit('loading-error', 'Error downloading available content list')
904
+ # _query_done_cb start:
905
+ gst.log('Query done')
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')
914
+ gst.log('Got info, querying etag')
915
+ remote_etag = remote_info.get_etag()
917
+ remote_etag = remote_etag.strip('"')
918
+ gst.log('Remote etag: ' + remote_etag)
920
+ cache_fn = self.createCacheFileName(remote_etag)
921
+ cache_file = gio.File(cache_fn)
923
+ # if file already exists, get size to double-check against server's
925
+ cache_size = cache_file.query_info('standard::size').get_size()
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)
936
+ # delete old cache file if it exists
938
+ cache_file.delete()
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 ...')
948
+ # load_async start:
949
+ gst.log('starting loading')
951
+ # init global singleton variable codec_cache, if needed
954
+ if not codec_cache:
955
+ codec_cache = CodecCache()
956
+ codec_cache.connect('loaded', self._on_codec_cache_loaded)
957
+ codec_cache.reload_async()
959
+ etag = self.getCacheETag()
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)
977
+ gst.log('Cached etag: None')
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, '*')
985
+ def parseBrands(self, graph):
987
+ for b in graph.subjects(RDF.type, PO['Brand']):
989
+ brand.parseBrand(graph, b)
990
+ brands.append(brand)
991
+ gst.log('[%3d eps] %s %s' % (len(brand.episodes), brand.title, brand.genres))
994
+ ''' returns array of brands which can potentially be played '''
995
+ def getUsableBrands(self):
997
+ for brand in self.brands:
998
+ if brand.hasUsableEpisodes():
999
+ usable_brands.append(brand)
1000
+ return usable_brands
1003
+###############################################################################
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
1013
+ def __init__(self):
1014
+ gtk.TreeView.__init__ (self)
1017
+ self.set_headers_visible(False)
1019
+ self.connect('row-activated', self.onRowActivated)
1021
+ self.set_property('has-tooltip', True)
1022
+ self.connect('query-tooltip', self.onQueryTooltip)
1024
+ self.set_message('Loading ...')
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()
1036
+ self.pool.load_async()
1037
+ gst.log('started loading')
1039
+ def _on_content_pool_message(self, content_pool, msg):
1040
+ self.set_message(msg)
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)
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:
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:
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])
1069
+ def populate(self):
1070
+ gst.log('populating treeview')
1072
+ brands = self.pool.getUsableBrands()
1073
+ gst.info('%d brands with usable episodes/encodings' % (len(brands)))
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)
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)
1089
+ # now make all this visible (view might be showing model with message)
1090
+ self.set_model(self.filter)
1092
+ # expand top-level categories
1093
+ for _iter in toplevel_iters:
1094
+ path = self.store.get_path(_iter)
1095
+ self.expand_row(path, False)
1097
+ def get_brand_tooltip(self, brand):
1098
+ if not brand or not brand.description:
1100
+ return '<b>%s</b>\n<i>%s</i>' % (gobject.markup_escape_text(brand.title),
1101
+ gobject.markup_escape_text(brand.description))
1103
+ def get_episode_tooltip(self, brand, episode):
1104
+ if not episode or not episode.description:
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))
1110
+ def onQueryTooltip(self, view, x, y, keyboard_tip, tip):
1112
+ model, path, _iter = self.get_tooltip_context(x, y, keyboard_tip)
1114
+ return False # probably no content yet
1116
+ brand, episode, msg, genre = model.get(_iter, 0, 1, 2, 3)
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)
1126
+ tip.set_markup(markup)
1128
+ tip.set_text('No details available') # FIXME: translate
1131
+ def onRowActivated(self, view, path, col):
1132
+ model = self.get_model()
1134
+ _iter = model.get_iter(path)
1135
+ brand, episode = self.get_model().get(_iter, 0, 1)
1137
+ self.emit('play-episode', episode)
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)
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)
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)
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)
1157
+ def renderCell(self, column, renderer, model, _iter):
1158
+ brand, episode, msg, genre = model.get(_iter, 0, 1, 2, 3)
1160
+ self.renderMessageCell(column, renderer, model, _iter, msg)
1162
+ self.renderGenreCell(column, renderer, model, _iter, genre)
1164
+ self.renderBrandCell(column, renderer, model, _iter, brand)
1166
+ self.renderEpisodeCell(column, renderer, model, _iter, brand, episode)
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)
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
1181
+ # genre always comes before any other siblings (like brands or episodes)
1187
+ # brands and episodes are sorted alphabetically by title
1188
+ elif not episode1 or not episode2:
1191
+ elif episode1 and episode2:
1192
+ s1 = episode1.title
1193
+ s2 = episode2.title
1195
+ gst.warning('should not be reached (should be genre label comparison)')
1197
+ # string comparison
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 + '"')
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()
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)
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])
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()
1241
+ scrollwin.add(view)
1245
Index: b/src/plugins/bbc/genres.py
1246
===================================================================
1248
+++ b/src/plugins/bbc/genres.py
1253
+# Copyright (C) 2008 Tim-Philipp Müller <tim.muller@collabora.co.uk>
1254
+# Copyright (C) 2008 Canonical Ltd.
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.
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.
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.
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.
1275
+# See license_change file for details.
1278
+gobject.threads_init()
1280
+pygst.require ("0.10")
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",
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",
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",
1359
+ "religionandethics": "Religion & Ethics",
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"
1408
+# lowest = at the top
1409
+shortref_to_sortrank_map = {
1413
+ "entertainmentandcomedy": 4,
1417
+ "religionandethics": 8,
1423
+GenrePool: keeps track of the already-created genres, mainly so we can easily
1424
+ find already-existing parents for to-be-created genres
1426
+class GenrePool(object):
1427
+ __slots__ = [ 'genres', 'toplevel_genres' ]
1429
+ def __init__(self):
1433
+ self.genres = { } # maps short_ref => genre object
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]
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)
1449
+ genre = Genre(short_ref, parent)
1450
+ self.genres[short_ref] = genre
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
1462
+Genre: represents a genre
1464
+class Genre(object):
1465
+ __slots__ = [ 'short_ref', 'label', 'sort_rank', 'parent', 'children', 'brands' ]
1467
+ def __init__(self, short_ref, parent_genre):
1468
+ self.short_ref = short_ref
1470
+ if short_ref in shortref_to_label_map:
1471
+ self.label = shortref_to_label_map[short_ref]
1473
+ self.label = 'Unknown: ' + short_ref
1475
+ if short_ref in shortref_to_sortrank_map:
1476
+ self.sort_rank = shortref_to_sortrank_map[short_ref]
1478
+ self.sort_rank = 99999
1480
+ self.parent = parent_genre
1481
+ self.children = []
1484
+ if parent_genre is not None:
1485
+ parent_genre.add_child(self)
1487
+ gst.log('created genre ' + short_ref + ' = ' + self.label)
1489
+ def add_child(self, child_genre):
1490
+ if child_genre not in self.children:
1491
+ self.children.append(child_genre)
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)
1498
+if __name__ == "__main__":
1501
Index: b/src/plugins/bbc/installablecodecs.py
1502
===================================================================
1504
+++ b/src/plugins/bbc/installablecodecs.py
1509
+# Copyright (C) 2008 Tim-Philipp Müller <tim.muller@collabora.co.uk>
1510
+# Copyright (C) 2008 Canonical Ltd.
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.
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.
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.
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.
1531
+# See license_change file for details.
1534
+gobject.threads_init()
1536
+pygst.require ("0.10")
1541
+def allPackagesAvailableApt(package_descs, codec_name, apt_cache):
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))
1559
+ elif toinstall.find(pkg_name) < 0:
1560
+ toinstall += pkg_name + ' '
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
1567
+ gst.debug('%s: not available (all already installed)' % (codec_name))
1570
+ gst.debug('%s: available (install %s)' % (codec_name, toinstall[:-1]))
1573
+def getInstallableCodecsUbuntu():
1575
+ warnings.filterwarnings("ignore", "apt API not stable yet", FutureWarning)
1582
+ apt_cache = apt.Cache()
1584
+ gst.warning('Failed to read APT cache')
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-'):
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')
1602
+def getInstallableCodecs():
1604
+ if os.access('/var/cache/app-install/gai-codec-map.gdbm', os.R_OK):
1605
+ codecs = getInstallableCodecsUbuntu()
1609
+if __name__ == "__main__":
1610
+ codecs = getInstallableCodecs()
1611
+ if len(codecs) > 0:
1612
+ for codec in codecs:
1613
+ print "installable: %s" % (codec)
1615
+ print 'No codecs known to be installable'