refactor: full rewrite

This commit is contained in:
Simon Vieille 2025-09-12 21:31:34 +02:00
commit 8f8dfaf514
Signed by: deblan
GPG key ID: 579388D585F70417
16 changed files with 885 additions and 542 deletions

View file

@ -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:

75
api/api.go Normal file
View file

@ -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"})
}

202
cmd/cli/main.go Normal file
View file

@ -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()
// }
// }

31
go.mod
View file

@ -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
)

84
go.sum
View file

@ -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=

5
internal/api/error.go Normal file
View file

@ -0,0 +1,5 @@
package api
type ApiError struct {
Error string `json:"error"`
}

28
internal/api/remote.go Normal file
View file

@ -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
}

30
internal/cmd/generator.go Normal file
View file

@ -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
}

39
internal/fs/file.go Normal file
View file

@ -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)
}

118
internal/fs/filter.go Normal file
View file

@ -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))
}

54
internal/fs/scanner.go Normal file
View file

@ -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
}

22
internal/render/files.go Normal file
View file

@ -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()
}

112
internal/shell/shell.go Normal file
View file

@ -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()
}
}

51
internal/web/list.go Normal file
View file

@ -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, " ")
}

32
internal/web/stream.go Normal file
View file

@ -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"})
}

View file

@ -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
}