shithub: nvi

Download patch

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;
+}