Add MoviePilot plugin and backend
authorPhilipp Zabel <philipp.zabel@gmail.com>
Mon, 1 Feb 2010 23:27:36 +0000 (00:27 +0100)
committerPhilipp Zabel <philipp.zabel@gmail.com>
Wed, 14 Jul 2010 21:34:08 +0000 (23:34 +0200)
Makefile.am
configure.ac
data/org.maemo.cinaest.MoviePilot.service.in [new file with mode: 0644]
po/POTFILES.in
src/backends/moviepilot/moviepilot-backend.vala [new file with mode: 0644]
src/plugins/moviepilot-plugin.vala [new file with mode: 0644]

index 4b2a35f..4ba24c9 100644 (file)
@@ -15,17 +15,20 @@ lib_LTLIBRARIES = \
 libexec_PROGRAMS = \
        google-poster-downloader \
        imdb-plaintext-downloader \
-       cinaest-google-backend
+       cinaest-google-backend \
+       cinaest-moviepilot-backend
 
 pkglib_LTLIBRARIES = \
        libcatalog-plugin.la \
        libgoogle-plugin.la \
-       libimdb-plugin.la
+       libimdb-plugin.la \
+       libmoviepilot-plugin.la
 
 dbusservice_DATA = \
        data/org.maemo.cinaest.service \
        data/org.maemo.cinaest.IMDb.service \
        data/org.maemo.cinaest.GoogleShowtimes.service \
+       data/org.maemo.cinaest.MoviePilot.service \
        data/org.maemo.movieposter.GoogleImages.service
 
 desktopentry_DATA = \
@@ -172,6 +175,21 @@ libimdb_plugin_la_LDFLAGS = -module
 src/plugins/imdb-plugin.c: ${libimdb_plugin_la_VALASOURCES}
        ${VALAC} -C ${libimdb_plugin_la_VALASOURCES} ${libimdb_plugin_la_VALAFLAGS}
 
+libmoviepilot_plugin_la_SOURCES = \
+       src/plugins/moviepilot-plugin.c
+
+libmoviepilot_plugin_la_VALASOURCES = \
+       src/plugins/moviepilot-plugin.vala
+
+libmoviepilot_plugin_la_VALAFLAGS = --vapidir ./vapi --pkg config --pkg cinaest \
+       --pkg dbus-glib-1 --pkg hildon-1 --pkg json-glib-1.0 --pkg libosso
+libmoviepilot_plugin_la_CFLAGS = ${CINAEST_CFLAGS} ${HILDON_CFLAGS} ${JSON_CFLAGS} ${OSSO_CFLAGS}
+libmoviepilot_plugin_la_LIBADD = ${CINAEST_LIBS} ${HILDON_LIBS} ${JSON_LIBS} ${OSSO_LIBS}
+libmoviepilot_plugin_la_LDFLAGS = -module
+
+src/plugins/moviepilot-plugin.c: ${libmoviepilot_plugin_la_VALASOURCES}
+       ${VALAC} -C $^ ${libmoviepilot_plugin_la_VALAFLAGS}
+
 cinaest_google_backend_SOURCES = \
        src/backends/google/google-backend.c \
        src/backends/google/google-parser.c
