From: Philipp Zabel Date: Wed, 4 Aug 2010 22:12:33 +0000 (+0200) Subject: Add IMDb poster downloader X-Git-Url: https://vcs.maemo.org/git/?p=cinaest;a=commitdiff_plain;h=d234cebff2d54ca3357cd2e59aa696c4632f2df9 Add IMDb poster downloader The Google image search downloader might be fast, but it just returns too much false positives, and nearly nothing useful for more obscure movies. --- diff --git a/Makefile.am b/Makefile.am index 8236c8d..c8ee810 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 = \ diff --git a/configure.ac b/configure.ac index f595916..cbd3215 100644 --- a/configure.ac +++ b/configure.ac @@ -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 index 0000000..d153452 --- /dev/null +++ b/data/org.maemo.movieposter.IMDb.service.in @@ -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 index 0000000..4827bd0 --- /dev/null +++ b/src/poster/imdb-poster-downloader.vala @@ -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 . + */ + +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); + } + } +}