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

« back to all changes in this revision

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

  • Committer: David Callé
  • Date: 2012-03-09 21:16:19 UTC
  • Revision ID: davidc@framli.eu-20120309211619-hu5gtzjiw97fwh8h
Ensure we are passing strings to Dee + catch TypeError

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
27
25
 
28
26
#pylint: disable=E0611
29
27
from zeitgeist.client import ZeitgeistClient
44
42
)
45
43
#pylint: enable=E0611
46
44
 
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
 
 
58
45
BUS_NAME = "net.launchpad.scope.RemoteVideos"
59
 
SERVER = "http://videosearch.ubuntu.com/v0"
 
46
SERVER = "http://video.u1.to/v0"
60
47
 
61
48
REFRESH_INTERVAL = 3600         # fetch sources & recommendations once an hour
62
49
RETRY_INTERVAL = 60             # retry sources/recommendations after a minute
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)
101
83
 
102
84
    def update_recommendations(self):
103
85
        """Query the server for 'recommendations'.
104
86
 
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.
107
88
        """
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)
110
91
 
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)
153
134
 
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)
157
138
 
158
 
    def refresh_online_videos(self, *_):
159
 
        """ Periodically refresh the results """
160
 
        self.scope.queue_search_changed(Unity.SearchType.DEFAULT)
161
 
        return True
 
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)
162
143
 
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
190
170
        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
 
 
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()
215
188
 
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):
 
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')
239
 
                result_icon = icon
240
197
                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
 
 
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()
 
205
        if search:
 
206
            search.finished()
 
207
        if self.sources_list == []:
 
208
            self.query_list_of_sources()
265
209
 
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)
270
214
            return
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'
276
219
        if sources:
277
220
            query['sources'] = sources.encode("utf-8")
278
221
        query = urlencode(query)
302
245
                results = json.loads(msg.response_body.data)
303
246
            except ValueError:
304
247
                print "Error: got garbage json from the server"
305
 
        if isinstance(results, dict) and 'results' in results:
 
248
        if isinstance(results, dict):
306
249
            results = results.get('results', [])
307
250
        return results
308
251
 
321
264
        # Then close the Dash
322
265
        return Unity.ActivationResponse(goto_uri='', handled=2 )
323
266
 
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
 
 
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 
448
287
 
449
288
def main():
450
289
    """Connect to the session bus, exit if there is a running instance."""
451
 
    try:
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)),
459
 
                                        0, -1, None)
460
 
 
461
 
        # Unpack variant response with signature "(u)". 1 means we got it.
462
 
        result = result.unpack()[0]
463
 
 
464
 
        if result != 1:
465
 
            print >> sys.stderr, "Failed to own name %s. Bailing out." % BUS_NAME
466
 
            raise SystemExit(1)
467
 
    except:
 
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)),
 
297
                                    0, -1, None)
 
298
 
 
299
    # Unpack variant response with signature "(u)". 1 means we got it.
 
300
    result = result.unpack()[0]
 
301
 
 
302
    if result != 1:
 
303
        print >> sys.stderr, "Failed to own name %s. Bailing out." % BUS_NAME
468
304
        raise SystemExit(1)
469
305
 
470
306
    Daemon()