Add IMDb poster downloader
authorPhilipp Zabel <philipp.zabel@gmail.com>
Wed, 4 Aug 2010 22:12:33 +0000 (00:12 +0200)
committerPhilipp Zabel <philipp.zabel@gmail.com>
Thu, 5 Aug 2010 19:04:54 +0000 (21:04 +0200)
The Google image search downloader might be fast, but it just returns
too much false positives, and nearly nothing useful for more obscure movies.

Makefile.am
configure.ac
data/org.maemo.movieposter.IMDb.service.in [new file with mode: 0644]
src/poster/imdb-poster-downloader.vala [new file with mode: 0644]

index 8236c8d..c8ee810 100644 (file)
@@ -14,6 +14,7 @@ lib_LTLIBRARIES = \
 
 libexec_PROGRAMS = \
        google-poster-downloader \
+       imdb-poster-downloader \
        imdb-plaintext-downloader \
        cinaest-google-backend \
        cinaest-moviepilot-backend
@@ -29,7 +30,8 @@ dbusservice_DATA = \
        data/org.maemo.cinaest.IMDb.service \
        data/org.maemo.cinaest.GoogleShowtimes.service \
        data/org.maemo.cinaest.MoviePilot.service \
-       data/org.maemo.movieposter.GoogleImages.service
+       data/org.maemo.movieposter.GoogleImages.service \
+       data/org.maemo.movieposter.IMDb.service
 
 desktopentry_DATA = \
        data/cinaest.desktop
@@ -274,6 +276,22 @@ google_poster_downloader_LDADD = ${DBUS_LIBS} ${GIO_LIBS} ${SOUP_LIBS}
 src/poster/google-poster-downloader.c: ${google_poster_downloader_VALASOURCES}
        ${VALAC} -C ${google_poster_downloader_VALASOURCES} ${google_poster_downloader_VALAFLAGS}
 
+imdb_poster_downloader_SOURCES = \
+       src/poster/imdb-poster-downloader.c \
+       src/poster/poster-downloader-interface.c
+
+imdb_poster_downloader_VALASOURCES = \
+       src/poster/imdb-poster-downloader.vala \
+       src/poster/poster-downloader-interface.vala
+
+imdb_poster_downloader_VALAFLAGS = --thread --vapidir ./vapi \
+       --pkg dbus-glib-1 --pkg gio-2.0 --pkg libsoup-2.4 --pkg gdk-pixbuf-2.0
+imdb_poster_downloader_CFLAGS = ${DBUS_CFLAGS} ${GIO_CFLAGS} ${SOUP_CFLAGS} ${GDK_CFLAGS}
+imdb_poster_downloader_LDADD = ${DBUS_LIBS} ${GIO_LIBS} ${SOUP_LIBS} ${GDK_LIBS}
+
+src/poster/imdb-poster-downloader.c: ${imdb_poster_downloader_VALASOURCES}
+       ${VALAC} -C ${imdb_poster_downloader_VALASOURCES} ${imdb_poster_downloader_VALAFLAGS}
+
 ACLOCAL_AMFLAGS = -Im4
 
 CLEANFILES = \
index f595916..cbd3215 100644 (file)
@@ -43,6 +43,10 @@ PKG_CHECK_MODULES(GCONF, gconf-2.0 >= 2.16.0)
 AC_SUBST(GCONF_LIBS)
 AC_SUBST(GCONF_CFLAGS)
 
+PKG_CHECK_MODULES(GDK, gdk-pixbuf-2.0)
+AC_SUBST(GDK_LIBS)
+AC_SUBST(GDK_CFLAGS)
+
 PKG_CHECK_MODULES(HILDON, hildon-1 >= 2.2.0)
 AC_SUBST(HILDON_LIBS)
 AC_SUBST(HILDON_CFLAGS)
@@ -129,5 +133,6 @@ AC_OUTPUT([
        data/org.maemo.cinaest.GoogleShowtimes.service
        data/org.maemo.cinaest.MoviePilot.service
        data/org.maemo.movieposter.GoogleImages.service
+       data/org.maemo.movieposter.IMDb.service
        data/cinaest.desktop
 ])
diff --git a/data/org.maemo.movieposter.IMDb.service.in b/data/org.maemo.movieposter.IMDb.service.in
new file mode 100644 (file)
index 0000000..d153452
--- /dev/null
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.maemo.movieposter.IMDb
+Exec=@libexecdir@/imdb-poster-downloader
diff --git a/src/poster/imdb-poster-downloader.vala b/src/poster/imdb-poster-downloader.vala
new file mode 100644 (file)
index 0000000..4827bd0
--- /dev/null
@@ -0,0 +1,313 @@
+/* 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);
+               }
+       }
+}