/* This file is part of Cinaest. * * Copyright (C) 2010 Philipp Zabel * * Cinaest is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Cinaest is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Cinaest. If not, see . */ namespace Hildon { public const int MARGIN_DOUBLE = 16; public const int MARGIN_HALF = 4; } namespace Poster { public const int SMALL_WIDTH = (800 - 2*Hildon.MARGIN_DOUBLE - 4*Hildon.MARGIN_HALF)/5; public const int SMALL_HEIGHT = (420 - Hildon.MARGIN_HALF)/2; public const int ICON_WIDTH = 46; public const int ICON_HEIGHT = 64; } // A single IMDb poster image search (parsing and retrieval of the poster image URI) public class IMDbMessage : Soup.Message { public string title; public int year; public string poster_uri = null; private bool thumbnail; public IMDbMessage (string title_, int year_, bool thumbnail_ = false) { Object (method: "GET"); title = title_; year = year_; thumbnail = thumbnail_; string url = "http://www.imdb.com/find?s=tt&q=%s (%d)".printf (convert (title, -1, "iso8859-1", "utf-8"), year); uri = new Soup.URI (url); } } // Encapsulation of a single poster download (IMDb title search query and image file download) public class IMDbPosterDownload : Object { private IMDbPosterDownloader downloader; private Soup.Session session; private string cache_dir; private string cache_filename; private bool thumbnail; private bool cancelled = false; public int handle; public IMDbPosterDownload (string title, string year, bool thumbnail_, int _handle, IMDbPosterDownloader _downloader) { handle = _handle; downloader = _downloader; session = downloader.session; thumbnail = thumbnail_; var message = new IMDbMessage (title, year.to_int (), thumbnail); session.queue_message (message, title_page_callback); if (thumbnail) { // FIXME cache_dir = Path.build_filename (Environment.get_tmp_dir(), "cinaest-thumbnails"); } else { cache_dir = Path.build_filename (Environment.get_user_cache_dir(), "media-art"); } cache_filename = "movie-" + Checksum.compute_for_string (ChecksumType.MD5, (title).down ()) + "-" + Checksum.compute_for_string (ChecksumType.MD5, (year).down ()) + ".jpeg"; } private void title_page_callback (Soup.Session session, Soup.Message message) { if (cancelled || message.status_code != Soup.KnownStatusCode.OK) { print ("[%d] NO POSTER FOR %s (CODE %u)\n", handle, message.uri.to_string (false), message.status_code); downloader.finished (this, null); return; } if (message.uri.path == "/find") { print ("[%d] AMBIGUOUS RESULTS: %s\n", handle, message.uri.to_string (false)); var msg = (IMDbMessage) message; var re3 = new Regex ("]*>([^<]*) *\\(([0-9]*)"); MatchInfo match; if (re3.match ((string) message.response_body.data, 0, out match)) { do { print ("[%d] POTENTIAL RESULT: %s (%s)\n", handle, match.fetch (2), match.fetch (3)); if (msg.title.down () == match.fetch (2).down () && msg.year == match.fetch (3).to_int ()) { string url = "http://www.imdb.com" + match.fetch (1); print ("[%d] CHOSE RESULT URL: %s\n", handle, url); message.uri = new Soup.URI (url); session.queue_message (message, this.title_page_callback); return; } } while (match.next ()); } print ("[%d] NO MATCH\n", handle); downloader.finished (this, null); return; } print ("[%d] GOT TITLE PAGE FOR %s\n", handle, message.uri.path); MatchInfo match; if (downloader.re1.match ((string) message.response_body.data, 0, out match)) { string url = "http://www.imdb.com" + match.fetch (1); print ("[%d] FOUND PHOTO PAGE URL: %s\n", handle, url); message.uri = new Soup.URI (url); session.queue_message (message, this.photo_page_callback); } else { print ("[%d] NO POSTER AVAILABLE\n", handle); downloader.finished (this, null); } } private void photo_page_callback (Soup.Session session, Soup.Message message) { if (cancelled || message.status_code != Soup.KnownStatusCode.OK) { downloader.finished (this, null); return; } print ("[%d] GOT PHOTO PAGE %s\n", handle, message.uri.path); MatchInfo match; if (downloader.re2.match ((string) message.response_body.data, 0, out match)) { string url = match.fetch (1); print ("[%d] FOUND IMAGE URL: %s\n", handle, url); message.uri = new Soup.URI (url); session.queue_message (message, this.image_callback); } } private void image_callback (Soup.Session session, Soup.Message message) { if (cancelled || message.status_code != Soup.KnownStatusCode.OK) { downloader.finished (this, null); return; } print ("[%d] Downloaded poster: %s\n", handle, message.uri.to_string (false)); // Define cache path according to the Media Art Storage Spec (http://live.gnome.org/MediaArtStorageSpec) string cache_path = Path.build_filename (cache_dir, cache_filename); // Make sure the directory .album_arts is available DirUtils.create_with_parents (cache_dir, 0770); if (FileUtils.set_contents (cache_path + ".part", (string) message.response_body.data, (ssize_t) message.response_body.length)) { if (!thumbnail) { FileUtils.rename (cache_path + ".part", cache_path); } else { // Scale and crop var p1 = new Gdk.Pixbuf.from_file (cache_path + ".part"); var p2 = new Gdk.Pixbuf (Gdk.Colorspace.RGB, true, 8, Poster.SMALL_WIDTH, Poster.SMALL_HEIGHT); double xscale = (double)Poster.SMALL_WIDTH / (double)p1.width; double yscale = (double)Poster.SMALL_HEIGHT / (double)p1.height; double scale; int ofs_x = 0; int ofs_y = 0; if (xscale > yscale) { scale = xscale; ofs_y = -(int) (p1.height*scale - Poster.SMALL_HEIGHT)/2; } else { scale = yscale; ofs_x = -(int) (p1.width*scale - Poster.SMALL_WIDTH)/2; } p1.scale (p2, 0, 0, Poster.SMALL_WIDTH, Poster.SMALL_HEIGHT, ofs_x, ofs_y, scale, scale, Gdk.InterpType.BILINEAR); 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); p2.save (cache_path, "png", null); FileUtils.unlink (cache_path + ".part"); } print ("[%d] Stored as: %s\n", handle, cache_path); } else { stdout.printf ("[%d] Failed to store poster\n", handle); } downloader.finished (this, cache_path); } public void cancel () { print ("[%d] Cancelled\n", handle); cancelled = true; } } // The D-Bus service to manage poster downloads public class IMDbPosterDownloader : Object, PosterDownloader { private MainLoop loop; private int fetch_handle = 1; private List downloads = null; private uint source_id; public Soup.SessionAsync session; public Regex re1; public Regex re2; public IMDbPosterDownloader () { loop = new MainLoop (null); session = new Soup.SessionAsync (); session.max_conns = 40; session.max_conns_per_host = 20; try { re1 = new Regex ("\"(/rg/action-box-title/primary-photo/media/[^\"]*)\""); re2 = new Regex ("\"(http://ia.media-imdb.com/images[^\"]*)\""); } catch (RegexError e) { } } public void timeout_quit () { // With every change we reset the timer to 3min if (source_id != 0) { Source.remove (source_id); } source_id = Timeout.add_seconds (180, quit); } private bool quit () { loop.quit (); print ("Timeout. Quitting with %u remaining downloads.\n", downloads.length ()); foreach (IMDbPosterDownload download in downloads) failed (download.handle); // One-shot only return false; } public void run () { loop.run (); } public void finished (IMDbPosterDownload download, string? cache_path) { if (cache_path != null) fetched (download.handle, cache_path); else failed (download.handle); downloads.remove (download); timeout_quit (); } // Implement the PosterDownloader interface public int Fetch (string title, string year, string kind) throws DBus.Error { print ("Fetch (\"%s\", \"%s\", \"%s\") = %d\n", title, year, kind, fetch_handle+1); var download = new IMDbPosterDownload (title, year, false, ++fetch_handle, this); downloads.append (download); return fetch_handle; } public int FetchThumbnail (string title, string year, string kind) throws DBus.Error { print ("FetchThumbnail (\"%s\", \"%s\", \"%s\") = %d\n", title, year, kind, fetch_handle+1); var download = new IMDbPosterDownload (title, year, true, ++fetch_handle, this); downloads.append (download); return fetch_handle; } public void Unqueue (int handle) throws DBus.Error { print ("Unqueue (%d)\n", handle); IMDbPosterDownload download = null; foreach (IMDbPosterDownload d in downloads) { if (d.handle == handle) { download = d; d.cancel (); break; } } if (download != null) { downloads.remove (download); } } static void main () { try { var conn = DBus.Bus.get (DBus.BusType.SESSION); dynamic DBus.Object bus = conn.get_object ("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus"); // Try to register service in session bus uint res = bus.request_name ("org.maemo.movieposter.IMDb", (uint) 0); if (res == DBus.RequestNameReply.PRIMARY_OWNER) { // Start server var server = new IMDbPosterDownloader (); conn.register_object ("/org/maemo/movieposter/IMDb", server); server.timeout_quit (); server.run (); } } catch (Error e) { error ("Oops: %s\n", e.message); } } }