150 lines
3.4 KiB
Go
150 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"strings"
|
|
|
|
ffprobe "gopkg.in/vansante/go-ffprobe.v2"
|
|
|
|
"golang.org/x/net/webdav"
|
|
)
|
|
|
|
type Handler struct {
|
|
webdav.Handler
|
|
dataRoot string
|
|
thumbnailRoot string
|
|
filterVideo string
|
|
}
|
|
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case "GET", "HEAD":
|
|
if strings.HasSuffix(r.URL.Path, "/") {
|
|
r.URL.Path += "index.html"
|
|
}
|
|
r.ParseForm()
|
|
if _, ok := r.Form["thumbnail"]; (h.thumbnailRoot != "") && ok {
|
|
h.ServeThumbnail(w, r)
|
|
return
|
|
}
|
|
}
|
|
h.Handler.ServeHTTP(w, r)
|
|
}
|
|
|
|
/* cleanPath returns a sanitized HTTP path.
|
|
*
|
|
* This removes .. from everything,
|
|
* and will never return any path above /
|
|
*/
|
|
func cleanPath(p string) string {
|
|
if p == "" {
|
|
return "/"
|
|
}
|
|
if p[0] != '/' {
|
|
p = "/" + p
|
|
}
|
|
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 > 1.0
|
|
skipSeconds := ffdata.Format.StartTimeSeconds + (ffdata.Format.DurationSeconds * 0.25)
|
|
|
|
// 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 {
|
|
cmd.Args = append(cmd.Args,
|
|
"-ss", fmt.Sprintf("%f", skipSeconds),
|
|
"-i", srcPath,
|
|
"-frames:v", "5",
|
|
"-filter:v", h.filterVideo+",fps=2",
|
|
"-loop", "0",
|
|
"-map_metadata", "0",
|
|
thumbnailPath,
|
|
)
|
|
} else {
|
|
cmd.Args = append(cmd.Args,
|
|
"-i", srcPath,
|
|
"-filter:v", h.filterVideo,
|
|
"-map_metadata", "0",
|
|
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, "/")
|
|
thumbnailPath := path.Join(h.thumbnailRoot, reqPath) + ".webp"
|
|
|
|
// If there's not already a thumbnail, make one
|
|
if _, err := os.Stat(thumbnailPath); err != nil {
|
|
if err := h.makeThumbnail(reqPath, thumbnailPath); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
http.ServeFile(w, req, thumbnailPath)
|
|
}
|
|
|
|
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),
|
|
LockSystem: webdav.NewMemLS(),
|
|
},
|
|
dataRoot: *dataRoot,
|
|
thumbnailRoot: *thumbnailRoot,
|
|
filterVideo: *filterVideo,
|
|
}
|
|
|
|
log.Println("Listening on", *address)
|
|
log.Fatal(http.ListenAndServe(*address, handler))
|
|
}
|