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