Improved movie poster fetching
[butaca:maevies.git] / src / Maevies / gui.py
1 # -*- coding: utf-8 -*-
2
3 ###########################################################################
4 #    Maevies
5 #    Copyright (C) 2010 Simón Pena <spenap@gmail.com>
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU General Public License as published by
9 #    the Free Software Foundation, either version 3 of the License, or
10 #    (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU General Public License for more details.
16 #
17 #    You should have received a copy of the GNU General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 ###########################################################################
20
21 import pygtk
22 import os
23 pygtk.require('2.0')
24 import gtk
25 import hildon
26 import pango
27 import gobject
28 from webkit import WebView
29
30 from lib import constants
31 from lib.asyncworker import AsyncWorker, AsyncItem
32 from lib.util import image_downloader
33 from moviemanager import MovieManager, TmdbMovie, TmdbMovieImage, \
34     WatcMovie
35
36 moviemanager = MovieManager()
37
38 class Maevies(hildon.StackableWindow):
39
40     ACTION_SEARCH = 0
41     ACTION_ABOUT = 1
42     ACTION_THEATERS = 2
43     ACTION_FAVORITES = 3
44
45     def __init__(self):
46         super(Maevies, self).__init__()
47         self.set_title('Maevies - 0.1')
48         self.connect('delete-event',
49                      lambda widget, event: gtk.main_quit())
50
51         self.add(self._create_contents())
52         self.set_app_menu(self._create_app_menu())
53
54         self.show_all()
55
56     def _create_button(self, title, subtitle, action):
57         box = gtk.VBox()
58         box.set_border_width(20)
59
60         button = hildon.Button(gtk.HILDON_SIZE_THUMB_HEIGHT,
61                                hildon.BUTTON_ARRANGEMENT_VERTICAL,
62                                title, subtitle)
63         button.connect('clicked', self._button_clicked, action)
64
65         box.pack_start(button, expand=True, fill=False)
66
67         return box
68
69     def _create_contents(self):
70         contents = gtk.HBox()
71         contents.set_border_width(60)
72         contents.set_homogeneous(True)
73         contents.pack_start(self._create_button('On Theaters',
74                                                 'Movies playing',
75                                                 self.ACTION_THEATERS),
76                             expand=True, fill=True)
77         contents.pack_start(self._create_button('Favorites',
78                                                 'Your saved searches',
79                                                 self.ACTION_FAVORITES),
80                             expand=True, fill=True)
81         contents.pack_start(self._create_button('Search',
82                                                 'Enter a new search',
83                                                 self.ACTION_SEARCH),
84                             expand=True, fill=True)
85
86         return contents;
87
88     def _button_clicked(self, button, action):
89         if action == self.ACTION_THEATERS:
90             theaters_view = TheatersWindow()
91             theaters_view.display_shows()
92         elif action == self.ACTION_SEARCH:
93             search_dialog = SearchDialog(self)
94             if search_dialog.run() == gtk.RESPONSE_ACCEPT:
95                 results_window = ResultsWindow()
96                 results_window.start_search(search_dialog.get_search_term(),
97                                             search_dialog.get_search_category())
98             search_dialog.destroy()
99         elif action == self.ACTION_ABOUT:
100             about_dialog = AboutDialog(self)
101             about_dialog.run()
102             about_dialog.destroy()
103
104     def _create_app_menu(self):
105         menu = hildon.AppMenu()
106
107         about = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
108         about.set_label('About')
109         about.connect('clicked', self._button_clicked, self.ACTION_ABOUT)
110
111         menu.append(about)
112
113         menu.show_all()
114
115         return menu
116
117     def run(self):
118         gtk.main()
119
120 class SearchDialog(gtk.Dialog):
121
122     TMDB_SEARCH = 0
123     WATC_SEARCH = 1
124     search_fields = {TMDB_SEARCH:'TMDb',
125                       WATC_SEARCH:'WATC'}
126
127     def __init__(self, parent):
128         super(SearchDialog, self).__init__(parent=parent,
129                                            flags=gtk.DIALOG_DESTROY_WITH_PARENT)
130         self.set_title('Enter search terms')
131
132         self.vbox.pack_start(self._create_contents(), True, False, 0)
133         self.add_button(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
134
135         self.show_all()
136
137     def _create_contents(self):
138         self._search_entry = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
139         search_button = self._create_picker_button()
140
141         search_contents = gtk.VBox()
142
143         search_contents.pack_start(self._search_entry,
144                                    expand=True, fill=True)
145         search_contents.pack_start(search_button,
146                                    expand=True, fill=True)
147
148         return search_contents
149
150     def _create_picker_button(self):
151         self._picker_button = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT,
152                                                   hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
153         self._picker_button.set_title('Search on')
154
155         selector = hildon.TouchSelector(text=True)
156         selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
157
158         for search_method in [self.TMDB_SEARCH, self.WATC_SEARCH]:
159             selector.append_text(self.search_fields[search_method])
160
161         self._picker_button.set_selector(selector)
162         self._picker_button.set_active(0)
163
164         return self._picker_button
165
166     def get_search_term(self):
167         return self._search_entry.get_text()
168
169     def get_search_category(self):
170         return self._picker_button.get_active()
171
172 class ResultsWindow(hildon.StackableWindow):
173
174     def __init__(self):
175         super(ResultsWindow, self).__init__()
176         self.set_title('Search results')
177
178         self.add(self._create_contents())
179
180         moviemanager.response_received_cb = self._response_received_cb
181         self.show_all()
182
183     def _create_contents(self):
184         content_area = hildon.PannableArea()
185         self._movies_view = MoviesView()
186         self._movies_view.connect('row-activated', self._row_activated_cb)
187
188         content_area.add(self._movies_view)
189         return content_area
190
191     def _row_activated_cb(self, view, path, column):
192         movie = view.get_movie_from_path(path)
193         if isinstance(movie, TmdbMovie):
194             MovieWindow(movie)
195         elif isinstance(movie, WatcMovie):
196             WatcWindow(movie)
197
198     def start_search(self, search_term, search_category):
199         self._show_banner(search_term, search_category)
200         hildon.hildon_gtk_window_set_progress_indicator(self, True)
201         moviemanager.query(search_term, search_category)
202
203     def _response_received_cb(self, movies):
204         self._movies_view.add_movies(movies)
205         hildon.hildon_gtk_window_set_progress_indicator(self, False)
206
207     def _show_banner(self, search_term, search_category):
208         message = ('Searching on <i>%(category)s</i> for <i>%(term)s</i>' %
209                    {'category': SearchDialog.search_fields[search_category],
210                     'term' : search_term})
211         banner = hildon.hildon_banner_show_information_with_markup(self,
212                                                                    'ignored',
213                                                                    message)
214         banner.set_timeout(constants.TIMEOUT_TIME_MILLIS)
215
216 class MoviesView(gtk.TreeView):
217
218     def __init__(self):
219         super(MoviesView, self).__init__()
220         model = MoviesListStore()
221         self.set_model(model)
222
223         movie_image_renderer = gtk.CellRendererPixbuf()
224         column = gtk.TreeViewColumn('Image', movie_image_renderer,
225                                     pixbuf=model.IMAGE_COLUMN)
226         self.append_column(column)
227
228         movie_text_renderer = gtk.CellRendererText()
229         movie_text_renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
230         column = gtk.TreeViewColumn('Name', movie_text_renderer,
231                                     markup=model.INFO_COLUMN)
232         self.append_column(column)
233
234         self.show_all()
235
236     def add_movies(self, movie_list):
237         model = self.get_model()
238         if model:
239             model.add(movie_list)
240
241     def get_movie_from_path(self, path):
242         model = self.get_model()
243         return model[path][model.MOVIE_COLUMN]
244
245 class MoviesListStore(gtk.ListStore):
246
247     IMAGE_COLUMN = 0
248     INFO_COLUMN = 1
249     MOVIE_COLUMN = 2
250
251     def __init__(self):
252         super(MoviesListStore, self).__init__(gtk.gdk.Pixbuf,
253                                               str,
254                                               gobject.TYPE_PYOBJECT)
255
256     def add(self, movies_found):
257         self.clear()
258         for movie in movies_found:
259             row = {self.IMAGE_COLUMN: movie.get_placeholder_image(),
260                    self.INFO_COLUMN: movie.get_info(),
261                    self.MOVIE_COLUMN: movie,
262                   }
263             self.append(row.values())
264
265 class AboutDialog(gtk.Dialog):
266
267     def __init__(self, parent):
268         super(AboutDialog, self).__init__(parent=parent,
269                                           flags=gtk.DIALOG_DESTROY_WITH_PARENT)
270         self.set_title('About Maevies')
271
272         self.show_all()
273
274 class MovieWindow(hildon.StackableWindow):
275
276     def _fetch_movie_image(self, movie):
277         image = gtk.Image()
278         image.set_from_pixbuf(gtk.IconTheme().load_icon('general_video',
279                                                         256, 0))
280         movie_image = movie.get_image('poster', 'mid')
281         if isinstance(movie_image, TmdbMovieImage):
282             image_file = os.path.abspath(os.path.join(constants.MVS_CACHE, movie_image.get_id() + '.jpg'))
283             if os.path.isfile(image_file):
284                 image.set_from_pixbuf(gtk.gdk.pixbuf_new_from_file_at_size(image_file,
285                                                                        256,
286                                                                        256))
287             else:
288                 banner = hildon.hildon_banner_show_information_with_markup(self,
289                                                                            'ignored',
290                                                                            'Fetching movie poster')
291                 banner.set_timeout(constants.TIMEOUT_TIME_MILLIS)
292                 hildon.hildon_gtk_window_set_progress_indicator(self, True)
293
294                 async_item = AsyncItem(image_downloader, (movie_image.get_url(),
295                                                           os.path.join(constants.MVS_CACHE,
296                                                                        movie_image.get_id())),
297                                        self._set_fetched_image, (image,))
298                 self.async_worker.queue.put(async_item)
299                 self.async_worker.start()
300
301         return image
302
303     def _set_fetched_image(self, image, target, error):
304         if not error:
305             image_file = os.path.abspath(target)
306             image.set_from_pixbuf(gtk.gdk.pixbuf_new_from_file_at_size(image_file,
307                                                                        256,
308                                                                        256))
309         hildon.hildon_gtk_window_set_progress_indicator(self, False)
310
311     def _create_contents(self, movie):
312         main_area = hildon.PannableArea()
313
314         main_box = gtk.VBox(False, 20)
315         main_box.set_border_width(20)
316         upper_content = gtk.HBox(False, 40)
317         upper_content.set_border_width(20)
318
319         image = self._fetch_movie_image(movie)
320
321         side_content = gtk.VBox(False, 30)
322
323         for key in movie.fields:
324             label = gtk.Label()
325             label.set_markup('<b>%(field)s:</b> <small>%(value)s</small>' %
326                              {'field' : key,
327                               'value' : movie.get_value(key)})
328             label.set_alignment(constants.LEFT_ALIGNMENT,
329                                 constants.CENTER_ALIGNMENT)
330             side_content.pack_start(label, False, False)
331
332         upper_content.pack_start(image, False, False)
333         upper_content.pack_start(side_content, False, False)
334
335         movie_overview = hildon.TextView()
336         movie_overview.set_placeholder('Overview')
337         movie_overview.set_wrap_mode(gtk.WRAP_WORD)
338         movie_overview.get_buffer().set_text(movie.get_overview())
339
340         label = gtk.Label()
341         label.set_markup('<b>Overview:</b>')
342         label.set_alignment(constants.LEFT_ALIGNMENT,
343                             constants.CENTER_ALIGNMENT)
344
345         main_box.pack_start(upper_content, False, False)
346         main_box.pack_start(label, False, False)
347         main_box.pack_start(movie_overview, False, False)
348
349         main_area.add_with_viewport(main_box)
350         main_area.set_size_request_policy(hildon.SIZE_REQUEST_CHILDREN)
351
352         return main_area
353
354     def __init__(self, movie):
355         super(MovieWindow, self).__init__()
356         self.async_worker = AsyncWorker()
357         self.set_title('Movie info')
358         self.add(self._create_contents(movie))
359         self.show_all()
360
361 class WebkitWindow(hildon.StackableWindow):
362
363     def __init__(self):
364         super(WebkitWindow, self).__init__()
365
366         self.view = WebView()
367         self.view.connect('load-started', self._load_start)
368         self.view.connect('load-finished', self._load_finished)
369         self.view.set_full_content_zoom(True)
370
371         wbk_settings = self.view.get_settings()
372         wbk_settings.set_property('auto-shrink-images', True)
373
374         self.pannable = hildon.PannableArea()
375         self.pannable.add(self.view)
376         self.pannable.set_size_request_policy(hildon.SIZE_REQUEST_CHILDREN)
377         self.pannable.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
378
379         self.add(self.pannable)
380         self.show_all()
381
382     def _load_start(self, view, data):
383         hildon.hildon_gtk_window_set_progress_indicator(self, True)
384
385     def _load_finished(self, view, data):
386         hildon.hildon_gtk_window_set_progress_indicator(self, False)
387
388     def load_url(self, url):
389         self.view.open(url)
390
391 class TheatersWindow(WebkitWindow):
392
393     def __init__(self):
394         super(TheatersWindow, self).__init__()
395         self.show_all()
396
397     def display_shows(self):
398         self.load_url('http://www.google.com/movies')
399
400 class WatcWindow(WebkitWindow):
401
402     def __init__(self, movie):
403         super(WatcWindow, self).__init__()
404         self.show_all()
405         has_stingers = movie.get_stingers()
406         if has_stingers == 0:
407             stingers = '?'
408         elif has_stingers == 1:
409             stingers = '*'
410         else:
411             stingers = ''
412         url = ('http://whatsafterthecredits.com/index.php?title=%(title)s_(%(year)s)%(stingers)s' %
413                       {'title':movie.get_title().strip(),
414                        'year':movie.get_year().strip(),
415                        'stingers':stingers})
416         print url
417         self.load_url(url)
418
419 if __name__ == '__main__':
420     maevies = Maevies()
421     maevies.run()