youtube: workaround, don't list topic/playlist items as channels - frontends - front-ends for some sites (experiment)
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit b608dae128234bedd5ed09fe89c22c0dd5ef0b28
 (DIR) parent 6242ec658d5610990c6b19944473fa7277493ac1
 (HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Sun,  1 Jun 2025 13:37:34 +0200
       
       youtube: workaround, don't list topic/playlist items as channels
       
       Listing channel videos based on this topic or playlist would serve different
       JSON which we don't handle, so it would serve an empty page.
       For now just reset the channelid if the channeltitle ends with " - Topic".
       
       Separate the function for checking the renderer name. This might make it easier
       to adapt to support playlists in the future. The JSON output for
       playlists/topics has a "shelfRenderer".
       
       Diffstat:
         M youtube/youtube.c                   |      40 +++++++++++++++++++++++++-------
       
       1 file changed, 32 insertions(+), 8 deletions(-)
       ---
 (DIR) diff --git a/youtube/youtube.c b/youtube/youtube.c
       @@ -145,6 +145,12 @@ extractjson_video(const char *s, const char **start, const char **end)
                return 0;
        }
        
       +static int
       +isrenderername(const char *name)
       +{
       +        return !strcmp(name, "videoRenderer");
       +}
       +
        static void
        processnode_search(struct json_node *nodes, size_t depth, const char *value, size_t valuelen,
                void *pp)
       @@ -159,7 +165,7 @@ processnode_search(struct json_node *nodes, size_t depth, const char *value, siz
                   (items|contents)[].videoRenderer objects */
                if (depth >= 3 &&
                    nodes[depth - 1].type == JSON_TYPE_OBJECT &&
       -            !strcmp(nodes[depth - 1].name, "videoRenderer")) {
       +            isrenderername(nodes[depth - 1].name)) {
                        r->nitems++;
                        return;
                }
       @@ -170,7 +176,7 @@ processnode_search(struct json_node *nodes, size_t depth, const char *value, siz
        
                if (depth >= 4 &&
                    nodes[depth - 1].type == JSON_TYPE_STRING &&
       -            !strcmp(nodes[depth - 2].name, "videoRenderer") &&
       +            isrenderername(nodes[depth - 2].name) &&
                    !strcmp(nodes[depth - 1].name, "videoId")) {
                        strlcpy(item->id, value, sizeof(item->id));
                }
       @@ -181,7 +187,7 @@ processnode_search(struct json_node *nodes, size_t depth, const char *value, siz
                    nodes[depth - 3].type == JSON_TYPE_ARRAY &&
                    nodes[depth - 2].type == JSON_TYPE_OBJECT &&
                    nodes[depth - 1].type == JSON_TYPE_STRING &&
       -            !strcmp(nodes[depth - 5].name, "videoRenderer") &&
       +            isrenderername(nodes[depth - 5].name) &&
                    !strcmp(nodes[depth - 4].name, "title") &&
                    !strcmp(nodes[depth - 3].name, "runs") &&
                    !strcmp(nodes[depth - 1].name, "text") &&
       @@ -198,7 +204,7 @@ processnode_search(struct json_node *nodes, size_t depth, const char *value, siz
                    nodes[depth - 3].type == JSON_TYPE_ARRAY &&
                    nodes[depth - 2].type == JSON_TYPE_OBJECT &&
                    nodes[depth - 1].type == JSON_TYPE_STRING &&
       -            !strcmp(nodes[depth - 7].name, "videoRenderer") &&
       +            isrenderername(nodes[depth - 7].name) &&
                    !strcmp(nodes[depth - 6].name, "detailedMetadataSnippets") &&
                    !strcmp(nodes[depth - 4].name, "snippetText") &&
                    !strcmp(nodes[depth - 3].name, "runs") &&
       @@ -213,7 +219,7 @@ processnode_search(struct json_node *nodes, size_t depth, const char *value, siz
                    nodes[depth - 3].type == JSON_TYPE_ARRAY &&
                    nodes[depth - 2].type == JSON_TYPE_OBJECT &&
                    nodes[depth - 1].type == JSON_TYPE_STRING &&
       -            !strcmp(nodes[depth - 5].name, "videoRenderer") &&
       +            isrenderername(nodes[depth - 5].name) &&
                    !strcmp(nodes[depth - 4].name, "descriptionSnippet") &&
                    !strcmp(nodes[depth - 3].name, "runs") &&
                    !strcmp(nodes[depth - 1].name, "text")) {
       @@ -225,7 +231,7 @@ processnode_search(struct json_node *nodes, size_t depth, const char *value, siz
                    nodes[depth - 3].type == JSON_TYPE_OBJECT &&
                    nodes[depth - 2].type == JSON_TYPE_OBJECT &&
                    nodes[depth - 1].type == JSON_TYPE_STRING &&
       -            !strcmp(nodes[depth - 3].name, "videoRenderer") &&
       +            isrenderername(nodes[depth - 3].name) &&
                    !strcmp(nodes[depth - 1].name, "simpleText")) {
                        if (!strcmp(nodes[depth - 2].name, "viewCountText") &&
                            !item->viewcount[0]) {
       @@ -248,7 +254,7 @@ processnode_search(struct json_node *nodes, size_t depth, const char *value, siz
                    nodes[depth - 3].type == JSON_TYPE_OBJECT &&
                    nodes[depth - 2].type == JSON_TYPE_OBJECT &&
                    nodes[depth - 1].type == JSON_TYPE_STRING &&
       -            !strcmp(nodes[depth - 7].name, "videoRenderer") &&
       +            isrenderername(nodes[depth - 7].name) &&
                    !strcmp(nodes[depth - 6].name, "longBylineText") &&
                    !strcmp(nodes[depth - 5].name, "runs") &&
                    !strcmp(nodes[depth - 3].name, "navigationEndpoint") &&
       @@ -265,7 +271,7 @@ processnode_search(struct json_node *nodes, size_t depth, const char *value, siz
                    nodes[depth - 3].type == JSON_TYPE_ARRAY &&
                    nodes[depth - 2].type == JSON_TYPE_OBJECT &&
                    nodes[depth - 1].type == JSON_TYPE_STRING &&
       -            !strcmp(nodes[depth - 5].name, "videoRenderer") &&
       +            isrenderername(nodes[depth - 5].name) &&
                    !strcmp(nodes[depth - 4].name, "longBylineText") &&
                    !strcmp(nodes[depth - 3].name, "runs")) {
                        if (!strcmp(nodes[depth - 1].name, "text") &&
       @@ -279,7 +285,9 @@ static struct search_response *
        parse_search_response(const char *data)
        {
                struct search_response *r;
       +        struct item *item;
                const char *s, *start, *end;
       +        size_t i, len;
                int ret;
        
                if (!(s = strstr(data, "\r\n\r\n")))
       @@ -300,6 +308,22 @@ parse_search_response(const char *data)
                        free(r);
                        return NULL;
                }
       +
       +        /* workaround: sometimes playlists or topics are listed as channels, filter
       +           these topic/playlist links away because they won't work for channel videos. The
       +           JSON response would have to be parsed in a different way than channels. */
       +        for (i = 0; i < r->nitems; i++) {
       +                item = &(r->items[i]);
       +                len = strlen(item->channeltitle);
       +
       +                if (len > sizeof(" - Topic") &&
       +                    !strcmp(item->channeltitle + len - sizeof(" - Topic") + 1, " - Topic")) {
       +                        /* reset information that doesn't work for topics */
       +                        item->channelid[0] = '\0';
       +                        item->viewcount[0] = '\0';
       +                }
       +        }
       +
                return r;
        }