show members only videos and unplayable status - frontends - front-ends for some sites (experiment)
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit fa4e5a13ff43b7e0d7849355c2b5b3334da5c9b3
 (DIR) parent dfe9d705355efc8d67dfb40f015f503bc5a089bf
 (HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Sat, 18 Oct 2025 17:13:32 +0200
       
       show members only videos and unplayable status
       
       In the search page members only videos are now visible with "[Members only] ".
       In the detailed video page the status will be shown (if present).
       Videos can be unplayable for members only or other regions, etc.
       
       Some more work needed on youtube/feed.c
       
       Diffstat:
         M youtube/cgi.c                       |      14 ++++++++++++++
         M youtube/cli.c                       |      32 ++++++++++++++++++++++---------
         M youtube/gopher.c                    |      14 ++++++++++++++
         M youtube/youtube.c                   |      30 ++++++++++++++++++++++++++++++
         M youtube/youtube.h                   |       3 +++
       
       5 files changed, 84 insertions(+), 9 deletions(-)
       ---
 (DIR) diff --git a/youtube/cgi.c b/youtube/cgi.c
       @@ -212,6 +212,9 @@ render_search(struct search_response *r)
                                        OUT(">");
                                }
        
       +                        if (v->membersonly)
       +                                OUT("[Members only] ");
       +
                                switch (v->linktype) {
                                case Channel:
                                        OUT("[Channel] ");
       @@ -352,6 +355,17 @@ render_video(struct video_response *r)
                        OUT("</td></tr>\n");
                }
        
       +        /* show playability state and reason: for example when it is a members-only video */
       +        if (r->playabilityreason[0]) {
       +                OUT("<tr><td><b>Status:</b></td><td><b>");
       +                if (r->playabilitystatus[0]) {
       +                        OUT(r->playabilitystatus);
       +                        OUT(": ");
       +                }
       +                OUT(r->playabilityreason);
       +                OUT("</b></td></tr>\n");
       +        }
       +
                if (r->author[0]) {
                        OUT("<tr><td><b>Channel:</b></td><td>");
                        if (r->channelid[0]) {
 (DIR) diff --git a/youtube/cli.c b/youtube/cli.c
       @@ -101,6 +101,8 @@ render_search_tsv(struct search_response *r)
                        OUTESCAPE(v->userid);
                        OUT("\t");
                        OUTENCODED(v->shortdescription);
       +                OUT("\t");
       +                OUT(v->membersonly ? "1" : "0");
                        OUT("\n");
                }
        
       @@ -117,23 +119,24 @@ render_search(struct search_response *r)
                        v = &(r->items[i]);
        
                        switch (v->linktype) {
       +                case Channel:  OUT("Channel:   "); break;
       +                case Movie:    OUT("Movie:     "); break;
       +                case Playlist: OUT("Playlist:  "); break;
       +                default:       OUT("           "); break;
       +                }
       +
       +                switch (v->linktype) {
                        case Channel:
                                OUT("Channel:   ");
                                OUTESCAPE(v->channeltitle);
                                break;
       -                case Movie:
       -                        OUT("Movie:     ");
       -                        OUTESCAPE(v->title);
       -                        break;
       -                case Playlist:
       -                        OUT("Playlist:  ");
       -                        OUTESCAPE(v->title);
       -                        break;
                        default:
       -                        OUT("           ");
       +                        if (v->membersonly)
       +                                OUT("[Members only] ");
                                OUTESCAPE(v->title);
                                break;
                        }
       +
                        if (v->duration[0]) {
                                OUT(" [");
                                OUTESCAPE(v->duration);
       @@ -200,6 +203,17 @@ render_video(struct video_response *r)
                        OUT("\n");
                }
        
       +        /* show playability state and reason: for example when it is a members-only video */
       +        if (r->playabilityreason[0]) {
       +                OUT("Status:    ");
       +                if (r->playabilitystatus[0]) {
       +                        OUTESCAPE(r->playabilitystatus);
       +                        OUT(": ");
       +                }
       +                OUTESCAPE(r->playabilityreason);
       +                OUT("\n");
       +        }
       +
                OUT("Views:     ");
                snprintf(buf, sizeof(buf), "%ld", r->viewcount);
                if (!printnumsep(buf))
 (DIR) diff --git a/youtube/gopher.c b/youtube/gopher.c
       @@ -76,6 +76,9 @@ render_search(struct search_response *r)
                        else
                                putchar('i');
        
       +                if (v->membersonly)
       +                        OUT("[Members only] ");
       +
                        switch (v->linktype) {
                        case Channel:
                                OUT("[Channel] ");
       @@ -197,6 +200,17 @@ render_video(struct video_response *r)
                        printf("\t%s\t%s\t%s\r\n", "", host, port);
                }
        
       +        /* show playability state and reason: for example when it is a members-only video */
       +        if (r->playabilityreason[0]) {
       +                OUT("iStatus:    ");
       +                if (r->playabilitystatus[0]) {
       +                        OUTTEXT(r->playabilitystatus);
       +                        OUTTEXT(": ");
       +                }
       +                OUTTEXT(r->playabilityreason);
       +                printf("\t%s\t%s\t%s\r\n", "", host, port);
       +        }
       +
                OUT("hThumbnail: https://i.ytimg.com/vi/");
                OUTTEXT(r->id);
                OUT("/hqdefault.jpg\tURL:https://i.ytimg.com/vi/");
 (DIR) diff --git a/youtube/youtube.c b/youtube/youtube.c
       @@ -226,6 +226,21 @@ processnode_search(struct json_node *nodes, size_t depth, const char *value, siz
                        strlcat(item->shortdescription, value, sizeof(item->shortdescription));
                }
        
       +        /* try to detect members/sponsor/subscription-only videos */
       +        if (depth >= 7 &&
       +            nodes[depth - 5].type == JSON_TYPE_OBJECT &&
       +            nodes[depth - 4].type == JSON_TYPE_ARRAY &&
       +            nodes[depth - 3].type == JSON_TYPE_OBJECT &&
       +            nodes[depth - 2].type == JSON_TYPE_OBJECT &&
       +            nodes[depth - 1].type == JSON_TYPE_STRING &&
       +            isrenderername(nodes[depth - 5].name) &&
       +            !strcmp(nodes[depth - 4].name, "badges") &&
       +            !strcmp(nodes[depth - 2].name, "metadataBadgeRenderer") &&
       +            !strcmp(nodes[depth - 1].name, "label")) {
       +                if (strstr(value, "Members only"))
       +                        item->membersonly = 1;
       +        }
       +
                if (depth >= 5 &&
                    nodes[depth - 4].type == JSON_TYPE_OBJECT &&
                    nodes[depth - 3].type == JSON_TYPE_OBJECT &&
       @@ -335,6 +350,21 @@ processnode_video(struct json_node *nodes, size_t depth, const char *value, size
                struct video_format *f;
        
                if (depth > 1) {
       +                /* playability status: could be unplayable / members-only video */
       +                if (nodes[0].type == JSON_TYPE_OBJECT &&
       +                    !strcmp(nodes[1].name, "playabilityStatus")) { /* example: "UNPLAYABLE" */
       +                        if (depth == 3 &&
       +                            nodes[2].type == JSON_TYPE_STRING &&
       +                            !strcmp(nodes[2].name, "status")) {
       +                                strlcpy(r->playabilitystatus, value, sizeof(r->playabilitystatus));
       +                        }
       +                        if (depth == 3 &&
       +                            nodes[2].type == JSON_TYPE_STRING &&
       +                            !strcmp(nodes[2].name, "reason")) {
       +                                strlcpy(r->playabilityreason, value, sizeof(r->playabilityreason));
       +                        }
       +                }
       +
                        if (nodes[0].type == JSON_TYPE_OBJECT &&
                            !strcmp(nodes[1].name, "streamingData")) {
                                if (depth == 2 &&
 (DIR) diff --git a/youtube/youtube.h b/youtube/youtube.h
       @@ -9,6 +9,7 @@ struct item {
                char viewcount[32]; /* view count string, formatted */
                char duration[32]; /* duration string */
                char shortdescription[4096];
       +        int membersonly; /* members only videos */
        };
        
        #define MAX_VIDEOS 50
       @@ -49,6 +50,8 @@ struct video_response {
                char category[256];
                int isfamilysafe;
                int isunlisted;
       +        char playabilitystatus[64];
       +        char playabilityreason[256];
        
                int isfound;