~woodrow-shen/totem/mybranch

« back to all changes in this revision

Viewing changes to src/plugins/youtube/youtube.py

Tags: 2.24.3-3
* totem-mozilla.docs: ship README.browser-plugin which explains how to 
  disable the plugin for some MIME types.
* rules: remove the hack that only let totem-xine support VCDs and 
  DVDs, now that GStreamer supports them. Closes: #370789.
* 01_fake_keypresses.patch: new patch. Completely disable the broken 
  XTEST code that generates fake keypresses. Closes: #500330.
* 90_autotools.patch: regenerated.
* Build-depend on nautilus 2.22 to be sure to build the extension for 
  the correct version.
* totem-xine depends on libxine1-x.
* Standards version is 3.8.1.
* Upload to unstable.
* 04_tracker_build.patch: new patch, stolen upstream. Fix build with 
  latest tracker version.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
import totem
2
 
import gobject, gtk
 
2
import gobject, gtk, gconf
 
3
gobject.threads_init()
3
4
import gdata.service
4
5
import urllib
5
6
import httplib
6
7
import atom
7
8
import threading
 
9
import time
8
10
import re
9
 
from os import unlink
 
11
import os
 
12
import random
10
13
 
11
14
class DownloadThread (threading.Thread):
12
15
        def __init__ (self, youtube, url, treeview_name):
13
16
                self.youtube = youtube
14
17
                self.url = url
15
18
                self.treeview_name = treeview_name
16
 
                threading.Thread.__init__ (self)
17
 
        def run (self):
18
 
                self.youtube.entry_lock.acquire (True)
19
 
                self.youtube.entry[self.treeview_name] = self.youtube.service.Get (self.url).entry
20
 
                self.youtube.entry_lock.release ()
 
19
                self._done = False
 
20
                self._lock = threading.Lock ()
 
21
                threading.Thread.__init__ (self)
 
22
 
 
23
        def run (self):
 
24
                try:
 
25
                        res = self.youtube.service.Get (self.url).entry
 
26
                except gdata.service.RequestError:
 
27
                        """Probably a 503 service unavailable. Unfortunately we can't give an error message, as we're not in the GUI thread"""
 
28
                        """Just let the lock go and return"""
 
29
                        res = None
 
30
                gobject.idle_add (self.publish_results, res)
 
31
 
 
32
        def publish_results(self, res):
 
33
                self._lock.acquire (True)
 
34
                self.youtube.entry[self.treeview_name] = res
 
35
                self._done = True
 
36
                self._lock.release ()
 
37
                return False
 
38
 
 
39
        @property
 
40
        def done (self):
 
41
                """ Thread-safe property to know whether the query is done or not """
 
42
                self._lock.acquire (True)
 
43
                res = self._done
 
44
                self._lock.release ()
 
45
                return res
 
46
 
 
47
class CallbackThread (threading.Thread):
 
48
        def __init__ (self, callback, *args, **kwargs):
 
49
                self.callback = callback
 
50
                self.args = args
 
51
                self.kwargs = kwargs
 
52
                threading.Thread.__init__ (self)
 
53
 
 
54
        def run (self):
 
55
                res = self.callback (*self.args, **self.kwargs)
 
56
                while res == True:
 
57
                        res = self.callback (*self.args, **self.kwargs)
21
58
 
22
59
class YouTube (totem.Plugin):
23
60
        def __init__ (self):
24
 
                totem.Plugin.__init__(self)
 
61
                totem.Plugin.__init__ (self)
25
62
                self.debug = False
26
63
                self.gstreamer_plugins_present = True
27
64
 
 
65
                """Search counters (per search type)"""
 
66
                self.in_search = {}
 
67
                self.search_token = {} # Used as an ID for a search thread
 
68
 
28
69
                self.max_results = 20
29
70
                self.button_down = False
30
71
 
34
75
                self.start_index = {}
35
76
                self.results = {} # This is just the number of results from the last pagination query
36
77
                self.entry = {}
37
 
                self.entry_lock = threading.Lock ()
38
78
 
39
79
                self.current_treeview_name = ""
40
80
                self.notebook_pages = []
41
81
 
42
82
                self.vadjust = {}
43
83
                self.liststore = {}
 
84
                self.treeview = {}
 
85
 
44
86
        def activate (self, totem_object):
45
 
                """Check for the availability of the flvdemux and soup GStreamer plugins"""
 
87
                """Check for the availability of the flvdemux and souphttpsrc GStreamer plugins"""
46
88
                bvw_name = totem_object.get_video_widget_backend_name ()
