69
82
self.scope = Unity.Scope.new("/net/launchpad/scope/remotevideos")
70
83
self.scope.search_in_global = False
71
84
self.scope.connect("search-changed", self.on_search_changed)
72
self.scope.connect("notify::active", self.on_lens_active)
73
self.scope.connect("filters-changed", self.on_filtering_changed)
85
self.scope.connect("filters-changed", self.on_filtering_or_preference_changed)
74
86
self.scope.props.sources.connect("notify::filtering",
75
self.on_filtering_changed)
76
self.scope.connect ("activate-uri", self.on_activate_uri)
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())
77
91
self.session = Soup.SessionAsync()
78
92
self.session.props.user_agent = "Unity Video Lens Remote Scope v0.4"
79
93
self.session.add_feature_by_type(SoupGNOME.ProxyResolverGNOME)
80
94
self.query_list_of_sources()
81
95
self.update_recommendations()
96
self.preferences = Unity.PreferencesManager.get_default()
97
self.preferences.connect("notify::remote-content-search", self.on_filtering_or_preference_changed)
82
98
self.scope.export()
99
# refresh the at least once every 30 minutes
100
GLib.timeout_add_seconds(REFRESH_INTERVAL/2, self.refresh_online_videos)
84
102
def update_recommendations(self):
85
103
"""Query the server for 'recommendations'.
87
In v0, that means simply do an empty search.
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.
89
msg = Soup.Message.new("GET", SERVER + "/search")
108
msg = Soup.Message.new("GET", SERVER + "/search?q=&sources=Amazon")
90
109
self.session.queue_message(msg, self._recs_cb, None)
92
111
def _recs_cb(self, session, msg, user_data):
132
151
dt = RETRY_INTERVAL
133
152
GLib.timeout_add_seconds(dt, self.query_list_of_sources)
135
def on_filtering_changed(self, *_):
136
"""Run another search when a filter change is notified."""
154
def on_filtering_or_preference_changed(self, *_):
155
"""Run another search when a filter or preference change is notified."""
137
156
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)
158
def refresh_online_videos(self, *_):
159
""" Periodically refresh the results """
160
self.scope.queue_search_changed(Unity.SearchType.DEFAULT)
144
163
def source_activated(self, source):
145
164
"""Return the state of a sources filter option."""
165
184
the server, update the model and notify the lens when we are done."""
166
185
search_string = search.props.search_string.strip()
167
186
# note search_string is a utf-8 encoded string, now:
168
print "Search changed to %r" % search_string
187
print "Remote scope search changed to %r" % search_string
169
188
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:
171
198
# Create a list of activated sources
172
199
sources = [source for source in self.sources_list
173
200
if self.source_activated(source)]
186
213
if search and not working:
187
214
search.finished()
189
def update_results_model(self, search, model, results):
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):
190
232
"""Run the search method and check if a list of sources is present.
191
233
This is useful when coming out of a network issue."""
192
234
for i in results:
194
236
uri = i['url'].encode('utf-8')
195
237
title = i['title'].encode('utf-8')
196
238
icon = i['img'].encode('utf-8')
197
240
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()
198
256
if uri.startswith('http'):
199
257
# Instead of an uri, we pass a string of uri + metadata
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, AttributeError):
203
print "Malformed result, skipping."
204
model.flush_revision_queue()
207
if self.sources_list == []:
208
self.query_list_of_sources()
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
210
266
def get_results(self, search_string, search, model, sources):
211
267
"""Query the server with the search string and the list of sources."""
212
268
if not search_string and not sources and self.recommendations:
213
self.update_results_model(search, model, self.recommendations)
269
self.update_results_model(search, model, self.recommendations, True)
215
271
if self._pending is not None:
216
272
self.session.cancel_message(self._pending,
217
273
Soup.KnownStatusCode.CANCELLED)
218
274
query = dict(q=search_string)
275
query['split'] = 'true'
220
277
query['sources'] = sources.encode("utf-8")
221
278
query = urlencode(query)
264
321
# Then close the Dash
265
322
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)
267
428
def zeitgeist_insert_event(self, uri, title, icon):
268
429
"""Insert item uri as a new Zeitgeist event"""
269
430
# "storage" is an optional zg string