aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorrgl <devnull@localhost>2020-01-05 11:52:01 +0100
committerrgl <devnull@localhost>2020-01-05 11:52:01 +0100
commit0c3c8d97316d4bb76a12d8c5fd77c2d95fcd0921 (patch)
treee524cd465fad9d77de5bcee559c025aec62274af
downloadfilmoteca-0c3c8d97316d4bb76a12d8c5fd77c2d95fcd0921.tar.gz
filmoteca-0c3c8d97316d4bb76a12d8c5fd77c2d95fcd0921.tar.bz2
filmoteca-0c3c8d97316d4bb76a12d8c5fd77c2d95fcd0921.zip
initial commit.
-rw-r--r--Makefile35
-rw-r--r--args.h22
-rw-r--r--doc/filmoteca.ms306
-rw-r--r--doc/filmoteca.pdfbin0 -> 11310 bytes
-rw-r--r--doc/mkfile15
-rw-r--r--filmoteca.c1125
-rwxr-xr-xfilmsrv2
7 files changed, 1505 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..59f4c54
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,35 @@
+CC=cc
+CFLAGS=-Wall -Wno-missing-braces -Wno-parentheses -Wno-switch -Wno-pointer-to-int-cast -fno-diagnostics-color -ggdb -c -O2
+LDFLAGS=-static
+O=o
+
+TARG=filmoteca
+OFILES=\
+ filmoteca.$O\
+
+LIBS=\
+ libutf/libutf.a\
+
+HFILES=\
+ libutf/utf.h\
+
+.PHONY: all clean
+all: $(TARG)
+
+%.$O: %.c
+ $(CC) $(CFLAGS) $<
+
+$(OFILES): $(HFILES)
+
+$(TARG): $(OFILES)
+ $(CC) -o $@ $(OFILES) $(LIBS) $(LDFLAGS)
+
+install: $(TARG)
+ cp $(TARG) $(HOME)/bin/
+ cp filmsrv $(HOME)/bin/
+
+uninstall:
+ rm -f $(HOME)/bin/$(TARG) $(HOME)/bin/filmsrv
+
+clean:
+ rm $(OFILES) $(TARG)
diff --git a/args.h b/args.h
new file mode 100644
index 0000000..f7e3d5f
--- /dev/null
+++ b/args.h
@@ -0,0 +1,22 @@
+extern char *argv0;
+#define SET(x) ((x)=0)
+#define USED(x) if(x);else
+#define ARGBEGIN for((argv0? 0: (argv0=*argv)),argv++,argc--;\
+ argv[0] && argv[0][0]=='-' && argv[0][1];\
+ argc--, argv++) {\
+ char *_args, *_argt;\
+ Rune _argc;\
+ _args = &argv[0][1];\
+ if(_args[0]=='-' && _args[1]==0){\
+ argc--; argv++; break;\
+ }\
+ _argc = 0;\
+ while(*_args && (_args += chartorune(&_argc, _args)))\
+ switch(_argc)
+#define ARGEND SET(_argt);USED(_argt); USED(_argc); USED(_args);}USED(argv); USED(argc);
+#define ARGF() (_argt=_args, _args="",\
+ (*_argt? _argt: argv[1]? (argc--, *++argv): 0))
+#define ARGC() _argc
+
+#define EARGF(x) (_argt=_args, _args="",\
+ (*_argt? _argt: argv[1]? (argc--, *++argv): (x, (char*)0)))
diff --git a/doc/filmoteca.ms b/doc/filmoteca.ms
new file mode 100644
index 0000000..06b0d27
--- /dev/null
+++ b/doc/filmoteca.ms
@@ -0,0 +1,306 @@
+.nr PS 12
+.nr VS 14
+.TL
+Filmoteca
+.AU
+Rodrigo G. López
+.sp
+rgl@antares-labs.eu
+.AI
+Antares Telecom Laboratories
+Albatera, Alicante
+.AB
+.DA
+This document is a work in progress and is subject to change by the
+author at any time. It shows the ideas that are being implemented in
+a 3TB multimedia dataset distributed among two hard drives, for the
+author's personal consumption.
+.AE
+.SH
+Introduction
+.LP
+The filmoteca is an attempt at organizing motion picture data like
+movies, tv shows and documentaries in a formalised file hierarchy that
+is easy to use and maintain. We take influence from the Plan 9
+operating system and its file servers to design an interface that
+makes it simple to script any operation using standard Unix tools.
+.NH 1
+The Tree
+.PP
+The file hierarchy is structured with the first-level directories
+named after the titles they keep. Any collision is resolved by
+appending a space and a roman numeral between parentheses to the
+title, similar to how IMDb does it [IMDb], except we favor historical
+correctness over time of insertion into the database. This means that
+if we add Rush (2012) first and later we find about Rush (1991), we
+will add it under the title
+.I "Rush (I)"
+and the latest one will become
+.I "Rush (II)" .
+The inner levels are thoroughly explained in Section 2.
+.NH 2
+Movies
+.P1
+.../filmoteca/
+ title/
+ title/release
+ title/synopsis
+ title/cover
+ title/video
+ title/history
+ title/sub/
+ title/sub/en
+ title/sub/es
+ title/sub/...
+ title/dub/
+ title/dub/es/
+ title/dub/es/video
+ title/dub/...
+ title/extra/
+ title/extra/Opening.mp4
+ title/extra/Ending.mp4
+ title/extra/Deleted Scenes.mp4
+ title/extra/...
+ title/remake/
+ title/remake/1984/
+ title/remake/1984/video
+ title/remake/1984/...
+ title/remake/y...
+.P2
+.NH 2
+Multipart Movies
+.P1
+ title/
+ title/release
+ title/synopsis
+ title/cover
+ title/video1
+ title/video2
+ title/videon...
+ title/history
+ title/sub1/
+ title/sub1/en
+ title/sub1/...
+ title/subn...
+ title/dub1/
+ title/dub1/es/
+ title/dub1/es/video
+ title/dub1/...
+ title/dubn...
+ title/extra/...
+ title/remake/y...
+.P2
+.NH 2
+Series
+.P1
+ title/
+ title/release
+ title/synopsis
+ title/cover
+ title/history
+ title/s/
+ title/s/1/
+ title/s/1/1/
+ title/s/1/1/video
+ title/s/1/1/sub/
+ title/s/1/1/sub/en
+ title/s/1/1/sub/...
+ title/s/1/1/dub/
+ title/s/1/1/dub/es/
+ title/s/1/1/dub/es/video
+ title/s/1/1/dub/...
+ title/s/1/n...
+ title/s/n...
+ title/extra/...
+ title/remake/y...
+.P2
+.NH 1
+Walking Down the Tree
+.NH 2
+The
+.CW release
+file
+.PP
+The
+.I release
+file stores the date on which the work was first released. In the
+case of a movie (including multipart) there is only one line, whereas
+in a series there is one line per season. If there is some season
+lacking between two other seasons, an empty string is used instead.
+The date string has two different formats:
+.CW yyyy
+and
+.CW ddmmmyyyy ,
+e.g.
+.CW 2016 ,
+.CW 1932 ,
+.CW 23mar1997 ,
+.CW 1jun1978 ,
+etc.
+.sp
+I am going to show you two examples, to clarify the structure of a hollowed
+file. First we have
+.I "Black Mirror" ,
+with two seasons, the first and the third:
+.P1
+$ cat 'Black Mirror'/release
+2011
+
+2016
+$ xd -c 'Black Mirror'/release
+0000000 2 0 1 1 \en \en 2 0 1 6 \en
+000000b
+.P2
+Then a extreme case,
+.I "The X Files" ,
+where there is only one season, the tenth:
+.P1
+$ cat 'The X Files'/release
+
+
+
+
+
+
+
+
+
+2016
+$ xd -c 'The X Files'/release
+0000000 \en \en \en \en \en \en \en \en \en 2 0 1 6 \en
+000000e
+.P2
+You can then use the following script to figure out the folders in
+.I s
+(explained in Section 2.8):
+.P1
+$ <'Black Mirror'/release awk '
+ BEGIN{ s = 0; }
+ /^$/ { s++; }
+ /^[0-9]+$/ { print ++s }
+ '
+1
+3
+.P2
+.NH 2
+The
+.CW synopsis
+file
+.PP
+Contains a brief summary of the plot.
+.NH 2
+The
+.CW cover
+file*
+.PP
+The
+.I cover
+file is the poster of the movie or tv show encoded in well-known image
+formats such as JPEG, PNG, TIFF or GIF.
+.FS
+* This file will be renamed to
+.I poster
+in the future.
+.FE
+.NH 2
+The
+.CW video[n]
+file
+.PP
+The
+.I video
+file is the fruit of the tree, it stores the movie or a series episode
+under some multimedia container format, the most common being MP4,
+Matroska (MKV) and AVI.
+.NH 2
+The
+.CW history
+file
+.PP
+In an attempt to educate the user, a
+.I history
+file is provided containing an explanation, facts and references for
+movies that claim to be based on/inspired by true events. You might
+be surprised.
+.NH 2
+The
+.CW sub[n]
+folder
+.PP
+Files within
+.I sub
+are SubRip (SRT) subtitle files, each named using a two-letter
+country code* for the language they provide.
+.FS
+* This will be changed to use a reasonable subset of the
+two/three-letter IANA language subtag registry [IANALang].
+.FE
+.NH 2
+The
+.CW dub[n]
+folder
+.PP
+The
+.I dub
+folder stores revoicings and provides one folder per language, using
+the same conventions as the subtitles. Inside each of these, there is
+a video file (see Section 2.4)†.
+.FS
+† Although under best conditions it should be just an audio file one
+could swap into the stream. This is an idea that is going to affect
+the video file as well, since one could compose their own media
+streams into a single experience, for example grabbing the original
+video, adding japanese audio and english subtitles; a very common
+setup for watching anime.
+.FE
+.NH 2
+The
+.CW s
+folder‡
+.PP
+.I S
+is a three-level directory containing the episodes of a series. The
+first level are seasons by number, if there is some season that is
+lacking, we completely avoid it (signaled by the
+.I release
+file, Section 2.1), there are no empty folders. The second level are
+episodes following the same naming rules. The structure of an episode
+folder is similar to that of a movie, except it only has a
+.I video ,
+and the
+.I sub
+and
+.I dub
+folders.
+.FS
+‡ It really is a leftover from a previous design, since it means
+``season'', but there is no
+.I e
+directory containing ``episodes''.
+.FE
+.NH 2
+The
+.CW extra
+folder
+.PP
+The
+.I extra
+folder holds a bunch of random files that make up the featurettes and
+other behind-the-scenes content, such as the making-of, auditions, the
+director's commentary, additional posters, OSTs, etc.
+.NH 2
+The
+.CW remake
+folder
+.PP
+.I Remake
+is a two-level directory, with the first level holding directories
+named after the year the remake was released on, and inside each of these, the
+exact same file structure you would find in a movie (see Section 1.1).
+.bp
+.SH
+References
+.LP
+[IMDb] https://help.imdb.com/article/imdb/discover-watch/what-do-the-roman-numerals-like-i-and-ii-after-people-s-names-mean/GA827M8GK5KVH8TC?ref_=helpart_nav_33#, last checked March 19, 2019
+.br
+[IANALang] https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
diff --git a/doc/filmoteca.pdf b/doc/filmoteca.pdf
new file mode 100644
index 0000000..37dc4bd
--- /dev/null
+++ b/doc/filmoteca.pdf
Binary files differ
diff --git a/doc/mkfile b/doc/mkfile
new file mode 100644
index 0000000..3a61b6c
--- /dev/null
+++ b/doc/mkfile
@@ -0,0 +1,15 @@
+FONTS='.FP times'
+DOCNAME=filmoteca
+
+all:VQ: $DOCNAME.ps $DOCNAME.pdf
+
+clean:VQ:
+ rm -f $DOCNAME.ps $DOCNAME.pdf
+
+$DOCNAME.ps:V: $DOCNAME.ms
+ {echo $FONTS; cat $prereq}> _$prereq
+ eval `{doctype _$prereq} | lp -dstdout > $target && rm -f _$prereq
+
+$DOCNAME.pdf:V: $DOCNAME.ps
+ cat /sys/doc/docfonts $prereq > _$prereq
+ ps2pdf _$prereq $target && rm -f _$prereq
diff --git a/filmoteca.c b/filmoteca.c
new file mode 100644
index 0000000..8013cf3
--- /dev/null
+++ b/filmoteca.c
@@ -0,0 +1,1125 @@
+/*
+ * filmoteca - web interface
+ */
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <dirent.h>
+#include <ctype.h>
+#include <errno.h>
+#include "libutf/utf.h"
+#include "args.h"
+
+#define nil NULL
+typedef unsigned int uint;
+typedef unsigned long ulong;
+typedef long long vlong;
+typedef unsigned long long uvlong;
+
+enum {
+ Sok = 200,
+ Spartial = 206,
+ Sbadreq = 400,
+ Sforbid = 403,
+ Snotfound = 404,
+ Snotrange = 416,
+ Sinternal = 500,
+ Snotimple = 501,
+ Swrongver = 505,
+};
+char *statusmsg[] = {
+ [Sok] "OK",
+ [Spartial] "Partial Content",
+ [Sbadreq] "Bad Request",
+ [Sforbid] "Forbidden",
+ [Snotfound] "Not Found",
+ [Snotrange] "Range Not Satisfiable",
+ [Sinternal] "Internal Server Error",
+ [Snotimple] "Not Implemented",
+ [Swrongver] "HTTP Version Not Supported",
+};
+
+typedef struct Movie Movie;
+typedef struct Multipart Multipart;
+typedef struct Part Part;
+typedef struct Series Series;
+typedef struct Season Season;
+typedef struct Episode Episode;
+typedef struct Resource Resource;
+
+struct Movie {
+ char *release; /* release date */
+ int hassynopsis; /* is there a synopsis, */
+ int hascover; /* cover, */
+ int hasvideo; /* video, */
+ int hashistory; /* or history file? */
+ char **subs; /* list of subtitle languages */
+ int nsub;
+ char **dubs; /* list of revoicing languages */
+ int ndub;
+ char **extras; /* list of extra content */
+ int nextra;
+ char **remakes; /* list of remake years */
+ int nremake;
+};
+
+struct Multipart {
+ char *release; /* release date */
+ int hassynopsis; /* is there a synopsis, */
+ int hascover; /* cover, */
+ int hashistory; /* or history file? */
+ Part *part0; /* list of parts */
+ char **extras; /* list of extra content */
+ int nextra;
+ char **remakes; /* list of remake years */
+ int nremake;
+};
+
+struct Part {
+ int no; /* part number */
+ char **subs; /* list of subtitle languages */
+ int nsub;
+ char **dubs; /* list of revoicing languages */
+ int ndub;
+ Part *next;
+};
+
+struct Series {
+ int hassynopsis; /* is there a synopsis, */
+ int hascover; /* cover, */
+ int hashistory; /* or history file? */
+ Season *s; /* list of seasons */
+ char **extras; /* list of extra content */
+ int nextra;
+ char **remakes; /* list of remake years */
+ int nremake;
+};
+
+struct Season {
+ char *release; /* release date */
+ int no; /* season number */
+ Episode *pilot; /* list of episodes */
+ Season *next;
+};
+
+struct Episode {
+ int no; /* episode number */
+ int hasvideo; /* is there a video file? */
+ char **subs; /* list of subtitle languages */
+ int nsub;
+ char **dubs; /* list of revoicing languages */
+ int ndub;
+ Episode *next;
+};
+
+enum {
+ Rmovie,
+ Rmulti,
+ Rserie,
+ Runknown
+};
+struct Resource {
+ int type;
+ union {
+ Movie movie;
+ Multipart multi;
+ Series serie;
+ };
+};
+
+typedef struct Req Req;
+typedef struct Res Res;
+typedef struct HField HField;
+
+struct Req {
+ char *method, *target, *version;
+ HField *fields;
+};
+
+struct Res {
+ int status;
+ HField *fields;
+};
+
+struct HField {
+ char *key, *value;
+ HField *next;
+};
+
+char httpver[] = "HTTP/1.1";
+char srvname[] = "filmoteca";
+char errmsg[] = "NO MOVIES HERE";
+char listhead[] = "<!doctype html>\n<html>\n<head>\n"
+ "<link rel=\"stylesheet\" href=\"/style\" media=\"all\" type=\"text/css\"/>\n"
+ "<title>Filmoteca</title>\n"
+ "</head>\n<body>\n"
+ "<h1>Filmoteca</h1>\n";
+char listfeet[] = "</body>\n</html>\n";
+char portalhead[] = "<!doctype html>\n<html>\n<head>\n"
+ "<link rel=\"stylesheet\" href=\"/style\" media=\"all\" type=\"text/css\"/>\n"
+ "<title>Filmoteca - %s</title>\n"
+ "</head>\n<body>\n<center>\n"
+ "<h1>%s</h1>\n";
+char portalcover[] = "<a href=\"%s/cover\"><img id=\"cover\" src=\"%s/cover\"/></a>\n";
+char portalrelease[] = "<table>\n"
+ "\t<tr>\n"
+ "\t\t<td>Release</td><td>";
+char portalmoviestream[] = "</td>\n"
+ "\t</tr>\n"
+ "\t<tr>\n"
+ "\t\t<td>Stream</td><td><a href=\"%s/video\">link</a>";
+char portalmultistream[] = "</td>\n"
+ "\t</tr>\n"
+ "\t<tr>\n"
+ "\t\t<td>Stream</td><td>";
+char portalseriestream[] = "</td>\n"
+ "\t</tr>\n"
+ "\t<tr>\n"
+ "\t\t<td>Stream</td><td>";
+char portalsynopsis[] = "</td>\n"
+ "\t</tr>\n"
+ "\t<tr>\n"
+ "\t\t<td>Synopsis</td><td>";
+char portalhistory[] = "</td>\n"
+ "\t</tr>\n"
+ "\t<tr>\n"
+ "\t\t<td>History</td><td>";
+char portalsub[] = "</td>\n"
+ "\t</tr>\n"
+ "\t<tr>\n"
+ "\t\t<td>Subs</td><td>";
+char portaldub[] = "</td>\n"
+ "\t</tr>\n"
+ "\t<tr>\n"
+ "\t\t<td>Dubs</td><td>";
+char portalextra[] = "</td>\n"
+ "\t</tr>\n"
+ "\t<tr>\n"
+ "\t\t<td>Extras</td><td>";
+char portalfeet[] = "</td>\n\t</tr>\n</table>\n</center></body>\n</html>\n";
+char stylepath[] = "/home/cinema/lib/film/style.css";
+char fvicopath[] = "/home/cinema/lib/film/favicon.ico";
+char *wdir = "/home/cinema/films";
+Req *req;
+Res *res;
+
+void hfatal(char *);
+
+/* a crappy mimic of the original */
+void
+sysfatal(char *s)
+{
+ perror(s);
+ exit(1);
+}
+
+void *
+emalloc(ulong n)
+{
+ void *p;
+
+ p = malloc(n);
+ if(p == nil)
+ hfatal("malloc");
+ memset(p, 0, n);
+ return p;
+}
+
+void *
+erealloc(void *ptr, ulong n)
+{
+ void *p;
+
+ p = realloc(ptr, n);
+ if(p == nil)
+ hfatal("realloc");
+ return p;
+}
+
+long
+truestrlen(char *s)
+{
+ char *e;
+ int waste;
+
+ waste = 0;
+ for(e = s; *e != 0; e++)
+ if(*e == '%'){
+ waste++;
+ if(*(e+1) == '%'){
+ e += 2;
+ continue;
+ }
+ /* rudimentary but works for me. */
+ while(isalnum(*++e) || *e == '.')
+ waste++;
+ }
+ return e-s-waste;
+}
+
+int
+numcmp(const void *a, const void *b)
+{
+ int na, nb;
+ char **sa = (char **)a;
+ char **sb = (char **)b;
+
+ na = strtol(*sa, nil, 0);
+ nb = strtol(*sb, nil, 0);
+ return na - nb;
+}
+
+int
+stringcmp(const void *a, const void *b)
+{
+ char **sa = (char **)a;
+ char **sb = (char **)b;
+
+ return strcmp(*sa, *sb);
+}
+
+int
+urldecode(char *url, char *out, long n)
+{
+ char *o, *ep;
+ int c;
+
+ ep = url+n;
+ for(o = out; url <= ep; o++){
+ c = *url++;
+ if(c == '%' &&
+ (!isxdigit(*url++) ||
+ !isxdigit(*url++) ||
+ !sscanf(url-2, "%2x", &c)))
+ return -1;
+ *o = c;
+ }
+ return o - out;
+}
+
+int
+mimetype(int fd, char *mime, long len)
+{
+ char m[256];
+ uvlong n;
+ int pf[2];
+ char *argv[] = {
+ "sh", "-c",
+ "file -i - | sed 's/^.*:\\s*//' | tr -d '\\n'",
+ nil,
+ };
+
+ memset(m, 0, sizeof m);
+ if(pipe(pf) < 0)
+ return -1;
+ switch(fork()){
+ case -1: return -1;
+ case 0:
+ close(pf[0]);
+ dup2(fd, 0);
+ dup2(pf[1], 1);
+ close(pf[1]);
+ execv("/bin/sh", argv);
+ sysfatal("execl");
+ default:
+ close(pf[1]);
+ if((n = read(pf[0], m, sizeof(m)-1)) < 0)
+ return -1;
+ close(pf[0]);
+ /* file(1) is not that good at guessing. */
+ if(strcmp(req->target, "/style") == 0)
+ strncpy(m, "text/css; charset=utf-8", sizeof(m)-1);
+ if(strncmp(m, "audio", 5) == 0)
+ strncpy(m, "video", 5);
+ strncpy(mime, m, len);
+ wait(nil);
+ }
+ return 0;
+}
+
+int
+filldirlist(char *path, char ***l, int *len)
+{
+ DIR *d;
+ struct dirent *dir;
+
+ d = opendir(path);
+ if(d == nil)
+ return -1;
+ while((dir = readdir(d)) != nil)
+ if(strcmp(dir->d_name, ".") != 0 &&
+ strcmp(dir->d_name, "..") != 0){
+ *l = erealloc(*l, ++*len*sizeof(char *));
+ (*l)[*len-1] = strdup(dir->d_name);
+ }
+ closedir(d);
+ return 0;
+}
+
+void
+insertepisode(Season *s, Episode *e, int no)
+{
+ Episode *ep, *olde;
+
+ olde = nil;
+ if(s->pilot == nil){
+ s->pilot = e;
+ return;
+ }
+ for(ep = s->pilot; ep != nil && ep->no < e->no; olde = ep, ep = ep->next)
+ ;
+ if(olde == nil)
+ s->pilot = e;
+ else
+ olde->next = e;
+ e->next = ep;
+}
+
+HField *
+allochdr(char *k, char *v)
+{
+ HField *h;
+
+ h = emalloc(sizeof(HField));
+ h->key = strdup(k);
+ h->value = strdup(v);
+ return h;
+}
+
+void
+freehdr(HField *h)
+{
+ HField *hn;
+
+ while(h != nil){
+ hn = h->next;
+ free(h->value);
+ free(h->key);
+ free(h);
+ h = hn;
+ }
+}
+
+void
+inserthdr(HField **h, char *k, char *v)
+{
+ while(*h != nil)
+ h = &(*h)->next;
+ *h = allochdr(k, v);
+}
+
+char *
+lookuphdr(HField *h, char *k)
+{
+ while(h != nil){
+ if(strcmp(h->key, k) == 0)
+ return h->value;
+ h = h->next;
+ }
+ return nil;
+}
+
+Req *
+allocreq(char *meth, char *targ, char *vers)
+{
+ Req *r;
+
+ r = emalloc(sizeof(Req));
+ r->method = strdup(meth);
+ r->target = strdup(targ);
+ r->version = strdup(vers);
+ return r;
+}
+
+void
+freereq(Req *r)
+{
+ freehdr(r->fields);
+ free(r->version);
+ free(r->target);
+ free(r->method);
+ free(r);
+}
+
+Res *
+allocres(int sc)
+{
+ Res *r;
+
+ r = emalloc(sizeof(Res));
+ r->status = sc;
+ inserthdr(&r->fields, "Server", srvname);
+ return r;
+}
+
+void
+freeres(Res *r)
+{
+ freehdr(r->fields);
+ free(r);
+}
+
+int
+hprint(char *fmt, ...)
+{
+ va_list ap;
+ int rc;
+
+ va_start(ap, fmt);
+ rc = vprintf(fmt, ap);
+ rc += printf("\r\n");
+ va_end(ap);
+ return rc;
+}
+
+void
+hstline(int sc)
+{
+ hprint("%s %d %s", httpver, sc, statusmsg[sc]);
+}
+
+void
+hprinthdr(void)
+{
+ HField *hp;
+
+ hstline(res->status);
+ for(hp = res->fields; hp != nil; hp = hp->next)
+ hprint("%s: %s", hp->key, hp->value);
+ hprint("");
+ fflush(stdout);
+}
+
+void
+hfail(int sc)
+{
+ char clen[16];
+
+ res = allocres(sc);
+ snprintf(clen, sizeof clen, "%u", strlen(errmsg));
+ inserthdr(&res->fields, "Content-Type", "text/plain; charset=utf-8");
+ inserthdr(&res->fields, "Content-Length", clen);
+ hprinthdr();
+ hprint("%s", errmsg);
+ hprint("");
+ exit(0);
+}
+
+void
+hfatal(char *ctx)
+{
+ hstline(Sinternal);
+ hprint("Content-Type: %s", "text/plain; charset=utf-8");
+ hprint("Content-Length: %u", strlen(errmsg));
+ hprint("");
+ hprint("%s", errmsg);
+ hprint("");
+ fflush(stdout);
+ sysfatal(ctx);
+}
+
+void
+hparsereq(void)
+{
+ char *line, *meth, *targ, *vers, *k, *v;
+ uint linelen;
+ int n;
+
+ n = getline(&line, &linelen, stdin);
+ meth = strtok(line, " ");
+ targ = strtok(nil, " ");
+ vers = strtok(nil, " \r");
+ if(meth == nil || targ == nil || vers == nil)
+ hfail(Sbadreq);
+ if(targ[strlen(targ)-1] == '/')
+ targ[strlen(targ)-1] = 0;
+ req = allocreq(meth, targ, vers);
+ while((n = getline(&line, &linelen, stdin)) > 0){
+ if(strcmp(line, "\r\n") == 0)
+ break;
+ k = strtok(line, ": ");
+ v = strtok(nil, " \r");
+ if(k == nil || v == nil)
+ hfail(Sbadreq);
+ inserthdr(&req->fields, k, v);
+ }
+}
+
+void
+sendfile(FILE *f, struct stat *fst)
+{
+ char buf[128*1024], mime[256], *s, crstr[6+3*16+1+1+1], clstr[16];
+ uvlong brange[2], n, clen;
+
+ n = clen = 0;
+ if(mimetype(fileno(f), mime, sizeof mime) < 0)
+ hfatal("sendfile: mimetype");
+ clen = fst->st_size;
+ if((s = lookuphdr(req->fields, "Range")) != nil){
+ while(!isdigit(*++s) && *s != 0)
+ ;
+ if(*s == 0)
+ hfail(Sbadreq);
+ brange[0] = strtoull(s, &s, 0);
+ if(*s++ != '-')
+ hfail(Sbadreq);
+ if(!isdigit(*s))
+ brange[1] = fst->st_size-1;
+ else
+ brange[1] = strtoull(s, &s, 0);
+ if(brange[0] > brange[1] || brange[1] >= fst->st_size){
+ res = allocres(Snotrange);
+ snprintf(crstr, sizeof crstr, "bytes */%llu",
+ fst->st_size);
+ }else{
+ res = allocres(Spartial);
+ fseeko(f, brange[0], SEEK_SET);
+ clen = brange[1]-brange[0]+1;
+ snprintf(crstr, sizeof crstr, "bytes %llu-%llu/%llu",
+ brange[0], brange[1], fst->st_size);
+ }
+ inserthdr(&res->fields, "Content-Range", crstr);
+ }else
+ res = allocres(Sok);
+ inserthdr(&res->fields, "Accept-Ranges", "bytes");
+ inserthdr(&res->fields, "Content-Type", mime);
+ snprintf(clstr, sizeof clstr, "%llu", clen);
+ inserthdr(&res->fields, "Content-Length", clstr);
+ if((s = lookuphdr(req->fields, "Connection")) != nil)
+ inserthdr(&res->fields, "Connection", s);
+ hprinthdr();
+ if(strcmp(req->method, "HEAD") == 0)
+ return;
+ while(clen -= n, !feof(f) && clen > 0){
+ n = fread(buf, 1, sizeof buf, f);
+ if(ferror(f))
+ break;
+ if(fwrite(buf, 1, n, stdout) <= 0)
+ break;
+ }
+}
+
+void
+sendlist(char *path)
+{
+ FILE *f;
+ struct stat fst;
+ char **dirlist;
+ int i, ndir;
+
+ ndir = 0;
+ dirlist = nil;
+ f = tmpfile();
+ if(f == nil)
+ hfatal("sendlist: tmpfile");
+ filldirlist(path, &dirlist, &ndir);
+ qsort(dirlist, ndir, sizeof(char *), stringcmp);
+ fprintf(f, listhead);
+ fprintf(f, "<ul>\n");
+ for(i = 0; i < ndir; i++)
+ fprintf(f, "<li><a href=\"%s/%s\">%s</a></li>\n",
+ strcmp(req->target, "/") == 0 ? "" : req->target,
+ dirlist[i], dirlist[i]);
+ fprintf(f, "</ul>\n");
+ fprintf(f, listfeet);
+ fseeko(f, 0, SEEK_SET);
+ if(fstat(fileno(f), &fst) < 0)
+ switch(errno){
+ case EACCES: hfail(Sforbid);
+ case ENOENT: hfail(Snotfound);
+ default: hfatal("sendlist: fstat");
+ }
+ sendfile(f, &fst);
+ fclose(f);
+}
+
+void
+sendportal(char *path)
+{
+ Resource r;
+ Part *p;
+ Season *s;
+ Episode *e;
+ FILE *f, *auxf;
+ struct stat fst;
+ DIR *root, *d, *ed;
+ struct dirent *rdir, *dir, *edir;
+ char *title, *line, auxpath[512], buf[1024];
+ uint linelen;
+ int n, sno, canintrosubs, canintrodubs, canintroseason;
+
+ p = nil;
+ s = nil;
+ e = nil;
+ line = nil;
+ sno = 0;
+ memset(&r, 0, sizeof(Resource));
+ r.type = Runknown;
+ memset(auxpath, 0, sizeof auxpath);
+ title = strrchr(path, '/');
+ if(*++title == 0)
+ hfail(Sbadreq);
+ root = opendir(path);
+ if(root == nil)
+ hfatal("sendportal: opendir");
+ fprintf(stdout, "%s\n", getcwd(nil, 0));
+ while((rdir = readdir(root)) != nil){
+ switch(r.type){
+ case Runknown: break;
+ case Rmovie:
+ if(strcmp(rdir->d_name, "release") == 0){
+ snprintf(auxpath, sizeof auxpath, "%s/%s", path, rdir->d_name);
+ f = fopen(auxpath, "r");
+ if(f == nil)
+ goto Rogue;
+ n = getline(&line, &linelen, f);
+ if(line[n-1] == '\n')
+ line[(n--)-1] = 0;
+ r.movie.release = strdup(line);
+ fclose(f);
+ }else if(strcmp(rdir->d_name, "synopsis") == 0)
+ r.movie.hassynopsis++;
+ else if(strcmp(rdir->d_name, "cover") == 0)
+ r.movie.hascover++;
+ else if(strcmp(rdir->d_name, "video") == 0)
+ r.movie.hasvideo++;
+ else if(strcmp(rdir->d_name, "history") == 0)
+ r.movie.hashistory++;
+ else if(strcmp(rdir->d_name, "sub") == 0){
+ snprintf(auxpath, sizeof auxpath, "%s/%s", path, rdir->d_name);
+ filldirlist(auxpath, &r.movie.subs, &r.movie.nsub);
+ }else if(strcmp(rdir->d_name, "dub") == 0){
+ snprintf(auxpath, sizeof auxpath, "%s/%s", path, rdir->d_name);
+ filldirlist(auxpath, &r.movie.dubs, &r.movie.ndub);
+ }else if(strcmp(rdir->d_name, "extra") == 0){
+ snprintf(auxpath, sizeof auxpath, "%s/%s", path, rdir->d_name);
+ filldirlist(auxpath, &r.movie.extras, &r.movie.nextra);
+ }else if(strcmp(rdir->d_name, "remake") == 0){
+ snprintf(auxpath, sizeof auxpath, "%s/%s", path, rdir->d_name);
+ filldirlist(auxpath, &r.movie.remakes, &r.movie.nremake);
+ }
+ continue;
+ case Rmulti:
+ if(strcmp(rdir->d_name, "release") == 0){
+ snprintf(auxpath, sizeof auxpath, "%s/%s", path, rdir->d_name);
+ f = fopen(auxpath, "r");
+ if(f == nil)
+ goto Rogue;
+ n = getline(&line, &linelen, f);
+ if(line[n-1] == '\n')
+ line[(n--)-1] = 0;
+ r.multi.release = strdup(line);
+ fclose(f);
+ }else if(strcmp(rdir->d_name, "synopsis") == 0)
+ r.multi.hassynopsis++;
+ else if(strcmp(rdir->d_name, "cover") == 0)
+ r.multi.hascover++;
+ else if(strcmp(rdir->d_name, "history") == 0)
+ r.multi.hashistory++;
+ else if(strncmp(rdir->d_name, "video", 5) == 0){
+ if(p == nil){
+ r.multi.part0 = emalloc(sizeof(Part));
+ p = r.multi.part0;
+ }else{
+ p->next = emalloc(sizeof(Part));
+ p = p->next;
+ }
+ p->no = strtol(rdir->d_name+5, nil, 0);
+ snprintf(auxpath, sizeof auxpath, "%s/sub%d", path, p->no);
+ filldirlist(auxpath, &p->subs, &p->nsub);
+ snprintf(auxpath, sizeof auxpath, "%s/dub%d", path, p->no);
+ filldirlist(auxpath, &p->dubs, &p->ndub);
+ }else if(strcmp(rdir->d_name, "extra") == 0){
+ snprintf(auxpath, sizeof auxpath, "%s/%s", path, rdir->d_name);
+ filldirlist(auxpath, &r.multi.extras, &r.multi.nextra);
+ }else if(strcmp(rdir->d_name, "remake") == 0){
+ snprintf(auxpath, sizeof auxpath, "%s/%s", path, rdir->d_name);
+ filldirlist(auxpath, &r.multi.remakes, &r.multi.nremake);
+ }
+ continue;
+ case Rserie:
+ if(strcmp(rdir->d_name, "synopsis") == 0)
+ r.serie.hassynopsis++;
+ else if(strcmp(rdir->d_name, "cover") == 0)
+ r.serie.hascover++;
+ else if(strcmp(rdir->d_name, "history") == 0)
+ r.serie.hashistory++;
+ else if(strcmp(rdir->d_name, "release") == 0){
+ snprintf(auxpath, sizeof auxpath, "%s/%s", path, rdir->d_name);
+ f = fopen(auxpath, "r");
+ if(f == nil)
+ goto Rogue;
+ while((n = getline(&line, &linelen, f)) > 0){
+ sno++;
+ if(line[n-1] == '\n')
+ line[(n--)-1] = 0;
+ if(!isdigit(*line))
+ continue;
+ if(s == nil){
+ r.serie.s = emalloc(sizeof(Season));
+ s = r.serie.s;
+ }else{
+ s->next = emalloc(sizeof(Season));
+ s = s->next;
+ }
+ s->release = strdup(line);
+ s->no = sno;
+ e = nil;
+ snprintf(auxpath, sizeof auxpath, "%s/s/%d", path, s->no);
+ d = opendir(auxpath);
+ if(d == nil)
+ goto Rogue;
+ while((dir = readdir(d)) != nil){
+ if(!isdigit(dir->d_name[0]))
+ continue;
+ e = emalloc(sizeof(Episode));
+ e->no = strtol(dir->d_name, nil, 0);
+ insertepisode(s, e, e->no);
+ /*
+ * it must be e->no instead of dir->d_name. we need to
+ * handle ranged episode folders, like `s/1/1-2' in
+ * Battlestar Galactica. or perhaps split the episode.
+ */
+ snprintf(auxpath, sizeof auxpath, "%s/s/%d/%s", path, s->no, dir->d_name);
+ ed = opendir(auxpath);
+ if(ed == nil)
+ goto Rogue;
+ while((edir = readdir(ed)) != nil){
+ if(strcmp(edir->d_name, "video") == 0)
+ e->hasvideo++;
+ else if(strcmp(edir->d_name, "sub") == 0){
+ snprintf(auxpath, sizeof auxpath, "%s/s/%d/%d/%s", path, s->no, e->no, edir->d_name);
+ filldirlist(auxpath, &e->subs, &e->nsub);
+ }else if(strcmp(edir->d_name, "dub") == 0){
+ snprintf(auxpath, sizeof auxpath, "%s/s/%d/%d/%s", path, s->no, e->no, edir->d_name);
+ filldirlist(auxpath, &e->dubs, &e->ndub);
+ }
+ }
+ closedir(ed);
+ }
+ closedir(d);
+ }
+ fclose(f);
+ }else if(strcmp(rdir->d_name, "extra") == 0){
+ snprintf(auxpath, sizeof auxpath, "%s/%s", path, rdir->d_name);
+ filldirlist(auxpath, &r.serie.extras, &r.serie.nextra);
+ }else if(strcmp(rdir->d_name, "remake") == 0){
+ snprintf(auxpath, sizeof auxpath, "%s/%s", path, rdir->d_name);
+ filldirlist(auxpath, &r.serie.remakes, &r.serie.nremake);
+ }
+ continue;
+ }
+ if(strcmp(rdir->d_name, "video") == 0)
+ r.type = Rmovie;
+ else if(strcmp(rdir->d_name, "video1") == 0)
+ r.type = Rmulti;
+ else if(strcmp(rdir->d_name, "s") == 0)
+ r.type = Rserie;
+ if(r.type != Runknown)
+ rewinddir(root);
+ }
+ closedir(root);
+ if(r.type == Runknown){
+Rogue:
+ sendlist(path);
+ exit(0);
+ }
+ fprintf(stderr, "tmpfile incoming\n");
+ f = tmpfile();
+ if(f == nil)
+ hfatal("sendportal: tmpfile");
+ fprintf(f, portalhead, title, title);
+ switch(r.type){
+ case Rmovie:
+ if(r.movie.hascover)
+ fprintf(f, portalcover, req->target, req->target);
+ fprintf(f, portalrelease);
+ fwrite(r.movie.release, 1, strlen(r.movie.release), f);
+ fprintf(f, portalmoviestream, req->target);
+ if(r.movie.hassynopsis){
+ fprintf(f, portalsynopsis);
+ snprintf(auxpath, sizeof auxpath, "%s/synopsis", path);
+ auxf = fopen(auxpath, "r");
+ if(auxf == nil)
+ break;
+ while(!feof(auxf)){
+ n = fread(buf, 1, sizeof buf, auxf);
+ if(ferror(auxf))
+ break;
+ if(fwrite(buf, 1, n, f) <= 0)
+ break;
+ }
+ fclose(auxf);
+ }
+ if(r.movie.hashistory){
+ fprintf(f, portalhistory);
+ snprintf(auxpath, sizeof auxpath, "%s/history", path);
+ auxf = fopen(auxpath, "r");
+ if(auxf == nil)
+ break;
+ while(!feof(auxf)){
+ n = fread(buf, 1, sizeof buf, auxf);
+ if(ferror(auxf))
+ break;
+ if(fwrite(buf, 1, n, f) <= 0)
+ break;
+ }
+ fclose(auxf);
+ }
+ if(r.movie.nsub > 0){
+ fprintf(f, portalsub);
+ fprintf(f, "<ul>\n");
+ for(; r.movie.nsub--; r.movie.subs++)
+ fprintf(f, "<li><a href=\"%s/sub/%s\">%s</a></li>\n", req->target, *r.movie.subs, *r.movie.subs);
+ fprintf(f, "</ul>");
+ }
+ if(r.movie.ndub > 0){
+ fprintf(f, portaldub);
+ fprintf(f, "<ul>\n");
+ for(; r.movie.ndub--; r.movie.dubs++)
+ fprintf(f, "<li><a href=\"%s/dub/%s\">%s</a></li>\n", req->target, *r.movie.dubs, *r.movie.dubs);
+ fprintf(f, "</ul>");
+ }
+ if(r.movie.nextra > 0){
+ fprintf(f, portalextra);
+ fprintf(f, "<ul>\n");
+ for(; r.movie.nextra--; r.movie.extras++)
+ fprintf(f, "<li><a href=\"%s/extra/%s\">%s</a></li>\n", req->target, *r.movie.extras, *r.movie.extras);
+ fprintf(f, "</ul>");
+ }
+ break;
+ case Rmulti:
+ if(r.multi.hascover)
+ fprintf(f, portalcover, req->target, req->target);
+ fprintf(f, portalrelease);
+ fwrite(r.multi.release, 1, strlen(r.multi.release), f);
+ fprintf(f, portalmultistream, req->target);
+ fprintf(f, "<ul>\n");
+ for(p = r.multi.part0; p != nil; p = p->next)
+ fprintf(f, "<li><a href=\"%s/video%d\">Part %d</a></li>\n", req->target, p->no, p->no);
+ fprintf(f, "</ul>");
+ if(r.multi.hassynopsis){
+ fprintf(f, portalsynopsis);
+ snprintf(auxpath, sizeof auxpath, "%s/synopsis", path);
+ auxf = fopen(auxpath, "r");
+ if(auxf == nil)
+ break;
+ while(!feof(auxf)){
+ n = fread(buf, 1, sizeof buf, auxf);
+ if(ferror(auxf))
+ break;
+ if(fwrite(buf, 1, n, f) <= 0)
+ break;
+ }
+ fclose(auxf);
+ }
+ if(r.multi.hashistory){
+ fprintf(f, portalhistory);
+ snprintf(auxpath, sizeof auxpath, "%s/history", path);
+ auxf = fopen(auxpath, "r");
+ if(auxf == nil)
+ break;
+ while(!feof(auxf)){
+ n = fread(buf, 1, sizeof buf, auxf);
+ if(ferror(auxf))
+ break;
+ if(fwrite(buf, 1, n, f) <= 0)
+ break;
+ }
+ fclose(auxf);
+ }
+ canintrosubs = canintrodubs = 1;
+ for(p = r.multi.part0; p != nil; p = p->next)
+ if(p->nsub > 0){
+ if(canintrosubs)
+ fprintf(f, portalsub), canintrosubs--;
+ fprintf(f, "<ul><li>Part %d", p->no);
+ fprintf(f, "<ul>\n");
+ for(; p->nsub--; p->subs++)
+ fprintf(f, "<li><a href=\"%s/sub%d/%s\">%s</a></li>\n", req->target, p->no, *p->subs, *p->subs);
+ fprintf(f, "</ul></li></ul>\n");
+ }
+ for(p = r.multi.part0; p != nil; p = p->next)
+ if(p->ndub > 0){
+ if(canintrodubs)
+ fprintf(f, portaldub), canintrodubs--;
+ fprintf(f, "<ul><li>Part %d", p->no);
+ fprintf(f, "<ul>\n");
+ for(; p->ndub--; p->dubs++)
+ fprintf(f, "<li><a href=\"%s/dub%d/%s\">%s</a></li>\n", req->target, p->no, *p->dubs, *p->dubs);
+ fprintf(f, "</ul></li></ul>\n");
+ }
+ if(r.movie.nextra > 0){
+ fprintf(f, portalextra);
+ fprintf(f, "<ul>\n");
+ for(; r.movie.nextra--; r.movie.extras++)
+ fprintf(f, "<li><a href=\"%s/extra/%s\">%s</a></li>\n", req->target, *r.movie.extras, *r.movie.extras);
+ fprintf(f, "</ul>");
+ }
+ break;
+ case Rserie:
+ if(r.serie.hascover)
+ fprintf(f, portalcover, req->target, req->target);
+ fprintf(f, portalrelease);
+ fprintf(f, "<ul>\n");
+ for(s = r.serie.s; s != nil; s = s->next)
+ fprintf(f, "<li>Season %d on %s</li>\n", s->no, s->release);
+ fprintf(f, "</ul>");
+ fprintf(f, portalseriestream);
+ fprintf(f, "<ul>\n");
+ for(s = r.serie.s; s != nil; s = s->next){
+ fprintf(f, "<li>Season %d", s->no);
+ fprintf(f, "<ul>\n");
+ for(e = s->pilot; e != nil; e = e->next)
+ if(e->hasvideo)
+ fprintf(f, "<li><a href=\"%s/s/%d/%d/video\">Episode %d</a></li>\n",
+ req->target, s->no, e->no, e->no);
+ else
+ fprintf(f, "<li>Episode %d is unavailable</li>\n", e->no);
+ fprintf(f, "</ul></li>\n");
+ }
+ fprintf(f, "</ul>");
+ if(r.serie.hassynopsis){
+ fprintf(f, portalsynopsis);
+ snprintf(auxpath, sizeof auxpath, "%s/synopsis", path);
+ auxf = fopen(auxpath, "r");
+ if(auxf == nil)
+ break;
+ while(!feof(auxf)){
+ n = fread(buf, 1, sizeof buf, auxf);
+ if(ferror(auxf))
+ break;
+ if(fwrite(buf, 1, n, f) <= 0)
+ break;
+ }
+ fclose(auxf);
+ }
+ if(r.serie.hashistory){
+ fprintf(f, portalhistory);
+ snprintf(auxpath, sizeof auxpath, "%s/history", path);
+ auxf = fopen(auxpath, "r");
+ if(auxf == nil)
+ break;
+ while(!feof(auxf)){
+ n = fread(buf, 1, sizeof buf, auxf);
+ if(ferror(auxf))
+ break;
+ if(fwrite(buf, 1, n, f) <= 0)
+ break;
+ }
+ fclose(auxf);
+ }
+ canintrosubs = canintrodubs = 1;
+ for(s = r.serie.s; s != nil; s = s->next){
+ canintroseason = 1;
+ for(e = s->pilot; e != nil; e = e->next)
+ if(e->nsub > 0){
+ if(canintrosubs)
+ fprintf(f, portalsub), canintrosubs--;
+ if(canintroseason)
+ fprintf(f, "<ul><li>Season %d", s->no), canintroseason--;
+ fprintf(f, "<ul>\n");
+ for(; e->nsub--; e->subs++)
+ fprintf(f, "<li>Episode %d: <a href=\"%s/s/%d/%d/sub/%s\">%s</a></li>\n",
+ e->no, req->target, s->no, e->no, *e->subs, *e->subs);
+ fprintf(f, "</ul>");
+ }
+ if(!canintroseason)
+ fprintf(f, "</li></ul>\n");
+ }
+ for(s = r.serie.s; s != nil; s = s->next){
+ canintroseason = 1;
+ for(e = s->pilot; e != nil; e = e->next)
+ if(e->ndub > 0){
+ if(canintrodubs)
+ fprintf(f, portaldub), canintrodubs--;
+ if(canintroseason)
+ fprintf(f, "<ul><li>Season %d", s->no), canintroseason--;
+ fprintf(f, "<ul>\n");
+ for(; e->ndub--; e->dubs++)
+ fprintf(f, "<li>Episode %d: <a href=\"%s/s/%d/%d/dub/%s\">%s</a></li>\n",
+ e->no, req->target, s->no, e->no, *e->dubs, *e->dubs);
+ fprintf(f, "</ul>");
+ }
+ if(!canintroseason)
+ fprintf(f, "</li></ul>\n");
+ }
+ if(r.serie.nextra > 0){
+ fprintf(f, portalextra);
+ fprintf(f, "<ul>\n");
+ for(; r.serie.nextra--; r.serie.extras++)
+ fprintf(f, "<li><a href=\"%s/extra/%s\">%s</a></li>\n", req->target, *r.serie.extras, *r.serie.extras);
+ fprintf(f, "</ul>");
+ }
+ break;
+ default: goto Rogue;
+ }
+ fprintf(f, portalfeet);
+ fseeko(f, 0, SEEK_SET);
+ if(fstat(fileno(f), &fst) < 0)
+ switch(errno){
+ case EACCES: hfail(Sforbid);
+ case ENOENT: hfail(Snotfound);
+ default: hfatal("sendportal: fstat");
+ }
+ sendfile(f, &fst);
+ fclose(f);
+}
+
+char *argv0;
+
+void
+usage(void)
+{
+ fprintf(stderr, "usage: %s [-d wdir]\n", argv0);
+ exit(1);
+}
+
+int
+main(int argc, char *argv[])
+{
+ FILE *f;
+ struct stat fst;
+ char path[512];
+
+ ARGBEGIN{
+ case 'd':
+ wdir = EARGF(usage());
+ break;
+ default: usage();
+ }ARGEND;
+ memset(path, 0, sizeof path);
+ hparsereq();
+ if(strcmp(req->method, "GET") != 0 && strcmp(req->method, "HEAD") != 0)
+ hfail(Snotimple);
+ if(strcmp(req->version, httpver) != 0)
+ hfail(Swrongver);
+ if(strcmp(req->target, "/style") == 0)
+ strncpy(path, stylepath, sizeof(path)-1);
+ else if(strcmp(req->target, "/favicon.ico") == 0)
+ strncpy(path, fvicopath, sizeof(path)-1);
+ else
+ snprintf(path, sizeof path, "%s%s", wdir, req->target);
+ if(urldecode(path, path, strlen(path)) < 0)
+ hfail(Sbadreq);
+ if(stat(path, &fst) < 0)
+ switch(errno){
+ case EACCES: hfail(Sforbid);
+ case ENOENT: hfail(Snotfound);
+ default: hfatal("stat");
+ }
+ if(S_ISREG(fst.st_mode)){
+ f = fopen(path, "r");
+ if(f == nil)
+ hfatal("fopen");
+ sendfile(f, &fst);
+ fclose(f);
+ }else
+ sendportal(path);
+ exit(0);
+}
diff --git a/filmsrv b/filmsrv
new file mode 100755
index 0000000..53e2a9b
--- /dev/null
+++ b/filmsrv
@@ -0,0 +1,2 @@
+#!/bin/sh
+/home/cinema/bin/filmoteca -d /filmoteca 2>>/home/cinema/lib/film/filmoteca.log