47
89
 
 
90
                """If the user's selected 1.5Mbps or greater as their connection speed, grab higher-quality videos
 
91
                   and drop the requirement for the flvdemux plugin."""
 
92
                self.gconf_client = gconf.client_get_default ()
 
93
 
48
94
                if bvw_name.find ("GStreamer") != -1:
49
95
                        try:
50
96
                                import pygst
52
98
                                import gst
53
99
 
54
100
                                registry = gst.registry_get_default ()
55
 
                                if registry.find_plugin ("flvdemux") == None or registry.find_plugin ("soup") == None:
 
101
                                if registry.find_plugin ("soup") == None:
56
102
                                        """This means an error will be displayed when they try to play anything"""
57
103
                                        self.gstreamer_plugins_present = False
58
104
                        except ImportError:
59
105
                                """Do nothing; either it's using xine or python-gstreamer isn't installed"""
60
106
 
61
 
                """Continue loading the plugin as before"""             
 
107
                """Continue loading the plugin as before"""
62
108
                self.builder = self.load_interface ("youtube.ui", True, totem_object.get_main_window (), self)
63
109
                self.totem = totem_object
64
110
 
66
112
                self.search_entry.connect ("activate", self.on_search_entry_activated)
67
113
                self.search_button = self.builder.get_object ("yt_search_button")
68
114
                self.search_button.connect ("clicked", self.on_search_button_clicked)
 
115
                self.progress_bar = self.builder.get_object ("yt_progress_bar")
69
116
 
70
117
                self.notebook = self.builder.get_object ("yt_notebook")
71
118
                self.notebook.connect ("switch-page", self.on_notebook_page_changed)
80
127
                totem_object.add_sidebar_page ("youtube", _("YouTube"), self.vbox)
81
128
 
82
129
                """Set up the service"""
83
 
                self.service = gdata.service.GDataService (None, None, "HOSTED_OR_GOOGLE", None, None, "gdata.youtube.com")
 
130
                self.service = gdata.service.GDataService (account_type = "HOSTED_OR_GOOGLE", server = "gdata.youtube.com")
 
131
 
84
132
        def deactivate (self, totem):
85
133
                totem.remove_sidebar_page ("youtube")
 
134
 
86
135
        def setup_treeview (self, treeview_name):
87
136
                self.start_index[treeview_name] = 1
88
137
                self.results[treeview_name] = 0
89
138
                self.entry[treeview_name] = None
 
139
                self.in_search[treeview_name] = False
90
140
 
91
141
                """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."""
92
142
                renderer = totem.CellRendererVideo (use_placeholder = True)
96
146
                treeview.connect_after ("starting-video", self.on_starting_video)
97
147
                treeview.insert_column_with_attributes (0, _("Videos"), renderer, thumbnail=0, title=1)
98
148
 
 
149
                """Add the extra popup menu options. This is done here rather than in the UI file, because it's done for multiple treeviews;
 
150
                if it were done in the UI file, the same action group would be used multiple times, which GTK+ doesn't like."""
 
151
                ui_manager = treeview.get_ui_manager ()
 
152
                action_group = gtk.ActionGroup ("youtube-action-group")
 
153
                action = gtk.Action ("open-in-web-browser", _("_Open in Web Browser"), _("Open the video in your web browser"), "gtk-jump-to")
 
154
                action_group.add_action_with_accel (action, None)
 
155
 
 
156
                ui_manager.insert_action_group (action_group, 1)
 
157
                ui_manager.add_ui (ui_manager.new_merge_id (),
 
158
                                   "/ui/totem-video-list-popup/",
 
159
                                   "open-in-web-browser",
 
160
                                   "open-in-web-browser",
 
161
                                   gtk.UI_MANAGER_MENUITEM,
 
162
                                   False)
 
163
                menu_item = ui_manager.get_action ("/ui/totem-video-list-popup/open-in-web-browser")
 
164
                menu_item.connect ("activate", self.on_open_in_web_browser_activated)
 
165
 
99
166
                self.vadjust[treeview_name] = treeview.get_vadjustment ()
100
167
                self.vadjust[treeview_name].connect ("value-changed", self.on_value_changed)
101
168
                vscroll = self.builder.get_object ("yt_scrolled_window_" + treeview_name).get_vscrollbar ()
103
170
                vscroll.connect ("button-release-event", self.on_button_release_event)
104
171
 
105
172
                self.liststore[treeview_name] = self.builder.get_object ("yt_liststore_" + treeview_name)
 
173
                self.treeview[treeview_name] = treeview
106
174
                treeview.set_model (self.liststore[treeview_name])
 
175
 
