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