def attach_getiplayer(self): location_correct = False loc = self.config.config_getiplayer_location or which("get-iplayer") if loc is not None: flvstreamerloc = self.config.config_flvstreamer_location or which("rtmpdump") or which("flvstreamer") ffmpegloc = self.config.config_ffmpeg_location or which("ffmpeg") localfiles_dirs = self.config.config_localfiles_directories if self.gip is not None: self.gip.close() try: self.gip = GetIPlayer(loc, flvstreamerloc, ffmpegloc, localfiles_dirs) except OSError: pass else: location_correct = True # Add the interface to Totem's sidebar only if get_iplayer is accessible, otherwise show error if not location_correct: self.totem.action_error("Get iPlayer", "Cannot find get_iplayer. Please install it if you need to and set the location under plugin configuration.") self.reset_ui(location_correct) return location_correct
class GetIplayerPlugin (totem.Plugin): def __init__ (self): totem.Plugin.__init__ (self) self.totem = None self.showing_info = None self._mode_callback_id = None self.has_sidebar = False self.current_search = None self._current_search_input = None self.gip = None self._is_starting_stream = False # Used when a file is closed to figure out when to avoid killing a stream def activate (self, totem_object): # Build the interface builder = self.load_interface ("get-iplayer.ui", True, totem_object.get_main_window (), self) self._ui_container = builder.get_object ('getiplayer_top_pane') self._ui_container.show_all () # Sidebar progs_store = builder.get_object("getiplayer_progs_store") self._ui_progs_list = builder.get_object("getiplayer_progs_list") self._ui_progs_list.connect("row-expanded", self._row_expanded_cb) self._ui_progs_list.get_selection().connect("changed", self._row_selection_changed_cb) self._ui_progs_list.connect("row-activated", self._row_activated_cb) self._ui_progs_refresh = builder.get_object("getiplayer_progs_refresh") self._ui_progs_refresh.connect("clicked", self._refresh_clicked_cb) self._ui_search_entry = builder.get_object("getiplayer_search_entry") self._ui_search_entry.set_tooltip_text("Current Search: None") self._ui_search_clear = builder.get_object("getiplayer_search_clear") self._ui_search_search = builder.get_object("getiplayer_search_search") self._ui_programme_info = builder.get_object("getiplayer_description_pane") self._ui_series = builder.get_object("getiplayer_series_text") self._ui_episode = builder.get_object("getiplayer_episode_text") self._ui_duration = builder.get_object("getiplayer_duration_text") self._ui_expiry = builder.get_object("getiplayer_expiry_text") self._ui_desc = builder.get_object("getiplayer_desc_text") self._ui_thumb = builder.get_object("getiplayer_thumbnail") self._ui_mode_list = builder.get_object("getiplayer_modes") self._ui_version_list = builder.get_object("getiplayer_versions") self._ui_record = builder.get_object("getiplayer_record") self._ui_play = builder.get_object("getiplayer_play") self._ui_history_list = builder.get_object("getiplayer_history") self._ui_history_pane = builder.get_object("getiplayer_history_scroll") self._ui_search_entry.connect("changed", self._search_changed_cb) self._ui_search_entry.connect("activate", self._search_activated_cb) self._ui_search_clear.connect("clicked", self._search_clear_clicked_cb) self._ui_search_search.connect("clicked", self._search_clicked_cb) self._ui_record.connect("clicked", self._record_clicked_cb) self._ui_play.connect("clicked", self._play_clicked_cb) self._ui_history_list.connect("row-activated", self._history_activated_cb) self._ui_history_list.connect("key-press-event", self._history_keypress_cb) self._ui_mode_list.connect("changed", self._mode_selected_cb) self.config = Configuration(builder, self.attach_getiplayer) self.totem = totem_object self.totem.connect("file-closed", self._file_closed_cb) self.attach_getiplayer() def deactivate (self, totem_object): totem_object.remove_sidebar_page ("get-iplayer") self.has_sidebar = False if self.gip is not None: self.gip.close() def attach_getiplayer(self): location_correct = False loc = self.config.config_getiplayer_location or which("get-iplayer") if loc is not None: flvstreamerloc = self.config.config_flvstreamer_location or which("rtmpdump") or which("flvstreamer") ffmpegloc = self.config.config_ffmpeg_location or which("ffmpeg") localfiles_dirs = self.config.config_localfiles_directories if self.gip is not None: self.gip.close() try: self.gip = GetIPlayer(loc, flvstreamerloc, ffmpegloc, localfiles_dirs) except OSError: pass else: location_correct = True # Add the interface to Totem's sidebar only if get_iplayer is accessible, otherwise show error if not location_correct: self.totem.action_error("Get iPlayer", "Cannot find get_iplayer. Please install it if you need to and set the location under plugin configuration.") self.reset_ui(location_correct) return location_correct def _reset_progtree(self): if self.has_sidebar: self._ui_progs_list.get_model().clear() self._populate_filter_level(self._ui_progs_list, None) def reset_ui(self, iplayer_attached): if iplayer_attached: if not self.has_sidebar: self.totem.add_sidebar_page ("get-iplayer", _("Get iPlayer"), self._ui_container) self.has_sidebar = True self._search_clear_clicked_cb(self._ui_search_clear) self._ui_programme_info.hide_all() self._ui_history_pane.hide_all() self._reset_progtree() self._populate_history() else: if self.has_sidebar: self.totem.remove_sidebar_page("get-iplayer") self.has_sidebar = False def create_configure_dialog(self, *args): return self.config.create_configure_dialog(args) def _search_changed_cb(self, entry): has_text = bool(entry.get_text()) self._ui_search_clear.set_sensitive(has_text) self._ui_search_search.set_sensitive(has_text) def _search_activated_cb(self, entry): self._ui_search_search.clicked() def _search_clear_clicked_cb(self, button): self._ui_search_entry.set_text("") self._search_clicked_cb(button) def _search_clicked_cb(self, button): self._ui_search_search.set_sensitive(False) old_search = self._current_search_input self._current_search_input = self._ui_search_entry.get_text() or None self.current_search = None if self._current_search_input is None else self._convert_search_terms(self._current_search_input) if old_search != self._current_search_input: self._ui_search_entry.set_tooltip_text("Current Search: %s" % (self._current_search_input,)) self._reset_progtree() def _row_expanded_cb(self, tree, iter, path): try: self._populate_filter_level(tree, iter) except ValueError: # this is not a filtered level of the tree if tree.get_model().iter_depth(iter) == len(self.config.config_filter_order)-1: self._populate_series_and_episodes(tree, iter) # Try to expand so long as there is only one child open_iter = iter treemodel = tree.get_model() while open_iter is not None: if treemodel.iter_n_children(open_iter) == 1: tree.expand_row(treemodel.get_path(open_iter), False) open_iter = treemodel.iter_children(open_iter) else: open_iter = None def _row_selection_changed_cb(self, selection): treestore, branch = selection.get_selected() index = None if branch is None else treestore.get_value(branch, IDX_PROGRAMME_INDEX) self._load_info(None if index == -1 else index) def _row_activated_cb(self, tree, path, column): row_iter = tree.get_model().get_iter(path) index = tree.get_model().get_value(row_iter, IDX_PROGRAMME_INDEX) if index != -1: self.play_programme(index) def _refresh_clicked_cb(self, button): self._ui_container.set_sensitive(False) oldbuttontt = button.get_tooltip_text() button.set_tooltip_text("Refreshing...") def refresh_complete(): button.set_tooltip_text(oldbuttontt) self._ui_container.set_sensitive(True) self.reset_ui(True) self.gip.refresh_cache(False).on_complete(lambda _: gobject.idle_add(refresh_complete), self.show_errors("refreshing")) def _record_clicked_cb(self, button): if self.showing_info is None: return selected_version = self._ui_version_list.get_active_iter() selected_mode = self._ui_mode_list.get_active_iter() if selected_version is None or selected_mode is None: return version = self._ui_version_list.get_model().get_value(selected_version, 0) mode = self._ui_mode_list.get_model().get_value(selected_mode, 0) if not mode: return # Loading def on_recording_fail(errs): self.show_errors("recording")(errs) self._populate_history() self.gip.record_programme( self.showing_info, None, version, mode ).on_complete( lambda _: self._populate_history(), on_recording_fail ) self._populate_history() def _play_clicked_cb(self, button): if self.showing_info is None: return selected_version = self._ui_version_list.get_active_iter() selected_mode = self._ui_mode_list.get_active_iter() if selected_version is None or selected_mode is None: return version = self._ui_version_list.get_model().get_value(selected_version, 0) mode = self._ui_mode_list.get_model().get_value(selected_mode, 0) if not mode: return # Loading name = self._ui_series.get_text() + " - " + self._ui_episode.get_text() self.play_programme(self.showing_info, name=name, version=version, mode=mode) def play_programme(self, index, name=None, version=None, mode=None): ''' Give this the correct information and it will play a programme. With the wrong information it will try and fail somewhere in the streaming. Give no information and it will work it out if it can... ''' if name is None or version is None: def got_info(info): newname = name if name is not None else "%s - %s" % (info.get("name", "Unknown name"), info.get("episode", "")) if version is None and self.config.config_preferred_version not in info.get("versions", "").split(","): self.totem.action_error("Get iPlayer", "Preferred version not found for programme %s." % index) return newversion = version if version is not None else self.config.config_preferred_version self.play_programme(index, name=newname, version=newversion, mode=mode) self.gip.get_programme_info(index).on_complete(got_info, self.show_errors("trying to play programme")) return if mode is None: def got_streams(streams): mode_and_bitrate = [(mode, int(stream["bitrate"])) for mode, stream in streams.iteritems() if stream["bitrate"]] if not mode_and_bitrate: return within_acceptable = filter(lambda mb: mb[1] <= self.config.config_preferred_bitrate, mode_and_bitrate) newmode = min(within_acceptable if within_acceptable else mode_and_bitrate, key=lambda mb: (mb[1], mb[0]))[0] self.play_programme(index, name=name, version=version, mode=newmode) self.gip.get_stream_info(index, version).on_complete(got_streams, self.show_errors("trying find programme streams")) return self._is_starting_stream = True # Next file close will not kill the main stream fd, streamresult = self.gip.stream_programme_to_pipe(index, version, mode) streamresult.on_complete(onerror=self.show_errors("playing programme")) gobject.idle_add(self.totem.add_to_playlist_and_play, "fd://%s" % fd, name, False) def _version_selected_cb(self, version_list, index, info): if self.showing_info != index: return selected = version_list.get_active_iter() if selected is None: return version = version_list.get_model().get_value(selected, 0) self._ui_mode_list.set_sensitive(False) self._ui_mode_list.get_model().clear() self._ui_mode_list.get_model().append(["", "Loading..."]) self._ui_mode_list.set_active(0) def got_modes(modes, version): version_selected = version_list.get_active_iter() if version_selected is None or version_list.get_model().get_value(version_selected, 0) != version or self.showing_info != index: return self._ui_mode_list.get_model().clear() active_mode_iter = None # Will be the closest under or equal to preferred bitrate lowest_mode_iter = None # Used rather than mode_iter at the end in case there were no modes moderates = combine_modes((mode, int(modeinfo["bitrate"]) if modeinfo.get("bitrate", "") else 0) for mode, modeinfo in modes.iteritems()) for mode, bitrate in sorted(moderates.iteritems(), key=lambda m:(-m[1], m[0])): display = "%s (%skbps)" % (mode, bitrate) if bitrate else mode mode_iter = self._ui_mode_list.get_model().append([mode, display]) # Only assign first when we get to correct bitrate, and ignore 0 bitrate streams if bitrate != 0: if active_mode_iter is None and bitrate <= self.config.config_preferred_bitrate: active_mode_iter = mode_iter # bitrates decreasing, so we want first one after we pass (or equal) the preference lowest_mode_iter = mode_iter if active_mode_iter is None: active_mode_iter = lowest_mode_iter if active_mode_iter is not None: self._ui_mode_list.set_active_iter(active_mode_iter) else: self._ui_mode_list.set_active(0) self._ui_mode_list.set_sensitive(True) self.gip.get_stream_info(index, version).on_complete(lambda modes: gobject.idle_add(got_modes, modes, version), self.show_errors("retrieving modes")) def _mode_selected_cb(self, mode_list): mode_iter = mode_list.get_active_iter() selected_mode = None if mode_iter is None else mode_list.get_model().get_value(mode_iter, 0) islive = self._ui_episode.get_text() == "live" self._ui_play.set_sensitive(bool(selected_mode)) # Enabled if we have a valid mode self._ui_record.set_sensitive(bool(selected_mode) and not islive) def _history_activated_cb(self, treeview, path, column): treemodel = treeview.get_model() iter = treemodel.get_iter(path) file = treemodel.get_value(iter, IDXH_LOCATION) if not file: return episode = treemodel.get_value(iter, IDXH_NAME) series = treemodel.get_value(treemodel.iter_parent(iter), IDXH_NAME) name = series + " - " + episode self.totem.add_to_playlist_and_play("file://" + file, name, True) def _history_keypress_cb(self, treeview, event): if "Delete" != gtk.gdk.keyval_name(event.keyval): return False treemodel, treeiter = treeview.get_selection().get_selected() if treeiter is None: return True name = treemodel.get_value(treeiter, IDXH_NAME) file = treemodel.get_value(treeiter, IDXH_LOCATION) wholeseries = not file dlg = gtk.MessageDialog( parent=self.totem.get_main_window(), flags=gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_OK_CANCEL, message_format="You are about to delete the %s; %s. Are you sure you want to do this?" % ( "entire series" if wholeseries else "programme", name ) ) response = dlg.run() dlg.destroy() if response != gtk.RESPONSE_OK: return True if wholeseries: childiter = treemodel.iter_children(treeiter) while childiter is not None: os.remove(treemodel.get_value(childiter, IDXH_LOCATION)) childiter = treemodel.iter_next(childiter) else: os.remove(file) self._populate_history() return True def _file_closed_cb(self, totem): if not self._is_starting_stream: self.gip.close_main_stream() self._is_starting_stream = False def _filter_at_branch(self, progs_store, branch): node_names = [] while branch is not None: node_names.append(progs_store.get_value(branch, IDX_TITLE)) branch = progs_store.iter_parent(branch) return tuple(reversed(node_names)) def _active_filters(self, progs_store, branch): path = self._filter_at_branch(progs_store, branch) return dict(zip(self.config.config_filter_order, path)) def _populate_filter_level(self, progs_list, branch): progs_store = progs_list.get_model() populate_depth = 0 if branch is None else progs_store.iter_depth(branch)+1 if populate_depth >= len(self.config.config_filter_order): raise ValueError("This level does not contain filters.") populating = self.config.config_filter_order[populate_depth] populate = load_branch(progs_list, branch) if populate is None: return def got_filters(filters): populate([(TreeValues(f, info_type=populating), []) for f in filters]) active_filters = self._active_filters(progs_store, branch) self.gip.get_filters_and_blanks( populating, self.current_search, **active_filters ).on_complete( got_filters, self.show_errors_and_cancel_populate(populate, populating) ) def _populate_series_and_episodes(self, progs_list, branch): populate = load_branch(progs_list, branch) if populate is None: return def got_episodes(series): populate([ (TreeValues(s, info_type="series"), [(TreeValues(ep[1], prog_idx=ep[0], info_type="episode"), None) for ep in eps]) for s, eps in series.iteritems() ]) active_filters = self._active_filters(progs_list.get_model(), branch) self.gip.get_episodes( self.current_search, **active_filters ).on_complete( got_episodes, self.show_errors_and_cancel_populate(populate, "programme") ) def _populate_history(self): def populate_store(history): self._ui_history_pane.hide_all() historystore = self._ui_history_list.get_model() historystore.clear() if self.gip.recordings.values(): recording_branch = historystore.append(None, [-1, "Currently Recording", "", "", ""]) for name, version, mode in self.gip.recordings.values(): historystore.append(recording_branch, [-1, name + " (Recording...)", version, mode, ""]) by_series = defaultdict(list) for index, series, episode, version, mode, location in history: by_series[series].append((index, episode, version, mode, location)) for series, episodes in by_series.iteritems(): series_branch = historystore.append(None, [-1, series, "", "", ""]) for index, episode, version, mode, location in episodes: historystore.append(series_branch, [index, episode, version, mode, location]) if historystore.get_iter_root() is not None: self._ui_history_pane.show_all() self.gip.get_history().on_complete(lambda history: gobject.idle_add(populate_store, history), self.show_errors("retrieving recordings")) def _convert_search_terms(self, terms): st = self.config.config_search_type if st == "word": words = terms.split() return ''.join(r"(?=.*(\W|\b)" + re.escape(word) + r"(\b|\W))" for word in words) elif st == "wildcard": # Should be split up by words, no particular order for words # Normal word should search for whole word # Single * can be ignored, searches for anything or nothing # Word with * can be a single word with anything where the * is - e.g. garden*=gardening or garden etc words = terms.split() regex_words = [] for word in words: if re.search(r"[^\*]+", word) is not None: # not just **** word = re.escape(word) regex_words.append(word.replace(r"\*", r"\w*")) return ''.join(r"(?=.*(\b|\W)" + word + r"(\W|\b))" for word in regex_words) elif st == "regex": # Exact search with regex return terms else: return terms # On error use regex... def _load_info(self, index): '''Loads information for a particular programme.''' self.showing_info = index # First show a loading page def prepare_loading(): if self.showing_info != index: return self._ui_programme_info.hide_all() self._ui_programme_info.set_sensitive(False) if index is None: return self._ui_series.set_text("Loading programme %s..." % index) self._ui_episode.set_text("") self._ui_duration.set_text("") self._ui_expiry.set_text("") self._ui_desc.get_buffer().set_text("") self._ui_thumb.clear() if self._mode_callback_id is not None: self._ui_version_list.disconnect(self._mode_callback_id) self._mode_callback_id = None self._ui_mode_list.get_model().clear() self._ui_version_list.get_model().clear() self._ui_play.set_sensitive(False) self._ui_record.set_sensitive(False) self._ui_programme_info.show_all() gobject.idle_add(prepare_loading) if index is None: return # Then load up the info and populate it when done (if the index has not changed) def got_info(info): if self.showing_info != index: return self._ui_series.set_text(info.get("name", "Unknown name")) self._ui_episode.set_text(info.get("episode", "")) duration = info.get("duration", "Unknown") try: duration = int(duration) # seconds duration = str(duration // 60) + " minutes" except ValueError: # Wasn't a number, try mins:seconds format try: minutes, seconds = duration.split(":") minutes = int(minutes) seconds = int(seconds) if seconds >= 30: minutes += 1 duration = str(minutes) + " minutes" except ValueError: pass # Leave as it is self._ui_duration.set_text(duration) timetoexpiry = info.get("expiryrel") if timetoexpiry or "versions" in info: self._ui_programme_info.set_sensitive(True) self._ui_expiry.set_markup( "Expires %s" % timetoexpiry if timetoexpiry else ('' if "versions" in info or not info.get("hasexpired", False) else "<span foreground='red'><b>Expired</b></span>")) self._ui_desc.get_buffer().set_text(info.get("desc", "No description")) self._ui_mode_list.get_model().clear() self._ui_version_list.get_model().clear() active_version_iter = None for version in info.get("versions", "").split(","): if version: version_iter = self._ui_version_list.get_model().append([version]) if version == self.config.config_preferred_version: active_version_iter = version_iter self._mode_callback_id = self._ui_version_list.connect("changed", self._version_selected_cb, index, info) if active_version_iter is not None: self._ui_version_list.set_active_iter(active_version_iter) else: self._ui_version_list.set_active(0) # Need to load image on another thread thumb = info.get("thumbnail") if thumb: load_image_in_background(self._ui_thumb, thumb, cancelcheck=lambda: self.showing_info != index, transform=lambda pb: ensure_image_small(pb, 150, 100)) def on_fail(errs): self._ui_programme_info.hide_all() self.show_errors("loading programme information")(errs) def finished(result, errs): if len(errs) == 1 and errs[0].startswith("WARNING: No programmes are available for this pid"): errs = [] # Programme has expired result["hasexpired"] = True if errs: gobject.idle_add(on_fail, errs) else: gobject.idle_add(got_info, result) self.gip.get_programme_info(index).on_complete(always=finished) def show_errors(self, activity=None): '''Creates a function that can display a list of errors.''' message = "There were %s errors%s:\n%s" activitystr = " while %s" % (activity,) if activity is not None else "" def show_errs(errs): dlg = gtk.MessageDialog( parent=self.totem.get_main_window(), flags=gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK, message_format=message % (len(errs), activitystr, "\n".join(errs)) ) dlg.run() dlg.destroy() return lambda errs: gobject.idle_add(show_errs, errs) def show_errors_and_cancel_populate(self, populate, activity=None): title = "Failed to load" if activity is not None: title += " %ss" % (activity,) def pop_and_show(errs): gobject.idle_add(populate, [(TreeValues(title, loaded=True), [])]) self.show_errors("populating %ss" % (activity,))(errs) return pop_and_show