107
176
        def on_notebook_page_changed (self, notebook, notebook_page, page_num):
108
177
                self.current_treeview_name = self.notebook_pages[page_num]
 
178
 
109
179
        def on_row_activated (self, treeview, path, column):
 
180
                if self.debug:
 
181
                        print "Activating row"
 
182
 
110
183
                model, rows = treeview.get_selection ().get_selected_rows ()
111
184
                iter = model.get_iter (rows[0])
112
185
                youtube_id = model.get_value (iter, 3)
113
 
                
 
186
 
114
187
                """Get related videos"""
115
188
                self.youtube_id = youtube_id
116
189
                self.start_index["related"] = 1
117
190
                self.results["related"] = 0
118
 
                self.get_results ("/feeds/videos/" + urllib.quote (youtube_id) + "/related?max-results=" + str (self.max_results), "related")
 
191
                if self.in_search == False or self.current_treeview_name == "related":
 
192
                        self.progress_bar.set_text (_("Fetching related videos..."))
 
193
                self.get_results ("/feeds/api/videos/" + urllib.quote (youtube_id) + "/related?max-results=" + str (self.max_results), "related")
 
194
 
 
195
                if self.debug:
 
196
                        print "Done activating row"
 
197
 
 
198
        def get_fmt_string (self):
 
199
                if self.gconf_client.get_int ("/apps/totem/connection_speed") >= 10:
 
200
                        return "&fmt=18"
 
201
                else:
 
202
                        return ""
 
203
 
 
204
        def resolve_t_param (self, youtube_id):
 
205
                """We have to get the t parameter from the actual video page, since Google changed how their URLs work"""
 
206
                stream = urllib.urlopen ("http://youtube.com/watch?v=" + urllib.quote (youtube_id))
 
207
                regexp1 = re.compile ("swfArgs.*\"t\": \"([^\"]+)\"")
 
208
                regexp2 = re.compile ("</head>")
 
209
 
 
210
                contents = stream.read ()
 
211
                if contents != "":
 
212
                        """Check for the t parameter, which is now in a JavaScript array on the video page"""
 
213
                        matches = regexp1.search (contents)
 
214
                        if (matches != None):
 
215
                                stream.close ()
 
216
                                return matches.group (1)
 
217
 
 
218
                        """Check to see if we've come to the end of the <head> tag; in which case, we should give up"""
 
219
                        if (regexp2.search (contents) != None):
 
220
                                stream.close ()
 
221
                                return ""
 
222
 
 
223
                stream.close ()
 
224
                return ""
 
225
 
119
226
        def on_starting_video (self, treeview, path, user_data):
120
227
                """Display an error if the required GStreamer plugins aren't installed"""
121
228
                if self.gstreamer_plugins_present == False:
126
233
                                                              self.totem.get_main_window ())
127
234
                        return False
128
235
 
129
 
                model, rows = treeview.get_selection ().get_selected_rows ()
 
236
                return True
 
237
 
 
238
        def on_open_in_web_browser_activated (self, action):
 
239
                model, rows = self.treeview[self.current_treeview_name].get_selection ().get_selected_rows ()
130
240
                iter = model.get_iter (rows[0])
131
241
                youtube_id = model.get_value (iter, 3)
132
242
 
133
 
                """Get the video stream MRL"""
134
 
                try:
135
 
                        conn = httplib.HTTPConnection ("www.youtube.com")
136
 
                        conn.request ("GET", "/v/" + urllib.quote (youtube_id))
137
 
                        response = conn.getresponse ()
138
 
                except:
139
 
                        print "Could not resolve stream MRL for YouTube video \"" + youtube_id + "\"."
140
 
                        return False
141
 
 
142
 
                if response.status == 303:
143
 
                        location = response.getheader("location")
144
 
                        mrl = "http://www.youtube.com/get_video?video_id=" + urllib.quote (youtube_id) + "&t=" + urllib.quote (re.match (".*[?&]t=([^&]+)", location).groups ()[0])
145
 
                else:
146
 
                        mrl = "http://www.youtube.com/v/" + urllib.quote (youtube_id)
147
 
                conn.close ()
148
 
 
149
 
                model.set_value (iter, 2, mrl)
150
 
 
151
 
                return True
 
243
                """Open the video in the browser"""
 
244
                os.spawnlp (os.P_NOWAIT, "xdg-open", "xdg-open", "http://www.youtube.com/watch?v=" + urllib.quote (youtube_id) + self.get_fmt_string ())
 
245
 
152
246
        def on_button_press_event (self, widget, event):
153
247
                self.button_down = True
 
