From 8f8dfaf514689202450c45cf97c7e13db245cdca Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 12 Sep 2025 21:31:34 +0200 Subject: [PATCH] refactor: full rewrite --- Makefile | 8 +- api/api.go | 75 ++++++ cmd/cli/main.go | 202 ++++++++++++++ go.mod | 31 ++- go.sum | 84 ++++++ internal/api/error.go | 5 + internal/api/remote.go | 28 ++ internal/cmd/generator.go | 30 +++ internal/fs/file.go | 39 +++ internal/fs/filter.go | 118 +++++++++ internal/fs/scanner.go | 54 ++++ internal/render/files.go | 22 ++ internal/shell/shell.go | 112 ++++++++ internal/web/list.go | 51 ++++ internal/web/stream.go | 32 +++ src/main.go | 536 -------------------------------------- 16 files changed, 885 insertions(+), 542 deletions(-) create mode 100644 api/api.go create mode 100644 cmd/cli/main.go create mode 100644 internal/api/error.go create mode 100644 internal/api/remote.go create mode 100644 internal/cmd/generator.go create mode 100644 internal/fs/file.go create mode 100644 internal/fs/filter.go create mode 100644 internal/fs/scanner.go create mode 100644 internal/render/files.go create mode 100644 internal/shell/shell.go create mode 100644 internal/web/list.go create mode 100644 internal/web/stream.go delete mode 100644 src/main.go diff --git a/Makefile b/Makefile index 4a96683..a27bfb3 100644 --- a/Makefile +++ b/Makefile @@ -38,19 +38,19 @@ win64: $(WINBIN) .PHONY: $(OSXBIN) $(OSXBIN): - GO111MODULE=$(GOMOD) GOARCH=$(GOARCH) GOOS=$(GOOSX) CGO_ENABLED=$(CGO_ENABLED) $(CC) $(CFLAGS) -o $(OSXBIN) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" ./src + GO111MODULE=$(GOMOD) GOARCH=$(GOARCH) GOOS=$(GOOSX) CGO_ENABLED=$(CGO_ENABLED) $(CC) $(CFLAGS) -o $(OSXBIN) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" ./cmd/cli .PHONY: $(ARMBIN) $(ARMBIN): - GO111MODULE=$(GOMOD) GOARCH=$(GOARCH) GOOS=$(GOOSLINUX) CGO_ENABLED=$(CGO_ENABLED) $(CC) $(CFLAGS) -o $(ARMBIN) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" ./src + GO111MODULE=$(GOMOD) GOARCH=$(GOARCH) GOOS=$(GOOSLINUX) CGO_ENABLED=$(CGO_ENABLED) $(CC) $(CFLAGS) -o $(ARMBIN) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" ./cmd/cli .PHONY: $(LINUXBIN) $(LINUXBIN): - GO111MODULE=$(GOMOD) GOARCH=$(GOARCH) GOOS=$(GOOSLINUX) CGO_ENABLED=$(CGO_ENABLED) $(CC) $(CFLAGS) -o $(LINUXBIN) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" ./src + GO111MODULE=$(GOMOD) GOARCH=$(GOARCH) GOOS=$(GOOSLINUX) CGO_ENABLED=$(CGO_ENABLED) $(CC) $(CFLAGS) -o $(LINUXBIN) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" ./cmd/cli .PHONY: $(WINBIN) $(WINBIN): - GO111MODULE=$(GOMOD) GOARCH=$(GOARCH) GOOS=$(GOOSWIN) CGO_ENABLED=$(CGO_ENABLED) $(CC) $(CFLAGS) -o $(WINBIN) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" ./src + GO111MODULE=$(GOMOD) GOARCH=$(GOARCH) GOOS=$(GOOSWIN) CGO_ENABLED=$(CGO_ENABLED) $(CC) $(CFLAGS) -o $(WINBIN) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)" ./cmd/cli .PHONY: clean clean: diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..ce35c0f --- /dev/null +++ b/api/api.go @@ -0,0 +1,75 @@ +package api + +import ( + "fmt" + "net/http" + "regexp" + "sort" + "strings" + + "github.com/labstack/echo/v4" + "gitnet.fr/deblan/mu-go/fs" +) + +type ApiError struct { + Error string `json:"error"` +} + +func List(e echo.Context, directory, url string) error { + files, err := fs.Files(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 []fs.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 Stream(e echo.Context, directory, url string) error { + files, err := fs.Files(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"}) +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go new file mode 100644 index 0000000..7469102 --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,202 @@ +package main + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/urfave/cli/v2" + "gitnet.fr/deblan/mu-go/internal/shell" + "gitnet.fr/deblan/mu-go/internal/web" +) + +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 web.List(ctx, directory, url) + }) + + e.GET("/api/stream", func(ctx echo.Context) error { + return web.Stream(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 { + s := shell.NewShell( + ctx.String(flagName), + ctx.String(flagOrder), + ctx.String(flagDirectory), + ctx.String(flagApiUrl), + ) + + return s.Run("play", ctx.Args()) + }, + }, + { + 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 { + s := shell.NewShell( + ctx.String(flagName), + ctx.String(flagOrder), + ctx.String(flagDirectory), + ctx.String(flagApiUrl), + ) + + return s.Run("download", ctx.Args()) + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} + +// 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() +// } +// } diff --git a/go.mod b/go.mod index 8bf55e0..2c64939 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module gitnet.fr/deblan/mu-go -go 1.18 +go 1.24.0 + +toolchain go1.24.6 require ( github.com/h2non/filetype v1.1.3 @@ -9,18 +11,43 @@ require ( ) require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/c-bata/go-prompt v0.2.6 // indirect + github.com/charmbracelet/bubbles v0.21.0 // indirect + github.com/charmbracelet/bubbletea v1.3.9 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/fatih/color v1.18.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/labstack/gommon v0.4.1 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-tty v0.0.3 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/pkg/term v1.2.0-beta.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rodaine/table v1.3.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/cast v1.10.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect golang.org/x/crypto v0.17.0 // indirect golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/term v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect ) diff --git a/go.sum b/go.sum index e135d75..a37efb5 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,38 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= +github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= +github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= +github.com/charmbracelet/bubbletea v1.3.9 h1:OBYdfRo6QnlIcXNmcoI2n1NNS65Nk6kI2L2FO1puS/4= +github.com/charmbracelet/bubbletea v1.3.9/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/labstack/echo/v4 v4.8.0 h1:wdc6yKVaHxkNOEdz4cRZs1pQkwSXPiRjq69yWP4QQS8= @@ -20,10 +47,17 @@ github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8 github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk= github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -31,11 +65,41 @@ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPn github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= +github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= +github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE= +github.com/rodaine/table v1.3.0/go.mod h1:47zRsHar4zw0jgxGxL9YtFfs7EGN6B/TaS+/Dmk4WxU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v2 v2.11.2 h1:FVfNg4m3vbjbBpLYxW//WjxUoHvJ9TlppXcqY9Q9ZfA= github.com/urfave/cli/v2 v2.11.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY= @@ -48,6 +112,8 @@ github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52 github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= @@ -64,7 +130,18 @@ golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -74,6 +151,12 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= @@ -89,3 +172,4 @@ golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/error.go b/internal/api/error.go new file mode 100644 index 0000000..dbca653 --- /dev/null +++ b/internal/api/error.go @@ -0,0 +1,5 @@ +package api + +type ApiError struct { + Error string `json:"error"` +} diff --git a/internal/api/remote.go b/internal/api/remote.go new file mode 100644 index 0000000..3c511ab --- /dev/null +++ b/internal/api/remote.go @@ -0,0 +1,28 @@ +package api + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "gitnet.fr/deblan/mu-go/internal/fs" +) + +func RequestFileList(url, name, order string) fs.Files { + 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 fs.Files + + json.Unmarshal([]byte(body), &files) + + return files +} diff --git a/internal/cmd/generator.go b/internal/cmd/generator.go new file mode 100644 index 0000000..f91146b --- /dev/null +++ b/internal/cmd/generator.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "fmt" + "os/exec" + + "gitnet.fr/deblan/mu-go/internal/fs" +) + +func PlayerCmd(files fs.Files) *exec.Cmd { + cmd := exec.Command("mpv", "-fs") + + for _, f := range files { + cmd.Args = append(cmd.Args, f.Url) + } + + return cmd +} + +func DownloadCmds(files fs.Files, 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 +} diff --git a/internal/fs/file.go b/internal/fs/file.go new file mode 100644 index 0000000..9ed988c --- /dev/null +++ b/internal/fs/file.go @@ -0,0 +1,39 @@ +package fs + +import ( + "net/http" + "os" + + "github.com/h2non/filetype" +) + +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:"-"` +} + +type Files []File + +func (f Files) Empty() bool { + return len(f) == 0 +} + +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) +} diff --git a/internal/fs/filter.go b/internal/fs/filter.go new file mode 100644 index 0000000..75596d2 --- /dev/null +++ b/internal/fs/filter.go @@ -0,0 +1,118 @@ +package fs + +import ( + "regexp" + "strconv" + "strings" + + "github.com/spf13/cast" +) + +func (f Files) FilterByWildcard() Files { + var result Files + + for i := len(f) - 1; i >= 0; i-- { + result = append(result, f[i]) + } + + return result +} + +func (f Files) FilterByRange1(from, to int) Files { + var result Files + size := len(f) + + if from > to { + for i := from; i >= to; i-- { + result = append(result, f[size-i]) + } + } else { + for i := from; i <= to; i++ { + result = append(result, f[size-i]) + } + } + + return result +} + +func (f Files) FilterByRange2(from int) Files { + var result Files + size := len(f) + + for i := from; i >= 1; i-- { + result = append(result, f[size-i]) + } + + return result +} + +func (f Files) FilterByRange3(from int) Files { + var result Files + size := len(f) + + for i := from; i <= size; i++ { + result = append(result, f[size-i]) + } + + return result +} + +func (f Files) FilterByWords(words []string) Files { + var result Files + size := len(f) + + for _, word := range words { + isInt, _ := regexp.MatchString("^[0-9]+$", word) + + if isInt { + value, _ := strconv.Atoi(word) + result = append(result, f[size-value]) + } + } + + return result +} + +func (f Files) Select(input string) Files { + switch input { + case "*": + case "*+": + return f.FilterByWildcard() + case "*-": + return f + } + + 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 isRange1 { + regex, _ := regexp.Compile(range1Regex) + data := regex.FindStringSubmatch(input) + + return f.FilterByRange1( + cast.ToInt(data[1]), + cast.ToInt(data[2]), + ) + } + + if isRange2 { + regex, _ := regexp.Compile(range2Regex) + data := regex.FindStringSubmatch(input) + + return f.FilterByRange2(cast.ToInt(data[1])) + } + + if isRange3 { + regex, _ := regexp.Compile(range3Regex) + data := regex.FindStringSubmatch(input) + + return f.FilterByRange3(cast.ToInt(data[1])) + } + + return f.FilterByWords(strings.Fields(input)) +} diff --git a/internal/fs/scanner.go b/internal/fs/scanner.go new file mode 100644 index 0000000..b0e949a --- /dev/null +++ b/internal/fs/scanner.go @@ -0,0 +1,54 @@ +package fs + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + "strings" +) + +func DirectoryFiles(directory, baseUrl string) (Files, error) { + var files Files + + 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", baseUrl, url.QueryEscape(relativePath)), + } + + file.GenerateHeadAndContentType() + + if file.IsSupported() { + files = append(files, file) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return files, nil +} diff --git a/internal/render/files.go b/internal/render/files.go new file mode 100644 index 0000000..8f1d377 --- /dev/null +++ b/internal/render/files.go @@ -0,0 +1,22 @@ +package render + +import ( + "fmt" + + "github.com/fatih/color" + "github.com/rodaine/table" + "gitnet.fr/deblan/mu-go/internal/fs" +) + +func RenderFiles(files fs.Files) { + tbl := table.New("ID", "Name") + tbl.WithFirstColumnFormatter(color.New(color.FgCyan).SprintfFunc()) + + size := len(files) + + for key, file := range files { + tbl.AddRow(fmt.Sprintf("%3d", size-key), file.Name) + } + + tbl.Print() +} diff --git a/internal/shell/shell.go b/internal/shell/shell.go new file mode 100644 index 0000000..4fdce30 --- /dev/null +++ b/internal/shell/shell.go @@ -0,0 +1,112 @@ +package shell + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/urfave/cli/v2" + "gitnet.fr/deblan/mu-go/internal/api" + "gitnet.fr/deblan/mu-go/internal/cmd" + "gitnet.fr/deblan/mu-go/internal/render" +) + +type Shell struct { + Name string + Order string + Directory string + ApiUrl string +} + +func NewShell(name, order, directory, apiUrl string) *Shell { + return &Shell{ + Name: name, + Order: order, + Directory: directory, + ApiUrl: strings.TrimSuffix(apiUrl, "/"), + } +} + +func (s *Shell) Prompt(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 (s *Shell) Run(action string, args cli.Args) error { + directory := strings.TrimSuffix(s.Directory, "/") + + files := api.RequestFileList( + s.ApiUrl, + s.Name, + s.Order, + ) + + input := strings.Trim(args.Get(0), " ") + + switch input { + case "": + render.RenderFiles(files) + input = s.Prompt("1") + case "q": + return nil + } + + files = files.Select(input) + + if files.Empty() { + fmt.Println("Empty list, aborded!") + return nil + } + + var commands []*exec.Cmd + + switch action { + case "play": + commands = append(commands, cmd.PlayerCmd(files)) + case "download": + commands = cmd.DownloadCmds(files, directory) + } + + s.ExecCommands(commands) + + return nil +} + +func (s *Shell) ExecCommands(commands []*exec.Cmd) { + for _, cmd := range commands { + fmt.Printf("<----->\nCommand: %s\n<----->\n", cmd) + + 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 commands { + cmd.Wait() + } +} diff --git a/internal/web/list.go b/internal/web/list.go new file mode 100644 index 0000000..f22a530 --- /dev/null +++ b/internal/web/list.go @@ -0,0 +1,51 @@ +package web + +import ( + "fmt" + "net/http" + "regexp" + "sort" + "strings" + + "github.com/labstack/echo/v4" + "gitnet.fr/deblan/mu-go/internal/api" + "gitnet.fr/deblan/mu-go/internal/fs" +) + +func List(e echo.Context, directory, url string) error { + files, err := fs.DirectoryFiles(directory, url) + + if err != nil { + return e.JSON(http.StatusInternalServerError, api.ApiError{Error: fmt.Sprintf("%s", err)}) + } + + name := e.QueryParam("name") + order := e.QueryParam("order") + + if name != "" { + name = strings.ToLower(name) + var newFiles []fs.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, " ") +} diff --git a/internal/web/stream.go b/internal/web/stream.go new file mode 100644 index 0000000..6298a79 --- /dev/null +++ b/internal/web/stream.go @@ -0,0 +1,32 @@ +package web + +import ( + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + "gitnet.fr/deblan/mu-go/internal/api" + "gitnet.fr/deblan/mu-go/internal/fs" +) + +func Stream(e echo.Context, directory, url string) error { + files, err := fs.DirectoryFiles(directory, url) + path := e.QueryParam("path") + + if err != nil { + return e.JSON(http.StatusInternalServerError, api.ApiError{Error: fmt.Sprintf("%s", err)}) + } + + if path == "" { + return e.JSON(http.StatusBadRequest, api.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, api.ApiError{Error: "file not found"}) +} diff --git a/src/main.go b/src/main.go deleted file mode 100644 index 31796cf..0000000 --- a/src/main.go +++ /dev/null @@ -1,536 +0,0 @@ -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 -}