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-channel.h"
20 #include "gt-app.h"
21 #include "gt-http.h"
22 #include <json-glib/json-glib.h>
23 #include <glib/gi18n.h>
24 
25 #define TAG "GtChannel"
26 #include "gnome-twitch/gt-log.h"
27 #include "utils.h"
28 
29 #define N_JSON_PROPS 2
30 
31 typedef struct
32 {
33     GtChannelData* data;
34 
35     GdkPixbuf* preview;
36 
37     gboolean followed;
38 
39     gboolean auto_update;
40     gboolean updating;
41 
42     gboolean error;
43     gchar* error_message;
44     gchar* error_details;
45 
46     guint update_id;
47 
48     JsonParser* json_parser;
49 
50     GCancellable* cancel;
51 } GtChannelPrivate;
52 
53 G_DEFINE_TYPE_WITH_CODE(GtChannel, gt_channel, G_TYPE_INITIALLY_UNOWNED,
54     G_ADD_PRIVATE(GtChannel))
55 
56 enum
57 {
58     PROP_0,
59     PROP_ID,
60     PROP_STATUS,
61     PROP_GAME,
62     PROP_NAME,
63     PROP_DISPLAY_NAME,
64     PROP_PREVIEW_URL,
65     PROP_VIDEO_BANNER_URL,
66     PROP_LOGO_URL,
67     PROP_PROFILE_URL,
68     PROP_PREVIEW,
69     PROP_VIEWERS,
70     PROP_STREAM_STARTED_TIME,
71     PROP_FOLLOWED,
72     PROP_ONLINE,
73     PROP_AUTO_UPDATE,
74     PROP_UPDATING,
75     PROP_ERROR,
76     NUM_PROPS
77 };
78 
79 static GParamSpec* props[NUM_PROPS];
80 
81 static void
channel_followed_cb(GtFollowsManager * mgr,GtChannel * chan,gpointer udata)82 channel_followed_cb(GtFollowsManager* mgr,
83     GtChannel* chan, gpointer udata)
84 {
85     RETURN_IF_FAIL(GT_IS_FOLLOWS_MANAGER(mgr));
86     RETURN_IF_FAIL(GT_IS_CHANNEL(chan));
87     RETURN_IF_FAIL(GT_IS_CHANNEL(udata));
88 
89     GtChannel* self = GT_CHANNEL(udata);
90     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
91 
92     if (!gt_channel_compare(self, chan) && !priv->followed)
93     {
94         GQuark detail = g_quark_from_static_string("followed");
95         g_signal_handlers_block_matched(self, G_SIGNAL_MATCH_DATA | G_SIGNAL_MATCH_DETAIL, 0, detail, NULL, NULL, main_app->fav_mgr);
96         g_object_set(self, "followed", TRUE, NULL);
97         g_signal_handlers_unblock_matched(self, G_SIGNAL_MATCH_DATA | G_SIGNAL_MATCH_DETAIL, 0, detail, NULL, NULL, main_app->fav_mgr);
98     }
99 }
100 
101 static void
channel_unfollowed_cb(GtFollowsManager * mgr,GtChannel * chan,gpointer udata)102 channel_unfollowed_cb(GtFollowsManager* mgr,
103     GtChannel* chan, gpointer udata)
104 {
105     RETURN_IF_FAIL(GT_IS_FOLLOWS_MANAGER(mgr));
106     RETURN_IF_FAIL(GT_IS_CHANNEL(chan));
107     RETURN_IF_FAIL(GT_IS_CHANNEL(udata));
108 
109     GtChannel* self = GT_CHANNEL(udata);
110     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
111 
112     if (!gt_channel_compare(self, chan) && priv->followed)
113     {
114         GQuark detail = g_quark_from_static_string("followed");
115         g_signal_handlers_block_matched(self, G_SIGNAL_MATCH_DATA | G_SIGNAL_MATCH_DETAIL, 0, detail, NULL, NULL, main_app->fav_mgr);
116         g_object_set(self, "followed", FALSE, NULL);
117         g_signal_handlers_unblock_matched(self, G_SIGNAL_MATCH_DATA | G_SIGNAL_MATCH_DETAIL, 0, detail, NULL, NULL, main_app->fav_mgr);
118     }
119 }
120 
121 static gboolean
auto_update_cb(gpointer udata)122 auto_update_cb(gpointer udata)
123 {
124     RETURN_VAL_IF_FAIL(udata != NULL, G_SOURCE_REMOVE);
125 
126     g_autoptr(GtChannel) self = g_weak_ref_get(udata);
127 
128     /* NOTE: This will never happen because we're running on the main
129      * thread but just as a safety pre-caution */
130     if (!self)
131     {
132         DEBUG("Unreffed while waiting");
133 
134         return G_SOURCE_REMOVE;
135     }
136 
137     g_object_set_data_full(G_OBJECT(self), "category",
138         g_strdup("gt-channel-auto-update"), g_free);
139 
140     gt_channel_update(self);
141 
142     return G_SOURCE_CONTINUE;
143 }
144 
145 /* TODO: Move this into set_property */
146 static void
auto_update_set_cb(GObject * src,GParamSpec * pspec,gpointer udata)147 auto_update_set_cb(GObject* src,
148     GParamSpec* pspec, gpointer udata)
149 {
150     RETURN_IF_FAIL(GT_IS_CHANNEL(src));
151 
152     GtChannel* self = GT_CHANNEL(src);
153     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
154 
155     if (priv->auto_update)
156     {
157         priv->update_id = g_timeout_add_seconds_full(G_PRIORITY_LOW, 120, /* TODO: Add the timeout as a setting */
158             auto_update_cb, utils_weak_ref_new(self), (GDestroyNotify) utils_weak_ref_free);
159     }
160     else
161         g_source_remove(priv->update_id);
162 }
163 
164 static gboolean
notify_preview_cb(gpointer udata)165 notify_preview_cb(gpointer udata)
166 {
167     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(udata), G_SOURCE_REMOVE);
168 
169     GtChannel* self = GT_CHANNEL(udata);
170     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
171 
172     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_PREVIEW]);
173 
174     priv->updating = FALSE;
175     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UPDATING]);
176 
177     if (priv->error)
178         g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ERROR]);
179 
180     return G_SOURCE_REMOVE;
181 }
182 
183 static void
handle_preview_download_cb(GObject * source,GAsyncResult * res,gpointer udata)184 handle_preview_download_cb(GObject* source,
185     GAsyncResult* res, gpointer udata)
186 {
187     RETURN_IF_FAIL(G_IS_ASYNC_RESULT(res));
188     RETURN_IF_FAIL(udata != NULL);
189 
190     g_autoptr(GWeakRef) ref = udata;
191     g_autoptr(GtChannel) self = g_weak_ref_get(ref);
192 
193     if (!self) {TRACE("Unreffed while waiting"); return;}
194 
195     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
196     g_autoptr(GError) err = NULL;
197 
198     priv->preview = gdk_pixbuf_new_from_stream_finish(res, &err);
199 
200     if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED))
201     {
202         WARNING("Unable to download preview because: %s", err->message);
203 
204         priv->error_message = g_strdup_printf(_("Unable to update preview image"));
205         priv->error_details = g_strdup_printf(_("Unable to update preview image because: %s"), err->message);
206     }
207 
208     notify_preview_cb(self);
209 }
210 
211 static void
handle_preview_response_cb(GtHTTP * http,gpointer ret,GError * error,gpointer udata)212 handle_preview_response_cb(GtHTTP* http, gpointer ret,
213     GError* error, gpointer udata)
214 {
215     RETURN_IF_FAIL(GT_IS_HTTP(http));
216     RETURN_IF_FAIL(udata != NULL);
217 
218     g_autoptr(GWeakRef) ref = udata;
219     g_autoptr(GtChannel) self = g_weak_ref_get(ref);
220 
221     if (!self) {TRACE("Unreffed while waiting"); return;}
222 
223     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
224     GInputStream* istream = ret;
225 
226     if (error)
227     {
228         if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
229         {
230             WARNING("Unable to send request to download preview because: %s", error->message);
231 
232             priv->error_message = g_strdup_printf(_("Unable to update preview image"));
233             priv->error_details = g_strdup_printf(_("Unable to update preveiw image because: %s"), error->message);
234         }
235 
236         notify_preview_cb(self);
237 
238         return;
239     }
240 
241     gdk_pixbuf_new_from_stream_at_scale_async(istream, 320, 180, FALSE,
242         priv->cancel, handle_preview_download_cb, g_steal_pointer(&ref));
243 }
244 
245 static void
update_preview(GtChannel * self)246 update_preview(GtChannel* self)
247 {
248     RETURN_IF_FAIL(GT_IS_CHANNEL(self));
249 
250     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
251     g_autoptr(GError) err = NULL;
252 
253     if (priv->data->online)
254     {
255         gt_http_get_with_category(main_app->http, priv->data->preview_url, g_object_get_data(G_OBJECT(self), "category"),
256             DEFAULT_TWITCH_HEADERS, priv->cancel, G_CALLBACK(handle_preview_response_cb), utils_weak_ref_new(self),
257             GT_HTTP_FLAG_RETURN_STREAM);
258     }
259     else if (!utils_str_empty(priv->data->video_banner_url))
260     {
261         g_object_set_data_full(G_OBJECT(self), "category", g_strdup("gt-channel-auto-update"), g_free);
262         gt_http_get_with_category(main_app->http, priv->data->video_banner_url, g_object_get_data(G_OBJECT(self), "category"),
263             DEFAULT_TWITCH_HEADERS, priv->cancel, G_CALLBACK(handle_preview_response_cb), utils_weak_ref_new(self),
264             GT_HTTP_FLAG_RETURN_STREAM | GT_HTTP_FLAG_CACHE_RESPONSE);
265     }
266     else
267     {
268         priv->preview = gdk_pixbuf_new_from_resource_at_scale(
269             "/com/vinszent/GnomeTwitch/icons/offline-cover.png", 320, 180, FALSE, &err);
270 
271         if (err)
272         {
273             WARNING("Unable to load default offline preview image from resources because: %s", err->message);
274 
275             priv->error_message = g_strdup_printf(_("Unable to update preview image"));
276             priv->error_details = g_strdup_printf(_("Unable to update preview image because: %s"), err->message);
277         }
278 
279         notify_preview_cb(self);
280     }
281 
282     /* Restore normal category in case it was the auto-update category */
283     g_object_set_data_full(G_OBJECT(self), "category", g_strdup("gt-channel"), g_free);
284 }
285 
286 static void
update_from_data(GtChannel * self,GtChannelData * data)287 update_from_data(GtChannel* self, GtChannelData* data)
288 {
289     RETURN_IF_FAIL(GT_IS_CHANNEL(self));
290     RETURN_IF_FAIL(data != NULL);
291 
292     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
293 
294     g_autoptr(GtChannelData) old_data = priv->data;
295 
296     priv->updating = TRUE;
297     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UPDATING]);
298 
299     priv->data = data;
300 
301     if (old_data)
302     {
303         if (!STRING_EQUALS(old_data->id, data->id))
304         {
305             WARNING("Unable to update channel with id '%s' and name '%s' because: "
306                 "New data with id '%s' does not match the current one",
307                 old_data->id, old_data->name, data->id);
308 
309             priv->error_message = g_strdup("Unable to update data");
310             priv->error_details = g_strdup_printf("Unable to update data for channel with id '%s' and name '%s' because: "
311                 "New data with id '%s' does not match the current one",
312                 old_data->id, old_data->name, data->id);
313 
314             priv->error = TRUE;
315             g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ERROR]);
316 
317             return;
318         }
319 
320         if (!STRING_EQUALS(old_data->name, data->name))
321         {
322             WARNING("Unable to update channel with id '%s' and name '%s' because: "
323                 "New data with name '%s' does not match the current one",
324                 old_data->id, old_data->name, data->name);
325 
326             priv->error_message = g_strdup("Unable to update data");
327             priv->error_details = g_strdup_printf("Unable to update data for channel with id '%s' and name '%s' because: "
328                 "New data with name '%s' does not match the current one",
329                 old_data->id, old_data->name, data->name);
330 
331             priv->error = TRUE;
332             g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ERROR]);
333 
334             return;
335         }
336 
337         if (!STRING_EQUALS(old_data->game, data->game))
338             g_object_notify_by_pspec(G_OBJECT(self), props[PROP_GAME]);
339         if (!STRING_EQUALS(old_data->status, data->status))
340             g_object_notify_by_pspec(G_OBJECT(self), props[PROP_STATUS]);
341         if (!STRING_EQUALS(old_data->display_name, data->display_name))
342             g_object_notify_by_pspec(G_OBJECT(self), props[PROP_DISPLAY_NAME]);
343         if (!STRING_EQUALS(old_data->preview_url, data->preview_url))
344             g_object_notify_by_pspec(G_OBJECT(self), props[PROP_PREVIEW_URL]);
345         if (!STRING_EQUALS(old_data->video_banner_url, data->video_banner_url))
346             g_object_notify_by_pspec(G_OBJECT(self), props[PROP_VIDEO_BANNER_URL]);
347         if (!STRING_EQUALS(old_data->logo_url, data->logo_url))
348             g_object_notify_by_pspec(G_OBJECT(self), props[PROP_LOGO_URL]);
349         if (!STRING_EQUALS(old_data->profile_url, data->profile_url))
350             g_object_notify_by_pspec(G_OBJECT(self), props[PROP_PROFILE_URL]);
351         if (old_data->online != data->online)
352             g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ONLINE]);
353         if (old_data->viewers != data->viewers)
354             g_object_notify_by_pspec(G_OBJECT(self), props[PROP_VIEWERS]);
355         if (data->stream_started_time && old_data->stream_started_time &&
356             g_date_time_compare(old_data->stream_started_time, data->stream_started_time) != 0)
357             g_object_notify_by_pspec(G_OBJECT(self), props[PROP_STREAM_STARTED_TIME]);
358     }
359 
360     update_preview(self);
361 }
362 
363 static void
process_channel_json_cb(GObject * source,GAsyncResult * res,gpointer udata)364 process_channel_json_cb(GObject* source,
365     GAsyncResult* res, gpointer udata)
366 {
367     RETURN_IF_FAIL(JSON_IS_PARSER(source));
368     RETURN_IF_FAIL(G_IS_ASYNC_RESULT(res));
369     RETURN_IF_FAIL(udata != NULL);
370 
371     g_autoptr(GWeakRef) ref = udata;
372     g_autoptr(GtChannel) self = g_weak_ref_get(ref);
373 
374     if (!self) {TRACE("Unreffed while waiting"); return;}
375 
376     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
377     g_autoptr(JsonReader) reader = NULL;
378     g_autoptr(GError) err = NULL;
379     g_autoptr(GtChannelData) chan_data = NULL;
380 
381     json_parser_load_from_stream_finish(priv->json_parser, res, &err);
382 
383     if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED))
384     {
385         DEBUG("Processing json cancelled");
386         return;
387     }
388     else if (err)
389     {
390         WARNING("Unable to update channel data because: %s", err->message);
391 
392         priv->error_message = g_strdup(_("Unable to update channel data"));
393         priv->error_details = g_strdup_printf(_("Unable to update channel data because: %s"), err->message);
394 
395         notify_preview_cb(self);
396 
397         return;
398     }
399 
400     reader = json_reader_new(json_parser_get_root(priv->json_parser));
401 
402     chan_data = utils_parse_channel_from_json(reader, &err);
403 
404     if (err)
405     {
406         WARNING("Unable to update channel data because: %s", err->message);
407 
408         priv->error_message = g_strdup(_("Unable to update channel data"));
409 
410         priv->error_details = g_strdup_printf(_("Unable to update channel data because: %s"), err->message);
411 
412         notify_preview_cb(self);
413 
414         return;
415     }
416 
417     update_from_data(self, g_steal_pointer(&chan_data));
418 }
419 
420 static void
handle_channel_response_cb(GtHTTP * http,gpointer ret,GError * error,gpointer udata)421 handle_channel_response_cb(GtHTTP* http, gpointer ret,
422     GError* error, gpointer udata)
423 {
424     RETURN_IF_FAIL(GT_IS_HTTP(http));
425     RETURN_IF_FAIL(G_IS_INPUT_STREAM(ret));
426     RETURN_IF_FAIL(udata != NULL);
427 
428     g_autoptr(GWeakRef) ref = udata;
429     g_autoptr(GtChannel) self = g_weak_ref_get(ref);
430 
431     if (!self) {TRACE("Unreffed while waiting"); return;}
432 
433     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
434     GInputStream* istream = ret;
435 
436     if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
437     {
438         DEBUG("Cancelled");
439         return;
440     }
441     else if (error)
442     {
443         WARNING("Unable to update channel data because: %s", error->message);
444 
445         priv->error_message = g_strdup(_("Unable to update channel data"));
446         priv->error_details = g_strdup_printf(_("Unable to update channel data because: %s"), error->message);
447 
448         return;
449     }
450 
451     json_parser_load_from_stream_async(priv->json_parser, istream,
452         priv->cancel, process_channel_json_cb, g_steal_pointer(&ref));
453 }
454 
455 static void
process_stream_json_cb(GObject * source,GAsyncResult * res,gpointer udata)456 process_stream_json_cb(GObject* source,
457     GAsyncResult* res, gpointer udata)
458 {
459     RETURN_IF_FAIL(JSON_IS_PARSER(source));
460     RETURN_IF_FAIL(G_IS_ASYNC_RESULT(res));
461     RETURN_IF_FAIL(udata != NULL);
462 
463     g_autoptr(GWeakRef) ref = udata;
464     g_autoptr(GtChannel) self = g_weak_ref_get(ref);
465 
466     if (!self) {TRACE("Unreffed while waiting"); return;}
467 
468     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
469     g_autoptr(JsonReader) reader = NULL;
470     g_autoptr(GtChannelData) chan_data = NULL;
471     g_autoptr(GError) err =NULL;
472 
473     json_parser_load_from_stream_finish(priv->json_parser, res, &err);
474 
475     if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED))
476     {
477         DEBUG("Processing json cancelled");
478         return;
479     }
480     else if (err)
481     {
482         WARNING("Unable to update channel data because: %s", err->message);
483 
484         priv->error_message = g_strdup(_("Unable to update channel data"));
485         priv->error_details = g_strdup_printf(_("Unable to update channel data because: %s"), err->message);
486 
487         notify_preview_cb(self);
488 
489         return;
490     }
491 
492     reader = json_reader_new(json_parser_get_root(priv->json_parser));
493 
494     if (!json_reader_read_member(reader, "stream"))
495     {
496         const GError* err = json_reader_get_error(reader);
497 
498         WARNING("Could not update channel data because: %s", err->message);
499 
500         priv->error_message = g_strdup(_("Could not update channel data"));
501         priv->error_details = g_strdup_printf(_("Could not update channel data because: %s"), err->message);
502 
503         notify_preview_cb(self);
504 
505         return;
506     }
507 
508     if (json_reader_get_null_value(reader))
509     {
510         g_autofree gchar* uri = g_strdup_printf("https://api.twitch.tv/kraken/channels/%s", priv->data->id);
511 
512         gt_http_get_with_category(main_app->http, uri, g_object_get_data(G_OBJECT(self), "category"),
513             DEFAULT_TWITCH_HEADERS, priv->cancel, G_CALLBACK(handle_channel_response_cb), g_steal_pointer(&ref),
514             GT_HTTP_FLAG_RETURN_STREAM | GT_HTTP_FLAG_CACHE_RESPONSE);
515     }
516     else
517     {
518         g_autoptr(GError) err = NULL;
519 
520         chan_data = utils_parse_stream_from_json(reader, &err);
521 
522         if (err)
523         {
524             WARNING("Unable to update channel data because: %s", err->message);
525 
526             priv->error_message = g_strdup(_("Unable to update channel data"));
527             priv->error_details = g_strdup_printf(_("Unable to update channel data because: %s"), err->message);
528 
529             notify_preview_cb(self);
530 
531             return;
532         }
533 
534         update_from_data(self, g_steal_pointer(&chan_data));
535     }
536 }
537 
538 static void
handle_stream_response_cb(GtHTTP * http,gpointer ret,GError * error,gpointer udata)539 handle_stream_response_cb(GtHTTP* http,
540     gpointer ret, GError* error, gpointer udata)
541 {
542     RETURN_IF_FAIL(GT_IS_HTTP(http));
543     RETURN_IF_FAIL(G_IS_INPUT_STREAM(ret));
544     RETURN_IF_FAIL(udata != NULL);
545 
546     g_autoptr(GWeakRef) ref = udata;
547     g_autoptr(GtChannel) self = g_weak_ref_get(ref);
548 
549     if (!self) {TRACE("Unreffed while waiting"); return;}
550 
551     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
552     GInputStream* istream = ret;
553 
554     if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
555     {
556         DEBUG("Cancelled");
557         return;
558     }
559     else if (error)
560     {
561         WARNING("Could not update channel data because: %s", error->message);
562 
563         priv->error_message = g_strdup(_("Could not update channel data"));
564         priv->error_details = g_strdup_printf(_("Could not update channel data because: %s"), error->message);
565 
566         notify_preview_cb(self);
567 
568         return;
569     }
570 
571     json_parser_load_from_stream_async(priv->json_parser, istream,
572         priv->cancel, process_stream_json_cb, g_steal_pointer(&ref));
573 }
574 
575 static void
dispose(GObject * object)576 dispose(GObject* object)
577 {
578     GtChannel* self = GT_CHANNEL(object);
579     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
580 
581     g_cancellable_cancel(priv->cancel);
582 
583     g_clear_object(&priv->cancel);
584     g_clear_object(&priv->preview);
585 
586     G_OBJECT_CLASS(gt_channel_parent_class)->dispose(object);
587 }
588 
589 static void
finalize(GObject * object)590 finalize(GObject* object)
591 {
592     GtChannel* self = (GtChannel*) object;
593     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
594 
595     gt_channel_data_free(priv->data);
596 
597     if (priv->update_id > 0)
598         g_source_remove(priv->update_id);
599 
600     g_signal_handlers_disconnect_by_func(main_app->fav_mgr, channel_followed_cb, self);
601     g_signal_handlers_disconnect_by_func(main_app->fav_mgr, channel_unfollowed_cb, self);
602 
603     G_OBJECT_CLASS(gt_channel_parent_class)->finalize(object);
604 }
605 
606 static void
get_property(GObject * obj,guint prop,GValue * val,GParamSpec * pspec)607 get_property (GObject*    obj,
608               guint       prop,
609               GValue*     val,
610               GParamSpec* pspec)
611 {
612     GtChannel* self = GT_CHANNEL(obj);
613     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
614 
615     switch (prop)
616     {
617         case PROP_ID:
618             g_value_set_string(val, priv->data->id);
619             break;
620         case PROP_STATUS:
621             g_value_set_string(val, priv->data->status);
622             break;
623         case PROP_NAME:
624             g_value_set_string(val, priv->data->name);
625             break;
626         case PROP_DISPLAY_NAME:
627             g_value_set_string(val, priv->data->display_name);
628             break;
629         case PROP_GAME:
630             g_value_set_string(val, priv->data->game);
631             break;
632         case PROP_PREVIEW_URL:
633             g_value_set_string(val, priv->data->preview_url);
634             break;
635         case PROP_VIDEO_BANNER_URL:
636             g_value_set_string(val, priv->data->video_banner_url);
637             break;
638         case PROP_LOGO_URL:
639             g_value_set_string(val, priv->data->logo_url);
640             break;
641         case PROP_PROFILE_URL:
642             g_value_set_string(val, priv->data->profile_url);
643             break;
644         case PROP_VIEWERS:
645             g_value_set_int64(val, priv->data->viewers);
646             break;
647         case PROP_STREAM_STARTED_TIME:
648             g_value_set_pointer(val,
649                 priv->data->stream_started_time ?
650                 g_date_time_ref(priv->data->stream_started_time) : NULL);
651             break;
652         case PROP_ONLINE:
653             g_value_set_boolean(val, priv->data->online);
654             break;
655         case PROP_FOLLOWED:
656             g_value_set_boolean(val, priv->followed);
657             break;
658         case PROP_AUTO_UPDATE:
659             g_value_set_boolean(val, priv->auto_update);
660             break;
661         case PROP_UPDATING:
662             g_value_set_boolean(val, priv->updating);
663             break;
664         case PROP_PREVIEW:
665             g_value_set_object(val, priv->preview);
666             break;
667         case PROP_ERROR:
668             g_value_set_boolean(val, priv->error);
669             break;
670         default:
671             G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
672     }
673 }
674 
675 static void
set_property(GObject * obj,guint prop,const GValue * val,GParamSpec * pspec)676 set_property(GObject*      obj,
677              guint         prop,
678              const GValue* val,
679              GParamSpec*   pspec)
680 {
681     GtChannel* self = GT_CHANNEL(obj);
682     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
683 
684     switch (prop)
685     {
686         case PROP_FOLLOWED:
687             priv->followed = g_value_get_boolean(val);
688             break;
689         case PROP_AUTO_UPDATE:
690             priv->auto_update = g_value_get_boolean(val);
691             break;
692         default:
693             G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
694     }
695 }
696 
697 static void
gt_channel_class_init(GtChannelClass * klass)698 gt_channel_class_init(GtChannelClass* klass)
699 {
700     GObjectClass* object_class = G_OBJECT_CLASS(klass);
701 
702     object_class->finalize = finalize;
703     object_class->dispose = dispose;
704     object_class->get_property = get_property;
705     object_class->set_property = set_property;
706 
707     props[PROP_ID] = g_param_spec_string("id", "ID", "ID of channel",
708         NULL, G_PARAM_READABLE);
709 
710     props[PROP_NAME] = g_param_spec_string("name", "Name", "Name of channel",
711         NULL, G_PARAM_READABLE);
712 
713     props[PROP_STATUS] = g_param_spec_string("status", "Status", "Status of channel",
714         NULL, G_PARAM_READABLE);
715 
716     props[PROP_DISPLAY_NAME] = g_param_spec_string("display-name", "Display Name", "Display Name of channel",
717         NULL, G_PARAM_READABLE);
718 
719     props[PROP_GAME] = g_param_spec_string("game", "Game", "Game being played by channel",
720         NULL, G_PARAM_READABLE);
721 
722     props[PROP_PREVIEW_URL] = g_param_spec_string("preview-url", "Preview Url", "Url for preview image",
723         NULL, G_PARAM_READABLE);
724 
725     props[PROP_VIDEO_BANNER_URL] = g_param_spec_string("video-banner-url", "Video Banner Url", "Url for video banner image",
726         NULL, G_PARAM_READABLE);
727 
728     props[PROP_LOGO_URL] = g_param_spec_string("logo-url", "Logo Url", "Url for logo",
729         NULL, G_PARAM_READABLE);
730 
731     props[PROP_PROFILE_URL] = g_param_spec_string("profile-url", "Profile Url", "Url for profile",
732         NULL, G_PARAM_READABLE);
733 
734     props[PROP_VIEWERS] = g_param_spec_int64("viewers", "Viewers", "Number of viewers",
735         0, G_MAXINT64, 0, G_PARAM_READABLE);
736 
737     props[PROP_PREVIEW] = g_param_spec_object("preview", "Preview", "Preview of channel",
738         GDK_TYPE_PIXBUF, G_PARAM_READABLE);
739 
740     //TODO: Spec this as a boxed type instead
741     props[PROP_STREAM_STARTED_TIME] = g_param_spec_pointer("stream-started-time", "Stream started time", "Stream started time",
742         G_PARAM_READABLE);
743 
744     props[PROP_ONLINE] = g_param_spec_boolean("online", "Online", "Whether the channel is online",
745         TRUE, G_PARAM_READABLE);
746 
747     props[PROP_UPDATING] = g_param_spec_boolean("updating", "Updating", "Whether updating",
748         FALSE, G_PARAM_READABLE);
749 
750     props[PROP_FOLLOWED] = g_param_spec_boolean("followed", "Followed", "Whether the channel is followed",
751         FALSE, G_PARAM_READWRITE);
752 
753     props[PROP_AUTO_UPDATE] = g_param_spec_boolean("auto-update", "Auto Update", "Whether it should update itself automatically",
754         FALSE, G_PARAM_READWRITE);
755 
756     props[PROP_ERROR] = g_param_spec_boolean("error", "Error", "Whether in error state",
757         FALSE, G_PARAM_READABLE);
758 
759     g_object_class_install_properties(object_class, NUM_PROPS, props);
760 }
761 
762 
763 static void
gt_channel_init(GtChannel * self)764 gt_channel_init(GtChannel* self)
765 {
766     g_assert(GT_IS_CHANNEL(self));
767 
768     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
769 
770     priv->data = NULL;
771     priv->updating = FALSE;
772     priv->cancel = g_cancellable_new();
773 
774     priv->update_id = 0;
775 
776     priv->error_message = NULL;
777     priv->error_details = NULL;
778 
779     priv->json_parser = json_parser_new();
780 
781     g_signal_connect(self, "notify::auto-update", G_CALLBACK(auto_update_set_cb), NULL);
782     g_signal_connect(main_app->fav_mgr, "channel-followed", G_CALLBACK(channel_followed_cb), self);
783     g_signal_connect(main_app->fav_mgr, "channel-unfollowed", G_CALLBACK(channel_unfollowed_cb), self);
784 
785     gt_follows_manager_attach_to_channel(main_app->fav_mgr, self);
786 
787     /* NOTE: Set the initial category to the 'default' one */
788     g_object_set_data_full(G_OBJECT(self), "category", g_strdup("gt-channel"), g_free);
789 }
790 
791 GtChannel*
gt_channel_new(GtChannelData * data)792 gt_channel_new(GtChannelData* data)
793 {
794     RETURN_VAL_IF_FAIL(data != NULL, NULL);
795 
796     GtChannel* channel = g_object_new(GT_TYPE_CHANNEL, NULL);
797     GtChannelPrivate* priv = gt_channel_get_instance_private(channel);
798 
799     update_from_data(channel, data);
800 
801     priv->followed = gt_follows_manager_is_channel_followed(main_app->fav_mgr, channel);
802 
803     return channel;
804 }
805 
806 GtChannel*
gt_channel_new_from_id_and_name(const gchar * id,const gchar * name)807 gt_channel_new_from_id_and_name(const gchar* id, const gchar* name)
808 {
809     RETURN_VAL_IF_FAIL(!utils_str_empty(id), NULL);
810     RETURN_VAL_IF_FAIL(!utils_str_empty(name), NULL);
811 
812     GtChannel* channel = g_object_new(GT_TYPE_CHANNEL, NULL);
813     GtChannelPrivate* priv = gt_channel_get_instance_private(channel);
814     GtChannelData* data = gt_channel_data_new();
815 
816     data->id = g_strdup(id);
817     data->name = g_strdup(name);
818 
819     priv->data = data;
820 
821     gt_channel_update(channel);
822 
823     priv->followed = gt_follows_manager_is_channel_followed(main_app->fav_mgr, channel);
824 
825     return channel;
826 }
827 
828 void
gt_channel_toggle_followed(GtChannel * self)829 gt_channel_toggle_followed(GtChannel* self)
830 {
831     RETURN_IF_FAIL(self);
832 
833     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
834 
835     g_object_set(self, "followed", !priv->followed, NULL);
836 }
837 
838 void
gt_channel_list_free(GtChannelList * list)839 gt_channel_list_free(GtChannelList* list)
840 {
841     g_list_free_full(list, g_object_unref);
842 }
843 
844 gboolean
gt_channel_compare(GtChannel * self,gpointer other)845 gt_channel_compare(GtChannel* self, gpointer other)
846 {
847     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
848     gboolean ret = TRUE;
849 
850     if (GT_IS_CHANNEL(other))
851     {
852         GtChannelPrivate* opriv = gt_channel_get_instance_private(GT_CHANNEL(other));
853 
854         ret = !(STRING_EQUALS(priv->data->name, opriv->data->name) &&
855             STRING_EQUALS(priv->data->id, opriv->data->id));
856     }
857 
858     return ret;
859 }
860 
861 const gchar*
gt_channel_get_name(GtChannel * self)862 gt_channel_get_name(GtChannel* self)
863 {
864     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(self), NULL);
865 
866     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
867 
868     return priv->data->name;
869 }
870 
871 const gchar*
gt_channel_get_display_name(GtChannel * self)872 gt_channel_get_display_name(GtChannel* self)
873 {
874     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(self), NULL);
875 
876     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
877 
878     return utils_str_empty(priv->data->game) ?
879         priv->data->name : priv->data->display_name;
880 }
881 
882 const gchar*
gt_channel_get_id(GtChannel * self)883 gt_channel_get_id(GtChannel* self)
884 {
885     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(self), NULL);
886 
887     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
888 
889     return priv->data->id;
890 }
891 
892 const gchar*
gt_channel_get_game_name(GtChannel * self)893 gt_channel_get_game_name(GtChannel* self)
894 {
895     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(self), NULL);
896 
897     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
898 
899     return utils_str_empty(priv->data->game) ? "" : priv->data->game;
900 }
901 
902 const gchar*
gt_channel_get_status(GtChannel * self)903 gt_channel_get_status(GtChannel* self)
904 {
905     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(self), NULL);
906 
907     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
908 
909     return priv->data->status;
910 }
911 
912 gboolean
gt_channel_is_online(GtChannel * self)913 gt_channel_is_online(GtChannel* self)
914 {
915     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(self), FALSE);
916 
917     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
918 
919     return priv->data->online;
920 }
921 
922 gboolean
gt_channel_is_error(GtChannel * self)923 gt_channel_is_error(GtChannel* self)
924 {
925     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(self), FALSE);
926 
927     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
928 
929     return priv->error;
930 }
931 
932 gboolean
gt_channel_is_updating(GtChannel * self)933 gt_channel_is_updating(GtChannel* self)
934 {
935     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(self), FALSE);
936 
937     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
938 
939     return priv->updating;
940 }
941 
942 gboolean
gt_channel_is_followed(GtChannel * self)943 gt_channel_is_followed(GtChannel* self)
944 {
945     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(self), FALSE);
946 
947     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
948 
949     return priv->followed;
950 }
951 
952 gboolean
gt_channel_update(GtChannel * self)953 gt_channel_update(GtChannel* self)
954 {
955     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(self), FALSE);
956 
957     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
958     g_autoptr(SoupMessage) msg = NULL;
959     g_autofree gchar* uri = NULL;
960 
961     utils_refresh_cancellable(&priv->cancel);
962 
963     DEBUG("Initiating update for channel with id '%s' and name '%s'",
964         priv->data->id, priv->data->name);
965 
966     g_clear_pointer(&priv->error_message, g_free);
967     g_clear_pointer(&priv->error_details, g_free);
968 
969     priv->error = FALSE;
970     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_ERROR]);
971 
972     priv->updating = TRUE;
973     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_UPDATING]);
974 
975     uri = g_strdup_printf("https://api.twitch.tv/kraken/streams/%s", priv->data->id);
976 
977     gt_http_get_with_category(main_app->http, uri, g_object_get_data(G_OBJECT(self), "category"),
978         DEFAULT_TWITCH_HEADERS, priv->cancel, G_CALLBACK(handle_stream_response_cb), utils_weak_ref_new(self),
979         GT_HTTP_FLAG_RETURN_STREAM);
980 
981     return TRUE;
982 }
983 
984 const gchar*
gt_channel_get_error_message(GtChannel * self)985 gt_channel_get_error_message(GtChannel* self)
986 {
987     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(self), NULL);
988 
989     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
990 
991     return priv->error_message;
992 }
993 
994 const gchar*
gt_channel_get_error_details(GtChannel * self)995 gt_channel_get_error_details(GtChannel* self)
996 {
997     RETURN_VAL_IF_FAIL(GT_IS_CHANNEL(self), NULL);
998 
999     GtChannelPrivate* priv = gt_channel_get_instance_private(self);
1000 
1001     return priv->error_details;
1002 }
1003 
1004 GtChannelData*
gt_channel_data_new()1005 gt_channel_data_new()
1006 {
1007     return g_slice_new0(GtChannelData);
1008 }
1009 
1010 void
gt_channel_data_free(GtChannelData * data)1011 gt_channel_data_free(GtChannelData* data)
1012 {
1013     if (!data) return;
1014 
1015     g_free(data->name);
1016     g_free(data->id);
1017     g_free(data->game);
1018     g_free(data->status);
1019     g_free(data->display_name);
1020     g_free(data->preview_url);
1021     g_free(data->video_banner_url);
1022     if (data->stream_started_time)
1023         g_date_time_unref(data->stream_started_time);
1024     g_slice_free(GtChannelData, data);
1025 }
1026 
1027 void
gt_channel_data_list_free(GtChannelDataList * list)1028 gt_channel_data_list_free(GtChannelDataList* list)
1029 {
1030     g_list_free_full(list, (GDestroyNotify) gt_channel_data_free);
1031 }
1032 
1033 gint
gt_channel_data_compare(GtChannelData * a,GtChannelData * b)1034 gt_channel_data_compare(GtChannelData* a, GtChannelData* b)
1035 {
1036     if (!a || !b)
1037         return -1;
1038 
1039     if (STRING_EQUALS(a->id, b->id))
1040         return 0;
1041 
1042     return -1;
1043 }
1044