1 /*
2  *  This file is part of GNOME Twitch - 'Enjoy Twitch on your GNU/Linux desktop'
3  *  Copyright © 2017 Vincent Szolnoky <vinszent@vinszent.com>
4  *
5  *  GNOME Twitch 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  *  GNOME Twitch 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 GNOME Twitch. If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include <glib/gstdio.h>
20 #include <glib.h>
21 #include <stdlib.h>
22 #include <string.h>
23 #include "utils.h"
24 #include "config.h"
25 #include "gt-win.h"
26 
27 #define TAG "Utils"
28 #include "gnome-twitch/gt-log.h"
29 
30 gpointer
utils_value_ref_sink_object(const GValue * val)31 utils_value_ref_sink_object(const GValue* val)
32 {
33     if (val == NULL || !G_VALUE_HOLDS_OBJECT(val) || g_value_get_object(val) == NULL)
34         return NULL;
35     else
36         return g_object_ref_sink(g_value_get_object(val));
37 }
38 
39 gchar*
utils_value_dup_string_allow_null(const GValue * val)40 utils_value_dup_string_allow_null(const GValue* val)
41 {
42     if (g_value_get_string(val))
43         return g_value_dup_string(val);
44 
45     return NULL;
46 }
47 
48 void
utils_container_clear(GtkContainer * cont)49 utils_container_clear(GtkContainer* cont)
50 {
51     for(GList* l = gtk_container_get_children(cont);
52         l != NULL; l = l->next)
53     {
54         gtk_container_remove(cont, GTK_WIDGET(l->data));
55     }
56 }
57 
58 guint64
utils_timestamp_filename(const gchar * filename,GError ** error)59 utils_timestamp_filename(const gchar* filename, GError** error)
60 {
61     RETURN_VAL_IF_FAIL(!utils_str_empty(filename), 0);
62 
63     GError* err = NULL; /* NOTE: Doesn't need to be freed because we propagate it */
64 
65     if (g_file_test(filename, G_FILE_TEST_EXISTS))
66     {
67         GTimeVal time;
68 
69         g_autoptr(GFile) file = g_file_new_for_path(filename);
70 
71         return utils_timestamp_file(file, error);
72     }
73 
74     RETURN_VAL_IF_REACHED(0);
75 }
76 
77 guint64
utils_timestamp_file(GFile * file,GError ** error)78 utils_timestamp_file(GFile* file, GError** error)
79 {
80     g_autoptr(GFileInfo) info = g_file_query_info(file,
81         G_FILE_ATTRIBUTE_TIME_MODIFIED, G_FILE_QUERY_INFO_NONE,
82         NULL, error);
83     GTimeVal time;
84 
85     if (error)
86     {
87         g_prefix_error(error, "Could not timestamp file because: ");
88 
89         WARNING("%s", (*error)->message);
90 
91         return 0;
92     }
93 
94     g_file_info_get_modification_time(info, &time);
95 
96     return time.tv_sec;
97 }
98 
99 gint64
utils_timestamp_now(void)100 utils_timestamp_now(void)
101 {
102     gint64 timestamp;
103     GDateTime* now;
104 
105     now = g_date_time_new_now_utc();
106     timestamp = g_date_time_to_unix(now);
107     g_date_time_unref(now);
108 
109     return timestamp;
110 }
111 
112 void
utils_pixbuf_scale_simple(GdkPixbuf ** pixbuf,gint width,gint height,GdkInterpType interp)113 utils_pixbuf_scale_simple(GdkPixbuf** pixbuf, gint width, gint height, GdkInterpType interp)
114 {
115     if (!*pixbuf)
116         return;
117 
118     GdkPixbuf* tmp = gdk_pixbuf_scale_simple(*pixbuf, width, height, interp);
119     g_clear_object(pixbuf);
120     *pixbuf = tmp;
121 }
122 
123 guint64
utils_http_full_date_to_timestamp(const char * string)124 utils_http_full_date_to_timestamp(const char* string)
125 {
126     gint64 ret = G_MAXINT64;
127     g_autoptr(SoupDate) date = NULL;
128 
129     date = soup_date_new_from_string(string);
130 
131     RETURN_VAL_IF_FAIL(date != NULL, ret);
132 
133     ret = soup_date_to_time_t(date);
134 
135     return ret;
136 }
137 
138 const gchar*
utils_search_key_value_strv(gchar ** strv,const gchar * key)139 utils_search_key_value_strv(gchar** strv, const gchar* key)
140 {
141     if (!strv)
142         return NULL;
143 
144     for (gchar** s = strv; *s != NULL; s += 2)
145     {
146         if (g_strcmp0(*s, key) == 0)
147             return *(s+1);
148     }
149 
150     return NULL;
151 }
152 
153 static gboolean
utils_mouse_hover_enter_cb(GtkWidget * widget,GdkEvent * evt,gpointer udata)154 utils_mouse_hover_enter_cb(GtkWidget* widget,
155                            GdkEvent* evt,
156                            gpointer udata)
157 {
158     GdkWindow* win;
159     GdkDisplay* disp;
160     GdkCursor* cursor;
161 
162     win = ((GdkEventMotion*) evt)->window;
163     disp = gdk_window_get_display(win);
164     cursor = gdk_cursor_new_for_display(disp, GDK_HAND2);
165 
166     gdk_window_set_cursor(win, cursor);
167 
168     g_object_unref(cursor);
169 
170     return FALSE;
171 }
172 
173 static gboolean
utils_mouse_hover_leave_cb(GtkWidget * widget,GdkEvent * evt,gpointer udata)174 utils_mouse_hover_leave_cb(GtkWidget* widget,
175                            GdkEvent* evt,
176                            gpointer udata)
177 {
178     GdkWindow* win;
179     GdkDisplay* disp;
180     GdkCursor* cursor;
181 
182     win = ((GdkEventMotion*) evt)->window;
183     disp = gdk_window_get_display(win);
184     cursor = gdk_cursor_new_for_display(disp, GDK_LEFT_PTR);
185 
186     gdk_window_set_cursor(win, cursor);
187 
188     g_object_unref(cursor);
189 
190     return FALSE;
191 }
192 
193 gboolean
utils_str_empty(const gchar * str)194 utils_str_empty(const gchar* str)
195 {
196     return !(str && strlen(str) > 0);
197 }
198 
199 gchar*
utils_str_capitalise(const gchar * str)200 utils_str_capitalise(const gchar* str)
201 {
202     g_assert_false(utils_str_empty(str));
203 
204     gchar* ret = g_strdup_printf("%c%s", g_ascii_toupper(*str), str+1);
205 
206     return ret;
207 }
208 
209 typedef struct
210 {
211     gpointer instance;
212     GCallback cb;
213     gpointer udata;
214 } OneshotData;
215 
216 static void
oneshot_cb(OneshotData * data)217 oneshot_cb(OneshotData* data)
218 {
219     g_signal_handlers_disconnect_by_func(data->instance,
220                                          data->cb,
221                                          data->udata);
222     g_signal_handlers_disconnect_by_func(data->instance,
223                                          oneshot_cb,
224                                          data);
225 }
226 
227 void
utils_signal_connect_oneshot(gpointer instance,const gchar * signal,GCallback cb,gpointer udata)228 utils_signal_connect_oneshot(gpointer instance,
229                              const gchar* signal,
230                              GCallback cb,
231                              gpointer udata)
232 {
233     OneshotData* data = g_new(OneshotData, 1);
234 
235     data->instance = instance;
236     data->cb = cb;
237     data->udata = udata;
238 
239     g_signal_connect(instance, signal, cb, udata);
240 
241     g_signal_connect_data(instance,
242                           signal,
243                           G_CALLBACK(oneshot_cb),
244                           data,
245                           (GClosureNotify) g_free,
246                           G_CONNECT_AFTER | G_CONNECT_SWAPPED);
247 }
248 
249 void
utils_signal_connect_oneshot_swapped(gpointer instance,const gchar * signal,GCallback cb,gpointer udata)250 utils_signal_connect_oneshot_swapped(gpointer instance,
251     const gchar* signal, GCallback cb, gpointer udata)
252 {
253 
254     OneshotData* data = g_new(OneshotData, 1);
255 
256     data->instance = instance;
257     data->cb = cb;
258     data->udata = udata;
259 
260     g_signal_connect_swapped(instance, signal, cb, udata);
261 
262     g_signal_connect_data(instance,
263                           signal,
264                           G_CALLBACK(oneshot_cb),
265                           data,
266                           (GClosureNotify) g_free,
267                           G_CONNECT_AFTER | G_CONNECT_SWAPPED);
268 }
269 
270 inline void
utils_refresh_cancellable(GCancellable ** cancel)271 utils_refresh_cancellable(GCancellable** cancel)
272 {
273     if (*cancel && !g_cancellable_is_cancelled(*cancel))
274         g_cancellable_cancel(*cancel);
275     g_clear_object(cancel);
276     *cancel = g_cancellable_new();
277 }
278 
279 static void
weak_ref_notify_cb(gpointer udata,GObject * obj)280 weak_ref_notify_cb(gpointer udata, GObject* obj) /* NOTE: This object is no longer valid, can only use the addres */
281 {
282     RETURN_IF_FAIL(udata != NULL);
283 
284     g_weak_ref_set(udata, NULL);
285 }
286 
287 GWeakRef*
utils_weak_ref_new(gpointer obj)288 utils_weak_ref_new(gpointer obj)
289 {
290     GWeakRef* ref = g_malloc(sizeof(GWeakRef));
291 
292     g_weak_ref_init(ref, obj);
293     g_object_weak_ref(obj, weak_ref_notify_cb, ref);
294 
295     return ref;
296 }
297 
298 void
utils_weak_ref_free(GWeakRef * ref)299 utils_weak_ref_free(GWeakRef* ref)
300 {
301     g_autoptr(GObject) obj = g_weak_ref_get(ref);
302 
303     if (obj)
304         g_object_weak_unref(obj, weak_ref_notify_cb, ref);
305 
306     g_weak_ref_clear(ref);
307     g_free(ref);
308 }
309 
310 GDateTime*
utils_parse_time_iso_8601(const gchar * time,GError ** error)311 utils_parse_time_iso_8601(const gchar* time, GError** error)
312 {
313     GDateTime* ret = NULL;
314 
315     gint year, month, day, hour, min, sec;
316 
317     gint scanned = sscanf(time, "%d-%d-%dT%d:%d:%dZ", &year, &month, &day, &hour, &min, &sec);
318 
319     if (scanned != 6)
320     {
321         g_set_error(error, GT_UTILS_ERROR, GT_UTILS_ERROR_PARSING_TIME,
322             "Unable to parse time from input '%s'", time);
323     }
324     else
325         ret = g_date_time_new_utc(year, month, day, hour, min, sec);
326 
327     return ret;
328 }
329 
330 GenericTaskData*
generic_task_data_new()331 generic_task_data_new()
332 {
333     return g_slice_new0(GenericTaskData);
334 }
335 
336 void
generic_task_data_free(GenericTaskData * data)337 generic_task_data_free(GenericTaskData* data)
338 {
339     g_free(data->str_1);
340     g_free(data->str_2);
341     g_free(data->str_3);
342 
343     g_slice_free(GenericTaskData, data);
344 }
345 
346 JsonReader*
utils_parse_json(const gchar * data,GError ** error)347 utils_parse_json(const gchar* data, GError** error)
348 {
349     g_autoptr(JsonReader) reader = NULL;
350     g_autoptr(JsonParser) parser = NULL;
351     g_autoptr(GError) err = NULL;
352 
353     parser = json_parser_new();
354 
355     json_parser_load_from_data(parser, data, -1, &err);
356 
357     if (err)
358     {
359         WARNING("Unable to parse JSON because: %s", err->message);
360 
361         g_propagate_prefixed_error(error, g_steal_pointer(&err), "Unable to parse JSON because: ");
362     }
363     else
364         reader = json_reader_new(json_parser_get_root(parser));
365 
366     return g_steal_pointer(&reader);
367 }
368 
369 #define RETURN_JSON_ERROR                                       \
370     G_STMT_START                                                \
371     {                                                           \
372         WARNING("Coultn'd parse channel from JSON because: %s", \
373             json_reader_get_error(reader)-> message);           \
374         g_set_error(error, GT_UTILS_ERROR, GT_UTILS_ERROR_JSON, \
375             "Couldn't parse channel from JSON because: %s",     \
376             json_reader_get_error(reader)->message);            \
377         return NULL;                                            \
378     } G_STMT_END
379 
380 GtChannelData*
utils_parse_channel_from_json(JsonReader * reader,GError ** error)381 utils_parse_channel_from_json(JsonReader* reader, GError** error)
382 {
383     g_autoptr(GtChannelData) data = gt_channel_data_new();
384 
385     if (!json_reader_read_member(reader, "_id")) RETURN_JSON_ERROR;
386     JsonNode* node = json_reader_get_value(reader);
387     if (STRING_EQUALS(json_node_type_name(node), "Integer"))
388         data->id = g_strdup_printf("%" G_GINT64_FORMAT, json_reader_get_int_value(reader));
389     else if (STRING_EQUALS(json_node_type_name(node), "String"))
390         data->id = g_strdup(json_reader_get_string_value(reader));
391     else
392     {
393         g_set_error(error, GT_UTILS_ERROR, GT_UTILS_ERROR_JSON,
394             "Unable to parse game from JSON because: '_id' was of incorrect format");
395         return NULL;
396     }
397     json_reader_end_member(reader);
398 
399     if (!json_reader_read_member(reader, "name")) RETURN_JSON_ERROR;
400     data->name = g_strdup(json_reader_get_string_value(reader));
401     json_reader_end_member(reader);
402 
403     if (!json_reader_read_member(reader, "display_name")) RETURN_JSON_ERROR;
404     data->display_name = json_reader_get_null_value(reader) ?
405         NULL : g_strdup(json_reader_get_string_value(reader));
406     json_reader_end_member(reader);
407 
408     if (!json_reader_read_member(reader, "status")) RETURN_JSON_ERROR;
409     data->status = json_reader_get_null_value(reader) ?
410         NULL : g_strdup(json_reader_get_string_value(reader));
411     json_reader_end_member(reader);
412 
413     if (!json_reader_read_member(reader, "video_banner")) RETURN_JSON_ERROR;
414     data->video_banner_url = json_reader_get_null_value(reader) ?
415         NULL : g_strdup(json_reader_get_string_value(reader));
416     json_reader_end_member(reader);
417 
418     if (!json_reader_read_member(reader, "logo")) RETURN_JSON_ERROR;
419     data->logo_url = json_reader_get_null_value(reader) ?
420         NULL : g_strdup(json_reader_get_string_value(reader));
421     json_reader_end_member(reader);
422 
423     if (!json_reader_read_member(reader, "url")) RETURN_JSON_ERROR;
424     data->profile_url = g_strdup(json_reader_get_string_value(reader));
425     json_reader_end_member(reader);
426 
427     data->online = FALSE;
428 
429     return g_steal_pointer(&data);
430 }
431 
432 #undef RETURN_JSON_ERROR
433 #define RETURN_JSON_ERROR                                       \
434     G_STMT_START                                                \
435     {                                                           \
436         WARNING("Coultn'd parse stream from JSON because: %s",  \
437             json_reader_get_error(reader)-> message);           \
438         g_set_error(error, GT_UTILS_ERROR, GT_UTILS_ERROR_JSON, \
439             "Couldn't parse stream from JSON because: %s",      \
440             json_reader_get_error(reader)->message);            \
441         return NULL;                                            \
442     } G_STMT_END
443 
444 GtChannelData*
utils_parse_stream_from_json(JsonReader * reader,GError ** error)445 utils_parse_stream_from_json(JsonReader* reader, GError** error)
446 {
447     g_autoptr(GtChannelData) data = NULL;
448 
449     if (!json_reader_read_member(reader, "channel")) RETURN_JSON_ERROR;
450     data = utils_parse_channel_from_json(reader, error);
451     json_reader_end_member(reader);
452 
453     if (*error) return NULL;
454 
455     if (!json_reader_read_member(reader, "game")) RETURN_JSON_ERROR;
456     data->game = json_reader_get_null_value(reader) ?
457         NULL : g_strdup(json_reader_get_string_value(reader));
458     json_reader_end_member(reader);
459 
460     if (!json_reader_read_member(reader, "viewers")) RETURN_JSON_ERROR;
461     data->viewers = json_reader_get_null_value(reader) ?
462         0 : json_reader_get_int_value(reader);
463     json_reader_end_member(reader);
464 
465     if (!json_reader_read_member(reader, "created_at")) RETURN_JSON_ERROR;
466     data->stream_started_time = json_reader_get_null_value(reader) ?
467         NULL : utils_parse_time_iso_8601(json_reader_get_string_value(reader), error);
468     json_reader_end_member(reader);
469 
470     if (*error)
471     {
472         g_prefix_error(error, "Unable to parse stream from JSON because: ");
473         return NULL;
474     }
475 
476     if (!json_reader_read_member(reader, "preview")) RETURN_JSON_ERROR;
477     if (!json_reader_read_member(reader, "large")) RETURN_JSON_ERROR;
478     data->preview_url = json_reader_get_null_value(reader) ?
479         NULL : g_strdup(json_reader_get_string_value(reader));
480     json_reader_end_member(reader);
481     json_reader_end_member(reader);
482 
483     data->online = TRUE;
484 
485     return g_steal_pointer(&data);
486 }
487 
488 #undef RETURN_JSON_ERROR
489 #define RETURN_JSON_ERROR                                       \
490     G_STMT_START                                                \
491     {                                                           \
492         WARNING("Coultn'd parse game from JSON because: %s",    \
493             json_reader_get_error(reader)-> message);           \
494         g_set_error(error, GT_UTILS_ERROR, GT_UTILS_ERROR_JSON, \
495             "Couldn't parse game from JSON because: %s",        \
496             json_reader_get_error(reader)->message);            \
497         return NULL;                                            \
498     } G_STMT_END
499 
500 GtGameData*
utils_parse_game_from_json(JsonReader * reader,GError ** error)501 utils_parse_game_from_json(JsonReader* reader, GError** error)
502 {
503     g_autoptr(GtGameData) data = gt_game_data_new();
504 
505     if (!json_reader_read_member(reader, "_id")) RETURN_JSON_ERROR;
506     JsonNode* node = json_reader_get_value(reader);
507     if (STRING_EQUALS(json_node_type_name(node), "Integer"))
508         data->id = g_strdup_printf("%" G_GINT64_FORMAT, json_reader_get_int_value(reader));
509     else if (STRING_EQUALS(json_node_type_name(node), "String"))
510         data->id = g_strdup(json_reader_get_string_value(reader));
511     else
512     {
513         g_set_error(error, GT_UTILS_ERROR, GT_UTILS_ERROR_JSON,
514             "Unable to parse game from JSON because: '_id' was of incorrect format");
515         return NULL;
516     }
517     json_reader_end_member(reader);
518 
519     if (!json_reader_read_member(reader, "name")) RETURN_JSON_ERROR;
520     data->name = g_strdup(json_reader_get_string_value(reader));
521     json_reader_end_member(reader);
522 
523     if (!json_reader_read_member(reader, "box")) RETURN_JSON_ERROR;
524     if (!json_reader_read_member(reader, "large")) RETURN_JSON_ERROR;
525     data->preview_url = g_strdup(json_reader_get_string_value(reader));
526     json_reader_end_member(reader);
527     json_reader_end_member(reader);
528 
529     if (!json_reader_read_member(reader, "logo")) RETURN_JSON_ERROR;
530     if (!json_reader_read_member(reader, "large")) RETURN_JSON_ERROR;
531     data->logo_url = g_strdup(json_reader_get_string_value(reader));
532     json_reader_end_member(reader);
533     json_reader_end_member(reader);
534 
535     return g_steal_pointer(&data);
536 }
537 
538 #undef RETURN_JSON_ERROR
539 #define RETURN_JSON_ERROR                                       \
540     G_STMT_START                                                \
541     {                                                           \
542         WARNING("Couldn't parse VOD from JSON because: %s",     \
543             json_reader_get_error(reader)-> message);           \
544         g_set_error(error, GT_UTILS_ERROR, GT_UTILS_ERROR_JSON, \
545             "Couldn't parse VOD from JSON because: %s",         \
546             json_reader_get_error(reader)->message);            \
547         return NULL;                                            \
548     } G_STMT_END
549 
550 GtVODData*
utils_parse_vod_from_json(JsonReader * reader,GError ** error)551 utils_parse_vod_from_json(JsonReader* reader, GError** error)
552 {
553     RETURN_VAL_IF_FAIL(JSON_IS_READER(reader), NULL);
554 
555     g_autoptr(GtVODData) data = gt_vod_data_new();
556     GTimeVal time;
557     JsonNode* node = NULL;
558 
559     if (!json_reader_read_member(reader, "_id")) RETURN_JSON_ERROR;
560     data->id = g_strdup(json_reader_get_string_value(reader));
561     json_reader_end_member(reader);
562 
563     if (!json_reader_read_member(reader, "broadcast_id")) RETURN_JSON_ERROR;
564     node = json_reader_get_value(reader);
565     if (STRING_EQUALS(json_node_type_name(node), "Integer"))
566         data->broadcast_id = g_strdup_printf("%" G_GINT64_FORMAT, json_reader_get_int_value(reader));
567     else if (STRING_EQUALS(json_node_type_name(node), "String"))
568         data->broadcast_id = g_strdup(json_reader_get_string_value(reader));
569     else
570     {
571         g_set_error(error, GT_UTILS_ERROR, GT_UTILS_ERROR_JSON,
572             "Unable to parse vod from JSON because: '_id' was of incorrect format");
573         return NULL;
574     }
575     json_reader_end_member(reader);
576 
577     if (!json_reader_read_member(reader, "created_at")) RETURN_JSON_ERROR;
578     if (!g_time_val_from_iso8601(json_reader_get_string_value(reader), &time))
579     {
580         g_set_error(error, GT_UTILS_ERROR, GT_UTILS_ERROR_PARSING_TIME,
581             "Couldn't parse time from string %s", json_reader_get_string_value(reader));
582         return NULL;
583     }
584     data->created_at = g_date_time_new_from_timeval_utc(&time);
585     json_reader_end_member(reader);
586 
587     if (!json_reader_read_member(reader, "published_at")) RETURN_JSON_ERROR;
588     if (!g_time_val_from_iso8601(json_reader_get_string_value(reader), &time))
589     {
590         g_set_error(error, GT_UTILS_ERROR, GT_UTILS_ERROR_PARSING_TIME,
591             "Couldn't parse time from string %s", json_reader_get_string_value(reader));
592         return NULL;
593     }
594     data->published_at = g_date_time_new_from_timeval_utc(&time);
595     json_reader_end_member(reader);
596 
597     if (!json_reader_read_member(reader, "description")) RETURN_JSON_ERROR;
598     data->description = json_reader_get_null_value(reader) ? NULL : g_strdup(json_reader_get_string_value(reader));
599     json_reader_end_member(reader);
600 
601     if (!json_reader_read_member(reader, "game")) RETURN_JSON_ERROR;
602     data->game = json_reader_get_null_value(reader) ? NULL : g_strdup(json_reader_get_string_value(reader));
603     json_reader_end_member(reader);
604 
605     if (!json_reader_read_member(reader, "language")) RETURN_JSON_ERROR;
606     data->language = json_reader_get_null_value(reader) ? NULL : g_strdup(json_reader_get_string_value(reader));
607     json_reader_end_member(reader);
608 
609     if (!json_reader_read_member(reader, "length")) RETURN_JSON_ERROR;
610     data->length = json_reader_get_int_value(reader);
611     json_reader_end_member(reader);
612 
613     /* NOTE: Uncomment when needed */
614     /* if (!json_reader_read_member(reader, "channel")) RETURN_JSON_ERROR; */
615     /* data->channel = utils_parse_channel_from_json(reader, error); */
616     /* if (*error) return NULL; */
617     /* json_reader_end_member(reader); */
618 
619     if (!json_reader_read_member(reader, "preview")) RETURN_JSON_ERROR;
620 
621     if (!json_reader_read_member(reader, "large")) RETURN_JSON_ERROR;
622     data->preview.large = g_strdup(json_reader_get_string_value(reader));
623     json_reader_end_member(reader);
624 
625     if (!json_reader_read_member(reader, "medium")) RETURN_JSON_ERROR;
626     data->preview.medium = g_strdup(json_reader_get_string_value(reader));
627     json_reader_end_member(reader);
628 
629     if (!json_reader_read_member(reader, "small")) RETURN_JSON_ERROR;
630     data->preview.small = g_strdup(json_reader_get_string_value(reader));
631     json_reader_end_member(reader);
632 
633     if (!json_reader_read_member(reader, "template")) RETURN_JSON_ERROR;
634     data->preview.template = g_strdup(json_reader_get_string_value(reader));
635     json_reader_end_member(reader);
636 
637     json_reader_end_member(reader);
638 
639     if (!json_reader_read_member(reader, "title")) RETURN_JSON_ERROR;
640     data->title = g_strdup(json_reader_get_string_value(reader));
641     json_reader_end_member(reader);
642 
643     if (!json_reader_read_member(reader, "url")) RETURN_JSON_ERROR;
644     data->url = g_strdup(json_reader_get_string_value(reader));
645     json_reader_end_member(reader);
646 
647     if (!json_reader_read_member(reader, "views")) RETURN_JSON_ERROR;
648     data->views = json_reader_get_int_value(reader);
649     json_reader_end_member(reader);
650 
651     if (!json_reader_read_member(reader, "tag_list")) RETURN_JSON_ERROR;
652     data->tag_list = g_strdup(json_reader_get_string_value(reader));
653     json_reader_end_member(reader);
654 
655     return g_steal_pointer(&data);
656 }
657 
658 void
gt_playlist_entry_free(GtPlaylistEntry * entry)659 gt_playlist_entry_free(GtPlaylistEntry* entry)
660 {
661     g_free(entry->name);
662     g_free(entry->resolution);
663     g_free(entry->uri);
664     g_slice_free(GtPlaylistEntry, entry);
665 }
666 
667 void
gt_playlist_entry_list_free(GtPlaylistEntryList * list)668 gt_playlist_entry_list_free(GtPlaylistEntryList* list)
669 {
670     g_list_free_full(list, (GDestroyNotify) gt_playlist_entry_free);
671 }
672 
673 GtPlaylistEntryList*
utils_parse_playlist(const gchar * str,GError ** error)674 utils_parse_playlist(const gchar* str, GError** error)
675 {
676     g_autoptr(GtPlaylistEntryList) entries = NULL;
677     g_auto(GStrv) lines = g_strsplit(str, "\n", 0);
678 
679     for (gchar** l = lines; *l != NULL; l++)
680     {
681         if (strncmp(*l, "#EXT-X-STREAM-INF", 17) == 0)
682         {
683             g_autoptr(GtPlaylistEntry) entry = g_slice_new0(GtPlaylistEntry);
684             g_auto(GStrv) values = NULL;
685 
686             if (strncmp(*(l - 1), "#EXT-X-MEDIA", 12) != 0)
687             {
688                 g_set_error(error, GT_UTILS_ERROR, GT_UTILS_ERROR_PARSING_PLAYLIST,
689                     "STREAM-INF entry wasn't preceded by a MEDIA entry");
690                 return NULL;
691             }
692 
693             values = g_strsplit(*(l - 1), ",", 0);
694 
695             for (gchar** m = values; *m != NULL; m++)
696             {
697                 g_auto(GStrv) split = g_strsplit(*m, "=", 0);
698 
699                 if (STRING_EQUALS(*split, "NAME"))
700                 {
701                     /* NOTE: This is to remove quotation marks */
702                     const gchar* name = *(split + 1);
703                     entry->name = g_strndup(name + 1, strlen(name) - 2);
704                     break;
705                 }
706             }
707 
708             values = g_strsplit(*l, ",", 0);
709 
710             for (gchar** m = values; *m != NULL; m++)
711             {
712                 g_auto(GStrv) split = g_strsplit(*m, "=", 0);
713 
714                 if (STRING_EQUALS(*split, "RESOLUTION"))
715                 {
716                     const gchar* resolution = *(split + 1);
717                     entry->resolution = g_strdup(resolution);
718                     break;
719                 }
720             }
721 
722             if (!(g_str_has_prefix(*(l + 1), "https://") || g_str_has_prefix(*(l + 1), "http://")))
723             {
724                 g_set_error(error, GT_UTILS_ERROR, GT_UTILS_ERROR_PARSING_PLAYLIST,
725                     "STREAM-INF entry wasn't succeeded by a uri");
726                 return NULL;
727             }
728 
729             entry->uri = g_strdup(*(l + 1));
730 
731             entries = g_list_append(entries, g_steal_pointer(&entry));
732         }
733     }
734 
735     return g_steal_pointer(&entries);
736 }
737