Convert Google plugin into a D-Bus service
authorPhilipp Zabel <philipp.zabel@gmail.com>
Mon, 1 Feb 2010 19:45:31 +0000 (20:45 +0100)
committerPhilipp Zabel <philipp.zabel@gmail.com>
Wed, 14 Jul 2010 21:33:56 +0000 (23:33 +0200)
This will serve as template for other backend services, like MoviePilot or
TheMovieDB.
The backend serves movies to the plugin as JSON objects over D-Bus.
Location configuration is stored in GConf instead of a custom GKeyFile.

Makefile.am
configure.ac
data/org.maemo.cinaest.GoogleShowtimes.service.in [new file with mode: 0644]
debian/cinaest-plugin-google.install
src/backends/google/google-backend.vala [new file with mode: 0644]
src/backends/google/google-parser.vala [new file with mode: 0644]
src/plugins/google-parser.vala [deleted file]
src/plugins/google-plugin.vala

index 50f5063..09a2537 100644 (file)
@@ -14,7 +14,8 @@ lib_LTLIBRARIES = \
 
 libexec_PROGRAMS = \
        google-poster-downloader \
-       imdb-plaintext-downloader
+       imdb-plaintext-downloader \
+       cinaest-google-backend
 
 pkglib_LTLIBRARIES = \
        libcatalog-plugin.la \
@@ -24,6 +25,7 @@ pkglib_LTLIBRARIES = \
 dbusservice_DATA = \
        data/org.maemo.cinaest.service \
        data/org.maemo.cinaest.IMDb.service \
+       data/org.maemo.cinaest.GoogleShowtimes.service \
        data/org.maemo.movieposter.GoogleImages.service
 
 desktopentry_DATA = \
@@ -134,19 +136,18 @@ src/plugins/catalog-plugin.c: ${libcatalog_plugin_la_VALASOURCES}
 libgoogle_plugin_la_SOURCES = \
        src/plugins/google-plugin.c \
        src/plugins/calendar-backend-adapter.cc \
-       src/plugins/calendar-backend.c \
-       src/plugins/google-parser.c
+       src/plugins/calendar-backend.c
 
 libgoogle_plugin_la_VALASOURCES = \
        src/plugins/google-plugin.vala \
-       src/plugins/calendar-backend.vala \
-       src/plugins/google-parser.vala
+       src/plugins/calendar-backend.vala
 
 libgoogle_plugin_la_VALAFLAGS = --vapidir ./vapi --pkg config --pkg cinaest \
-       --pkg hildon-1 --pkg libhildonmime --pkg libosso
-libgoogle_plugin_la_CFLAGS = ${CINAEST_CFLAGS} ${HILDON_CFLAGS} ${HILDONMIME_CFLAGS} ${OSSO_CFLAGS}
+       --pkg dbus-glib-1 --pkg hildon-1 --pkg gconf-2.0 --pkg json-glib-1.0 \
+       --pkg libhildonmime --pkg libosso
+libgoogle_plugin_la_CFLAGS = ${CINAEST_CFLAGS} ${GCONF_CFLAGS} ${HILDON_CFLAGS} ${HILDONMIME_CFLAGS} ${JSON_CFLAGS} ${OSSO_CFLAGS}
 libgoogle_plugin_la_CPPFLAGS = ${CALENDAR_CFLAGS}
-libgoogle_plugin_la_LIBADD = ${CALENDAR_LIBS} ${CINAEST_LIBS} ${HILDON_LIBS} ${HILDONMIME_LIBS} ${OSSO_LIBS}
+libgoogle_plugin_la_LIBADD = ${CALENDAR_LIBS} ${CINAEST_LIBS} ${GCONF_LIBS} ${HILDON_LIBS} ${HILDONMIME_LIBS} ${JSON_LIBS} ${OSSO_LIBS}
 libgoogle_plugin_la_LDFLAGS = -module
 
 src/plugins/google-plugin.c: ${libgoogle_plugin_la_VALASOURCES}
@@ -172,6 +173,22 @@ libimdb_plugin_la_LDFLAGS = -module
 src/plugins/imdb-plugin.c: ${libimdb_plugin_la_VALASOURCES}
        ${VALAC} -C ${libimdb_plugin_la_VALASOURCES} ${libimdb_plugin_la_VALAFLAGS}
 
+cinaest_google_backend_SOURCES = \
+       src/backends/google/google-backend.c \
+       src/backends/google/google-parser.c
+
+cinaest_google_backend_VALASOURCES = \
+       src/backends/google/google-backend.vala \
+       src/backends/google/google-parser.vala
+
+cinaest_google_backend_VALAFLAGS = --vapidir ./vapi --pkg dbus-glib-1 \
+       --pkg gconf-2.0 --pkg gee-1.0 --pkg gio-2.0
+cinaest_google_backend_CFLAGS = ${DBUS_CFLAGS} ${GCONF_CFLAGS} ${GEE_CFLAGS} ${GIO_CFLAGS}
+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}
+
 imdb_plaintext_downloader_SOURCES = \
         src/imdb/imdb-plaintext-downloader.c \
        src/imdb/imdb-ftp-downloader.c \
@@ -220,4 +237,5 @@ CLEANFILES = \
        $(patsubst %.vala,%.c,${libgoogle_plugin_la_VALASOURCES}) \
        ${libimdb_plugin_la_SOURCES} \
        ${imdb_plaintext_downloader_SOURCES} \
-       ${google_poster_downloader_SOURCES}
+       ${google_poster_downloader_SOURCES} \
+       ${cinaest_google_backend_SOURCES}
index 48657ed..9612d05 100644 (file)
@@ -31,6 +31,10 @@ PKG_CHECK_MODULES(CURL, libcurl)
 AC_SUBST(CURL_LIBS)
 AC_SUBST(CURL_CFLAGS)
 
