package main import ( "bufio" "encoding/json" "fmt" "io/ioutil" "net/http" "os" "os/exec" "path/filepath" "regexp" "sort" "strconv" "strings" "github.com/h2non/filetype" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/urfave/cli/v2" netUrl "net/url" ) var ( Commands = []*cli.Command{} version = "dev" defaultListen = "127.0.0.1" defaultPort = "4000" defaultApiUrl = "http://127.0.0.1:4000" defaultServerDirectory = "." defaultDownloadDirectory = "." defaultName = "" defaultOrder = "date" ) var ( flagListen = "listen" flagPort = "port" flagApiUrl = "api-url" flagDirectory = "directory" flagDebug = "debug" flagName = "name" flagOrder = "order" ) func main() { app := &cli.App{ Commands: []*cli.Command{ { Name: "serve", Aliases: []string{"s"}, Flags: []cli.Flag{ &cli.StringFlag{ Name: flagListen, Aliases: []string{"l"}, Value: defaultListen, }, &cli.StringFlag{ Name: flagPort, Aliases: []string{"p"}, Value: defaultPort, }, &cli.StringFlag{ Name: flagApiUrl, Aliases: []string{"u"}, Value: defaultApiUrl, }, &cli.StringFlag{ Name: flagDirectory, Aliases: []string{"d"}, Value: defaultServerDirectory, }, &cli.StringFlag{ Name: flagDebug, }, }, 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(flagDirectory), "/") url := strings.TrimSuffix(ctx.String(flagApiUrl), "/") e := echo.New() if ctx.Bool(flagDebug) { 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", Usage: "run player", Aliases: []string{"p"}, Flags: []cli.Flag{ &cli.StringFlag{ Name: flagApiUrl, Aliases: []string{"u"}, Value: defaultApiUrl, }, &cli.StringFlag{ Name: flagName, Aliases: []string{"n"}, Value: defaultName, }, &cli.StringFlag{ Name: flagOrder, Aliases: []string{"o"}, Value: defaultOrder, }, }, Action: func(ctx *cli.Context) error { return runShell(ctx, "play") }, }, { Name: "download", Usage: "run downloder", Aliases: []string{"d"}, Flags: []cli.Flag{ &cli.StringFlag{ Name: flagApiUrl, Aliases: []string{"u"}, Value: defaultApiUrl, }, &cli.StringFlag{ Name: flagDirectory, Aliases: []string{"d"}, Value: defaultDownloadDirectory, }, &cli.StringFlag{ Name: flagName, Aliases: []string{"n"}, Value: defaultName, }, &cli.StringFlag{ Name: flagOrder, Aliases: []string{"o"}, Value: defaultOrder, }, }, 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:"-"` Date int64 `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, Date: info.ModTime().Unix(), 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 { e.Response().Header().Del("Content-Length") 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)}) } name := e.QueryParam("name") order := e.QueryParam("order") if name != "" { name = strings.ToLower(name) var newFiles []File for _, file := range files { isMatchingName, _ := regexp.MatchString(name, strings.ToLower(file.Name)) if isMatchingName { newFiles = append(newFiles, file) } } files = newFiles } sort.SliceStable(files, func(i, j int) bool { if order == "date" { return files[i].Date < files[j].Date } else if order == "name" { return files[i].Name < files[j].Name } return false }) return e.JSONPretty(http.StatusOK, files, " ") } func showFiles(files []File) { size := len(files) for key, file := range files { fmt.Printf("[%3d] %s\n", size-key, file.Name) } } func promptInput(defaultValue string) string { reader := bufio.NewReader(os.Stdin) fmt.Printf("> [%s] ", defaultValue) input, _ := reader.ReadString('\n') input = strings.Replace(input, "\n", "", -1) if input == "" { input = defaultValue } return input } func getFilesByWildcard(files, result []File) []File { for i := len(files) - 1; i >= 0; i-- { result = append(result, files[i]) } return result } func getFilesByRange1(files, result []File, a, b int) []File { size := len(files) if a > b { fmt.Printf("%+v\n", a) fmt.Printf("%+v\n", b) for i := a; i >= b; i-- { result = append(result, files[size-i]) } fmt.Printf("%+v\n", result) } else { for i := a; i <= b; i++ { result = append(result, files[size-i]) } } return result } func getFilesByRange2(files, result []File, a int) []File { size := len(files) for i := a; i >= 1; i-- { result = append(result, files[size-i]) fmt.Println(i) } return result } func getFilesByRange3(files, result []File, a int) []File { size := len(files) for i := a; i <= size; i++ { result = append(result, files[size-i]) fmt.Println(i) } return result } func getFilesByWordSplit(files, result []File, words []string) []File { size := len(files) for _, word := range words { isInt, _ := regexp.MatchString("^[0-9]+$", word) if isInt { value, _ := strconv.Atoi(word) result = append(result, files[size-value]) } } return result } func requestFileList(url, name, order string) []File { response, err := http.Get(fmt.Sprintf("%s/api/list?name=%s&order=%s", url, name, order)) if err != nil { fmt.Println(err) return nil } body, err := ioutil.ReadAll(response.Body) var files []File json.Unmarshal([]byte(body), &files) return files } func generatePlayerCmd(files []File) *exec.Cmd { cmd := exec.Command("mpv", "-fs") for _, f := range files { cmd.Args = append(cmd.Args, f.Url) } return cmd } func generateDownloadCmds(files []File, directory string) []*exec.Cmd { var cmds []*exec.Cmd for _, f := range files { output := fmt.Sprintf("%s/%s", directory, f.Name) cmd := exec.Command("wget", "-o", "/dev/stdout", "-c", "--show-progress", "-O", output, f.Url) cmds = append(cmds, cmd) } return cmds } func selectFiles(files []File, input string) []File { var result []File 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) if input == "*" || input == "*+" { result = getFilesByWildcard(files, result) } 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]) result = getFilesByRange1(files, result, a, b) } else if isRange2 { // a- regex, _ := regexp.Compile(range2Regex) data := regex.FindStringSubmatch(input) a, _ := strconv.Atoi(data[1]) result = getFilesByRange2(files, result, a) } else if isRange3 { // a+ regex, _ := regexp.Compile(range3Regex) data := regex.FindStringSubmatch(input) a, _ := strconv.Atoi(data[1]) result = getFilesByRange3(files, result, a) } else { result = getFilesByWordSplit(files, result, strings.Fields(input)) } return result } func runCmds(cmds []*exec.Cmd) { for _, cmd := range cmds { fmt.Printf("<----->\nCommand: %s\n<----->\n", cmd) stdout, _ := cmd.StdoutPipe() scanner := bufio.NewScanner(stdout) err := cmd.Start() if err != nil { fmt.Printf("%+v\n", err) } 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() } } func runShell(ctx *cli.Context, action string) error { directory := strings.TrimSuffix(ctx.String(flagDirectory), "/") name := ctx.String(flagName) order := ctx.String(flagOrder) files := requestFileList(strings.TrimSuffix(ctx.String(flagApiUrl), "/"), name, order) input := strings.Trim(ctx.Args().Get(0), " ") if input == "" { showFiles(files) input = promptInput("1") } if input == "q" { return nil } result := selectFiles(files, input) if len(result) == 0 { fmt.Println("Empty list, aborded!") return nil } var cmds []*exec.Cmd if action == "play" { cmd := generatePlayerCmd(result) cmds = append(cmds, cmd) } else { cmds = generateDownloadCmds(result, directory) } runCmds(cmds) return nil }