82
69
self.scope = Unity.Scope.new("/net/launchpad/scope/remotevideos")
83
70
self.scope.search_in_global = False
84
71
self.scope.connect("search-changed", self.on_search_changed)
85
self.scope.connect("filters-changed", self.on_filtering_or_preference_changed)
72
self.scope.connect("notify::active", self.on_lens_active)
73
self.scope.connect("filters-changed", self.on_filtering_changed)
86
74
self.scope.props.sources.connect("notify::filtering",
87
self.on_filtering_or_preference_changed)
88
self.scope.connect("activate-uri", self.on_activate_uri)
89
self.scope.connect("preview-uri", self.on_preview_uri)
90
self.scope.connect("generate-search-key", lambda scope, search: search.props.search_string.strip())
75
self.on_filtering_changed)
76
self.scope.connect ("activate-uri", self.on_activate_uri)
91
77
self.session = Soup.SessionAsync()
92
78
self.session.props.user_agent = "Unity Video Lens Remote Scope v0.4"
93
79
self.session.add_feature_by_type(SoupGNOME.ProxyResolverGNOME)
94
80
self.query_list_of_sources()
95
81
self.update_recommendations()
96
self.preferences = Unity.PreferencesManager.get_default()
97
self.preferences.connect("notify::remote-content-search", self.on_filtering_or_preference_changed)
98
82
self.scope.export()
99
# refresh the at least once every 30 minutes
100
GLib.timeout_add_seconds(REFRESH_INTERVAL/2, self.refresh_online_videos)
102
84
def update_recommendations(self):
103
85
"""Query the server for 'recommendations'.
105
In v0 extended, that means simply do an empty search with Amazon
106
as the source since it's the only paid provider at the moment.
87
In v0, that means simply do an empty search.
108
msg = Soup.Message.new("GET", SERVER + "/search?q=&sources=Amazon")
89
msg = Soup.Message.new("GET", SERVER + "/search")
109
90
self.session.queue_message(msg, self._recs_cb, None)
111
92
def _recs_cb(self, session, msg, user_data):
151
132
dt = RETRY_INTERVAL
152
133
GLib.timeout_add_seconds(dt, self.query_list_of_sources)
154
def on_filtering_or_preference_changed(self, *_):
155
"""Run another search when a filter or preference change is notified."""
135
def on_filtering_changed(self, *_):
136
"""Run another search when a filter change is notified."""
156
137
self.scope.queue_search_changed(Unity.SearchType.DEFAULT)
158
def refresh_online_videos(self, *_):
159
""" Periodically refresh the results """
160
self.scope.queue_search_changed(Unity.SearchType.DEFAULT)
139
def on_lens_active(self, *_):
140
""" Run a search when the lens is opened """
141
if self.scope.props.active:
142
self.scope.queue_search_changed(Unity.SearchType.DEFAULT)
163
144
def source_activated(self, source):
164
145
"""Return the state of a sources filter option."""
184
165
the server, update the model and notify the lens when we are done."""
185
166
search_string = search.props.search_string.strip()
186
167
# note search_string is a utf-8 encoded string, now:
187
print "Remote scope search changed to %r" % search_string
168
print "Search changed to %r" % search_string
188
169
model = search.props.results_model
189
# Clear results details map
192
# only perform the request if the user has not disabled
193
# online/commercial suggestions. That will hide the category as well.
194
if self.preferences.props.remote_content_search != Unity.PreferencesManagerRemoteContent.ALL:
198
171
# Create a list of activated sources
199
172
sources = [source for source in self.sources_list
200
173
if self.source_activated(source)]
213
186
if search and not working:
214
187
search.finished()
216
def update_results_model(self, search, model, results, is_treat_yourself=False):
217
"""Run the search method and check if a list of sources is present.
218
This is useful when coming out of a network issue."""
219
if isinstance(results, dict):
220
self.process_result_array(results['other'], model, CAT_INDEX_ONLINE)
221
self.process_result_array(results['treats'], model, CAT_INDEX_MORE)
223
category = CAT_INDEX_MORE if is_treat_yourself else CAT_INDEX_ONLINE
224
self.process_result_array(results, model, category)
228
if self.sources_list == []:
229
self.query_list_of_sources()
231
def process_result_array(self, results, model, category):
189
def update_results_model(self, search, model, results):
232
190
"""Run the search method and check if a list of sources is present.
233
191
This is useful when coming out of a network issue."""
234
192
for i in results:
236
194
uri = i['url'].encode('utf-8')
237
195
title = i['title'].encode('utf-8')
238
196
icon = i['img'].encode('utf-8')
240
197
comment = i['source'].encode('utf-8')
243
details_url = i['details'].encode('utf-8')
244
if category == CAT_INDEX_MORE:
246
price_key = 'formatted_price'
249
if not price or price == 'free':
251
anno_icon = Unity.AnnotatedIcon.new(Gio.FileIcon.new(Gio.File.new_for_uri(result_icon)))
252
anno_icon.props.category = Unity.CategoryType.MOVIE
253
anno_icon.props.ribbon = price
254
result_icon = anno_icon.to_string()
256
198
if uri.startswith('http'):
257
199
# Instead of an uri, we pass a string of uri + metadata
258
fake_uri = "%slens-meta://%slens-meta://%slens-meta://%s"
259
fake_uri = fake_uri % (uri, title, icon, details_url)
260
model.append(fake_uri,
261
result_icon, category, "text/html",
262
str(title), str(comment), str(uri))
263
except (KeyError, TypeError, AttributeError) as detail:
264
print "Malformed result, skipping.", detail
200
model.append(uri+'lens-meta://'+title+'lens-meta://'+icon,
201
icon, 2, "text/html", str(title), str(comment), str(uri))
202
except (KeyError, TypeError):
203
print "Malformed result, skipping."
204
model.flush_revision_queue()
207
if self.sources_list == []:
208
self.query_list_of_sources()
266
210
def get_results(self, search_string, search, model, sources):
267
211
"""Query the server with the search string and the list of sources."""
268
212
if not search_string and not sources and self.recommendations:
269
self.update_results_model(search, model, self.recommendations, True)
213
self.update_results_model(search, model, self.recommendations)
271
215
if self._pending is not None:
272
216
self.session.cancel_message(self._pending,
273
217
Soup.KnownStatusCode.CANCELLED)
274
218
query = dict(q=search_string)
275
query['split'] = 'true'
277
220
query['sources'] = sources.encode("utf-8")
278
221
query = urlencode(query)
321
264
# Then close the Dash
322
265
return Unity.ActivationResponse(goto_uri='', handled=2 )
324
def on_preview_uri (self, scope, rawuri):
325
"""Preview request handler"""
326
details_url = rawuri.split('lens-meta://')[3]
327
details = self.get_details(details_url)
329
title = rawuri.split('lens-meta://')[1]
330
thumbnail_uri = rawuri.split('lens-meta://')[2]
334
if isinstance(details, dict):
335
title = details['title']
336
thumbnail_uri = details['image']
337
desc = details['description']
338
source = details['source']
341
thumbnail_file = Gio.File.new_for_uri (thumbnail_uri)
342
thumbnail_icon = Gio.FileIcon.new(thumbnail_file)
344
thumbnail_icon = None
349
if "release_date" in details:
351
# v1 spec states release_date will be YYYY-MM-DD
352
release_date = datetime.strptime(details["release_date"], "%Y-%m-%d")
354
print "Failed to parse release_date since it was not in format: YYYY-MM-DD"
356
if "duration" in details:
357
# Given in seconds, convert to minutes
358
duration = int(details["duration"]) / 60
360
if release_date != None:
361
subtitle = release_date.strftime("%Y")
363
d = n_("%d min", "%d mins", duration) % duration
364
if len(subtitle) > 0:
369
preview = Unity.MoviePreview.new(title, subtitle, desc, thumbnail_icon)
370
play_video = Unity.PreviewAction.new("play", _("Play"), None)
371
play_video.connect('activated', self.play_video)
372
preview.add_action(play_video)
373
# For now, rating == -1 and num_ratings == 0 hides the rating widget from the preview
374
preview.set_rating(-1, 0)
376
# TODO: For details of future source types, factor out common detail key/value pairs
377
if "directors" in details:
378
directors = details["directors"]
379
preview.add_info(Unity.InfoHint.new("directors",
380
n_("Director", "Directors", len(directors)), None, ', '.join(directors)))
381
if "starring" in details:
382
cast = details["starring"]
384
preview.add_info(Unity.InfoHint.new("cast",
385
n_("Cast", "Cast", len(cast)), None, ', '.join(cast)))
386
if "genres" in details:
387
genres = details["genres"]
389
preview.add_info(Unity.InfoHint.new("genres",
390
n_("Genre", "Genres", len(genres)), None, ', '.join(genres)))
391
# TODO: Add Vimeo & YouTube details for v1 of JSON API
392
if "uploaded_by" in details:
393
preview.add_info(Unity.InfoHint.new("uploaded-by", _("Uploaded by"),
394
None, details["uploaded_by"]))
395
if "date_uploaded" in details:
396
preview.add_info(Unity.InfoHint.new("uploaded-on", _("Uploaded on"),
397
None, details["date_uploaded"]))
401
def get_details(self, details_url):
402
"""Get online video details for preview"""
404
if len(details_url) == 0:
405
print "Cannot get preview details, no details_url specified."
408
if self._pending is not None:
409
self.session.cancel_message(self._pending,
410
Soup.KnownStatusCode.CANCELLED)
412
self._pending = Soup.Message.new("GET", details_url)
413
# Send a synchronous GET request for video metadata details
414
self.session.send_message(self._pending)
416
details = json.loads(self._pending.response_body.data)
418
print "Error: got garbage json from the server"
420
print "Error: failed to connect to videosearch server to retrieve details."
424
def play_video (self, action, uri):
425
"""Preview request - Play action handler"""
426
return self.on_activate_uri (action, uri)
428
267
def zeitgeist_insert_event(self, uri, title, icon):
429
268
"""Insert item uri as a new Zeitgeist event"""
430
269
# "storage" is an optional zg string
450
289
"""Connect to the session bus, exit if there is a running instance."""
452
session_bus_connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
453
session_bus = Gio.DBusProxy.new_sync(session_bus_connection, 0, None,
454
'org.freedesktop.DBus',
455
'/org/freedesktop/DBus',
456
'org.freedesktop.DBus', None)
457
result = session_bus.call_sync('RequestName',
458
GLib.Variant("(su)", (BUS_NAME, 0x4)),
461
# Unpack variant response with signature "(u)". 1 means we got it.
462
result = result.unpack()[0]
465
print >> sys.stderr, "Failed to own name %s. Bailing out." % BUS_NAME
290
session_bus_connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
291
session_bus = Gio.DBusProxy.new_sync(session_bus_connection, 0, None,
292
'org.freedesktop.DBus',
293
'/org/freedesktop/DBus',
294
'org.freedesktop.DBus', None)
295
result = session_bus.call_sync('RequestName',
296
GLib.Variant("(su)", (BUS_NAME, 0x4)),
299
# Unpack variant response with signature "(u)". 1 means we got it.
300
result = result.unpack()[0]
303
print >> sys.stderr, "Failed to own name %s. Bailing out." % BUS_NAME
468
304
raise SystemExit(1)