Poster downloading update
[cinaest] / src / poster / movie-poster-factory.vala
1 /* This file is part of Cinaest.
2  *
3  * Copyright (C) 2009 Philipp Zabel
4  *
5  * Cinaest is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * Cinaest is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with Cinaest. If not, see <http://www.gnu.org/licenses/>.
17  */
18
19 using GLib;
20
21 namespace MoviePoster {
22
23         public delegate void RequestCallback (Gdk.Pixbuf movieposter, Movie movie);
24
25         public class Factory : Object {
26                 private static Factory the_factory = null;
27                 internal List<Request> requests;
28                 internal dynamic DBus.Object server;
29                 internal bool download_posters;
30                 private GConf.Client gc;
31                 private uint cxnid;
32                 private Gdk.Pixbuf poster_failed;
33                 private Gdk.Pixbuf icon_failed;
34
35                 construct {
36                         try {
37                                 var conn = DBus.Bus.get (DBus.BusType.SESSION);
38                                 server = conn.get_object ("org.maemo.movieposter.IMDb",
39                                                           "/org/maemo/movieposter/IMDb",
40                                                           "org.maemo.movieposter.Provider");
41                                 server.Fetched.connect (this.on_poster_fetched);
42                                 server.Failed.connect (this.on_poster_failed);
43                         } catch (Error e) {
44                                 warning ("Couldn't connect to IMDb poster downloader: %s\n", e.message);
45                         }
46                         gc = GConf.Client.get_default ();
47
48                         try {
49                                 download_posters = gc.get_bool ("/apps/cinaest/download_posters");
50                                 gc.add_dir ("/apps/cinaest", GConf.ClientPreloadType.ONELEVEL);
51                                 cxnid = gc.notify_add ("/apps/cinaest/download_posters", on_download_posters_changed);
52                         } catch (Error e) {
53                                 stdout.printf ("Error installing GConf notification: %s\n", e.message);
54                         }
55                 }
56
57                 private static void on_download_posters_changed (GConf.Client gc, uint cxnid, GConf.Entry entry) {
58                         the_factory.download_posters = entry.get_value ().get_bool ();
59                 }
60
61                 public static Factory get_instance () {
62                         if (the_factory == null)
63                                 the_factory = new MoviePoster.Factory ();
64                         return the_factory;
65                 }
66
67                 public int queue (Movie movie, int width, int height, bool cropped, RequestCallback callback) throws Error {
68                         string path = get_poster_path (movie);
69
70                         foreach (Request request in requests) {
71                                 if (request.movie == movie &&
72                                     request.width >= width &&
73                                     request.height >= height)
74                                         return 0;
75                         }
76
77                         // TODO: make this async / put out of process
78                         Gdk.Pixbuf pixbuf;
79                         if (width <= 256 && height <= 256)
80                                 pixbuf = request_thumbnail (path, width, height, cropped);
81                         else
82                                 pixbuf = pixbuf_from_path (path, width, height, cropped);
83                         if (pixbuf != null) {
84                                 callback (pixbuf, movie);
85                                 return 0;
86                         }
87
88                         if (server != null && download_posters) {
89                                 var request = new Request ();
90
91                                 request.handle = server.Fetch (movie.title, movie.year.to_string (), "movie", width, height);
92                                 request.movie = movie;
93                                 request.callback = callback;
94                                 request.width = width;
95                                 request.height = height;
96                                 request.cropped = cropped;
97                                 requests.append (request);
98                         }
99                         return 0;
100                 }
101
102                 private Gdk.Pixbuf? request_thumbnail (string path, int width, int height, bool cropped) {
103                         string thumbnail_path = get_thumbnail_path (path, width, height);
104
105                         var pixbuf = pixbuf_from_path (thumbnail_path, width, height, cropped);
106                         if (pixbuf == null) {
107                                 pixbuf = pixbuf_from_path (path, width, height, cropped);
108                                 if (pixbuf != null)
109                                         pixbuf.save (thumbnail_path, "jpeg", null);
110                         }
111
112                         return pixbuf;
113                 }
114
115                 private Gdk.Pixbuf? pixbuf_from_path (string path, int width, int height, bool cropped) {
116                         if (FileUtils.test (path, FileTest.IS_REGULAR)) {
117                                 var pixbuf = new Gdk.Pixbuf.from_file (path);
118
119                                 if (pixbuf.width == width && pixbuf.height == height)
120                                         return pixbuf;
121
122                                 if (!cropped && (pixbuf.width == width || pixbuf.height == height))
123                                         return pixbuf;
124
125                                 if (!download_posters ||
126                                            (pixbuf.width >= width && pixbuf.height >= height) ||
127                                            (pixbuf.width >= Poster.SMALL_WIDTH && pixbuf.height >= Poster.SMALL_HEIGHT)) {
128                                         // Scale to fit
129                                         return scale_to_fit (pixbuf, width, height, cropped);
130
131                                 }
132                         }
133
134                         return null;
135                 }
136
137                 private Gdk.Pixbuf scale_to_fit (Gdk.Pixbuf source, int width, int height, bool cropped) {
138                         Gdk.InterpType interp_type;
139
140                         if (width > source.width && height > source.height)
141                                 interp_type = Gdk.InterpType.HYPER;
142                         else
143                                 interp_type = Gdk.InterpType.BILINEAR;
144
145                         if (cropped) {
146                                 double xscale = (double) width / (double) source.width;
147                                 double yscale = (double) height / (double) source.height;
148                                 double scale;
149                                 int ofs_x = 0;
150                                 int ofs_y = 0;
151                                 if (xscale > yscale) {
152                                         scale = xscale;
153                                         ofs_y = (int) (height - source.height * scale) / 2;
154                                 } else {
155                                         scale = yscale;
156                                         ofs_x = (int) (width - source.width * scale) / 2;
157                                 }
158
159                                 var dest = new Gdk.Pixbuf (Gdk.Colorspace.RGB, true, 8, width, height);
160                                 source.scale (dest, 0, 0, width, height, ofs_x, ofs_y, scale, scale, interp_type);
161
162                                 return dest;
163                         } else {
164                                 width = int.min (width,
165                                                  (source.width * height + source.height / 2) / source.height);
166                                 height = int.min ((source.height * width + source.width / 2) / source.width,
167                                                   height);
168
169                                 return source.scale_simple (width, height, interp_type);
170                         }
171                 }
172
173                 private void on_poster_fetched (dynamic DBus.Object server, int handle, string path) {
174                         Request request = null;
175                         foreach (Request r in requests) {
176                                 if (r.handle == handle) {
177                                         request = r;
178                                         break;
179                                 }
180                         }
181                         if (request == null)
182                                 return;
183
184                         var pixbuf = request_thumbnail (path, request.width, request.height, request.cropped);
185
186                         requests.remove (request);
187                         request.callback (pixbuf, request.movie);
188                 }
189
190                 private void on_poster_failed (dynamic DBus.Object server, int handle) {
191                         Request request = null;
192                         foreach (Request r in requests) {
193                                 if (r.handle == handle) {
194                                         request = r;
195                                         break;
196                                 }
197                         }
198                         if (request == null)
199                                 return;
200                         requests.remove (request);
201                         request.callback (failed_poster (request.width, request.height), request.movie);
202                 }
203
204                 private Gdk.Pixbuf failed_poster (int width, int height) {
205                         if (width == Poster.ICON_WIDTH && height == Poster.ICON_HEIGHT) {
206                                 if (icon_failed == null) {
207                                         // An empty icon for the list view
208                                         icon_failed = new Gdk.Pixbuf (Gdk.Colorspace.RGB, true, 8, Poster.ICON_WIDTH, Poster.ICON_HEIGHT);
209                                         icon_failed.fill (0);
210                                 }
211                                 return icon_failed;
212                         }
213                         if (width == Poster.SMALL_WIDTH && height == Poster.SMALL_HEIGHT) {
214                                 if (poster_failed == null) try {
215                                         // Broken image icon for the poster view
216                                         var no_pic = new Gdk.Pixbuf.from_file ("/usr/share/icons/hicolor/64x64/hildon/imageviewer_no_pic.png");
217                                         poster_failed = new Gdk.Pixbuf (Gdk.Colorspace.RGB, true, 8, Poster.SMALL_WIDTH, Poster.SMALL_HEIGHT);
218                                         poster_failed.fill (0);
219                                         no_pic.copy_area (0, 0, no_pic.width, no_pic.height, poster_failed,
220                                                           (Poster.SMALL_WIDTH - no_pic.width) / 2, (Poster.SMALL_HEIGHT - no_pic.height) / 2);
221                                 } catch (Error e) {
222                                         critical ("Missing imageviewer_no_pic icon: %s\n", e.message);
223                                 }
224                                 return poster_failed;
225                         }
226                         return null;
227                 }
228
229                 public void join () {
230                 }
231
232                 public static void factory_remove (Movie movie) {
233                 }
234
235                 public static void factory_clean_cache (int max_size, time_t min_mtime) {
236                 }
237
238                 public void clear_queue () {
239                         if (server != null) {
240                                 foreach (Request r in requests)
241                                         server.Unqueue (r.handle);
242                         }
243                         requests = null;
244                 }
245         }
246
247         public class Request {
248                 public int handle;
249                 public Movie movie;
250                 public RequestCallback callback;
251                 public int width;
252                 public int height;
253                 public bool cropped;
254
255                 public void unqueue () {
256                         if (Factory.get_instance ().server != null)
257                                 Factory.get_instance ().server.Unqueue (this.handle);
258
259                         Factory.get_instance ().requests.remove (this);
260                 }
261
262                 public void join () {
263                 }
264         }
265
266         public static bool is_cached (Movie movie) {
267                 string filename = get_poster_path (movie);
268                 if (FileUtils.test (filename, FileTest.IS_REGULAR))
269                         return true;
270                 else
271                         return false;
272         }
273
274         public static string get_poster_path (Movie movie) {
275         //      return Hildon.albumart_get_path (movie.title, movie.year.to_string (), "movie");
276                 return Path.build_filename (Environment.get_user_cache_dir (),
277                                             "media-art",
278                                             "movie-%s-%s.jpeg".printf (
279                        Checksum.compute_for_string (ChecksumType.MD5, movie.title.down ()),
280                        Checksum.compute_for_string (ChecksumType.MD5, movie.year.to_string ())));
281         }
282
283         public static string get_thumbnail_path (string poster_path, int width, int height) {
284                 bool large = width > 128 || height > 128;
285
286                 return Path.build_filename (Environment.get_home_dir (),
287                                             ".thumbnails",
288                                             large ? "large" : "normal",
289                                             "%s.jpeg".printf (
290                        Checksum.compute_for_string (ChecksumType.MD5, "file://" + poster_path)));
291         }
292 }
293