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-vod-container.h"
20 #include "gt-app.h"
21 #include "gt-vod.h"
22 #include "gt-vod-container-child.h"
23 #include "gt-win.h"
24 #include "gt-http.h"
25 #include "utils.h"
26 #include <json-glib/json-glib.h>
27 #include <gtk/gtk.h>
28 #include <glib/gi18n.h>
29
30 #define TAG "GtChannelVODContainer"
31 #include "gnome-twitch/gt-log.h"
32
33 #define CHILD_WIDTH 350
34 #define CHILD_HEIGHT 230
35 #define APPEND_EXTRA TRUE
36
37 typedef struct
38 {
39 gchar* channel_id;
40 JsonParser* json_parser;
41 GCancellable* cancel;
42 } GtChannelVODContainerPrivate;
43
44 G_DEFINE_TYPE_WITH_PRIVATE(GtChannelVODContainer, gt_channel_vod_container, GT_TYPE_ITEM_CONTAINER);
45
46 enum
47 {
48 PROP_0,
49 PROP_CHANNEL_ID,
50 NUM_PROPS
51 };
52
53 static GParamSpec* props[NUM_PROPS];
54
55 static void
get_container_properties(GtItemContainer * self,GtItemContainerProperties * props)56 get_container_properties(GtItemContainer* self, GtItemContainerProperties* props)
57 {
58 props->child_width = CHILD_WIDTH;
59 props->child_height = CHILD_HEIGHT;
60 props->append_extra = APPEND_EXTRA;
61 props->empty_label_text = g_strdup(_("No VODs found"));
62 props->empty_sub_label_text = g_strdup(_("This channel hasn't published any VODs"));
63 props->error_label_text = g_strdup(_("An error occurred when fetching VODs"));
64 props->empty_image_name = g_strdup("face-plain-symbolic");
65 props->fetching_label_text = g_strdup(_("Fetching VODs"));
66 }
67
68 static GtkWidget*
create_child(GtItemContainer * item_container,gpointer data)69 create_child(GtItemContainer* item_container, gpointer data)
70 {
71 RETURN_VAL_IF_FAIL(GT_IS_CHANNEL_VOD_CONTAINER(item_container), NULL);
72 RETURN_VAL_IF_FAIL(GT_IS_VOD(data), NULL);
73
74 return GTK_WIDGET(gt_vod_container_child_new(GT_VOD(data)));
75 }
76
77 static void
activate_child(GtItemContainer * item_container,gpointer child)78 activate_child(GtItemContainer* item_container, gpointer child)
79 {
80 RETURN_IF_FAIL(GT_IS_CHANNEL_VOD_CONTAINER(item_container));
81 RETURN_IF_FAIL(GT_IS_VOD_CONTAINER_CHILD(child));
82
83 GtChannelVODContainer* self = GT_CHANNEL_VOD_CONTAINER(item_container);
84 GtChannelVODContainerPrivate* priv = gt_channel_vod_container_get_instance_private(self);
85
86 GtWin* win = GT_WIN_TOPLEVEL(self);
87
88 RETURN_IF_FAIL(GT_IS_WIN(win));
89
90 gt_win_play_vod(win, GT_VOD_CONTAINER_CHILD(child)->vod);
91 }
92
93 static void
process_json_cb(GObject * source,GAsyncResult * res,gpointer udata)94 process_json_cb(GObject* source,
95 GAsyncResult* res, gpointer udata)
96 {
97 RETURN_IF_FAIL(JSON_IS_PARSER(source));
98 RETURN_IF_FAIL(G_IS_ASYNC_RESULT(res));
99 RETURN_IF_FAIL(udata != NULL);
100
101 g_autoptr(GWeakRef) ref = udata;
102 g_autoptr(GtChannelVODContainer) self = g_weak_ref_get(ref);
103
104 if (!self)
105 {
106 TRACE("Not processing json because we were unreffed while waiting");
107 return;
108 }
109
110 GtChannelVODContainerPrivate* priv = gt_channel_vod_container_get_instance_private(self);
111 g_autoptr(JsonReader) reader = NULL;
112 g_autoptr(GtGameList) items = NULL;
113 g_autoptr(GError) err = NULL;
114
115 json_parser_load_from_stream_finish(priv->json_parser, res, &err);
116
117 if (g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED))
118 {
119 DEBUG("Cancelled");
120 return;
121 }
122 else if (err)
123 {
124 WARNING("Unable to process JSON because: %s", err->message);
125 gt_item_container_show_error(GT_ITEM_CONTAINER(self), err);
126 return;
127 }
128
129 reader = json_reader_new(json_parser_get_root(priv->json_parser));
130
131 if (!json_reader_read_member(reader, "videos"))
132 {
133 WARNING("Unable to process JSON because: %s",
134 json_reader_get_error(reader)->message);
135 gt_item_container_show_error(GT_ITEM_CONTAINER(self),
136 json_reader_get_error(reader));
137 return;
138 }
139
140 for (gint i = 0; i < json_reader_count_elements(reader); i++)
141 {
142 g_autoptr(GtVODData) data = NULL;
143
144 if (!json_reader_read_element(reader, i))
145 {
146 WARNING("Unable to process JSON because: %s",
147 json_reader_get_error(reader)->message);
148 gt_item_container_show_error(GT_ITEM_CONTAINER(self),
149 json_reader_get_error(reader));
150 return;
151 }
152
153 data = utils_parse_vod_from_json(reader, &err);
154
155 if (err)
156 {
157 WARNING("Unable to process JSON because: %s", err->message);
158 gt_item_container_show_error(GT_ITEM_CONTAINER(self), err);
159 return;
160 }
161
162 items = g_list_append(items, gt_vod_new(g_steal_pointer(&data)));
163
164 json_reader_end_element(reader);
165 }
166
167 json_reader_end_member(reader);
168
169 gt_item_container_append_items(GT_ITEM_CONTAINER(self), g_steal_pointer(&items));
170 gt_item_container_set_fetching_items(GT_ITEM_CONTAINER(self), FALSE);
171 }
172
173
174 static void
handle_response_cb(GtHTTP * http,gpointer res,GError * error,gpointer udata)175 handle_response_cb(GtHTTP* http,
176 gpointer res, GError* error, gpointer udata)
177 {
178 RETURN_IF_FAIL(GT_IS_HTTP(http));
179 RETURN_IF_FAIL(udata != NULL);
180
181 g_autoptr(GWeakRef) ref = udata;
182 g_autoptr(GtChannelVODContainer) self = g_weak_ref_get(ref);
183
184 if (!self) {TRACE("Unreffed while waiting"); return;}
185
186 GtChannelVODContainerPrivate* priv = gt_channel_vod_container_get_instance_private(self);
187 GInputStream* istream = res; /* NOTE: Owned by GtHTTP */
188
189 if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
190 {
191 DEBUG("Cancelled");
192 return;
193 }
194 else if (error)
195 {
196 WARNING("Unable to handle response because: %s", error->message);
197 gt_item_container_show_error(GT_ITEM_CONTAINER(self), error);
198 return;
199 }
200
201 RETURN_IF_FAIL(G_IS_INPUT_STREAM(res));
202
203 json_parser_load_from_stream_async(priv->json_parser, res,
204 priv->cancel, process_json_cb, g_steal_pointer(&ref));
205 }
206
207 static void
request_extra_items(GtItemContainer * item_container,gint amount,gint offset)208 request_extra_items(GtItemContainer* item_container,
209 gint amount, gint offset)
210 {
211 RETURN_IF_FAIL(GT_IS_CHANNEL_VOD_CONTAINER(item_container));
212 RETURN_IF_FAIL(amount > 0);
213 RETURN_IF_FAIL(amount <= 100);
214 RETURN_IF_FAIL(offset >= 0);
215
216 GtChannelVODContainer* self = GT_CHANNEL_VOD_CONTAINER(item_container);
217 GtChannelVODContainerPrivate* priv = gt_channel_vod_container_get_instance_private(self);
218 g_autofree gchar* uri = NULL;
219
220 utils_refresh_cancellable(&priv->cancel);
221
222 uri = g_strdup_printf("https://api.twitch.tv/kraken/channels/%s/videos?limit=%d&offset=%d",
223 priv->channel_id, amount, offset);
224
225 gt_http_get_with_category(main_app->http, uri, "item-container", DEFAULT_TWITCH_HEADERS, priv->cancel,
226 G_CALLBACK(handle_response_cb), utils_weak_ref_new(self), GT_HTTP_FLAG_RETURN_STREAM);
227 }
228
229 static void
get_property(GObject * obj,guint prop,GValue * val,GParamSpec * pspec)230 get_property(GObject* obj, guint prop,
231 GValue* val, GParamSpec* pspec)
232 {
233 GtChannelVODContainer* self = GT_CHANNEL_VOD_CONTAINER(obj);
234 GtChannelVODContainerPrivate* priv = gt_channel_vod_container_get_instance_private(self);
235
236 switch (prop)
237 {
238 case PROP_CHANNEL_ID:
239 g_value_set_string(val, priv->channel_id);
240 break;
241 default:
242 G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
243 }
244 }
245
246 static void
set_property(GObject * obj,guint prop,const GValue * val,GParamSpec * pspec)247 set_property(GObject* obj, guint prop,
248 const GValue* val, GParamSpec* pspec)
249 {
250 GtChannelVODContainer* self = GT_CHANNEL_VOD_CONTAINER(obj);
251 GtChannelVODContainerPrivate* priv = gt_channel_vod_container_get_instance_private(self);
252
253 switch (prop)
254 {
255 case PROP_CHANNEL_ID:
256 g_free(priv->channel_id);
257 priv->channel_id = g_value_dup_string(val);
258 break;
259 default:
260 G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
261 }
262 }
263
264 static void
gt_channel_vod_container_class_init(GtChannelVODContainerClass * klass)265 gt_channel_vod_container_class_init(GtChannelVODContainerClass* klass)
266 {
267 G_OBJECT_CLASS(klass)->get_property = get_property;
268 G_OBJECT_CLASS(klass)->set_property = set_property;
269
270 GT_ITEM_CONTAINER_CLASS(klass)->get_container_properties = get_container_properties;
271 GT_ITEM_CONTAINER_CLASS(klass)->request_extra_items = request_extra_items;
272 GT_ITEM_CONTAINER_CLASS(klass)->create_child = create_child;
273 GT_ITEM_CONTAINER_CLASS(klass)->activate_child = activate_child;
274
275 props[PROP_CHANNEL_ID] = g_param_spec_string("channel-id", "Channel ID", "ID of the currently open channel",
276 NULL, G_PARAM_READWRITE);
277
278 g_object_class_install_properties(G_OBJECT_CLASS(klass), NUM_PROPS, props);
279 }
280
281 static void
gt_channel_vod_container_init(GtChannelVODContainer * self)282 gt_channel_vod_container_init(GtChannelVODContainer* self)
283 {
284 GtChannelVODContainerPrivate* priv = gt_channel_vod_container_get_instance_private(self);
285
286 priv->channel_id = NULL;
287 priv->cancel = NULL;
288 priv->json_parser = json_parser_new();
289 }
290