Add MoviePilot plugin and backend
[cinaest] / src / backends / moviepilot / moviepilot-backend.vala
1 using Gee;
2
3 public Gee.HashMap <string, MovieSearch> searches;
4 public DBus.Connection conn;
5
6 // A movie, serialized as JSON object in the movies_found D-Bus signal
7 class Movie : Object {
8         public string title { get; set; }
9         public int year { get; set; }
10         public double rating { get; set; }
11         public string genres { get; set; }
12         public string id { get; set; }
13 }
14
15 [DBus (name = "org.maemo.cinaest.MovieSearch", signals="movies_found")]
16 public class MovieSearch : Object {
17         private const string SERVICE_URL = "http://www.moviepilot.de";
18         private const string API_KEY = "1dab2d86f46d669766de572ba9b8eb";
19
20         private Rest.Proxy proxy;
21
22         public int id;
23         bool aborted;
24         string title;
25         MoviePilotMovieService service;
26         public string path;
27         public string sender;
28
29         private SourceFunc callback = null;
30         private GLib.List<Movie> results = null;
31
32         // D-Bus API
33
34         public void abort () {
35                 print ("aborting\n");
36                 aborted = true;
37         }
38
39         public void start (string query) {
40                 title = query;
41                 aborted = false;
42
43                 print ("starting query %d: \"%s\"\n", id, title);
44                 query_async.begin ();
45         }
46
47         public signal void movies_found (string[] movies, bool finished);
48
49         // Internal methods
50
51         internal MovieSearch (MoviePilotMovieService _service, string _sender, string _path) {
52                 service = _service;
53                 sender = _sender;
54                 path = _path;
55         }
56
57         internal async void query_async () {
58                 if (callback != null)
59                         return;
60                 callback = query_async.callback;
61                 try {
62                         print ("_search_movies (%s)\n", title);
63                         _search_movies (title);
64                         yield;
65                         if (!aborted) {
66                                 var movies = new string[results.length ()];
67                                 int i = 0;
68                                 for (weak GLib.List<Movie> node = results.first (); node != null; node = node.next) {
69                                 // FIXME: why does Json.serialize_gobject fail here?
70                                 //      movies[i++] = Json.serialize_gobject (node.data, null);
71                                         movies[i++] = "{\"title\":\"%s\",\"year\":%d,\"rating\":%f,\"genres\":\"%s\",\"id\":\"%s\"}".printf (node.data.title, node.data.year, node.data.rating, node.data.genres, node.data.id);
72                                 }
73                                 print ("got %d movies\n", movies.length);
74                                 movies_found (movies, true);
75                         } else {
76                                 print ("aborted\n");
77                         }
78                         service.timeout_quit ();
79                 } catch (GLib.Error e) {
80                         critical ("Error: %s\n", e.message);
81                 }
82                 callback = null;
83         }
84
85         // MoviePilot RESTful API calls, executed using librest
86
87         private void _search_movies (string query, int per_page = 20, int page = 1) throws GLib.Error {
88                 var call = proxy.new_call ();
89
90                 if (query == "") {
91                         Idle.add (callback);
92                         return;
93                 }
94
95                 call.set_function ("searches/movies.json");
96                 call.set_method ("GET");
97                 call.add_params ("q", query,
98                                  "api_key", API_KEY,
99                                  "per_page", per_page.to_string (),
100                                  "page", page.to_string ());
101                 call.run_async (search_movies_cb, this);
102         }
103 /*
104         internal void get_info (string id) throws GLib.Error {
105                 var call = proxy.new_call ();
106
107                 call.set_function ("movies/%s.json".printf (id));
108                 call.set_method ("GET");
109                 call.add_params ("api_key", API_KEY);
110                 call.run_async (get_info_cb, proxy);
111         }
112 */
113         // Callback functions handle JSON results using libjson-glib
114
115         private void search_movies_cb (Rest.ProxyCall call, GLib.Error? _error, GLib.Object? weak_object) {
116                 var parser = new Json.Parser ();
117                 unowned Json.Node node;
118
119                 try {
120                         parser.load_from_data (call.get_payload (), (ssize_t) call.get_payload_length ());
121                 } catch (Error e) {
122                         critical ("Payload: %s\nParse error: %s", call.get_payload (), e.message);
123                         Idle.add (this.callback);
124                         return;
125                 }
126
127                 node = parser.get_root ();
128                 if (node.get_node_type () != Json.NodeType.OBJECT) {
129                         critical ("JSON error, not an object:\n%s\n", call.get_payload ());
130                         Idle.add (this.callback);
131                         return;
132                 }
133
134                 var object = node.get_object ();
135                 if (!object.has_member ("total_entries")) {
136                         if (object.has_member ("error")) {
137                                 critical ("Error: %s\n", object.get_string_member ("error"));
138                         } else {
139                                 critical ("JSON error, not a movie search result:\n%s\n", call.get_payload ());
140                                 //{"suggestions":["matrix","material","matilda"],"total-entries":0}
141                         }
142                         Idle.add (this.callback);
143                         return;
144                 }
145
146                 if (object.get_int_member ("total_entries") == 0) {
147                         Idle.add (this.callback);
148                         return;
149                 }
150
151                 if (!object.has_member ("movies")) {
152                         critical ("JSON error, not a movie search result:\n%s\n", call.get_payload ());
153                         Idle.add (this.callback);
154                         return;
155                 }
156
157         //      var total_results = object.get_int_member ("total_entries");
158         //      print ("total_results = %lld\n", total_results);
159
160                 node = object.get_member ("movies");
161                 if (node.get_node_type () != Json.NodeType.ARRAY) {
162                         critical ("JSON error, not a movie array\n");
163                         return;
164                 }
165
166                 var array = node.get_array ();
167                 foreach (weak Json.Node n in array.get_elements ()) {
168                         var movie = handle_movie (n);
169
170                         results.append (movie);
171                 }
172
173                 Idle.add (this.callback);
174         }
175
176         private void get_info_cb (Rest.ProxyCall call, GLib.Error? _error, GLib.Object? weak_object) {
177                 var parser = new Json.Parser ();
178
179                 try {
180                         parser.load_from_data (call.get_payload (), (ssize_t) call.get_payload_length ());
181                 } catch (Error e) {
182                         critical ("Payload: %s\nParse error: %s", call.get_payload (), e.message);
183                         return;
184                 }
185
186                 var movie = handle_movie (parser.get_root ());
187
188                 results.append (movie);
189
190                 Idle.add (this.callback);
191         }
192
193         private Movie handle_movie (Json.Node node) requires (node.get_node_type () == Json.NodeType.OBJECT) {
194                 var object = node.get_object ();
195                 var movie = new Movie ();
196
197                 movie.title = object.get_string_member ("display_title");
198                 movie.year = object.get_string_member ("production_year").to_int ();
199                 movie.rating = object.get_double_member ("average_community_rating");
200
201                 movie.genres = object.get_string_member ("genres_list"); // CSV
202
203                 var url = object.get_string_member ("restful_url");
204                 if (url.has_prefix ("http://www.moviepilot.de/movies/")) {
205                         movie.id = url.offset (32); // "http://www.moviepilot.de/movies/".length ();
206                 } else {
207                         critical ("unknown restful_url prefix: %s", url);
208                 }
209
210         //      var countries = object.get_string_member ("countries_list");
211         //      var description = object.get_string_member ("short_description");
212         //      var on_tv = object.get_bool_member ("on_tv");
213         //      var homepage = object.get_string_member ("homepage");
214         //      var long_description = object.get_string_member ("long_description");
215         //      var premiere = object.get_string_member ("premiere_date"); // YYYY-MM-DD
216         //      object.get_member ("alternative_identifiers"); // an object with "service":"id" pairs
217         //      object.get_member ("poster"); // object with copyright, title, height, extension, width, photo_id, base_url, mime_type, size, restful_url and file_name_base properties
218         //      object.get_string_member ("cinema_start_date") // YYYY-MM-DD
219         //      object.get_int_member ("runtime");
220         //      object.get_double_member ("average_critics_rating");
221         //      object.get_string_member ("dvd_start_date");
222
223                 // FIXME: cache movie here
224
225                 return movie;
226         }
227
228         construct {
229                 proxy = new Rest.Proxy (SERVICE_URL, false);
230         }
231 }
232
233 [DBus (name = "org.maemo.cinaest.MovieService")]
234 public class MoviePilotMovieService : Object {
235         private MainLoop loop;
236         private uint source_id;
237         int id = 0;
238
239         // D-Bus API
240
241         public string new_search (DBus.BusName sender) {
242                 var path = "/org/maemo/cinaest/moviepilot/search%d".printf (id);
243                 var search = new MovieSearch (this, sender, path);
244                 search.id = id++;
245                 conn.register_object (path, search);
246
247                 print ("creating new search %s for %s\n", path, sender);
248                 searches.set (path, search);
249
250                 return path;
251         }
252
253         public void unregister (string path) {
254                 print ("unregistering search %s\n", path);
255
256                 searches.remove (path);
257                 print ("%d\n", searches.size);
258         }
259
260         // Internal methods
261
262         internal MoviePilotMovieService () {
263                 loop = new MainLoop (null);
264         }
265
266         internal void timeout_quit () {
267                 // With every change we reset the timer to 3min
268                 if (source_id != 0) {
269                         Source.remove (source_id);
270                 }
271                 source_id = Timeout.add_seconds (180, quit);
272         }
273
274         private bool quit () {
275                 loop.quit ();
276
277                 // One-shot only
278                 return false;
279         }
280
281         internal void run () {
282                 loop.run ();
283         }
284 }
285
286 void on_client_lost (DBus.Object sender, string name, string prev, string newp) {
287         if (newp == "") {
288                 var remove_list = new SList<string> ();
289                 // We lost a client
290                 print ("lost a client: %s\n", prev);
291                 foreach (MovieSearch search in searches.values) {
292                         if (search.sender == prev) {
293                                 print ("removing %s\n", search.path);
294                                 remove_list.append (search.path);
295                         }
296                 }
297                 foreach (string path in remove_list)
298                         searches.remove (path);
299         }
300 }
301
302 void main () {
303         try {
304                 conn = DBus.Bus.get (DBus.BusType. SESSION);
305
306                 searches = new Gee.HashMap <string, MovieSearch> ();
307
308                 dynamic DBus.Object bus = conn.get_object ("org.freedesktop.DBus",
309                                                            "/org/freedesktop/DBus",
310                                                            "org.freedesktop.DBus");
311
312                 uint res = bus.request_name ("org.maemo.cinaest.MoviePilot", (uint) 0);
313
314                 if (res == DBus.RequestNameReply.PRIMARY_OWNER) {
315                         var server = new MoviePilotMovieService ();
316
317                         conn.register_object ("/org/maemo/cinaest/moviepilot", server);
318
319                         bus.NameOwnerChanged.connect (on_client_lost);
320
321                         server.timeout_quit ();
322                         server.run ();
323                 }
324         } catch (Error e) {
325                 stderr.printf ("Oops: %s\n", e.message);
326         }
327 }