URI: 
       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 }