1 #include "gt-resource-downloader.h"
2 #include "utils.h"
3 #include "config.h"
4 #include <glib/gprintf.h>
5 #include <libsoup/soup.h>
6
7 #define TAG "GtResourceDownloader"
8 #include "gnome-twitch/gt-log.h"
9
10 typedef struct
11 {
12 gchar* filepath;
13 gchar* image_filetype;
14 SoupSession* soup;
15 GMutex mutex;
16 } GtResourceDownloaderPrivate;
17
18 typedef struct
19 {
20 gchar* uri;
21 gchar* name;
22 ResourceDownloaderFunc cb;
23 gpointer udata;
24 GtResourceDownloader* self;
25 SoupMessage* msg;
26 GInputStream* istream;
27 } ResourceData; /* FIXME: Better name? */
28
29 static GThreadPool* dl_pool;
30
31 G_DEFINE_TYPE_WITH_PRIVATE(GtResourceDownloader, gt_resource_downloader, G_TYPE_OBJECT);
32
33 static ResourceData*
resource_data_new()34 resource_data_new()
35 {
36 return g_slice_new0(ResourceData);
37 }
38
39 static void
resource_data_free(ResourceData * data)40 resource_data_free(ResourceData* data)
41 {
42 if (!data) return;
43
44 g_free(data->uri);
45 g_free(data->name);
46 g_object_unref(data->self);
47 g_object_unref(data->msg);
48 g_object_unref(data->istream);
49
50 g_slice_free(ResourceData, data);
51 }
52
53 /* FIXME: Throw error */
54 static GdkPixbuf*
download_image(GtResourceDownloader * self,const gchar * uri,const gchar * name,SoupMessage * msg,GInputStream * istream,gboolean * from_file,GError ** error)55 download_image(GtResourceDownloader* self,
56 const gchar* uri, const gchar* name,
57 SoupMessage* msg, GInputStream* istream,
58 gboolean* from_file, GError** error)
59 {
60 RETURN_VAL_IF_FAIL(GT_IS_RESOURCE_DOWNLOADER(self), NULL);
61 RETURN_VAL_IF_FAIL(!utils_str_empty(uri), NULL);
62 RETURN_VAL_IF_FAIL(SOUP_IS_MESSAGE(msg), NULL);
63 RETURN_VAL_IF_FAIL(G_IS_INPUT_STREAM(istream), NULL);
64
65 GtResourceDownloaderPrivate* priv = gt_resource_downloader_get_instance_private(self);
66 g_autofree gchar* filename = NULL;
67 gint64 file_timestamp = 0;
68 gboolean file_exists = FALSE;
69 g_autoptr(GdkPixbuf) ret = NULL;
70 g_autoptr(GError) err = NULL;
71
72 /* NOTE: If we aren't supplied a filename, we'll just create one by hashing the uri */
73 if (utils_str_empty(name))
74 {
75 gchar hash_str[15];
76 guint hash = 0;
77
78 hash = g_str_hash(uri); /* TODO: Replace this with murmur3 hash */
79
80 g_sprintf(hash_str, "%ud", hash);
81
82 filename = g_build_filename(priv->filepath, hash_str, NULL);
83 }
84 else
85 filename = g_build_filename(priv->filepath, name, NULL);
86
87 if (priv->filepath && (file_exists = g_file_test(filename, G_FILE_TEST_EXISTS)))
88 {
89 file_timestamp = utils_timestamp_filename(filename, NULL);
90 }
91
92 if (SOUP_STATUS_IS_SUCCESSFUL(msg->status_code))
93 {
94 const gchar* last_modified_str = NULL;
95
96 DEBUG("Successful return code from uri '%s'", uri);
97
98 last_modified_str = soup_message_headers_get_one(msg->response_headers, "Last-Modified");
99
100 if (utils_str_empty(last_modified_str))
101 {
102 DEBUG("No 'Last-Modified' header in response from uri '%s'", uri);
103
104 goto download;
105 }
106 else if (utils_http_full_date_to_timestamp(last_modified_str) < file_timestamp)
107 {
108 DEBUG("No new image at uri '%s'", uri);
109
110 if (file_exists)
111 {
112 DEBUG("Loading image from file '%s'", filename);
113
114 ret = gdk_pixbuf_new_from_file(filename, NULL);
115
116 if (from_file) *from_file = TRUE;
117 }
118 else
119 {
120 DEBUG("Image doesn't exist locally");
121
122 goto download;
123 }
124 }
125 else
126 {
127 download:
128 DEBUG("New image at uri '%s'", uri);
129
130 ret = gdk_pixbuf_new_from_stream(istream, NULL, &err);
131
132 if (err)
133 {
134 WARNING("Unable to download image from uri '%s' because: %s",
135 uri, err->message);
136
137 g_propagate_prefixed_error(error, g_steal_pointer(&err),
138 "Unable to download image from uri '%s' because: ", uri);
139
140 return NULL;
141 }
142
143 if (priv->filepath && STRING_EQUALS(priv->image_filetype, GT_IMAGE_FILETYPE_JPEG))
144 {
145 gdk_pixbuf_save(ret, filename, priv->image_filetype,
146 NULL, "quality", "100", NULL);
147 }
148 else if (priv->filepath)
149 {
150 gdk_pixbuf_save(ret, filename, priv->image_filetype,
151 NULL, NULL);
152 }
153
154 if (from_file) *from_file = FALSE;
155 }
156 }
157
158 return g_steal_pointer(&ret);
159 }
160
161 static void
download_cb(ResourceData * data,gpointer udata)162 download_cb(ResourceData* data,
163 gpointer udata)
164 {
165 RETURN_IF_FAIL(data != NULL);
166
167 g_autoptr(GdkPixbuf) ret = NULL;
168 g_autoptr(GError) err = NULL;
169 gboolean from_file = FALSE;
170
171 ret = download_image(data->self, data->uri, data->name, data->msg,
172 data->istream, &from_file, &err);
173
174 data->cb(from_file ? NULL : g_steal_pointer(&ret),
175 data->udata, g_steal_pointer(&err));
176
177 resource_data_free(data);
178 }
179
180 static void
send_message_cb(GObject * source,GAsyncResult * res,gpointer udata)181 send_message_cb(GObject* source,
182 GAsyncResult* res, gpointer udata)
183 {
184 RETURN_IF_FAIL(SOUP_IS_SESSION(source));
185 RETURN_IF_FAIL(G_IS_ASYNC_RESULT(res));
186 RETURN_IF_FAIL(udata != NULL);
187
188 ResourceData* data = udata;
189 GtResourceDownloader* self = GT_RESOURCE_DOWNLOADER(data->self);
190 GtResourceDownloaderPrivate* priv = gt_resource_downloader_get_instance_private(self);
191 g_autoptr(GError) err = NULL;
192
193 data->istream = soup_session_send_finish(SOUP_SESSION(source), res, &err);
194
195 if (!err)
196 g_thread_pool_push(dl_pool, data, NULL);
197 else
198 {
199 data->cb(NULL, data->udata, g_steal_pointer(&err));
200 resource_data_free(data);
201 }
202 }
203
204 static void
finalize(GObject * obj)205 finalize(GObject* obj)
206 {
207 g_assert(GT_IS_RESOURCE_DOWNLOADER(obj));
208
209 GtResourceDownloader* self = GT_RESOURCE_DOWNLOADER(obj);
210 GtResourceDownloaderPrivate* priv = gt_resource_downloader_get_instance_private(self);
211
212 g_free(priv->filepath);
213
214 G_OBJECT_CLASS(gt_resource_downloader_parent_class)->finalize(obj);
215 }
216
217 static void
dispose(GObject * obj)218 dispose(GObject* obj)
219 {
220 g_assert(GT_IS_RESOURCE_DOWNLOADER(obj));
221
222 GtResourceDownloader* self = GT_RESOURCE_DOWNLOADER(obj);
223 GtResourceDownloaderPrivate* priv = gt_resource_downloader_get_instance_private(self);
224
225 MESSAGE("Finalize");
226
227 g_clear_object(&priv->soup);
228
229 G_OBJECT_CLASS(gt_resource_downloader_parent_class)->dispose(obj);
230 }
231
232 static void
gt_resource_downloader_class_init(GtResourceDownloaderClass * klass)233 gt_resource_downloader_class_init(GtResourceDownloaderClass* klass)
234 {
235 G_OBJECT_CLASS(klass)->finalize = finalize;
236 G_OBJECT_CLASS(klass)->dispose = dispose;
237
238 dl_pool = g_thread_pool_new((GFunc) download_cb, NULL, g_get_num_processors(), FALSE, NULL);
239 }
240
241 static void
gt_resource_downloader_init(GtResourceDownloader * self)242 gt_resource_downloader_init(GtResourceDownloader* self)
243 {
244 g_assert(GT_IS_RESOURCE_DOWNLOADER(self));
245
246 GtResourceDownloaderPrivate* priv = gt_resource_downloader_get_instance_private(self);
247
248 priv->soup = soup_session_new();
249
250 g_mutex_init(&priv->mutex);
251 }
252
253 GtResourceDownloader*
gt_resource_downloader_new()254 gt_resource_downloader_new()
255 {
256 GtResourceDownloader* ret = g_object_new(GT_TYPE_RESOURCE_DOWNLOADER, NULL);
257
258 return ret;
259 }
260
261
262 GtResourceDownloader*
gt_resource_downloader_new_with_cache(const gchar * filepath)263 gt_resource_downloader_new_with_cache(const gchar* filepath)
264 {
265 RETURN_VAL_IF_FAIL(!utils_str_empty(filepath), NULL);
266
267 GtResourceDownloader* ret = g_object_new(GT_TYPE_RESOURCE_DOWNLOADER, NULL);
268 GtResourceDownloaderPrivate* priv = gt_resource_downloader_get_instance_private(ret);
269
270 priv->filepath = g_strdup(filepath);
271 priv->image_filetype = NULL;
272
273 return ret;
274 }
275
276 GdkPixbuf*
gt_resource_downloader_download_image(GtResourceDownloader * self,const gchar * uri,const gchar * name,GError ** error)277 gt_resource_downloader_download_image(GtResourceDownloader* self,
278 const gchar* uri, const gchar* name, GError** error)
279 {
280 RETURN_VAL_IF_FAIL(GT_IS_RESOURCE_DOWNLOADER(self), NULL);
281 RETURN_VAL_IF_FAIL(!utils_str_empty(uri), NULL);
282
283 GtResourceDownloaderPrivate* priv = gt_resource_downloader_get_instance_private(self);
284 g_autoptr(SoupMessage) msg = NULL;
285 g_autoptr(GInputStream) istream = NULL;
286 g_autoptr(GdkPixbuf) ret = NULL;
287 g_autoptr(GError) err = NULL;
288
289 DEBUG("Downloading image from uri '%s'", uri);
290
291 msg = soup_message_new(SOUP_METHOD_GET, uri);
292
293 /* NOTE: So libsoup isn't actually all that thread safe and
294 * calling soup_session_send from multiple threads causes it to
295 * crash, so we wrap a mutex around it. One should use the
296 * download_image_immediately func if one wants to download
297 * several images at the same time */
298 g_mutex_lock(&priv->mutex);
299 istream = soup_session_send(priv->soup, msg, NULL, &err);
300 g_mutex_unlock(&priv->mutex);
301
302 if (err)
303 {
304 WARNING("Unable to download image from uri '%s' because: %s",
305 uri, err->message);
306
307 g_propagate_prefixed_error(error, g_steal_pointer(&err),
308 "Unable to download image from uri '%s' because: ", uri);
309
310 return NULL;
311 }
312
313 ret = download_image(self, uri, name, msg, istream, NULL, error);
314
315 return g_steal_pointer(&ret);
316 }
317
318
319 static void
download_image_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)320 download_image_async_cb(GTask* task, gpointer source,
321 gpointer task_data, GCancellable* cancel)
322 {
323 RETURN_IF_FAIL(GT_IS_RESOURCE_DOWNLOADER(source));
324 RETURN_IF_FAIL(G_IS_TASK(task));
325 RETURN_IF_FAIL(task_data != NULL);
326
327 GtResourceDownloader* self = GT_RESOURCE_DOWNLOADER(source);
328 GError* err = NULL;
329 GenericTaskData* data = task_data;
330
331 GdkPixbuf* ret = gt_resource_downloader_download_image(self,
332 data->str_1, data->str_2, &err);
333
334 if (err)
335 g_task_return_error(task, err);
336 else
337 g_task_return_pointer(task, ret, (GDestroyNotify) g_object_unref);
338 }
339
340 void
gt_resource_downloader_download_image_async(GtResourceDownloader * self,const gchar * uri,const gchar * name,GAsyncReadyCallback cb,GCancellable * cancel,gpointer udata)341 gt_resource_downloader_download_image_async(GtResourceDownloader* self,
342 const gchar* uri, const gchar* name, GAsyncReadyCallback cb,
343 GCancellable* cancel, gpointer udata)
344 {
345 RETURN_IF_FAIL(GT_IS_RESOURCE_DOWNLOADER(self));
346
347 g_autoptr(GTask) task = g_task_new(self, cancel, cb, udata);
348 GenericTaskData* data = generic_task_data_new();
349
350 data->str_1 = g_strdup(uri);
351 data->str_2 = g_strdup(name);
352
353 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
354
355 g_task_run_in_thread(task, download_image_async_cb);
356 }
357
358 GdkPixbuf*
gt_resource_donwloader_download_image_finish(GtResourceDownloader * self,GAsyncResult * result,GError ** error)359 gt_resource_donwloader_download_image_finish(GtResourceDownloader* self,
360 GAsyncResult* result, GError** error)
361 {
362 RETURN_VAL_IF_FAIL(GT_IS_RESOURCE_DOWNLOADER(self), NULL);
363 RETURN_VAL_IF_FAIL(G_IS_ASYNC_RESULT(result), NULL);
364
365 GdkPixbuf* ret = g_task_propagate_pointer(G_TASK(result), error);
366
367 return ret;
368 }
369
370 void
gt_resource_downloader_set_image_filetype(GtResourceDownloader * self,const gchar * image_filetype)371 gt_resource_downloader_set_image_filetype(GtResourceDownloader* self, const gchar* image_filetype)
372 {
373 RETURN_IF_FAIL(GT_IS_RESOURCE_DOWNLOADER(self));
374 RETURN_IF_FAIL(!utils_str_empty(image_filetype));
375
376 GtResourceDownloaderPrivate* priv = gt_resource_downloader_get_instance_private(self);
377
378 priv->image_filetype = g_strdup(image_filetype);
379 }
380
381 /* FIXME: Make cancellable */
382 GdkPixbuf*
gt_resource_downloader_download_image_immediately(GtResourceDownloader * self,const gchar * uri,const gchar * name,ResourceDownloaderFunc cb,gpointer udata,GError ** error)383 gt_resource_downloader_download_image_immediately(GtResourceDownloader* self,
384 const gchar* uri, const gchar* name, ResourceDownloaderFunc cb,
385 gpointer udata, GError** error)
386 {
387 RETURN_VAL_IF_FAIL(GT_IS_RESOURCE_DOWNLOADER(self), NULL);
388 RETURN_VAL_IF_FAIL(!utils_str_empty(uri), NULL);
389
390 GtResourceDownloaderPrivate* priv = gt_resource_downloader_get_instance_private(self);
391 g_autofree gchar* filename = NULL;
392 g_autoptr(GdkPixbuf) ret = NULL;
393 g_autoptr(GError) err = NULL;
394 g_autoptr(SoupMessage) msg = NULL;
395 ResourceData* data = NULL;
396
397 /* NOTE: If we aren't supplied a filename, we'll just create one by hashing the uri */
398 if (utils_str_empty(name))
399 {
400 gchar hash_str[15];
401 guint hash = 0;
402
403 hash = g_str_hash(uri); /* TODO: Replace this with murmur3 hash */
404
405 g_sprintf(hash_str, "%ud", hash);
406
407 filename = g_build_filename(priv->filepath, hash_str, NULL);
408 }
409 else
410 filename = g_build_filename(priv->filepath, name, NULL);
411
412 if (priv->filepath && g_file_test(filename, G_FILE_TEST_EXISTS))
413 {
414 ret = gdk_pixbuf_new_from_file(filename, &err);
415
416 if (err)
417 {
418 WARNING("Unable to download image because: %s", err->message);
419
420 g_propagate_prefixed_error(error, g_steal_pointer(&err),
421 "Unable to download image because: ");
422
423 /* NOTE: Don't return here as we still might be able to
424 * download a new image*/
425 }
426 }
427
428 msg = soup_message_new(SOUP_METHOD_GET, uri);
429 soup_message_headers_append(msg->request_headers, "Client-ID", CLIENT_ID);
430
431 data = resource_data_new();
432 data->uri = g_strdup(uri);
433 data->name = g_strdup(name);
434 data->cb = cb;
435 data->udata = udata;
436 data->self = g_object_ref(self);
437 data->msg = g_steal_pointer(&msg);
438
439 soup_session_send_async(priv->soup, data->msg, NULL, send_message_cb, data);
440
441 /* NOTE: Return any found image immediately */
442 return g_steal_pointer(&ret);
443 }
444