ref: 1f0b53af3f9d0fcc462883c19a9d55669690ca20
author: Sigrid Solveig Haflínudóttir <ftrvxmtrx@gmail.com>
date: Sun Jun 20 13:47:29 EDT 2021
first
--- /dev/null
+++ b/README.md
@@ -1,0 +1,23 @@
+# nvi
+
+Downloads a Youtube video using Invidious public servers.
+
+## Examples
+
+Show available streams of a video:
+
+ nvi -i ybGOT4d2Hs8
+
+Download audio and video in best quality and play using
+[treason](https://git.sr.ht/~ft/treason):
+
+ nvi -a /tmp/audio -v /tmp/video ybGOT4d2Hs8 && treason -a /tmp/audio -v /tmp/video
+
+Download only audio, extract OPUS from the container using
+[mcfs](https://git.sr.ht/~ft/mcfs), and reencode to OGG/Vorbis:
+
+ nvi -a /fd/1 -A 251 ybGOT4d2Hs8 | mcfs -t audio | audio/opusdec | audio/oggenc > suffer.ogg
+
+Download and play combined audio and video (low quality):
+
+ nvi -V 18 -v /tmp/video ybGOT4d2Hs8 && treason /tmp/video
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,13 @@
+</$objtype/mkfile
+
+BIN=/$objtype/bin
+TARG=nvi
+
+HFILES=\
+
+OFILES=\
+ nvi.$O\
+ util.$O\
+ youtube.$O\
+
+</sys/src/cmd/mkone
--- /dev/null
+++ b/nvi.c
@@ -1,0 +1,141 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include "nvi.h"
+
+int cmd = Cdownload;
+int debug = 0;
+
+static int
+cmpfmt(void *a_, void *b_)
+{
+ Format *a, *b;
+
+ a = a_;
+ b = b_;
+ if(a->included != b->included)
+ return a->included - b->included;
+
+ return b->sz - a->sz;
+}
+
+static void
+usage(void)
+{
+ fprint(2, "usage: %s [-i | [-a file_audio] [-v file_video] [-A id|quality] [-V id|quality]] url|id\n", argv0);
+ threadexitsall("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+ char *vid, *oa, *ov, *ida[8], *idv[8];
+ Format *f, *fa, *fv, *ba, *bv;
+ Info *(*fun)(char *), *info;
+ int i, j, nida, nidv;
+ int afd, vfd;
+
+ fmtinstall('Z', Zfmt);
+
+ fun = youtube;
+ nida = 0;
+ nidv = 0;
+ oa = nil;
+ ov = nil;
+ ba = nil;
+ bv = nil;
+ ARGBEGIN{
+ case 'd':
+ debug++;
+ break;
+ case 'i':
+ cmd = Cinfo;
+ break;
+ case 'a':
+ oa = EARGF(usage());
+ break;
+ case 'v':
+ ov = EARGF(usage());
+ break;
+ case 'A':
+ if(nida >= nelem(ida))
+ sysfatal("too many ids for audio");
+ ida[nida++] = EARGF(usage());
+ break;
+ case 'V':
+ if(nidv >= nelem(idv))
+ sysfatal("too many ids for video");
+ idv[nidv++] = EARGF(usage());
+ break;
+ default:
+ usage();
+ }ARGEND
+
+ if(argc != 1)
+ usage();
+ if((vid = strrchr(argv[0], '/')) != nil)
+ vid++;
+ else
+ vid = argv[0];
+
+ if((info = fun(vid)) == nil){
+ fprint(2, "%r\n");
+ threadexitsall("failed");
+ }
+
+ qsort(info->fmt, info->nfmt, sizeof(Format), cmpfmt);
+ if(cmd == Cinfo){
+ for(i = 0, f = info->fmt; i < info->nfmt; i++, f++)
+ print("%d\t%s\t%Z\t%s\n", f->id, f->quality, f->sz, f->type);
+ }else if(cmd == Cdownload){
+ for(j = 0, fa = nil, f = info->fmt; j < info->nfmt && fa == nil; j++, f++){
+ if((f->included == Iaudio) == 0)
+ continue;
+ if(ba == nil)
+ ba = f;
+ for(i = 0; i < nida && fa == nil; i++){
+ if((alldigit(ida[i]) && atoi(ida[i]) == f->id) || strcmp(f->quality, ida[i]) == 0){
+ fa = f;
+ break;
+ }
+ }
+ }
+ for(j = 0, fv = nil, f = info->fmt; j < info->nfmt && fv == nil; j++, f++){
+ if((f->included & Ivideo) == 0)
+ continue;
+ if(bv == nil)
+ bv = f;
+ for(i = 0; i < nidv && fv == nil; i++){
+ if((alldigit(idv[i]) && atoi(idv[i]) == f->id) || strcmp(f->quality, idv[i]) == 0){
+ fv = f;
+ break;
+ }
+ }
+ }
+ if(fv == nil)
+ fv = bv;
+ if(fa == nil && (fv->included & Iaudio) == 0) /* no audio and video stream doesn't have it either */
+ fa = ba;
+
+ i = 0;
+ if(oa != nil){
+ if((afd = open(oa, OWRITE|OTRUNC)) < 0 && (afd = create(oa, OWRITE|OTRUNC, 0644)) < 0)
+ sysfatal("%r");
+ if(hget(fa->url, afd) < 0)
+ sysfatal("%r");
+ i++;
+ }
+ if(ov != nil){
+ if((vfd = open(ov, OWRITE|OTRUNC)) < 0 && (vfd = create(ov, OWRITE|OTRUNC, 0644)) < 0)
+ sysfatal("%r");
+ if(hget(fv->url, vfd) < 0)
+ sysfatal("%r");
+ i++;
+ }
+
+ while(i-- > 0)
+ procwait();
+ }
+
+ threadexitsall(nil);
+}
--- /dev/null
+++ b/nvi.h
@@ -1,0 +1,46 @@
+typedef struct Info Info;
+typedef struct Format Format;
+
+struct Format {
+ char *url;
+ char *type;
+ char *quality; /* "unknown" for audio, "360p"/etc for video */
+ vlong sz;
+ vlong bitrate;
+ int included; /* Iaudio|Ivideo */
+ int fps;
+ int id;
+};
+
+struct Info {
+ char *author;
+ char *title;
+ char *description;
+ vlong published;
+ vlong length;
+ Format *fmt;
+ int nfmt;
+};
+
+enum {
+ Cdownload,
+ Cinfo,
+
+ Iaudio = 1<<0,
+ Ivideo = 1<<1,
+};
+
+extern int cmd;
+extern int debug;
+
+Info *youtube(char *vid);
+
+int pipeexec(int *fd, char *file, char **argv);
+void procwait(void);
+char *readall(int f);
+int hget(char *url, int out);
+char *estrdup(char *s);
+int alldigit(char *s);
+
+#pragma varargck type "Z" vlong
+int Zfmt(Fmt *f);
--- /dev/null
+++ b/util.c
@@ -1,0 +1,151 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <ctype.h>
+#include "nvi.h"
+
+enum {
+ Us,
+ Them,
+};
+
+typedef struct Exec Exec;
+
+struct Exec {
+ char *file;
+ char **argv;
+ int p[2];
+ Channel *pid;
+};
+
+static void
+pexec(void *args)
+{
+ Exec *e = args;
+
+ if(e->p[0] >= 0){
+ dup(e->p[Them], 0);
+ dup(e->p[Them], 1);
+ close(e->p[0]);
+ close(e->p[1]);
+ }else{
+ close(0);
+ close(1);
+ }
+ if(debug < 1)
+ close(2);
+ procexec(e->pid, e->file, e->argv);
+}
+
+int
+pipeexec(int *fd, char *file, char **argv)
+{
+ int pid;
+ Exec e;
+
+ threadwaitchan();
+
+ e.file = file;
+ e.argv = argv;
+ e.pid = chancreate(sizeof(int), 0);
+ e.p[0] = fd[0];
+ e.p[1] = fd[1];
+ procrfork(pexec, &e, 4096, RFFDG);
+ recv(e.pid, &pid);
+ chanfree(e.pid);
+ close(e.p[Them]);
+ if(pid < 0){
+ close(e.p[Us]);
+ return -1;
+ }
+
+ return e.p[Us];
+}
+
+void
+procwait(void)
+{
+ free(recvp(threadwaitchan()));
+}
+
+char *
+readall(int f)
+{
+ int bufsz, sz, n;
+ char *s;
+
+ bufsz = 1023;
+ s = nil;
+ for(sz = 0;; sz += n){
+ if(bufsz-sz < 1024){
+ bufsz *= 2;
+ s = realloc(s, bufsz);
+ }
+ if((n = readn(f, s+sz, bufsz-sz-1)) < 1)
+ break;
+ }
+ if(n < 0 || sz < 1){
+ free(s);
+ return nil;
+ }
+ s[sz] = 0;
+
+ return s;
+}
+
+int
+hget(char *url, int out)
+{
+ char *argv[] = {"hget", url, nil};
+ int p[2];
+
+ if(out >= 0){
+ p[0] = open("/dev/null", OREAD);
+ p[1] = out;
+ }else{
+ pipe(p);
+ }
+
+ return pipeexec(p, "/bin/hget", argv);
+}
+
+char *
+estrdup(char *s)
+{
+ if((s = strdup(s == nil ? "" : s)) == nil)
+ sysfatal("memory");
+
+ return s;
+}
+
+int
+alldigit(char *s)
+{
+ if(*s == 0)
+ return 0;
+
+ for(; *s; s++)
+ if(!isdigit(*s))
+ return 0;
+
+ return 1;
+}
+
+int
+Zfmt(Fmt *f)
+{
+ vlong z;
+
+ z = va_arg(f->args, vlong);
+
+ if(z > 1024*1024*1024)
+ return fmtprint(f, "%.1fGb", (double)z / (1024.0*1024.0*1024.0));
+ if(z > 1024*1024)
+ return fmtprint(f, "%.1fMb", (double)z / (1024.0*1024.0));
+ if(z > 1024)
+ return fmtprint(f, "%lldKb", z / 1024);
+ if(z == 0)
+ return fmtprint(f, "----");
+
+ return fmtprint(f, "%lld", z);
+}
--- /dev/null
+++ b/youtube.c
@@ -1,0 +1,137 @@
+#include <u.h>
+#include <libc.h>
+#include <json.h>
+#include "nvi.h"
+
+static char *instlst = "https://api.invidious.io/instances.json?sort_by=health,type,users,signup";
+static char *fmtnames[] = {"adaptiveFormats", "formatStreams", nil};
+
+static int
+addfmt(Info *i, JSONEl *f)
+{
+ Format *fmt;
+ JSON *x;
+ char *s;
+
+ if((x = jsonbyname(f->val, "url")) == nil){
+ werrstr("no url");
+ return -1;
+ }
+ if((s = jsonstr(jsonbyname(f->val, "type"))) == nil){
+ werrstr("no type");
+ return -1;
+ }
+ i->nfmt++;
+ if((i->fmt = realloc(i->fmt, i->nfmt * sizeof(*fmt))) == nil)
+ sysfatal("memory");
+ fmt = &i->fmt[i->nfmt - 1];
+ memset(fmt, 0, sizeof(*fmt));
+ fmt->url = estrdup(jsonstr(x));
+
+ fmt->type = estrdup(s);
+ if(strncmp(s, "audio/", 6) == 0){
+ fmt->included |= Iaudio;
+ fmt->quality = estrdup("----");
+ }else if(strncmp(s, "video/", 6) == 0){
+ fmt->included |= Ivideo;
+ fmt->quality = estrdup(jsonstr(jsonbyname(f->val, "qualityLabel")));
+ if((x = jsonbyname(f->val, "fps")) != nil)
+ fmt->fps = x->n;
+ if(strstr(s, ", ") != nil) /* I know, not the best way */
+ fmt->included |= Iaudio;
+ }
+
+ if((x = jsonbyname(f->val, "itag")) != nil)
+ fmt->id = atoi(jsonstr(x));
+ if((x = jsonbyname(f->val, "clen")) != nil)
+ fmt->sz = atoll(jsonstr(x));
+ if((x = jsonbyname(f->val, "bitrate")) != nil)
+ fmt->bitrate = atoll(jsonstr(x));
+
+ return 0;
+}
+
+Info *
+youtube(char *vid)
+{
+ JSON *j, *z, *x;
+ JSONEl *e, *f;
+ char *s, *u, **fmtname;
+ Info *i;
+ int fd;
+
+ j = nil;
+ if((fd = hget(instlst, -1)) >= 0){
+ if((s = readall(fd)) != nil){
+ j = jsonparse(s);
+ free(s);
+ }
+ close(fd);
+ }
+ procwait();
+ if(j == nil){
+ werrstr("instances: %r");
+ return nil;
+ }
+
+ for(e = j->first, i = nil; e != nil && i == nil; e = e->next){
+ if(e->val->t != JSONArray || e->val->first == nil || e->val->first->next == nil)
+ continue;
+ f = e->val->first;
+ if(f->val->t != JSONString) /* first is the url */
+ continue;
+ if(f->next->val->t != JSONObject) /* second is the attributes */
+ continue;
+ if((s = jsonstr(jsonbyname(f->next->val, "type"))) == nil)
+ continue;
+ if(strncmp(s, "http", 4) != 0) /* don't even try onion */
+ continue;
+
+ u = smprint("%s://%s/api/v1/videos/%s", s, jsonstr(e->val->first->val), vid);
+ s = nil;
+ z = nil;
+ werrstr("");
+ if((fd = hget(u, -1)) < 0 || (s = readall(fd)) == nil || (z = jsonparse(s)) == nil || z->t != JSONObject){
+ free(s);
+ werrstr("%s: %r", u);
+ }else{
+ free(s);
+
+ if((i = calloc(1, sizeof(*i))) == nil)
+ sysfatal("memory");
+
+ i->author = estrdup(jsonstr(jsonbyname(z, "author")));
+ i->title = estrdup(jsonstr(jsonbyname(z, "title")));
+ i->description = estrdup(jsonstr(jsonbyname(z, "description")));
+ i->length = jsonbyname(z, "lengthSeconds")->n;
+ i->published = jsonbyname(z, "published")->n;
+
+ for(fmtname = fmtnames; *fmtname; fmtname++){
+ if((x = jsonbyname(z, *fmtname)) == nil){
+ if(debug) fprint(2, "%s: no streams\n", u);
+ continue;
+ }
+
+ for(f = x->first; f != nil; f = f->next)
+ addfmt(i, f);
+ }
+
+ if(i->nfmt < 1){
+ free(i->title);
+ free(i->description);
+ free(i);
+ i = nil;
+ }
+ }
+ close(fd);
+ jsonfree(z);
+ free(u);
+
+ if(fd >= 0)
+ procwait();
+ }
+
+ jsonfree(j);
+
+ return i;
+}