youtube.c - frontends - front-ends for some sites (experiment)
HTML git clone git://git.codemadness.org/frontends
DIR Log
DIR Files
DIR Refs
DIR README
DIR LICENSE
---
youtube.c (20266B)
---
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 <stdio.h>
9 #include <stdlib.h>
10 #include <string.h>
11 #include <unistd.h>
12
13 #include "https.h"
14 #include "json.h"
15 #include "util.h"
16 #include "youtube.h"
17
18 static long long
19 getnum(const char *s)
20 {
21 long long l;
22
23 l = strtoll(s, 0, 10);
24 if (l < 0)
25 l = 0;
26 return l;
27 }
28
29 static char *
30 youtube_request(const char *path)
31 {
32 return request("www.youtube.com", path, "");
33 }
34
35 static char *
36 request_video(const char *videoid)
37 {
38 char path[2048];
39 int r;
40
41 r = snprintf(path, sizeof(path), "/watch?v=%s", videoid);
42 /* check if request is too long (truncation) */
43 if (r < 0 || (size_t)r >= sizeof(path))
44 return NULL;
45
46 return youtube_request(path);
47 }
48
49 static char *
50 request_channel_videos(const char *channelid)
51 {
52 char path[2048];
53 int r;
54
55 r = snprintf(path, sizeof(path), "/channel/%s/videos", channelid);
56 /* check if request is too long (truncation) */
57 if (r < 0 || (size_t)r >= sizeof(path))
58 return NULL;
59
60 return youtube_request(path);
61 }
62
63 static char *
64 request_user_videos(const char *user)
65 {
66 char path[2048];
67 int r;
68
69 r = snprintf(path, sizeof(path), "/user/%s/videos", user);
70 /* check if request is too long (truncation) */
71 if (r < 0 || (size_t)r >= sizeof(path))
72 return NULL;
73
74 return youtube_request(path);
75 }
76
77 static char *
78 request_search(const char *s, const char *page, const char *order)
79 {
80 char path[4096];
81
82 snprintf(path, sizeof(path), "/results?search_query=%s", s);
83
84 /* NOTE: pagination doesn't work at the moment:
85 this parameter is not supported anymore by Youtube */
86 if (page[0]) {
87 strlcat(path, "&page=", sizeof(path));
88 strlcat(path, page, sizeof(path));
89 }
90
91 if (order[0] && strcmp(order, "relevance")) {
92 strlcat(path, "&sp=", sizeof(path));
93 if (!strcmp(order, "date"))
94 strlcat(path, "CAI%3D", sizeof(path));
95 else if (!strcmp(order, "views"))
96 strlcat(path, "CAM%3D", sizeof(path));
97 else if (!strcmp(order, "rating"))
98 strlcat(path, "CAE%3D", sizeof(path));
99 }
100
101 /* check if request is too long (truncation) */
102 if (strlen(path) >= sizeof(path) - 1)
103 return NULL;
104
105 return youtube_request(path);
106 }
107
108 static int
109 extractjson_search(const char *s, const char **start, const char **end)
110 {
111 *start = strstr(s, "window[\"ytInitialData\"] = ");
112 if (*start) {
113 (*start) += sizeof("window[\"ytInitialData\"] = ") - 1;
114 } else {
115 *start = strstr(s, "var ytInitialData = ");
116 if (*start)
117 (*start) += sizeof("var ytInitialData = ") - 1;
118 }
119 if (!*start)
120 return -1;
121 *end = strstr(*start, "};\n");
122 if (!*end)
123 *end = strstr(*start, "}; \n");
124 if (!*end)
125 *end = strstr(*start, "};<");
126 if (!*end)
127 return -1;
128 (*end)++;
129
130 return 0;
131 }
132
133 static int
134 extractjson_video(const char *s, const char **start, const char **end)
135 {
136 *start = strstr(s, "var ytInitialPlayerResponse = ");
137 if (!*start)
138 return -1;
139 (*start) += sizeof("var ytInitialPlayerResponse = ") - 1;
140 *end = strstr(*start, "};<");
141 if (!*end)
142 return -1;
143 (*end)++;
144
145 return 0;
146 }
147
148 static int
149 isrenderername(const char *name)
150 {
151 return !strcmp(name, "videoRenderer");
152 }
153
154 static void
155 processnode_search(struct json_node *nodes, size_t depth, const char *value, size_t valuelen,
156 void *pp)
157 {
158 struct search_response *r = (struct search_response *)pp;
159 static struct item *item;
160 char *p;
161
162 if (r->nitems > MAX_VIDEOS)
163 return;
164
165 /* new item, structures can be very deep, just check the end for:
166 (items|contents)[].videoRenderer objects */
167 if (depth >= 3 &&
168 nodes[depth - 1].type == JSON_TYPE_OBJECT &&
169 isrenderername(nodes[depth - 1].name)) {
170 r->nitems++;
171 return;
172 }
173
174 /* richItemRenderer, new channel format (from about 2026-05-10) */
175 if (depth >= 3 &&
176 nodes[depth - 1].type == JSON_TYPE_OBJECT &&
177 !strcmp(nodes[depth - 1].name, "richItemRenderer")) {
178 r->nitems++;
179 return;
180 }
181
182 if (r->nitems == 0)
183 return;
184 item = &(r->items[r->nitems - 1]);
185
186 /* format from before 2026-05-10, remove later */
187
188 if (depth >= 4 &&
189 nodes[depth - 1].type == JSON_TYPE_STRING &&
190 isrenderername(nodes[depth - 2].name) &&
191 !strcmp(nodes[depth - 1].name, "videoId")) {
192 strlcpy(item->id, value, sizeof(item->id));
193 }
194
195 if (depth >= 7 &&
196 nodes[depth - 5].type == JSON_TYPE_OBJECT &&
197 nodes[depth - 4].type == JSON_TYPE_OBJECT &&
198 nodes[depth - 3].type == JSON_TYPE_ARRAY &&
199 nodes[depth - 2].type == JSON_TYPE_OBJECT &&
200 nodes[depth - 1].type == JSON_TYPE_STRING &&
201 isrenderername(nodes[depth - 5].name) &&
202 !strcmp(nodes[depth - 4].name, "title") &&
203 !strcmp(nodes[depth - 3].name, "runs") &&
204 !strcmp(nodes[depth - 1].name, "text") &&
205 !item->title[0]) {
206 strlcpy(item->title, value, sizeof(item->title));
207 }
208
209 /* in search listing there is a short description, string items are appended */
210 if (depth >= 8 &&
211 nodes[depth - 7].type == JSON_TYPE_OBJECT &&
212 nodes[depth - 6].type == JSON_TYPE_ARRAY &&
213 nodes[depth - 5].type == JSON_TYPE_OBJECT &&
214 nodes[depth - 4].type == JSON_TYPE_OBJECT &&
215 nodes[depth - 3].type == JSON_TYPE_ARRAY &&
216 nodes[depth - 2].type == JSON_TYPE_OBJECT &&
217 nodes[depth - 1].type == JSON_TYPE_STRING &&
218 isrenderername(nodes[depth - 7].name) &&
219 !strcmp(nodes[depth - 6].name, "detailedMetadataSnippets") &&
220 !strcmp(nodes[depth - 4].name, "snippetText") &&
221 !strcmp(nodes[depth - 3].name, "runs") &&
222 !strcmp(nodes[depth - 1].name, "text")) {
223 strlcat(item->shortdescription, value, sizeof(item->shortdescription));
224 }
225
226 /* in channel/user videos listing there is a short description, string items are appended */
227 if (depth >= 7 &&
228 nodes[depth - 5].type == JSON_TYPE_OBJECT &&
229 nodes[depth - 4].type == JSON_TYPE_OBJECT &&
230 nodes[depth - 3].type == JSON_TYPE_ARRAY &&
231 nodes[depth - 2].type == JSON_TYPE_OBJECT &&
232 nodes[depth - 1].type == JSON_TYPE_STRING &&
233 isrenderername(nodes[depth - 5].name) &&
234 !strcmp(nodes[depth - 4].name, "descriptionSnippet") &&
235 !strcmp(nodes[depth - 3].name, "runs") &&
236 !strcmp(nodes[depth - 1].name, "text")) {
237 strlcat(item->shortdescription, value, sizeof(item->shortdescription));
238 }
239
240 /* try to detect members/sponsor/subscription-only videos */
241 if (depth >= 7 &&
242 nodes[depth - 5].type == JSON_TYPE_OBJECT &&
243 nodes[depth - 4].type == JSON_TYPE_ARRAY &&
244 nodes[depth - 3].type == JSON_TYPE_OBJECT &&
245 nodes[depth - 2].type == JSON_TYPE_OBJECT &&
246 nodes[depth - 1].type == JSON_TYPE_STRING &&
247 isrenderername(nodes[depth - 5].name) &&
248 !strcmp(nodes[depth - 4].name, "badges") &&
249 !strcmp(nodes[depth - 2].name, "metadataBadgeRenderer") &&
250 !strcmp(nodes[depth - 1].name, "label")) {
251 if (strstr(value, "Members only"))
252 item->membersonly = 1;
253 }
254
255 if (depth >= 5 &&
256 nodes[depth - 4].type == JSON_TYPE_OBJECT &&
257 nodes[depth - 3].type == JSON_TYPE_OBJECT &&
258 nodes[depth - 2].type == JSON_TYPE_OBJECT &&
259 nodes[depth - 1].type == JSON_TYPE_STRING &&
260 isrenderername(nodes[depth - 3].name) &&
261 !strcmp(nodes[depth - 1].name, "simpleText")) {
262 if (!strcmp(nodes[depth - 2].name, "viewCountText") &&
263 !item->viewcount[0]) {
264 strlcpy(item->viewcount, value, sizeof(item->viewcount));
265 } else if (!strcmp(nodes[depth - 2].name, "lengthText") &&
266 !item->duration[0]) {
267 strlcpy(item->duration, value, sizeof(item->duration));
268 } else if (!strcmp(nodes[depth - 2].name, "publishedTimeText") &&
269 !item->publishedat[0]) {
270 strlcpy(item->publishedat, value, sizeof(item->publishedat));
271 }
272 }
273
274 if (depth >= 9 &&
275 nodes[depth - 8].type == JSON_TYPE_OBJECT &&
276 nodes[depth - 7].type == JSON_TYPE_OBJECT &&
277 nodes[depth - 6].type == JSON_TYPE_OBJECT &&
278 nodes[depth - 5].type == JSON_TYPE_ARRAY &&
279 nodes[depth - 4].type == JSON_TYPE_OBJECT &&
280 nodes[depth - 3].type == JSON_TYPE_OBJECT &&
281 nodes[depth - 2].type == JSON_TYPE_OBJECT &&
282 nodes[depth - 1].type == JSON_TYPE_STRING &&
283 isrenderername(nodes[depth - 7].name) &&
284 !strcmp(nodes[depth - 6].name, "longBylineText") &&
285 !strcmp(nodes[depth - 5].name, "runs") &&
286 !strcmp(nodes[depth - 3].name, "navigationEndpoint") &&
287 !strcmp(nodes[depth - 2].name, "browseEndpoint")) {
288 if (!strcmp(nodes[depth - 1].name, "browseId")) {
289 strlcpy(item->channelid, value, sizeof(item->channelid));
290 }
291 }
292
293 if (depth >= 7 &&
294 nodes[depth - 6].type == JSON_TYPE_OBJECT &&
295 nodes[depth - 5].type == JSON_TYPE_OBJECT &&
296 nodes[depth - 4].type == JSON_TYPE_OBJECT &&
297 nodes[depth - 3].type == JSON_TYPE_ARRAY &&
298 nodes[depth - 2].type == JSON_TYPE_OBJECT &&
299 nodes[depth - 1].type == JSON_TYPE_STRING &&
300 isrenderername(nodes[depth - 5].name) &&
301 !strcmp(nodes[depth - 4].name, "longBylineText") &&
302 !strcmp(nodes[depth - 3].name, "runs")) {
303 if (!strcmp(nodes[depth - 1].name, "text") &&
304 !item->channeltitle[0]) {
305 strlcpy(item->channeltitle, value, sizeof(item->channeltitle));
306 }
307 }
308
309 /* /end of old format */
310
311 /* richItemRenderer, new channel format (from about 2026-05-10) */
312 if (depth >= 4 &&
313 nodes[depth - 1].type == JSON_TYPE_STRING &&
314 !strcmp(nodes[depth - 4].name, "richItemRenderer") &&
315 !strcmp(nodes[depth - 3].name, "content") &&
316 !strcmp(nodes[depth - 2].name, "lockupViewModel") &&
317 !strcmp(nodes[depth - 1].name, "contentId")) {
318 strlcpy(item->id, value, sizeof(item->id));
319 }
320
321 if (depth >= 7 &&
322 nodes[depth - 1].type == JSON_TYPE_STRING &&
323 !strcmp(nodes[depth - 7].name, "richItemRenderer") &&
324 !strcmp(nodes[depth - 6].name, "content") &&
325 !strcmp(nodes[depth - 5].name, "lockupViewModel") &&
326 !strcmp(nodes[depth - 4].name, "metadata") &&
327 !strcmp(nodes[depth - 3].name, "lockupMetadataViewModel") &&
328 !strcmp(nodes[depth - 2].name, "title") &&
329 !strcmp(nodes[depth - 1].name, "content") &&
330 !item->title[0]) {
331 strlcpy(item->title, value, sizeof(item->title));
332 }
333
334 if (depth >= 14 &&
335 nodes[depth - 1].type == JSON_TYPE_STRING &&
336 !strcmp(nodes[depth - 12].name, "richItemRenderer") &&
337 !strcmp(nodes[depth - 11].name, "content") &&
338 !strcmp(nodes[depth - 10].name, "lockupViewModel") &&
339 !strcmp(nodes[depth - 9].name, "contentImage") &&
340 !strcmp(nodes[depth - 8].name, "thumbnailViewModel") &&
341 !strcmp(nodes[depth - 7].name, "overlays") &&
342 !strcmp(nodes[depth - 5].name, "thumbnailBottomOverlayViewModel") &&
343 !strcmp(nodes[depth - 4].name, "badges") &&
344 !strcmp(nodes[depth - 2].name, "thumbnailBadgeViewModel") &&
345 !strcmp(nodes[depth - 1].name, "text")) {
346 if (value[0] && !item->duration[0])
347 strlcpy(item->duration, value, sizeof(item->duration));
348 }
349
350 if (depth >= 14 &&
351 nodes[depth - 1].type == JSON_TYPE_STRING &&
352 !strcmp(nodes[depth - 13].name, "richItemRenderer") &&
353 !strcmp(nodes[depth - 12].name, "content") &&
354 !strcmp(nodes[depth - 11].name, "lockupViewModel") &&
355 !strcmp(nodes[depth - 10].name, "metadata") &&
356 !strcmp(nodes[depth - 9].name, "lockupMetadataViewModel") &&
357 !strcmp(nodes[depth - 8].name, "metadata") &&
358 !strcmp(nodes[depth - 7].name, "contentMetadataViewModel") &&
359 !strcmp(nodes[depth - 6].name, "metadataRows") &&
360 !strcmp(nodes[depth - 4].name, "metadataParts") &&
361 !strcmp(nodes[depth - 2].name, "text") &&
362 !strcmp(nodes[depth - 1].name, "content")) {
363 if (strstr(value, " ago")) {
364 /* typically second item is published at */
365 if (value[0] && !item->publishedat[0])
366 strlcpy(item->publishedat, value, sizeof(item->publishedat));
367 } else if ((value[0] >= '0' && value[0] <= '9') && !item->viewcount[0]) {
368 strlcpy(item->viewcount, value, sizeof(item->viewcount));
369 }
370 }
371
372 if (depth >= 3 &&
373 nodes[depth - 1].type == JSON_TYPE_STRING &&
374 !strcmp(nodes[depth - 3].name, "microformat") &&
375 !strcmp(nodes[depth - 2].name, "microformatDataRenderer")) {
376 if (!strcmp(nodes[depth - 1].name, "urlCanonical")) {
377 /* copy ID from URL: https://www.youtube.com/channel/someid */
378 if ((p = strstr(value, "youtube.com/channel/"))) {
379 p += strlen("youtube.com/channel/");
380 strlcpy(r->channelid, p, sizeof(r->channelid));
381 }
382 } else if (!strcmp(nodes[depth - 1].name, "title")) {
383 if (value[0] && !r->channeltitle[0])
384 strlcpy(r->channeltitle, value, sizeof(r->channeltitle));
385 }
386 }
387 }
388
389 static struct search_response *
390 parse_search_response(const char *data)
391 {
392 struct search_response *r;
393 struct item *item;
394 const char *s, *start, *end;
395 size_t i, len;
396 int ret;
397
398 if (!(s = strstr(data, "\r\n\r\n")))
399 return NULL; /* invalid response */
400 /* skip header */
401 s += strlen("\r\n\r\n");
402
403 if (!(r = calloc(1, sizeof(*r))))
404 return NULL;
405
406 #if 0
407 start = data;
408 end = data + strlen(data);
409 #endif
410
411 if (extractjson_search(s, &start, &end) == -1) {
412 free(r);
413 return NULL;
414 }
415
416 #if 0
417 // DEBUG
418 dprintf(2, "DEBUG: ");
419 write(2, start, end - start);
420 dprintf(2, "\n");
421 #endif
422
423 ret = parsejson(start, end - start, processnode_search, r);
424 if (ret < 0) {
425 free(r);
426 return NULL;
427 }
428
429 /* workaround: sometimes playlists or topics are listed as channels, filter
430 these topic/playlist links away because they won't work for channel videos. The
431 JSON response would have to be parsed in a different way than channels. */
432 for (i = 0; i < r->nitems; i++) {
433 item = &(r->items[i]);
434 len = strlen(item->channeltitle);
435
436 /* copy channel title and ID if set and not set in the item (global microformat output) */
437 if (r->channeltitle[0] && !item->channeltitle[0])
438 strlcpy(item->channeltitle, r->channeltitle, sizeof(item->channeltitle));
439 if (r->channelid[0] && !item->channelid[0])
440 strlcpy(item->channelid, r->channelid, sizeof(item->channelid));
441
442 if (len > sizeof(" - Topic") &&
443 !strcmp(item->channeltitle + len - sizeof(" - Topic") + 1, " - Topic")) {
444 /* reset information that doesn't work for topics */
445 item->channelid[0] = '\0';
446 item->viewcount[0] = '\0';
447 }
448 }
449
450 return r;
451 }
452
453 static void
454 processnode_video(struct json_node *nodes, size_t depth, const char *value, size_t valuelen,
455 void *pp)
456 {
457 struct video_response *r = (struct video_response *)pp;
458 struct video_format *f;
459
460 if (depth > 1) {
461 /* playability status: could be unplayable / members-only video */
462 if (nodes[0].type == JSON_TYPE_OBJECT &&
463 !strcmp(nodes[1].name, "playabilityStatus")) { /* example: "UNPLAYABLE" */
464 if (depth == 3 &&
465 nodes[2].type == JSON_TYPE_STRING &&
466 !strcmp(nodes[2].name, "status")) {
467 strlcpy(r->playabilitystatus, value, sizeof(r->playabilitystatus));
468 }
469 if (depth == 3 &&
470 nodes[2].type == JSON_TYPE_STRING &&
471 !strcmp(nodes[2].name, "reason")) {
472 strlcpy(r->playabilityreason, value, sizeof(r->playabilityreason));
473 }
474 }
475
476 if (nodes[0].type == JSON_TYPE_OBJECT &&
477 !strcmp(nodes[1].name, "streamingData")) {
478 if (depth == 2 &&
479 nodes[2].type == JSON_TYPE_STRING &&
480 !strcmp(nodes[2].name, "expiresInSeconds")) {
481 r->expiresinseconds = getnum(value);
482 }
483
484 if (depth >= 3 &&
485 nodes[2].type == JSON_TYPE_ARRAY &&
486 (!strcmp(nodes[2].name, "formats") ||
487 !strcmp(nodes[2].name, "adaptiveFormats"))) {
488 if (r->nformats > MAX_FORMATS)
489 return; /* ignore: don't add too many formats */
490
491 if (depth == 4 && nodes[3].type == JSON_TYPE_OBJECT)
492 r->nformats++;
493
494 if (r->nformats == 0)
495 return;
496 f = &(r->formats[r->nformats - 1]); /* current video format item */
497
498 if (depth == 5 &&
499 nodes[2].type == JSON_TYPE_ARRAY &&
500 nodes[3].type == JSON_TYPE_OBJECT &&
501 (nodes[4].type == JSON_TYPE_STRING ||
502 nodes[4].type == JSON_TYPE_NUMBER ||
503 nodes[4].type == JSON_TYPE_BOOL)) {
504 if (!strcmp(nodes[4].name, "width")) {
505 f->width = getnum(value);
506 } else if (!strcmp(nodes[4].name, "height")) {
507 f->height = getnum(value);
508 } else if (!strcmp(nodes[4].name, "url")) {
509 strlcpy(f->url, value, sizeof(f->url));
510 } else if (!strcmp(nodes[4].name, "signatureCipher")) {
511 strlcpy(f->signaturecipher, value, sizeof(f->signaturecipher));
512 } else if (!strcmp(nodes[4].name, "qualityLabel")) {
513 strlcpy(f->qualitylabel, value, sizeof(f->qualitylabel));
514 } else if (!strcmp(nodes[4].name, "quality")) {
515 strlcpy(f->quality, value, sizeof(f->quality));
516 } else if (!strcmp(nodes[4].name, "fps")) {
517 f->fps = getnum(value);
518 } else if (!strcmp(nodes[4].name, "bitrate")) {
519 f->bitrate = getnum(value);
520 } else if (!strcmp(nodes[4].name, "averageBitrate")) {
521 f->averagebitrate = getnum(value);
522 } else if (!strcmp(nodes[4].name, "mimeType")) {
523 strlcpy(f->mimetype, value, sizeof(f->mimetype));
524 } else if (!strcmp(nodes[4].name, "itag")) {
525 f->itag = getnum(value);
526 } else if (!strcmp(nodes[4].name, "contentLength")) {
527 f->contentlength = getnum(value);
528 } else if (!strcmp(nodes[4].name, "lastModified")) {
529 f->lastmodified = getnum(value);
530 } else if (!strcmp(nodes[4].name, "audioChannels")) {
531 f->audiochannels = getnum(value);
532 } else if (!strcmp(nodes[4].name, "audioSampleRate")) {
533 f->audiosamplerate = getnum(value);
534 }
535 }
536 }
537 }
538 }
539
540 if (depth == 4 &&
541 nodes[0].type == JSON_TYPE_OBJECT &&
542 nodes[1].type == JSON_TYPE_OBJECT &&
543 nodes[2].type == JSON_TYPE_OBJECT &&
544 nodes[3].type == JSON_TYPE_STRING &&
545 !strcmp(nodes[1].name, "microformat") &&
546 !strcmp(nodes[2].name, "playerMicroformatRenderer")) {
547 r->isfound = 1;
548
549 if (!strcmp(nodes[3].name, "publishDate")) {
550 strlcpy(r->publishdate, value, sizeof(r->publishdate));
551 } else if (!strcmp(nodes[3].name, "uploadDate")) {
552 strlcpy(r->uploaddate, value, sizeof(r->uploaddate));
553 } else if (!strcmp(nodes[3].name, "category")) {
554 strlcpy(r->category, value, sizeof(r->category));
555 } else if (!strcmp(nodes[3].name, "isFamilySafe")) {
556 r->isfamilysafe = !strcmp(value, "true");
557 } else if (!strcmp(nodes[3].name, "isUnlisted")) {
558 r->isunlisted = !strcmp(value, "true");
559 }
560 }
561
562 if (depth == 3) {
563 if (nodes[0].type == JSON_TYPE_OBJECT &&
564 nodes[2].type == JSON_TYPE_STRING &&
565 !strcmp(nodes[1].name, "videoDetails")) {
566 r->isfound = 1;
567
568 if (!strcmp(nodes[2].name, "title")) {
569 strlcpy(r->title, value, sizeof(r->title));
570 } else if (!strcmp(nodes[2].name, "videoId")) {
571 strlcpy(r->id, value, sizeof(r->id));
572 } else if (!strcmp(nodes[2].name, "lengthSeconds")) {
573 r->lengthseconds = getnum(value);
574 } else if (!strcmp(nodes[2].name, "author")) {
575 strlcpy(r->author, value, sizeof(r->author));
576 } else if (!strcmp(nodes[2].name, "viewCount")) {
577 r->viewcount = getnum(value);
578 } else if (!strcmp(nodes[2].name, "channelId")) {
579 strlcpy(r->channelid, value, sizeof(r->channelid));
580 } else if (!strcmp(nodes[2].name, "shortDescription")) {
581 strlcpy(r->shortdescription, value, sizeof(r->shortdescription));
582 }
583 }
584 }
585 }
586
587 static struct video_response *
588 parse_video_response(const char *data)
589 {
590 struct video_response *r;
591 const char *s, *start, *end;
592 int ret;
593
594 if (!(s = strstr(data, "\r\n\r\n")))
595 return NULL; /* invalid response */
596 /* skip header */
597 s += strlen("\r\n\r\n");
598
599 if (!(r = calloc(1, sizeof(*r))))
600 return NULL;
601
602 if (extractjson_video(s, &start, &end) == -1) {
603 free(r);
604 return NULL;
605 }
606
607 ret = parsejson(start, end - start, processnode_video, r);
608 if (ret < 0) {
609 free(r);
610 return NULL;
611 }
612 return r;
613 }
614
615 struct search_response *
616 youtube_search(const char *rawsearch, const char *page, const char *order)
617 {
618 const char *data;
619
620 if (!(data = request_search(rawsearch, page, order)))
621 return NULL;
622
623 return parse_search_response(data);
624 }
625
626 struct search_response *
627 youtube_channel_videos(const char *channelid)
628 {
629 const char *data;
630
631 #if 0
632 data = readfile("debug.json");
633 if (!data)
634 return NULL;
635 #endif
636
637 if (!(data = request_channel_videos(channelid)))
638 return NULL;
639
640 return parse_search_response(data);
641 }
642
643 struct search_response *
644 youtube_user_videos(const char *user)
645 {
646 const char *data;
647
648 if (!(data = request_user_videos(user)))
649 return NULL;
650
651 return parse_search_response(data);
652 }
653
654 struct video_response *
655 youtube_video(const char *videoid)
656 {
657 const char *data;
658
659 if (!(data = request_video(videoid)))
660 return NULL;
661
662 return parse_video_response(data);
663 }