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