URI: 
       support new channel JSON format for videos - 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
       ---
   DIR commit a108fa2ffec484a5e1d7f402de33c6116f0eadda
   DIR parent 42fa84b70552cbd5338298050a35536132622eb9
  HTML Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Mon, 11 May 2026 20:00:19 +0200
       
       support new channel JSON format for videos
       
       Around 2026-05-10 Youtube changed their output format when listing channel
       videos.
       
       The new output is less structured to how it was.
       Try to support as many fields as possible in the output to how it was.
       
       Youtube shortens viewcounts now for this page (as opposed to the search page).
       Like "1,337 views" will be "1K views".
       
       Also keep support for the older format for now. Youtube tends to rollout these
       sort of changes per region or per device, etc.
       
       Bump LICENSE year and keep some commented debugging code.
       
       Diffstat:
         M LICENSE                             |       2 +-
         M util.c                              |       6 ++++++
         M youtube/youtube.c                   |     115 +++++++++++++++++++++++++++++++
         M youtube/youtube.h                   |       2 ++
       
       4 files changed, 124 insertions(+), 1 deletion(-)
       ---
   DIR diff --git a/LICENSE b/LICENSE
       @@ -1,6 +1,6 @@
        ISC License
        
       -Copyright (c) 2020-2025 Hiltjo Posthuma <hiltjo@codemadness.org>
       +Copyright (c) 2020-2026 Hiltjo Posthuma <hiltjo@codemadness.org>
        
        Permission to use, copy, modify, and/or distribute this software for any
        purpose with or without fee is hereby granted, provided that the above
   DIR diff --git a/util.c b/util.c
       @@ -235,6 +235,12 @@ printnumsep(const char *s)
                                ndigits++;
        
                for (p = s; *p; p++) {
       +                /* upper-case letters are allowed, for example in "100K views" */
       +                if (*p >= 'A' && *p <= 'Z') {
       +                        putchar(*p);
       +                        continue;
       +                }
       +
                        if (!(*p >= '0' && *p <= '9'))
                                continue;
        
   DIR diff --git a/youtube/youtube.c b/youtube/youtube.c
       @@ -157,6 +157,7 @@ processnode_search(struct json_node *nodes, size_t depth, const char *value, siz
        {
                struct search_response *r = (struct search_response *)pp;
                static struct item *item;
       +        char *p;
        
                if (r->nitems > MAX_VIDEOS)
                        return;
       @@ -170,10 +171,20 @@ processnode_search(struct json_node *nodes, size_t depth, const char *value, siz
                        return;
                }
        
       +        /* richItemRenderer, new channel format (from about 2026-05-10) */
       +        if (depth >= 3 &&
       +            nodes[depth - 1].type == JSON_TYPE_OBJECT &&
       +            !strcmp(nodes[depth - 1].name, "richItemRenderer")) {
       +                r->nitems++;
       +                return;
       +        }
       +
                if (r->nitems == 0)
                        return;
                item = &(r->items[r->nitems - 1]);
        
       +        /* format from before 2026-05-10, remove later */
       +
                if (depth >= 4 &&
                    nodes[depth - 1].type == JSON_TYPE_STRING &&
                    isrenderername(nodes[depth - 2].name) &&
       @@ -294,6 +305,86 @@ processnode_search(struct json_node *nodes, size_t depth, const char *value, siz
                                strlcpy(item->channeltitle, value, sizeof(item->channeltitle));
                        }
                }
       +
       +        /* /end of old format */
       +
       +        /* richItemRenderer, new channel format (from about 2026-05-10) */
       +        if (depth >= 4 &&
       +            nodes[depth - 1].type == JSON_TYPE_STRING &&
       +            !strcmp(nodes[depth - 4].name, "richItemRenderer") &&
       +            !strcmp(nodes[depth - 3].name, "content") &&
       +            !strcmp(nodes[depth - 2].name, "lockupViewModel") &&
       +            !strcmp(nodes[depth - 1].name, "contentId")) {
       +                strlcpy(item->id, value, sizeof(item->id));
       +        }
       +
       +        if (depth >= 7 &&
       +            nodes[depth - 1].type == JSON_TYPE_STRING &&
       +            !strcmp(nodes[depth - 7].name, "richItemRenderer") &&
       +            !strcmp(nodes[depth - 6].name, "content") &&
       +            !strcmp(nodes[depth - 5].name, "lockupViewModel") &&
       +            !strcmp(nodes[depth - 4].name, "metadata") &&
       +            !strcmp(nodes[depth - 3].name, "lockupMetadataViewModel") &&
       +            !strcmp(nodes[depth - 2].name, "title") &&
       +            !strcmp(nodes[depth - 1].name, "content") &&
       +                !item->title[0]) {
       +                strlcpy(item->title, value, sizeof(item->title));
       +        }
       +
       +        if (depth >= 14 &&
       +            nodes[depth - 1].type == JSON_TYPE_STRING &&
       +            !strcmp(nodes[depth - 12].name, "richItemRenderer") &&
       +            !strcmp(nodes[depth - 11].name, "content") &&
       +            !strcmp(nodes[depth - 10].name, "lockupViewModel") &&
       +            !strcmp(nodes[depth - 9].name, "contentImage") &&
       +            !strcmp(nodes[depth - 8].name, "thumbnailViewModel") &&
       +            !strcmp(nodes[depth - 7].name, "overlays") &&
       +            !strcmp(nodes[depth - 5].name, "thumbnailBottomOverlayViewModel") &&
       +            !strcmp(nodes[depth - 4].name, "badges") &&
       +            !strcmp(nodes[depth - 2].name, "thumbnailBadgeViewModel") &&
       +            !strcmp(nodes[depth - 1].name, "text")) {
       +                if (value[0] && !item->duration[0])
       +                        strlcpy(item->duration, value, sizeof(item->duration));
       +        }
       +
       +        if (depth >= 14 &&
       +            nodes[depth - 1].type == JSON_TYPE_STRING &&
       +            !strcmp(nodes[depth - 13].name, "richItemRenderer") &&
       +            !strcmp(nodes[depth - 12].name, "content") &&
       +            !strcmp(nodes[depth - 11].name, "lockupViewModel") &&
       +            !strcmp(nodes[depth - 10].name, "metadata") &&
       +            !strcmp(nodes[depth - 9].name, "lockupMetadataViewModel") &&
       +            !strcmp(nodes[depth - 8].name, "metadata") &&
       +            !strcmp(nodes[depth - 7].name, "contentMetadataViewModel") &&
       +            !strcmp(nodes[depth - 6].name, "metadataRows") &&
       +            !strcmp(nodes[depth - 4].name, "metadataParts") &&
       +            !strcmp(nodes[depth - 2].name, "text") &&
       +            !strcmp(nodes[depth - 1].name, "content")) {
       +                if (strstr(value, "views")) {
       +                        if (value[0] && !item->viewcount[0])
       +                                strlcpy(item->viewcount, value, sizeof(item->viewcount));
       +                } else {
       +                        /* typically second item is published at */
       +                        if (value[0] && !item->publishedat[0])
       +                                strlcpy(item->publishedat, value, sizeof(item->publishedat));
       +                }
       +        }
       +
       +        if (depth >= 3 &&
       +            nodes[depth - 1].type == JSON_TYPE_STRING &&
       +            !strcmp(nodes[depth - 3].name, "microformat") &&
       +            !strcmp(nodes[depth - 2].name, "microformatDataRenderer")) {
       +                if (!strcmp(nodes[depth - 1].name, "urlCanonical")) {
       +                        /* copy ID from URL: https://www.youtube.com/channel/someid */
       +                        if ((p = strstr(value, "youtube.com/channel/"))) {
       +                                p += strlen("youtube.com/channel/");
       +                                strlcpy(r->channelid, p, sizeof(r->channelid));
       +                        }
       +                } else if (!strcmp(nodes[depth - 1].name, "title")) {
       +                        if (value[0] && !r->channeltitle[0])
       +                                strlcpy(r->channeltitle, value, sizeof(r->channeltitle));
       +                }
       +        }
        }
        
        static struct search_response *
       @@ -313,11 +404,23 @@ parse_search_response(const char *data)
                if (!(r = calloc(1, sizeof(*r))))
                        return NULL;
        
       +#if 0
       +        start = data;
       +        end = data + strlen(data);
       +#endif
       +
                if (extractjson_search(s, &start, &end) == -1) {
                        free(r);
                        return NULL;
                }
        
       +#if 0
       +        // DEBUG
       +        dprintf(2, "DEBUG: ");
       +        write(2, start, end - start);
       +        dprintf(2, "\n");
       +#endif
       +
                ret = parsejson(start, end - start, processnode_search, r);
                if (ret < 0) {
                        free(r);
       @@ -331,6 +434,12 @@ parse_search_response(const char *data)
                        item = &(r->items[i]);
                        len = strlen(item->channeltitle);
        
       +                /* copy channel title and ID if set and not set in the item (global microformat output) */
       +                if (r->channeltitle[0] && !item->channeltitle[0])
       +                        strlcpy(item->channeltitle, r->channeltitle, sizeof(item->channeltitle));
       +                if (r->channelid[0] && !item->channelid[0])
       +                        strlcpy(item->channelid, r->channelid, sizeof(item->channelid));
       +
                        if (len > sizeof(" - Topic") &&
                            !strcmp(item->channeltitle + len - sizeof(" - Topic") + 1, " - Topic")) {
                                /* reset information that doesn't work for topics */
       @@ -520,6 +629,12 @@ youtube_channel_videos(const char *channelid)
        {
                const char *data;
        
       +#if 0
       +        data = readfile("debug.json");
       +        if (!data)
       +                return NULL;
       +#endif
       +
                if (!(data = request_channel_videos(channelid)))
                        return NULL;
        
   DIR diff --git a/youtube/youtube.h b/youtube/youtube.h
       @@ -19,6 +19,8 @@ struct item {
        struct search_response {
                struct item items[MAX_VIDEOS + 1];
                size_t nitems;
       +        char channeltitle[1024];
       +        char channelid[256];
        };
        
        struct video_format {