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