248
 
154
249
        def on_button_release_event (self, widget, event):
155
250
                self.button_down = False
156
251
                self.on_value_changed (self.vadjust[self.current_treeview_name])
 
252
 
157
253
        def on_value_changed (self, adjustment):
158
254
                """Load more results when we get near the bottom of the treeview"""
159
255
                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:
160
 
                        self.results[self.current_treeview_name] = 0
161
256
                        if self.current_treeview_name == "search":
162
 
                                self.get_results ("/feeds/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)
163
257
                                if self.debug:
164
258
                                        print "Getting more results for search \"" + self.search_terms + "\" from offset " + str (self.start_index["search"])
 
259
                                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)
165
260
                        elif self.current_treeview_name == "related":
166
 
                                self.get_results ("/feeds/videos/" + urllib.quote_plus (self.youtube_id) + "/related?max-results=" + str (self.max_results) + "&start-index=" + str (self.start_index["related"]), "related", False)
167
261
                                if self.debug:
168
262
                                        print "Getting more related videos for video \"" + self.youtube_id + "\" from offset " + str (self.start_index["related"])
 
263
                                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)
 
264
 
169
265
        def convert_url_to_id (self, url):
170
266
                """Find the last clause in the URL; after the last /"""
171
267
                return url.split ("/").pop ()
172
 
        def populate_list_from_results (self, treeview_name):
 
268
 
 
269
        def populate_list_from_results (self, search_token, treeview_name, thread):
 
270
                """Check to see if this search has been cancelled"""
 
271
                if search_token != self.search_token[treeview_name]:
 
272
                        return False
 
273
 
173
274
                """Check and acquire the lock"""
174
 
                if self.entry_lock.acquire (False) == False:
 
275
                if not thread.done:
 
276
                        if self.current_treeview_name == treeview_name:
 
277
                                self.progress_bar.pulse ()
175
278
                        return True
176
279
 
 
280
                CallbackThread (self.process_next_thumbnail, search_token, treeview_name).start ()
 
281
                return False
 
282
 
 
283
        def process_next_thumbnail (self, search_token, treeview_name):
 
284
                """Note that all the calls to gobject.idle_add are so that the respective
 
285
                   UI function calls are made in the main thread, since process_next_thumbnail
 
286
                   is run in the CallbackThread thread."""
 
287
 
 
288
                """Check to see if this search has been cancelled"""
 
289
                if search_token != self.search_token[treeview_name]:
 
290
                        return False
 
291
 
177
292
                """Return if there are no results (or we've finished)"""
178
293
                if self.entry[treeview_name] == None or len (self.entry[treeview_name]) == 0:
179
 
                        """Revert the cursor"""
180
 
                        window = self.vbox.window
181
 
                        window.set_cursor (None)
 
294
                        gobject.idle_add (self._clear_ui, treeview_name)
182
295
 
183
296
                        self.entry[treeview_name] = None
184
 
                        self.entry_lock.release ()
 
297
                        self.in_search[treeview_name] = False
185
298
 
186
299
                        return False
187
300
 
190
303
                self.results[treeview_name] += 1
191
304
                self.start_index[treeview_name] += 1
192
305
                youtube_id = self.convert_url_to_id (entry.id.text)
193
 
                mrl = "http://www.youtube.com/v/" + urllib.quote (youtube_id)
194
 
 
195
 
                self.entry_lock.release ()
196
 
 
197
 
                """Find the thumbnail tag"""
 
306
 
 
307
                """Find the content tag"""
198
308
                for _element in entry.extension_elements:
199
 
                        if _element.tag == "group":
 
309
                        if _element.tag =="group":
200
310
                                break
201
311
 
 
312
                content_elements = _element.FindChildren ("content")
 
313
                if len (content_elements) == 0:
 
314
                        return True
 
315
                mrl = content_elements[0].attributes['url']
 
316
 
202
317
                """Download the thumbnail and store it in a temporary location so we can get a pixbuf from it"""
203
318
                thumbnail_url = _element.FindChildren ("thumbnail")[0].attributes['url']
204
319
                try:
205
320
                        filename, headers = urllib.urlretrieve (thumbnail_url)
206
321
                except IOError:
207
 
                        print "Could not load thumbnail " + thumbnail_url + " for video."
208
322
                        return True
209
323
 
210
324
                try:
211
325
                        pixbuf = gtk.gdk.pixbuf_new_from_file (filename)
212
326
                except gobject.GError:
213
 
                        print "Could not load thumbnail " + filename + " for video. It has been left in place for investigation."
 
327
                        print "Could not open thumbnail " + filename + " for video. It has been left in place for investigation."