+PKG_CHECK_MODULES(GEE, gee-1.0)
+AC_SUBST(GEE_LIBS)
+AC_SUBST(GEE_CFLAGS)
+
 PKG_CHECK_MODULES(GMODULE, gmodule-2.0 >= 2.20.3)
 AC_SUBST(GMODULE_LIBS)
 AC_SUBST(GMODULE_CFLAGS)
@@ -47,6 +51,10 @@ PKG_CHECK_MODULES(HILDONMIME, libhildonmime >= 2.1.3)
 AC_SUBST(HILDONMIME_LIBS)
 AC_SUBST(HILDONMIME_CFLAGS)
 
+PKG_CHECK_MODULES(JSON, json-glib-1.0)
+AC_SUBST(JSON_LIBS)
+AC_SUBST(JSON_CFLAGS)
+
 AC_ARG_ENABLE([maemo-launcher],
               [AS_HELP_STRING([--enable-maemo-launcher],
                               [build with maemo-launcher support])],
@@ -110,6 +118,7 @@ AC_OUTPUT([
        po/Makefile
        data/org.maemo.cinaest.service
        data/org.maemo.cinaest.IMDb.service
+       data/org.maemo.cinaest.GoogleShowtimes.service
        data/org.maemo.movieposter.GoogleImages.service
        data/cinaest.desktop
 ])
diff --git a/data/org.maemo.cinaest.GoogleShowtimes.service.in b/data/org.maemo.cinaest.GoogleShowtimes.service.in
new file mode 100644 (file)
index 0000000..0035661
--- /dev/null
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.maemo.cinaest.GoogleShowtimes
+Exec=@libexecdir@/cinaest-google-backend
index 5937ff2..a456b12 100644 (file)
@@ -1 +1,3 @@
 usr/lib/cinaest/libgoogle-plugin.so*
+usr/share/dbus-1/services/org.maemo.cinaest.GoogleShowtimes.service
+usr/libexec/cinaest-google-backend
diff --git a/src/backends/google/google-backend.vala b/src/backends/google/google-backend.vala
new file mode 100644 (file)
index 0000000..6a375b8
--- /dev/null
@@ -0,0 +1,189 @@
+using Gee;
+
+public Gee.HashMap <string, MovieSearch> searches;
+public DBus.Connection conn;
+
+[DBus (name = "org.maemo.cinaest.MovieSearch", signals="movies_found")]
+public class MovieSearch : Object {
+       public int id;
+       bool aborted;
+       string title;
+       GoogleParser parser;
+       GLib.List<GoogleMovie> results;
+       GoogleMovieService service;
+
+       // D-Bus API
+
+       public void abort () {
+               print ("aborting\n");
+               aborted = true;
+       }
+
+       public void start (string query) {
+               parser = new GoogleParser ();
+               title = query;
+
+               query_async.begin ();
+       }
+
+       public signal void movies_found (string[] movies, bool finished);
+
+       // Internal methods
+
+       internal MovieSearch (GoogleMovieService _service) {
+               service = _service;
+       }
+
+       private async void query_async () {
+               var gc = GConf.Client.get_default ();
+
+               results = new GLib.List<GoogleMovie> ();
+
+               string location = null;
+               try {
+                       location = gc.get_string ("/apps/cinaest/location");
+               } catch (Error e) {
+                       stdout.printf ("Error getting GConf option: %s\n", e.message);
+               }
+               if (location == null)
+                       location = "";
+
+               int n = yield parser.query (title, location, receive_movie);
+
+               try {
+                       var locations = ((SList<string>) gc.get_list ("/apps/cinaest/locations", GConf.ValueType.STRING)).copy ();
+                       if (insert_location_sorted (parser.location, ref locations)) {
+                               gc.set_list ("/apps/cinaest/locations", GConf.ValueType.STRING, locations);
+                       }
+                       if (location != parser.location) {
+                               location = parser.location;
+                               gc.set_string ("/apps/cinaest/location", location);
+                       }
+               } catch (Error e2) {
+                       stdout.printf ("GConf error: %s\n", e2.message);
+               }
+
+               print ("got %d movies\n", n);
+
+               var m = new string[results.length ()];
+               int i = 0;
+               for (unowned GLib.List<GoogleMovie> node = results.first (); node != null; node = node.next) {
+                       m[i++] = "{\"title\":\"%s\",\"rating\":%f,\"showtimes\":\"%s\",\"cinema_name\":\"%s\",\"cinema_phone\":\"%s\"}".printf (node.data.title, node.data.rating, node.data.showtimes, node.data.cinema.name, node.data.cinema.phone);
+               }
+               movies_found (m, true);
+               service.timeout_quit ();
+       }
+
+       private bool insert_location_sorted (string? location, ref SList<string> locations) {
+               if (location == null)
+                       return false;
+               if (locations != null) {
+                       for (unowned SList<string> l = locations; l != null; l = l.next) {
+                               if (l.data == location) {
+                                       return false;
+                               }
+                               if (l.data > location) {
+                                       l.insert (location, 0);
+                                       return true;
+                               }
+                       }
+               }
+               locations.append (location);
+               return true;
+       }
+
+       private void receive_movie (GoogleMovie movie) {
+       //      print ("received %s\n", movie.title);
+               results.append (movie);
+       }
+}
+
+[DBus (name = "org.maemo.cinaest.MovieService")]
+public class GoogleMovieService : Object {
+       private MainLoop loop;
+       private uint source_id;
+
+       // D-Bus API
+
+       public string new_search (DBus.BusName sender) {
+               print ("new search requested by %s\n", sender);
+               var search = new MovieSearch (this);
+
+               search.id = searches.size;
+               string path = "/org/maemo/cinaest/googleshowtimes/search%d".printf (search.id);
+               conn.register_object (path, search);
+
+               searches.set (path, search);
+
+               return path;
+       }
+
+       public void unregister (string path) {
+
+       print ("unregistering %s\n", path);
+               searches.remove (path);
+               print ("%d\n", searches.size);
+       }
+
+       // Internal methods
+
+       internal GoogleMovieService () {
+               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 == "") {
+               // We lost a client
+               print ("lost a client: prev:%s\n", prev);
+               searches.remove (prev);
+       }
+}
+
+void main () {
+       var loop = new MainLoop (null, false);
+
+       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.GoogleShowtimes", (uint) 0);
+
+               if (res == DBus.RequestNameReply.PRIMARY_OWNER) {
+                       var server = new GoogleMovieService ();
+
+                       conn.register_object ("/org/maemo/cinaest/movies", 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/backends/google/google-parser.vala b/src/backends/google/google-parser.vala
new file mode 100644 (file)
index 0000000..886772d
--- /dev/null
@@ -0,0 +1,391 @@
+/* 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/>.
+ */
+
+errordomain ParserError {
+       WRONG_TAG,
+       EOF
+}
+
+public class Cinema {
+       public string name;
+       public string address;
+       public string phone;
+
+       public Cinema (string _name) {
+               name = _name;
+       }
+}
+
+public class GoogleMovie {
+       public string title;
+       public int rating;
+       public string secondary;
+       public Cinema cinema;
+       public string runtime;
+       public string fsk;
+       public string showtimes;
+}
+
+public class GoogleParser : Object {
+       char *current;
+       Cinema last_cinema;
+       public string location;
+       string _title;
+       PatternSpec pattern;
+
+       public delegate void ReceiveMovie (GoogleMovie movie);
+       public ReceiveMovie _get_callback;
+
+       public int next_tag_offset () {
+               int i = -1;
+               while (current[++i] != '<' && current[i] != 0);
+               return i;
+       }
+
+       public void next_tag () {
+               if (current[0] == 0)
+                       return;
+               current += next_tag_offset ();
+       }
+
+       public void finish_tag () {
+               while (current[0] != '>' && current[0] != 0)
+                       current++;
+               if (current[0] == '>')
+                       current++;
+       }
+
+       public weak string parse_tag (bool finish = true) throws Error {
+               weak string tag;
+               next_tag ();
+               int i = 1;
+               while (current[++i].isalnum ());
+               if (current[i] == 0)
+                       throw new ParserError.EOF ("EOF in tag");
+               if (current[i] == '>')
+                       finish = false;
+               current[i] = 0;
+               tag = (string) (current + 1);
+               current += i + 1;
+               if (finish)
+                       finish_tag ();
+               return tag;
+       }
+
+       public void expect_tag (string tag) throws Error {
+               var found = parse_tag (true);
+               if (tag != found) {
+                       throw new ParserError.WRONG_TAG ("Wrong tag \"%s\", expected \"%s\"",
+                                                        found, tag);
+               }
+       }
+
+       public string parse_text () {
+               string text = ((string) current).ndup (next_tag_offset ());
+               next_tag ();
+               return text;
+       }
+
+       public void parse_attribute (string _attr, out string value) {
+               string attr;
+               if (current[0] == 0)
+                       return;
+               int i = -1;
+               while (current[++i] != '=' && current[i] != '>' && current[i] != 0) {
+
+               }
+               attr = ((string) current).ndup (i);
+               current += i;
+               if (current[0] == 0)
+                       return;
+               current++;
+               i = -1;
+               while (!current[++i].isspace () && current[i] != '>' && current[i] != 0) {
+                       if (current[i] == '"')
+                               while (current[++i] != '"' && current[i] != 0);
+               }
+               if (attr == _attr) {
+                       if (current[0] == '"')
+                               value = ((string) current).substring (1, i - 2);
+                       else
+                               value = ((string) current).ndup (i);
+               }
+               current += i;
+       }
+
+       public void skip_whitespace () {
+               if (current[0] == 0)
+                       return;
+               int i = -1;
+               while (current[++i].isspace () && current[i] != 0);
+               current += i;
+       }
+
+       public string? parse_tag_attribute (string tag, string attribute) throws Error {
+               var found = parse_tag (false);
+               if (tag != found) {
+                       throw new ParserError.WRONG_TAG ("Wrong tag \"%s\", expected \"%s\"",
+                                                        found, tag);
+               }
+
+               string? value = null;
+               skip_whitespace ();
+               while (current[0] != '>' && current[0] != 0) {
+                       parse_attribute (attribute, out value);
+                       skip_whitespace ();
+               }
+               // Skip the closing '>' bracket
+               if (current[0] != 0)
+                       current++;
+
+               return value;
+       }
+
+       public string unescape_unicode (string s) {
+               string result = "";
+               int i, j;
+               long l = s.length;
+
+               for (i = 0; i < l; i++) {
+                       if (s[i] == '&' && s[i + 1] == '#') {
+                               for (j = i + 2; j < l; j++) {
+                                       if (!s[j].isdigit ())
+                                               break;
+                                       if (s[j] == ';')
+                                               break;
+                               }
+                               if (s[j] == ';') {
+                                       int codepoint = s.substring (i + 2, j - i - 2).to_int ();
+                                       char[] buf = new char[6];
+                                       ((unichar) codepoint).to_utf8 ((string) buf);
+                                       result += (string) buf;
+                                       i = j;
+                                       continue;
+                               }
+                       }
+                       if (s.offset (i).has_prefix ("&amp;")) {
+                               result += "&";
+                               i += 4;
+                               continue;
+                       }
+                       if (s.offset (i).has_prefix ("&quot;")) {
+                               result += "\"";
+                               i += 5;
+                               continue;
+                       }
+                       result += s.substring (i, 1);
+               }
+
+               return result;
+       }
+
+       public void parse_movie () throws Error {
+               expect_tag ("div"); // class=movie
+               expect_tag ("div"); // class=name
+               expect_tag ("a"); // href="/movies?near=city&amp;mid=..."
+               expect_tag ("span"); // dir=ltr
+               var title = unescape_unicode (convert (parse_text (), -1, "utf-8", "iso-8859-1")); // FIXME
+               expect_tag ("/span");
+               expect_tag ("/a");
+               expect_tag ("/div");
+               expect_tag ("span"); // class=info
+               string[] runtime_and_fsk = {};
+               double rating = 0.0;
+               var tag = parse_tag ();
+               if (tag == "a") {
+                       // Trailer
+                       expect_tag ("/a");
+                       tag = parse_tag ();
+               }
+               if (tag == "a") {
+                       // IMDb
+                       expect_tag ("/a");
+                       tag = parse_tag ();
+               }
+               if (tag == "nobr") {
+                       expect_tag ("nobr");
+                       string rating_string = parse_tag_attribute ("img", "alt").offset (6); // "Rated " ->"0.0 out of 5.0"
+                       rating = rating_string.to_double ();
+                       expect_tag ("img");
+                       expect_tag ("img");
+                       expect_tag ("img");
+                       expect_tag ("img");
+                       expect_tag ("/nobr");
+                       expect_tag ("/nobr");
+                       runtime_and_fsk = parse_text ().replace ("&#8206;", "").offset (3).split (" - ");
+                       if (parse_tag () == "a") {
+                               // Trailer
+                               expect_tag ("/a");
+                               if (parse_tag () == "a") {
+                                       // IMDb link
+                                       expect_tag ("/a");
+                                       expect_tag ("/span");
+                               }
+                       }
+               }
+               expect_tag ("div"); // class=times
+               var showtimes = parse_text ().replace ("&nbsp;", ",");
+               while (parse_tag () == "a") {
+                       showtimes += parse_text () + ",";
+                       expect_tag ("/a");
+               }
+
+               if (pattern == null) {
+                       if (!title.has_prefix (_title))
+                               return;
+               } else {
+                       if (!pattern.match ((uint) title.length, title, null))
+                               return;
+               }
+
+               var movie = new GoogleMovie ();
+
+               movie.title = strip_tags (title).replace ("\"", "\\\"");
+               movie.rating = (int) (rating * 10);
+
+               movie.cinema = last_cinema;
+               if (runtime_and_fsk.length >= 2) {
+                       movie.runtime = runtime_and_fsk[0];
+                       movie.fsk = runtime_and_fsk[1];
+               }
+               movie.showtimes = showtimes;
+
+               // TODO - could be configurable by settings
+               if (movie.runtime != null)
+                       movie.secondary = "%s - %s - %s".printf (movie.runtime, last_cinema.name, showtimes);
+               else
+                       movie.secondary = "%s - %s".printf (last_cinema.name, showtimes);
+
+               _get_callback (movie);
+       }
+
+       // FIXME - this is specific for Germany
+       private string strip_tags (string title) {
+               string tag_suffix = " (OmU)"; // original audio with subtitles
+               if (title.has_suffix (tag_suffix))
+                       return title.substring (0, title.length - tag_suffix.length);
+               tag_suffix = " (OV)"; // original audio
+               if (title.has_suffix (tag_suffix))
+                       return title.substring (0, title.length - tag_suffix.length);
+               return title.dup ();
+       }
+
+       public void parse_cinema () throws Error {
+               expect_tag ("div"); // class=theater
+               expect_tag ("div"); // class=desc id=theater_...
+               expect_tag ("h2"); // class=name
+               expect_tag ("a"); // href="/movies?near=city&amp;tid=..."
+               expect_tag ("span"); // dir=ltr
+               var name = unescape_unicode (convert (parse_text (), -1, "utf-8", "iso-8859-1")); // FIXME
+               expect_tag ("/span");
+               expect_tag ("/a");
+               expect_tag ("/h2");
+               expect_tag ("div"); // class=info
+               var address_and_phone = parse_text ().replace ("&nbsp;", " ").split (" - ");
+               string address = null;
+               string phone = null;
+               if (address_and_phone.length >= 2) {
+                       address = address_and_phone[0];
+                       phone = address_and_phone[1].replace (" ", "").replace ("-", "");
+               }
+               expect_tag ("a"); // target=_top
+               expect_tag ("/a");
+               expect_tag ("/div");
+               expect_tag ("/div");
+
+               last_cinema = new Cinema (name);
+               last_cinema.address = address;
+               last_cinema.phone = phone;
+       }
+
+       public int parse (ref char[] buf) throws Error {
+               int movies = 0;
+
+               current = buf;
+               next_tag ();
+               while (location == null && current[0] != 0) {
+                       int i = 1;
+                       while (current[i++] != '>');
+                       if (((string) current).has_prefix ("<a href=\"/movies?near=")) {
+                               string href = parse_tag_attribute ("a", "href");
+                               char* p = (char*) href.offset (13); // skip "/movies?near="
+                               int j = -1;
+
+                               while (p[++j] != '&' && p[j] != 0);
+                               p[0] = p[0].toupper ();
+                               location = ((string) p).ndup (j);
+                       }
+                       current += i;
+                       next_tag ();
+               }
+               while (current[0] != 0) {
+                       int i = 1;
+                       while (current[i++] != '>');
+                       if (((string) current).has_prefix ("<div class=movie>")) {
+                               parse_movie ();
+                               movies++;
+                       } else if (((string) current).has_prefix("<div class=theater>")) {
+                               parse_cinema ();
+                       } else {
+                               current += i;
+                       }
+                       next_tag ();
+               }
+
+               return movies;
+       }
+
+       public async int query (string title, string? location, ReceiveMovie callback, Cancellable? cancellable = null) {
+               _get_callback = callback;
+               _title = title;
+               if (title.chr(title.length, '*') != null) {
+                       pattern = new PatternSpec (title);
+               } else {
+                       pattern = null;
+               }
+               try {
+                       // TODO - use google.de in Germany, also provides genres
+                       string uri = "http://google.com/movies";
+                       if (location != null && location != "")
+                               uri += "?near=" + location;
+
+                       stdout.printf ("GET: %s\n", uri);
+
+                       File file = File.new_for_uri (uri);
+                       InputStream stream = yield file.read_async (Priority.DEFAULT_IDLE, null);
+
+                       char[] buf = new char[256*1024];
+                       size_t nread;
+                       size_t total = 0;
+                       while (total < 256*1024) {
+                               nread = yield stream.read_async ((char *)buf + total, 256*1024 - total, Priority.DEFAULT_IDLE, cancellable);
+                               total += nread;
+                               if (cancellable.is_cancelled ())
+                                       return 0;
+                               if (nread == 0)
+                                       break;
+                       }
+                       buf[total] = 0;
+                       return parse (ref buf);
+               } catch (Error e) {
+                       stderr.printf ("Error: %s\n", e.message);
+               }
+
+               return 0;
+       }
+}
diff --git a/src/plugins/google-parser.vala b/src/plugins/google-parser.vala
deleted file mode 100644 (file)
index cc5a0de..0000000
+++ /dev/null
@@ -1,387 +0,0 @@
-/* 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/>.
- */
-
-errordomain ParserError {
-       WRONG_TAG,
-       EOF
-}
-
-public class Cinema {
-       public string name;
-       public string address;
-       public string phone;
-
-       public Cinema (string _name) {
-               name = _name;
-       }
-}
-
-public class GoogleMovie : Movie {
-       public Cinema cinema;
-       public string runtime;
-       public string fsk;
-       public string showtimes;
-}
-
-public class GoogleParser : Object {
-       private MovieSource.ReceiveMovieFunction _get_callback;
-       char *current;
-       Cinema last_cinema;
-       public string location;
-       MovieFilter _filter;
-       PatternSpec pattern;
-
-       public int next_tag_offset () {
-               int i = -1;
-               while (current[++i] != '<' && current[i] != 0);
-               return i;
-       }
-
-       public void next_tag () {
-               if (current[0] == 0)
-                       return;
-               current += next_tag_offset ();
-       }
-
-       public void finish_tag () {
-               while (current[0] != '>' && current[0] != 0)
-                       current++;
-               if (current[0] == '>')
-                       current++;
-       }
-
-       public weak string parse_tag (bool finish = true) throws Error {
-               weak string tag;
-               next_tag ();
-               int i = 1;
-               while (current[++i].isalnum ());
-               if (current[i] == 0)
-                       throw new ParserError.EOF ("EOF in tag");
-               if (current[i] == '>')
-                       finish = false;
-               current[i] = 0;
-               tag = (string) (current + 1);
-               current += i + 1;
-               if (finish)
-                       finish_tag ();
-               return tag;
-       }
-
-       public void expect_tag (string tag) throws Error {
-               var found = parse_tag (true);
-               if (tag != found) {
-                       throw new ParserError.WRONG_TAG ("Wrong tag \"%s\", expected \"%s\"",
-                                                        found, tag);
-               }
-       }
-
-       public string parse_text () {
-               string text = ((string) current).ndup (next_tag_offset ());
-               next_tag ();
-               return text;
-       }
-
-       public void parse_attribute (string _attr, out string value) {
-               string attr;
-               if (current[0] == 0)
-                       return;
-               int i = -1;
-               while (current[++i] != '=' && current[i] != '>' && current[i] != 0) {
-                       
-               }
-               attr = ((string) current).ndup (i);
-               current += i;
-               if (current[0] == 0)
-                       return;
-               current++;
-               i = -1;
-               while (!current[++i].isspace () && current[i] != '>' && current[i] != 0) {
-                       if (current[i] == '"')
-                               while (current[++i] != '"' && current[i] != 0);
-               }
-               if (attr == _attr) {
-                       if (current[0] == '"')
-                               value = ((string) current).substring (1, i - 2);
-                       else
-                               value = ((string) current).ndup (i);
-               }
-               current += i;
-       }
-
-       public void skip_whitespace () {
-               if (current[0] == 0)
-                       return;
-               int i = -1;
-               while (current[++i].isspace () && current[i] != 0);
-               current += i;
-       }
-
-       public string? parse_tag_attribute (string tag, string attribute) throws Error {
-               var found = parse_tag (false);
-               if (tag != found) {
-                       throw new ParserError.WRONG_TAG ("Wrong tag \"%s\", expected \"%s\"",
-                                                        found, tag);
-               }
-
-               string? value = null;
-               skip_whitespace ();
-               while (current[0] != '>' && current[0] != 0) {
-                       parse_attribute (attribute, out value);
-                       skip_whitespace ();
-               }
-               // Skip the closing '>' bracket
-               if (current[0] != 0)
-                       current++;
-
-               return value;
-       }
-
-       public string unescape_unicode (string s) {
-               string result = "";
-               int i, j;
-               long l = s.length;
-
-               for (i = 0; i < l; i++) {
-                       if (s[i] == '&' && s[i + 1] == '#') {
-                               for (j = i + 2; j < l; j++) {
-                                       if (!s[j].isdigit ())
-                                               break;
-                                       if (s[j] == ';')
-                                               break;
-                               }
-                               if (s[j] == ';') {
-                                       int codepoint = s.substring (i + 2, j - i - 2).to_int ();
-                                       char[] buf = new char[6];
-                                       ((unichar) codepoint).to_utf8 ((string) buf);
-                                       result += (string) buf;
-                                       i = j;
-                                       continue;
-                               }
-                       }
-                       if (s.offset (i).has_prefix ("&amp;")) {
-                               result += "&";
-                               i += 4;
-                               continue;
-                       }
-                       if (s.offset (i).has_prefix ("&quot;")) {
-                               result += "\"";
-                               i += 5;
-                               continue;
-                       }
-                       result += s.substring (i, 1);
-               }
-
-               return result;
-       }
-
-       public void parse_movie () throws Error {
-               expect_tag ("div"); // class=movie
-               expect_tag ("div"); // class=name
-               expect_tag ("a"); // href="/movies?near=city&amp;mid=..."
-               expect_tag ("span"); // dir=ltr
-               var title = unescape_unicode (convert (parse_text (), -1, "utf-8", "iso-8859-1")); // FIXME
-               expect_tag ("/span");
-               expect_tag ("/a");
-               expect_tag ("/div");
-               expect_tag ("span"); // class=info
-               string[] runtime_and_fsk = {};
-               double rating = 0.0;
-               var tag = parse_tag ();
-               if (tag == "a") {
-                       // Trailer
-                       expect_tag ("/a");
-                       tag = parse_tag ();
-               }
-               if (tag == "a") {
-                       // IMDb
-                       expect_tag ("/a");
-                       tag = parse_tag ();
-               }
-               if (tag == "nobr") {
-                       expect_tag ("nobr");
-                       string rating_string = parse_tag_attribute ("img", "alt").offset (6); // "Rated " ->"0.0 out of 5.0"
-                       rating = rating_string.to_double ();
-                       expect_tag ("img");
-                       expect_tag ("img");
-                       expect_tag ("img");
-                       expect_tag ("img");
-                       expect_tag ("/nobr");
-                       expect_tag ("/nobr");
-                       runtime_and_fsk = parse_text ().replace ("&#8206;", "").offset (3).split (" - ");
-                       if (parse_tag () == "a") {
-                               // Trailer
-                               expect_tag ("/a");
-                               if (parse_tag () == "a") {
-                                       // IMDb link
-                                       expect_tag ("/a");
-                                       expect_tag ("/span");
-                               }
-                       }
-               }
-               expect_tag ("div"); // class=times
-               var showtimes = parse_text ().replace ("&nbsp;", ",");
-               while (parse_tag () == "a") {
-                       showtimes += parse_text () + ",";
-                       expect_tag ("/a");
-               }
-
-               if (pattern == null) {
-                       if (!title.has_prefix (_filter.title))
-                               return;
-               } else {
-                       if (!pattern.match ((uint) title.length, title, null))
-                               return;
-               }
-
-               var movie = new GoogleMovie ();
-
-               movie.title = strip_tags (title);
-               movie.year = 0;
-               movie.rating = (int) (rating * 10);
-
-               movie.cinema = last_cinema;
-               if (runtime_and_fsk.length >= 2) {
-                       movie.runtime = runtime_and_fsk[0];
-                       movie.fsk = runtime_and_fsk[1];
-               }
-               movie.showtimes = showtimes;
-
-               // TODO - could be configurable by settings
-               if (movie.runtime != null)
-                       movie.secondary = "%s - %s - %s".printf (movie.runtime, last_cinema.name, showtimes);
-               else
-                       movie.secondary = "%s - %s".printf (last_cinema.name, showtimes);
-
-               _get_callback (movie);
-       }
-
-       // FIXME - this is specific for Germany
-       private string strip_tags (string title) {
-               string tag_suffix = " (OmU)"; // original audio with subtitles
-               if (title.has_suffix (tag_suffix))
-                       return title.substring (0, title.length - tag_suffix.length);
-               tag_suffix = " (OV)"; // original audio
-               if (title.has_suffix (tag_suffix))
-                       return title.substring (0, title.length - tag_suffix.length);
-               return title.dup ();
-       }
-
-       public void parse_cinema () throws Error {
-               expect_tag ("div"); // class=theater
-               expect_tag ("div"); // class=desc id=theater_...
-               expect_tag ("h2"); // class=name
-               expect_tag ("a"); // href="/movies?near=city&amp;tid=..."
-               expect_tag ("span"); // dir=ltr
-               var name = unescape_unicode (convert (parse_text (), -1, "utf-8", "iso-8859-1")); // FIXME
-               expect_tag ("/span");
-               expect_tag ("/a");
-               expect_tag ("/h2");
-               expect_tag ("div"); // class=info
-               var address_and_phone = parse_text ().replace ("&nbsp;", " ").split (" - ");
-               string address = null;
-               string phone = null;
-               if (address_and_phone.length >= 2) {
-                       address = address_and_phone[0];
-                       phone = address_and_phone[1].replace (" ", "").replace ("-", "");
-               }
-               expect_tag ("a"); // target=_top
-               expect_tag ("/a");
-               expect_tag ("/div");
-               expect_tag ("/div");
-
-               last_cinema = new Cinema (name);
-               last_cinema.address = address;
-               last_cinema.phone = phone;
-       }
-
-       public int parse (ref char[] buf) throws Error {
-               int movies = 0;
-
-               current = buf;
-               next_tag ();
-               while (location == null && current[0] != 0) {
-                       int i = 1;
-                       while (current[i++] != '>');
-                       if (((string) current).has_prefix ("<a href=\"/movies?near=")) {
-                               string href = parse_tag_attribute ("a", "href");
-                               char* p = (char*) href.offset (13); // skip "/movies?near="
-                               int j = -1;
-
-                               while (p[++j] != '&' && p[j] != 0);
-                               p[0] = p[0].toupper ();
-                               location = ((string) p).ndup (j);
-                       }
-                       current += i;
-                       next_tag ();
-               }
-               while (current[0] != 0) {
-                       int i = 1;
-                       while (current[i++] != '>');
-                       if (((string) current).has_prefix ("<div class=movie>")) {
-                               parse_movie ();
-                               movies++;
-                       } else if (((string) current).has_prefix("<div class=theater>")) {
-                               parse_cinema ();
-                       } else {
-                               current += i;
-                       }
-                       next_tag ();
-               }
-
-               return movies;
-       }
-
-       public async int query (MovieFilter filter, string? location, MovieSource.ReceiveMovieFunction callback, Cancellable? cancellable) {
-               _get_callback = callback;
-               _filter = filter;
-               if (filter.title.chr(filter.title.length, '*') != null) {
-                       pattern = new PatternSpec (filter.title);
-               } else {
-                       pattern = null;
-               }
-               try {
-                       // TODO - use google.de in Germany, also provides genres
-                       string uri = "http://google.com/movies";
-                       if (location != null && location != "")
-                               uri += "?near=" + location;
-
-                       stdout.printf ("GET: %s\n", uri);
-
-                       File file = File.new_for_uri (uri);
-                       InputStream stream = yield file.read_async (Priority.DEFAULT_IDLE, null);
-
-                       char[] buf = new char[256*1024];
-                       size_t nread;
-                       size_t total = 0;
-                       while (total < 256*1024) {
-                               nread = yield stream.read_async ((char *)buf + total, 256*1024 - total, Priority.DEFAULT_IDLE, cancellable);
-                               total += nread;
-                               if (cancellable.is_cancelled ())
-                                       return 0;
-                               if (nread == 0)
-                                       break;
-                       }
-                       buf[total] = 0;
-                       return parse (ref buf);
-               } catch (Error e) {
-                       stderr.printf ("Error: %s\n", e.message);
-               }
-
-               return 0;
-       }
-}
index b37a36e..5491a6d 100644 (file)
 using Gtk;
 using Hildon;
 
+public class GoogleMovie : Movie {
+       public string cinema_name;
+       public string cinema_phone;
+       public string runtime;
+       public string showtimes;
+}
+
 class GooglePlugin : Plugin {
        List<MovieSource> sources;
-       List<string> locations;
+       SList<string> locations;
        string last_location;
+       GConf.Client gconf;
 
        public override void hello (Gtk.Window window, Osso.Context context) {
                stdout.printf ("Google Plugin Loaded.\n");
@@ -33,23 +41,12 @@ class GooglePlugin : Plugin {
                sources.append (source);
 
                locations = null;
+               gconf = GConf.Client.get_default ();
                try {
-                       var config_file = Path.build_filename (Environment.get_user_config_dir (), "cinaest", "cinaest.cfg");
-                       var keyfile = new KeyFile ();
-                       if (keyfile.load_from_file (config_file, KeyFileFlags.NONE)
-                           && keyfile.has_group ("GooglePlugin")) {
-                               if (keyfile.has_key ("GooglePlugin", "KnownLocations")) {
-                                       var l = keyfile.get_string_list ("GooglePlugin", "KnownLocations");
-                                       for (int i = 0; i < l.length; i++)
-                                               locations.append (l[i]);
-                               }
-                               if (keyfile.has_key ("GooglePlugin", "LastLocation")) {
-                                       source.location = last_location = keyfile.get_string ("GooglePlugin", "LastLocation");
-                               }
-                       }
+                       source.location = last_location = gconf.get_string ("/apps/cinaest/location");
+                       locations = gconf.get_list ("/apps/cinaest/locations", GConf.ValueType.STRING);
                } catch (Error e) {
-                       if (!(e is KeyFileError.NOT_FOUND))
-                               stdout.printf ("Error loading configuration: %s\n", e.message);
+                       stdout.printf ("Error getting GConf option: %s\n", e.message);
                }
 
                // FIXME - this forces the inclusion of config.h
@@ -66,7 +63,7 @@ class GooglePlugin : Plugin {
 
                if (movie != null) {
                        list.append (new MovieAction (_("Add to calendar"), on_add_calendar_event, movie, window));
-                       if (movie.cinema != null && movie.cinema.phone != null)
+                       if (movie.cinema_phone != null)
                                list.append (new MovieAction (_("Call cinema"), on_call_cinema, movie, window));
                }
 
@@ -105,7 +102,7 @@ class GooglePlugin : Plugin {
                                runtime = 7200;
                        }
 
-                       res = Calendar.add_event (movie.title, _("Movie"), movie.cinema.name, showtime, showtime + runtime);
+                       res = Calendar.add_event (movie.title, _("Movie"), movie.cinema_name, showtime, showtime + runtime);
                        var banner = (Banner) Banner.show_information_with_markup (window, null, (res == 0) ?
                                                                                   _("Added calendar event at %d:%02d").printf (hour, min) :
                                                                                   _("Failed to add calendar event"));
@@ -116,7 +113,7 @@ class GooglePlugin : Plugin {
 
        private void on_call_cinema (Movie _movie, Gtk.Window window) {
                var movie = (GoogleMovie) _movie;
-               var url = "tel://" + movie.cinema.phone;
+               var url = "tel://" + movie.cinema_phone;
 
                try {
                        var action = Hildon.URIAction.get_default_action_by_uri (url);
@@ -138,6 +135,12 @@ class GooglePlugin : Plugin {
                var dialog = new Gtk.Dialog ();
                dialog.set_transient_for (window);
                dialog.set_title (_("Google plugin settings"));
+               try {
+                       source.location = gconf.get_string ("/apps/cinaest/location");
+                       locations = gconf.get_list ("/apps/cinaest/locations", GConf.ValueType.STRING);
+               } catch (Error e) {
+                       stdout.printf ("Error getting GConf option: %s\n", e.message);
+               }
 
                var selector = new TouchSelectorEntry.text ();
                insert_location_sorted (source.location);
@@ -158,38 +161,13 @@ class GooglePlugin : Plugin {
                int res = dialog.run ();
                if (res == ResponseType.ACCEPT) {
                        source.location = button.get_value ();
-                       if (insert_location_sorted (source.location) || source.location != last_location) {
-                               var config_dir = Path.build_filename (Environment.get_user_config_dir (), "cinaest");
-                               var config_file = Path.build_filename (config_dir, "cinaest.cfg");
-
-                               // Make sure the directory is available
-                               DirUtils.create_with_parents (config_dir, 0770);
-
-                               var keyfile = new KeyFile ();
-                               try {
-                                       keyfile.load_from_file (config_file, KeyFileFlags.NONE);
-                               } catch (Error e) {
-                                       if (!(e is KeyFileError.NOT_FOUND))
-                                               stdout.printf ("Error loading configuration: %s\n", e.message);
-                               }
-                               var l = new string[locations.length ()];
-                               for (int i = 0; i < l.length; i++) {
-                                       l[i] = locations.nth_data (i);
-                               }
-                               keyfile.set_string_list ("GooglePlugin", "KnownLocations", l);
-                               keyfile.set_string ("GooglePlugin", "LastLocation", source.location);
-                               last_location = source.location;
-
-                               try {
-                                       var file = File.new_for_path (config_file + ".part");
-                                       var stream = file.create (FileCreateFlags.REPLACE_DESTINATION, null);
-                                       var data = keyfile.to_data ();
-
-                                       stream.write (data, data.length, null);
-                                       FileUtils.rename (config_file + ".part", config_file);
-                               } catch (Error e) {
-                                       stdout.printf ("Failed to store configuration: %s\n", e.message);
-                               }
+                       try {
+                               if (insert_location_sorted (source.location))
+                                       gconf.set_list ("/apps/cinaest/locations", GConf.ValueType.STRING, locations);
+                               if (source.location != last_location)
+                                       gconf.set_string ("/apps/cinaest/location", source.location);
+                       } catch (Error e) {
+                               stdout.printf ("Error setting GConf option: %s\n", e.message);
                        }
                }
                dialog.destroy ();
@@ -199,7 +177,7 @@ class GooglePlugin : Plugin {
                if (location == null)
                        return false;
                if (locations != null) {
-                       for (unowned List<string> l = locations.first (); l != null; l = l.next) {
+                       for (unowned SList<string> l = locations; l != null; l = l.next) {
                                if (l.data == location) {
                                        return false;
                                }
@@ -221,6 +199,8 @@ class GooglePlugin : Plugin {
 class GoogleSource : MovieSource {
        public string location;
        public string description;
+       public MovieSource.ReceiveMovieFunction callback;
+       dynamic DBus.Object search;
 
        public override bool active { get; set construct; }
 
@@ -228,15 +208,66 @@ class GoogleSource : MovieSource {
                GLib.Object (active: true);
        }
 
-       public override async int get_movies (MovieFilter filter, MovieSource.ReceiveMovieFunction callback, int limit, Cancellable? cancellable) {
-               var parser = new GoogleParser ();
+       SourceFunc get_movies_callback;
+       public override async int get_movies (MovieFilter filter, MovieSource.ReceiveMovieFunction _callback, int limit, Cancellable? cancellable) {
+               var conn = DBus.Bus.get (DBus.BusType.SESSION);
+               string search_path;
+
+               dynamic DBus.Object server = conn.get_object ("org.maemo.cinaest.GoogleShowtimes",
+                                                             "/org/maemo/cinaest/googleshowtimes",
+                                                             "org.maemo.cinaest.MovieService");
+               server.NewSearch (out search_path);
+
+               search = conn.get_object ("org.maemo.cinaest.GoogleShowtimes",
+                                         search_path,
+                                         "org.maemo.cinaest.MovieSearch");
+
+               callback = _callback;
+               search.MoviesFound += on_movies_found;
+               search.start (filter.title);
+
+               get_movies_callback = get_movies.callback;
+               if (cancellable != null)
+                       cancellable.cancelled.connect (() => { search.abort (); Idle.add (get_movies_callback); });
+               yield;
+
+               var gc = GConf.Client.get_default ();
+               try {
+                       location = gc.get_string ("/apps/cinaest/location");
+               } catch (Error e) {
+                       stdout.printf ("Error getting GConf option: %s\n", e.message);
+               }
+
+               return 1 /*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 GoogleMovie ();
+                       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.rating = (int) object.get_double_member ("rating");
+                       movie.cinema_name = object.get_string_member ("cinema_name");
+                       movie.cinema_phone = object.get_string_member ("cinema_phone");
+                       movie.showtimes = object.get_string_member ("showtimes");
+                       movie.secondary = movie.cinema_name + " - " + movie.showtimes;
 
-               int n = yield parser.query (filter, location, callback, cancellable);
-               if (location == null) {
-                       location = parser.location;
+                       callback (movie);
                }
 
-               return n;
+               if (finished) {
+                       search = null;
+                       Idle.add (get_movies_callback);
+               }
        }
 
        public override void add_movie (Movie movie) {