twitch.c - frontends - front-ends for some sites (experiment)
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
(DIR) LICENSE
---
twitch.c (12841B)
---
1 #include <sys/socket.h>
2 #include <sys/types.h>
3
4 #include <ctype.h>
5 #include <errno.h>
6 #include <netdb.h>
7 #include <stdarg.h>
8 #include <stdint.h>
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <string.h>
12 #include <time.h>
13 #include <unistd.h>
14
15 #include "https.h"
16 #include "json.h"
17 #include "twitch.h"
18 #include "util.h"
19
20 #ifndef TWITCH_API_KEY
21 #error "make sure set a TWITCH_API_KEY in twitch.c"
22 #define TWITCH_API_KEY "API key here"
23 #endif
24 static const char *twitch_headers = "Client-ID: " TWITCH_API_KEY "\r\n";
25
26 static char *
27 twitch_request(const char *path)
28 {
29 return request("api.twitch.tv", path, twitch_headers);
30 }
31
32 /* unmarshal JSON response, skip HTTP headers */
33 int
34 json_unmarshal(const char *data,
35 void (*cb)(struct json_node *, size_t, const char *, size_t, void *),
36 void *pp)
37 {
38 const char *s;
39
40 /* strip/skip header part */
41 if (!(s = strstr(data, "\r\n\r\n"))) {
42 fprintf(stderr, "error parsing HTTP response header\n");
43 return -1; /* invalid response */
44 }
45 s += strlen("\r\n\r\n");
46
47 /* parse */
48 if (parsejson(s, strlen(s), cb, pp) < 0) {
49 fprintf(stderr, "error parsing JSON\n");
50 return -1;
51 }
52
53 return 0;
54 }
55
56 char *
57 twitch_games_bygameids_data(const char *param)
58 {
59 char path[4096];
60 int r;
61
62 r = snprintf(path, sizeof(path), "/helix/games?%s", param);
63 if (r < 0 || (size_t)r >= sizeof(path))
64 return NULL;
65
66 return twitch_request(path);
67 }
68
69 char *
70 twitch_users_byuserids_data(const char *param)
71 {
72 char path[4096];
73 int r;
74
75 r = snprintf(path, sizeof(path), "/helix/users?%s", param);
76 if (r < 0 || (size_t)r >= sizeof(path))
77 return NULL;
78
79 return twitch_request(path);
80 }
81
82 char *
83 twitch_users_bylogin_data(const char *login)
84 {
85 char path[256];
86 int r;
87
88 r = snprintf(path, sizeof(path), "/helix/users?login=%s", login);
89 if (r < 0 || (size_t)r >= sizeof(path))
90 return NULL;
91
92 return twitch_request(path);
93 }
94
95 char *
96 twitch_videos_byuserid_data(const char *user_id)
97 {
98 char path[128];
99 int r;
100
101 r = snprintf(path, sizeof(path), "/helix/videos?first=100&user_id=%s",
102 user_id);
103 if (r < 0 || (size_t)r >= sizeof(path))
104 return NULL;
105
106 return twitch_request(path);
107 }
108
109 char *
110 twitch_streams_data(void)
111 {
112 return twitch_request("/helix/streams?first=100");
113 }
114
115 char *
116 twitch_streams_game_data(const char *game_id)
117 {
118 char path[64];
119 int r;
120
121 r = snprintf(path, sizeof(path), "/helix/streams?first=100&game_id=%s",
122 game_id);
123 if (r < 0 || (size_t)r >= sizeof(path))
124 return NULL;
125
126 return twitch_request(path);
127 }
128
129 char *
130 twitch_games_top_data(void)
131 {
132 return twitch_request("/helix/games/top?first=100");
133 }
134
135 void
136 twitch_games_processnode(struct json_node *nodes, size_t depth, const char *value, size_t valuelen,
137 void *pp)
138 {
139 struct games_response *r = (struct games_response *)pp;
140 struct game *item;
141
142 if (r->nitems > MAX_ITEMS)
143 return;
144
145 /* new item */
146 if (depth == 3 &&
147 nodes[0].type == TYPE_OBJECT &&
148 nodes[1].type == TYPE_ARRAY &&
149 nodes[2].type == TYPE_OBJECT &&
150 !strcmp(nodes[1].name, "data")) {
151 r->nitems++;
152 return;
153 }
154
155 if (r->nitems == 0)
156 return;
157 item = &(r->data[r->nitems - 1]);
158
159 if (depth == 4 &&
160 nodes[0].type == TYPE_OBJECT &&
161 nodes[1].type == TYPE_ARRAY &&
162 nodes[2].type == TYPE_OBJECT &&
163 nodes[3].type == TYPE_STRING &&
164 !strcmp(nodes[1].name, "data")) {
165 if (!strcmp(nodes[3].name, "id"))
166 strlcpy(item->id, value, sizeof(item->id));
167 else if (!strcmp(nodes[3].name, "name"))
168 strlcpy(item->name, value, sizeof(item->name));
169 }
170 }
171
172 struct games_response *
173 twitch_games_top(void)
174 {
175 struct games_response *r;
176 char *data;
177
178 if ((data = twitch_games_top_data()) == NULL) {
179 fprintf(stderr, "%s\n", __func__);
180 return NULL;
181 }
182
183 if (!(r = calloc(1, sizeof(*r)))) {
184 fprintf(stderr, "calloc\n");
185 return NULL;
186 }
187 if (json_unmarshal(data, twitch_games_processnode, r) == -1) {
188 free(r);
189 r = NULL;
190 }
191 free(data);
192
193 return r;
194 }
195
196 struct games_response *
197 twitch_games_bygameids(const char *param)
198 {
199 struct games_response *r;
200 char *data;
201
202 if ((data = twitch_games_bygameids_data(param)) == NULL) {
203 fprintf(stderr, "%s\n", __func__);
204 return NULL;
205 }
206
207 if (!(r = calloc(1, sizeof(*r)))) {
208 fprintf(stderr, "calloc\n");
209 return NULL;
210 }
211 if (json_unmarshal(data, twitch_games_processnode, r) == -1) {
212 free(r);
213 r = NULL;
214 }
215 free(data);
216
217 return r;
218 }
219
220 void
221 twitch_streams_processnode(struct json_node *nodes, size_t depth, const char *value, size_t valuelen,
222 void *pp)
223 {
224 struct streams_response *r = (struct streams_response *)pp;
225 struct stream *item;
226
227 if (r->nitems > MAX_ITEMS)
228 return;
229 item = &(r->data[r->nitems]);
230
231 /* new item */
232 if (depth == 3 &&
233 nodes[0].type == TYPE_OBJECT &&
234 nodes[1].type == TYPE_ARRAY &&
235 nodes[2].type == TYPE_OBJECT &&
236 !strcmp(nodes[1].name, "data")) {
237 r->nitems++;
238 return;
239 }
240
241 if (r->nitems == 0)
242 return;
243 item = &(r->data[r->nitems - 1]);
244
245 if (depth == 4 &&
246 nodes[0].type == TYPE_OBJECT &&
247 nodes[1].type == TYPE_ARRAY &&
248 nodes[2].type == TYPE_OBJECT &&
249 !strcmp(nodes[1].name, "data")) {
250 if (nodes[3].type == TYPE_STRING) {
251 if (!strcmp(nodes[3].name, "id"))
252 strlcpy(item->id, value, sizeof(item->id));
253 else if (!strcmp(nodes[3].name, "title"))
254 strlcpy(item->title, value, sizeof(item->title));
255 else if (!strcmp(nodes[3].name, "user_id"))
256 strlcpy(item->user_id, value, sizeof(item->user_id));
257 else if (!strcmp(nodes[3].name, "user_name"))
258 strlcpy(item->user_name, value, sizeof(item->user_name));
259 else if (!strcmp(nodes[3].name, "game_id"))
260 strlcpy(item->game_id, value, sizeof(item->game_id));
261 else if (!strcmp(nodes[3].name, "language"))
262 strlcpy(item->language, value, sizeof(item->language));
263 } else if (nodes[3].type == TYPE_NUMBER) {
264 /* TODO: check? */
265 if (!strcmp(nodes[3].name, "viewer_count"))
266 item->viewer_count = strtoll(value, NULL, 10);
267 }
268 }
269 }
270
271 struct streams_response *
272 twitch_streams_bygame(const char *game_id)
273 {
274 struct streams_response *r;
275 char *data;
276
277 if (game_id[0])
278 data = twitch_streams_game_data(game_id);
279 else
280 data = twitch_streams_data();
281
282 if (!(r = calloc(1, sizeof(*r)))) {
283 fprintf(stderr, "calloc\n");
284 return NULL;
285 }
286 if (json_unmarshal(data, twitch_streams_processnode, r) == -1) {
287 free(r);
288 r = NULL;
289 }
290 free(data);
291
292 return r;
293 }
294
295 struct streams_response *
296 twitch_streams(void)
297 {
298 return twitch_streams_bygame("");
299 }
300
301 int
302 ids_cmp(const void *v1, const void *v2)
303 {
304 const char *s1 = *((const char**)v1), *s2 = *((const char **)v2);
305
306 return strcmp(s1, s2);
307 }
308
309 /* fill in games in the streams response */
310 struct games_response *
311 twitch_streams_games(struct streams_response *r)
312 {
313 struct games_response *rg;
314 char *game_ids[MAX_ITEMS];
315 char game_ids_param[4096] = "";
316 size_t i, j;
317
318 /* create a list of game_ids, sort them and filter unique */
319 for (i = 0; i < r->nitems; i++)
320 game_ids[i] = r->data[i].game_id;
321
322 qsort(game_ids, r->nitems, sizeof(*game_ids), ids_cmp);
323 for (i = 0; i < r->nitems; i++) {
324 if (!game_ids[i][0])
325 continue;
326
327 /* first or different than previous */
328 if (i && !strcmp(game_ids[i], game_ids[i - 1]))
329 continue;
330
331 if (game_ids_param[0])
332 strlcat(game_ids_param, "&", sizeof(game_ids_param));
333
334 strlcat(game_ids_param, "id=", sizeof(game_ids_param));
335 strlcat(game_ids_param, game_ids[i], sizeof(game_ids_param));
336 }
337
338 if ((rg = twitch_games_bygameids(game_ids_param))) {
339 for (i = 0; i < r->nitems; i++) {
340 for (j = 0; j < rg->nitems; j++) {
341 /* match game on game_id */
342 if (!strcmp(r->data[i].game_id, rg->data[j].id)) {
343 r->data[i].game = &(rg->data[j]);
344 break;
345 }
346 }
347 }
348 }
349 return rg;
350 }
351
352 /* fill in users in the streams response */
353 struct users_response *
354 twitch_streams_users(struct streams_response *r)
355 {
356 struct users_response *ru = NULL;
357 char *user_ids[MAX_ITEMS];
358 char user_ids_param[4096] = "";
359 size_t i, j;
360
361 /* create a list of user_ids, sort them and filter unique */
362 for (i = 0; i < r->nitems; i++)
363 user_ids[i] = r->data[i].user_id;
364
365 qsort(user_ids, r->nitems, sizeof(*user_ids), ids_cmp);
366 for (i = 0; i < r->nitems; i++) {
367 if (!user_ids[i][0])
368 continue;
369 /* first or different than previous */
370 if (i && !strcmp(user_ids[i], user_ids[i - 1]))
371 continue;
372
373 if (user_ids_param[0])
374 strlcat(user_ids_param, "&", sizeof(user_ids_param));
375
376 strlcat(user_ids_param, "id=", sizeof(user_ids_param));
377 strlcat(user_ids_param, user_ids[i], sizeof(user_ids_param));
378 }
379
380 if ((ru = twitch_users_byuserids(user_ids_param))) {
381 for (i = 0; i < r->nitems; i++) {
382 for (j = 0; j < ru->nitems; j++) {
383 /* match user on user_id */
384 if (!strcmp(r->data[i].user_id, ru->data[j].id)) {
385 r->data[i].user = &(ru->data[j]);
386 break;
387 }
388 }
389 }
390 }
391 return ru;
392 }
393
394 void
395 twitch_users_processnode(struct json_node *nodes, size_t depth, const char *value, size_t valuelen,
396 void *pp)
397 {
398 struct users_response *r = (struct users_response *)pp;
399 struct user *item;
400
401 if (r->nitems > MAX_ITEMS)
402 return;
403 item = &(r->data[r->nitems]);
404
405 /* new item */
406 if (depth == 3 &&
407 nodes[0].type == TYPE_OBJECT &&
408 nodes[1].type == TYPE_ARRAY &&
409 nodes[2].type == TYPE_OBJECT &&
410 !strcmp(nodes[1].name, "data")) {
411 r->nitems++;
412 return;
413 }
414
415 if (r->nitems == 0)
416 return;
417 item = &(r->data[r->nitems - 1]);
418
419 if (depth == 4 &&
420 nodes[0].type == TYPE_OBJECT &&
421 nodes[1].type == TYPE_ARRAY &&
422 nodes[2].type == TYPE_OBJECT &&
423 !strcmp(nodes[1].name, "data")) {
424 if (nodes[3].type == TYPE_STRING) {
425 if (!strcmp(nodes[3].name, "id"))
426 strlcpy(item->id, value, sizeof(item->id));
427 else if (!strcmp(nodes[3].name, "login"))
428 strlcpy(item->login, value, sizeof(item->login));
429 else if (!strcmp(nodes[3].name, "display_name"))
430 strlcpy(item->display_name, value, sizeof(item->display_name));
431 } else if (nodes[3].type == TYPE_NUMBER) {
432 /* TODO: check? */
433 if (!strcmp(nodes[3].name, "view_count"))
434 item->view_count = strtoll(value, NULL, 10);
435 }
436 }
437 }
438
439 struct users_response *
440 twitch_users_byuserids(const char *param)
441 {
442 struct users_response *r;
443 char *data;
444
445 if ((data = twitch_users_byuserids_data(param)) == NULL) {
446 fprintf(stderr, "%s\n", __func__);
447 return NULL;
448 }
449
450 if (!(r = calloc(1, sizeof(*r)))) {
451 fprintf(stderr, "calloc\n");
452 return NULL;
453 }
454 if (json_unmarshal(data, twitch_users_processnode, r) == -1) {
455 free(r);
456 r = NULL;
457 }
458 free(data);
459
460 return r;
461 }
462
463 struct users_response *
464 twitch_users_bylogin(const char *login)
465 {
466 struct users_response *r;
467 char *data;
468
469 if ((data = twitch_users_bylogin_data(login)) == NULL) {
470 fprintf(stderr, "%s\n", __func__);
471 return NULL;
472 }
473
474 if (!(r = calloc(1, sizeof(*r)))) {
475 fprintf(stderr, "calloc\n");
476 return NULL;
477 }
478 if (json_unmarshal(data, twitch_users_processnode, r) == -1) {
479 free(r);
480 r = NULL;
481 }
482 free(data);
483
484 return r;
485 }
486
487 void
488 twitch_videos_processnode(struct json_node *nodes, size_t depth, const char *value, size_t valuelen,
489 void *pp)
490 {
491 struct videos_response *r = (struct videos_response *)pp;
492 struct video *item;
493
494 if (r->nitems > MAX_ITEMS)
495 return;
496 item = &(r->data[r->nitems]);
497
498 /* new item */
499 if (depth == 3 &&
500 nodes[0].type == TYPE_OBJECT &&
501 nodes[1].type == TYPE_ARRAY &&
502 nodes[2].type == TYPE_OBJECT &&
503 !strcmp(nodes[1].name, "data")) {
504 r->nitems++;
505 return;
506 }
507
508 if (r->nitems == 0)
509 return;
510 item = &(r->data[r->nitems - 1]);
511
512 if (depth == 4 &&
513 nodes[0].type == TYPE_OBJECT &&
514 nodes[1].type == TYPE_ARRAY &&
515 nodes[2].type == TYPE_OBJECT &&
516 !strcmp(nodes[1].name, "data")) {
517 if (nodes[3].type == TYPE_STRING) {
518 if (!strcmp(nodes[3].name, "id"))
519 strlcpy(item->id, value, sizeof(item->id));
520 else if (!strcmp(nodes[3].name, "user_id"))
521 strlcpy(item->user_id, value, sizeof(item->user_id));
522 else if (!strcmp(nodes[3].name, "user_name"))
523 strlcpy(item->user_name, value, sizeof(item->user_name));
524 else if (!strcmp(nodes[3].name, "title"))
525 strlcpy(item->title, value, sizeof(item->title));
526 else if (!strcmp(nodes[3].name, "created_at"))
527 strlcpy(item->created_at, value, sizeof(item->created_at));
528 else if (!strcmp(nodes[3].name, "url"))
529 strlcpy(item->url, value, sizeof(item->url));
530 else if (!strcmp(nodes[3].name, "duration"))
531 strlcpy(item->duration, value, sizeof(item->duration));
532 } else if (nodes[3].type == TYPE_NUMBER) {
533 /* TODO: check? */
534 if (!strcmp(nodes[3].name, "view_count"))
535 item->view_count = strtoll(value, NULL, 10);
536 }
537 }
538 }
539
540 struct videos_response *
541 twitch_videos_byuserid(const char *user_id)
542 {
543 struct videos_response *r;
544 char *data;
545
546 if ((data = twitch_videos_byuserid_data(user_id)) == NULL) {
547 fprintf(stderr, "%s\n", __func__);
548 return NULL;
549 }
550
551 if (!(r = calloc(1, sizeof(*r)))) {
552 fprintf(stderr, "calloc\n");
553 return NULL;
554 }
555 if (json_unmarshal(data, twitch_videos_processnode, r) == -1) {
556 free(r);
557 r = NULL;
558 }
559 free(data);
560
561 return r;
562 }