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;