@@ -188,6 +206,20 @@ cinaest_google_backend_LDADD = ${DBUS_LIBS} ${GCONF_LIBS} ${GEE_LIBS} ${GIO_LIBS
 src/backends/google/google-backend.c: ${cinaest_google_backend_VALASOURCES}
        ${VALAC} -C ${cinaest_google_backend_VALASOURCES} ${cinaest_google_backend_VALAFLAGS}
 
+cinaest_moviepilot_backend_SOURCES = \
+       src/backends/moviepilot/moviepilot-backend.c
+
+cinaest_moviepilot_backend_VALASOURCES = \
+       src/backends/moviepilot/moviepilot-backend.vala
+
+cinaest_moviepilot_backend_VALAFLAGS = --thread --vapidir ./vapi --pkg dbus-glib-1 \
+       --pkg gee-1.0 --pkg gio-2.0 --pkg json-glib-1.0 --pkg rest-0.6
+cinaest_moviepilot_backend_CFLAGS = ${DBUS_CFLAGS} ${GEE_CFLAGS} ${GIO_CFLAGS} ${JSON_CFLAGS} ${REST_CFLAGS}
+cinaest_moviepilot_backend_LDADD = ${DBUS_LIBS} ${GEE_LIBS} ${GIO_LIBS} ${JSON_LIBS} ${REST_LIBS}
+
+src/backends/moviepilot/moviepilot-backend.c: ${cinaest_moviepilot_backend_VALASOURCES}
+       ${VALAC} -C $^ ${cinaest_moviepilot_backend_VALAFLAGS}
+
 imdb_plaintext_downloader_SOURCES = \
         src/imdb/imdb-plaintext-downloader.c \
        src/imdb/imdb-ftp-downloader.c \
@@ -235,6 +267,8 @@ CLEANFILES = \
        ${libcatalog_plugin_la_SOURCES} \
        $(patsubst %.vala,%.c,${libgoogle_plugin_la_VALASOURCES}) \
        ${libimdb_plugin_la_SOURCES} \
+       ${libmoviepilot_plugin_la_SOURCES} \
        ${imdb_plaintext_downloader_SOURCES} \
        ${google_poster_downloader_SOURCES} \
-       ${cinaest_google_backend_SOURCES}
+       ${cinaest_google_backend_SOURCES} \
+       ${cinaest_moviepilot_backend_SOURCES}
index 9612d05..8a44a04 100644 (file)
@@ -82,6 +82,10 @@ PKG_CHECK_MODULES(SOUP, libsoup-2.4)
 AC_SUBST(SOUP_LIBS)
 AC_SUBST(SOUP_CFLAGS)
 
+PKG_CHECK_MODULES(REST, rest-0.6)
+AC_SUBST(REST_LIBS)
+AC_SUBST(REST_CFLAGS)
+
 PKG_CHECK_MODULES(SQLITE3, sqlite3 >= 3.6.14)
 AC_SUBST(SQLITE3_LIBS)
 AC_SUBST(SQLITE3_CFLAGS)
@@ -119,6 +123,7 @@ AC_OUTPUT([
        data/org.maemo.cinaest.service
        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/cinaest.desktop
 ])
diff --git a/data/org.maemo.cinaest.MoviePilot.service.in b/data/org.maemo.cinaest.MoviePilot.service.in
new file mode 100644 (file)
index 0000000..6567350
--- /dev/null
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.maemo.cinaest.MoviePilot
+Exec=@libexecdir@/cinaest-moviepilot-backend
index ec25323..0247b43 100644 (file)
@@ -6,6 +6,7 @@ src/movie-menu.vala
 src/plugins/catalog-plugin.vala
 src/plugins/imdb-plugin.vala
 src/plugins/google-plugin.vala
+src/plugins/moviepilot-plugin.vala
 src/settings-dialog.vala
 src/source-dialog.vala
 src/source-list-menu.vala
diff --git a/src/backends/moviepilot/moviepilot-backend.vala b/src/backends/moviepilot/moviepilot-backend.vala
new file mode 100644 (file)
index 0000000..8964483
--- /dev/null
@@ -0,0 +1,327 @@
+using Gee;
+
+public Gee.HashMap <string, MovieSearch> searches;
+public DBus.Connection conn;
+
+// A movie, serialized as JSON object in the movies_found D-Bus signal
+class Movie : Object {
+       public string title { get; set; }
+       public int year { get; set; }
+       public double rating { get; set; }
+       public string genres { get; set; }
+       public string id { get; set; }
+}
+
+[DBus (name = "org.maemo.cinaest.MovieSearch", signals="movies_found")]
+public class MovieSearch : Object {
+       private const string SERVICE_URL = "http://www.moviepilot.de";
+       private const string API_KEY = "1dab2d86f46d669766de572ba9b8eb";
+
+       private Rest.Proxy proxy;
+
+       public int id;
+       bool aborted;
+       string title;
+       MoviePilotMovieService service;
+       public string path;
+       public string sender;
+
+       private SourceFunc callback = null;
+       private GLib.List<Movie> results = null;
+
+       // D-Bus API
+
+       public void abort () {
+               print ("aborting\n");
+               aborted = true;
+       }
+
+       public void start (string query) {
+               title = query;
+               aborted = false;
+
+               print ("starting query %d: \"%s\"\n", id, title);
+               query_async.begin ();
+       }
+
+       public signal void movies_found (string[] movies, bool finished);
+
+       // Internal methods
+
+       internal MovieSearch (MoviePilotMovieService _service, string _sender, string _path) {
+               service = _service;
+               sender = _sender;
+               path = _path;
+       }
+
+       internal async void query_async () {
+               if (callback != null)
+                       return;
+               callback = query_async.callback;
+               try {
+                       print ("_search_movies (%s)\n", title);
+                       _search_movies (title);
+                       yield;
+                       if (!aborted) {
+                               var movies = new string[results.length ()];
+                               int i = 0;
+                               for (weak GLib.List<Movie> node = results.first (); node != null; node = node.next) {
+                               // FIXME: why does Json.serialize_gobject fail here?
+                               //      movies[i++] = Json.serialize_gobject (node.data, null);
+                                       movies[i++] = "{\"title\":\"%s\",\"year\":%d,\"rating\":%f,\"genres\":\"%s\",\"id\":\"%s\"}".printf (node.data.title, node.data.year, node.data.rating, node.data.genres, node.data.id);
+                               }
+                               print ("got %d movies\n", movies.length);
+                               movies_found (movies, true);
+                       } else {
+                               print ("aborted\n");
+                       }
+                       service.timeout_quit ();
+               } catch (GLib.Error e) {
+                       critical ("Error: %s\n", e.message);
+               }
+               callback = null;
+       }
+
+       // MoviePilot RESTful API calls, executed using librest
+
+       private void _search_movies (string query, int per_page = 20, int page = 1) throws GLib.Error {
+               var call = proxy.new_call ();
+
+               if (query == "") {
+                       Idle.add (callback);
+                       return;
+               }
+
+               call.set_function ("searches/movies.json");
+               call.set_method ("GET");
+               call.add_params ("q", query,
+                                "api_key", API_KEY,
+                                "per_page", per_page.to_string (),
+                                "page", page.to_string ());
+               call.run_async (search_movies_cb, this);
+       }
+/*
+       internal void get_info (string id) throws GLib.Error {
+               var call = proxy.new_call ();
+
+               call.set_function ("movies/%s.json".printf (id));
+               call.set_method ("GET");
+               call.add_params ("api_key", API_KEY);
+               call.run_async (get_info_cb, proxy);
+       }
+*/
+       // Callback functions handle JSON results using libjson-glib
+
+       private void search_movies_cb (Rest.ProxyCall call, GLib.Error? _error, GLib.Object? weak_object) {
+               var parser = new Json.Parser ();
+               unowned Json.Node node;
+
+               try {
+                       parser.load_from_data (call.get_payload (), (ssize_t) call.get_payload_length ());
+               } catch (Error e) {
+                       critical ("Payload: %s\nParse error: %s", call.get_payload (), e.message);
+                       Idle.add (this.callback);
+                       return;
+               }
+
+               node = parser.get_root ();
+               if (node.get_node_type () != Json.NodeType.OBJECT) {
+                       critical ("JSON error, not an object:\n%s\n", call.get_payload ());
+                       Idle.add (this.callback);
+                       return;
+               }
+
+               var object = node.get_object ();
+               if (!object.has_member ("total_entries")) {
+                       if (object.has_member ("error")) {
+                               critical ("Error: %s\n", object.get_string_member ("error"));
+                       } else {
+                               critical ("JSON error, not a movie search result:\n%s\n", call.get_payload ());
+                               //{"suggestions":["matrix","material","matilda"],"total-entries":0}
+                       }
+                       Idle.add (this.callback);
+                       return;
+               }
+
+               if (object.get_int_member ("total_entries") == 0) {
+                       Idle.add (this.callback);
+                       return;
+               }
+
+               if (!object.has_member ("movies")) {
+                       critical ("JSON error, not a movie search result:\n%s\n", call.get_payload ());
+                       Idle.add (this.callback);
+                       return;
+               }
+
+       //      var total_results = object.get_int_member ("total_entries");
+       //      print ("total_results = %lld\n", total_results);
+
+               node = object.get_member ("movies");
+               if (node.get_node_type () != Json.NodeType.ARRAY) {
+                       critical ("JSON error, not a movie array\n");
+                       return;
+               }
+
+               var array = node.get_array ();
+               foreach (weak Json.Node n in array.get_elements ()) {
+                       var movie = handle_movie (n);
+
+                       results.append (movie);
+               }
+
+               Idle.add (this.callback);
+       }
+
+       private void get_info_cb (Rest.ProxyCall call, GLib.Error? _error, GLib.Object? weak_object) {
+               var parser = new Json.Parser ();
+
+               try {
+                       parser.load_from_data (call.get_payload (), (ssize_t) call.get_payload_length ());
+               } catch (Error e) {
+                       critical ("Payload: %s\nParse error: %s", call.get_payload (), e.message);
+                       return;
+               }
+
+               var movie = handle_movie (parser.get_root ());
+
+               results.append (movie);
+
+               Idle.add (this.callback);
+       }
+
+       private Movie handle_movie (Json.Node node) requires (node.get_node_type () == Json.NodeType.OBJECT) {
+               var object = node.get_object ();
+               var movie = new Movie ();
+
+               movie.title = object.get_string_member ("display_title");
+               movie.year = object.get_string_member ("production_year").to_int ();
+               movie.rating = object.get_double_member ("average_community_rating");
+
+               movie.genres = object.get_string_member ("genres_list"); // CSV
+
+               var url = object.get_string_member ("restful_url");
+               if (url.has_prefix ("http://www.moviepilot.de/movies/")) {
+                       movie.id = url.offset (32); // "http://www.moviepilot.de/movies/".length ();
+               } else {
+                       critical ("unknown restful_url prefix: %s", url);
+               }
+
+       //      var countries = object.get_string_member ("countries_list");
+       //      var description = object.get_string_member ("short_description");
+       //      var on_tv = object.get_bool_member ("on_tv");
+       //      var homepage = object.get_string_member ("homepage");
+       //      var long_description = object.get_string_member ("long_description");
+       //      var premiere = object.get_string_member ("premiere_date"); // YYYY-MM-DD
+       //      object.get_member ("alternative_identifiers"); // an object with "service":"id" pairs
+       //      object.get_member ("poster"); // object with copyright, title, height, extension, width, photo_id, base_url, mime_type, size, restful_url and file_name_base properties
+       //      object.get_string_member ("cinema_start_date") // YYYY-MM-DD
+       //      object.get_int_member ("runtime");
+       //      object.get_double_member ("average_critics_rating");
+       //      object.get_string_member ("dvd_start_date");
+
+               // FIXME: cache movie here
+
+               return movie;
+       }
+
+       construct {
+               proxy = new Rest.Proxy (SERVICE_URL, false);
+       }
+}
+
+[DBus (name = "org.maemo.cinaest.MovieService")]
+public class MoviePilotMovieService : Object {
+       private MainLoop loop;
+       private uint source_id;
+       int id = 0;
+
+       // D-Bus API
+
+       public string new_search (DBus.BusName sender) {
+               var path = "/org/maemo/cinaest/moviepilot/search%d".printf (id);
+               var search = new MovieSearch (this, sender, path);
+               search.id = id++;
+               conn.register_object (path, search);
+
+               print ("creating new search %s for %s\n", path, sender);
+               searches.set (path, search);
+
+               return path;
+       }
+
+       public void unregister (string path) {
+               print ("unregistering search %s\n", path);
+
+               searches.remove (path);
+               print ("%d\n", searches.size);
+       }
+
+       // Internal methods
+
+       internal MoviePilotMovieService () {
+               loop = new MainLoop (null);
+       }
+
+       internal 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 ();
+
+                // One-shot only
+                return false;
+        }
+
+       internal void run () {
+               loop.run ();
+       }
+}
+
+void on_client_lost (DBus.Object sender, string name, string prev, string newp) {
+       if (newp == "") {
+               var remove_list = new SList<string> ();
+               // We lost a client
+               print ("lost a client: %s\n", prev);
+               foreach (MovieSearch search in searches.values) {
+                       if (search.sender == prev) {
+                               print ("removing %s\n", search.path);
+                               remove_list.append (search.path);
+                       }
+               }
+               foreach (string path in remove_list)
+                       searches.remove (path);
+       }
+}
+
+void main () {
+       try {
+               conn = DBus.Bus.get (DBus.BusType. SESSION);
+
+               searches = new Gee.HashMap <string, MovieSearch> ();
+
+               dynamic DBus.Object bus = conn.get_object ("org.freedesktop.DBus",
+                                                          "/org/freedesktop/DBus",
+                                                          "org.freedesktop.DBus");
+
+               uint res = bus.request_name ("org.maemo.cinaest.MoviePilot", (uint) 0);
+
+               if (res == DBus.RequestNameReply.PRIMARY_OWNER) {
+                       var server = new MoviePilotMovieService ();
+
+                       conn.register_object ("/org/maemo/cinaest/moviepilot", server);
+
+                       bus.NameOwnerChanged.connect (on_client_lost);
+
+                       server.timeout_quit ();
+                       server.run ();
+               }
+       } catch (Error e) {
+               stderr.printf ("Oops: %s\n", e.message);
+       }
+}
diff --git a/src/plugins/moviepilot-plugin.vala b/src/plugins/moviepilot-plugin.vala
new file mode 100644 (file)
index 0000000..b4baa65
--- /dev/null
@@ -0,0 +1,181 @@
+/* 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 Gtk;
+using Hildon;
+
+class MoviePilotPlugin : Plugin {
+       List<MovieSource> sources;
+
+       public override void hello (Gtk.Window window, Osso.Context context) {
+               stdout.printf ("MoviePilot Plugin Loaded.\n");
+
+               var source = new MoviePilotSource ();
+
+               sources = new List<MovieSource> ();
+               sources.append (source);
+
+               // FIXME - this forces the inclusion of config.h
+               (void) Config.GETTEXT_PACKAGE;
+       }
+
+       public override unowned List<MovieSource> get_sources () {
+               return sources;
+       }
+
+       public override List<MovieAction> get_actions (Movie _movie, Gtk.Window window) {
+               return new List<MovieAction> ();
+       }
+
+       public override void settings_dialog (Gtk.Window window) {
+               MoviePilotSource source = (MoviePilotSource) sources.data;
+               var dialog = new Gtk.Dialog ();
+               dialog.set_transient_for (window);
+               dialog.set_title (_("MoviePilot plugin settings"));
+
+               // Username
+               // Password
+               var hbox = new Gtk.HBox (false, 0);
+               var vbox = new Gtk.VBox (true, 0);
+               var label = new Gtk.Label ("User name");
+               vbox.pack_start (label, true, true, 0);
+               label = new Gtk.Label ("Password");
+               vbox.pack_start (label, true, true, 0);
+               hbox.pack_start (vbox, false, false, 0);
+               vbox = new Gtk.VBox (true, 0);
+               var entry = new Hildon.Entry (SizeType.FINGER_HEIGHT);
+               vbox.pack_start (entry, true, true, 0);
+               entry = new Hildon.Entry (SizeType.FINGER_HEIGHT);
+               vbox.pack_start (entry, true, true, 0);
+               hbox.pack_start (vbox, true, true, 0);
+
+               var content = (VBox) dialog.get_content_area ();
+               content.pack_start (hbox, true, true, 0);
+
+               dialog.add_button (_("Save"), ResponseType.ACCEPT);
+
+               dialog.show_all ();
+               int res = dialog.run ();
+               if (res == ResponseType.ACCEPT) {
+                       /* ... */
+               }
+               dialog.destroy ();
+       }
+
+       public override unowned string get_name () {
+               return "MoviePilot";
+       }
+}
+
+class MoviePilotSource : MovieSource {
+       public string location;
+       public string description;
+       public MovieSource.ReceiveMovieFunction callback;
+       dynamic DBus.Object search;
+
+       public override bool active { get; set construct; }
+
+       public MoviePilotSource () {
+               GLib.Object (active: true);
+       }
+
+       SourceFunc get_movies_callback;
+       public override async int get_movies (MovieFilter filter, MovieSource.ReceiveMovieFunction _callback, int limit, Cancellable? cancellable) {
+               try {
+                       string search_path;
+                       dynamic DBus.Object server;
+                       var conn = DBus.Bus.get (DBus.BusType.SESSION);
+
+                       server = conn.get_object ("org.maemo.cinaest.MoviePilot",
+                                                 "/org/maemo/cinaest/moviepilot",
+                                                 "org.maemo.cinaest.MovieService");
+                       server.NewSearch (out search_path);
+
+                       search = conn.get_object ("org.maemo.cinaest.MoviePilot",
+                                                 search_path,
+                                                 "org.maemo.cinaest.MovieSearch");
+
+                       callback = _callback;
+                       search.MoviesFound.connect (on_movies_found);
+                       print ("get_movies (%s)\n", filter.title);
+                       search.start (filter.title);
+               } catch (Error e1) {
+                       Banner.show_information (null, null, e1.message);
+                       return 0;
+               }
+
+               get_movies_callback = get_movies.callback;
+               if (cancellable != null)
+                       cancellable.cancelled.connect (() => { search.abort (); Idle.add (get_movies_callback); });
+               yield;
+
+               return 1 /* FIXME: n */;
+       }
+
+       private void on_movies_found (DBus.Object sender, string[] movies, bool finished) {
+               print ("found %d movies\n", movies.length);
+               var parser = new Json.Parser ();
+
+               for (int i = 0; i < movies.length; i++) {
+                       var movie = new Movie ();
+                       try {
+                               parser.load_from_data (movies[i], -1);
+                       } catch (Error e) {
+                               stderr.printf ("Error: %s\n%s\n", e.message, movies[i]);
+                       }
+
+                       var object = parser.get_root ().get_object ();
+                       movie.title = object.get_string_member ("title");
+                       movie.year = (int) object.get_int_member ("year");
+                       movie.rating = (int) object.get_double_member ("rating");
+                       movie.secondary = object.get_string_member ("genres").replace (",", ", ");
+
+                       callback (movie);
+               }
+
+               if (finished) {
+                       search = null;
+                       Idle.add (get_movies_callback);
+               }
+       }
+
+       public override void add_movie (Movie movie) {
+       }
+
+       public override void delete_movie (Movie movie) {
+       }
+
+       public override unowned string get_name () {
+               return _("MoviePilot");
+       }
+
+       public override unowned string get_description () {
+               description =  _("Movies on MoviePilot");
+               return description;
+       }
+
+       public override bool get_editable () {
+               return false;
+       }
+}
+
+[ModuleInit]
+public Type register_plugin () {
+       // types are registered automatically
+       return typeof (MoviePilotPlugin);
+}