364
369
+CodecCache: keeps track of what is currently installed and what we might be
365
+ able to install; cache this in a dict so we don't have to do
366
+ expensive checks more often than necessary
370
+ able to install; caches things internally in a dict so we don't
371
+ have to do expensive checks more often than necessary; do not
372
+ cache results elsewhere and make sure to listen to the 'loaded'
373
+ signal and refilter any content once the database of installable
374
+ and installed codecs is loaded (methods may just return False if
375
+ the database hasn't been loaded yet)
368
+class CodecCache(object):
377
+class CodecCache(gobject.GObject):
369
378
+ __slots__ = [ 'codec_cache', 'installable_codecs' ]
380
+ __gsignals__ = dict(loaded=(gobject.SIGNAL_RUN_LAST, None, ()))
371
382
+ def __init__(self):
383
+ gobject.GObject.__init__ (self)
375
384
+ self.codec_cache = { }
376
+ # FIXME: the following can take multiple seconds on machines with very
377
+ # low cpu power (culprit: apt.Cache()), we should probably do this
378
+ # async and then trigger a refiltering of the treeview when done
379
+ self.installable_codecs = installablecodecs.getInstallableCodecs()
385
+ self.installable_codecs = None
387
+ def reload_async(self):
388
+ gst.log('starting codec cache loading')
389
+ thread.start_new_thread(self._loading_thread, ())
391
+ def _loading_thread(self):
392
+ ''' idle callback to marshal result back into the main thread '''
393
+ def _loading_done_idle_cb(res):
394
+ gst.log('codec cache loaded (%d elements)' % (len(res)))
395
+ self.installable_codecs = res
396
+ self.emit('loaded')
397
+ return False # don't want to be called again
399
+ gst.log('in codec cache loading thread')
400
+ # the following can take quite a few seconds on machines with very
401
+ # low cpu power (culprit: apt.Cache()), so we do this in a separate
402
+ # thread and then trigger a refiltering of the treeview when done
403
+ res = installablecodecs.getInstallableCodecs()
404
+ gst.log('codec cache loading done, marshalling result into main thread')
405
+ gobject.idle_add(_loading_done_idle_cb, res)
381
407
+ def haveDecoderForCaps(self, decoder_caps):
382
408
+ caps_string = decoder_caps.to_string()
772
812
+ fn = self.CACHE_FILE_PREFIX + etag + self.CACHE_FILE_SUFFIX
773
813
+ return os.path.join(self.cache_dir, fn)
776
+ Checks if the local cache file needs updating and does so if needed
777
+ FIXME: this should be done fully async one day
778
+ Returns the filename of the cache file
780
+ def ensureCache(self):
781
+ etag = self.getCacheETag()
783
+ gst.log('Cached etag: ' + etag)
784
+ self.deleteStaleCacheFiles(etag)
785
+ existing_cache_fn = self.createCacheFileName(etag)
786
+ existing_cache_file = gio.File(existing_cache_fn)
787
+ existing_cache_info = existing_cache_file.query_info('time::modified')
788
+ existing_cache_mtime = existing_cache_info.get_modification_time()
789
+ # if the cache file is not older than N minutes/hours/days, then
790
+ # we'll just go ahead and use it instead of downloading a new one,
791
+ # even if it's not perfectly up-to-date.
792
+ # FIXME: UI should have a way to force an update
793
+ secs_since_update = time.time() - existing_cache_mtime
794
+ if secs_since_update >= 0 and secs_since_update < self.MAX_CACHE_FILE_AGE:
795
+ gst.log('Cache file is fairly recent, last updated %f secs ago' % (secs_since_update))
796
+ return existing_cache_fn
798
+ gst.log('Cached etag: None')
800
+ # CHECKME: is http always available as protocol?
815
+ def parse_async(self, cache_fn):
816
+ self.emit('progress-message', 'Parsing available content list ...')
817
+ thread.start_new_thread(self._parsing_thread, (cache_fn, ))
819
+ def _parsing_thread(self, cache_fn):
820
+ def _parse_idle_cb(err_msg, brands):
821
+ self.brands = brands
822
+ gst.info('Parsing done: %d brands' % (len(self.brands)))
824
+ self.emit('loading-error', err_msg)
826
+ self.emit('loading-done')
831
+ gst.debug('Loading ' + cache_fn)
832
+ store = ConjunctiveGraph()
834
+ gst.debug('Reading RDF file ...')
835
+ store.load(cache_fn)
836
+ gst.debug('Parsing ' + cache_fn)
837
+ brands = self.parseBrands(store)
839
+ gst.warning('Problem parsing RDF')
840
+ err_msg = 'Could not parse available content list'
842
+ gst.debug('Parsing done, marshalling result into main thread')
843
+ gobject.idle_add(_parse_idle_cb, err_msg, brands)
845
+ def _format_size_for_display(self, size):
847
+ return '%d bytes' % size
848
+ if size < 1024*1024:
849
+ return '%.1f kB' % (size / 1024.0)
851
+ return '%.1f MB' % (size / (1024.0*1024.0))
853
+ def load_async(self):
854
+ def _query_done_cb(remote_file, result):
855
+ pdata = [''] # mutable container so subfunctions can share access
857
+ def _read_async_cb(instream, result):
859
+ partial_data = instream.read_finish(result)
860
+ gst.log('Read %d bytes' % (len(partial_data)))
862
+ if len(partial_data) == 0:
864
+ outstream = cache_file.create(gio.FILE_CREATE_NONE)
865
+ outstream.write(data)
866
+ outsize = outstream.query_info('*').get_size()
868
+ gst.info('Wrote %ld bytes' % (outsize))
869
+ self.parse_async(cache_fn)
871
+ # FIXME: this probably results in many MBs of memcpy()
872
+ data += partial_data
874
+ instream.read_async(10240, _read_async_cb, io_priority=glib.PRIORITY_LOW-1)
875
+ bytes_read = len(data)
876
+ self.emit('progress-message',
877
+ 'Downloading available content list ... ' + '(' +
878
+ self._format_size_for_display(bytes_read) + ')')
880
+ gst.warning('Error downloading ' + self.AVAILABLE_CONTENT_URI)
883
+ cache_file.delete()
885
+ self.emit('loading-error', 'Error downloading available content list')
887
+ # _query_done_cb start:
888
+ gst.log('Query done')
890
+ remote_info = remote_file.query_info_finish(result)
891
+ except Exception, e:
892
+ # bail out if we can't query, not much point trying to download
893
+ gst.warning('Could not query %s: %s' % (self.AVAILABLE_CONTENT_URI, e.message))
894
+ self.emit('loading-error', 'Could not connect to server')
897
+ gst.log('Got info, querying etag')
898
+ remote_etag = remote_info.get_etag()
900
+ remote_etag = remote_etag.strip('"')
901
+ gst.log('Remote etag: ' + remote_etag)
903
+ cache_fn = self.createCacheFileName(remote_etag)
904
+ cache_file = gio.File(cache_fn)
906
+ # if file already exists, get size to double-check against server's
908
+ cache_size = cache_file.query_info('standard::size').get_size()
912
+ if etag and remote_etag and etag == remote_etag:
913
+ remote_size = remote_info.get_size()
914
+ if remote_size <= 0 or cache_size == remote_size:
915
+ gst.log('Cache file is up-to-date, nothing to do')
916
+ self.parse_async(cache_fn)
919
+ # delete old cache file if it exists
921
+ cache_file.delete()
925
+ # FIXME: use gio.File.copy_async() once it's wrapped
926
+ remote_file.read().read_async(10240, _read_async_cb, io_priority=glib.PRIORITY_LOW-1)
927
+ gst.info('copying ' + self.AVAILABLE_CONTENT_URI + ' -> ' + cache_fn)
928
+ self.emit('progress-message', 'Downloading available content list ...')
931
+ # load_async start:
932
+ gst.log('starting loading')
933
+ etag = self.getCacheETag()
935
+ gst.log('Cached etag: ' + etag)
936
+ self.deleteStaleCacheFiles(etag)
937
+ existing_cache_fn = self.createCacheFileName(etag)
938
+ existing_cache_file = gio.File(existing_cache_fn)
939
+ existing_cache_info = existing_cache_file.query_info('time::modified')
940
+ existing_cache_mtime = existing_cache_info.get_modification_time()
941
+ # if the cache file is not older than N minutes/hours/days, then
942
+ # we'll just go ahead and use it instead of downloading a new one,
943
+ # even if it's not perfectly up-to-date.
944
+ # FIXME: UI should have a way to force an update
945
+ secs_since_update = time.time() - existing_cache_mtime
946
+ if secs_since_update >= 0 and secs_since_update < self.MAX_CACHE_FILE_AGE:
947
+ gst.log('Cache file is fairly recent, last updated %f secs ago' % (secs_since_update))
948
+ self.parse_async(existing_cache_fn)
951
+ gst.log('Cached etag: None')
953
+ # CHECKME: what happens if http is not available as protocol?
802
954
+ remote_file = gio.File(self.AVAILABLE_CONTENT_URI)
803
+ gst.log('Querying ' + self.AVAILABLE_CONTENT_URI)
804
+ remote_info = remote_file.query_info('*')
805
+ gst.log('Got info, querying etag')
806
+ remote_etag = remote_info.get_etag()
808
+ remote_etag = remote_etag.strip('"')
809
+ gst.log('Remote etag: ' + remote_etag)
810
+ except Exception, e:
811
+ gst.warning('Could not query %s: %s' % (self.AVAILABLE_CONTENT_URI, e.message))
814
+ cache_fn = self.createCacheFileName(remote_etag)
815
+ print "cache file name ", cache_fn
817
+ cache_file = gio.File(cache_fn)
819
+ # if file already exists, get size to double-check against server's
821
+ cache_size = cache_file.query_info('standard::size').get_size()
825
+ if etag and remote_etag and etag == remote_etag and \
826
+ (remote_info.get_size() <= 0 or cache_size == remote_info.get_size()):
827
+ gst.log('Cache file is up-to-date, nothing to do')
831
+ cache_file.delete()
835
+ # FIXME: use gio.File.copy() once we can use pygobject >= 2.15.2
837
+ instream = remote_file.read()
839
+ gst.error('Error reading remote file')
843
+ outstream = cache_file.create(gio.FILE_CREATE_NONE)
844
+ gst.info('copying ' + self.AVAILABLE_CONTENT_URI + ' -> ' + cache_fn)
847
+ data = instream.read()
848
+ if not data or len(data) == 0:
850
+ gst.log('Read %d bytes' % (len(data)))
851
+ outstream.write(data)
856
+ outsize = outstream.query_info('*').get_size()
857
+ gst.info('Wrote %ld bytes' % (outsize))
860
+ except gobject.GError, err:
861
+ gst.error('Error updating content cache: ' + err.message)
862
+ cache_file.delete()
865
+ gst.error('Error updating content cache')
866
+ cache_file.delete()
955
+ gst.log('Contacting server ' + self.AVAILABLE_CONTENT_URI)
956
+ self.emit('progress-message', 'Connecting to server ...')
957
+ remote_file.query_info_async(_query_done_cb, '*')
871
959
+ def parseBrands(self, graph):
921
991
+ self.set_headers_visible(False)
923
+ self.pool = ContentPool()
924
+ if not self.pool.load():
925
+ print "loading available content failed"
928
993
+ self.connect('row-activated', self.onRowActivated)
930
995
+ self.set_property('has-tooltip', True)
931
996
+ self.connect('query-tooltip', self.onQueryTooltip)
998
+ self.set_message('Loading ...')
1000
+ self.pool = ContentPool()
1001
+ self.pool.connect('codec-cache-loaded', self._on_codec_cache_loaded)
1002
+ self.pool.connect('progress-message', self._on_content_pool_message)
1003
+ self.pool.connect('loading-error', self._on_content_pool_error)
1004
+ self.pool.connect('loading-done', self._on_content_pool_loading_done)
1005
+ self.codec_cache_loaded = False
1006
+ self.content_pool_loaded = False
1007
+ self.pool.load_async()
1008
+ gst.log('started loading')
1010
+ def _on_content_pool_message(self, content_pool, msg):
1011
+ self.set_message(msg)
1013
+ def _on_content_pool_error(self, content_pool, err_msg):
1014
+ gst.warning('Failed to load available content: ' + err_msg)
1015
+ self.set_message(err_msg)
1017
+ def _on_content_pool_loading_done(self, content_pool):
1018
+ gst.log('content pool loaded')
1019
+ self.content_pool_loaded = True
1020
+ if self.codec_cache_loaded:
1023
+ def _on_codec_cache_loaded(self, content_pool):
1024
+ gst.log('codec cache loaded, refilter')
1025
+ self.codec_cache_loaded = True
1026
+ #self.filter.refilter() FIXME: we don't filter at the moment
1027
+ if self.content_pool_loaded:
1030
+ def populate(self):
1031
+ gst.log('populating treeview')
1032
+ self.store.clear()
933
1033
+ brands = self.pool.getUsableBrands()
934
1034
+ gst.info('%d brands with usable episodes/encodings' % (len(brands)))
936
1035
+ for brand in brands:
937
+ self.addBrand(brand)
1036
+ brand_iter = self.store.append(None, [brand, None, None])
1037
+ for ep in brand.episodes:
1038
+ self.store.append(brand_iter, [brand, ep, None])
1039
+ self.set_model(self.filter)
939
1041
+ def get_brand_tooltip(self, brand):
940
1042
+ if not brand or not brand.description: