+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);
+ }
+}