Google poster downloader: quit the D-Bus server after 3 min of inactivity
[cinaest] / src / poster / google-poster-downloader.vala
index 5dee8c2..423d017 100644 (file)
-using GLib;
-
-[DBus (name = "org.maemo.movieposter.Provider", signals = "fetched")]
-public interface Provider {
-       public abstract int Fetch (string title, string year, string kind) throws DBus.Error;
-       public abstract int FetchThumbnail (string title, string year, string kind) throws DBus.Error;
-       public abstract void Unqueue (int handle) throws DBus.Error;
-       public signal void fetched (int handle, string path);
-}
-
-// http://live.gnome.org/MediaArtStorageSpec
-
-// Sample implementation of org.maemo.movieposter.Provider that uses Google 
-// images's first result as movie poster. There is of course no certainty 
-// that the first result on Google images is indeed the movie's poster - there
-// are quite a few false positives, especially for non-mainstream movies.
+/* This file is part of Cinaest.
+ *
+ * Copyright (C) 2009 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/>.
+ */
+
+using Soup;
+
+// A single google image search (parsing and retrieval of the poster image URI)
+public class GoogleImageSearch : Soup.Message {
+       public string poster_uri = null;
+       private bool thumbnail;
+       private string last_chunk = null;
+
+       public GoogleImageSearch (string search, bool _thumbnail = false) {
+               Object (method: "GET");
+               uri = new URI ("http://images.google.com/images?imgsz=m&imgar=t&q=" + Uri.escape_string (search, "+", false));
+               thumbnail = _thumbnail;
+               got_chunk.connect (this.on_got_chunk);
+       }
 
-public class GoogleImages : Object, Provider {
-       int fetch_handle;
-       Cancellable cancellable;
+       void on_got_chunk (Buffer chunk) {
+               weak string found;
 
-       public int Fetch (string title, string year, string kind) throws DBus.Error {
-               int handle = fetch_handle++;
+               if (poster_uri != null)
+                       return;
 
-               _fetch_async.begin (title, year, kind, false, handle);
-               return handle;
+               // FIXME - store a copy of the last chunk and retry if the parser fails, it can't handle partial input
+               if (last_chunk == null) {
+                       // FIXME - can we avoid this copy without ugly hacks?
+                       found = chunk.data.ndup (chunk.length).str ("dyn.setResults([[");
+               } else {
+                       found = (last_chunk + chunk.data).str ("dyn.setResults([[");
+                       last_chunk = null;
+               }
+               if (found != null) {
+                       var parser = new GoogleImageSearchParser (found);
+                       try {
+                               poster_uri = parser.run (thumbnail);
+                               if (poster_uri != null)
+                                       got_poster_uri (this);
+                       } catch (Error e) {
+                               if (e is ParserError.EOF) {
+                                       last_chunk = chunk.data.ndup (chunk.length);
+                               } else {
+                                       stdout.printf ("Parser error: %s\n", e.message);
+                               }
+                       }
+               }
        }
 
-       public int FetchThumbnail (string title, string year, string kind) throws DBus.Error {
-               int handle = fetch_handle++;
+       signal void got_poster_uri (GoogleImageSearch search);
+}
 
-               _fetch_async.begin (title, year, kind, true, handle);
-               return handle;
+// Encapsulation of a single poster download (google image search query and image file download)
+public class GooglePosterDownload : Object {
+       private GooglePosterDownloader downloader;
+       private Soup.Session session;
+       public int handle;
+       private string cache_dir;
+       private string cache_filename;
+       private bool cancelled = false;
+
+       public GooglePosterDownload (string title, string year, bool thumbnail, int _handle, GooglePosterDownloader _downloader) {
+               var search = title + "+" + year + "+movie+poster";
+
+               handle = _handle;
+               downloader = _downloader;
+               session = downloader.session;
+
+               var message = new GoogleImageSearch (search, thumbnail);
+               message.got_poster_uri.connect (on_got_poster_uri);
+               session.queue_message (message, google_search_finished);
+
+               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";
        }
 
-       public void Unqueue (int handle) {
-               // FIXME - cancel everything for now
-               cancellable.cancel ();
+       private void on_got_poster_uri (GoogleImageSearch message) {
+               session.cancel_message (message, Soup.KnownStatusCode.OK);
        }
 
-       private async void _fetch_async (string title, string year, string kind, bool thumbnail, int handle) {
-               uint u = 0;
-               size_t hread = 0;
-               string [] pieces = title.split (" ", -1);
-               string stitched = "";
-               bool first = true;
+       private void google_search_finished (Session session, Message message) {
+               if (cancelled)
+                       return;
 
-               // FIXME - cancel everything for now
-               cancellable = new Cancellable ();
+               var search_message = (GoogleImageSearch) message;
+               print ("Finished search: %s\n", message.uri.to_string (false));
 
-               stdout.printf ("Fetching %s \"%s (%s)\" ...\n", kind, title, year);
+               var poster_message = new Soup.Message ("GET", search_message.poster_uri);
+               session.queue_message (poster_message, poster_downloaded);
+       }
 
-               if (title == null || title == "")
-                       title = "  ";
+       private void poster_downloaded (Session session, Message message) {
+               if (cancelled)
+                       return;
 
-               if (year == null || year == "")
-                       year = "  ";
+               print ("Downloaded poster: %s\n", message.uri.to_string (false));
 
-               // Convert the title and year into something that will work for Google images
+               // 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);
 
-               while (pieces[u] != null) {
-                       if (!first)
-                               stitched += "+";
-                       stitched += pieces[u];
-                       u++;
-                       first = false;
-               }
+               // Make sure the directory .album_arts is available
+               DirUtils.create_with_parents (cache_dir, 0770);
 
-               stitched += "+";
+               try {
+                       var file = File.new_for_path (cache_path + ".part");
+                       var stream = file.create (FileCreateFlags.NONE, null);
 
-               u = 0;
-               first = true;
-               pieces = year.split (" ", -1);
+                       stream.write (message.response_body.data, (size_t) message.response_body.length, null);
 
-               while (pieces[u] != null) {
-                       if (!first)
-                               stitched += "+";
-                       stitched += pieces[u];
-                       u++;
-                       first = false;
-               }
+                       FileUtils.rename (cache_path + ".part", cache_path);
 
-               stitched += "+movie+poster";
+                       print ("Stored as: %s\n", cache_path);
 
-               // Start the query on Google images
+                       downloader.fetched (handle, cache_path);
+                       downloader.timeout_quit ();
+               } catch (Error e) {
+                       stdout.printf ("Failed to store poster: %s\n", e.message);
+               }
+       }
 
-               stdout.printf("GET http://images.google.com/images?q=" + Uri.escape_string (stitched, "+", false) + "\n");
+       public void cancel () {
+               cancelled = true;
+       }
+}
 
-               File google_search = File.new_for_uri ("http://images.google.com/images?q=" + Uri.escape_string (stitched, "+", false));
+// The D-Bus service to manage poster downloads
+public class GooglePosterDownloader : Object, PosterDownloader {
+       private MainLoop loop;
+       public SessionAsync session;
+       private int fetch_handle = 1;
+       private List<GooglePosterDownload> downloads = null;
+       private uint source_id;
 
-               try {
-                       char [] buffer = new char [40000];
-                       string asstring;
-                       size_t total = 0;
+       public GooglePosterDownloader () {
+               loop = new MainLoop (null);
 
-                       // Fetch the first page
+               session = new SessionAsync ();
+               session.max_conns_per_host = 7;
+       }
 
-                       InputStream stream = google_search.read (null);
+       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);
+       }
 
-                       while (total < 40000) {
-                               hread = yield stream.read_async ((char *)buffer + total, 40000 - total, Priority.DEFAULT_IDLE, cancellable);
-                               total += hread;
-                               if (cancellable.is_cancelled ()) {
-                                       stdout.printf ("CANCELED\n");
-                                       return;
-                               }
-                               if (hread == 0)
-                                       break;
-                       }
-                       buffer[total] = 0;
-
-                       asstring = (string) buffer;
-
-                       // Find the first result
-
-                       string found = null;
-                       int i;
-                       char end;
-                       if (thumbnail) {
-                               do {
-                                       found = asstring.str ("http://t");
-                               } while (found != null && !found.offset (9).has_prefix (".gstatic.com/images?q=tbn"));
-                               i = 0;
-                               end = ' ';
-                       } else {
-                               found = asstring.str ("href=/imgres?imgurl=");
-                               i = 20;
-                               end = '&';
-                       }
+        private bool quit () {
+               loop.quit ();
 
-                       if (found != null) {
+                // One-shot only
+                return false;
+        }
 
-                               StringBuilder url = new StringBuilder ();
-                               long y = found.len();
+       public void run () {
+               loop.run ();
+       }
 
-                               while (found[i] != end && i < y) {
-                                       url.append_unichar (found[i]);
-                                       i++;
-                               }
+       // Implement the PosterDownloader interface
+       public int Fetch (string title, string year, string kind) throws DBus.Error {
+               var download = new GooglePosterDownload (title, year, false, ++fetch_handle, this);
 
-                               string cache_path;
+               downloads.append (download);
 
-                               string cache_dir;
-                               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");
-                               }
+               return fetch_handle;
+       }
 
-                               // Define cache path = ~/.album_art/MD5 (down (albumartist)).jpeg
-
-                               cache_path = Path.build_filename (cache_dir, kind + "-" +
-                                                                 Checksum.compute_for_string (
-                                                                                  ChecksumType.MD5, 
-                                                                                  (title).down (), 
-                                                                                  -1) + "-" +
-                                                                 Checksum.compute_for_string (
-                                                                                  ChecksumType.MD5, 
-                                                                                  (year).down (), 
-                                                                                  -1) +
-                                                                  ".jpeg", null);
-
-                               // Make sure the directory .album_arts is available
-                               DirUtils.create_with_parents (cache_dir, 0770);
-
-                               stdout.printf ("GET %s --> %s\n", url.str, cache_path);
-
-                               File online_image = File.new_for_uri (url.str);
-                               File cache_image = File.new_for_path (cache_path + ".part");
-
-                               // Copy from Google images to local cache
-
-                               yield online_image.copy_async (cache_image,
-                                                              FileCopyFlags.NONE,
-                                                              Priority.DEFAULT_IDLE,
-                                                              cancellable,
-                                                              null);
-                               if (cancellable.is_cancelled ()) {
-                                       stdout.printf ("CANCELED\n");
-                                       return;
-                               }
+       public int FetchThumbnail (string title, string year, string kind) throws DBus.Error {
+               var download = new GooglePosterDownload (title, year, true, ++fetch_handle, this);
 
-                               FileUtils.rename (cache_path + ".part", cache_path);
+               downloads.append (download);
 
-                               fetched (handle, cache_path);
+               return fetch_handle;
+       }
 
-                               stdout.printf ("DONE\n");
-                       } else {
-                               stdout.printf ("NOT FOUND\n");
-                       //      stdout.printf ("%s\n", asstring);
+       public void Unqueue (int handle) throws DBus.Error {
+               GooglePosterDownload download = null;
+               foreach (GooglePosterDownload d in downloads) {
+                       if (d.handle == handle) {
+                               download = d;
+                               d.cancel ();
+                               break;
                        }
-
-               } catch (GLib.Error error) {
-                       stderr.printf ("Error: %s\n", error.message);
+               }
+               if (download != null) {
+                       downloads.remove (download);
                }
        }
-}
-
-void main () {
-       var loop = new MainLoop (null, false);
-
-       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 request_name_result = bus.request_name ("org.maemo.movieposter.GoogleImages", (uint) 0);
-
-               if (request_name_result == DBus.RequestNameReply.PRIMARY_OWNER) {
-                       // Start server
-                       var server = new GoogleImages ();
-                       conn.register_object ("/org/maemo/movieposter/GoogleImages", server);
-
-                       loop.run ();
+       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.GoogleImages", (uint) 0);
+                       if (res == DBus.RequestNameReply.PRIMARY_OWNER) {
+                               // Start server
+                               var server = new GooglePosterDownloader ();
+                               conn.register_object ("/org/maemo/movieposter/GoogleImages", server);
+
+                               server.timeout_quit ();
+                               server.run ();
+                       }
+               } catch (Error e) {
+                       error ("Oops: %s\n", e.message);
                }
-       } catch (Error e) {
-               error ("Oops: %s\n", e.message);
        }
 }
+