2
import gobject, gtk, gconf
11
14
class DownloadThread (threading.Thread):
12
15
def __init__ (self, youtube, url, treeview_name):
13
16
self.youtube = youtube
15
18
self.treeview_name = treeview_name
16
threading.Thread.__init__ (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 ()
20
self._lock = threading.Lock ()
21
threading.Thread.__init__ (self)
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"""
30
gobject.idle_add (self.publish_results, res)
32
def publish_results(self, res):
33
self._lock.acquire (True)
34
self.youtube.entry[self.treeview_name] = res
41
""" Thread-safe property to know whether the query is done or not """
42
self._lock.acquire (True)
47
class CallbackThread (threading.Thread):
48
def __init__ (self, callback, *args, **kwargs):
49
self.callback = callback
52
threading.Thread.__init__ (self)
55
res = self.callback (*self.args, **self.kwargs)
57
res = self.callback (*self.args, **self.kwargs)
22
59
class YouTube (totem.Plugin):
23
60
def __init__ (self):
24
totem.Plugin.__init__(self)
61
totem.Plugin.__init__ (self)
26
63
self.gstreamer_plugins_present = True
65
"""Search counters (per search type)"""
67
self.search_token = {} # Used as an ID for a search thread
28
69
self.max_results = 20
29
70
self.button_down = False
34
75
self.start_index = {}
35
76
self.results = {} # This is just the number of results from the last pagination query
37
self.entry_lock = threading.Lock ()
39
79
self.current_treeview_name = ""
40
80
self.notebook_pages = []
43
83
self.liststore = {}
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 ()
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 ()
48
94
if bvw_name.find ("GStreamer") != -1:
80
127
totem_object.add_sidebar_page ("youtube", _("YouTube"), self.vbox)
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")
84
132
def deactivate (self, totem):
85
133
totem.remove_sidebar_page ("youtube")
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
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)
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)
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,
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)
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)
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])
107
176
def on_notebook_page_changed (self, notebook, notebook_page, page_num):
108
177
self.current_treeview_name = self.notebook_pages[page_num]
109
179
def on_row_activated (self, treeview, path, column):
181
print "Activating row"
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)
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")
196
print "Done activating row"
198
def get_fmt_string (self):
199
if self.gconf_client.get_int ("/apps/totem/connection_speed") >= 10:
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>")
210
contents = stream.read ()
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):
216
return matches.group (1)
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):
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 ())
129
model, rows = treeview.get_selection ().get_selected_rows ()
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)
133
"""Get the video stream MRL"""
135
conn = httplib.HTTPConnection ("www.youtube.com")
136
conn.request ("GET", "/v/" + urllib.quote (youtube_id))
137
response = conn.getresponse ()
139
print "Could not resolve stream MRL for YouTube video \"" + youtube_id + "\"."
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])
146
mrl = "http://www.youtube.com/v/" + urllib.quote (youtube_id)
149
model.set_value (iter, 2, mrl)
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 ())
152
246
def on_button_press_event (self, widget, event):
153
247
self.button_down = True
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])
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)
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)
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)
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):
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]:
173
274
"""Check and acquire the lock"""
174
if self.entry_lock.acquire (False) == False:
276
if self.current_treeview_name == treeview_name:
277
self.progress_bar.pulse ()
280
CallbackThread (self.process_next_thumbnail, search_token, treeview_name).start ()
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."""
288
"""Check to see if this search has been cancelled"""
289
if search_token != self.search_token[treeview_name]:
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)
183
296
self.entry[treeview_name] = None
184
self.entry_lock.release ()
297
self.in_search[treeview_name] = False
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)
195
self.entry_lock.release ()
197
"""Find the thumbnail tag"""
307
"""Find the content tag"""
198
308
for _element in entry.extension_elements:
199
if _element.tag == "group":
309
if _element.tag =="group":
312
content_elements = _element.FindChildren ("content")
313
if len (content_elements) == 0:
315
mrl = content_elements[0].attributes['url']
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']
205
320
filename, headers = urllib.urlretrieve (thumbnail_url)
207
print "Could not load thumbnail " + thumbnail_url + " for video."
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."
216
330
"""Don't leak the temporary file"""
219
self.liststore[treeview_name].append ([pixbuf, entry.title.text, mrl, youtube_id])
333
"""Get the video stream MRL"""
334
t_param = self.resolve_t_param (youtube_id)
337
mrl = "http://www.youtube.com/get_video?video_id=" + urllib.quote (youtube_id) + "&t=" + urllib.quote (t_param) + self.get_fmt_string ()
339
gobject.idle_add (self._append_to_liststore, treeview_name, pixbuf, entry.title.text, mrl, youtube_id, search_token)
343
def _clear_ui (self, treeview_name):
344
"""Revert the cursor"""
345
window = self.vbox.window
346
window.set_cursor (None)
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 ("")
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]:
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])
222
365
def on_search_button_clicked (self, button):
223
366
search_terms = self.search_entry.get_text ()
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")
235
380
def on_search_entry_activated (self, entry):
236
381
self.search_button.clicked ()
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"""
387
print "Cancelling getting more results due to existing incomplete search."
388
self.in_search[treeview_name] = False
391
self.results[self.current_treeview_name] = 0
392
self.progress_bar.set_text (_("Fetching more videos..."))
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."""
239
398
self.liststore[treeview_name].clear ()
242
401
print "Getting results from URL \"" + url + "\""
403
self.in_search[treeview_name] = True
404
self.search_token[treeview_name] = random.random ()
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 ()
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)