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 "gt-follows-manager.h"
20 #include "gt-app.h"
21 #include "gt-http.h"
22 #include "gt-win.h"
23 #include "utils.h"
24 #include <json-glib/json-glib.h>
25 #include <glib/gprintf.h>
26 #include <glib/gi18n.h>
27 #include <glib/gstdio.h>
28 
29 #define TAG "GtFollowsManager"
30 #include "gnome-twitch/gt-log.h"
31 
32 #define OLD_FAV_CHANNELS_FILE g_build_filename(g_get_user_data_dir(), "gnome-twitch", "favourite-channels.json", NULL);
33 #define FAV_CHANNELS_FILE g_build_filename(g_get_user_data_dir(), "gnome-twitch", "followed-channels.json", NULL);
34 
35 #define FOLLOWED_CHANNELS_FILE_VERSION 1
36 
37 struct _GtFollowsManagerPrivate
38 {
39     gboolean loading_follows;
40     GCancellable* cancel;
41     JsonParser* json_parser;
42     gint current_offset;
43     GtChannelList* loading_list;
44 };
45 
46 G_DEFINE_TYPE_WITH_PRIVATE(GtFollowsManager, gt_follows_manager, G_TYPE_OBJECT)
47 
48 enum
49 {
50     PROP_0,
51     PROP_LOADING_FOLLOWS,
52     NUM_PROPS
53 };
54 
55 enum
56 {
57     SIG_CHANNEL_FOLLOWED,
58     SIG_CHANNEL_UNFOLLOWED,
59     NUM_SIGS
60 };
61 
62 static GParamSpec* props[NUM_PROPS];
63 
64 static guint sigs[NUM_SIGS];
65 
66 GtFollowsManager*
gt_follows_manager_new(void)67 gt_follows_manager_new(void)
68 {
69     return g_object_new(GT_TYPE_FOLLOWS_MANAGER,
70                         NULL);
71 }
72 
73 static void
channel_online_cb(GObject * source,GParamSpec * pspec,gpointer udata)74 channel_online_cb(GObject* source,
75     GParamSpec* pspec, gpointer udata)
76 {
77     RETURN_IF_FAIL(GT_IS_CHANNEL(source));
78     RETURN_IF_FAIL(GT_IS_FOLLOWS_MANAGER(udata));
79 
80     GtChannel* chan = GT_CHANNEL(source);
81 
82     if (gt_channel_is_online(chan) && gt_app_should_show_notifications(main_app))
83     {
84         g_autoptr(GNotification) notification;
85         g_autofree gchar* title_str = NULL;
86         g_autofree gchar* body_str = NULL;
87         GVariant* var = NULL;
88 
89         title_str = g_strdup_printf(_("%s started streaming %s"),
90             gt_channel_get_display_name(chan), gt_channel_get_game_name(chan));
91         body_str = g_strdup_printf(_("Click here to start watching"));
92 
93         var = g_variant_new_string(gt_channel_get_id(chan));
94 
95         notification = g_notification_new(title_str);
96         g_notification_set_body(notification, body_str);
97         g_notification_set_default_action_and_target_value(notification,
98             "app.open-channel-from-id", var);
99 
100         g_application_send_notification(G_APPLICATION(main_app), NULL, notification);
101     }
102 }
103 
104 static void
105 channel_followed_cb(GObject* source,
106                       GParamSpec* pspec,
107                       gpointer udata);
108 static gboolean
toggle_followed_cb(gpointer udata)109 toggle_followed_cb(gpointer udata)
110 {
111     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(udata), G_SOURCE_REMOVE);
112 
113     GtChannel* chan = GT_CHANNEL(udata);
114     GtFollowsManager* self = main_app->fav_mgr;
115 
116     g_signal_handlers_block_by_func(chan, channel_followed_cb, self);
117     gt_channel_toggle_followed(chan);
118     g_signal_handlers_unblock_by_func(chan, channel_followed_cb, self);
119 
120     return G_SOURCE_REMOVE;
121 }
122 
123 static void
channel_followed_cb(GObject * source,GParamSpec * pspec,gpointer udata)124 channel_followed_cb(GObject* source,
125     GParamSpec* pspec, gpointer udata)
126 {
127     RETURN_IF_FAIL(GT_IS_FOLLOWS_MANAGER(udata));
128     RETURN_IF_FAIL(GT_IS_CHANNEL(source));
129 
130     GtFollowsManager* self = GT_FOLLOWS_MANAGER(udata);
131     GtChannel* chan = GT_CHANNEL(source);
132     g_autoptr(GError) err = NULL;
133     const gchar* name = gt_channel_get_name(chan);
134 
135     if (gt_channel_is_followed(chan))
136     {
137         if (gt_app_is_logged_in(main_app))
138         {
139             gt_twitch_follow_channel(main_app->twitch, name, &err); //TODO: Error handling
140 //            gt_twitch_follow_channel_async(main_app->twitch, gt_channel_get_name(chan), NULL, NULL); //TODO: Error handling
141 
142             if (err)
143             {
144                 GtWin* win = NULL;
145 
146                 WARNING("Unable to follow channel '%s' because: %s",
147                     name, err->message);
148 
149                 win = GT_WIN_ACTIVE;
150 
151                 RETURN_IF_FAIL(GT_IS_WIN(win));
152 
153                 gt_win_show_error_message(win, _("Unable to follow channel"),
154                     "Unable to follow channel '%s' because: %s",
155                     name, err->message);
156 
157                 g_idle_add((GSourceFunc) toggle_followed_cb, chan);
158 
159                 return;
160             }
161         }
162 
163         self->follow_channels = g_list_append(self->follow_channels, chan);
164         g_signal_connect(chan, "notify::online", G_CALLBACK(channel_online_cb), self);
165         g_object_ref(chan);
166 
167         MESSAGEF("Followed channel '%s'", name);
168 
169         g_signal_emit(self, sigs[SIG_CHANNEL_FOLLOWED], 0, chan);
170     }
171     else
172     {
173         GList* found = g_list_find_custom(self->follow_channels, chan, (GCompareFunc) gt_channel_compare);
174 
175         /* NOTE: This should never be NULL */
176         RETURN_IF_FAIL(found != NULL);
177 
178         if (gt_app_is_logged_in(main_app))
179         {
180 //            gt_twitch_unfollow_channel_async(main_app->twitch, gt_channel_get_name(chan), NULL, NULL); //TODO: Error handling
181             gt_twitch_unfollow_channel(main_app->twitch, gt_channel_get_name(chan), &err);
182 
183             if (err)
184             {
185                 GtWin* win = NULL;
186 
187                 WARNING("Unable to unfollow channel '%s' because: %s",
188                     name, err->message);
189 
190                 win = GT_WIN_ACTIVE;
191 
192                 RETURN_IF_FAIL(GT_IS_WIN(win));
193 
194                 gt_win_show_error_message(win, _("Unable to unfollow channel"),
195                     "Unable to unfollow channel '%s' because: %s",
196                     name, err->message);
197 
198                 g_idle_add((GSourceFunc) toggle_followed_cb, chan);
199 
200                 return;
201             }
202         }
203 
204         g_signal_handlers_disconnect_by_func(found->data, channel_online_cb, self);
205 
206         // Remove the link before the signal is emitted
207         self->follow_channels = g_list_remove_link(self->follow_channels, found);
208 
209         MESSAGEF("Unfollowed channel '%s'", gt_channel_get_name(chan));
210 
211         g_signal_emit(self, sigs[SIG_CHANNEL_UNFOLLOWED], 0, found->data);
212 
213         // Unref here so that the GtChannel has a ref while the signal is being emitted
214         g_clear_object(&found->data);
215         g_list_free(found);
216     }
217 }
218 
219 static void
channel_updating_oneshot_cb(GObject * source,GParamSpec * pspec,gpointer udata)220 channel_updating_oneshot_cb(GObject* source,
221     GParamSpec* pspec, gpointer udata)
222 {
223     RETURN_IF_FAIL(GT_IS_FOLLOWS_MANAGER(udata));
224     RETURN_IF_FAIL(GT_IS_CHANNEL(source));
225 
226     GtFollowsManager* self = GT_FOLLOWS_MANAGER(udata);
227     GtChannel* chan = GT_CHANNEL(source);
228 
229     if (!gt_channel_is_updating(chan))
230     {
231         g_signal_connect(chan, "notify::online",
232             G_CALLBACK(channel_online_cb), self);
233 
234         g_signal_handlers_disconnect_by_func(source,
235             channel_updating_oneshot_cb, self);
236     }
237 }
238 
239 static void
shutdown_cb(GApplication * app,gpointer udata)240 shutdown_cb(GApplication* app,
241             gpointer udata)
242 {
243     GtFollowsManager* self = GT_FOLLOWS_MANAGER(udata);
244 
245 //    gt_follows_manager_save(self);
246 }
247 
248 static GList*
load_from_file(const char * filepath,GError ** error)249 load_from_file(const char* filepath, GError** error)
250 {
251     g_autoptr(JsonParser) parser = json_parser_new();
252     g_autoptr(JsonReader) reader = NULL;
253     g_autoptr(GError) err = NULL;
254     GList* ret = NULL;
255 
256     if (!g_file_test(filepath, G_FILE_TEST_EXISTS))
257     {
258         MESSAGE("Follows file at '%s' doesn't exist", filepath);
259 
260         g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
261             "File '%s' does not exist", filepath);
262 
263         return NULL;
264     }
265 
266     json_parser_load_from_file(parser, filepath, error);
267 
268     reader = json_reader_new(json_parser_get_root(parser));
269 
270     for (gint i = 0; i < json_reader_count_elements(reader); i++)
271     {
272         const gchar* id;
273         const gchar* name;
274         JsonNode* id_node = NULL;
275         GtChannel* chan = NULL;
276 
277         if (!json_reader_read_element(reader, i))
278         {
279             *error = g_error_copy(json_reader_get_error(reader));
280 
281             goto error;
282         }
283 
284         if (!json_reader_read_member(reader, "id"))
285         {
286             *error = g_error_copy(json_reader_get_error(reader));
287 
288             goto error;
289         }
290 
291         id_node = json_reader_get_value(reader); //NOTE: Owned by JsonReader, don't unref
292 
293         //NOTE: Backwards compatability for when id's used to be integers
294         if (STRING_EQUALS(json_node_type_name(id_node), "Integer"))
295             id = g_strdup_printf("%" G_GINT64_FORMAT, json_reader_get_int_value(reader));
296         else if (STRING_EQUALS(json_node_type_name(id_node), "String"))
297             id = g_strdup(json_reader_get_string_value(reader));
298         else
299             goto error;
300 
301         json_reader_end_element(reader);
302 
303         if (!json_reader_read_member(reader, "name"))
304         {
305             *error = g_error_copy(json_reader_get_error(reader));
306 
307             goto error;
308         }
309 
310         name = json_reader_get_string_value(reader);
311 
312         json_reader_end_member(reader);
313 
314         json_reader_end_element(reader);
315 
316         chan = gt_channel_new_from_id_and_name(id, name);
317 
318         g_object_ref_sink(chan);
319 
320         ret = g_list_append(ret, chan);
321     }
322 
323     return ret;
324 
325 error:
326     gt_channel_list_free(ret);
327 
328     return ret;
329 }
330 
331 static void
follow_next_channel_cb(GObject * source,GAsyncResult * res,gpointer udata)332 follow_next_channel_cb(GObject* source,
333     GAsyncResult* res, gpointer udata)
334 {
335     RETURN_IF_FAIL(G_IS_ASYNC_RESULT(res));
336     RETURN_IF_FAIL(udata != NULL);
337 
338     GList* l = udata;
339     g_autoptr(GError) err = NULL;
340     g_task_propagate_pointer(G_TASK(res), &err); //NOTE: Doesn't return a value
341 
342     if (err)
343     {
344         GList* last = g_list_last(l);
345 
346         RETURN_IF_FAIL(GT_IS_FOLLOWS_MANAGER(last->data));
347 
348         GtFollowsManager* self = last->data;
349         GtFollowsManagerPrivate* priv = gt_follows_manager_get_instance_private(udata);
350         GtWin* win = NULL;
351 
352         win = GT_WIN_ACTIVE;
353 
354         RETURN_IF_FAIL(GT_IS_WIN(win));
355 
356         gt_win_show_error_message(win, _("Unable to move your local follows to Twitch"),
357             "Unable to move your local follows to Twitch because: %s", err->message);
358 
359         priv->loading_follows = TRUE;
360         g_object_notify_by_pspec(G_OBJECT(self), props[PROP_LOADING_FOLLOWS]);
361 
362         return;
363     }
364 
365     if (GT_IS_CHANNEL(l->data))
366     {
367         gt_twitch_follow_channel_async(main_app->twitch,
368             gt_channel_get_name(GT_CHANNEL(l->data)),
369             follow_next_channel_cb, l->next);
370     }
371     else if (GT_IS_FOLLOWS_MANAGER(l->data))
372     {
373         g_autofree gchar* fp = FAV_CHANNELS_FILE;
374         g_autofree gchar* new_fp = g_strconcat(fp, ".bak", NULL);
375 
376         g_rename(fp, new_fp);
377 
378         gt_follows_manager_load_from_twitch(GT_FOLLOWS_MANAGER(l->data));
379     }
380     else
381         RETURN_IF_REACHED();
382 }
383 
384 static void
move_local_follows_cb(GtkInfoBar * bar,gint res,gpointer udata)385 move_local_follows_cb(GtkInfoBar* bar,
386     gint res, gpointer udata)
387 {
388     RETURN_IF_FAIL(GT_IS_FOLLOWS_MANAGER(udata));
389 
390     GtFollowsManager* self = GT_FOLLOWS_MANAGER(udata);
391 
392     if (res == GTK_RESPONSE_YES)
393     {
394         g_autofree gchar* filepath = FAV_CHANNELS_FILE;
395         g_autoptr(GError) err = NULL;
396         GList* channels = NULL;
397 
398         channels = load_from_file(filepath, &err);
399 
400         if (err)
401         {
402             GtWin* win;
403 
404             WARNING("Unable to move local follows to Twitch because: %s", err->message);
405 
406             win = GT_WIN_ACTIVE;
407 
408             RETURN_IF_FAIL(GT_IS_WIN(win));
409 
410             gt_win_show_error_message(win, _("Unable to move your local follows to Twitch"),
411                 "Unable to move local follows to Twitch because: %s", err->message);
412 
413             return;
414         }
415 
416         channels = g_list_append(channels, self);
417 
418         gt_twitch_follow_channel_async(main_app->twitch,
419             gt_channel_get_name(GT_CHANNEL(channels->data)),
420             follow_next_channel_cb, channels->next);
421     }
422     else
423     {
424         g_autofree gchar* fp = FAV_CHANNELS_FILE;
425         g_autofree gchar* new_fp = g_strconcat(fp, ".bak", NULL);
426 
427         g_rename(fp, new_fp);
428 
429         gt_follows_manager_load_from_twitch(self);
430     }
431 }
432 
433 static void
logged_in_cb(GObject * source,GParamSpec * pspec,gpointer udata)434 logged_in_cb(GObject* source,
435              GParamSpec* pspec,
436              gpointer udata)
437 {
438     GtFollowsManager* self = GT_FOLLOWS_MANAGER(udata);
439 
440     if (gt_app_is_logged_in(main_app))
441     {
442         g_autofree gchar* filepath = FAV_CHANNELS_FILE;
443 
444         if (g_file_test(filepath, G_FILE_TEST_EXISTS))
445         {
446             gt_win_ask_question(GT_WIN_ACTIVE,
447                 _("GNOME Twitch has detected local follows, would you like to move them to Twitch?"),
448                 G_CALLBACK(move_local_follows_cb), self);
449         }
450         else
451             gt_follows_manager_load_from_twitch(self);
452     }
453     else
454         gt_follows_manager_load_from_file(self);
455 }
456 
457 static void
458 handle_channels_response_cb(GtHTTP* http,
459     gpointer res, GError* error, gpointer udata);
460 
461 static void
process_channels_json_cb(GObject * source,GAsyncResult * res,gpointer udata)462 process_channels_json_cb(GObject* source,
463     GAsyncResult* res, gpointer udata)
464 {
465     RETURN_IF_FAIL(JSON_IS_PARSER(source));
466     RETURN_IF_FAIL(G_IS_ASYNC_RESULT(res));
467     RETURN_IF_FAIL(udata != NULL);
468 
469     g_autoptr(GWeakRef) ref = udata;
470     g_autoptr(GtFollowsManager) self = g_weak_ref_get(ref);
471 
472     if (!self)
473     {
474         TRACE("Not processing followed streams json because we were unreffed while waiting");
475         return;
476     }
477 
478     GtFollowsManagerPrivate* priv = gt_follows_manager_get_instance_private(self);
479     g_autoptr(JsonReader) reader = NULL;
480     g_autoptr(GError) err = NULL;
481     gint num_elements;
482     gint64 total;
483 
484     json_parser_load_from_stream_finish(priv->json_parser, res, &err);
485 
486     RETURN_IF_ERROR(err); /* FIXME: Handle error */
487 
488     reader = json_reader_new(json_parser_get_root(priv->json_parser));
489 
490     if (!json_reader_read_member(reader, "_total")) RETURN_IF_REACHED(); /* FIXME: Handle error */
491     total = json_reader_get_int_value(reader);
492     json_reader_end_member(reader);
493 
494     if (!json_reader_read_member(reader, "follows")) RETURN_IF_REACHED(); /* FIXME: Handle error */
495 
496     num_elements = json_reader_count_elements(reader);
497 
498     for (gint i = 0; i < num_elements; i++)
499     {
500         GtChannelData* data = NULL;
501         gboolean found = FALSE;
502 
503         if (!json_reader_read_element(reader, i)) RETURN_IF_REACHED(); /* FIXME: Handle error */
504         if (!json_reader_read_member(reader, "channel")) RETURN_IF_REACHED(); /* FIXME: Handle error */
505 
506         data = utils_parse_channel_from_json(reader, &err);
507 
508         RETURN_IF_ERROR(err); /* FIXME: Handle error */
509 
510         for (GList* l = priv->loading_list; l != NULL; l = l->next)
511         {
512             GtChannel* chan = GT_CHANNEL(l->data);
513 
514             if (STRING_EQUALS(gt_channel_get_id(chan), data->id)
515                 && STRING_EQUALS(gt_channel_get_name(chan), data->name))
516             {
517                 gt_channel_data_free(data);
518                 found = TRUE;
519 
520                 break;
521             }
522         }
523 
524         if (!found)
525             priv->loading_list = g_list_append(priv->loading_list, gt_channel_new(data));
526 
527         json_reader_end_member(reader);
528         json_reader_end_element(reader);
529     }
530 
531     json_reader_end_member(reader);
532 
533     priv->current_offset += num_elements;
534 
535     const GtOAuthInfo* info = gt_app_get_oauth_info(main_app);
536 
537     if (priv->current_offset < total)
538     {
539         g_autofree gchar* uri = NULL;
540 
541         uri = g_strdup_printf("https://api.twitch.tv/kraken/users/%s/follows/channels?limit=%d&offset=%d",
542             info->user_id, 100, priv->current_offset);
543 
544         gt_http_get_with_category(main_app->http, uri, "gt-follows-manager", DEFAULT_TWITCH_HEADERS, priv->cancel,
545             G_CALLBACK(handle_channels_response_cb), g_steal_pointer(&ref), GT_HTTP_FLAG_RETURN_STREAM);
546     }
547     else
548     {
549         for (GList* l = priv->loading_list; l != NULL; l = l->next)
550         {
551             GtChannel* chan = l->data;
552 
553             RETURN_IF_FAIL(GT_IS_CHANNEL(chan));
554             RETURN_IF_FAIL(g_object_is_floating(chan));
555 
556             g_object_ref_sink(chan);
557 
558             g_signal_connect(chan, "notify::updating",
559                 G_CALLBACK(channel_updating_oneshot_cb), self);
560 
561             g_signal_handlers_block_by_func(chan, channel_followed_cb, self);
562 
563             g_object_set(chan,
564                 "auto-update", TRUE,
565                 "followed", TRUE,
566                 NULL);
567             g_signal_emit(self, sigs[SIG_CHANNEL_FOLLOWED], 0, chan);
568 
569             g_signal_handlers_unblock_by_func(chan, channel_followed_cb, self);
570         }
571 
572         gt_channel_list_free(self->follow_channels);
573         self->follow_channels = priv->loading_list;
574         priv->loading_list = NULL;
575 
576         priv->loading_follows = FALSE;
577         g_object_notify_by_pspec(G_OBJECT(self), props[PROP_LOADING_FOLLOWS]);
578 
579         MESSAGE("Loaded '%d' follows", g_list_length(self->follow_channels));
580     }
581 }
582 
583 static void
handle_channels_response_cb(GtHTTP * http,gpointer res,GError * error,gpointer udata)584 handle_channels_response_cb(GtHTTP* http,
585     gpointer res, GError* error, gpointer udata)
586 {
587     RETURN_IF_FAIL(GT_IS_HTTP(http));
588     RETURN_IF_FAIL(udata != NULL);
589 
590     g_autoptr(GWeakRef) ref = udata;
591     g_autoptr(GtFollowsManager) self = g_weak_ref_get(ref);
592 
593     if (!self) {TRACE("Unreffed while waiting"); return;}
594 
595     GtFollowsManagerPrivate* priv = gt_follows_manager_get_instance_private(self);
596 
597     RETURN_IF_ERROR(error); /* FIXME: Handle error */
598 
599     RETURN_IF_FAIL(G_IS_INPUT_STREAM(res));
600 
601     json_parser_load_from_stream_async(priv->json_parser, res, priv->cancel,
602         process_channels_json_cb, g_steal_pointer(&ref));
603 }
604 
605 static void
606 handle_streams_response_cb(GtHTTP* http,
607     gpointer res, GError* error, gpointer udata);
608 
609 static void
process_streams_json_cb(GObject * source,GAsyncResult * res,gpointer udata)610 process_streams_json_cb(GObject* source,
611     GAsyncResult* res, gpointer udata)
612 {
613     RETURN_IF_FAIL(JSON_IS_PARSER(source));
614     RETURN_IF_FAIL(G_IS_ASYNC_RESULT(res));
615     RETURN_IF_FAIL(udata != NULL);
616 
617     g_autoptr(GWeakRef) ref = udata;
618     g_autoptr(GtFollowsManager) self = g_weak_ref_get(ref);
619 
620     if (!self)
621     {
622         TRACE("Not processing followed channels json because we were unreffed while waiting");
623         return;
624     }
625 
626     GtFollowsManagerPrivate* priv = gt_follows_manager_get_instance_private(self);
627     g_autoptr(JsonReader) reader = NULL;
628     g_autoptr(GError) err = NULL;
629     gint num_elements;
630     gint64 total;
631 
632     json_parser_load_from_stream_finish(priv->json_parser, res, &err);
633 
634     RETURN_IF_ERROR(err); /* FIXME: Handle error */
635 
636     reader = json_reader_new(json_parser_get_root(priv->json_parser));
637 
638     if (!json_reader_read_member(reader, "_total")) RETURN_IF_REACHED(); /* FIXME: Handle error */
639     total = json_reader_get_int_value(reader);
640     json_reader_end_member(reader);
641 
642     if (!json_reader_read_member(reader, "streams")) RETURN_IF_REACHED(); /* FIXME: Handle error */
643 
644     num_elements = json_reader_count_elements(reader);
645 
646     for (gint i = 0; i < num_elements; i++)
647     {
648         json_reader_read_element(reader, i);
649 
650         priv->loading_list = g_list_append(priv->loading_list,
651             gt_channel_new(utils_parse_stream_from_json(reader, &err)));
652 
653         RETURN_IF_ERROR(err); /* FIXME: Handle error */
654 
655         json_reader_end_element(reader);
656     }
657 
658     json_reader_end_member(reader);
659 
660     priv->current_offset += num_elements;
661 
662     const GtOAuthInfo* info = gt_app_get_oauth_info(main_app);
663 
664     g_autofree gchar* uri = NULL;
665 
666     if (priv->current_offset < total)
667     {
668         uri = g_strdup_printf("https://api.twitch.tv/kraken/streams/followed?oauth_token=%s&limit=%d&offset=%d&stream_type=live",
669             info->oauth_token, 100, priv->current_offset);
670 
671         gt_http_get_with_category(main_app->http, uri, "gt-follows-manager", DEFAULT_TWITCH_HEADERS, priv->cancel,
672             G_CALLBACK(handle_streams_response_cb), g_steal_pointer(&ref), GT_HTTP_FLAG_RETURN_STREAM);
673     }
674     else
675     {
676         priv->current_offset = 0;
677 
678         uri = g_strdup_printf("https://api.twitch.tv/kraken/users/%s/follows/channels?limit=%d&offset=%d",
679             info->user_id, 100, 0);
680 
681         gt_http_get_with_category(main_app->http, uri, "gt-follows-manager", DEFAULT_TWITCH_HEADERS, priv->cancel,
682             G_CALLBACK(handle_channels_response_cb), g_steal_pointer(&ref), GT_HTTP_FLAG_RETURN_STREAM);
683     }
684 }
685 
686 static void
handle_streams_response_cb(GtHTTP * http,gpointer res,GError * error,gpointer udata)687 handle_streams_response_cb(GtHTTP* http,
688     gpointer res, GError* error, gpointer udata)
689 {
690     RETURN_IF_FAIL(GT_IS_HTTP(http));
691     RETURN_IF_FAIL(udata != NULL);
692 
693     g_autoptr(GWeakRef) ref = udata;
694     g_autoptr(GtFollowsManager) self = g_weak_ref_get(ref);
695 
696     if (!self) {TRACE("Unreffed while waiting"); return;}
697 
698     GtFollowsManagerPrivate* priv = gt_follows_manager_get_instance_private(self);
699 
700     RETURN_IF_ERROR(error); /* FIXME: Handle error */
701 
702     RETURN_IF_FAIL(G_IS_INPUT_STREAM(res));
703 
704     json_parser_load_from_stream_async(priv->json_parser, res, priv->cancel,
705         process_streams_json_cb, g_steal_pointer(&ref));
706 }
707 
708 static void
finalize(GObject * object)709 finalize(GObject* object)
710 {
711     GtFollowsManager* self = (GtFollowsManager*) object;
712 
713     gt_channel_list_free(self->follow_channels);
714 
715     G_OBJECT_CLASS(gt_follows_manager_parent_class)->finalize(object);
716 }
717 
718 static void
get_property(GObject * obj,guint prop,GValue * val,GParamSpec * pspec)719 get_property (GObject*    obj,
720               guint       prop,
721               GValue*     val,
722               GParamSpec* pspec)
723 {
724     GtFollowsManager* self = GT_FOLLOWS_MANAGER(obj);
725     GtFollowsManagerPrivate* priv = gt_follows_manager_get_instance_private(self);
726 
727     switch (prop)
728     {
729         case PROP_LOADING_FOLLOWS:
730             g_value_set_boolean(val, priv->loading_follows);
731             break;
732         default:
733             G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
734     }
735 }
736 
737 static void
set_property(GObject * obj,guint prop,const GValue * val,GParamSpec * pspec)738 set_property(GObject*      obj,
739              guint         prop,
740              const GValue* val,
741              GParamSpec*   pspec)
742 {
743     GtFollowsManager* self = GT_FOLLOWS_MANAGER(obj);
744 
745     switch (prop)
746     {
747         default:
748             G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
749     }
750 }
751 
752 static void
gt_follows_manager_class_init(GtFollowsManagerClass * klass)753 gt_follows_manager_class_init(GtFollowsManagerClass* klass)
754 {
755     GObjectClass* object_class = G_OBJECT_CLASS(klass);
756 
757     object_class->finalize = finalize;
758     object_class->get_property = get_property;
759     object_class->set_property = set_property;
760 
761     props[PROP_LOADING_FOLLOWS] = g_param_spec_boolean("loading-follows",
762         "Loading follows", "Whether loading follows", FALSE, G_PARAM_READABLE);
763 
764     sigs[SIG_CHANNEL_FOLLOWED] = g_signal_new("channel-followed",
765         GT_TYPE_FOLLOWS_MANAGER, G_SIGNAL_RUN_LAST, 0,
766         NULL, NULL, NULL, G_TYPE_NONE, 1, GT_TYPE_CHANNEL);
767 
768     sigs[SIG_CHANNEL_UNFOLLOWED] = g_signal_new("channel-unfollowed",
769         GT_TYPE_FOLLOWS_MANAGER, G_SIGNAL_RUN_LAST, 0,
770         NULL, NULL, NULL, G_TYPE_NONE, 1, GT_TYPE_CHANNEL);
771 }
772 
773 static void
gt_follows_manager_init(GtFollowsManager * self)774 gt_follows_manager_init(GtFollowsManager* self)
775 {
776     g_assert(GT_IS_FOLLOWS_MANAGER(self));
777 
778     GtFollowsManagerPrivate* priv = gt_follows_manager_get_instance_private(self);
779 
780     priv->json_parser = json_parser_new();
781     priv->loading_list = NULL;
782 
783     self->follow_channels = NULL;
784 
785     g_autofree gchar* old_fp = OLD_FAV_CHANNELS_FILE;
786     g_autofree gchar* new_fp = FAV_CHANNELS_FILE;
787 
788     g_signal_connect(main_app, "shutdown", G_CALLBACK(shutdown_cb), self);
789     g_signal_connect(main_app, "notify::logged-in", G_CALLBACK(logged_in_cb), self);
790 
791     //TODO: Remove this in a release or two
792     if (g_file_test(old_fp, G_FILE_TEST_EXISTS))
793         g_rename(old_fp, new_fp);
794 }
795 
796 void
gt_follows_manager_load_from_twitch(GtFollowsManager * self)797 gt_follows_manager_load_from_twitch(GtFollowsManager* self)
798 {
799     RETURN_IF_FAIL(GT_IS_FOLLOWS_MANAGER(self));
800 
801     GtFollowsManagerPrivate* priv = gt_follows_manager_get_instance_private(self);
802 
803     priv->loading_follows = TRUE;
804     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_LOADING_FOLLOWS]);
805 
806     const GtOAuthInfo* info = gt_app_get_oauth_info(main_app);
807     g_autofree gchar* uri = NULL;
808 
809     utils_refresh_cancellable(&priv->cancel);
810 
811     priv->current_offset = 0;
812 
813     uri = g_strdup_printf("https://api.twitch.tv/kraken/streams/followed?oauth_token=%s&limit=%d&offset=%d&stream_type=live",
814         info->oauth_token, 100, 0);
815 
816     gt_http_get_with_category(main_app->http, uri, "gt-follows-manager", DEFAULT_TWITCH_HEADERS, priv->cancel,
817         G_CALLBACK(handle_streams_response_cb), utils_weak_ref_new(self), GT_HTTP_FLAG_RETURN_STREAM);
818 }
819 
820 void
gt_follows_manager_load_from_file(GtFollowsManager * self)821 gt_follows_manager_load_from_file(GtFollowsManager* self)
822 {
823     RETURN_IF_FAIL(GT_IS_FOLLOWS_MANAGER(self));
824 
825     GtFollowsManagerPrivate* priv = gt_follows_manager_get_instance_private(self);
826     g_autofree gchar* filepath = FAV_CHANNELS_FILE;
827     g_autoptr(GError) err = NULL;
828 
829     priv->loading_follows = TRUE;
830     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_LOADING_FOLLOWS]);
831 
832     g_clear_pointer(&self->follow_channels,
833         (GDestroyNotify) gt_channel_list_free);
834 
835     self->follow_channels = load_from_file(filepath, &err);
836 
837     if (err)
838     {
839         if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
840             goto finish;
841         else
842         {
843             GtWin* win = GT_WIN_ACTIVE;
844 
845             RETURN_IF_FAIL(GT_IS_WIN(win));
846 
847             WARNING("Error loading followed channels from file because: %s", err->message);
848 
849             gt_win_show_error_message(win, _("Unable to load followed channels from file"),
850                 "Unable to load followed channels from file because: %s", err->message);
851         }
852 
853         return;
854     }
855 
856     for (GList* l = self->follow_channels; l != NULL; l = l->next)
857     {
858         GtChannel* chan = l->data;
859 
860         RETURN_IF_FAIL(GT_IS_CHANNEL(chan));
861 
862         g_signal_handlers_block_by_func(chan, channel_followed_cb, self);
863 
864         g_object_set(chan,
865             "auto-update", TRUE,
866             "followed", TRUE,
867             NULL);
868 
869         g_signal_handlers_unblock_by_func(chan, channel_followed_cb, self);
870 
871         g_signal_connect(chan, "notify::updating", G_CALLBACK(channel_updating_oneshot_cb), self);
872     }
873 
874     MESSAGE("Loaded '%d' follows from file", g_list_length(self->follow_channels));
875 
876 finish:
877     priv->loading_follows = FALSE;
878     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_LOADING_FOLLOWS]);
879 
880     return;
881 }
882 
883 void
gt_follows_manager_save(GtFollowsManager * self)884 gt_follows_manager_save(GtFollowsManager* self)
885 {
886     RETURN_IF_FAIL(GT_IS_FOLLOWS_MANAGER(self));
887 
888     g_autofree gchar* fp = FAV_CHANNELS_FILE;
889 
890     MESSAGE("Saving follows to file '%s'", fp);
891 
892     if (g_list_length(self->follow_channels) == 0)
893     {
894         if (g_file_test(fp, G_FILE_TEST_EXISTS))
895         {
896             g_remove(fp);
897             g_free(fp);
898         }
899 
900         return;
901     }
902 
903     g_autoptr(JsonBuilder) builder = json_builder_new();
904     g_autoptr(JsonGenerator) gen = json_generator_new();
905     g_autoptr(JsonNode) final = NULL;
906 
907     //NOTE: I'll just leave this here in case I want to include
908     //a version tag in the future
909 
910     /* json_builder_begin_object(builder); */
911 
912     /* json_builder_set_member_name(builder, "version"); */
913     /* json_builder_add_int_value(builder, FOLLOWED_CHANNELS_FILE_VERSION); */
914     /* json_builder_end_object(builder); */
915 
916     /* json_builder_set_member_name(builder, "followed-channels"); */
917 
918     json_builder_begin_array(builder);
919 
920     for (GList* l = self->follow_channels; l != NULL; l = l->next)
921     {
922         json_builder_begin_object(builder);
923 
924         json_builder_set_member_name(builder, "id");
925         json_builder_add_string_value(builder, gt_channel_get_id(l->data));
926 
927         json_builder_set_member_name(builder, "name");
928         json_builder_add_string_value(builder, gt_channel_get_name(l->data));
929 
930         json_builder_end_object(builder);
931     }
932 
933     json_builder_end_array(builder);
934 
935     /* json_builder_end_object(builder); */
936 
937     final = json_builder_get_root(builder);
938 
939     json_generator_set_root(gen, final);
940     json_generator_to_file(gen, fp, NULL);
941 }
942 
943 gboolean
gt_follows_manager_is_channel_followed(GtFollowsManager * self,GtChannel * chan)944 gt_follows_manager_is_channel_followed(GtFollowsManager* self, GtChannel* chan)
945 {
946     RETURN_VAL_IF_FAIL(GT_IS_FOLLOWS_MANAGER(self), FALSE);
947     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(chan), FALSE);
948 
949     return g_list_find_custom(self->follow_channels, chan, (GCompareFunc) gt_channel_compare) != NULL;
950 }
951 
952 gboolean
gt_follows_manager_is_loading_follows(GtFollowsManager * self)953 gt_follows_manager_is_loading_follows(GtFollowsManager* self)
954 {
955     RETURN_VAL_IF_FAIL(GT_IS_FOLLOWS_MANAGER(self), FALSE);
956 
957     GtFollowsManagerPrivate* priv = gt_follows_manager_get_instance_private(self);
958 
959     return priv->loading_follows;
960 }
961 
962 void
gt_follows_manager_attach_to_channel(GtFollowsManager * self,GtChannel * chan)963 gt_follows_manager_attach_to_channel(GtFollowsManager* self, GtChannel* chan)
964 {
965     RETURN_IF_FAIL(GT_IS_FOLLOWS_MANAGER(self));
966     RETURN_IF_FAIL(GT_IS_CHANNEL(chan));
967 
968     g_signal_connect(chan, "notify::followed", G_CALLBACK(channel_followed_cb), self);
969 }
970 
971 void
gt_follows_manager_refresh(GtFollowsManager * self)972 gt_follows_manager_refresh(GtFollowsManager* self)
973 {
974     RETURN_IF_FAIL(GT_IS_FOLLOWS_MANAGER(self));
975 
976     if (gt_app_is_logged_in(main_app))
977         gt_follows_manager_load_from_twitch(self);
978     else
979         g_list_foreach(self->follow_channels, (GFunc) gt_channel_update, NULL);
980 }
981