commit 4f14f73eb8c0ee5ed1062baddcd69f6ae9d90c3b
Author: Kris Maglione <jg@suckless.org>
Date: Thu, 9 Aug 2007 13:10:55 -0400
Initial commit.
Diffstat:
http.c | | | 237 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
last.c | | | 334 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
last.h | | | 33 | +++++++++++++++++++++++++++++++++ |
mkfile | | | 20 | ++++++++++++++++++++ |
player.c | | | 149 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
posix.c | | | 39 | +++++++++++++++++++++++++++++++++++++++ |
util.c | | | 42 | ++++++++++++++++++++++++++++++++++++++++++ |
7 files changed, 854 insertions(+), 0 deletions(-)
diff --git a/http.c b/http.c
@@ -0,0 +1,237 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include "last.h"
+
+#define HTTPVER "HTTP/1.0"
+
+char*
+query(const char *path, const char *fmt) {
+ char *p, *q;
+ int i;
+
+ i = 0;
+ if(path)
+ i = strlen(path)+1;
+
+ p = malloc(i+strlen(fmt)+1);
+ for(q=p+i; *fmt; q++, fmt++)
+ if(*fmt == ' ')
+ *q = '&';
+ else
+ *q = *fmt;
+ *q = '\0';
+
+ if(i) {
+ memcpy(p, path, i-1);
+ p[i-1] = '?';
+ }
+ setmalloctag(p, getcallerpc(&path));
+ return p;
+}
+
+int
+parseuri(char *uri, char **host, char **path) {
+ char *p, *q;
+ char c;
+
+ if(strncmp(uri, "http://", 7))
+ return 1;
+ q = uri+7;
+ for(p=q; c = *p; p++)
+ if(c == '/')
+ break;
+ else if(c == ':')
+ *p = '!';
+ if(c != '/')
+ return 1;
+
+ memmove(uri, "tcp!", 4);
+ memmove(uri+4, q, p-q);
+ uri[p-q+4] = '\0';
+
+ *host = uri;
+ *path = p;
+ return 0;
+}
+
+int
+hconv(Fmt *f) {
+ char *s, *p;
+ int ret, c;
+
+ ret = 0;
+ s = va_arg(f->args, char*);
+ for(; *s; s=p) {
+ for(p=s; (c = *p); p++) {
+ if(c & 0x80 || !isprint(c) || isspace(c))
+ break;
+ switch(c) {
+ default:
+ continue;
+ case '%':
+ case '<':
+ case '>':
+ case '{':
+ case '}':
+ case '|':
+ case '\\':
+ case ';':
+ case '&':
+ case '=':
+ case '@':
+ case '/':
+ case '?':
+ case '+':
+ break;
+ }
+ }
+ if(p != s)
+ ret += fmtprint(f, "%.*s", p-s, s);
+ if(c) {
+ if(c == ' ')
+ ret += fmtstrcpy(f, "+");
+ else
+ ret += fmtprint(f, "%%%02X", c);
+ p++;
+ }
+ }
+ return ret;
+}
+
+int
+Hconv(Fmt *f) {
+ char *s, *p;
+ int ret, c;
+
+ ret = 0;
+ s = va_arg(f->args, char*);
+ for(; *s; s=p) {
+ for(p=s; (c = *p); p++) {
+ if(!isprint(c) || isspace(c) || c & 0x80)
+ break;
+ switch(c) {
+ default:
+ continue;
+ case '%':
+ case '<':
+ case '>':
+ case '{':
+ case '}':
+ case '|':
+ case '\\':
+ break;
+ }
+ }
+ if(p != s)
+ ret += fmtprint(f, "%.*s", p-s, s);
+ if(c) {
+ ret += fmtprint(f, "%%%02X", c);
+ p++;
+ }
+ }
+ return ret;
+}
+
+void
+httpinit(void) {
+ fmtinstall('h', hconv);
+ fmtinstall('H', Hconv);
+}
+
+#define error(...) do{werrstr(__VA_ARGS__); goto error;}while(0)
+
+static char
+ Ebadresp[] = "Bad HTTP response code",
+ Ebadhdr[] = "Bad HTTP response header";
+
+Biobuf*
+httpget(char *host, char *path) {
+ Biobuf *bi, bo;
+ char *s, *p;
+ ulong code;
+ int fd, n, nredir, c;
+
+ s = nil;
+ nredir = 0;
+again:
+ if(nredir++ > 50) {
+ werrstr("redirect loop");
+ return nil;
+ }
+
+ fd = dial(netmkaddr(host, "tcp", "http"), 0, 0, 0);
+ if(fd < 0)
+ return nil;
+
+ if(debug['h'])
+ print("GET %s %q\n", host, path);
+ Binit(&bo, fd, OWRITE);
+ Bprint(&bo, "GET %s %s\r\n", path, HTTPVER);
+ Bprint(&bo, "Host: %s\r\n", host);
+ Bprint(&bo, "User-Agent: http.c Plan 9\r\n");
+ Bprint(&bo, "Accept: */*\r\n");
+ Bprint(&bo, "\r\n");
+ Bterm(&bo);
+
+ if(s)
+ free(s);
+
+ bi = Bfdopen(fd, OREAD);
+
+ s = Brdline(bi, '\n');
+ if(s == nil)
+ error("%s: no data", Ebadresp);
+
+ n = BLINELEN(bi);
+ if(n < 2)
+ error(Ebadresp);
+ if(s[n-2] != '\r')
+ error(Ebadresp);
+ s[n-2] = '\0';
+
+ if(debug['h'])
+ print("Resp: %s\n", s);
+
+ if(strncmp(s, HTTPVER " ", sizeof(HTTPVER)))
+ error("%s: bad version response", Ebadresp);
+ s += sizeof(HTTPVER);
+
+ code = strtoul(s, &p, 10);
+ if(p == s || !isspace(p[0]))
+ error("%s: non-numeric response code", Ebadresp);
+
+ if(code != 200 && code != 301 && code != 302)
+ error("http: %s", s);
+
+ for(;;) {
+ s = Brdline(bi, '\n');
+ n = BLINELEN(bi);
+ if(n < 2 || s[n-2] != '\r')
+ error(Ebadhdr);
+ if(n == 2)
+ break;
+ s[n-2] = '\0';
+ if(debug['h'])
+ print("Headr: %s\n", s);
+ p = strchr(s, ':');
+ if(p == nil)
+ error(Ebadhdr);
+ *p++ = '\0';
+ if((code == 301 || code == 302) && !cistrcmp(s, "Location")) {
+ while((c = *p) && isspace(c))
+ p++;
+ s = strdup(p);
+ parseuri(s, &host, &path);
+ Bterm(bi);
+ goto again;
+ }
+ }
+
+ return bi;
+
+error:
+ Bterm(bi);
+ return nil;
+}
+
diff --git a/last.c b/last.c
@@ -0,0 +1,334 @@
+#define EXTERN
+#include <u.h>
+#include <libc.h>
+#include <auth.h>
+#include <bio.h>
+#include <mp.h>
+#include <libsec.h>
+#include <thread.h>
+#include "last.h"
+
+static long endtime;
+
+enum {
+ Rmsg,
+ Rresponse,
+ Rbase_path,
+ Rbase_url,
+ Rstream_url,
+ Ralbum,
+ Rartist,
+ Rartist_url,
+ Rsession,
+ Rstation,
+ Rstreaming,
+ Rtrack,
+ Rtrackduration,
+};
+char *respnam[] = {
+ "msg",
+ "response",
+ "base_path",
+ "base_url",
+ "stream_url",
+ "album",
+ "artist",
+ "artist_url",
+ "session",
+ "station",
+ "streaming",
+ "track",
+ "trackduration",
+};
+char *resp[nelem(respnam)];
+
+typedef struct Assoc Assoc;
+struct Assoc {
+ int field;
+ char *str;
+} junk[] = {
+ Rartist, "John Cage",
+ Rartist, "Morton Feldman",
+ Rartist, "Conlon Nancarrow",
+ Rartist, "Iannis Xenakis",
+ Rartist, "Olivier Messiaen",
+ Ralbum, "Music Box",
+ 0, 0,
+};
+
+char*
+erespval(int i) {
+
+ if(resp[i] == nil)
+ sysfatal("expected response value %q is missing",
+ respnam[i]);
+ return strdup(resp[i]);
+}
+
+void
+exitsall(char *err) {
+ if(err)
+ print("threadexitsall(%s)\n", err);
+ threadexitsall(err);
+}
+
+void
+msg(char *fmt, ...) {
+ va_list ap;
+
+ va_start(ap, fmt);
+ vfprint(1, fmt, ap);
+ va_end(ap);
+ print("> ");
+}
+
+int
+Rconv(Fmt *f) {
+ int i;
+
+ i = va_arg(f->args, int);
+ assert(i < nelem(resp));
+ return fmtprint(f, "%s", (resp[i] ? resp[i] : "unknown"));
+}
+
+int
+Vconv(Fmt *f) {
+ VFmt *v;
+
+ v = va_arg(f->args, VFmt*);
+ return fmtvprint(f, (char*)v->fmt, v->args);
+}
+
+int
+get(char *path, char *fmt, ...) {
+ va_list ap;
+ Biobuf *b;
+ char *s, *t, *uri;
+ char buf[1024];
+ int i, n;
+
+ s = query(path, fmt);
+ va_start(ap, fmt);
+ uri = vsmprint(s, ap);
+ va_end(ap);
+ free(s);
+
+ n = strlen(uri);
+ if(n && uri[n-1] == '&')
+ uri[n-1] = '\0';
+
+ for(i=0; i <= Rresponse; i++)
+ if(resp[i]) {
+ free(resp[i]);
+ resp[i] = nil;
+ }
+
+ b = httpget(host, uri);
+ free(uri);
+ if(b == nil) {
+ resp[Rresponse] = smprint("get fails: %r");
+ return 1;
+ }
+
+ if(debug['a'])
+ print("Attributes:\n");
+
+ while((s = Brdline(b, '\n'))) {
+ s[BLINELEN(b)-1] = '\0';
+ attr:
+ if(debug['a'])
+ print("\t%s\n", s);
+ t = strchr(s, '=');
+ if(t == nil)
+ continue;
+ *t++ = '\0';
+ for(i=0; i < nelem(respnam); i++)
+ if(!strcmp(respnam[i], s)) {
+ if(resp[i])
+ free(resp[i]);
+ resp[i] = strdup(t);
+ break;
+ }
+ }
+
+ n = Bread(b, buf, sizeof(buf)-1);
+ if(n > 0) {
+ buf[n] = '\0';
+ s = buf;
+ goto attr;
+ }
+
+ Bterm(b);
+ return 0;
+}
+
+int
+rpc(const char *cmd, const char *fmt, ...) {
+ VFmt v;
+ int i;
+
+ v.fmt = query(nil, fmt);
+ va_start(v.args, fmt);
+ i = get("%s/%s.php", "session=%h %V", basepath, cmd, session, &v);
+ va_end(v.args);
+ free(v.fmt);
+ return i;
+}
+
+static void
+printresp(void) {
+ char *s;
+
+ s = resp[Rresponse];
+ print("Response: %s\n", s);
+ if(s && !strcmp(s, "FAILED"))
+ print(" Reason: %R\n", Rmsg);
+}
+
+void
+getmeta(void) {
+ Assoc *a;
+ char *s;
+ long sec;
+ int n, i;
+
+ for(i=Rartist; i < nelem(resp); i++) {
+ free(resp[i]);
+ resp[i] = nil;
+ }
+
+ n = 0;
+ do {
+ if(n > 50)
+ exitsall("meta");
+ if(rpc("np", ""))
+ exitsall("rpc np");
+ }while(resp[Rstreaming] == nil || strcmp(resp[Rstreaming], "true"));
+
+ s = resp[Rtrackduration];
+ sec = 0;
+ if(s)
+ sec = strtol(s, nil, 10);
+ endtime = time(0) + sec;
+
+ print("\n");
+ print("Station: %R\n", Rstation);
+ print("Atrist: %R <%R>\n", Rartist, Rartist_url);
+ print("Album: %R\n", Ralbum);
+ print("Track: %R\n", Rtrack);
+ print("Duration: %d:%02d\n", sec/60, sec%60);
+ print("Ends: (approx) %s", ctime(endtime));
+ print("\n");
+ print("> ");
+
+ label("Playing \"%R\" by %R", Rtrack, Rartist);
+
+ for(a=junk; a->str; a++)
+ if(resp[a->field] && cistrstr(resp[a->field], a->str)) {
+ print("Junk %s; banning.\n", respnam[a->field]);
+ rpc("control", "command=ban");
+ printresp();
+ skipping = 1;
+ break;
+ }
+}
+
+static void
+userinfo(char **user, char **pass) {
+ static char buf[MD5dlen*2];
+ uchar digest[MD5dlen];
+ UserPasswd *up;
+ int i;
+
+ up = auth_getuserpasswd(auth_getkey, "proto=pass dom=last.fm role=client");
+ if(up == nil)
+ sysfatal("no username/password: %r");
+
+ md5(up->passwd, strlen(up->passwd), digest, nil);
+ for(i=0; i < MD5dlen; i++)
+ sprint(buf+2*i, "%02x", digest[i]);;
+
+ *user = strdup(up->user);
+ *pass = buf;
+ memset(up->passwd, 0, strlen(up->passwd));
+ free(up);
+}
+
+void
+threadmain(int argc, char *argv[]) {
+ Biobuf in;
+ char *uri, *s, *q, *user, *pass;
+ long n;
+
+ ARGBEGIN{
+ default:
+ n = ARGC();
+ if((ulong)n < 128)
+ debug[n]++;
+ break;
+ }ARGEND;
+
+ fmtinstall('V', Vconv);
+ fmtinstall('R', Rconv);
+ quotefmtinstall();
+
+ label("Wait...");
+
+ httpinit();
+
+ userinfo(&user, &pass);
+
+ host = "ws.audioscrobbler.com";
+ if(get("/radio/handshake.php", "version=1.0.1 platform=Plan+9 username=%h passwordmd5=%h",
+ user, pass))
+ sysfatal("handshake fails: %r");
+
+ host = erespval(Rbase_url);
+ basepath = erespval(Rbase_path);
+ session = erespval(Rsession);
+
+ uri = erespval(Rstream_url);
+ if(parseuri(uri, &streamhost, &streampath))
+ sysfatal("Bad stream URI");
+
+ initplayer();
+
+ label("Stopped.");
+ Binit(&in, 0, OREAD);
+ while(print("> "), s = Brdline(&in, '\n')) {
+ s[BLINELEN(&in)-1] = '\0';
+ q = tok(&s);
+ if(!strcmp(q, "start")) {
+ newstream();
+ }else
+ if(!strcmp(q, "stop")) {
+ endstream();
+ }else
+ if(!strcmp(q, "skip") || !strcmp(q, "ban")) {
+ skipping = 1;
+ label("Skipping...");
+ rpc("control", "command=%h", s);
+ printresp();
+ }else
+ if(!strcmp(q, "love")) {
+ rpc("control", "command=love");
+ printresp();
+ }else
+ if(!strcmp(q, "station")) {
+ rpc("adjust", "url=%h", s);
+ printresp();
+ }else
+ if(!strcmp(q, "time")) {
+ n = endtime - time(0);
+ print("Time left: (abbrox) %d:%02d\n", n/60, n%60);
+ print("Ends: (approx) %s", ctime(endtime));
+ }else
+ if(!strcmp(q, "help")) {
+ print("Commands: start, stop, skip, love, ban, info, time, station <station>\n");
+ }else if(q[0] != 0)
+ print("?\n");
+ }
+
+ exitsall(nil);
+}
+
diff --git a/last.h b/last.h
@@ -0,0 +1,33 @@
+#ifndef EXTERN
+# define EXTERN extern
+#endif
+
+EXTERN char debug[0x7f];
+EXTERN int skipping;
+EXTERN char *host, *basepath, *streamhost, *streampath, *session;
+
+typedef struct VFmt VFmt;
+struct VFmt {
+ char *fmt;
+ va_list args;
+};
+
+void msg(char*, ...);
+void exitsall(char*);
+void getmeta(void);
+
+void initplayer(void);
+void newstream(void);
+void endstream(void);
+
+char* query(const char*, const char*);
+int parseuri(char*, char**, char**);
+void httpinit(void);
+Biobuf* httpget(char*, char*);
+
+void noblock(int);
+void label(const char*, ...);
+
+char* tok(char**);
+void printfile(char*, const char*, ...);
+
diff --git a/mkfile b/mkfile
@@ -0,0 +1,20 @@
+MKSHELL=rc
+<$PLAN9/src/mkhdr
+
+TARG=last
+
+CFLAGS= -DPLAYER='"mpg123 -"'
+
+OFILES=\
+ http.$O\
+ last.$O\
+ player.$O\
+ posix.$O\
+ util.$O\
+
+<$PLAN9/src/mkone
+
+# Why? Because MKSHELL doesn't apply to mkone, so it botches the cflags.
+%.$O: %.c
+ $CC $CFLAGS $stem.c
+
diff --git a/player.c b/player.c
@@ -0,0 +1,149 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include "last.h"
+#undef pipe
+
+static char *player[] = { "rc", "-c", "exec " PLAYER, 0 };
+
+static QLock lk;
+static Biobuf *stream;
+static Biobuf playerin;
+static int playing;
+
+static void
+playerproc(void *v) {
+ Channel *cwait;
+ Waitmsg *m;
+ int fd[3], fdin;
+ int cpid, i;
+
+ fdin = ((int*)v)[0];
+ cwait = threadwaitchan();
+ for(;;) {
+ fd[0] = dup(fdin, -1);
+ fd[1] = open("/dev/null", OWRITE);
+ fd[2] = open("/dev/null", OWRITE);
+ cpid = threadspawn(fd, player[0], player);
+ do{
+ m = recvp(cwait);
+ i = 0;
+ if(m == nil)
+ continue;
+ i = m->pid;
+ free(m);
+ }while(i != cpid);
+ msg("Player died.\n");
+ }
+}
+
+void
+initplayer(void) {
+ static int fd[2];
+
+ notifyoff("alarm");
+ notedisable("sys: window size change");
+
+ if(pipe(fd) < 0)
+ exitsall("pipe");
+
+ proccreate(playerproc, fd, mainstacksize);
+ Binit(&playerin, fd[1], OWRITE);
+}
+
+static int
+tgetc(Biobuf *b) {
+ int c;
+
+ alarm(1000);
+ c = Bgetc(b);
+ alarm(0);
+ return c;
+}
+
+#define GETC(bp) \
+ ((bp)->icount?(bp)->bbuf[(bp)->bsize+(bp)->icount++]:tgetc((bp)))
+
+static void
+proxyproc(void *v) {
+ char errbuf[16];
+ int c, i;
+
+ qlock(&lk);
+ while(playing) {
+ label("Starting stream.");
+ if(debug['p'])
+ msg("New stream\n");
+ skipping = 0;
+ i = 0;
+ do {
+ stream = httpget(streamhost, streampath);
+ if(stream)
+ break;
+ if(i++ > 5) {
+ fprint(2, "can't get stream: %r\n");
+ playing = 0;
+ }
+ rerrstr(errbuf, sizeof errbuf);
+ }while(playing && !strcmp(errbuf, "interrupted"));
+
+ c = 0;
+ while(playing && (c = GETC(stream)) != Beof) {
+ if(c == 'S') {
+ i = 0;
+ if(i++, 'Y' == GETC(stream))
+ if(i++, 'N' == GETC(stream))
+ if(i++, 'C' == GETC(stream)) {
+ if(debug['v'])
+ msg("SYNC\n");
+ skipping = 0;
+ getmeta();
+ continue;
+ }
+ while(i--)
+ Bungetc(stream);
+ }
+ if(!skipping)
+ BPUTC(&playerin, c);
+ else {
+ playerin.ocount = -playerin.bsize;
+ if(skipping++ > 8192)
+ break;
+ }
+ }
+ if(debug['p'] && !playing)
+ msg("playing => 0\n");
+ if(c == Beof)
+ msg("Stream died\n");
+ Bterm(stream);
+ }
+ qunlock(&lk);
+}
+
+void
+newstream(void) {
+
+ if(playing)
+ endstream();
+
+ playing = 1;
+ proccreate(proxyproc, 0, mainstacksize);
+}
+
+void
+endstream(void) {
+
+ label("Stopped.");
+ if(!playing)
+ return;
+
+ playing = 0;
+ qlock(&lk);
+ qunlock(&lk);
+
+ if(stream)
+ Bterm(stream);
+ stream = nil;
+}
+
diff --git a/posix.c b/posix.c
@@ -0,0 +1,39 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <fcntl.h>
+#include "last.h"
+
+void
+noblock(int fd) {
+ if(fcntl(fd, F_SETFL, O_NONBLOCK) == -1)
+ sysfatal("Can't set O_NONBLOCK for %d: %r", fd);
+}
+
+void
+label(const char *fmt, ...) {
+ static char *status;
+ char *home;
+ VFmt v;
+
+ if(!isatty(1))
+ return;
+
+ v.fmt = (char*)fmt;
+ va_start(v.args, fmt);
+ print("\033];LastFM: %V\007", &v);
+ va_end(v.args);
+
+ return;
+
+ if(status == nil) {
+ home = getenv("HOME");
+ status = smprint("%s/lib/status", home);
+ free(home);
+ }
+ /* Why does this block? */
+ va_start(v.args, fmt);
+ printfile(status, "LastFM: %V", &v);
+ va_end(v.args);
+}
+
diff --git a/util.c b/util.c
@@ -0,0 +1,42 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include "last.h"
+
+static void
+eat(char **s, int (*p)(int), int r) {
+ char *q;
+
+ for(q=*s; p(*q) == r; q++)
+ ;
+ *s = q;
+}
+
+char*
+tok(char **s) {
+ char *p;
+
+ eat(s, isspace, 1);
+ p = *s;
+ eat(s, isspace, 0);
+ eat(s, isspace, 1);
+ return p;
+}
+
+void
+printfile(char *file, const char *fmt, ...) {
+ va_list ap;
+ int fd;
+
+ fd = open(file, OWRITE);
+ if(fd < 0)
+ return;
+ noblock(fd);
+
+ va_start(ap, fmt);
+ vfprint(fd, (char*)fmt, ap);
+ va_end(ap);
+
+ close(fd);
+}
+