Update README
This commit is contained in:
parent
b4054e26b1
commit
bf3bd35fdb
23
README.md
23
README.md
|
@ -1,8 +1,25 @@
|
|||
# webfs
|
||||
|
||||
It's a WebDAV server.
|
||||
|
||||
That's all it does. It serves WebDAV.
|
||||
It's a WebDAV server that generates and caches media thumbnails.
|
||||
|
||||
I front-end it with Caddy,
|
||||
which handles authentication and access controls.
|
||||
Because the thumbnails live at the same path as the original media,
|
||||
so I don't need any special gubbins to protect thumbnails the same way.
|
||||
|
||||
|
||||
## Thumbnails
|
||||
|
||||
If you put `?thumbnail` at the end of the URL,
|
||||
it will use `ffmpeg` to generate a WebP thumbnail that fits in a 320x200 box.
|
||||
|
||||
I use WebP because it provides alpha channels, animations,
|
||||
and good compression.
|
||||
If your browser doesn't have WebP support,
|
||||
that's a real pity,
|
||||
and this isn't a good software solution for you.
|
||||
|
||||
Thumbnails are cached.
|
||||
No attempt is made to verify whether thumbnails are up-to-date,
|
||||
or to clean up the thumbnail directory.
|
||||
Something else is going to have to manage that.
|
||||
|
|
|
@ -20,6 +20,7 @@ type Handler struct {
|
|||
webdav.Handler
|
||||
dataRoot string
|
||||
thumbnailRoot string
|
||||
filterVideo string
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -28,7 +29,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
r.URL.Path += "index.html"
|
||||
}
|
||||
if r.FormValue("thumbnail") != "" {
|
||||
r.ParseForm()
|
||||
if _, ok := r.Form["thumbnail"]; (h.thumbnailRoot != "") && ok {
|
||||
h.ServeThumbnail(w, r)
|
||||
return
|
||||
}
|
||||
|
@ -51,6 +53,53 @@ func cleanPath(p string) string {
|
|||
return path.Clean(p)
|
||||
}
|
||||
|
||||
func (h *Handler) makeThumbnail(reqPath, thumbnailPath string) error {
|
||||
srcPath := path.Join(h.dataRoot, reqPath)
|
||||
|
||||
// Run ffprobe to figure out what kind of file this is
|
||||
ffdata, err := ffprobe.ProbeURL(context.Background(), srcPath, "-hide_banner")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isVideo := ffdata.Format.DurationSeconds > 0.0
|
||||
|
||||
// Build up ffmpeg invocation
|
||||
// XXX: some day soon you will want CommandContext
|
||||
cmd := exec.Command("ffmpeg", "-hide_banner", "-loglevel", "error")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if isVideo {
|
||||
skipSeconds := ffdata.Format.StartTimeSeconds + (ffdata.Format.DurationSeconds * 0.25)
|
||||
cmd.Args = append(cmd.Args,
|
||||
"-ss", fmt.Sprintf("%f", skipSeconds),
|
||||
"-i", srcPath,
|
||||
"-frames:v", "5",
|
||||
"-filter:v", h.filterVideo+",fps=2",
|
||||
"-loop", "0",
|
||||
thumbnailPath,
|
||||
)
|
||||
} else {
|
||||
cmd.Args = append(cmd.Args,
|
||||
"-i", srcPath,
|
||||
"-filter:v", h.filterVideo,
|
||||
thumbnailPath,
|
||||
)
|
||||
}
|
||||
|
||||
// Make sure the thumbnail has a directory to live in
|
||||
if err := os.MkdirAll(path.Dir(thumbnailPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert!
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) ServeThumbnail(w http.ResponseWriter, req *http.Request) {
|
||||
reqPath := cleanPath(req.URL.Path)
|
||||
reqPath = strings.TrimLeft(reqPath, "/")
|
||||
|
@ -58,43 +107,7 @@ func (h *Handler) ServeThumbnail(w http.ResponseWriter, req *http.Request) {
|
|||
|
||||
// If there's not already a thumbnail, make one
|
||||
if _, err := os.Stat(thumbnailPath); err != nil {
|
||||
srcPath := path.Join(h.dataRoot, reqPath)
|
||||
|
||||
ffdata, err := ffprobe.ProbeURL(context.Background(), srcPath, "-hide_banner")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
isVideo := ffdata.Format.DurationSeconds > 0.0
|
||||
|
||||
// XXX: some day soon you will want CommandContext
|
||||
cmd := exec.Command("ffmpeg", "-hide_banner", "-loglevel", "error")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if isVideo {
|
||||
skipSeconds := ffdata.Format.StartTimeSeconds + (ffdata.Format.DurationSeconds * 0.25)
|
||||
cmd.Args = append(cmd.Args,
|
||||
"-ss", fmt.Sprintf("%f", skipSeconds),
|
||||
"-i", srcPath,
|
||||
"-frames:v", "5",
|
||||
"-filter:v", "scale='min(320,iw)':'min(200,ih)':force_original_aspect_ratio=decrease,fps=2",
|
||||
"-loop", "0",
|
||||
thumbnailPath,
|
||||
)
|
||||
} else {
|
||||
cmd.Args = append(cmd.Args,
|
||||
"-i", srcPath,
|
||||
"-filter:v", "scale='min(320,iw)':'min(200,ih)':force_original_aspect_ratio=decrease",
|
||||
thumbnailPath,
|
||||
)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(path.Dir(thumbnailPath), 0755); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := h.makeThumbnail(reqPath, thumbnailPath); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
@ -107,8 +120,18 @@ func main() {
|
|||
address := flag.String("listen", ":8080", "Address to listen to")
|
||||
dataRoot := flag.String("root", ".", "Directory to serve")
|
||||
thumbnailRoot := flag.String("thumbnails", "/thumbnails", "Where to store thumbnails")
|
||||
filterVideo := flag.String(
|
||||
"filter:v",
|
||||
"scale='min(320,iw)':'min(200,ih)':force_original_aspect_ratio=decrease",
|
||||
"ffmpeg video filter",
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if _, err := os.Stat(*thumbnailRoot); err != nil {
|
||||
log.Println("Disabling thumbnail generation:", err)
|
||||
*thumbnailRoot = ""
|
||||
}
|
||||
|
||||
handler := &Handler{
|
||||
Handler: webdav.Handler{
|
||||
FileSystem: webdav.Dir(*dataRoot),
|
||||
|
@ -116,8 +139,9 @@ func main() {
|
|||
},
|
||||
dataRoot: *dataRoot,
|
||||
thumbnailRoot: *thumbnailRoot,
|
||||
filterVideo: *filterVideo,
|
||||
}
|
||||
|
||||
log.Println("Listening on", *address)
|
||||
http.ListenAndServe(*address, handler)
|
||||
log.Fatal(http.ListenAndServe(*address, handler))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue