From: Philipp Zabel Date: Mon, 1 Feb 2010 19:45:31 +0000 (+0100) Subject: Convert Google plugin into a D-Bus service X-Git-Url: https://vcs.maemo.org/git/?a=commitdiff_plain;h=691d0d71efa25af00773285d3eca0845b8734168;p=cinaest Convert Google plugin into a D-Bus service 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. --- diff --git a/Makefile.am b/Makefile.am index 50f5063..09a2537 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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} diff --git a/configure.ac b/configure.ac index 48657ed..9612d05 100644 --- a/configure.ac +++ b/configure.ac @@ -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 index 0000000..0035661 --- /dev/null +++ b/data/org.maemo.cinaest.GoogleShowtimes.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.maemo.cinaest.GoogleShowtimes +Exec=@libexecdir@/cinaest-google-backend diff --git a/debian/cinaest-plugin-google.install b/debian/cinaest-plugin-google.install index 5937ff2..a456b12 100644 --- a/debian/cinaest-plugin-google.install +++ b/debian/cinaest-plugin-google.install @@ -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 index 0000000..6a375b8 --- /dev/null +++ b/src/backends/google/google-backend.vala @@ -0,0 +1,189 @@ +using Gee; + +public Gee.HashMap 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 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 (); + + 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) 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 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 locations) { + if (location == null) + return false; + if (locations != null) { + for (unowned SList 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 (); + + 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 index 0000000..886772d --- /dev/null +++ b/src/backends/google/google-parser.vala @@ -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 . + */ + +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 ("&")) { + result += "&"; + i += 4; + continue; + } + if (s.offset (i).has_prefix (""")) { + 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&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 ("‎", "").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 (" ", ","); + 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&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 (" ", " ").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 ("")) { + parse_movie (); + movies++; + } else if (((string) current).has_prefix("
")) { + 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 index cc5a0de..0000000 --- a/src/plugins/google-parser.vala +++ /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 . - */ - -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 ("&")) { - result += "&"; - i += 4; - continue; - } - if (s.offset (i).has_prefix (""")) { - 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&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 ("‎", "").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 (" ", ","); - 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&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 (" ", " ").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 ("")) { - parse_movie (); - movies++; - } else if (((string) current).has_prefix("
")) { - 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; - } -} diff --git a/src/plugins/google-plugin.vala b/src/plugins/google-plugin.vala index b37a36e..5491a6d 100644 --- a/src/plugins/google-plugin.vala +++ b/src/plugins/google-plugin.vala @@ -19,10 +19,18 @@ 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 sources; - List locations; + SList 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 l = locations.first (); l != null; l = l.next) { + for (unowned SList 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) {