214
328
                        return True
215
329
 
216
330
                """Don't leak the temporary file"""
217
 
                unlink (filename)
218
 
 
219
 
                self.liststore[treeview_name].append ([pixbuf, entry.title.text, mrl, youtube_id])
 
331
                os.unlink (filename)
 
332
 
 
333
                """Get the video stream MRL"""
 
334
                t_param = self.resolve_t_param (youtube_id)
 
335
 
 
336
                if t_param != "":
 
337
                        mrl = "http://www.youtube.com/get_video?video_id=" + urllib.quote (youtube_id) + "&t=" + urllib.quote (t_param) + self.get_fmt_string ()
 
338
 
 
339
                gobject.idle_add (self._append_to_liststore, treeview_name, pixbuf, entry.title.text, mrl, youtube_id, search_token)
220
340
 
221
341
                return True
 
342
 
 
343
        def _clear_ui (self, treeview_name):
 
344
                """Revert the cursor"""
 
345
                window = self.vbox.window
 
346
                window.set_cursor (None)
 
347
 
 
348
                if self.in_search == True or self.current_treeview_name == treeview_name:
 
349
                        """Only blank the progress bar if we're the only search taking place"""
 
350
                        self.progress_bar.set_fraction (0.0)
 
351
                        self.progress_bar.set_text ("")
 
352
 
 
353
                return False
 
354
 
 
355
        def _append_to_liststore (self, treeview_name, pixbuf, title, mrl, id, search_token):
 
356
                """Check to see if this search has been cancelled"""
 
357
                if search_token != self.search_token[treeview_name]:
 
358
                        return False
 
359
 
 
360
                if self.in_search == True or self.current_treeview_name == treeview_name:
 
361
                        self.progress_bar.set_fraction (float (self.results[treeview_name]) / float (self.max_results))
 
362
                self.liststore[treeview_name].append ([pixbuf, title, mrl, id])
 
363
                return False
 
364
 
222
365
        def on_search_button_clicked (self, button):
223
366
                search_terms = self.search_entry.get_text ()
224
367
 
231
374
                self.search_terms = search_terms
232
375
                self.start_index["search"] = 1
233
376
                self.results["search"] = 0
234
 
                self.get_results ("/feeds/videos?vq=" + urllib.quote_plus (search_terms) + "&orderby=relevance&max-results=" + str (self.max_results), "search")
 
377
                self.progress_bar.set_text (_("Fetching search results..."))
 
378
                self.get_results ("/feeds/api/videos?vq=" + urllib.quote_plus (search_terms) + "&orderby=relevance&max-results=" + str (self.max_results), "search")
 
379
 
235
380
        def on_search_entry_activated (self, entry):
236
381
                self.search_button.clicked ()
 
382
 
237
383
        def get_results (self, url, treeview_name, clear = True):
 
384
                if self.in_search[treeview_name] == True and clear == False:
 
385
                        """If we're trying to fetch more results while another search is happening, just cancel"""
 
386
                        if self.debug:
 
387
                                print "Cancelling getting more results due to existing incomplete search."
 
388
                        self.in_search[treeview_name] = False
 
389
                        return
 
390
                elif clear == False:
 
391
                        self.results[self.current_treeview_name] = 0
 
392
                        self.progress_bar.set_text (_("Fetching more videos..."))
 
393
 
 
394
                """If we're trying to do another full search while one's already happening, just continue as
 
395
                   normal. The populate_list_from_results function will notice, and cancel the old search."""
 
396
 
238
397
                if clear:
239
398
                        self.liststore[treeview_name].clear ()
240
399
 
241
400
                if self.debug:
242
401
                        print "Getting results from URL \"" + url + "\""
243
402
 
 
403
                self.in_search[treeview_name] = True
 
404
                self.search_token[treeview_name] = random.random ()
 
405
 
244
406
                """Give us a nice waiting cursor"""
245
407
                window = self.vbox.window
246
408
                window.set_cursor (gtk.gdk.Cursor (gtk.gdk.WATCH))
 
409
                if self.current_treeview_name == treeview_name:
 
410
                        self.progress_bar.pulse ()
247
411
 
248
 
                self.results_downloaded = False
249
 
                DownloadThread (self, url, treeview_name).start ()
250
 
                gobject.idle_add (self.populate_list_from_results, treeview_name)
 
412
                thread = DownloadThread (self, url, treeview_name)
 
413
                gobject.timeout_add (350, self.populate_list_from_results, self.search_token[treeview_name], treeview_name, thread)
 
414
                thread.start()
251
415