From c00090712d2dde2ab6b565835c056fc38f5a8456 Mon Sep 17 00:00:00 2001 From: timerzz Date: Mon, 1 Jul 2024 17:22:31 +0800 Subject: [PATCH] =?UTF-8?q?feat=20=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/build-push.yml | 20 +++++ go.mod | 28 +++++++ go.sum | 51 +++++++++++++ subtitles/client.go | 129 ++++++++++++++++++++++++++++++++ subtitles/data.go | 61 +++++++++++++++ subtitles/main.go | 62 +++++++++++++++ subtitles/swag/docs.go | 73 ++++++++++++++++++ subtitles/swag/swagger.json | 44 +++++++++++ subtitles/swag/swagger.yaml | 27 +++++++ subtitles/validator.go | 15 ++++ 10 files changed, 510 insertions(+) create mode 100644 .gitea/workflows/build-push.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 subtitles/client.go create mode 100644 subtitles/data.go create mode 100644 subtitles/main.go create mode 100644 subtitles/swag/docs.go create mode 100644 subtitles/swag/swagger.json create mode 100644 subtitles/swag/swagger.yaml create mode 100644 subtitles/validator.go diff --git a/.gitea/workflows/build-push.yml b/.gitea/workflows/build-push.yml new file mode 100644 index 0000000..2db353f --- /dev/null +++ b/.gitea/workflows/build-push.yml @@ -0,0 +1,20 @@ +name: Build image +on: [push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: https://git.timerzz.com:20443/timerzz/setup-go@v4 + env: + HTTPS_PROXY: http://192.168.31.55:10809 + with: + go-version: '1.22.x' + - uses: https://git.timerzz.com:20443/timerzz/checkout@v4 + - uses: https://git.timerzz.com:20443/timerzz/setup-ko@v0.6 + with: + version: v0.15.4 + env: + KO_DOCKER_REPO: 192.168.31.55:5000/paas/bilibili + - run: ko build --bare ./subtitles + env: + KO_DOCKER_REPO: 192.168.31.55:5000/paas/bilibili \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4dbcd4d --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module gitea.timerzz.com/paas/bilibili + +go 1.22.2 + +require ( + github.com/go-playground/validator/v10 v10.22.0 + github.com/gofiber/fiber/v3 v3.0.0-beta.2 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.6 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.52.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..60e22d8 --- /dev/null +++ b/go.sum @@ -0,0 +1,51 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/gofiber/fiber/v3 v3.0.0-beta.2 h1:mVVgt8PTaHGup3NGl/+7U7nEoZaXJ5OComV4E+HpAao= +github.com/gofiber/fiber/v3 v3.0.0-beta.2/go.mod h1:w7sdfTY0okjZ1oVH6rSOGvuACUIt0By1iK0HKUb3uqM= +github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co= +github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= +github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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.16/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= +github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/subtitles/client.go b/subtitles/client.go new file mode 100644 index 0000000..f45cf51 --- /dev/null +++ b/subtitles/client.go @@ -0,0 +1,129 @@ +package main + +import ( + "fmt" + "github.com/gofiber/fiber/v3/client" + "net/url" + "regexp" + "strconv" + "strings" +) + +var headers = map[string][]string{ + "Accept": {"application/json"}, + "Content-Type": {"application/json"}, + "User-Agent": {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"}, + "Host": {"api.bilibili.com"}, +} + +type Client struct { + cli *client.Client +} + +func NewClient(sessData string) *Client { + return &Client{ + cli: client.New().AddHeaders(headers).SetCookie("SESSDATA", sessData), + } +} + +func (c *Client) GetSubtitles(url string) (string, error) { + vid, page, err := parseBilibiliUrl(url) + if err != nil { + return "", err + } + data, err := c.callView(vid) + if err != nil { + return "", err + } + var aid, cid = data.Data.Stat.Aid, 0 + for _, p := range data.Data.Pages { + if p.Page == page { + cid = p.Cid + } + } + player, err := c.callPlayer(aid, cid) + if err != nil { + return "", err + } + subTitlesUrl := "" + for _, sub := range player.Data.Subtitle.Subtitles { + if sub.SubtitleUrl != "" { + subTitlesUrl = fmt.Sprintf("https:%s", sub.SubtitleUrl) + break + } + } + subtitles, err := c.callSubtitles(subTitlesUrl) + if err != nil { + return "", err + } + var subtitlesList = make([]string, 0, len(subtitles.Body)) + for _, sub := range subtitles.Body { + subtitlesList = append(subtitlesList, sub.Content) + } + return strings.Join(subtitlesList, "\n"), nil +} + +func (c *Client) callView(vid string) (data Response[ViewResponseData], err error) { + var r = c.cli.R() + if strings.HasPrefix(vid, "av") { + r.AddParam("aid", vid[2:]) + } else { + r.AddParam("bvid", vid) + } + resp, err := r.Get("https://api.bilibili.com/x/web-interface/view") + if err != nil { + return data, fmt.Errorf("请求 %s 出错:%v", r.URL(), err) + } + err = resp.JSON(&data) + return data, err +} + +func (c *Client) callPlayer(aid, cid int) (data Response[PlayerResponseData], err error) { + var r = c.cli.R() + r.AddParam("aid", strconv.Itoa(aid)) + r.AddParam("cid", strconv.Itoa(cid)) + + resp, err := r.Get("https://api.bilibili.com/x/player/v2") + if err != nil { + return data, fmt.Errorf("请求 %s 出错:%v", r.URL(), err) + } + err = resp.JSON(&data) + return data, err +} + +func (c *Client) callSubtitles(url string) (data SubtitlesResponse, err error) { + var r = c.cli.R() + resp, err := r.Get(url) + if err != nil { + return data, fmt.Errorf("请求 %s 出错:%v", r.URL(), err) + } + err = resp.JSON(&data) + return data, err +} + +func parseBilibiliUrl(bu string) (vid string, page int, err error) { + u, err := url.Parse(bu) + if err != nil { + return "", 0, err + } + if u.Host != "www.bilibili.com" && u.Host != "m.bilibili.com" { + return "", 0, fmt.Errorf("链接似乎不是bilibili的视频链接") + } + re := regexp.MustCompile("/video/(?P[a-zA-Z0-9]+)") + parts := re.FindStringSubmatch(u.Path) + names := re.SubexpNames() + for _, name := range names { + if name == "vid" { + vid = parts[re.SubexpIndex(name)] + } + } + if vid == "" { + return "", 0, fmt.Errorf("链接似乎不是bilibili的视频链接,未获取到视频id") + } + query := u.Query() + if query.Get("p") != "" { + pageStr := query.Get("p") + page, _ = strconv.Atoi(pageStr) + } + return vid, page, nil +} diff --git a/subtitles/data.go b/subtitles/data.go new file mode 100644 index 0000000..716dcc5 --- /dev/null +++ b/subtitles/data.go @@ -0,0 +1,61 @@ +package main + +type ResponseData interface { + ViewResponseData | PlayerResponseData +} + +type Response[T ResponseData] struct { + Code int `json:"code"` + Message string `json:"message"` + Ttl int `json:"ttl"` + Data T `json:"data"` +} + +type ViewResponseData struct { + Bvid string `json:"bvid"` + Title string `json:"title"` + Duration int `json:"duration"` + Stat struct { + Aid int `json:"aid"` + } `json:"stat"` + Pages []struct { + Cid int `json:"cid"` + Page int `json:"page"` + From string `json:"from"` + Part string `json:"part"` + Duration int `json:"duration"` + Vid string `json:"vid"` + Weblink string `json:"weblink"` + } `json:"pages"` +} + +type PlayerResponseData struct { + Aid int `json:"aid"` + Bvid string `json:"bvid"` + Cid int `json:"cid"` + PageNo int `json:"page_no"` + Subtitle struct { + Subtitles []struct { + Id int64 `json:"id"` + Lan string `json:"lan"` + LanDoc string `json:"lan_doc"` + IsLock bool `json:"is_lock"` + SubtitleUrl string `json:"subtitle_url"` + Type int `json:"type"` + IdStr string `json:"id_str"` + AiType int `json:"ai_type"` + AiStatus int `json:"ai_status"` + } `json:"subtitles"` + } `json:"subtitle"` +} + +type SubtitlesResponse struct { + Body []struct { + From float64 `json:"from"` + To float64 `json:"to"` + Sid int `json:"sid"` + Location int `json:"location"` + Content string `json:"content"` + Music int `json:"music"` + } `json:"body"` +} diff --git a/subtitles/main.go b/subtitles/main.go new file mode 100644 index 0000000..f7dd31f --- /dev/null +++ b/subtitles/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/cors" + "github.com/gofiber/fiber/v3/middleware/healthcheck" + "github.com/gofiber/fiber/v3/middleware/recover" + "os" +) + +type Request struct { + Url string `json:"url" validate:"required"` +} + +func main() { + sessData := os.Getenv("SESSDATA") + if sessData == "" { + panic("SESSDATA is required") + } + + cli := NewClient(sessData) + s := &Service{cli: cli} + app := fiber.New(fiber.Config{ + StructValidator: &structValidator{validate: validator.New()}, + }) + + app.Use(recover.New(), cors.New()) + app.Get(healthcheck.DefaultLivenessEndpoint, healthcheck.NewHealthChecker()) + // Provide a minimal config for readiness check + app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker()) + + app.Post("/api/v1/bilibili/subtitles", s.GetSubtitles) + + _ = app.Listen(":80") +} + +type Service struct { + cli *Client +} + +// GetSubtitles @Summary 获取字幕 +// @Description 获取字幕 +// @Accept application/json +// @Produce application/json +// @Param json body Request true "请求参数" +// @Router /api/v1/bilibili/subtitles [post] +func (s *Service) GetSubtitles(ctx fiber.Ctx) error { + var req Request + if err := ctx.Bind().JSON(&req); err != nil { + return err + } + data, err := s.cli.GetSubtitles(req.Url) + if err != nil { + return err + } + return ctx.JSON(fiber.Map{ + "data": data, + "code": 0, + "msg": "ok", + }) +} diff --git a/subtitles/swag/docs.go b/subtitles/swag/docs.go new file mode 100644 index 0000000..4ae9304 --- /dev/null +++ b/subtitles/swag/docs.go @@ -0,0 +1,73 @@ +// Package swag Code generated by swaggo/swag. DO NOT EDIT +package swag + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/v1/bilibili/subtitles": { + "post": { + "description": "获取字幕", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "请求参数", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.Request" + } + } + ], + "responses": {} + } + } + }, + "definitions": { + "main.Request": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/subtitles/swag/swagger.json b/subtitles/swag/swagger.json new file mode 100644 index 0000000..492f373 --- /dev/null +++ b/subtitles/swag/swagger.json @@ -0,0 +1,44 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/api/v1/bilibili/subtitles": { + "post": { + "description": "获取字幕", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "请求参数", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.Request" + } + } + ], + "responses": {} + } + } + }, + "definitions": { + "main.Request": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/subtitles/swag/swagger.yaml b/subtitles/swag/swagger.yaml new file mode 100644 index 0000000..b668790 --- /dev/null +++ b/subtitles/swag/swagger.yaml @@ -0,0 +1,27 @@ +definitions: + main.Request: + properties: + url: + type: string + required: + - url + type: object +info: + contact: {} +paths: + /api/v1/bilibili/subtitles: + post: + consumes: + - application/json + description: 获取字幕 + parameters: + - description: 请求参数 + in: body + name: json + required: true + schema: + $ref: '#/definitions/main.Request' + produces: + - application/json + responses: {} +swagger: "2.0" diff --git a/subtitles/validator.go b/subtitles/validator.go new file mode 100644 index 0000000..e10dd6c --- /dev/null +++ b/subtitles/validator.go @@ -0,0 +1,15 @@ +package main + +import "github.com/go-playground/validator/v10" + +type structValidator struct { + validate *validator.Validate +} + +func (v *structValidator) Engine() any { + return "" +} + +func (v *structValidator) ValidateStruct(out any) error { + return v.validate.Struct(out) +}