Google poster downloader: make got_poster_uri signal public
[cinaest] / src / poster / google-poster-downloader.vala
1 /* This file is part of Cinaest.
2  *
3  * Copyright (C) 2009 Philipp Zabel
4  *
5  * Cinaest is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * Cinaest is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with Cinaest. If not, see <http://www.gnu.org/licenses/>.
17  */
18
19 using Soup;
20
21 // A single google image search (parsing and retrieval of the poster image URI)
22 public class GoogleImageSearch : Soup.Message {
23         public string poster_uri = null;
24         private bool thumbnail;
25         private string last_chunk = null;
26
27         public GoogleImageSearch (string search, bool _thumbnail = false) {
28                 Object (method: "GET");
29                 uri = new URI ("http://images.google.com/images?imgsz=m&imgar=t&q=" + Uri.escape_string (search, "+", false));
30                 thumbnail = _thumbnail;
31                 got_chunk.connect (this.on_got_chunk);
32         }
33
34         void on_got_chunk (Buffer chunk) {
35                 weak string found;
36
37                 if (poster_uri != null)
38                         return;
39
40                 // FIXME - store a copy of the last chunk and retry if the parser fails, it can't handle partial input
41                 if (last_chunk == null) {
42                         // FIXME - can we avoid this copy without ugly hacks?
43                         found = chunk.data.ndup (chunk.length).str ("dyn.setResults([[");
44                 } else {
45                         found = (last_chunk + chunk.data).str ("dyn.setResults([[");
46                         last_chunk = null;
47                 }
48                 if (found != null) {
49                         var parser = new GoogleImageSearchParser (found);
50                         try {
51                                 poster_uri = parser.run (thumbnail);
52                                 if (poster_uri != null)
53                                         got_poster_uri (this);
54                         } catch (Error e) {
55                                 if (e is ParserError.EOF) {
56                                         last_chunk = chunk.data.ndup (chunk.length);
57                                 } else {
58                                         stdout.printf ("Parser error: %s\n", e.message);
59                                 }
60                         }
61                 }
62         }
63
64         public signal void got_poster_uri (GoogleImageSearch search);
65 }
66
67 // Encapsulation of a single poster download (google image search query and image file download)
68 public class GooglePosterDownload : Object {
69         private GooglePosterDownloader downloader;
70         private Soup.Session session;
71         public int handle;
72         private string cache_dir;
73         private string cache_filename;
74         private bool cancelled = false;
75
76         public GooglePosterDownload (string title, string year, bool thumbnail, int _handle, GooglePosterDownloader _downloader) {
77                 var search = title + "+" + year + "+movie+poster";
78
79                 handle = _handle;
80                 downloader = _downloader;
81                 session = downloader.session;
82
83                 var message = new GoogleImageSearch (search, thumbnail);
84                 message.got_poster_uri.connect (on_got_poster_uri);
85                 session.queue_message (message, google_search_finished);
86
87                 if (thumbnail) {
88                         // FIXME
89                         cache_dir = Path.build_filename (Environment.get_tmp_dir(), "cinaest-thumbnails");
90                 } else {
91                         cache_dir = Path.build_filename (Environment.get_user_cache_dir(), "media-art");
92                 }
93                 cache_filename = "movie-" +
94                                  Checksum.compute_for_string (ChecksumType.MD5, (title).down ()) + "-" +
95                                  Checksum.compute_for_string (ChecksumType.MD5, (year).down ()) + ".jpeg";
96         }
97
98         private void on_got_poster_uri (GoogleImageSearch message) {
99                 session.cancel_message (message, Soup.KnownStatusCode.OK);
100         }
101
102         private void google_search_finished (Session session, Message message) {
103                 if (cancelled)
104                         return;
105
106                 var search_message = (GoogleImageSearch) message;
107                 print ("Finished search: %s\n", message.uri.to_string (false));
108
109                 var poster_message = new Soup.Message ("GET", search_message.poster_uri);
110                 session.queue_message (poster_message, poster_downloaded);
111         }
112
113         private void poster_downloaded (Session session, Message message) {
114                 if (cancelled)
115                         return;
116
117                 print ("Downloaded poster: %s\n", message.uri.to_string (false));
118
119                 // Define cache path according to the Media Art Storage Spec (http://live.gnome.org/MediaArtStorageSpec)
120                 string cache_path = Path.build_filename (cache_dir, cache_filename);
121
122                 // Make sure the directory .album_arts is available
123                 DirUtils.create_with_parents (cache_dir, 0770);
124
125                 try {
126                         var file = File.new_for_path (cache_path + ".part");
127                         var stream = file.create (FileCreateFlags.NONE, null);
128
129                         stream.write (message.response_body.data, (size_t) message.response_body.length, null);
130
131                         FileUtils.rename (cache_path + ".part", cache_path);
132
133                         print ("Stored as: %s\n", cache_path);
134
135                         downloader.fetched (handle, cache_path);
136                         downloader.timeout_quit ();
137                 } catch (Error e) {
138                         stdout.printf ("Failed to store poster: %s\n", e.message);
139                 }
140         }
141
142         public void cancel () {
143                 cancelled = true;
144         }
145 }
146
147 // The D-Bus service to manage poster downloads
148 public class GooglePosterDownloader : Object, PosterDownloader {
149         private MainLoop loop;
150         public SessionAsync session;
151         private int fetch_handle = 1;
152         private List<GooglePosterDownload> downloads = null;
153         private uint source_id;
154
155         public GooglePosterDownloader () {
156                 loop = new MainLoop (null);
157
158                 session = new SessionAsync ();
159                 session.max_conns_per_host = 7;
160         }
161
162         public void timeout_quit () {
163                 // With every change we reset the timer to 3min
164                 if (source_id != 0) {
165                         Source.remove (source_id);
166                 }
167                 source_id = Timeout.add_seconds (180, quit);
168         }
169
170         private bool quit () {
171                 loop.quit ();
172
173                 // One-shot only
174                 return false;
175         }
176
177         public void run () {
178                 loop.run ();
179         }
180
181         // Implement the PosterDownloader interface
182         public int Fetch (string title, string year, string kind) throws DBus.Error {
183                 var download = new GooglePosterDownload (title, year, false, ++fetch_handle, this);
184
185                 downloads.append (download);
186
187                 return fetch_handle;
188         }
189
190         public int FetchThumbnail (string title, string year, string kind) throws DBus.Error {
191                 var download = new GooglePosterDownload (title, year, true, ++fetch_handle, this);
192
193                 downloads.append (download);
194
195                 return fetch_handle;
196         }
197
198         public void Unqueue (int handle) throws DBus.Error {
199                 GooglePosterDownload download = null;
200                 foreach (GooglePosterDownload d in downloads) {
201                         if (d.handle == handle) {
202                                 download = d;
203                                 d.cancel ();
204                                 break;
205                         }
206                 }
207                 if (download != null) {
208                         downloads.remove (download);
209                 }
210         }
211
212         static void main () {
213                 try {
214                         var conn = DBus.Bus.get (DBus.BusType.SESSION);
215                         dynamic DBus.Object bus = conn.get_object ("org.freedesktop.DBus",
216                                                                    "/org/freedesktop/DBus",
217                                                                    "org.freedesktop.DBus");
218
219                         // Try to register service in session bus
220                         uint res = bus.request_name ("org.maemo.movieposter.GoogleImages", (uint) 0);
221                         if (res == DBus.RequestNameReply.PRIMARY_OWNER) {
222                                 // Start server
223                                 var server = new GooglePosterDownloader ();
224                                 conn.register_object ("/org/maemo/movieposter/GoogleImages", server);
225
226                                 server.timeout_quit ();
227                                 server.run ();
228                         }
229                 } catch (Error e) {
230                         error ("Oops: %s\n", e.message);
231                 }
232         }
233 }
234