package main import ( "fmt" "net/http" "os" "path/filepath" "strconv" "bufio" "encoding/json" "github.com/h2non/filetype" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/urfave/cli/v2" "io/ioutil" netUrl "net/url" "os/exec" "regexp" "strings" ) var Commands = []*cli.Command{} var version = "dev" func main() { app := &cli.App{ Commands: []*cli.Command{ { Name: "serve", Aliases: []string{"s"}, Flags: []cli.Flag{ &cli.StringFlag{ Name: "listen", Aliases: []string{"l"}, Value: "127.0.0.1", }, &cli.StringFlag{ Name: "port", Aliases: []string{"p"}, Value: "4000", }, &cli.StringFlag{ Name: "api-url", Aliases: []string{"u"}, Value: "http://127.0.0.1:4000", }, &cli.StringFlag{ Name: "directory", Aliases: []string{"d"}, Value: ".", }, &cli.StringFlag{ Name: "debug", }, }, Usage: "run http server to serve api and files", Action: func(ctx *cli.Context) error { listen := ctx.String("listen") port := ctx.Int64("port") directory := strings.TrimSuffix(ctx.String("directory"), "/") url := strings.TrimSuffix(ctx.String("api-url"), "/") e := echo.New() if ctx.Bool("debug") { e.Use(middleware.Logger()) e.Use(middleware.Recover()) } e.GET("/api/list", func(ctx echo.Context) error { return apiList(ctx, directory, url) }) e.GET("/api/stream", func(ctx echo.Context) error { return apiStream(ctx, directory, url) }) e.Logger.Fatal(e.Start(fmt.Sprintf("%s:%d", listen, port))) return nil }, }, { Name: "play", Aliases: []string{"p"}, Flags: []cli.Flag{ &cli.StringFlag{ Name: "api-url", Aliases: []string{"u"}, Value: "http://127.0.0.1:4000", }, }, Usage: "run client", Action: func(ctx *cli.Context) error { return runShell(ctx, "play") }, }, { Name: "download", Aliases: []string{"d"}, Flags: []cli.Flag{ &cli.StringFlag{ Name: "api-url", Aliases: []string{"u"}, Value: "http://127.0.0.1:4000", }, }, Usage: "run client", Action: func(ctx *cli.Context) error { return runShell(ctx, "download") }, }, }, } if err := app.Run(os.Args); err != nil { fmt.Println(err) } } type File struct { Name string `json:"name"` Url string `json:"url"` Path string `json:"-"` RelativePath string `json:"-"` Head []byte `json:"-"` ContentType string `json:"-"` } func (f *File) GenerateHeadAndContentType() { fo, _ := os.Open(f.Path) head := make([]byte, 261) fo.Read(head) f.Head = head f.ContentType = http.DetectContentType(head) fo.Close() } func (f File) IsSupported() bool { return filetype.IsVideo(f.Head) } type ApiError struct { Error string `json:"error"` } func getFiles(directory, url string) ([]File, error) { files := []File{} absoluteRootPath, err := filepath.Abs(directory) if err != nil { return nil, err } err = filepath.Walk(directory, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } basename := string(info.Name()) relativePath := strings.Replace(path, absoluteRootPath, "", 1) file := File{ Name: basename, Path: path, RelativePath: relativePath, Url: fmt.Sprintf("%s/api/stream?path=%s", url, netUrl.QueryEscape(relativePath)), } file.GenerateHeadAndContentType() if file.IsSupported() { files = append(files, file) } return nil }) if err != nil { return nil, err } return files, nil } func apiStream(e echo.Context, directory, url string) error { files, err := getFiles(directory, url) path := e.QueryParam("path") if err != nil { return e.JSON(http.StatusInternalServerError, ApiError{Error: fmt.Sprintf("%s", err)}) } if path == "" { return e.JSON(http.StatusBadRequest, ApiError{Error: "\"path\" query param is missing"}) } for _, file := range files { if file.RelativePath == path { // stat, _ := os.Stat(file.Path) e.Response().Header().Del("Content-Length") //e.Response().Header().Set("Content-Length", string(stat.Size())) http.ServeFile(e.Response(), e.Request(), file.Path) } } return e.JSON(http.StatusNotFound, ApiError{Error: "file not found"}) } func apiList(e echo.Context, directory, url string) error { files, err := getFiles(directory, url) if err != nil { return e.JSON(http.StatusInternalServerError, ApiError{Error: fmt.Sprintf("%s", err)}) } return e.JSONPretty(http.StatusOK, files, " ") } func runShell(ctx *cli.Context, action string) error { url := strings.TrimSuffix(ctx.String("api-url"), "/") response, err := http.Get(fmt.Sprintf("%s/api/list", url)) if err != nil { fmt.Println(err) return nil } body, err := ioutil.ReadAll(response.Body) var files []File json.Unmarshal([]byte(body), &files) size := len(files) for key, file := range files { fmt.Printf("[%3d] %s\n", size-key, file.Name) } reader := bufio.NewReader(os.Stdin) fmt.Print("> [1] ") input, _ := reader.ReadString('\n') input = strings.Replace(input, "\n", "", -1) if input == "" { input = "1" } if input == "q" { return nil } range1Regex := "^([0-9]+)-([0-9]+)$" range2Regex := "^([0-9]+)-$" range3Regex := "^([0-9]+)\\+$" isRange1, _ := regexp.MatchString(range1Regex, input) isRange2, _ := regexp.MatchString(range2Regex, input) isRange3, _ := regexp.MatchString(range3Regex, input) var result []File if input == "*" || input == "*+" { for i := len(files) - 1; i >= 0; i-- { result = append(result, files[i]) } } else if input == "-*" { result = files } else if isRange1 { // a-b regex, _ := regexp.Compile(range1Regex) data := regex.FindStringSubmatch(input) a, _ := strconv.Atoi(data[1]) b, _ := strconv.Atoi(data[2]) if a > b { for i := a - 1; i >= b-1; i-- { result = append(result, files[i]) fmt.Println(i) } } else { for i := b - 1; i >= a-1; i-- { result = append(result, files[i]) fmt.Println(i) } } } else if isRange2 { // a- regex, _ := regexp.Compile(range2Regex) data := regex.FindStringSubmatch(input) a, _ := strconv.Atoi(data[1]) for i := a - 1; i >= 0; i-- { result = append(result, files[i]) fmt.Println(i) } } else if isRange3 { // a+ regex, _ := regexp.Compile(range3Regex) data := regex.FindStringSubmatch(input) a, _ := strconv.Atoi(data[1]) for i := a - 1; i < len(files); i++ { result = append(result, files[i]) fmt.Println(i) } } else { words := strings.Fields(input) for _, word := range words { isInt, _ := regexp.MatchString("^[0-9]+$", word) if isInt { value, _ := strconv.Atoi(word) result = append(result, files[value-1]) } } } if len(result) == 0 { fmt.Println("Empty list") return nil } var cmds []*exec.Cmd if action == "play" { cmd := exec.Command("mpv", "-fs") for _, f := range result { cmd.Args = append(cmd.Args, f.Url) } cmds = append(cmds, cmd) } else { for _, f := range result { output := fmt.Sprintf("/home/simon/Videos/%s", f.Name) cmd := exec.Command("wget", "-o", "/dev/stdout", "-c", "--show-progress", "-O", output, f.Url) cmds = append(cmds, cmd) } } for _, cmd := range cmds { stdout, _ := cmd.StdoutPipe() scanner := bufio.NewScanner(stdout) cmd.Start() for scanner.Scan() { out := fmt.Sprintf("%q", scanner.Text()) out = strings.Trim(out, "\"") out = strings.ReplaceAll(out, `\u00a0`, " ") if out != "" { fmt.Print("\r") fmt.Print(out) } else { fmt.Print("\n") } } } for _, cmd := range cmds { cmd.Wait() } return nil }