Introduction
Introduction Statistics Contact Development Disclaimer Help
youtube: various improvements - frontends - front-ends for some sites (experime…
Log
Files
Refs
README
LICENSE
---
commit 11f745425e13385e5a69cf3f8cdceaa3027dad64
parent 587b2d3d299bff29e6b941c22fe7aa526cbc9135
Author: Hiltjo Posthuma <[email protected]>
Date: Fri, 24 Feb 2023 21:51:44 +0100
youtube: various improvements
- initial support for detailed information of a video, only for youtube/cli for
now (-i option).
- list video formats per video, similar to youtube-dl/yt-dlp.
- various small fixes and improvements.
Diffstat:
M youtube/cli.c | 227 ++++++++++++++++++++++++++---…
M youtube/youtube.c | 211 +++++++++++++++++++++++++++++…
M youtube/youtube.h | 50 ++++++++++++++++++++++++++---…
3 files changed, 437 insertions(+), 51 deletions(-)
---
diff --git a/youtube/cli.c b/youtube/cli.c
@@ -26,17 +26,31 @@ printescape(const char *s)
fputc(*s, stdout);
}
+void
+printescape_multiline(const char *s, const char *indent)
+{
+ int i = 0;
+
+ for (; *s; ++s) {
+ if (!i)
+ fputs(indent, stdout);
+
+ if (*s == '\n') {
+ i = 0;
+ fputc(*s, stdout);
+ } else if (!iscntrl((unsigned char)*s)) {
+ fputc(*s, stdout);
+ i = 1;
+ }
+ }
+}
+
int
-render_tsv(struct search_response *r)
+render_search_tsv(struct search_response *r)
{
struct item *videos = r->items;
size_t i;
- if (pledge("stdio", NULL) == -1) {
- fprintf(stderr, "pledge: %s\n", strerror(errno));
- exit(1);
- }
-
for (i = 0; i < r->nitems; i++) {
OUTESCAPE(videos[i].id);
OUT("\t");
@@ -73,7 +87,7 @@ render_tsv(struct search_response *r)
}
int
-render(struct search_response *r)
+render_search(struct search_response *r)
{
struct item *videos = r->items;
size_t i;
@@ -81,31 +95,39 @@ render(struct search_response *r)
for (i = 0; i < r->nitems; i++) {
switch (videos[i].linktype) {
case Channel:
- OUT("[Channel] ");
+ OUT("Channel: ");
OUTESCAPE(videos[i].channeltitle);
break;
case Movie:
- OUT("[Movie] ");
+ OUT("Movie: ");
OUTESCAPE(videos[i].title);
break;
case Playlist:
- OUT("[Playlist] ");
+ OUT("Playlist: ");
OUTESCAPE(videos[i].title);
break;
default:
+ OUT(" ");
OUTESCAPE(videos[i].title);
break;
}
+ if (videos[i].duration[0]) {
+ OUT(" [");
+ OUTESCAPE(videos[i].duration);
+ OUT("]");
+ }
OUT("\n");
if (videos[i].id[0]) {
- OUT("URL: https://www.youtube.com/embed/");
+ OUT("URL: https://www.youtube.com/embed/");
OUTESCAPE(videos[i].id);
OUT("\n");
}
if (videos[i].channelid[0] || videos[i].userid[0]) {
- OUT("Atom feed: https://www.youtube.com/feeds/vide…
+ OUT("Channel: ");
+ OUTESCAPE(videos[i].channeltitle);
+ OUT(": https://www.youtube.com/feeds/videos.xml?");
if (videos[i].channelid[0]) {
OUT("channel_id=");
OUTESCAPE(videos[i].channelid);
@@ -115,37 +137,153 @@ render(struct search_response *r)
}
OUT("\n");
}
-
- if (videos[i].channelid[0] || videos[i].userid[0]) {
- OUT("Channel title: ");
- OUTESCAPE(videos[i].channeltitle);
- OUT("\n");
- if (videos[i].channelid[0]) {
- OUT("Channelid: ");
- OUTESCAPE(videos[i].channelid);
- OUT("\n");
- } else if (videos[i].userid[0]) {
- OUT("Userid: ");
- OUTESCAPE(videos[i].userid);
- OUT("\n");
- }
- }
if (videos[i].publishedat[0]) {
- OUT("Published: ");
+ OUT("Published: ");
OUTESCAPE(videos[i].publishedat);
OUT("\n");
}
if (videos[i].viewcount[0]) {
- OUT("Viewcount: ");
+ OUT("Views: ");
OUTESCAPE(videos[i].viewcount);
OUT("\n");
}
- if (videos[i].duration[0]) {
- OUT("Duration: " );
- OUTESCAPE(videos[i].duration);
+ OUT("\n");
+ }
+
+ return 0;
+}
+
+int
+render_video(struct video_response *r)
+{
+ struct video_format *f;
+ long l;
+ int i;
+
+ OUT("URL: ");
+ OUTESCAPE(r->id);
+ OUT(", https://www.youtube.com/embed/");
+ OUTESCAPE(r->id);
+ OUT("\n");
+
+ OUT("Title: ");
+ OUTESCAPE(r->title);
+ OUT("\n");
+
+ OUT("Views: ");
+ OUTESCAPE(r->viewcount);
+ OUT("\n");
+
+ OUT("Length: ");
+ OUTESCAPE(r->lengthseconds);
+ OUT("\n");
+
+ OUT("Published: ");
+ OUTESCAPE(r->publishdate);
+ OUT("\n");
+
+ OUT("Uploaded: ");
+ OUTESCAPE(r->uploaddate);
+ OUT("\n");
+
+ if (r->author[0]) {
+ OUT("Channel: ");
+ OUTESCAPE(r->author);
+ if (r->channelid[0]) {
+ OUT(": https://www.youtube.com/feeds/videos.xml?channe…
+ OUTESCAPE(r->channelid);
+ }
+ OUT("\n");
+ }
+
+ if (r->shortdescription[0]) {
+ OUT("Description:\n\n");
+ printescape_multiline(r->shortdescription, "");
+ OUT("\n");
+ }
+
+ if (r->nformats == 0)
+ return 0;
+
+ OUT("\n\nFormats:\n\n");
+
+ /* links expiration */
+ if (r->expiresinseconds[0]) {
+ OUT("Expires in ");
+ OUTESCAPE(r->expiresinseconds);
+ OUT(" seconds\n");
+ }
+
+ for (i = 0; i < r->nformats; i++) {
+ f = &(r->formats[i]);
+
+#if 0
+ l = strtol(f->width, NULL, 10);
+ if (l < 1280)
+ continue;
+ l = strtol(f->height, NULL, 10);
+ if (l < 720)
+ continue;
+#endif
+
+#if 0
+ OUT("\titag: ");
+ OUTESCAPE(f->itag);
+ OUT("\n");
+
+ OUT("\tLast modified: ");
+ OUTESCAPE(f->lastmodified);
+ OUT("\n");
+
+ OUT("\tContent-Length: ");
+ OUTESCAPE(f->contentlength);
+ OUT("\n");
+#endif
+
+ OUT("\tURL: ");
+ OUTESCAPE(f->url);
+ OUT("\n");
+
+ OUT("\tMime-type: ");
+ OUTESCAPE(f->mimetype);
+ OUT("\n");
+
+ OUT("\tBitrate: ");
+ OUTESCAPE(f->bitrate);
+ OUT("\n");
+
+ OUT("\tQuality: ");
+ if (f->qualitylabel[0])
+ OUTESCAPE(f->qualitylabel);
+ else if (f->quality[0])
+ OUTESCAPE(f->quality);
+
+ if (f->width[0]) {
+ OUT(", ");
+ OUTESCAPE(f->width);
+ OUT("x");
+ OUTESCAPE(f->height);
+ OUT("");
+ }
+ if (f->fps[0]) {
+ OUT(", ");
+ OUTESCAPE(f->fps);
+ OUT(" FPS");
+ }
+ OUT("\n");
+
+ if (f->audiochannels[0]) {
+ OUT("\tAudio channels: ");
+ OUTESCAPE(f->audiochannels);
+ OUT("\n");
+ }
+ if (f->audiosamplerate[0]) {
+ OUT("\tAudio sample rate: ");
+ OUTESCAPE(f->audiosamplerate);
OUT("\n");
}
- OUT("===\n");
+
+ OUT("\n");
}
return 0;
@@ -154,7 +292,7 @@ render(struct search_response *r)
static void
usage(const char *argv0)
{
- fprintf(stderr, "usage: %s [-t] <keyword> | <-c channelid> | <-u user>…
+ fprintf(stderr, "usage: %s [-t] <keyword> | <-c channelid> | <-u user>…
exit(1);
}
@@ -162,8 +300,9 @@ int
main(int argc, char *argv[])
{
struct search_response *r = NULL;
+ struct video_response *vr = NULL;
char search[1024];
- const char *keywords = NULL, *channelid = NULL, *user = NULL;
+ const char *keywords = NULL, *channelid = NULL, *user = NULL, *videoid…
int i, usetsv = 0;
if (pledge("stdio dns inet rpath unveil", NULL) == -1) {
@@ -180,6 +319,12 @@ main(int argc, char *argv[])
channelid = argv[i + 1];
i++;
break;
+ case 'i':
+ if (i + 1 >= argc)
+ usage(argv[0]);
+ videoid = argv[i + 1];
+ i++;
+ break;
case 'u':
if (i + 1 >= argc)
usage(argv[0]);
@@ -212,6 +357,14 @@ main(int argc, char *argv[])
r = youtube_channel_videos(channelid);
} else if (user) {
r = youtube_user_videos(user);
+ } else if (videoid) {
+ vr = youtube_video(videoid);
+ if (!vr || vr->isfound == 0) {
+ OUT("No video found\n");
+ exit(1);
+ }
+ render_video(vr);
+ return 0;
} else if (keywords) {
if (!uriencode(keywords, search, sizeof(search)))
usage(argv[0]);
@@ -228,9 +381,9 @@ main(int argc, char *argv[])
}
if (usetsv)
- render_tsv(r);
+ render_search_tsv(r);
else
- render(r);
+ render_search(r);
return 0;
}
diff --git a/youtube/youtube.c b/youtube/youtube.c
@@ -22,9 +22,25 @@ youtube_request(const char *path)
}
static char *
+request_video(const char *videoid)
+{
+ char path[2048];
+ int r;
+
+ r = snprintf(path, sizeof(path), "/watch?v=%s", videoid);
+ /* check if request is too long (truncation) */
+ if (r < 0 || (size_t)r >= sizeof(path))
+ return NULL;
+
+// return readfile("/tmp/data"); // DEBUG
+
+ return youtube_request(path);
+}
+
+static char *
request_channel_videos(const char *channelid)
{
- char path[4096];
+ char path[2048];
int r;
r = snprintf(path, sizeof(path), "/channel/%s/videos", channelid);
@@ -38,7 +54,7 @@ request_channel_videos(const char *channelid)
static char *
request_user_videos(const char *user)
{
- char path[4096];
+ char path[2048];
int r;
r = snprintf(path, sizeof(path), "/user/%s/videos", user);
@@ -81,7 +97,7 @@ request_search(const char *s, const char *page, const char *o…
}
static int
-extractjson(const char *s, const char **start, const char **end)
+extractjson_search(const char *s, const char **start, const char **end)
{
*start = strstr(s, "window[\"ytInitialData\"] = ");
if (*start) {
@@ -105,8 +121,23 @@ extractjson(const char *s, const char **start, const char …
return 0;
}
+static int
+extractjson_video(const char *s, const char **start, const char **end)
+{
+ *start = strstr(s, "var ytInitialPlayerResponse = ");
+ if (!*start)
+ return -1;
+ (*start) += sizeof("var ytInitialPlayerResponse = ") - 1;
+ *end = strstr(*start, "};<");
+ if (!*end)
+ return -1;
+ (*end)++;
+
+ return 0;
+}
+
static void
-processnode(struct json_node *nodes, size_t depth, const char *value,
+processnode_search(struct json_node *nodes, size_t depth, const char *value,
void *pp)
{
struct search_response *r = (struct search_response *)pp;
@@ -141,7 +172,6 @@ processnode(struct json_node *nodes, size_t depth, const ch…
nodes[depth - 3].type == JSON_TYPE_ARRAY &&
nodes[depth - 2].type == JSON_TYPE_OBJECT &&
nodes[depth - 1].type == JSON_TYPE_STRING &&
-
!strcmp(nodes[depth - 5].name, "videoRenderer") &&
!strcmp(nodes[depth - 4].name, "title") &&
!strcmp(nodes[depth - 3].name, "runs") &&
@@ -150,6 +180,23 @@ processnode(struct json_node *nodes, size_t depth, const c…
strlcpy(item->title, value, sizeof(item->title));
}
+ /* in channel/user videos listing there is a short description */
+#ifdef neinneinnein
+ if (depth >= 7 &&
+ nodes[depth - 5].type == JSON_TYPE_OBJECT &&
+ nodes[depth - 4].type == JSON_TYPE_OBJECT &&
+ nodes[depth - 3].type == JSON_TYPE_ARRAY &&
+ nodes[depth - 2].type == JSON_TYPE_OBJECT &&
+ nodes[depth - 1].type == JSON_TYPE_STRING &&
+ !strcmp(nodes[depth - 5].name, "videoRenderer") &&
+ !strcmp(nodes[depth - 4].name, "descriptionSnippet") &&
+ !strcmp(nodes[depth - 3].name, "runs") &&
+ !strcmp(nodes[depth - 1].name, "text") &&
+ !item->shortdescription[0]) {
+ strlcpy(item->shortdescription, value, sizeof(item->shortdescr…
+ }
+#endif
+
if (depth >= 5 &&
nodes[depth - 4].type == JSON_TYPE_OBJECT &&
nodes[depth - 3].type == JSON_TYPE_OBJECT &&
@@ -220,12 +267,151 @@ parse_search_response(const char *data)
if (!(r = calloc(1, sizeof(*r))))
return NULL;
- if (extractjson(s, &start, &end) == -1) {
+ if (extractjson_search(s, &start, &end) == -1) {
free(r);
return NULL;
}
- ret = parsejson(start, end - start, processnode, r);
+ ret = parsejson(start, end - start, processnode_search, r);
+ if (ret < 0) {
+ free(r);
+ return NULL;
+ }
+ return r;
+}
+
+static void
+processnode_video(struct json_node *nodes, size_t depth, const char *value,
+ void *pp)
+{
+ struct video_response *r = (struct video_response *)pp;
+ struct video_format *f;
+ static struct item *item;
+
+ if (depth > 1) {
+ if (nodes[0].type == JSON_TYPE_OBJECT &&
+ !strcmp(nodes[1].name, "streamingData")) {
+ r->isfound = 1; /* a video is found */
+
+ if (depth == 2 &&
+ nodes[2].type == JSON_TYPE_STRING &&
+ !strcmp(nodes[2].name, "expiresInSeconds")) {
+ strlcpy(r->expiresinseconds, value, sizeof(r->…
+ }
+
+ if (depth >= 3 &&
+ nodes[2].type == JSON_TYPE_ARRAY &&
+ (!strcmp(nodes[2].name, "formats") ||
+ !strcmp(nodes[2].name, "adaptiveFormats"))) {
+ if (r->nformats > MAX_FORMATS)
+ return; /* ignore: don't add too many …
+
+ if (depth == 4 && nodes[3].type == JSON_TYPE_O…
+ r->nformats++;
+ }
+
+ if (r->nformats == 0)
+ return;
+ f = &(r->formats[r->nformats - 1]); /* current…
+
+ if (depth == 5 &&
+ nodes[2].type == JSON_TYPE_ARRAY &&
+ nodes[3].type == JSON_TYPE_OBJECT &&
+ (nodes[4].type == JSON_TYPE_STRING ||
+ nodes[4].type == JSON_TYPE_NUMBER ||
+ nodes[4].type == JSON_TYPE_BOOL)) {
+ if (!strcmp(nodes[4].name, "width")) {
+ strlcpy(f->width, value, sizeo…
+ } else if (!strcmp(nodes[4].name, "hei…
+ strlcpy(f->height, value, size…
+ } else if (!strcmp(nodes[4].name, "url…
+ strlcpy(f->url, value, sizeof(…
+ } else if (!strcmp(nodes[4].name, "qua…
+ strlcpy(f->qualitylabel, value…
+ } else if (!strcmp(nodes[4].name, "qua…
+ strlcpy(f->quality, value, siz…
+ } else if (!strcmp(nodes[4].name, "fps…
+ strlcpy(f->fps, value, sizeof(…
+ } else if (!strcmp(nodes[4].name, "bit…
+ strlcpy(f->bitrate, value, siz…
+ } else if (!strcmp(nodes[4].name, "mim…
+ strlcpy(f->mimetype, value, si…
+ } else if (!strcmp(nodes[4].name, "ita…
+ strlcpy(f->itag, value, sizeof…
+ } else if (!strcmp(nodes[4].name, "con…
+ strlcpy(f->contentlength, valu…
+ } else if (!strcmp(nodes[4].name, "las…
+ strlcpy(f->lastmodified, value…
+ } else if (!strcmp(nodes[4].name, "aud…
+ strlcpy(f->audiochannels, valu…
+ } else if (!strcmp(nodes[4].name, "aud…
+ strlcpy(f->audiosamplerate, va…
+ }
+ }
+ }
+ }
+ }
+
+ if (depth == 4 &&
+ nodes[0].type == JSON_TYPE_OBJECT &&
+ nodes[1].type == JSON_TYPE_OBJECT &&
+ nodes[2].type == JSON_TYPE_OBJECT &&
+ nodes[3].type == JSON_TYPE_STRING &&
+ !strcmp(nodes[1].name, "microformat") &&
+ !strcmp(nodes[2].name, "playerMicroformatRenderer")) {
+ if (!strcmp(nodes[3].name, "publishDate")) {
+ strlcpy(r->publishdate, value, sizeof(r->publishdate));
+ } if (!strcmp(nodes[3].name, "uploadDate")) {
+ strlcpy(r->uploaddate, value, sizeof(r->uploaddate));
+ }
+ }
+
+ if (depth == 3) {
+ if (nodes[0].type == JSON_TYPE_OBJECT &&
+ nodes[2].type == JSON_TYPE_STRING &&
+ !strcmp(nodes[1].name, "videoDetails")) {
+ if (!strcmp(nodes[2].name, "title")) {
+ strlcpy(r->title, value, sizeof(r->title));
+ } else if (!strcmp(nodes[2].name, "videoId")) {
+ strlcpy(r->id, value, sizeof(r->id));
+ } else if (!strcmp(nodes[2].name, "lengthSeconds")) {
+ strlcpy(r->lengthseconds, value, sizeof(r->len…
+ } else if (!strcmp(nodes[2].name, "author")) {
+ strlcpy(r->author, value, sizeof(r->author));
+ } else if (!strcmp(nodes[2].name, "viewCount")) {
+ strlcpy(r->viewcount, value, sizeof(r->viewcou…
+ } else if (!strcmp(nodes[2].name, "channelId")) {
+ strlcpy(r->channelid, value, sizeof(r->channel…
+ } else if (!strcmp(nodes[2].name, "shortDescription"))…
+ strlcpy(r->shortdescription, value, sizeof(r->…
+ }
+ }
+ }
+}
+
+static struct video_response *
+parse_video_response(const char *data)
+{
+ struct video_response *r;
+ const char *s, *start, *end;
+ int ret;
+
+ if (!(s = strstr(data, "\r\n\r\n")))
+ return NULL; /* invalid response */
+ /* skip header */
+ s += strlen("\r\n\r\n");
+
+// s = data; // DEBUG
+
+ if (!(r = calloc(1, sizeof(*r))))
+ return NULL;
+
+ if (extractjson_video(s, &start, &end) == -1) {
+ free(r);
+ return NULL;
+ }
+
+ ret = parsejson(start, end - start, processnode_video, r);
if (ret < 0) {
free(r);
return NULL;
@@ -265,3 +451,14 @@ youtube_user_videos(const char *user)
return parse_search_response(data);
}
+
+struct video_response *
+youtube_video(const char *videoid)
+{
+ const char *data;
+
+ if (!(data = request_video(videoid)))
+ return NULL;
+
+ return parse_video_response(data);
+}
diff --git a/youtube/youtube.h b/youtube/youtube.h
@@ -8,19 +8,55 @@ struct item {
char publishedat[32];
char viewcount[32];
char duration[32];
+
+#ifdef neinneinnein
+ char shortdescription[4096];
+#endif
};
-#define MAX_VIDEOS 100
+#define MAX_VIDEOS 50
struct search_response {
struct item items[MAX_VIDEOS + 1];
size_t nitems;
};
-struct search_response *
-youtube_search(const char *rawsearch, const char *page, const char *order);
+struct video_format {
+ char itag[32]; /* video id */
+ char url[2048];
+ char mimetype[256]; /* mime-type and video codecs, etc */
+ char bitrate[256];
+ char width[32]; /* pixel width */
+ char height[32]; /* pixel width */
+ char fps[16]; /* frames-per-second */
+ char qualitylabel[64];
+ char quality[64];
+ char contentlength[64]; /* content length in bytes */
+ char lastmodified[64];
+ char audiosamplerate[32];
+ char audiochannels[16];
+};
+
+#define MAX_FORMATS 50
+struct video_response {
+ char id[32]; /* video id */
+ char title[1024];
+ char author[1024]; /* channel name / title */
+ char channelid[256];
+ char publishdate[32]; /* YYYY-mm-dd */
+ char uploaddate[32]; /* YYYY-mm-dd */
+ char viewcount[32];
+ char lengthseconds[32];
+ char shortdescription[4096 * 4];
+
+ int isfound;
-struct search_response *
-youtube_channel_videos(const char *channelid);
+ /* expiration for URLs in video formats */
+ char expiresinseconds[32];
+ struct video_format formats[MAX_FORMATS + 1];
+ int nformats;
+};
-struct search_response *
-youtube_user_videos(const char *user);
+struct search_response *youtube_search(const char *rawsearch, const char *page…
+struct search_response *youtube_channel_videos(const char *channelid);
+struct search_response *youtube_user_videos(const char *user);
+struct video_response *youtube_video(const char *videoid);
You are viewing proxied material from codemadness.org. The copyright of proxied material belongs to its original authors. Any comments or complaints in relation to proxied material should be directed to the original authors of the content concerned. Please see the disclaimer for more details.