twitch.c - frontends - front-ends for some sites (experiment) | |
Log | |
Files | |
Refs | |
README | |
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, voi… | |
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_i… | |
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_… | |
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 ch… | |
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 … | |
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-… | |
255 else if (!strcmp(nodes[3].name, "user_id")) | |
256 strlcpy(item->user_id, value, sizeof(ite… | |
257 else if (!strcmp(nodes[3].name, "user_name")) | |
258 strlcpy(item->user_name, value, sizeof(i… | |
259 else if (!strcmp(nodes[3].name, "game_id")) | |
260 strlcpy(item->game_id, value, sizeof(ite… | |
261 else if (!strcmp(nodes[3].name, "language")) | |
262 strlcpy(item->language, value, sizeof(it… | |
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… | |
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_par… | |
333 | |
334 strlcat(game_ids_param, "id=", sizeof(game_ids_param)); | |
335 strlcat(game_ids_param, game_ids[i], sizeof(game_ids_par… | |
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… | |
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_par… | |
375 | |
376 strlcat(user_ids_param, "id=", sizeof(user_ids_param)); | |
377 strlcat(user_ids_param, user_ids[i], sizeof(user_ids_par… | |
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… | |
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 ch… | |
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-… | |
429 else if (!strcmp(nodes[3].name, "display_name")) | |
430 strlcpy(item->display_name, value, sizeo… | |
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, … | |
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 c… | |
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(ite… | |
522 else if (!strcmp(nodes[3].name, "user_name")) | |
523 strlcpy(item->user_name, value, sizeof(i… | |
524 else if (!strcmp(nodes[3].name, "title")) | |
525 strlcpy(item->title, value, sizeof(item-… | |
526 else if (!strcmp(nodes[3].name, "created_at")) | |
527 strlcpy(item->created_at, value, sizeof(… | |
528 else if (!strcmp(nodes[3].name, "url")) | |
529 strlcpy(item->url, value, sizeof(item->u… | |
530 else if (!strcmp(nodes[3].name, "duration")) | |
531 strlcpy(item->duration, value, sizeof(it… | |
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, … | |
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 } |