rpms/totem/F-9 youtube.py,NONE,1.1

Bastien Nocera hadess at fedoraproject.org
Fri Apr 24 14:21:31 UTC 2009


Author: hadess

Update of /cvs/pkgs/rpms/totem/F-9
In directory cvs1.fedora.phx.redhat.com:/tmp/cvs-serv20203

Added Files:
	youtube.py 
Log Message:
* Fri Apr 24 2009 Bastien Nocera <bnocera at redhat.com> 2.23.2-15
- Update youtube plugin to latest upstream (#487927)


--- NEW FILE youtube.py ---
import totem
import gobject, gtk, gconf
gobject.threads_init()
import gdata.service
import urllib
import httplib
import atom
import threading
import time
import re
import os
import random

class DownloadThread (threading.Thread):
	def __init__ (self, youtube, url, treeview_name):
		self.youtube = youtube
		self.url = url
		self.treeview_name = treeview_name
		self._done = False
		self._lock = threading.Lock ()
		threading.Thread.__init__ (self)

	def run (self):
		try:
			res = self.youtube.service.Get (self.url).entry
		except gdata.service.RequestError:
			"""Probably a 503 service unavailable. Unfortunately we can't give an error message, as we're not in the GUI thread"""
			"""Just let the lock go and return"""
			res = None
		gobject.idle_add (self.publish_results, res)

	def publish_results(self, res):
		self._lock.acquire (True)
		self.youtube.entry[self.treeview_name] = res
		self._done = True
		self._lock.release ()
		return False

	@property
	def done (self):
		""" Thread-safe property to know whether the query is done or not """
		self._lock.acquire (True)
		res = self._done
		self._lock.release ()
		return res

class CallbackThread (threading.Thread):
	def __init__ (self, callback, *args, **kwargs):
		self.callback = callback
		self.args = args
		self.kwargs = kwargs
		threading.Thread.__init__ (self)

	def run (self):
		res = self.callback (*self.args, **self.kwargs)
		while res == True:
			res = self.callback (*self.args, **self.kwargs)

class YouTube (totem.Plugin):
	def __init__ (self):
		totem.Plugin.__init__ (self)
		self.debug = False
		self.gstreamer_plugins_present = True

		"""Search counters (per search type)"""
		self.in_search = {}
		self.search_token = {} # Used as an ID for a search thread

		self.max_results = 20
		self.button_down = False

		self.search_terms = ""
		self.youtube_id = ""

		self.start_index = {}
		self.results = {} # This is just the number of results from the last pagination query
		self.entry = {}

		self.current_treeview_name = ""
		self.notebook_pages = []

		self.vadjust = {}
		self.liststore = {}
		self.treeview = {}

	def activate (self, totem_object):
		"""Check for the availability of the flvdemux and souphttpsrc GStreamer plugins"""
		bvw_name = totem_object.get_video_widget_backend_name ()

		"""If the user's selected 1.5Mbps or greater as their connection speed, grab higher-quality videos
		   and drop the requirement for the flvdemux plugin."""
		self.gconf_client = gconf.client_get_default ()

		if bvw_name.find ("GStreamer") != -1:
			try:
				import pygst
				pygst.require ("0.10")
				import gst

				registry = gst.registry_get_default ()
				if registry.find_plugin ("soup") == None:
					"""This means an error will be displayed when they try to play anything"""
					self.gstreamer_plugins_present = False
			except ImportError:
				"""Do nothing; either it's using xine or python-gstreamer isn't installed"""

		"""Continue loading the plugin as before"""
		self.builder = self.load_interface ("youtube.ui", True, totem_object.get_main_window (), self)
		self.totem = totem_object

		self.search_entry = self.builder.get_object ("yt_search_entry")
		self.search_entry.connect ("activate", self.on_search_entry_activated)
		self.search_button = self.builder.get_object ("yt_search_button")
		self.search_button.connect ("clicked", self.on_search_button_clicked)
		self.progress_bar = self.builder.get_object ("yt_progress_bar")

		self.notebook = self.builder.get_object ("yt_notebook")
		self.notebook.connect ("switch-page", self.on_notebook_page_changed)

		self.notebook_pages = ["search", "related"]
		for page in self.notebook_pages:
			self.setup_treeview (page)
		self.current_treeview_name = "search"

		self.vbox = self.builder.get_object ("yt_vbox")
		self.vbox.show_all ()
		totem_object.add_sidebar_page ("youtube", _("YouTube"), self.vbox)

		"""Set up the service"""
		self.service = gdata.service.GDataService (account_type = "HOSTED_OR_GOOGLE", server = "gdata.youtube.com")

	def deactivate (self, totem):
		totem.remove_sidebar_page ("youtube")

	def setup_treeview (self, treeview_name):
		self.start_index[treeview_name] = 1
		self.results[treeview_name] = 0
		self.entry[treeview_name] = None
		self.in_search[treeview_name] = False

		"""This is done here rather than in the UI file, because UI files parsed in C and GObjects created in Python apparently don't mix."""
		renderer = totem.CellRendererVideo (use_placeholder = True)
		treeview = self.builder.get_object ("yt_treeview_" + treeview_name)
		treeview.set_property ("totem", self.totem)
		treeview.connect ("row-activated", self.on_row_activated)
		treeview.connect_after ("starting-video", self.on_starting_video)
		treeview.insert_column_with_attributes (0, _("Videos"), renderer, thumbnail=0, title=1)

		"""Add the extra popup menu options. This is done here rather than in the UI file, because it's done for multiple treeviews;
		if it were done in the UI file, the same action group would be used multiple times, which GTK+ doesn't like."""
		ui_manager = treeview.get_ui_manager ()
		action_group = gtk.ActionGroup ("youtube-action-group")
		action = gtk.Action ("open-in-web-browser", _("_Open in Web Browser"), _("Open the video in your web browser"), "gtk-jump-to")
		action_group.add_action_with_accel (action, None)

		ui_manager.insert_action_group (action_group, 1)
		ui_manager.add_ui (ui_manager.new_merge_id (),
				   "/ui/totem-video-list-popup/",
				   "open-in-web-browser",
				   "open-in-web-browser",
				   gtk.UI_MANAGER_MENUITEM,
				   False)
		menu_item = ui_manager.get_action ("/ui/totem-video-list-popup/open-in-web-browser")
		menu_item.connect ("activate", self.on_open_in_web_browser_activated)

		self.vadjust[treeview_name] = treeview.get_vadjustment ()
		self.vadjust[treeview_name].connect ("value-changed", self.on_value_changed)
		vscroll = self.builder.get_object ("yt_scrolled_window_" + treeview_name).get_vscrollbar ()
		vscroll.connect ("button-press-event", self.on_button_press_event)
		vscroll.connect ("button-release-event", self.on_button_release_event)

		self.liststore[treeview_name] = self.builder.get_object ("yt_liststore_" + treeview_name)
		self.treeview[treeview_name] = treeview
		treeview.set_model (self.liststore[treeview_name])

	def on_notebook_page_changed (self, notebook, notebook_page, page_num):
		self.current_treeview_name = self.notebook_pages[page_num]

	def on_row_activated (self, treeview, path, column):
		if self.debug:
			print "Activating row"

		model, rows = treeview.get_selection ().get_selected_rows ()
		iter = model.get_iter (rows[0])
		youtube_id = model.get_value (iter, 3)

		"""Get related videos"""
		self.youtube_id = youtube_id
		self.start_index["related"] = 1
		self.results["related"] = 0
		if self.in_search == False or self.current_treeview_name == "related":
			self.progress_bar.set_text (_("Fetching related videos..."))
		self.get_results ("/feeds/api/videos/" + urllib.quote (youtube_id) + "/related?max-results=" + str (self.max_results), "related")

		if self.debug:
			print "Done activating row"

	def get_fmt_string (self):
		if self.gconf_client.get_int ("/apps/totem/connection_speed") >= 10:
			return "&fmt=18"
		else:
			return ""

	def resolve_t_param (self, youtube_id):
		"""We have to get the t parameter from the actual video page, since Google changed how their URLs work"""
		stream = urllib.urlopen ("http://youtube.com/watch?v=" + urllib.quote (youtube_id))
		regexp1 = re.compile ("swfArgs.*\"t\": \"([^\"]+)\"")
		regexp2 = re.compile ("</head>")

		contents = stream.read ()
		if contents != "":
			"""Check for the t parameter, which is now in a JavaScript array on the video page"""
			matches = regexp1.search (contents)
			if (matches != None):
				stream.close ()
				return matches.group (1)

			"""Check to see if we've come to the end of the <head> tag; in which case, we should give up"""
			if (regexp2.search (contents) != None):
				stream.close ()
				return ""

		stream.close ()
		return ""

	def on_starting_video (self, treeview, path, user_data):
		"""Display an error if the required GStreamer plugins aren't installed"""
		if self.gstreamer_plugins_present == False:
			self.totem.interface_error_with_link (_("Totem cannot play this type of media (%s) because you do not have the appropriate plugins to handle it.") % _("YouTube"),
							      _("Please install the necessary plugins and restart Totem to be able to play this media."),
							      "http://www.gnome.org/projects/totem/#codecs",
							      _("More information about media plugins"),
							      self.totem.get_main_window ())
			return False

		return True

	def on_open_in_web_browser_activated (self, action):
		model, rows = self.treeview[self.current_treeview_name].get_selection ().get_selected_rows ()
		iter = model.get_iter (rows[0])
		youtube_id = model.get_value (iter, 3)

		"""Open the video in the browser"""
		os.spawnlp (os.P_NOWAIT, "xdg-open", "xdg-open", "http://www.youtube.com/watch?v=" + urllib.quote (youtube_id) + self.get_fmt_string ())

	def on_button_press_event (self, widget, event):
		self.button_down = True

	def on_button_release_event (self, widget, event):
		self.button_down = False
		self.on_value_changed (self.vadjust[self.current_treeview_name])

	def on_value_changed (self, adjustment):
		"""Load more results when we get near the bottom of the treeview"""
		if not self.button_down and (adjustment.get_value () + adjustment.page_size) / adjustment.upper > 0.8 and self.results[self.current_treeview_name] >= self.max_results:
			if self.current_treeview_name == "search":
				if self.debug:
					print "Getting more results for search \"" + self.search_terms + "\" from offset " + str (self.start_index["search"])
				self.get_results ("/feeds/api/videos?vq=" + urllib.quote_plus (self.search_terms) + "&max-results=" + str (self.max_results) + "&orderby=relevance&start-index=" + str (self.start_index["search"]), "search", False)
			elif self.current_treeview_name == "related":
				if self.debug:
					print "Getting more related videos for video \"" + self.youtube_id + "\" from offset " + str (self.start_index["related"])
				self.get_results ("/feeds/api/videos/" + urllib.quote_plus (self.youtube_id) + "/related?max-results=" + str (self.max_results) + "&start-index=" + str (self.start_index["related"]), "related", False)

	def convert_url_to_id (self, url):
		"""Find the last clause in the URL; after the last /"""
		return url.split ("/").pop ()

	def populate_list_from_results (self, search_token, treeview_name, thread):
		"""Check to see if this search has been cancelled"""
		if search_token != self.search_token[treeview_name]:
			return False

		"""Check and acquire the lock"""
		if not thread.done:
			if self.current_treeview_name == treeview_name:
				self.progress_bar.pulse ()
			return True

		CallbackThread (self.process_next_thumbnail, search_token, treeview_name).start ()
		return False

	def process_next_thumbnail (self, search_token, treeview_name):
		"""Note that all the calls to gobject.idle_add are so that the respective
		   UI function calls are made in the main thread, since process_next_thumbnail
		   is run in the CallbackThread thread."""

		"""Check to see if this search has been cancelled"""
		if search_token != self.search_token[treeview_name]:
			return False

		"""Return if there are no results (or we've finished)"""
		if self.entry[treeview_name] == None or len (self.entry[treeview_name]) == 0:
			gobject.idle_add (self._clear_ui, treeview_name)

			self.entry[treeview_name] = None
			self.in_search[treeview_name] = False

			return False

		"""Only do one result at a time, as the thumbnail has to be downloaded; give them a temporary MRL until the real one is resolved before playing"""
		entry = self.entry[treeview_name].pop (0)
		self.results[treeview_name] += 1
		self.start_index[treeview_name] += 1
		youtube_id = self.convert_url_to_id (entry.id.text)

		"""Find the content tag"""
		for _element in entry.extension_elements:
			if _element.tag =="group":
				break

		content_elements = _element.FindChildren ("content")
		if len (content_elements) == 0:
			return True
		mrl = content_elements[0].attributes['url']

		"""Download the thumbnail and store it in a temporary location so we can get a pixbuf from it"""
		thumbnail_url = _element.FindChildren ("thumbnail")[0].attributes['url']
		try:
			filename, headers = urllib.urlretrieve (thumbnail_url)
		except IOError:
			return True

		try:
			pixbuf = gtk.gdk.pixbuf_new_from_file (filename)
		except gobject.GError:
			print "Could not open thumbnail " + filename + " for video. It has been left in place for investigation."
			return True

		"""Don't leak the temporary file"""
		os.unlink (filename)

		"""Get the video stream MRL"""
		t_param = self.resolve_t_param (youtube_id)

		if t_param != "":
			mrl = "http://www.youtube.com/get_video?video_id=" + urllib.quote (youtube_id) + "&t=" + urllib.quote (t_param) + self.get_fmt_string ()

		gobject.idle_add (self._append_to_liststore, treeview_name, pixbuf, entry.title.text, mrl, youtube_id, search_token)

		return True

	def _clear_ui (self, treeview_name):
		"""Revert the cursor"""
		window = self.vbox.window
		window.set_cursor (None)

		if self.in_search == True or self.current_treeview_name == treeview_name:
			"""Only blank the progress bar if we're the only search taking place"""
			self.progress_bar.set_fraction (0.0)
			self.progress_bar.set_text ("")

		return False

	def _append_to_liststore (self, treeview_name, pixbuf, title, mrl, id, search_token):
		"""Check to see if this search has been cancelled"""
		if search_token != self.search_token[treeview_name]:
			return False

		if self.in_search == True or self.current_treeview_name == treeview_name:
			self.progress_bar.set_fraction (float (self.results[treeview_name]) / float (self.max_results))
		self.liststore[treeview_name].append ([pixbuf, title, mrl, id])
		return False

	def on_search_button_clicked (self, button):
		search_terms = self.search_entry.get_text ()

		if self.debug:
			print "Searching for \"" + search_terms + "\""

		"""Focus the "Search" tab"""
		self.notebook.set_current_page (self.notebook_pages.index ("search"))

		self.search_terms = search_terms
		self.start_index["search"] = 1
		self.results["search"] = 0
		self.progress_bar.set_text (_("Fetching search results..."))
		self.get_results ("/feeds/api/videos?vq=" + urllib.quote_plus (search_terms) + "&orderby=relevance&max-results=" + str (self.max_results), "search")

	def on_search_entry_activated (self, entry):
		self.search_button.clicked ()

	def get_results (self, url, treeview_name, clear = True):
		if self.in_search[treeview_name] == True and clear == False:
			"""If we're trying to fetch more results while another search is happening, just cancel"""
			if self.debug:
				print "Cancelling getting more results due to existing incomplete search."
			self.in_search[treeview_name] = False
			return
		elif clear == False:
			self.results[self.current_treeview_name] = 0
			self.progress_bar.set_text (_("Fetching more videos..."))

		"""If we're trying to do another full search while one's already happening, just continue as
		   normal. The populate_list_from_results function will notice, and cancel the old search."""

		if clear:
			self.liststore[treeview_name].clear ()

		if self.debug:
			print "Getting results from URL \"" + url + "\""

		self.in_search[treeview_name] = True
		self.search_token[treeview_name] = random.random ()

		"""Give us a nice waiting cursor"""
		window = self.vbox.window
		window.set_cursor (gtk.gdk.Cursor (gtk.gdk.WATCH))
		if self.current_treeview_name == treeview_name:
			self.progress_bar.pulse ()

		thread = DownloadThread (self, url, treeview_name)
		gobject.timeout_add (350, self.populate_list_from_results, self.search_token[treeview_name], treeview_name, thread)
		thread.start()





More information about the fedora-extras-commits mailing list