1 /* This file is part of Cinaest.
3 * Copyright (C) 2010 Philipp Zabel
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.
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.
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/>.
20 public const int MARGIN_DOUBLE = 16;
21 public const int MARGIN_HALF = 4;
25 public const int SMALL_WIDTH = (800 - 2*Hildon.MARGIN_DOUBLE - 4*Hildon.MARGIN_HALF)/5;
26 public const int SMALL_HEIGHT = (420 - Hildon.MARGIN_HALF)/2;
27 public const int ICON_WIDTH = 46;
28 public const int ICON_HEIGHT = 64;
31 // A single IMDb poster image search (parsing and retrieval of the poster image URI)
32 public class IMDbMessage : Soup.Message {
36 public string poster_uri = null;
37 private bool thumbnail;
39 public IMDbMessage (string title_, int year_, bool thumbnail_ = false) {
40 Object (method: "GET");
44 thumbnail = thumbnail_;
45 string url = "http://www.imdb.com/find?s=tt&q=%s (%d)".printf (convert (title, -1, "iso8859-1", "utf-8"), year);
46 uri = new Soup.URI (url);
50 // Encapsulation of a single poster download (IMDb title search query and image file download)
51 public class IMDbPosterDownload : Object {
52 private IMDbPosterDownloader downloader;
53 private Soup.Session session;
54 private string cache_dir;
55 private string cache_filename;
56 private bool thumbnail;
57 private bool cancelled = false;
61 public IMDbPosterDownload (string title, string year, bool thumbnail_, int _handle, IMDbPosterDownloader _downloader) {
63 downloader = _downloader;
64 session = downloader.session;
65 thumbnail = thumbnail_;
67 var message = new IMDbMessage (title, year.to_int (), thumbnail);
68 session.queue_message (message, title_page_callback);
72 cache_dir = Path.build_filename (Environment.get_tmp_dir(), "cinaest-thumbnails");
74 cache_dir = Path.build_filename (Environment.get_user_cache_dir(), "media-art");
76 cache_filename = "movie-" +
77 Checksum.compute_for_string (ChecksumType.MD5, (title).down ()) + "-" +
78 Checksum.compute_for_string (ChecksumType.MD5, (year).down ()) + ".jpeg";
81 private void title_page_callback (Soup.Session session, Soup.Message message) {
83 message.status_code != Soup.KnownStatusCode.OK) {
84 print ("[%d] NO POSTER FOR %s (CODE %u)\n", handle, message.uri.to_string (false), message.status_code);
85 downloader.finished (this, null);
89 if (message.uri.path == "/find") {
90 print ("[%d] AMBIGUOUS RESULTS: %s\n", handle, message.uri.to_string (false));
92 var msg = (IMDbMessage) message;
94 var re3 = new Regex ("<a href=\"(/title/[^\"]*/)\"[^>]*>([^<]*)</a> *\\(([0-9]*)");
97 if (re3.match ((string) message.response_body.data, 0, out match)) {
99 print ("[%d] POTENTIAL RESULT: %s (%s)\n", handle, match.fetch (2), match.fetch (3));
100 if (msg.title.down () == match.fetch (2).down () &&
101 msg.year == match.fetch (3).to_int ()) {
102 string url = "http://www.imdb.com" + match.fetch (1);
103 print ("[%d] CHOSE RESULT URL: %s\n", handle, url);
104 message.uri = new Soup.URI (url);
105 session.queue_message (message, this.title_page_callback);
108 } while (match.next ());
110 print ("[%d] NO MATCH\n", handle);
111 downloader.finished (this, null);
115 print ("[%d] GOT TITLE PAGE FOR %s\n", handle, message.uri.path);
118 if (downloader.re1.match ((string) message.response_body.data, 0, out match)) {
119 string url = "http://www.imdb.com" + match.fetch (1);
120 print ("[%d] FOUND PHOTO PAGE URL: %s\n", handle, url);
121 message.uri = new Soup.URI (url);
122 session.queue_message (message, this.photo_page_callback);
124 print ("[%d] NO POSTER AVAILABLE\n", handle);
125 downloader.finished (this, null);
129 private void photo_page_callback (Soup.Session session, Soup.Message message) {
131 message.status_code != Soup.KnownStatusCode.OK) {
132 downloader.finished (this, null);
136 print ("[%d] GOT PHOTO PAGE %s\n", handle, message.uri.path);
139 if (downloader.re2.match ((string) message.response_body.data, 0, out match)) {
140 string url = match.fetch (1);
141 print ("[%d] FOUND IMAGE URL: %s\n", handle, url);
142 message.uri = new Soup.URI (url);
143 session.queue_message (message, this.image_callback);
147 private void image_callback (Soup.Session session, Soup.Message message) {
149 message.status_code != Soup.KnownStatusCode.OK) {
150 downloader.finished (this, null);
154 print ("[%d] Downloaded poster: %s\n", handle, message.uri.to_string (false));
156 // Define cache path according to the Media Art Storage Spec (http://live.gnome.org/MediaArtStorageSpec)
157 string cache_path = Path.build_filename (cache_dir, cache_filename);
159 // Make sure the directory .album_arts is available
160 DirUtils.create_with_parents (cache_dir, 0770);
162 if (FileUtils.set_contents (cache_path + ".part",
163 (string) message.response_body.data,
164 (ssize_t) message.response_body.length)) {
166 FileUtils.rename (cache_path + ".part", cache_path);
169 var p1 = new Gdk.Pixbuf.from_file (cache_path + ".part");
170 var p2 = new Gdk.Pixbuf (Gdk.Colorspace.RGB, true, 8, Poster.SMALL_WIDTH, Poster.SMALL_HEIGHT);
171 double xscale = (double)Poster.SMALL_WIDTH / (double)p1.width;
172 double yscale = (double)Poster.SMALL_HEIGHT / (double)p1.height;
176 if (xscale > yscale) {
178 ofs_y = -(int) (p1.height*scale - Poster.SMALL_HEIGHT)/2;
181 ofs_x = -(int) (p1.width*scale - Poster.SMALL_WIDTH)/2;
183 p1.scale (p2, 0, 0, Poster.SMALL_WIDTH, Poster.SMALL_HEIGHT, ofs_x, ofs_y, scale, scale, Gdk.InterpType.BILINEAR);
184 print ("[%d] Scaled %d x %d --> %d x %d (%+d,%+d)\n", handle, p1.width, p1.height, p2.width, p2.height, ofs_x, ofs_y);
185 p2.save (cache_path, "png", null);
186 FileUtils.unlink (cache_path + ".part");
188 print ("[%d] Stored as: %s\n", handle, cache_path);
190 stdout.printf ("[%d] Failed to store poster\n", handle);
193 downloader.finished (this, cache_path);
196 public void cancel () {
197 print ("[%d] Cancelled\n", handle);
202 // The D-Bus service to manage poster downloads
203 public class IMDbPosterDownloader : Object, PosterDownloader {
204 private MainLoop loop;
205 private int fetch_handle = 1;
206 private List<IMDbPosterDownload> downloads = null;
207 private uint source_id;
209 public Soup.SessionAsync session;
213 public IMDbPosterDownloader () {
214 loop = new MainLoop (null);
216 session = new Soup.SessionAsync ();
217 session.max_conns = 40;
218 session.max_conns_per_host = 20;
220 re1 = new Regex ("\"(/rg/action-box-title/primary-photo/media/[^\"]*)\"");
221 re2 = new Regex ("\"(http://ia.media-imdb.com/images[^\"]*)\"");
222 } catch (RegexError e) {
226 public void timeout_quit () {
227 // With every change we reset the timer to 3min
228 if (source_id != 0) {
229 Source.remove (source_id);
231 source_id = Timeout.add_seconds (180, quit);
234 private bool quit () {
237 print ("Timeout. Quitting with %u remaining downloads.\n", downloads.length ());
238 foreach (IMDbPosterDownload download in downloads)
239 failed (download.handle);
249 public void finished (IMDbPosterDownload download, string? cache_path) {
250 if (cache_path != null)
251 fetched (download.handle, cache_path);
253 failed (download.handle);
254 downloads.remove (download);
258 // Implement the PosterDownloader interface
259 public int Fetch (string title, string year, string kind) throws DBus.Error {
260 print ("Fetch (\"%s\", \"%s\", \"%s\") = %d\n", title, year, kind, fetch_handle+1);
261 var download = new IMDbPosterDownload (title, year, false, ++fetch_handle, this);
263 downloads.append (download);
268 public int FetchThumbnail (string title, string year, string kind) throws DBus.Error {
269 print ("FetchThumbnail (\"%s\", \"%s\", \"%s\") = %d\n", title, year, kind, fetch_handle+1);
270 var download = new IMDbPosterDownload (title, year, true, ++fetch_handle, this);
272 downloads.append (download);
277 public void Unqueue (int handle) throws DBus.Error {
278 print ("Unqueue (%d)\n", handle);
279 IMDbPosterDownload download = null;
280 foreach (IMDbPosterDownload d in downloads) {
281 if (d.handle == handle) {
287 if (download != null) {
288 downloads.remove (download);
292 static void main () {
294 var conn = DBus.Bus.get (DBus.BusType.SESSION);
295 dynamic DBus.Object bus = conn.get_object ("org.freedesktop.DBus",
296 "/org/freedesktop/DBus",
297 "org.freedesktop.DBus");
299 // Try to register service in session bus
300 uint res = bus.request_name ("org.maemo.movieposter.IMDb", (uint) 0);
301 if (res == DBus.RequestNameReply.PRIMARY_OWNER) {
303 var server = new IMDbPosterDownloader ();
304 conn.register_object ("/org/maemo/movieposter/IMDb", server);
306 server.timeout_quit ();
310 error ("Oops: %s\n", e.message);