Merge lp:~jhodapp/unity-lens-video/remote-videos-scope-trunk into lp:unity-lens-video/remote-videos-scope-trunk
- remote-videos-scope-trunk
- Merge into remote-videos
Status: | Superseded |
---|---|
Proposed branch: | lp:~jhodapp/unity-lens-video/remote-videos-scope-trunk |
Merge into: | lp:unity-lens-video/remote-videos-scope-trunk |
Diff against target: |
169 lines (+83/-12) 1 file modified
src/unity-scope-video-remote (+83/-12) |
To merge this branch: | bzr merge lp:~jhodapp/unity-lens-video/remote-videos-scope-trunk |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paweł Stołowski (community) | Needs Fixing | ||
Review via email: mp+121458@code.launchpad.net |
This proposal has been superseded by a proposal from 2012-08-29.
Commit message
Description of the change
Added details to the Amazon video preview.
Jim Hodapp (jhodapp) wrote : | # |
- 59. By Jim Hodapp
-
Merged Saviq's changes.
- 60. By Jim Hodapp
-
Renable the main videosearch API server URL instead of the staging one.
- 61. By Jim Hodapp
-
Remove staging server URL.
Jim Hodapp (jhodapp) wrote : | # |
Change SERVER to videosearch.
Paweł Stołowski (stolowski) wrote : | # |
Looking good, just minor comments:
46 # Clear results details map
47 - self.details_map = {}
The comment above is not needed anymore.
128 + duration = int(details[
129 + # TODO: Extend VideoPreview class and set property for showing video length
Extending MoviePreview shouldn't be needed; looking at the mockups, video length should be displayed next to year as preview subtitle; in fact year property is not really used by Unity, so you should populate subtitle with both year and duration; don't forget about units, e.g. "2012, 88 mins".
Paweł Stołowski (stolowski) : | # |
- 62. By Jim Hodapp
-
Display video release year and length in minutes as the subtitle.
Paweł Stołowski (stolowski) wrote : | # |
109 + subtitle = release_
This will crash if release_date or duration are unset (i.e. not in details). Also str(duration) + " mins" should be i18n friendly.
- 63. By Jim Hodapp
-
Initialize release_date and duration, and internationalize 'mins'.
- 64. By Jim Hodapp
-
More flexible duration internationaliz
ation. Also allow year or duration to be shown independently of each other being present. - 65. By Jim Hodapp
-
Hide the rating widget by default since the JSON API doesn't yet support rating data in details.
- 66. By Jim Hodapp
-
Make the fetch of details data more robust. Signify when the remote server could not be connected to.
- 67. By Jim Hodapp
-
No longer need the variable 'uri' in on_preview_uri().
Unmerged revisions
Preview Diff
1 | === modified file 'src/unity-scope-video-remote' | |||
2 | --- src/unity-scope-video-remote 2012-08-14 12:50:14 +0000 | |||
3 | +++ src/unity-scope-video-remote 2012-08-29 13:04:18 +0000 | |||
4 | @@ -22,6 +22,7 @@ | |||
5 | 22 | import sys | 22 | import sys |
6 | 23 | from urllib import urlencode | 23 | from urllib import urlencode |
7 | 24 | import time | 24 | import time |
8 | 25 | from datetime import datetime | ||
9 | 25 | import gettext | 26 | import gettext |
10 | 26 | 27 | ||
11 | 27 | #pylint: disable=E0611 | 28 | #pylint: disable=E0611 |
12 | @@ -49,6 +50,7 @@ | |||
13 | 49 | gettext.bindtextdomain(APP_NAME, LOCAL_PATH) | 50 | gettext.bindtextdomain(APP_NAME, LOCAL_PATH) |
14 | 50 | gettext.textdomain(APP_NAME) | 51 | gettext.textdomain(APP_NAME) |
15 | 51 | _ = gettext.gettext | 52 | _ = gettext.gettext |
16 | 53 | n_ = gettext.ngettext | ||
17 | 52 | 54 | ||
18 | 53 | BUS_NAME = "net.launchpad.scope.RemoteVideos" | 55 | BUS_NAME = "net.launchpad.scope.RemoteVideos" |
19 | 54 | SERVER = "http://videosearch.ubuntu.com/v0" | 56 | SERVER = "http://videosearch.ubuntu.com/v0" |
20 | @@ -89,14 +91,14 @@ | |||
21 | 89 | self.query_list_of_sources() | 91 | self.query_list_of_sources() |
22 | 90 | self.update_recommendations() | 92 | self.update_recommendations() |
23 | 91 | self.scope.export() | 93 | self.scope.export() |
24 | 92 | self.details_map = {} #holds details not passed via lens-meta://, needed by preview | ||
25 | 93 | 94 | ||
26 | 94 | def update_recommendations(self): | 95 | def update_recommendations(self): |
27 | 95 | """Query the server for 'recommendations'. | 96 | """Query the server for 'recommendations'. |
28 | 96 | 97 | ||
30 | 97 | In v0, that means simply do an empty search. | 98 | In v0 extended, that means simply do an empty search with Amazon |
31 | 99 | as the source since it's the only paid provider at the moment. | ||
32 | 98 | """ | 100 | """ |
34 | 99 | msg = Soup.Message.new("GET", SERVER + "/search") | 101 | msg = Soup.Message.new("GET", SERVER + "/search?q=&sources=Amazon") |
35 | 100 | self.session.queue_message(msg, self._recs_cb, None) | 102 | self.session.queue_message(msg, self._recs_cb, None) |
36 | 101 | 103 | ||
37 | 102 | def _recs_cb(self, session, msg, user_data): | 104 | def _recs_cb(self, session, msg, user_data): |
38 | @@ -175,11 +177,10 @@ | |||
39 | 175 | the server, update the model and notify the lens when we are done.""" | 177 | the server, update the model and notify the lens when we are done.""" |
40 | 176 | search_string = search.props.search_string.strip() | 178 | search_string = search.props.search_string.strip() |
41 | 177 | # note search_string is a utf-8 encoded string, now: | 179 | # note search_string is a utf-8 encoded string, now: |
43 | 178 | print "Search changed to %r" % search_string | 180 | print "Remote scope search changed to %r" % search_string |
44 | 179 | model = search.props.results_model | 181 | model = search.props.results_model |
45 | 180 | model.clear() | 182 | model.clear() |
46 | 181 | # Clear results details map | 183 | # Clear results details map |
47 | 182 | self.details_map = {} | ||
48 | 183 | # Create a list of activated sources | 184 | # Create a list of activated sources |
49 | 184 | sources = [source for source in self.sources_list | 185 | sources = [source for source in self.sources_list |
50 | 185 | if self.source_activated(source)] | 186 | if self.source_activated(source)] |
51 | @@ -207,13 +208,17 @@ | |||
52 | 207 | title = i['title'].encode('utf-8') | 208 | title = i['title'].encode('utf-8') |
53 | 208 | icon = i['img'].encode('utf-8') | 209 | icon = i['img'].encode('utf-8') |
54 | 209 | comment = i['source'].encode('utf-8') | 210 | comment = i['source'].encode('utf-8') |
56 | 210 | self.details_map[uri] = (comment) #TODO: more to come | 211 | details_url = "" |
57 | 212 | if "details" in i: | ||
58 | 213 | details_url = i['details'].encode('utf-8') | ||
59 | 214 | |||
60 | 211 | if uri.startswith('http'): | 215 | if uri.startswith('http'): |
61 | 212 | # Instead of an uri, we pass a string of uri + metadata | 216 | # Instead of an uri, we pass a string of uri + metadata |
63 | 213 | model.append(uri+'lens-meta://'+title+'lens-meta://'+icon, | 217 | model.append(uri+'lens-meta://'+title+'lens-meta://'+icon+'lens-meta://'+details_url, |
64 | 214 | icon, 2, "text/html", str(title), str(comment), str(uri)) | 218 | icon, 2, "text/html", str(title), str(comment), str(uri)) |
65 | 215 | except (KeyError, TypeError, AttributeError): | 219 | except (KeyError, TypeError, AttributeError): |
66 | 216 | print "Malformed result, skipping." | 220 | print "Malformed result, skipping." |
67 | 221 | |||
68 | 217 | model.flush_revision_queue() | 222 | model.flush_revision_queue() |
69 | 218 | if search: | 223 | if search: |
70 | 219 | search.finished() | 224 | search.finished() |
71 | @@ -279,27 +284,93 @@ | |||
72 | 279 | 284 | ||
73 | 280 | def on_preview_uri (self, scope, rawuri): | 285 | def on_preview_uri (self, scope, rawuri): |
74 | 281 | """Preview request handler""" | 286 | """Preview request handler""" |
75 | 287 | details_url = rawuri.split('lens-meta://')[3] | ||
76 | 288 | details = self.get_details(details_url) | ||
77 | 289 | |||
78 | 282 | uri = rawuri.split('lens-meta://')[0] | 290 | uri = rawuri.split('lens-meta://')[0] |
79 | 283 | title = rawuri.split('lens-meta://')[1] | 291 | title = rawuri.split('lens-meta://')[1] |
80 | 284 | thumbnail_uri = rawuri.split('lens-meta://')[2] | 292 | thumbnail_uri = rawuri.split('lens-meta://')[2] |
81 | 285 | subtitle = '' | 293 | subtitle = '' |
82 | 286 | desc = '' | 294 | desc = '' |
85 | 287 | if self.details_map.has_key (uri): | 295 | source = '' |
86 | 288 | desc = self.details_map[uri][0] | 296 | if isinstance(details, dict): |
87 | 297 | uri = details['browser_url'] | ||
88 | 298 | title = details['title'] | ||
89 | 299 | thumbnail_uri = details['image'] | ||
90 | 300 | desc = details['description'] | ||
91 | 301 | source = details['source'] | ||
92 | 302 | |||
93 | 289 | try: | 303 | try: |
94 | 290 | thumbnail_file = Gio.File.new_for_uri (thumbnail_uri) | 304 | thumbnail_file = Gio.File.new_for_uri (thumbnail_uri) |
95 | 291 | thumbnail_icon = Gio.FileIcon.new(thumbnail_file) | 305 | thumbnail_icon = Gio.FileIcon.new(thumbnail_file) |
96 | 292 | except: | 306 | except: |
97 | 293 | thumbnail_icon = None | 307 | thumbnail_icon = None |
98 | 308 | |||
99 | 309 | if "release_date" in details: | ||
100 | 310 | try: | ||
101 | 311 | # v1 spec states release_date will be YYYY-MM-DD | ||
102 | 312 | release_date = datetime.strptime(details["release_date"], "%Y-%m-%d") | ||
103 | 313 | except ValueError: | ||
104 | 314 | print "Failed to parse release_date since it was not in format: YYYY-MM-DD" | ||
105 | 315 | if "duration" in details: | ||
106 | 316 | # Given in seconds, convert to minutes | ||
107 | 317 | duration = int(details["duration"]) / 60 | ||
108 | 318 | |||
109 | 319 | subtitle = release_date.strftime("%Y") + ", " + str(duration) + " mins" | ||
110 | 320 | |||
111 | 294 | preview = Unity.MoviePreview.new(title, subtitle, desc, thumbnail_icon) | 321 | preview = Unity.MoviePreview.new(title, subtitle, desc, thumbnail_icon) |
112 | 295 | play_video = Unity.PreviewAction.new("play", _("Play"), None) | 322 | play_video = Unity.PreviewAction.new("play", _("Play"), None) |
113 | 296 | play_video.connect('activated', self.play_video) | 323 | play_video.connect('activated', self.play_video) |
114 | 297 | preview.add_action(play_video) | 324 | preview.add_action(play_video) |
118 | 298 | #TODO: insert proper uploader and upload date when available in videosearch metadata | 325 | |
119 | 299 | preview.add_info(Unity.InfoHint.new("uploaded-by", _("Uploaded by"), None, "")); | 326 | # TODO: For details of future source types, factor out common detail key/value pairs |
120 | 300 | preview.add_info(Unity.InfoHint.new("uploaded-on", _("Uploaded on"), None, "")); | 327 | if source == "Amazon": |
121 | 328 | if "directors" in details: | ||
122 | 329 | directors = details["directors"] | ||
123 | 330 | preview.add_info(Unity.InfoHint.new("directors", | ||
124 | 331 | n_("Director", "Directors", len(directors)), None, ', '.join(directors))) | ||
125 | 332 | if "starring" in details: | ||
126 | 333 | cast = details["starring"] | ||
127 | 334 | if cast: | ||
128 | 335 | preview.add_info(Unity.InfoHint.new("cast", | ||
129 | 336 | n_("Cast", "Cast", len(cast)), None, ', '.join(cast))) | ||
130 | 337 | if "genres" in details: | ||
131 | 338 | genres = details["genres"] | ||
132 | 339 | if genres: | ||
133 | 340 | preview.add_info(Unity.InfoHint.new("genres", | ||
134 | 341 | n_("Genre", "Genres", len(genres)), None, ', '.join(genres))) | ||
135 | 342 | # TODO: Add Vimeo & YouTube details for v1 of JSON API | ||
136 | 343 | else: | ||
137 | 344 | if "uploaded_by" in details: | ||
138 | 345 | preview.add_info(Unity.InfoHint.new("uploaded-by", _("Uploaded by"), | ||
139 | 346 | None, details["uploaded_by"])) | ||
140 | 347 | if "date_uploaded" in details: | ||
141 | 348 | preview.add_info(Unity.InfoHint.new("uploaded-on", _("Uploaded on"), | ||
142 | 349 | None, details["date_uploaded"])) | ||
143 | 350 | |||
144 | 301 | return preview | 351 | return preview |
145 | 302 | 352 | ||
146 | 353 | def get_details(self, details_url): | ||
147 | 354 | """Get online video details for preview""" | ||
148 | 355 | details = [] | ||
149 | 356 | if len(details_url) == 0: | ||
150 | 357 | print "Cannot get preview details, no details_url specified." | ||
151 | 358 | return details | ||
152 | 359 | |||
153 | 360 | if self._pending is not None: | ||
154 | 361 | self.session.cancel_message(self._pending, | ||
155 | 362 | Soup.KnownStatusCode.CANCELLED) | ||
156 | 363 | |||
157 | 364 | self._pending = Soup.Message.new("GET", details_url) | ||
158 | 365 | # Send a synchronous GET request for video metadata details | ||
159 | 366 | self.session.send_message(self._pending) | ||
160 | 367 | try: | ||
161 | 368 | details = json.loads(self._pending.response_body.data) | ||
162 | 369 | except ValueError: | ||
163 | 370 | print "Error: got garbage json from the server" | ||
164 | 371 | |||
165 | 372 | return details | ||
166 | 373 | |||
167 | 303 | def play_video (self, action, uri): | 374 | def play_video (self, action, uri): |
168 | 304 | """Preview request - Play action handler""" | 375 | """Preview request - Play action handler""" |
169 | 305 | return self.on_activate_uri (action, uri) | 376 | return self.on_activate_uri (action, uri) |
One caveat: the SERVER is currently set to the staging URL. This will need to be changed when the JSON API v0 extended is pushed into production.