--- /dev/null
+/* 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 ("<a href=\"(/title/[^\"]*/)\"[^>]*>([^<]*)</a> *\\(([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<IMDbPosterDownload> 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);
+ }
+ }
+}