Exemplo n.º 1
0
	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
Exemplo n.º 2
0
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