URI: 
       youtube/feed: support Shorts or items not found on the channel - 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 c698f5e349d484a4d2b0182b95c016f68d079212
   DIR parent 0563dc62349cd62abd39cb40cd626a28620f5839
  HTML Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Tue, 12 May 2026 20:47:51 +0200
       
       youtube/feed: support Shorts or items not found on the channel
       
       youtube/feed matches videos of the channel with the Atom feed and combines them
       (add the duration to the title, etc).  This excludes Youtube Shorts and other
       kinds of videos (a feature!).
       
       Add a specifier to the format "+notfound" to optionally show them.
       Add "[Short]" in the title (detected by "/shorts/" in the URL).
       
       Code cleanup: parse "format" with isformat() function.
       
       Some channels do have interesting Shorts sometimes.
       
       Diffstat:
         M youtube/feed.c                      |     119 +++++++++++++++++++++----------
       
       1 file changed, 81 insertions(+), 38 deletions(-)
       ---
   DIR diff --git a/youtube/feed.c b/youtube/feed.c
       @@ -19,6 +19,8 @@
        /* string and byte-length */
        #define STRP(s)           s,sizeof(s)-1
        
       +#define YT_SHORTS_TITLE " [Short]"
       +
        enum FeedType {
                FeedTypeNone = 0,
                FeedTypeAtom = 2
       @@ -155,8 +157,10 @@ static XMLParser parser; /* XML parser state */
        static String attrrel, tmpstr;
        
        static struct search_response *search_res = NULL;
       -static void (*printfields)(void) = sfeed_item;
       +static void (*printfields)(void) = sfeed_item; /* default: sfeed(5) format */
        static int cgimode = 0, godmode = 0;
       +/* allow items not found/matched on the channel with the feed (like shorts). */
       +static int allownotfound = 0;
        static const char *server_name = "127.0.0.1", *server_port = "70";
        
        static int
       @@ -456,14 +460,17 @@ atom_item(void)
                }
                /* Only print the video if it was found in the feed aswell.
                   This way it filters away shorts too. */
       -        if (!found)
       +        if (!allownotfound && !found)
                        return;
        
                fputs("<entry>\n\t<title>", stdout);
       -        if (found->membersonly)
       +        if (found && found->membersonly)
                        xmlencode(MEMBERS_ONLY);
                xmlencode(ctx.fields[FeedFieldTitle].str.data);
       -        if (found->duration[0]) {
       +        if (ctx.fields[FeedFieldLink].str.len &&
       +            strstr(ctx.fields[FeedFieldLink].str.data, "/shorts/"))
       +                fputs(YT_SHORTS_TITLE, stdout);
       +        if (found && found->duration[0]) {
                        fputs(" [", stdout);
                        xmlencode(found->duration);
                        fputs("]", stdout);
       @@ -535,7 +542,7 @@ html_item(void)
                }
                /* Only print the video if it was found in the feed aswell.
                   This way it filters away shorts too. */
       -        if (!found)
       +        if (!allownotfound && !found)
                        return;
        
                /* just print the original timestamp, it should conform */
       @@ -548,10 +555,13 @@ html_item(void)
                        fputs("\">", stdout);
                }
        
       -        if (found->membersonly)
       +        if (found && found->membersonly)
                        xmlencode(MEMBERS_ONLY);
                xmlencode(ctx.fields[FeedFieldTitle].str.data);
       -        if (found->duration[0]) {
       +        if (ctx.fields[FeedFieldLink].str.len &&
       +            strstr(ctx.fields[FeedFieldLink].str.data, "/shorts/"))
       +                fputs(YT_SHORTS_TITLE, stdout);
       +        if (found && found->duration[0]) {
                        fputs(" [", stdout);
                        xmlencode(found->duration);
                        fputs("]", stdout);
       @@ -596,17 +606,20 @@ gph_item(void)
                }
                /* Only print the video if it was found in the feed aswell.
                   This way it filters away shorts too. */
       -        if (!found)
       +        if (!allownotfound && !found)
                        return;
        
                fputs("h", stdout);
                /* just print the original timestamp, it should conform */
                gphencode(ctx.fields[FeedFieldTime].str.data);
                fputs(" ", stdout);
       -        if (found->membersonly)
       +        if (found && found->membersonly)
                        gphencode(MEMBERS_ONLY);
                gphencode(ctx.fields[FeedFieldTitle].str.data);
       -        if (found->duration[0]) {
       +        if (ctx.fields[FeedFieldLink].str.len &&
       +            strstr(ctx.fields[FeedFieldLink].str.data, "/shorts/"))
       +                gphencode(YT_SHORTS_TITLE);
       +        if (found && found->duration[0]) {
                        fputs(" [", stdout);
                        gphencode(found->duration);
                        fputs("]", stdout);
       @@ -667,7 +680,7 @@ json_item(void)
                }
                /* Only print the video if it was found in the feed aswell.
                   This way it filters away shorts too. */
       -        if (!found)
       +        if (!allownotfound && !found)
                        return;
        
                if (!json_firstitem)
       @@ -684,10 +697,13 @@ json_item(void)
                fputs("\"", stdout);
        
                fputs(",\n\t\"title\": \"", stdout);
       -        if (found->membersonly)
       +        if (found && found->membersonly)
                        json_printfield(MEMBERS_ONLY);
                json_printfield(ctx.fields[FeedFieldTitle].str.data);
       -        if (found->duration[0]) {
       +        if (ctx.fields[FeedFieldLink].str.len &&
       +            strstr(ctx.fields[FeedFieldLink].str.data, "/shorts/"))
       +                json_printfield(YT_SHORTS_TITLE);
       +        if (found && found->duration[0]) {
                        fputs(" [", stdout);
                        json_printfield(found->duration);
                        fputs("]", stdout);
       @@ -728,15 +744,18 @@ sfeed_item(void)
                }
                /* Only print the video if it was found in the feed aswell.
                   This way it filters away shorts too. */
       -        if (!found)
       +        if (!allownotfound && !found)
                        return;
        
                string_print_timestamp(&ctx.fields[FeedFieldTime].str);
                putchar(FieldSeparator);
       -        if (found->membersonly)
       +        if (found && found->membersonly)
                        fputs(MEMBERS_ONLY, stdout);
                string_print(&ctx.fields[FeedFieldTitle].str);
       -        if (found->duration[0]) {
       +        if (ctx.fields[FeedFieldLink].str.len &&
       +            strstr(ctx.fields[FeedFieldLink].str.data, "/shorts/"))
       +                fputs(YT_SHORTS_TITLE, stdout);
       +        if (found && found->duration[0]) {
                        fputs(" [", stdout);
                        fputs(found->duration, stdout);
                        fputs("]", stdout);
       @@ -775,15 +794,18 @@ twtxt_item(void)
                }
                /* Only print the video if it was found in the feed aswell.
                   This way it filters away shorts too. */
       -        if (!found)
       +        if (!allownotfound && !found)
                        return;
        
                string_print(&ctx.fields[FeedFieldTime].str);
                putchar(FieldSeparator);
       -        if (found->membersonly)
       +        if (found && found->membersonly)
                        fputs(MEMBERS_ONLY, stdout);
                string_print(&ctx.fields[FeedFieldTitle].str);
       -        if (found->duration[0]) {
       +        if (ctx.fields[FeedFieldLink].str.len &&
       +            strstr(ctx.fields[FeedFieldLink].str.data, "/shorts/"))
       +                fputs(YT_SHORTS_TITLE, stdout);
       +        if (found && found->duration[0]) {
                        fputs(" [", stdout);
                        fputs(found->duration, stdout);
                        fputs("]", stdout);
       @@ -1050,7 +1072,7 @@ void
        usage(void)
        {
                const char *line1 = "Bad Request, path should be the channel id + file extension, for example: UCrbvoMC0zUvPL8vjswhLOSw.json";
       -        const char *line2 = "Supported extensions are: [atom|gph|html|json|tsv|txt]";
       +        const char *line2 = "Supported extensions are: [atom|gph|html|json|tsv|txt][+notfound]";
        
                if (cgimode) {
                        if (godmode) {
       @@ -1064,12 +1086,28 @@ usage(void)
                        }
                        exit(0);
                } else {
       -                fputs("usage: feed <channelid> [atom|gph|html|json|tsv|txt]\n", stderr);
       +                fputs("usage: feed <channelid> [atom|gph|html|json|tsv|txt][+notfound]\n", stderr);
                        fputs("For example: feed UCrbvoMC0zUvPL8vjswhLOSw txt\n", stderr);
                        exit(1);
                }
        }
        
       +/* check format, ignore specifier, like "+notfound" */
       +int
       +isformat(const char *input, const char *check)
       +{
       +        size_t len;
       +
       +        len = strcspn(input, "+");
       +        if (!len)
       +                return 0;
       +
       +        if (!strncmp(input, check, len))
       +                return 1;
       +
       +        return 0;
       +}
       +
        int
        main(int argc, char *argv[])
        {
       @@ -1118,21 +1156,26 @@ main(int argc, char *argv[])
                if (!channelid || !isvalidchannel(channelid))
                        usage();
        
       -        if (!strcmp(format, "atom") || !strcmp(format, "xml"))
       +        /* formats: if invalid use the default */
       +        if (isformat(format, "atom") || isformat(format, "xml"))
                        printfields = atom_item;
       -        else if (!strcmp(format, "gph"))
       +        else if (isformat(format, "gph"))
                        printfields = gph_item;
       -        else if (!strcmp(format, "html"))
       +        else if (isformat(format, "html"))
                        printfields = html_item;
       -        else if (!strcmp(format, "json"))
       +        else if (isformat(format, "json"))
                        printfields = json_item;
       -        else if (!strcmp(format, "tsv") || !strcmp(format, "sfeed"))
       +        else if (isformat(format, "tsv") || isformat(format, "sfeed"))
                        printfields = sfeed_item;
       -        else if (!strcmp(format, "txt") || !strcmp(format, "twtxt"))
       +        else if (isformat(format, "txt") || isformat(format, "twtxt"))
                        printfields = twtxt_item;
                else
                        usage();
        
       +        /* specifier to allow items to print items not matched (for shorts) */
       +        if (strstr(format, "+notfound"))
       +                allownotfound = 1;
       +
                search_res = youtube_channel_videos(channelid);
                if (!search_res || search_res->nitems == 0) {
                        /* error or no videos found */
       @@ -1167,35 +1210,35 @@ main(int argc, char *argv[])
        
                if (cgimode && !godmode) {
                        fputs("Status: 200 OK\r\n", stdout);
       -                if (!strcmp(format, "atom") || !strcmp(format, "xml"))
       +                if (isformat(format, "atom") || isformat(format, "xml"))
                                fputs("Content-Type: text/xml; charset=utf-8\r\n\r\n", stdout);
       -                else if (!strcmp(format, "html"))
       +                else if (isformat(format, "html"))
                                fputs("Content-Type: text/html; charset=utf-8\r\n\r\n", stdout);
       -                else if (!strcmp(format, "json"))
       +                else if (isformat(format, "json"))
                                fputs("Content-Type: application/json; charset=utf-8\r\n\r\n", stdout);
                        else
                                fputs("Content-Type: text/plain; charset=utf-8\r\n\r\n", stdout);
                }
        
       -        if (!strcmp(format, "atom") || !strcmp(format, "xml"))
       +        if (isformat(format, "atom") || isformat(format, "xml"))
                        atom_header();
       -        else if (!strcmp(format, "gph"))
       +        else if (isformat(format, "gph"))
                        gph_header();
       -        else if (!strcmp(format, "html"))
       +        else if (isformat(format, "html"))
                        html_header();
       -        else if (!strcmp(format, "json"))
       +        else if (isformat(format, "json"))
                        json_header();
        
                /* NOTE: getnext is defined in xml.h for inline optimization */
                xml_parse(&parser);
        
       -        if (!strcmp(format, "atom") || !strcmp(format, "xml"))
       +        if (isformat(format, "atom") || isformat(format, "xml"))
                        atom_footer();
       -        else if (!strcmp(format, "gph"))
       +        else if (isformat(format, "gph"))
                        gph_footer();
       -        else if (!strcmp(format, "html"))
       +        else if (isformat(format, "html"))
                        html_footer();
       -        else if (!strcmp(format, "json"))
       +        else if (isformat(format, "json"))
                        json_footer();
        
                return 0;