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 {