~unity-lens-videos/unity-lens-video/remote-videos

« back to all changes in this revision

Viewing changes to src/unity-scope-video-remote

Tags: unity-scope-video-remote-0.3.9
Moving trunk to it's own lp project

Show diffs side-by-side

added added

removed removed

Lines of Context:
22
22
import sys
23
23
from urllib import urlencode
24
24
import time
 
25
from datetime import datetime
 
26
import gettext
25
27
 
26
28
#pylint: disable=E0611
27
29
from zeitgeist.client import ZeitgeistClient
42
44
)
43
45
#pylint: enable=E0611
44
46
 
 
47
APP_NAME = "unity-lens-video"
 
48
LOCAL_PATH = "/usr/share/locale/"
 
49
 
 
50
gettext.bindtextdomain(APP_NAME, LOCAL_PATH)
 
51
gettext.textdomain(APP_NAME)
 
52
_ = gettext.gettext
 
53
n_ = gettext.ngettext
 
54
 
 
55
CAT_INDEX_ONLINE = 1
 
56
CAT_INDEX_MORE = 2
 
57
 
45
58
BUS_NAME = "net.launchpad.scope.RemoteVideos"
46
59
SERVER = "http://videosearch.ubuntu.com/v0"
47
60
 
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)
83
101
 
84
102
    def update_recommendations(self):
85
103
        """Query the server for 'recommendations'.
86
104
 
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.
88
107
        """
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)
91
110
 
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)
134
153
 
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)
138
157
 
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)
 
161
        return True
143
162
 
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
170
190
        model.clear()
 
191
 
 
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:
 
195
            search.finished()
 
196
            return
 
197
 
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()
188
215
 
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)
 
222
        else:
 
223
            category = CAT_INDEX_MORE if is_treat_yourself else CAT_INDEX_ONLINE
 
224
            self.process_result_array(results, model, category)
 
225
 
 
226
        if search:
 
227
            search.finished()
 
228
        if self.sources_list == []:
 
229
            self.query_list_of_sources()
 
230
 
 
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')
 
239
                result_icon = icon
197
240
                comment = i['source'].encode('utf-8')
 
241
                details_url = ""
 
242
                if "details" in i:
 
243
                    details_url = i['details'].encode('utf-8')
 
244
                if category == CAT_INDEX_MORE:
 
245
                    price = None
 
246
                    price_key = 'formatted_price'
 
247
                    if price_key in i:
 
248
                        price = i[price_key]
 
249
                    if not price or price == 'free':
 
250
                        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()
 
255
 
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()
205
 
        if search:
206
 
            search.finished()
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
209
265
 
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)
214
270
            return
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'
219
276
        if sources:
220
277
            query['sources'] = sources.encode("utf-8")
221
278
        query = urlencode(query)
245
302
                results = json.loads(msg.response_body.data)
246
303
            except ValueError:
247
304
                print "Error: got garbage json from the server"
248
 
        if isinstance(results, dict):
 
305
        if isinstance(results, dict) and 'results' in results:
249
306
            results = results.get('results', [])
250
307
        return results
251
308
 
264
321
        # Then close the Dash
265
322
        return Unity.ActivationResponse(goto_uri='', handled=2 )
266
323
 
 
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)
 
328
 
 
329
        title = rawuri.split('lens-meta://')[1]
 
330
        thumbnail_uri = rawuri.split('lens-meta://')[2]
 
331
        subtitle = ''
 
332
        desc = ''
 
333
        source = ''
 
334
        if isinstance(details, dict):
 
335
            title = details['title']
 
336
            thumbnail_uri = details['image']
 
337
            desc = details['description']
 
338
            source = details['source']
 
339
 
 
340
        try:
 
341
            thumbnail_file = Gio.File.new_for_uri (thumbnail_uri)
 
342
            thumbnail_icon = Gio.FileIcon.new(thumbnail_file)
 
343
        except:
 
344
            thumbnail_icon = None
 
345
        
 
346
        release_date = None
 
347
        duration = 0
 
348
            
 
349
        if "release_date" in details:
 
350
            try:
 
351
                # v1 spec states release_date will be YYYY-MM-DD
 
352
                release_date = datetime.strptime(details["release_date"], "%Y-%m-%d")
 
353
            except ValueError:
 
354
                print "Failed to parse release_date since it was not in format: YYYY-MM-DD"
 
355
 
 
356
        if "duration" in details:
 
357
            # Given in seconds, convert to minutes
 
358
            duration = int(details["duration"]) / 60
 
359
        
 
360
        if release_date != None:
 
361
            subtitle = release_date.strftime("%Y")
 
362
        if duration > 0:
 
363
            d = n_("%d min", "%d mins", duration) % duration
 
364
            if len(subtitle) > 0:
 
365
                subtitle += ", " + d
 
366
            else:
 
367
                subtitle = d
 
368
            
 
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)
 
375
 
 
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"]
 
383
            if cast:
 
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"] 
 
388
            if 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"]))
 
398
 
 
399
        return preview
 
400
 
 
401
    def get_details(self, details_url):
 
402
        """Get online video details for preview"""
 
403
        details = []
 
404
        if len(details_url) == 0:
 
405
            print "Cannot get preview details, no details_url specified."
 
406
            return details
 
407
 
 
408
        if self._pending is not None:
 
409
            self.session.cancel_message(self._pending,
 
410
                                        Soup.KnownStatusCode.CANCELLED)
 
411
 
 
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)
 
415
        try:
 
416
            details = json.loads(self._pending.response_body.data)
 
417
        except ValueError:
 
418
            print "Error: got garbage json from the server"
 
419
        except TypeError:
 
420
            print "Error: failed to connect to videosearch server to retrieve details."
 
421
 
 
422
        return details
 
423
 
 
424
    def play_video (self, action, uri):
 
425
        """Preview request - Play action handler"""
 
426
        return self.on_activate_uri (action, uri)
 
427
 
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