From: Philipp Zabel Date: Mon, 1 Feb 2010 23:27:36 +0000 (+0100) Subject: Add MoviePilot plugin and backend X-Git-Url: http://vcs.maemo.org/git/?a=commitdiff_plain;h=88faa190901c5f61de7dd52ebef8c1dec1204404;p=cinaest Add MoviePilot plugin and backend --- diff --git a/Makefile.am b/Makefile.am index 4b2a35f..4ba24c9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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} diff --git a/configure.ac b/configure.ac index 9612d05..8a44a04 100644 --- a/configure.ac +++ b/configure.ac @@ -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 index 0000000..6567350 --- /dev/null +++ b/data/org.maemo.cinaest.MoviePilot.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.maemo.cinaest.MoviePilot +Exec=@libexecdir@/cinaest-moviepilot-backend diff --git a/po/POTFILES.in b/po/POTFILES.in index ec25323..0247b43 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -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 index 0000000..8964483 --- /dev/null +++ b/src/backends/moviepilot/moviepilot-backend.vala @@ -0,0 +1,327 @@ +using Gee; + +public Gee.HashMap 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 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 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 (); + // 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 (); + + 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 index 0000000..b4baa65 --- /dev/null +++ b/src/plugins/moviepilot-plugin.vala @@ -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 . + */ + +using Gtk; +using Hildon; + +class MoviePilotPlugin : Plugin { + List sources; + + public override void hello (Gtk.Window window, Osso.Context context) { + stdout.printf ("MoviePilot Plugin Loaded.\n"); + + var source = new MoviePilotSource (); + + sources = new List (); + sources.append (source); + + // FIXME - this forces the inclusion of config.h + (void) Config.GETTEXT_PACKAGE; + } + + public override unowned List get_sources () { + return sources; + } + + public override List get_actions (Movie _movie, Gtk.Window window) { + return new List (); + } + + 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); +}