last

git clone git://oldgit.suckless.org/last/
Log | Files | Refs

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