diff --git a/.gitignore b/.gitignore index 9509550..053d99c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ dist .idea log data -config.yaml +config/server.yaml +config/account.yaml *.exe bin static \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 2dda7f1..2514570 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -5,9 +5,6 @@ builds: - env: - CGO_ENABLED=0 ldflags: -s -w - hooks: - post: - - upx --best "{{ .Path }}" goos: - linux - windows @@ -46,7 +43,8 @@ archives: format: zip files: - README.md - - config.yaml.example + - config/server.yaml.example + - config/account.yaml.example - LICENSE - static snapshot: diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6edd259 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +.PHONY:build +build: + goreleaser release --skip-publish --snapshot --rm-dist \ No newline at end of file diff --git a/README.md b/README.md index 4fc6027..5de1077 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -![](https://socialify.git.ci/iyear/biligo/image?description=1&font=Raleway&forks=1&issues=1&logo=https://s4.ax1x.com/2021/12/06/orLSGF.png&owner=1&pattern=Circuit%20Board&stargazers=1&theme=Dark) +![](https://socialify.git.ci/iyear/pure-live-core/image?description=1&font=Raleway&forks=1&issues=1&logo=https://s4.ax1x.com/2021/12/06/orLSGF.png&owner=1&pattern=Circuit%20Board&stargazers=1&theme=Dark) -## 简介 +## 🎉 简介 -![](https://img.shields.io/github/go-mod/go-version/iyear/biligo?style=flat-square) +![](https://img.shields.io/github/go-mod/go-version/iyear/pure-live-core?style=flat-square) ![](https://img.shields.io/badge/license-GPL-lightgrey.svg?style=flat-square) -![](https://img.shields.io/github/v/release/iyear/biligo?color=red&style=flat-square) -![](https://img.shields.io/github/last-commit/iyear/biligo?style=flat-square) +![](https://img.shields.io/github/v/release/iyear/pure-live-core?color=red&style=flat-square) +![](https://img.shields.io/github/last-commit/iyear/pure-live-core?style=flat-square) **该项目仅供学习,请勿用于商业用途。任何使用该项目造成的后果由使用者自行承担。** @@ -13,57 +13,101 @@ 没有礼物、粉丝团、弹窗,只有直播、弹幕 -### 特性 +## ✨ 特性 -- 直播间信息获取、直播流获取、发送弹幕 -- 平台 `Websocket` 协议封装,支持转发弹幕消息、直播间热度消息 -- 解决跨域问题,支持直播流本地转发 -- 简易的收藏夹功能支持 -- 支持设置 `Socks5` 代理 (未测试) -- 良好的项目结构设计,解耦直播平台和核心功能 -- 同时它也是一个简单的命令行工具。 -- ...... +🔎 直播间信息、直播流、弹幕流、发送弹幕 -### 参考 +⌛ 平台 `Websocket` 协议封装,支持转发弹幕消息、直播间热度消息 -https://github.com/wbt5/real-url +🗝️ 解决跨域问题,支持直播流本地转发 -https://github.com/flxxyz/douyudm +📂 简易的收藏夹功能支持 -https://github.com/BacooTang/huya-danmu +🎯 资源占用低,5开百万热度直播间、蓝光直播流转发、弹幕全开占用 `40M` 内存 + +🧬 跨平台支持,甚至可以运行在路由器上 + +🔨 支持设置 `Socks5` 代理 (未测试) -## 使用 +🧱 良好的项目结构设计,解耦直播平台和核心功能 -### 快速开始 +⚙️ 同时它也是一个简单的命令行工具。 -下载 [Release](https://baidu.com) 的最新打包文件,解压后重命名 `config.yaml.example` 为 `config.yaml` ,填写相关信息。 +...... -```sh +## 🛠️ 部署 + +### 后端 + +下载 [Release](https://baidu.com) 的最新打包文件 + +解压后重命名 `config` 目录下的 `server.yaml.example` 为 `server.yaml` , `config/account.yaml.example` 为 `account.yaml` ,填写相关信息。 + +```shell +chmod +x ./pure-live ./pure-live run ``` 打开对应的本地地址 `localhost:` ,即可看到前端界面,开始使用 `pure-live` 吧! +`pure-live` 的初衷是本地或局域网的直播流推送,对于 `websocket` 推送没有做压缩或优化处理。 + +将 `pure-live` 运行在局域网内的 `NAS` 或其他小型服务器上,就可以使整个局域网内享受到 `pure-live` 的支持。 ### 前端 +`Release` 都已经内置了默认的前端页面 + +如果前端有小BUG修复,请前往前端仓库下载最新版本替换 `static` 目录下的所有文件 前端自己快速看了一下 `Vue` 一把梭写出来的,仅仅是能用的水平,代码结构也很庞杂凌乱,期待更好的第三方前端页面出现。 +前端仓库: https://github.com/iyear/pure-live-frontend + **其他前端页面:** - ...... -### 命令行 +## ⚙️ 命令行 + +查看帮助: +```shell +./pure-live -h +./pure-live run -h +./pure-live get -h +./pure-live export -h +``` + +### run +#### 启动本地服务器 + +`-s` : 服务器配置文件路径,默认为 `config/server.yaml` + +`-a` : 账号配置文件路径,默认为 `config/account.yaml` -1. **获取直播流** +```shell +./pure-live run +./pure-live run -s myserver.yml +./pure-live run -s my/myserver.yml -a my/myaccount.yml +``` - `pure-live` 也支持命令行获取直播信息和直播流 +### get +#### 获取直播信息、直播流、弹幕流 `-p` :平台名。涉及的平台参数在 [API文档](./docs/API.md#直播平台) 中查询 -`-r` : 房间号。长短号均可。 +`-r` : 房间号。长短号均可 + +`--stream` : 下载对应的直播流(暂时只支持 `flv`),不传入则不下载,传入文件名。此方式下载的 `flv` 文件较大,如需要更精细的控制请使用 `ffmpeg` -```sh +`--danmaku` : 抓取对应的弹幕流,以 `xlsx` 格式保存,不传入则不抓取,传入文件名 + +`--roll` : 抓取弹幕是否显示弹幕滚动信息 + +```shell ./pure-live get -p bilibili -r 6 +./pure-live get -p bilibili -r 6 --stream b.flv +./pure-live get -p bilibili -r 6 --stream b.flv --danmaku dm.xlsx +./pure-live get -p bilibili -r 6 --danmaku dm.xlsx --roll +./pure-live get -p bilibili -r 6 --stream b.flv --danmaku dm.xlsx --roll ``` 成功获得相关信息 @@ -76,13 +120,45 @@ Link: https://live.bilibili.com/7734200 Stream: https://d1--cn-gotcha03.bilivideo.com/live-bvc/842331/live_50329118_9516950.flv?cdn=cn-gotch...... ``` -## 文档 +### export +#### 导出收藏及收藏夹信息 + +`-d` : 数据库路径。默认 `data/data.db` + +`-p` : 导出路径。默认 `export.xlsx` + +```shell +./pure-live export +./pure-live export -d mydata/data.db +./pure-live export -d mydata/data.db -p mydata.xlsx +``` + +## 📝 文档 如何写一个自己的前端? [API文档](./docs/API.md) 如何添加新的平台支持? [Client文档](./docs/Client.md) -## TODO +移动平台 `gomobile` 支持? [TODO]() + +## 📷 预览 + +[WEB前端预览](img/frontend) + +## 🔩 贡献 + +### ISSUE +请使用 `issue` 发起任何问题,非重要事情请勿私聊。 + +- 提出新的特性帮助 `pure-live` 成长。特性的支持效率取决于其重要程度。 +- 提出 `BUG` 解决使用中的问题。 `BUG` 的修复将优先考虑。 +- ...... + +### PR + +在 `dev` 分支签出一个自己的分支,请勿向 `master` 发起 `PR` + +## 🔌 TODO ### 基本直播功能(直播流+弹幕接收) @@ -98,3 +174,20 @@ Stream: https://d1--cn-gotcha03.bilivideo.com/live-bvc/842331/live_50329118_9516 - [x] 哔哩哔哩 - [ ] 虎牙 - [ ] 斗鱼 + +### get + +- [ ] 弹幕JSON保存 +- [ ] 弹幕ASS保存 + +## 🗒️ 参考 + +https://github.com/wbt5/real-url + +https://github.com/flxxyz/douyudm + +https://github.com/BacooTang/huya-danmu + +## 🔖 LICENSE + +GPL-3.0 License \ No newline at end of file diff --git a/api/api.go b/api/api.go deleted file mode 100644 index 42e4c0e..0000000 --- a/api/api.go +++ /dev/null @@ -1,41 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "github.com/gin-gonic/gin" - "github.com/iyear/pure-live/pkg/e" - "net/http" -) - -type H map[string]interface{} - -func RespFmt(c *gin.Context, code int, err error, data interface{}) { - type resp struct { - Code int `json:"code"` - Msg string `json:"msg"` - Data interface{} `json:"data,omitempty"` - } - - var msg = fmt.Sprintf("%s", e.GetMsg(code)) - if err != nil { - msg += fmt.Sprintf(": %s", err.Error()) - } - c.JSON(http.StatusOK, &resp{ - Code: code, - Msg: msg, - Data: data, - }) -} - -func MsgFmt(tp string, data interface{}) []byte { - type msg struct { - Type string `json:"type"` - Data interface{} `json:"data,omitempty"` - } - b, _ := json.Marshal(&msg{ - Type: tp, - Data: data, - }) - return b -} diff --git a/api/v1/live.go b/api/v1/live.go deleted file mode 100644 index c66a833..0000000 --- a/api/v1/live.go +++ /dev/null @@ -1,126 +0,0 @@ -package v1 - -import ( - "context" - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "github.com/gorilla/websocket" - "github.com/iyear/pure-live/api" - "github.com/iyear/pure-live/global" - "github.com/iyear/pure-live/pkg/client" - "github.com/iyear/pure-live/pkg/e" - "github.com/iyear/pure-live/service/srv_live" - "go.uber.org/zap" - "net/http" -) - -func Serve(c *gin.Context) { - var err error - up := websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, - } - - id := "" - for { - id = uuid.New().String() - if _, ok := global.Hub.Conn.Load(id); !ok { - break - } - } - - header := http.Header{} - cookie := http.Cookie{ - Name: "uuid", - Path: "/", - Value: id, - Secure: false, - HttpOnly: false, - } - header.Set("Set-Cookie", cookie.String()) - - cli, err := client.GetClient(c.Query("plat")) - if err != nil { - zap.S().Warnw("failed to get platform", "id", id, "error", err, "plat", c.Query("plat")) - c.Status(http.StatusBadRequest) - return - } - defer cli.Stop() - - ws := &websocket.Conn{} - if ws, err = up.Upgrade(c.Writer, c.Request, header); err != nil { - zap.S().Errorw("failed to upgrade to websocket connection", "id", id, "error", err) - return - } - defer ws.Close() - - global.Hub.Conn.Store(id, &global.Conn{ - Server: ws, - Room: c.Query("room"), - Client: cli, - }) - defer global.Hub.Conn.Delete(id) - - ctx, stop := context.WithCancel(context.WithValue(context.Background(), "id", id)) - defer stop() - - zap.S().Infow("start serving...", "id", id, "room", c.Query("room"), "plat", c.Query("plat")) - - srv_live.Serve(ctx) - - zap.S().Infow("stop serving...", "id", id, "room", c.Query("room"), "plat", c.Query("plat")) -} - -func GetPlayURL(c *gin.Context) { - req := struct { - Plat string `form:"plat" binding:"required" json:"plat"` - Room string `form:"room" binding:"required" json:"room"` - }{} - if err := c.ShouldBind(&req); err != nil { - api.RespFmt(c, e.InvalidParams, nil, nil) - return - } - url, err := srv_live.GetPlayURL(req.Plat, req.Room) - if err != nil { - api.RespFmt(c, e.ErrorGetPlayURL, err, nil) - zap.S().Warnw("failed to get play url", "error", err, "req", req) - return - } - api.RespFmt(c, e.Success, nil, url) -} -func GetRoomInfo(c *gin.Context) { - req := struct { - Plat string `form:"plat" binding:"required" json:"plat"` - Room string `form:"room" binding:"required" json:"room"` - }{} - if err := c.ShouldBind(&req); err != nil { - api.RespFmt(c, e.InvalidParams, nil, nil) - return - } - info, err := srv_live.GetRoomInfo(req.Plat, req.Room) - if err != nil { - api.RespFmt(c, e.ErrorGetRoomInfo, err, nil) - zap.S().Warnw("failed to get room info", "error", err, "req", req) - return - } - api.RespFmt(c, e.Success, nil, info) -} -func SendDanmaku(c *gin.Context) { - req := struct { - ID string `form:"id" binding:"required,uuid"` // 服务端分发的uuid - Content string `form:"content" binding:"required" json:"content"` - Type int `form:"type,default=0" binding:"gte=0,lte=2" json:"type"` // 1:顶部 0:滚动 2:底部 - Color int64 `form:"color,default=16777215" json:"color"` - }{} - if err := c.ShouldBind(&req); err != nil { - api.RespFmt(c, e.InvalidParams, nil, nil) - return - } - if err := srv_live.SendDanmaku(req.ID, req.Content, req.Type, req.Color); err != nil { - zap.S().Warnw("failed to send danmaku", "error", err, "req", req) - api.RespFmt(c, e.ErrorSendDanmaku, err, nil) - return - } - api.RespFmt(c, e.Success, nil, nil) -} diff --git a/app/export/export.go b/app/export/export.go new file mode 100644 index 0000000..6941c7d --- /dev/null +++ b/app/export/export.go @@ -0,0 +1,113 @@ +package export + +import ( + "fmt" + "github.com/fatih/color" + "github.com/iyear/pure-live/model" + "github.com/iyear/pure-live/pkg/db" + "github.com/iyear/pure-live/pkg/util" + "github.com/xuri/excelize/v2" + "time" +) + +func Export(dbPath string, savePath string) { + if !util.FileExists(dbPath) { + color.Red("[ERROR] %s is not exists", dbPath) + return + } + + if util.FileExists(savePath) { + color.Yellow("[WARN] file %s already exists, so skip exporting", savePath) + return + } + + sqlite, err := db.Init(dbPath) + if err != nil { + color.Red("[ERROR] failed to init db.ERROR: %s", err) + return + } + + var lists []*model.FavoritesList + + if err = sqlite.Find(&lists).Error; err != nil { + color.Red("[ERROR] failed to get fav lists.ERROR: %s", err) + return + } + + file := excelize.NewFile() + for _, list := range lists { + file.NewSheet(list.Title) + writer, err := file.NewStreamWriter(list.Title) + if err != nil { + color.Red("[ERROR] failed to create new excel writer.ERROR: %s", err) + return + } + + _ = writer.SetRow("A1", []interface{}{ + excelize.Cell{Value: fmt.Sprintf("ID: %d", list.ID)}, + excelize.Cell{Value: fmt.Sprintf("标题: %s", list.Title)}, + excelize.Cell{Value: fmt.Sprintf("排序: %d", list.Order)}, + excelize.Cell{Value: fmt.Sprintf("创建时间: %s", time.Unix(list.CreatedAt, 0).Format("2006-01-02 15:04:05"))}, + excelize.Cell{Value: fmt.Sprintf("最后更新: %s", time.Unix(list.UpdatedAt, 0).Format("2006-01-02 15:04:05"))}, + }) + + _ = writer.SetRow("A2", []interface{}{}) + + _ = writer.SetRow("A3", []interface{}{ + excelize.Cell{Value: "ID"}, + excelize.Cell{Value: "平台"}, + excelize.Cell{Value: "房间号"}, + excelize.Cell{Value: "主播"}, + excelize.Cell{Value: "排序"}, + excelize.Cell{Value: "创建时间"}, + excelize.Cell{Value: "最后更新"}, + }) + + var favs []*model.Favorite + if err = sqlite.Where("fid = ?", list.ID).Find(&favs).Error; err != nil { + color.Red("[ERROR] failed to get favs.ERROR: %s", err) + return + } + + if err = writeFavs(writer, favs); err != nil { + color.Red("[ERROR] failed to write favs.ERROR: %s", err) + return + } + + if err = writer.Flush(); err != nil { + color.Red("[ERROR] failed to flush excel writer.ERROR: %s", err) + return + } + } + + file.DeleteSheet("Sheet1") + if err := file.SaveAs(savePath); err != nil { + color.Red("[ERROR] failed to save file.ERROR: %s", err) + return + } + +} + +func writeFavs(writer *excelize.StreamWriter, favs []*model.Favorite) error { + var err error + count := 4 // 前面已经有3行了 + for _, fav := range favs { + cell := "" + if cell, err = excelize.CoordinatesToCellName(1, count); err != nil { + return err + } + if err = writer.SetRow(cell, []interface{}{ + excelize.Cell{Value: fav.ID}, + excelize.Cell{Value: util.Plat2Desc(fav.Plat)}, + excelize.Cell{Value: fav.Room}, + excelize.Cell{Value: fav.Upper}, + excelize.Cell{Value: fav.Order}, + excelize.Cell{Value: time.Unix(fav.CreatedAt, 0).Format("2006-01-02 15:04:05")}, + excelize.Cell{Value: time.Unix(fav.UpdatedAt, 0).Format("2006-01-02 15:04:05")}, + }); err != nil { + return err + } + count++ + } + return nil +} diff --git a/app/get/get.go b/app/get/get.go new file mode 100644 index 0000000..cb99014 --- /dev/null +++ b/app/get/get.go @@ -0,0 +1,193 @@ +package get + +import ( + "context" + "fmt" + "github.com/fatih/color" + "github.com/google/uuid" + "github.com/gorilla/websocket" + "github.com/iyear/pure-live/model" + "github.com/iyear/pure-live/pkg/client" + "github.com/iyear/pure-live/pkg/conf" + "github.com/iyear/pure-live/pkg/forwarder" + "github.com/iyear/pure-live/pkg/util" + "github.com/iyear/pure-live/service/srv_live" + "github.com/q191201771/lal/pkg/httpflv" + "github.com/q191201771/naza/pkg/nazalog" + "github.com/xuri/excelize/v2" + "os" + "os/signal" + "sync" + "time" +) + +func Get(plat, room, stream, danmaku string, roll bool) { + info, err := srv_live.GetRoomInfo(plat, room) + if err != nil { + color.Red("[ERROR] can't get room info: %s\n", err) + return + } + + if info.Status == 0 { + color.Yellow("[WARN] room is not online,so can't get the stream\n") + infoOutput(info) + return + } + + room = info.Room + + url, err := srv_live.GetPlayURL(plat, info.Room) + if err != nil { + color.Red("[ERROR] can't get room info: %s\n", err) + return + } + infoOutput(info) + _, _ = fmt.Fprintf(color.Output, "Stream: %s\n", color.New(color.FgBlue).SprintFunc()(url.Origin)) + + color.Yellow("\n[WARN] Ctrl + C to finish downloading\n") + ctx, stop := context.WithCancel(context.Background()) + + wg := sync.WaitGroup{} + if stream != "" { + wg.Add(1) + go func() { + defer wg.Done() + if err := dlStream(ctx, url.Type, url.Origin, stream); err != nil { + color.Red("[ERROR] can't download stream: %s\n", err) + return + } + }() + } + + if danmaku != "" { + wg.Add(1) + go func() { + defer wg.Done() + if err := dlDanmaku(ctx, plat, room, danmaku, roll); err != nil { + color.Red("[ERROR] can't download danmaku: %s\n", err) + return + } + }() + } + + sig := make(chan os.Signal) + signal.Notify(sig, os.Interrupt, os.Kill) + <-sig + stop() + wg.Wait() +} + +func infoOutput(info *model.RoomInfo) { + blue := color.New(color.FgBlue).SprintFunc() + _, _ = fmt.Fprintf(color.Output, "Room: %s\nUpper: %s\nTitle: %s\nLink: %s\n", + blue(info.Room), + blue(info.Upper), + blue(info.Title), + blue(info.Link)) +} + +func dlStream(ctx context.Context, tp string, url string, path string) error { + if util.FileExists(path) { + color.Yellow("[WARN] file %s already exists, so skip stream downloading\n", path) + return nil + } + + // 关闭nazalog的输出 + _ = nazalog.Init(func(option *nazalog.Option) { + option.IsToStdout = false + }) + + writer := httpflv.FlvFileWriter{} + + if err := writer.Open(path); err != nil { + return err + } + + if err := writer.WriteFlvHeader(); err != nil { + return err + } + + err := forwarder.Pull(forwarder.GetIn(tp), url, func(tag httpflv.Tag) { + _ = writer.WriteTag(tag) + }) + if err != nil { + return err + } + + <-ctx.Done() + + if err = writer.Dispose(); err != nil { + return err + } + color.Blue("[INFO] Download Live Stream Succ...\n") + return nil +} + +func dlDanmaku(ctx context.Context, plat, room, path string, roll bool) error { + if util.FileExists(path) { + color.Yellow("[WARN] file %s already exists, so skip danmaku downloading\n", path) + return nil + } + + cli, err := client.GetClient(plat) + if err != nil { + return err + } + defer cli.Stop() + + rev, err := srv_live.Serve(ctx, websocket.DefaultDialer, uuid.New().String(), cli, room) + if err != nil { + return err + } + + file := excelize.NewFile() + file.NewSheet("弹幕") + file.DeleteSheet("Sheet1") + writer, err := file.NewStreamWriter("弹幕") + if err != nil { + return err + } + + _ = writer.SetRow("A1", []interface{}{ + excelize.Cell{Value: "内容"}, + excelize.Cell{Value: "颜色(十进制)"}, + excelize.Cell{Value: "位置"}, + excelize.Cell{Value: "时间"}, + }) + + count := 2 + for { + select { + case <-ctx.Done(): + if err = writer.Flush(); err != nil { + return err + } + if err = file.SaveAs(path); err != nil { + return err + } + color.Blue("[INFO] Download Live Danmaku Succ...\n") + return nil + case transport := <-rev: + if transport.Msg.Event() != conf.EventDanmaku { + continue + } + dm := transport.Msg.(*model.MsgDanmaku) + if roll { + color.Cyan("%s %d\n", dm.Content, dm.Color) + } + cell, err := excelize.CoordinatesToCellName(1, count) + if err != nil { + return err + } + if err = writer.SetRow(cell, []interface{}{ + excelize.Cell{Value: dm.Content}, + excelize.Cell{Value: dm.Color}, + excelize.Cell{Value: util.DmMode2Desc(dm.Type)}, + excelize.Cell{Value: time.Now().Format("2006-01-02 15:04:05")}, + }); err != nil { + return err + } + count++ + } + } +} diff --git a/api/v1/fav.go b/app/server/internal/api/v1/fav.go similarity index 74% rename from api/v1/fav.go rename to app/server/internal/api/v1/fav.go index 0deab23..9907f60 100644 --- a/api/v1/fav.go +++ b/app/server/internal/api/v1/fav.go @@ -2,9 +2,9 @@ package v1 import ( "github.com/gin-gonic/gin" - "github.com/iyear/pure-live/api" "github.com/iyear/pure-live/model" - "github.com/iyear/pure-live/pkg/e" + "github.com/iyear/pure-live/pkg/ecode" + "github.com/iyear/pure-live/pkg/format" "github.com/iyear/pure-live/service/srv_fav" "go.uber.org/zap" ) @@ -15,40 +15,40 @@ func AddFavList(c *gin.Context) { Order int `form:"order" binding:"required,gte=0,lte=100" json:"order"` }{} if err := c.ShouldBind(&req); err != nil { - api.RespFmt(c, e.InvalidParams, err, nil) + format.HTTP(c, ecode.InvalidParams, err, nil) return } result, err := srv_fav.AddFavList(req.Title, req.Order) if err != nil { - api.RespFmt(c, e.ErrorAddFavList, err, nil) + format.HTTP(c, ecode.ErrorAddFavList, err, nil) zap.S().Warnw("failed to add fav list", "error", err, "req", req) return } - api.RespFmt(c, e.Success, nil, result) + format.HTTP(c, ecode.Success, nil, result) } func GetAllFavLists(c *gin.Context) { result, err := srv_fav.GetAllFavLists() if err != nil { - api.RespFmt(c, e.ErrorGetAllFavList, err, nil) + format.HTTP(c, ecode.ErrorGetAllFavList, err, nil) zap.S().Warnw("failed to get all fav lists", "error", err) return } - api.RespFmt(c, e.Success, nil, result) + format.HTTP(c, ecode.Success, nil, result) } func DelFavList(c *gin.Context) { req := struct { ID uint64 `form:"id" binding:"required" json:"id"` }{} if err := c.ShouldBind(&req); err != nil { - api.RespFmt(c, e.InvalidParams, err, nil) + format.HTTP(c, ecode.InvalidParams, err, nil) return } if err := srv_fav.DelFavList(req.ID); err != nil { - api.RespFmt(c, e.ErrorDelFavList, err, nil) + format.HTTP(c, ecode.ErrorDelFavList, err, nil) zap.S().Warnw("failed to del fav list", "error", err, "req", req) return } - api.RespFmt(c, e.Success, nil, nil) + format.HTTP(c, ecode.Success, nil, nil) } func EditFavList(c *gin.Context) { req := struct { @@ -58,16 +58,16 @@ func EditFavList(c *gin.Context) { }{} if err := c.ShouldBind(&req); err != nil { - api.RespFmt(c, e.InvalidParams, err, nil) + format.HTTP(c, ecode.InvalidParams, err, nil) return } r, err := srv_fav.EditFavList(req.ID, req.Title, req.Order) if err != nil { - api.RespFmt(c, e.ErrorEditFavList, err, nil) + format.HTTP(c, ecode.ErrorEditFavList, err, nil) zap.S().Warnw("failed to edit fav list", "error", err, "req", req) return } - api.RespFmt(c, e.Success, nil, r) + format.HTTP(c, ecode.Success, nil, r) } func GetFavList(c *gin.Context) { req := struct { @@ -75,16 +75,16 @@ func GetFavList(c *gin.Context) { }{} if err := c.ShouldBind(&req); err != nil { - api.RespFmt(c, e.InvalidParams, err, nil) + format.HTTP(c, ecode.InvalidParams, err, nil) return } list, favs, err := srv_fav.GetFavList(req.ID) if err != nil { - api.RespFmt(c, e.ErrorGetFavList, err, nil) + format.HTTP(c, ecode.ErrorGetFavList, err, nil) zap.S().Warnw("failed to get fav list", "error", err, "req", req) return } - api.RespFmt(c, e.Success, nil, &struct { + format.HTTP(c, ecode.Success, nil, &struct { *model.FavoritesList Favorites []*model.Favorite `json:"favorites"` }{FavoritesList: list, Favorites: favs}) @@ -94,17 +94,17 @@ func GetFav(c *gin.Context) { ID uint64 `form:"id" binding:"required" json:"id"` }{} if err := c.ShouldBind(&req); err != nil { - api.RespFmt(c, e.InvalidParams, err, nil) + format.HTTP(c, ecode.InvalidParams, err, nil) return } r, err := srv_fav.GetFav(req.ID) if err != nil { - api.RespFmt(c, e.ErrorGetFav, err, nil) + format.HTTP(c, ecode.ErrorGetFav, err, nil) zap.S().Warnw("failed to get fav", "error", err) return } - api.RespFmt(c, e.Success, nil, r) + format.HTTP(c, ecode.Success, nil, r) } func AddFav(c *gin.Context) { req := struct { @@ -115,16 +115,16 @@ func AddFav(c *gin.Context) { Upper string `form:"upper" binding:"required" json:"upper"` }{} if err := c.ShouldBind(&req); err != nil { - api.RespFmt(c, e.InvalidParams, err, nil) + format.HTTP(c, ecode.InvalidParams, err, nil) return } result, err := srv_fav.AddFav(req.FID, req.Order, req.Plat, req.Room, req.Upper) if err != nil { - api.RespFmt(c, e.ErrorAddFav, err, nil) + format.HTTP(c, ecode.ErrorAddFav, err, nil) zap.S().Warnw("failed to add fav", "error", err, "req", req) return } - api.RespFmt(c, e.Success, nil, result) + format.HTTP(c, ecode.Success, nil, result) } func DelFav(c *gin.Context) { req := struct { @@ -132,16 +132,16 @@ func DelFav(c *gin.Context) { }{} if err := c.ShouldBind(&req); err != nil { - api.RespFmt(c, e.InvalidParams, err, nil) + format.HTTP(c, ecode.InvalidParams, err, nil) return } if err := srv_fav.DelFav(req.ID); err != nil { - api.RespFmt(c, e.ErrorDelFav, err, nil) + format.HTTP(c, ecode.ErrorDelFav, err, nil) zap.S().Warnw("failed to del fav", "error", err) return } - api.RespFmt(c, e.Success, nil, nil) + format.HTTP(c, ecode.Success, nil, nil) } func EditFav(c *gin.Context) { req := struct { @@ -152,15 +152,15 @@ func EditFav(c *gin.Context) { Upper string `form:"upper" binding:"required,gte=0,lte=100" json:"upper"` }{} if err := c.ShouldBind(&req); err != nil { - api.RespFmt(c, e.InvalidParams, err, nil) + format.HTTP(c, ecode.InvalidParams, err, nil) return } r, err := srv_fav.EditFav(req.ID, req.Order, req.Plat, req.Room, req.Upper) if err != nil { - api.RespFmt(c, e.ErrorEditFav, err, nil) + format.HTTP(c, ecode.ErrorEditFav, err, nil) zap.S().Warnw("failed to edit fav", "error", err) return } - api.RespFmt(c, e.Success, nil, r) + format.HTTP(c, ecode.Success, nil, r) } diff --git a/app/server/internal/api/v1/live.go b/app/server/internal/api/v1/live.go new file mode 100644 index 0000000..d08c14f --- /dev/null +++ b/app/server/internal/api/v1/live.go @@ -0,0 +1,71 @@ +package v1 + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/iyear/pure-live/global" + "github.com/iyear/pure-live/pkg/ecode" + "github.com/iyear/pure-live/pkg/format" + "github.com/iyear/pure-live/service/srv_live" + "go.uber.org/zap" +) + +func GetPlayURL(c *gin.Context) { + req := struct { + Plat string `form:"plat" binding:"required,max=15" json:"plat"` + Room string `form:"room" binding:"required" json:"room"` + }{} + if err := c.ShouldBind(&req); err != nil { + format.HTTP(c, ecode.InvalidParams, nil, nil) + return + } + url, err := srv_live.GetPlayURL(req.Plat, req.Room) + if err != nil { + format.HTTP(c, ecode.ErrorGetPlayURL, err, nil) + zap.S().Warnw("failed to get play url", "error", err, "req", req) + return + } + format.HTTP(c, ecode.Success, nil, url) +} +func GetRoomInfo(c *gin.Context) { + req := struct { + Plat string `form:"plat" binding:"required,max=15" json:"plat"` + Room string `form:"room" binding:"required" json:"room"` + }{} + if err := c.ShouldBind(&req); err != nil { + format.HTTP(c, ecode.InvalidParams, nil, nil) + return + } + info, err := srv_live.GetRoomInfo(req.Plat, req.Room) + if err != nil { + format.HTTP(c, ecode.ErrorGetRoomInfo, err, nil) + zap.S().Warnw("failed to get room info", "error", err, "req", req) + return + } + format.HTTP(c, ecode.Success, nil, info) +} +func SendDanmaku(c *gin.Context) { + req := struct { + ID string `form:"id" binding:"required,uuid"` // 服务端分发的uuid + Content string `form:"content" binding:"required" json:"content"` + Type int `form:"type" binding:"gte=0,lte=2" json:"type"` // 1:顶部 0:滚动 2:底部 + Color int64 `form:"color" binding:"required" json:"color"` + }{} + if err := c.ShouldBind(&req); err != nil { + format.HTTP(c, ecode.InvalidParams, nil, nil) + return + } + + conn, err := global.GetConn(req.ID) + if err != nil { + format.HTTP(c, ecode.UnknownError, fmt.Errorf("can not get global conn"), nil) + return + } + + if err = srv_live.SendDanmaku(conn.Client, conn.Room, req.Content, req.Type, req.Color); err != nil { + zap.S().Warnw("failed to send danmaku", "error", err, "req", req) + format.HTTP(c, ecode.ErrorSendDanmaku, err, nil) + return + } + format.HTTP(c, ecode.Success, nil, nil) +} diff --git a/app/server/internal/api/v1/os.go b/app/server/internal/api/v1/os.go new file mode 100644 index 0000000..e93d7f6 --- /dev/null +++ b/app/server/internal/api/v1/os.go @@ -0,0 +1,95 @@ +package v1 + +import ( + "github.com/gin-gonic/gin" + "github.com/iyear/pure-live/model" + "github.com/iyear/pure-live/pkg/ecode" + "github.com/iyear/pure-live/pkg/format" + "github.com/iyear/pure-live/service/srv_os" +) + +func GetOSInfo(c *gin.Context) { + info, err := srv_os.GetOSInfo() + if err != nil { + format.HTTP(c, ecode.ErrorGetOSInfo, err, nil) + return + } + format.HTTP(c, ecode.Success, nil, info) +} + +func GetSysMem(c *gin.Context) { + r, err := srv_os.GetSysMem() + if err != nil { + format.HTTP(c, ecode.ErrorGetSysMem, err, nil) + return + } + format.HTTP(c, ecode.Success, nil, r) +} + +func GetSelfMem(c *gin.Context) { + r, err := srv_os.GetSelfMem() + if err != nil { + format.HTTP(c, ecode.ErrorGetSelfMem, err, nil) + return + } + format.HTTP(c, ecode.Success, nil, r) +} + +func GetSysCPU(c *gin.Context) { + r, err := srv_os.GetSysCPU() + if err != nil { + format.HTTP(c, ecode.ErrorGetSysCPU, err, nil) + return + } + format.HTTP(c, ecode.Success, nil, r) +} + +func GetSelfCPU(c *gin.Context) { + r, err := srv_os.GetSelfCPU() + if err != nil { + format.HTTP(c, ecode.ErrorGetSelfCPU, err, nil) + return + } + format.HTTP(c, ecode.Success, nil, r) +} + +func GetOSAll(c *gin.Context) { + info := &model.OSInfo{} + sysCPU := &model.SysCPU{} + selfCPU := &model.SelfCPU{} + sysMem := &model.SysMem{} + selfMem := &model.SelfMem{} + + err := func() error { + var err error + if info, err = srv_os.GetOSInfo(); err != nil { + return err + } + if sysCPU, err = srv_os.GetSysCPU(); err != nil { + return err + } + if selfCPU, err = srv_os.GetSelfCPU(); err != nil { + return err + } + if sysMem, err = srv_os.GetSysMem(); err != nil { + return err + } + if selfMem, err = srv_os.GetSelfMem(); err != nil { + return err + } + return nil + }() + + if err != nil { + format.HTTP(c, ecode.ErrorGetOsAll, err, nil) + return + } + + format.HTTP(c, ecode.Success, nil, gin.H{ + "info": info, + "sys_cpu": sysCPU, + "self_cpu": selfCPU, + "sys_mem": sysMem, + "self_mem": selfMem, + }) +} diff --git a/api/v1/play.go b/app/server/internal/api/v1/play.go similarity index 100% rename from api/v1/play.go rename to app/server/internal/api/v1/play.go diff --git a/app/server/internal/api/v1/serve.go b/app/server/internal/api/v1/serve.go new file mode 100644 index 0000000..a4e4d23 --- /dev/null +++ b/app/server/internal/api/v1/serve.go @@ -0,0 +1,123 @@ +package v1 + +import ( + "context" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/gorilla/websocket" + "github.com/iyear/pure-live/app/server/internal/config" + "github.com/iyear/pure-live/global" + "github.com/iyear/pure-live/pkg/client" + "github.com/iyear/pure-live/pkg/conf" + "github.com/iyear/pure-live/pkg/ecode" + "github.com/iyear/pure-live/pkg/format" + "github.com/iyear/pure-live/pkg/util" + "github.com/iyear/pure-live/service/srv_live" + "go.uber.org/zap" + "net/http" + "time" +) + +func Serve(c *gin.Context) { + req := struct { + Plat string `form:"plat" binding:"required,max=15" json:"plat"` + Room string `form:"room" binding:"required" json:"room"` + }{} + if err := c.ShouldBind(&req); err != nil { + format.HTTP(c, ecode.InvalidParams, err, nil) + return + } + + upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }} + id := getUniqueID() + + cli, err := client.GetClient(req.Plat) + if err != nil { + zap.S().Warnw("failed to get platform", "id", id, "error", err, "plat", req.Plat) + format.HTTP(c, ecode.UnknownError, err, nil) + return + } + defer cli.Stop() + + srvWS := &websocket.Conn{} + if srvWS, err = upgrader.Upgrade(c.Writer, c.Request, getUpgradeHeader(id)); err != nil { + zap.S().Errorw("failed to upgrade to websocket connection", "id", id, "error", err) + return + } + defer func(ws *websocket.Conn) { + _ = ws.Close() + }(srvWS) + + global.Hub.Conn.Store(id, &global.Conn{ + Server: srvWS, + Room: req.Room, + Client: cli, + }) + defer global.Hub.Conn.Delete(id) + + ctx, stop := context.WithCancel(context.Background()) + defer stop() + + zap.S().Infow("start serving...", "id", id, "room", req.Room, "plat", req.Plat) + + dialer := websocket.DefaultDialer + if config.Server.Socks5.Enable { + dialer.NetDial = util.MustGetSocks5(config.Server.Socks5.Host, config.Server.Socks5.Port, config.Server.Socks5.User, config.Server.Socks5.Password).Dial + } + + rev, err := srv_live.Serve(ctx, dialer, id, cli, req.Room) + if err != nil { + zap.S().Warnw("failed to start serve", "id", id, "error", err) + return + } + + // 开始获取数据 + // 客户端心跳检查 + health := time.NewTicker(5 * time.Second) + defer health.Stop() + defer zap.S().Infow("stop serving...", "id", id, "room", req.Room, "plat", req.Plat) + + for { + select { + // 5秒检查一次客户端存活 + case <-health.C: + if err = srvWS.WriteMessage(websocket.TextMessage, format.WS(conf.EventCheck, nil)); err != nil { + zap.S().Warnw("failed to write ws message", "id", id, "error", err) + return + } + case transport := <-rev: + if transport.Error != nil { + zap.S().Warnw("receive transport error", "id", id, "error", err) + continue + } + if err = srvWS.WriteMessage(websocket.TextMessage, format.WS(transport.Msg.Event(), transport.Msg)); err != nil { + zap.S().Warnw("failed to write ws message", "id", id, "error", err) + return + } + } + } +} + +func getUpgradeHeader(id string) http.Header { + header := http.Header{} + cookie := http.Cookie{ + Name: "uuid", + Path: "/", + Value: id, + Secure: false, + HttpOnly: false, + } + header.Set("Set-Cookie", cookie.String()) + return header +} + +func getUniqueID() string { + id := "" + for { + id = uuid.New().String() + if _, ok := global.Hub.Conn.Load(id); !ok { + break + } + } + return id +} diff --git a/app/server/internal/api/version.go b/app/server/internal/api/version.go new file mode 100644 index 0000000..49b1e4c --- /dev/null +++ b/app/server/internal/api/version.go @@ -0,0 +1,15 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "github.com/iyear/pure-live/global" + "github.com/iyear/pure-live/pkg/ecode" + "github.com/iyear/pure-live/pkg/format" +) + +func GetVersion(c *gin.Context) { + format.HTTP(c, ecode.Success, nil, gin.H{ + "ver": global.Version, + "runtime": global.GetRuntime(), + }) +} diff --git a/app/server/internal/config/config.go b/app/server/internal/config/config.go new file mode 100644 index 0000000..1ec2367 --- /dev/null +++ b/app/server/internal/config/config.go @@ -0,0 +1,35 @@ +package config + +import ( + "github.com/spf13/viper" +) + +var Server server + +func InitServer(path string) error { + c := viper.New() + c.SetConfigFile(path) + if err := c.ReadInConfig(); err != nil { + return err + } + if err := c.Unmarshal(&Server); err != nil { + return err + } + + return nil +} + +type server struct { + Port int `mapstructure:"port"` + Debug bool `mapstructure:"debug"` + Path string `mapstructure:"path"` + Socks5 socks5 `mapstructure:"socks5"` +} + +type socks5 struct { + Enable bool `mapstructure:"enable"` + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` +} diff --git a/logger/logger.go b/app/server/internal/logger/logger.go similarity index 100% rename from logger/logger.go rename to app/server/internal/logger/logger.go diff --git a/middleware/cors.go b/app/server/internal/middleware/cors.go similarity index 100% rename from middleware/cors.go rename to app/server/internal/middleware/cors.go diff --git a/middleware/recovery.go b/app/server/internal/middleware/recovery.go similarity index 100% rename from middleware/recovery.go rename to app/server/internal/middleware/recovery.go diff --git a/app/server/internal/middleware/static.go b/app/server/internal/middleware/static.go new file mode 100644 index 0000000..0f2874a --- /dev/null +++ b/app/server/internal/middleware/static.go @@ -0,0 +1,22 @@ +package middleware + +import ( + "github.com/gin-contrib/static" + "github.com/gin-gonic/gin" + "path" +) + +const ( + // StaticPath is the path to the static files + staticPath = "static" +) + +func Static() gin.HandlerFunc { + return static.Serve("/", static.LocalFile(staticPath, true)) +} + +func NoRoute() gin.HandlerFunc { + return func(c *gin.Context) { + c.File(path.Join(staticPath, "index.html")) + } +} diff --git a/router/router.go b/app/server/internal/router/router.go similarity index 53% rename from router/router.go rename to app/server/internal/router/router.go index 06960fc..af0cfc2 100644 --- a/router/router.go +++ b/app/server/internal/router/router.go @@ -2,23 +2,28 @@ package router import ( "github.com/gin-gonic/gin" - "github.com/iyear/pure-live/api/v1" - "github.com/iyear/pure-live/conf" - "github.com/iyear/pure-live/middleware" - "github.com/iyear/pure-live/util" + "github.com/iyear/pure-live/app/server/internal/api" + "github.com/iyear/pure-live/app/server/internal/api/v1" + "github.com/iyear/pure-live/app/server/internal/config" + "github.com/iyear/pure-live/app/server/internal/middleware" + "github.com/iyear/pure-live/pkg/util" ) var r *gin.Engine func Init() *gin.Engine { - gin.SetMode(util.IF(conf.C.Server.Debug, gin.DebugMode, gin.ReleaseMode).(string)) + gin.SetMode(util.IF(config.Server.Debug, gin.DebugMode, gin.ReleaseMode).(string)) r = gin.New() r.Use(middleware.Recovery()) r.Use(middleware.CORS()) r.Use(middleware.Static()) + // SPA需要设置此中间件,将404重新返回单页面入口,vue-router便会再次重定向回对应uri的页面 + r.NoRoute(middleware.NoRoute()) g := r.Group("/api") + g.GET("/version", api.GetVersion) + apiV1 := g.Group("/v1") { apiV1.GET("/live/serve", v1.Serve) @@ -37,6 +42,13 @@ func Init() *gin.Engine { apiV1.POST("/fav/add", v1.AddFav) apiV1.POST("/fav/del", v1.DelFav) apiV1.POST("/fav/edit", v1.EditFav) + + apiV1.GET("/os/info", v1.GetOSInfo) + apiV1.GET("/os/all", v1.GetOSAll) + apiV1.GET("/os/mem/sys", v1.GetSysMem) + apiV1.GET("/os/mem/self", v1.GetSelfMem) + apiV1.GET("/os/cpu/sys", v1.GetSysCPU) + apiV1.GET("/os/cpu/self", v1.GetSelfCPU) } return r diff --git a/app/server/server.go b/app/server/server.go new file mode 100644 index 0000000..0d25295 --- /dev/null +++ b/app/server/server.go @@ -0,0 +1,86 @@ +package server + +import ( + "context" + "fmt" + "github.com/iyear/pure-live/app/server/internal/config" + "github.com/iyear/pure-live/app/server/internal/logger" + "github.com/iyear/pure-live/app/server/internal/router" + "github.com/iyear/pure-live/global" + "github.com/iyear/pure-live/pkg/conf" + "github.com/iyear/pure-live/pkg/db" + "github.com/iyear/pure-live/pkg/request" + "github.com/iyear/pure-live/pkg/util" + "github.com/q191201771/naza/pkg/nazalog" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "log" + "net/http" + "os" + "os/signal" + "path" + "syscall" + "time" +) + +func Run(serverConf string, accountConf string) { + // lal包中的nazalog + _ = nazalog.Init(func(option *nazalog.Option) { + option.IsToStdout = config.Server.Debug + }) + + if err := config.InitServer(serverConf); err != nil { + log.Fatalf("failed to read server config: %s", err) + } + if err := conf.InitAccount(accountConf); err != nil { + log.Fatalf("failed to read account config: %s", err) + } + + logger.Init(util.IF(config.Server.Debug, zapcore.DebugLevel, zapcore.InfoLevel).(zapcore.LevelEnabler)) + + zap.S().Infof("read config succ...") + + if err := os.MkdirAll(config.Server.Path, 0774); err != nil { + zap.S().Fatalw("failed to mkdir", "error", err) + } + + sqlite, err := db.Init(path.Join(config.Server.Path, "data.db")) + if err != nil { + zap.S().Fatalw("failed to init database", "error", err) + } + global.DB = sqlite + zap.S().Infof("init database succ...") + + if config.Server.Socks5.Enable { + request.SetSocks5(config.Server.Socks5.Host, config.Server.Socks5.Port, config.Server.Socks5.User, config.Server.Socks5.Password) + } + + zap.S().Infof("server runs on :%d,debug: %v", config.Server.Port, config.Server.Debug) + + handler := router.Init() + + s := &http.Server{ + Addr: fmt.Sprintf(":%d", config.Server.Port), + Handler: handler, + } + + go func() { + if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { + zap.S().Infow("failed to start to listen and serve", "error", err, "port", config.Server.Port) + } + }() + + quit := make(chan os.Signal) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + zap.S().Info("shutdown server...") + + ctx, stop := context.WithTimeout(context.Background(), 5*time.Second) + defer stop() + + if err = s.Shutdown(ctx); err != nil { + zap.S().Fatalw("server forced to shutdown", "error", err) + } + + zap.S().Infow("server exited...") +} diff --git a/cmd/export.go b/cmd/export.go new file mode 100644 index 0000000..3cbd8aa --- /dev/null +++ b/cmd/export.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "github.com/iyear/pure-live/app/export" + "github.com/spf13/cobra" +) + +var dbPath string +var savePath string + +// exportCmd represents the run command +var exportCmd = &cobra.Command{ + Use: "export", + Short: "Export Favorites And Favorites List Information", + Long: `Export Favorites And Favorites List Information`, + Run: func(cmd *cobra.Command, args []string) { + export.Export(dbPath, savePath) + }, +} + +func init() { + rootCmd.AddCommand(exportCmd) + + exportCmd.PersistentFlags().StringVarP(&dbPath, "db", "d", "data/data.db", "the path to data.db") + exportCmd.PersistentFlags().StringVarP(&savePath, "path", "p", "export.xlsx", "the path to savePath") +} diff --git a/cmd/get.go b/cmd/get.go index aae914a..9ede223 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -1,25 +1,25 @@ package cmd import ( - "fmt" - "github.com/fatih/color" - "github.com/iyear/pure-live/model" - "github.com/iyear/pure-live/service/srv_live" + "github.com/iyear/pure-live/app/get" "github.com/spf13/cobra" ) var ( - plat string - room string + plat string + room string + stream string + danmaku string + roll bool ) // getCmd represents the get command var getCmd = &cobra.Command{ Use: "get", - Short: "get info", - Long: `get info`, + Short: "Get live info", + Long: `Get live information, live stream, and danmaku stream`, Run: func(cmd *cobra.Command, args []string) { - get() + get.Get(plat, room, stream, danmaku, roll) }, } @@ -27,36 +27,8 @@ func init() { rootCmd.AddCommand(getCmd) getCmd.PersistentFlags().StringVarP(&plat, "plat", "p", "bilibili", "live platform") - getCmd.PersistentFlags().StringVarP(&room, "room", "r", "6", "live room") - -} - -func get() { - info, err := srv_live.GetRoomInfo(plat, room) - if err != nil { - color.Red("[ERROR] can't get room info: %s", err) - return - } - - if info.Status == 0 { - color.Yellow("[WARN] room is not online,so can't get the stream") - infoOutput(info) - return - } - url, err := srv_live.GetPlayURL(plat, info.Room) - if err != nil { - color.Red("[ERROR] can't get room info: %s", err) - return - } - infoOutput(info) - _, _ = fmt.Fprintf(color.Output, "Stream: %s", color.New(color.FgBlue).SprintFunc()(url.Origin)) -} - -func infoOutput(info *model.RoomInfo) { - blue := color.New(color.FgBlue).SprintFunc() - _, _ = fmt.Fprintf(color.Output, "Room: %s\nUpper: %s\nTitle: %s\nLink: %s\n", - blue(info.Room), - blue(info.Upper), - blue(info.Title), - blue(info.Link)) + getCmd.PersistentFlags().StringVarP(&room, "room", "r", "6", "live room,it can be short string or real string") + getCmd.PersistentFlags().StringVar(&stream, "stream", "", "download live stream to .flv file") + getCmd.PersistentFlags().StringVar(&danmaku, "danmaku", "", "download live danmaku to .xlsx file") + getCmd.PersistentFlags().BoolVar(&roll, "roll", false, "display danmaku content and scroll") } diff --git a/cmd/root.go b/cmd/root.go index ebc4662..9bbefb3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,18 +1,34 @@ package cmd import ( + "github.com/fatih/color" + "github.com/iyear/pure-live/global" "github.com/spf13/cobra" ) // TODO write cmd usages +var version bool + // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "pure-live", - Short: "pure-live", - Long: `pure-live`, + Short: "Make Live Pure Again", + Long: `Make Live Pure Again. +No gift, fan group, pop-up window, only live, danmaku. +`, + Run: func(cmd *cobra.Command, args []string) { + if version { + color.Blue("%s\n%s", global.Version, global.GetRuntime()) + } + }, +} + +func init() { + rootCmd.PersistentFlags().BoolVarP(&version, "version", "v", false, "check the version of pure-live") } func Execute() { cobra.CheckErr(rootCmd.Execute()) + } diff --git a/cmd/run.go b/cmd/run.go index 96d3693..afb9927 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -1,25 +1,28 @@ package cmd import ( - "github.com/iyear/pure-live/server" + "github.com/iyear/pure-live/app/server" "github.com/spf13/cobra" ) -var cfgFile string +var ( + serverCfg string + accountCfg string +) // runCmd represents the run command var runCmd = &cobra.Command{ Use: "run", - Short: "run the server", - Long: `run the server locally`, + Short: "Start the local server", + Long: `Start the local server`, Run: func(cmd *cobra.Command, args []string) { - server.Run(cfgFile) + server.Run(serverCfg, accountCfg) }, } func init() { rootCmd.AddCommand(runCmd) - runCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "config.yaml", "config file (default is ./config.yaml)") - + runCmd.PersistentFlags().StringVarP(&serverCfg, "server", "s", "config/server.yaml", "server config file") + runCmd.PersistentFlags().StringVarP(&accountCfg, "account", "a", "config/account.yaml", "account config file") } diff --git a/conf/conf.go b/conf/conf.go deleted file mode 100644 index 9c17ef5..0000000 --- a/conf/conf.go +++ /dev/null @@ -1,26 +0,0 @@ -package conf - -import ( - "github.com/spf13/viper" - "go.uber.org/zap" - "log" -) - -var C config - -func Init(file string) { - c := viper.New() - c.SetConfigFile(file) - // If a config file is found, read it in. - if err := c.ReadInConfig(); err != nil { - log.Fatalf("cannot read in config, file: %s", c.ConfigFileUsed()) - return - } - - if err := c.Unmarshal(&C); err != nil { - zap.S().Fatalw("cannot unmarshal config", "file", c.ConfigFileUsed(), "error", err) - return - } - - zap.S().Infof("read in config succ...") -} diff --git a/config.yaml.example b/config.yaml.example deleted file mode 100644 index a841c21..0000000 --- a/config.yaml.example +++ /dev/null @@ -1,23 +0,0 @@ -server: - port: 8800 # 本地监听端口 - debug: false # 是否开启Debug模式 - path: ./data # 数据存储目录 -socks5: - enable: false # 是否启用socks5代理 - host: 127.0.0.1 # socks5相关信息 - port: 1080 - user: - password: -account: # 平台账号信息,用于发送弹幕,如果不需要发送弹幕功能可以不用填写以下所有内容 - bilibili: - # 是否启用bilibili账号 - enable: true - # 以下为b站的Cookies - DedeUserID: xxx - DedeUserIDCkMd5: xxx - SESSDATA: xxx - BiliJCT: xxx - huya: - enable: false - douyu: - enable: false \ No newline at end of file diff --git a/config/account.yaml.example b/config/account.yaml.example new file mode 100644 index 0000000..7a33bd0 --- /dev/null +++ b/config/account.yaml.example @@ -0,0 +1,12 @@ +bilibili: + # 是否启用bilibili账号 + enable: true + # 以下为b站的Cookies + DedeUserID: xxx + DedeUserIDCkMd5: xxx + SESSDATA: xxx + BiliJCT: xxx +huya: + enable: false +douyu: + enable: false \ No newline at end of file diff --git a/config/server.yaml.example b/config/server.yaml.example new file mode 100644 index 0000000..e1de1db --- /dev/null +++ b/config/server.yaml.example @@ -0,0 +1,9 @@ +port: 8800 # 本地监听端口 +debug: false # 是否开启Debug模式 +path: ./data # 数据存储目录 +socks5: + enable: false # 是否启用socks5代理 + host: 127.0.0.1 # socks5相关信息 + port: 1080 + user: + password: \ No newline at end of file diff --git a/db/db.go b/db/db.go deleted file mode 100644 index 5099164..0000000 --- a/db/db.go +++ /dev/null @@ -1,39 +0,0 @@ -package db - -import ( - "github.com/iyear/pure-live/conf" - "github.com/iyear/pure-live/model" - "github.com/iyear/sqlite" - "gorm.io/gorm" - "path" - "time" -) - -var SQLite *gorm.DB - -func Init() error { - var err error - SQLite, err = gorm.Open(sqlite.Open(path.Join(conf.C.Server.Path, "data.db")), &gorm.Config{ - DisableForeignKeyConstraintWhenMigrating: true, - NowFunc: func() time.Time { - return time.Now().UTC() - }, - }) - if err != nil { - return err - } - - if err = SQLite.AutoMigrate(&model.FavoritesList{}, &model.Favorite{}); err != nil { - return err - } - - // 创建默认收藏夹 - if err = SQLite.FirstOrCreate(&model.FavoritesList{}, &model.FavoritesList{ - ID: 1, - Title: "默认收藏夹", - Order: 0, - }).Error; err != nil { - return err - } - return nil -} diff --git a/docs/API.md b/docs/API.md index e3327db..65c9af2 100644 --- a/docs/API.md +++ b/docs/API.md @@ -12,6 +12,29 @@ | huya | 虎牙 | | douyu | 斗鱼 | +## 参数说明 + +所有参数后端没有默认值,即前端均需传入一个确定的值 + +## 版本检查 + +### GetVersion 获取core版本信息 + +> GET /api/version + +**Response:** + +```json +{ + "code": 0, + "msg": "ok", + "data": { + "runtime": "go1.17.3 windows/amd64", + "ver": "v0.1.0" + } +} +``` + ## 直播信息类 @@ -36,13 +59,14 @@ "msg": "ok", "data": { "status": 1, - "room": "763679", + "room": "763679", "upper": "老骚豆腐", - "link": "https://live.bilibili.com/763679", - "title": "【豆腐】杀鸡+第五+躲猫猫!新游!" + "link": "https://live.bilibili.com/763679", + "title": "【豆腐】杀鸡+第五+躲猫猫!新游!" } } ``` + status 开播情况 0:未开播 1:开播 room 真实房间号 @@ -83,19 +107,20 @@ title 直播间标题 } } ``` + qn 清晰度,暂时无切换清晰度功能 desc 清晰度描述,暂时无切换清晰度功能 origin 直播流地址 -cors 是否有跨域问题 true:有 false:无 若为true必须通过本地流量转发才能播放,若为false直接播放或本地转发均可 +cors 是否有跨域限制 true:有 false:无 若为true必须通过本地流量转发才能播放,若为false直接播放或本地转发均可 type 直播流编码格式 ### Play 直播流本地转发 -一些直播流开启了防盗链,获取的直播流无法直接在播放器内加载,所以 `core` 提供了本地的流量转发功能。 +一些直播流开启了跨域限制,获取的直播流无法直接在前端加载,所以 `core` 提供了本地的流量转发功能。 > GET /api/v1/live/play @@ -156,7 +181,8 @@ type 直播流编码格式 } } ``` -content 弹幕内容 + + content 弹幕内容 type 0:右侧飞行弹幕 1:顶部弹幕 2:底部弹幕 @@ -174,7 +200,8 @@ color 弹幕十进制颜色 } } ``` -hot 热度值 + + hot 热度值 - 心跳检测(`check`) @@ -247,10 +274,7 @@ hot 热度值 请求示例:`/api/v1/fav/list/add` ```json -{ - "title": "测试收藏夹", - "order": 30 -} +{ "title": "测试收藏夹", "order": 30} ``` **Response:** @@ -586,3 +610,134 @@ hot 热度值 } ``` +## 资源监控类 + +### GetOSInfo 获取系统信息 + +> GET /api/v1/os/info + +**Response:** + +```json +{ + "code": 0, + "msg": "ok", + "data": { + "uptime": 75898, + "os": "windows", + "platform": "Microsoft Windows 10 Home China", + "platform_version": "10.0.19042 Build 19042", + "kernel_version": "10.0.19042 Build 19042", + "kernel_arch": "x86_64" + } +} +``` + +### GetSysMem 获取系统内存占用 + +> GET /api/v1/os/mem/sys + +**Response:** + +```json +{ + "code": 0, + "msg": "ok", + "data": { + "total": 16487870464, + "total_str": "15.36 GB", + "avl": 3288862720, + "avl_str": "3.06 GB" + } +} +``` + +### GetSelfMem 获取自身内存占用 + +> GET /api/v1/os/mem/self + +**Response:** + +```json +{ + "code": 0, + "msg": "ok", + "data": { + "mem": 22511616, + "mem_str": "21.47 MB" + } +} +``` + +### GetSysCPU 获取系统CPU占用 + +> GET /api/v1/os/cpu/sys + +**Response:** + +```json +{ + "code": 0, + "msg": "ok", + "data": { + "percent": 5.88235294117647 + } +} +``` + +### GetSelfCPU 获取自身CPU占用 + +> GET /api/v1/os/cpu/self + +**Response:** + +```json +{ + "code": 0, + "msg": "ok", + "data": { + "percent": 0.040586327084460326 + } +} +``` + +### GetOSAll 获取全部信息 + +> GET /api/v1/os/all + +**Response:** + +```json +{ + "code": 0, + "msg": "ok", + "data": { + "info": { + "uptime": 76138, + "os": "windows", + "platform": "Microsoft Windows 10 Home China", + "platform_version": "10.0.19042 Build 19042", + "kernel_version": "10.0.19042 Build 19042", + "kernel_arch": "x86_64" + }, + "self_cpu": { + "percent": 0.038924943854536424 + }, + "self_mem": { + "mem": 22695936, + "mem_str": "21.64 MB" + }, + "sys_cpu": { + "percent": 3.125 + }, + "sys_mem": { + "total": 16487870464, + "total_str": "15.36 GB", + "avl": 3263381504, + "avl_str": "3.04 GB" + } + } +} +``` + +## \ No newline at end of file diff --git a/docs/Client.md b/docs/Client.md index e69de29..15e2dc8 100644 --- a/docs/Client.md +++ b/docs/Client.md @@ -0,0 +1,31 @@ +## 如何新增一个Client? + +首先你需要实现以下接口: +```go +type Client interface { + Plat() string + // GetPlayURL qn传入 conf.QnBest conf.QnHigh conf.QnMid conf.QnLow + GetPlayURL(room string, qn int) (*PlayURL, error) + // GetRoomInfo room可以为短号也可以为长号 + GetRoomInfo(room string) (*RoomInfo, error) + // Host ws host + Host() string + // Enter 一次可以返回多条消息,hub将按顺序依次发送,用于需要一次发送多条进入直播间消息的场景 + Enter(room string) (tp int, data [][]byte, err error) + // Handle matched为是否读取msg的操作,用于跳过不想匹配的消息,err为错误,先判断错误再判断matched + Handle(tp int, data []byte) (msg []Msg, matched bool, err error) + HeartBeat() (tp int, data []byte, err error) + // SendDanmaku tp 1:top 0:right 2:bottom color:十进制颜色值 + SendDanmaku(room string, content string, tp int, color int64) error + // Stop 释放内部资源 + Stop() +} +``` + +- 所有的 `tp` 为 `Websocket Message Type`,使用 `websocket.xxxMessage` 传入 + +具体参考已有的Client写法 + +1. 新增后需要在 `/conf/const.go` 新增平台名常量 +2. 在 `/docs/API.md` 中新增平台的说明 +3. 需要在 `/pkg/client/plat.go` 中新增一个 `Client` 的实现 diff --git a/global/db.go b/global/db.go new file mode 100644 index 0000000..12d2f36 --- /dev/null +++ b/global/db.go @@ -0,0 +1,5 @@ +package global + +import "gorm.io/gorm" + +var DB *gorm.DB diff --git a/global/version.go b/global/version.go new file mode 100644 index 0000000..bdd1437 --- /dev/null +++ b/global/version.go @@ -0,0 +1,14 @@ +package global + +import ( + "fmt" + "runtime" +) + +const ( + Version = "v0.1.0.211224-beta" +) + +func GetRuntime() string { + return fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH) +} diff --git a/go.mod b/go.mod index c711de4..cbb137d 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/TarsCloud/TarsGo v1.1.6 github.com/andybalholm/brotli v1.0.4 github.com/dop251/goja v0.0.0-20211115154819-26ebff68a7d5 - github.com/fatih/color v1.13.0 // indirect - github.com/gin-contrib/cors v1.3.1 // indirect + github.com/fatih/color v1.13.0 + github.com/gin-contrib/cors v1.3.1 github.com/gin-contrib/static v0.0.1 github.com/gin-gonic/gin v1.7.4 github.com/go-playground/validator/v10 v10.9.0 // indirect @@ -19,14 +19,15 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/q191201771/lal v0.26.1-0.20211127051713-e208a3ac3635 - github.com/q191201771/naza v0.28.0 // indirect + github.com/q191201771/naza v0.28.0 + github.com/shirou/gopsutil/v3 v3.21.11 github.com/spf13/cobra v1.2.1 github.com/spf13/viper v1.9.0 github.com/tidwall/gjson v1.11.0 - github.com/ugorji/go v1.2.6 // indirect + github.com/xuri/excelize/v2 v2.4.1 go.uber.org/zap v1.17.0 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect - golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect + golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d golang.org/x/text v0.3.7 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 gorm.io/gorm v1.22.3 diff --git a/go.sum b/go.sum index 69aed6c..6610047 100644 --- a/go.sum +++ b/go.sum @@ -125,6 +125,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= @@ -302,6 +304,8 @@ github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdA github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= @@ -341,6 +345,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -364,25 +370,21 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/q191201771/lal v0.26.0 h1:Y+vD9PYF2ASv6JHavRsJJDAZP3xXRTKOLU4PIY+rT7g= -github.com/q191201771/lal v0.26.0/go.mod h1:IE1CEODMX2lS5ZMEc1ealjX51vJXcO5XdqoL9AyZkXs= github.com/q191201771/lal v0.26.1-0.20211127051713-e208a3ac3635 h1:cKg0Avhr1u/kNUhzhZii7B0I5AIhh9Xi8EMUz7GQZcc= github.com/q191201771/lal v0.26.1-0.20211127051713-e208a3ac3635/go.mod h1:wlP9cYehXhSLIx6OhAUwQ2yiQsSBWhWEHYwVL9DQbVc= -github.com/q191201771/naza v0.22.0 h1:T/z0sxBnoXP9wQ5c0YSqzxBvUIrktPllIsUVWRwwwlo= -github.com/q191201771/naza v0.22.0/go.mod h1:5LeGupZZFtYP1g/S203n9vXoUNVdlRnPIfM6rExjqt0= -github.com/q191201771/naza v0.26.0 h1:ISRz+LXU3uNPjanrDOt8Q/oun1TV4AZScFS+a5ufRx0= -github.com/q191201771/naza v0.26.0/go.mod h1:n+dpJjQSh90PxBwxBNuifOwQttywvSIN5TkWSSYCeBk= -github.com/q191201771/naza v0.27.0 h1:ZrxqyCS3FrEC3EbfybqkLbfdCdU1tgdRSApo0zsuCXc= -github.com/q191201771/naza v0.27.0/go.mod h1:n+dpJjQSh90PxBwxBNuifOwQttywvSIN5TkWSSYCeBk= -github.com/q191201771/naza v0.27.2 h1:gVQp1mpjqxlpbuBlqmBdD9iA+pt8NvAr5xWInFHSCkw= -github.com/q191201771/naza v0.27.2/go.mod h1:n+dpJjQSh90PxBwxBNuifOwQttywvSIN5TkWSSYCeBk= github.com/q191201771/naza v0.28.0 h1:CMfs41VikFMeZJEZ+vflf+K70ILg4QunDuQiomHPWM8= github.com/q191201771/naza v0.28.0/go.mod h1:n+dpJjQSh90PxBwxBNuifOwQttywvSIN5TkWSSYCeBk= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/richardlehane/mscfb v1.0.3 h1:rD8TBkYWkObWO0oLDFCbwMeZ4KoalxQy+QgniCj3nKI= +github.com/richardlehane/mscfb v1.0.3/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1 h1:RfrALnSNXzmXLbGct/P2b4xkFz4e8Gmj/0Vj9M9xC1o= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -392,6 +394,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shirou/gopsutil/v3 v3.21.11 h1:d5tOAP5+bmJ8Hf2+4bxOSkQ/64+sjEbjU9nSW9nJgG0= +github.com/shirou/gopsutil/v3 v3.21.11/go.mod h1:BToYZVTlSVlfazpDDYFnsVZLaoRG+g8ufT6fPQLdJzA= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -430,20 +434,28 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo= +github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= +github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ= +github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E= -github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= -github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 h1:EpI0bqf/eX9SdZDwlMmahKM+CDBgNbsXMhsN28XrM8o= +github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.4.1 h1:veeeFLAJwsNEBPBlDepzPIYS1eLyBVcXNZUW79exZ1E= +github.com/xuri/excelize/v2 v2.4.1/go.mod h1:rSu0C3papjzxQA3sdK8cU544TebhrPUoTOaGPIh0Q1A= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= @@ -486,6 +498,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -550,6 +564,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -593,6 +608,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -621,6 +637,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -638,9 +655,11 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/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-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02 h1:7NCfEGl0sfUojmX78nK9pBJuUlSZWEJA/TwASvfiPLo= golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/img/frontend/README.md b/img/frontend/README.md new file mode 100644 index 0000000..c1d49e9 --- /dev/null +++ b/img/frontend/README.md @@ -0,0 +1,15 @@ +## 使用预览 + +![预览1](pre_1.png) +![预览2](pre_2.png) +![预览3](pre_3.png) + +## 页面预览 + +![播放器界面](player.png) +![收藏夹界面](fav.png) +![编辑收藏夹](fav_list_edit.png) +![删除收藏夹](fav_list_del.png) +![编辑收藏项](fav_edit.png) +![菜单](menu.png) +![发送弹幕](send_danmaku.png) \ No newline at end of file diff --git a/img/frontend/fav.png b/img/frontend/fav.png new file mode 100644 index 0000000..68c2aee Binary files /dev/null and b/img/frontend/fav.png differ diff --git a/img/frontend/fav_edit.png b/img/frontend/fav_edit.png new file mode 100644 index 0000000..e3b7066 Binary files /dev/null and b/img/frontend/fav_edit.png differ diff --git a/img/frontend/fav_list_del.png b/img/frontend/fav_list_del.png new file mode 100644 index 0000000..75f952c Binary files /dev/null and b/img/frontend/fav_list_del.png differ diff --git a/img/frontend/fav_list_edit.png b/img/frontend/fav_list_edit.png new file mode 100644 index 0000000..15cc7e6 Binary files /dev/null and b/img/frontend/fav_list_edit.png differ diff --git a/img/frontend/menu.png b/img/frontend/menu.png new file mode 100644 index 0000000..23b9f2c Binary files /dev/null and b/img/frontend/menu.png differ diff --git a/img/frontend/player.png b/img/frontend/player.png new file mode 100644 index 0000000..22d8c02 Binary files /dev/null and b/img/frontend/player.png differ diff --git a/img/frontend/pre_1.png b/img/frontend/pre_1.png new file mode 100644 index 0000000..e6bee1d Binary files /dev/null and b/img/frontend/pre_1.png differ diff --git a/img/frontend/pre_2.png b/img/frontend/pre_2.png new file mode 100644 index 0000000..73966ce Binary files /dev/null and b/img/frontend/pre_2.png differ diff --git a/img/frontend/pre_3.png b/img/frontend/pre_3.png new file mode 100644 index 0000000..e674632 Binary files /dev/null and b/img/frontend/pre_3.png differ diff --git a/img/frontend/send_danmaku.png b/img/frontend/send_danmaku.png new file mode 100644 index 0000000..c88f6c5 Binary files /dev/null and b/img/frontend/send_danmaku.png differ diff --git a/middleware/static.go b/middleware/static.go deleted file mode 100644 index 694ff2d..0000000 --- a/middleware/static.go +++ /dev/null @@ -1,10 +0,0 @@ -package middleware - -import ( - "github.com/gin-contrib/static" - "github.com/gin-gonic/gin" -) - -func Static() gin.HandlerFunc { - return static.Serve("/", static.LocalFile("./static", false)) -} diff --git a/model/fav.go b/model/fav.go index e650215..417562c 100644 --- a/model/fav.go +++ b/model/fav.go @@ -1,18 +1,18 @@ package model type FavoritesList struct { - ID uint64 `gorm:"primaryKey;column:id" json:"id"` + ID uint64 `gorm:"primaryKey;column:id;index" json:"id"` Title string `gorm:"not null;unique;column:title" json:"title"` Order int `gorm:"not null;column:order" json:"order"` TimeHook } type Favorite struct { - ID uint64 `gorm:"primaryKey;column:id" json:"id"` // ID - FID uint64 `gorm:"not null;column:fid" json:"fid"` // 收藏夹ID - Order int `gorm:"not null;column:order" json:"order"` // 排序 - Plat string `gorm:"not null;column:plat" json:"plat"` // 平台 - Room string `gorm:"not null;column:room" json:"room"` // 房间名 - Upper string `gorm:"not null;column:upper" json:"upper"` // 主播名 + ID uint64 `gorm:"primaryKey;column:id;index" json:"id"` // ID + FID uint64 `gorm:"not null;column:fid;index" json:"fid"` // 收藏夹ID + Order int `gorm:"not null;column:order" json:"order"` // 排序 + Plat string `gorm:"not null;column:plat" json:"plat"` // 平台 + Room string `gorm:"not null;column:room" json:"room"` // 房间名 + Upper string `gorm:"not null;column:upper" json:"upper"` // 主播名 TimeHook } diff --git a/model/msg.go b/model/msg.go index da95210..50e3720 100644 --- a/model/msg.go +++ b/model/msg.go @@ -1,6 +1,6 @@ package model -import "github.com/iyear/pure-live/conf" +import "github.com/iyear/pure-live/pkg/conf" type Msg interface { Event() string diff --git a/model/os.go b/model/os.go new file mode 100644 index 0000000..1c55405 --- /dev/null +++ b/model/os.go @@ -0,0 +1,29 @@ +package model + +type OSInfo struct { + Uptime uint64 `json:"uptime"` + OS string `json:"os"` + Platform string `json:"platform"` + PlatformVersion string `json:"platform_version"` + KernelVersion string `json:"kernel_version"` + KernelArch string `json:"kernel_arch"` +} + +type SysCPU struct { + Percent float64 `json:"percent"` +} + +type SelfCPU struct { + Percent float64 `json:"percent"` +} + +type SysMem struct { + Total uint64 `json:"total"` + TotalStr string `json:"total_str"` + Avl uint64 `json:"avl"` + AvlStr string `json:"avl_str"` +} +type SelfMem struct { + Mem uint64 `json:"mem"` + MemStr string `json:"mem_str"` +} diff --git a/pkg/client/internal/bilibili/base.go b/pkg/client/internal/bilibili/base.go index 8887d85..6bfd53f 100644 --- a/pkg/client/internal/bilibili/base.go +++ b/pkg/client/internal/bilibili/base.go @@ -2,30 +2,30 @@ package bilibili import ( "github.com/iyear/biligo" - "github.com/iyear/pure-live/conf" "github.com/iyear/pure-live/model" - "github.com/iyear/pure-live/util" + "github.com/iyear/pure-live/pkg/conf" + "github.com/iyear/pure-live/pkg/util" "strconv" ) type base struct{} func NewBiliBili() (model.Client, error) { - if !conf.C.Account.BiliBili.Enable { + if !conf.Account.BiliBili.Enable { return &BiliComm{ client: biligo.NewCommClient(&biligo.CommSetting{ - DebugMode: conf.C.Server.Debug, + DebugMode: false, }), }, nil } b, err := biligo.NewBiliClient(&biligo.BiliSetting{ Auth: &biligo.CookieAuth{ - DedeUserID: conf.C.Account.BiliBili.DedeUserID, - DedeUserIDCkMd5: conf.C.Account.BiliBili.DedeUserIDCkMd5, - SESSDATA: conf.C.Account.BiliBili.SESSDATA, - BiliJCT: conf.C.Account.BiliBili.BiliJCT, + DedeUserID: conf.Account.BiliBili.DedeUserID, + DedeUserIDCkMd5: conf.Account.BiliBili.DedeUserIDCkMd5, + SESSDATA: conf.Account.BiliBili.SESSDATA, + BiliJCT: conf.Account.BiliBili.BiliJCT, }, - DebugMode: conf.C.Server.Debug, + DebugMode: false, }) if err != nil { return nil, err diff --git a/pkg/client/internal/bilibili/serve.go b/pkg/client/internal/bilibili/serve.go index 022b2a1..0dbc0b4 100644 --- a/pkg/client/internal/bilibili/serve.go +++ b/pkg/client/internal/bilibili/serve.go @@ -9,7 +9,7 @@ import ( ) func (c *base) Host() string { - return wsDefaultHost + return "wss://broadcastlv.chat.bilibili.com/sub" } func (c *base) Enter(room string) (int, [][]byte, error) { r, err := strconv.ParseInt(room, 10, 64) diff --git a/pkg/client/internal/bilibili/util.go b/pkg/client/internal/bilibili/util.go index 08392e9..9b28cfb 100644 --- a/pkg/client/internal/bilibili/util.go +++ b/pkg/client/internal/bilibili/util.go @@ -5,7 +5,7 @@ import ( "compress/zlib" "encoding/binary" "github.com/andybalholm/brotli" - "github.com/iyear/pure-live/conf" + "github.com/iyear/pure-live/pkg/conf" "io" ) @@ -17,9 +17,6 @@ const ( wsOpEnterRoom = 7 // 请求进入房间 wsOpEnterRoomSuccess = 8 // 进房回应 ) -const ( - wsDefaultHost = "wss://broadcastlv.chat.bilibili.com/sub" -) // Header const ( diff --git a/pkg/client/internal/douyu/base.go b/pkg/client/internal/douyu/base.go index e891966..d4a34f5 100644 --- a/pkg/client/internal/douyu/base.go +++ b/pkg/client/internal/douyu/base.go @@ -6,10 +6,10 @@ import ( "github.com/dop251/goja" "github.com/gorilla/websocket" "github.com/guonaihong/gout" - "github.com/iyear/pure-live/conf" "github.com/iyear/pure-live/model" + "github.com/iyear/pure-live/pkg/conf" "github.com/iyear/pure-live/pkg/request" - "github.com/iyear/pure-live/util" + "github.com/iyear/pure-live/pkg/util" "github.com/tidwall/gjson" "regexp" "strconv" @@ -19,8 +19,6 @@ import ( type Douyu struct{} -const wsDefaultHost = "wss://danmuproxy.douyu.com:8503/" - func NewDouyu() (model.Client, error) { return &Douyu{}, nil } @@ -166,7 +164,7 @@ func (d *Douyu) GetRoomInfo(room string) (*model.RoomInfo, error) { } func (d *Douyu) Host() string { - return wsDefaultHost + return "wss://danmuproxy.douyu.com:8503/" } func (d *Douyu) Enter(room string) (int, [][]byte, error) { diff --git a/pkg/client/internal/douyu/base_test.go b/pkg/client/internal/douyu/base_test.go index f2ece22..29f40b1 100644 --- a/pkg/client/internal/douyu/base_test.go +++ b/pkg/client/internal/douyu/base_test.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/dop251/goja" "github.com/guonaihong/gout" - "github.com/iyear/pure-live/conf" + "github.com/iyear/pure-live/pkg/conf" "github.com/iyear/pure-live/pkg/request" "log" "net/http" diff --git a/pkg/client/internal/egame/base.go b/pkg/client/internal/egame/base.go new file mode 100644 index 0000000..d24e93b --- /dev/null +++ b/pkg/client/internal/egame/base.go @@ -0,0 +1,101 @@ +package egame + +import ( + "fmt" + "github.com/guonaihong/gout" + "github.com/iyear/pure-live/model" + "github.com/iyear/pure-live/pkg/conf" + "github.com/iyear/pure-live/pkg/util" + "github.com/tidwall/gjson" + "strings" +) + +type EGame struct{} + +func NewEGame() (model.Client, error) { + return &EGame{}, nil +} + +func (e *EGame) Plat() string { + return conf.PlatEGame +} + +func getInfo(room string) (*gjson.Result, error) { + resp := "" + tmpl := `{"0":{"module":"pgg_live_read_svr","method":"get_live_and_profile_info","param":{"anchor_id":{{id}},"layout_id":"hot","index":1,"other_uid":0}}}` + + err := gout.GET("https://share.egame.qq.com/cgi-bin/pgg_async_fcgi"). + SetQuery(gout.H{ + "param": strings.ReplaceAll(tmpl, "{{id}}", room), + }).BindBody(&resp).Do() + + if err != nil { + return nil, err + } + + r := gjson.Get(resp, "data.\\0.retBody.data") + return &r, nil + +} +func (e *EGame) GetPlayURL(room string, qn int) (*model.PlayURL, error) { + r, err := getInfo(room) + if err != nil { + return nil, err + } + url := r.Get("video_info.stream_infos.0.play_url").String() + + return &model.PlayURL{ + Qn: qn, + Desc: util.Qn2Desc(qn), + Origin: url, + CORS: true, + Type: conf.StreamFlv, + }, nil +} + +func (e *EGame) GetRoomInfo(room string) (*model.RoomInfo, error) { + r, err := getInfo(room) + if err != nil { + return nil, err + } + title := r.Get("video_info.title").String() + profile := r.Get("profile_info") + return &model.RoomInfo{ + Status: int(profile.Get("is_live").Int()), + Room: room, + Upper: profile.Get("nick_name").String(), + Link: fmt.Sprintf("https://egame.qq.com/%s", room), + Title: title, + }, nil +} + +func (e *EGame) Host() string { + return "wss://barragepush.egame.qq.com/sub" +} + +func (e *EGame) Enter(room string) (tp int, data [][]byte, err error) { + _ = room + return 0, nil, fmt.Errorf("not supported") +} + +func (e *EGame) Handle(tp int, data []byte) (msg []model.Msg, matched bool, err error) { + _ = tp + _ = data + return nil, false, nil +} + +func (e *EGame) HeartBeat() (tp int, data []byte, err error) { + return 0, nil, fmt.Errorf("not supported") +} + +func (e *EGame) SendDanmaku(room string, content string, tp int, color int64) error { + _ = room + _ = content + _ = tp + _ = color + return fmt.Errorf("not supported") +} + +func (e *EGame) Stop() { + +} diff --git a/pkg/client/internal/egame/base_test.go b/pkg/client/internal/egame/base_test.go new file mode 100644 index 0000000..7f8863f --- /dev/null +++ b/pkg/client/internal/egame/base_test.go @@ -0,0 +1,87 @@ +package egame + +import ( + "fmt" + "github.com/guonaihong/gout" + "github.com/tidwall/gjson" + "log" + "strings" + "testing" +) + +// room_url = 'https://share.egame.qq.com/cgi-bin/pgg_async_fcgi' +// post_data = { +// 'param': '''{"0":{"module":"pgg_live_read_svr","method":"get_live_and_profile_info","param":{"anchor_id":''' +// + str(self.rid) + ''',"layout_id":"hot","index":1,"other_uid":0}}}''' +// } +// try: +// response = requests.post(url=room_url, data=post_data).json() +// data = response.get('data', 0) +// if data: +// video_info = data.get('0').get( +// 'retBody').get('data').get('video_info') +// pid = video_info.get('pid', 0) +// if pid: +// is_live = data.get('0').get( +// 'retBody').get('data').get('profile_info').get('is_live', 0) +// if is_live: +// play_url = video_info.get('stream_infos')[ +// 0].get('play_url') +// real_url = re.findall(r'([\w\W]+?)&uid=', play_url)[0] +// else: +// raise Exception('直播间未开播') +// else: +// raise Exception('直播间未启用') +// else: +// raise Exception('直播间不存在') +// except: +// raise Exception('数据请求错误') +// return real_url +func TestEGame(t *testing.T) { + resp := "" + tmpl := `{"0":{"module":"pgg_live_read_svr","method":"get_live_and_profile_info","param":{"anchor_id":[[id]],"layout_id":"hot","index":1,"other_uid":0}}}` + err := gout.GET("https://share.egame.qq.com/cgi-bin/pgg_async_fcgi").SetQuery(gout.H{ + "param": strings.ReplaceAll(tmpl, "[[id]]", "383204988"), + }).BindBody(&resp).Do() + if err != nil { + log.Println(err) + return + } + fmt.Println(resp) + // http://3954-out.liveplay.myqcloud.com/live/3954_383204988_1024p.flv?bizid=3954&txSecret=594c5267d2d345ee61aaa7084ed73757&txTime=61c55b8e&uid=0&fromdj=&_qedj_t=BhgX1Z1373Muj17v4RWDFG7xFNsZviDJw7EQASYUMzgzMjA0OTg4XzE2Mzk3MTkxODEyYbw530xWHHBnSzVDX1FMQUFDajd3MWFVdE1GQU1EMkxRTmhmAHyGK3BnZ19saXZlX3JlYWRfaWZjX210X3N2ci5lbnRyeV9oNV9saXZlX3Jvb22WAKYNNjAuMTkxLjEyMi4zNLzGANYA7PwP + r := gjson.Get(resp, "data.\\0.retBody.data") + + video := r.Get("video_info") + pid := video.Get("pid").String() + if pid == "" { + log.Println("直播间不存在") + return + } + + url := video.Get("stream_infos.0.play_url") + + profile := r.Get("profile_info") + + status := int(profile.Get("is_live").Int()) + + upper := profile.Get("nick_name").String() + + fmt.Println(status, upper) + fmt.Println(url) + if err != nil { + t.Error(err) + t.FailNow() + } +} + +func TestEGameWs(t *testing.T) { + r, err := getInfo("303441526") + if err != nil { + t.Error(err) + t.FailNow() + } + + _ = r.Get("video_info.pid").String() + + _ = `{"0":{"module":"pgg.ws_token_go_svr.DefObj","method":"get_token","param":{"scene_flag":16,"subinfo":{"page":{"scene":1,"page_id":{{room}},"str_id":"{{pid}}","msg_type_list":[1,2]}},"version":1,"message_seq":-1,"dc_param":{"params":{"info":{"aid":"{{}}"}},"position":{"page_id":"QG_HEARTBEAT_PAGE_LIVE_ROOM"},"refer":{}},"other_uid":0}}}` +} diff --git a/pkg/client/internal/huya/base.go b/pkg/client/internal/huya/base.go index adf05aa..3909d32 100644 --- a/pkg/client/internal/huya/base.go +++ b/pkg/client/internal/huya/base.go @@ -6,17 +6,16 @@ import ( "fmt" "github.com/TarsCloud/TarsGo/tars/protocol/codec" "github.com/gorilla/websocket" - "github.com/iyear/pure-live/conf" "github.com/iyear/pure-live/model" "github.com/iyear/pure-live/pkg/client/internal/huya/internal/tars/danmaku" "github.com/iyear/pure-live/pkg/client/internal/huya/internal/tars/online" "github.com/iyear/pure-live/pkg/client/internal/huya/internal/tars/push_msg" "github.com/iyear/pure-live/pkg/client/internal/huya/internal/tars/ws_cmd" - "github.com/iyear/pure-live/util" + "github.com/iyear/pure-live/pkg/conf" + "github.com/iyear/pure-live/pkg/util" "strings" ) -const wsHost = "wss://cdnws.api.huya.com/" const hb = "00031d0000690000006910032c3c4c56086f6e6c696e657569660f4f6e557365724865617274426561747d00003c0800010604745265711d00002f0a0a0c1600260036076164725f77617046000b1203aef00f2203aef00f3c426d5202605c60017c82000bb01f9cac0b8c980ca80c20" type Huya struct{} @@ -47,7 +46,7 @@ func (h *Huya) GetPlayURL(room string, qn int) (*model.PlayURL, error) { link = strings.ReplaceAll(link, "m3u8", "flv") return &model.PlayURL{ Qn: qn, - Desc: util.Qn2Desc(conf.QnBest), + Desc: util.Qn2Desc(qn), Origin: fmt.Sprintf("https:%s", link), CORS: false, Type: conf.StreamFlv, @@ -69,7 +68,7 @@ func (h *Huya) GetRoomInfo(room string) (*model.RoomInfo, error) { } func (h *Huya) Host() string { - return wsHost + return "wss://cdnws.api.huya.com/" } func (h *Huya) Enter(room string) (int, [][]byte, error) { diff --git a/pkg/client/plat.go b/pkg/client/plat.go index b9c018a..1b4d846 100644 --- a/pkg/client/plat.go +++ b/pkg/client/plat.go @@ -2,11 +2,11 @@ package client import ( "fmt" - "github.com/iyear/pure-live/conf" "github.com/iyear/pure-live/model" "github.com/iyear/pure-live/pkg/client/internal/bilibili" "github.com/iyear/pure-live/pkg/client/internal/douyu" "github.com/iyear/pure-live/pkg/client/internal/huya" + "github.com/iyear/pure-live/pkg/conf" ) func GetClient(plat string) (model.Client, error) { diff --git a/pkg/conf/conf.go b/pkg/conf/conf.go new file mode 100644 index 0000000..b3a1bc4 --- /dev/null +++ b/pkg/conf/conf.go @@ -0,0 +1,22 @@ +package conf + +import ( + "github.com/spf13/viper" +) + +var ( + Account account +) + +func InitAccount(path string) error { + c := viper.New() + c.SetConfigFile(path) + if err := c.ReadInConfig(); err != nil { + return err + } + if err := c.Unmarshal(&Account); err != nil { + return err + } + + return nil +} diff --git a/conf/const.go b/pkg/conf/const.go similarity index 94% rename from conf/const.go rename to pkg/conf/const.go index 1cf0e48..5b5a8f1 100644 --- a/conf/const.go +++ b/pkg/conf/const.go @@ -4,6 +4,7 @@ const ( PlatBiliBili = "bilibili" PlatHuya = "huya" PlatDouyu = "douyu" + PlatEGame = "egame" ) const ( diff --git a/conf/model.go b/pkg/conf/model.go similarity index 56% rename from conf/model.go rename to pkg/conf/model.go index 1e69e3f..35c02f1 100644 --- a/conf/model.go +++ b/pkg/conf/model.go @@ -1,16 +1,5 @@ package conf -type config struct { - Server server `mapstructure:"server"` - Socks5 socks5 `mapstructure:"socks5"` - Account account `mapstructure:"account"` -} -type server struct { - Port int `mapstructure:"port"` - Debug bool `mapstructure:"debug"` - Path string `mapstructure:"path"` -} - type account struct { BiliBili bilibili `mapstructure:"bilibili"` Huya huya `mapstructure:"huya"` @@ -32,11 +21,3 @@ type bilibili struct { SESSDATA string `mapstructure:"SESSDATA"` // SESSDATA BiliJCT string `mapstructure:"BiliJCT"` // bili_jct } - -type socks5 struct { - Enable bool `mapstructure:"enable"` - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - User string `mapstructure:"user"` - Password string `mapstructure:"password"` -} diff --git a/pkg/db/db.go b/pkg/db/db.go new file mode 100644 index 0000000..f9d7ce4 --- /dev/null +++ b/pkg/db/db.go @@ -0,0 +1,34 @@ +package db + +import ( + "github.com/iyear/pure-live/model" + "github.com/iyear/sqlite" + "gorm.io/gorm" + "time" +) + +func Init(path string) (*gorm.DB, error) { + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, + NowFunc: func() time.Time { + return time.Now().UTC() + }, + }) + if err != nil { + return nil, err + } + + if err = db.AutoMigrate(&model.FavoritesList{}, &model.Favorite{}); err != nil { + return nil, err + } + + // 创建默认收藏夹 + if err = db.FirstOrCreate(&model.FavoritesList{}, &model.FavoritesList{ + ID: 1, + Title: "默认收藏夹", + Order: 0, + }).Error; err != nil { + return nil, err + } + return db, nil +} diff --git a/pkg/e/code.go b/pkg/ecode/code.go similarity index 71% rename from pkg/e/code.go rename to pkg/ecode/code.go index 394d998..0549daa 100644 --- a/pkg/e/code.go +++ b/pkg/ecode/code.go @@ -1,4 +1,4 @@ -package e +package ecode const ( Success = 0 @@ -17,4 +17,10 @@ const ( ErrorDelFav = 10010 ErrorEditFav = 10011 ErrorGetFav = 10012 + ErrorGetSysMem = 10013 + ErrorGetSelfMem = 10014 + ErrorGetSysCPU = 10015 + ErrorGetSelfCPU = 10016 + ErrorGetOSInfo = 10017 + ErrorGetOsAll = 10018 ) diff --git a/pkg/e/msg.go b/pkg/ecode/msg.go similarity index 72% rename from pkg/e/msg.go rename to pkg/ecode/msg.go index d64dcf7..dffa04d 100644 --- a/pkg/e/msg.go +++ b/pkg/ecode/msg.go @@ -1,4 +1,4 @@ -package e +package ecode var msg = map[int]string{ Success: "ok", @@ -17,6 +17,12 @@ var msg = map[int]string{ ErrorDelFav: "failed to del fav", ErrorEditFav: "failed to edit fav", ErrorGetFav: "failed to get fav", + ErrorGetSysMem: "failed to get sys mem", + ErrorGetSelfMem: "failed to get self mem", + ErrorGetSysCPU: "failed to get sys cpu", + ErrorGetSelfCPU: "failed to get self cpu", + ErrorGetOSInfo: "failed to get os info", + ErrorGetOsAll: "failed to get all os info", } func GetMsg(code int) string { diff --git a/pkg/format/http.go b/pkg/format/http.go new file mode 100644 index 0000000..9b10926 --- /dev/null +++ b/pkg/format/http.go @@ -0,0 +1,26 @@ +package format + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/iyear/pure-live/pkg/ecode" + "net/http" +) + +func HTTP(c *gin.Context, code int, err error, data interface{}) { + type resp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data,omitempty"` + } + + var msg = fmt.Sprintf("%s", ecode.GetMsg(code)) + if err != nil { + msg += fmt.Sprintf(": %s", err.Error()) + } + c.JSON(http.StatusOK, &resp{ + Code: code, + Msg: msg, + Data: data, + }) +} diff --git a/pkg/format/ws.go b/pkg/format/ws.go new file mode 100644 index 0000000..59f0bc0 --- /dev/null +++ b/pkg/format/ws.go @@ -0,0 +1,15 @@ +package format + +import "encoding/json" + +func WS(tp string, data interface{}) []byte { + type msg struct { + Type string `json:"type"` + Data interface{} `json:"data,omitempty"` + } + b, _ := json.Marshal(&msg{ + Type: tp, + Data: data, + }) + return b +} diff --git a/pkg/forwarder/out.go b/pkg/forwarder/out.go index ec51acf..bdcf05a 100644 --- a/pkg/forwarder/out.go +++ b/pkg/forwarder/out.go @@ -16,7 +16,8 @@ func OutLoop(conn net.Conn, pullURL string, rawURL string, in In) error { sub.WriteHttpResponseHeader() sub.WriteFlvHeader() - if err = in.Pull( + if err = Pull( + in, pullURL, func(tag httpflv.Tag) { sub.Write(tag.Raw) @@ -30,3 +31,7 @@ func OutLoop(conn net.Conn, pullURL string, rawURL string, in In) error { } return nil } + +func Pull(in In, pullURL string, fn func(tag httpflv.Tag)) error { + return in.Pull(pullURL, fn) +} diff --git a/pkg/ps/cpu.go b/pkg/ps/cpu.go new file mode 100644 index 0000000..5f676f0 --- /dev/null +++ b/pkg/ps/cpu.go @@ -0,0 +1,24 @@ +package ps + +import ( + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/process" + "os" + "time" +) + +func GetSysCPU(internal time.Duration, percpu bool) ([]float64, error) { + return cpu.Percent(internal, percpu) +} + +func GetSelfCPU() (float64, error) { + proc, err := process.NewProcess(int32(os.Getpid())) + if err != nil { + return 0, err + } + per, err := proc.CPUPercent() + if err != nil { + return 0, err + } + return per, nil +} diff --git a/pkg/ps/mem.go b/pkg/ps/mem.go new file mode 100644 index 0000000..b859dfb --- /dev/null +++ b/pkg/ps/mem.go @@ -0,0 +1,24 @@ +package ps + +import ( + "github.com/shirou/gopsutil/v3/mem" + "github.com/shirou/gopsutil/v3/process" + "os" +) + +func GetSysMem() (*mem.VirtualMemoryStat, error) { + return mem.VirtualMemory() +} + +func GetSelfMem() (*process.MemoryInfoStat, error) { + proc, err := process.NewProcess(int32(os.Getpid())) + if err != nil { + return nil, err + } + m, err := proc.MemoryInfo() + if err != nil { + return nil, err + } + + return m, nil +} diff --git a/pkg/ps/os.go b/pkg/ps/os.go new file mode 100644 index 0000000..5653653 --- /dev/null +++ b/pkg/ps/os.go @@ -0,0 +1,7 @@ +package ps + +import "github.com/shirou/gopsutil/v3/host" + +func GetOsInfo() (*host.InfoStat, error) { + return host.Info() +} diff --git a/pkg/ps/ps_test.go b/pkg/ps/ps_test.go new file mode 100644 index 0000000..701b473 --- /dev/null +++ b/pkg/ps/ps_test.go @@ -0,0 +1,50 @@ +package ps + +import ( + "fmt" + "github.com/iyear/pure-live/pkg/util" + "testing" + "time" +) + +func TestGetSysMem(t *testing.T) { + m, err := GetSysMem() + if err != nil { + t.Error(err) + t.FailNow() + } + + fmt.Println(m) +} + +func TestGetSelfMem(t *testing.T) { + m, err := GetSelfMem() + if err != nil { + t.Error(err) + t.FailNow() + return + } + + fmt.Println(util.MemoryHuman(m.RSS)) +} + +func TestGetSysCPU(t *testing.T) { + info, err := GetSysCPU(25*time.Millisecond, false) + if err != nil { + t.Error(err) + t.FailNow() + } + + for _, f := range info { + t.Log(f) + } +} + +func TestGetOsInfo(t *testing.T) { + info, err := GetOsInfo() + if err != nil { + t.Error(err) + t.FailNow() + } + t.Log(info.Uptime, info.OS, info.Platform, info.PlatformVersion, info.KernelVersion, info.KernelArch) +} diff --git a/pkg/request/http.go b/pkg/request/http.go index bdb17b4..4610953 100644 --- a/pkg/request/http.go +++ b/pkg/request/http.go @@ -3,17 +3,20 @@ package request import ( "github.com/guonaihong/gout" "github.com/guonaihong/gout/dataflow" - "github.com/iyear/pure-live/conf" - "github.com/iyear/pure-live/util" + "github.com/iyear/pure-live/pkg/util" + "net" "net/http" ) +var dial = net.Dial + +func SetSocks5(host string, port int, user, password string) { + dial = util.MustGetSocks5(host, port, user, password).Dial +} + func HTTP() *dataflow.DataFlow { c := http.DefaultClient - tsp := &http.Transport{} - if conf.C.Socks5.Enable { - tsp.Dial = util.MustGetSocks5(conf.C.Socks5.Host, conf.C.Socks5.Port, conf.C.Socks5.User, conf.C.Socks5.Password).Dial - } + tsp := &http.Transport{Dial: dial} c.Transport = tsp return gout.New(c).Debug(false) } diff --git a/util/base.go b/pkg/util/base.go similarity index 93% rename from util/base.go rename to pkg/util/base.go index 50f9756..18be9f6 100644 --- a/util/base.go +++ b/pkg/util/base.go @@ -45,7 +45,7 @@ func MustGetSocks5(host string, port int, user, password string) proxy.Dialer { Password: password, }, proxy.Direct) if err != nil { - panic(fmt.Errorf("failed to get socks5 proxy: %s", err)) + return &net.Dialer{} } return dialer } diff --git a/pkg/util/live.go b/pkg/util/live.go new file mode 100644 index 0000000..6c29075 --- /dev/null +++ b/pkg/util/live.go @@ -0,0 +1,35 @@ +package util + +import "github.com/iyear/pure-live/pkg/conf" + +var qn2desc = map[int]string{ + conf.QnBest: "原画", + conf.QnHigh: "蓝光", + conf.QnMid: "超清", + conf.QnLow: "流畅", +} + +func Qn2Desc(qn int) string { + return qn2desc[qn] +} + +var mode2desc = map[int]string{ + conf.DanmakuTypeTop: "顶部", + conf.DanmakuTypeRight: "飞行", + conf.DanmakuTypeBottom: "底部", +} + +func DmMode2Desc(mode int) string { + return mode2desc[mode] +} + +var plat2desc = map[string]string{ + conf.PlatBiliBili: "哔哩哔哩", + conf.PlatHuya: "虎牙", + conf.PlatDouyu: "斗鱼", + conf.PlatEGame: "企鹅电竞", +} + +func Plat2Desc(plat string) string { + return plat2desc[plat] +} diff --git a/pkg/util/os.go b/pkg/util/os.go new file mode 100644 index 0000000..91ed720 --- /dev/null +++ b/pkg/util/os.go @@ -0,0 +1,23 @@ +package util + +import ( + "fmt" + "os" +) + +func FileExists(file string) bool { + _, err := os.Stat(file) + return err == nil || os.IsExist(err) +} + +func MemoryHuman(m uint64) string { + if m < 1024 { + return fmt.Sprintf("%d B", m) + } else if m < 1024*1024 { + return fmt.Sprintf("%.2f KB", float64(m)/1024) + } else if m < 1024*1024*1024 { + return fmt.Sprintf("%.2f MB", float64(m)/1024/1024) + } else { + return fmt.Sprintf("%.2f GB", float64(m)/1024/1024/1024) + } +} diff --git a/server/server.go b/server/server.go deleted file mode 100644 index 1517cd4..0000000 --- a/server/server.go +++ /dev/null @@ -1,42 +0,0 @@ -package server - -import ( - "fmt" - "github.com/iyear/pure-live/conf" - "github.com/iyear/pure-live/db" - "github.com/iyear/pure-live/logger" - "github.com/iyear/pure-live/router" - "github.com/iyear/pure-live/util" - "github.com/q191201771/naza/pkg/nazalog" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "os" -) - -func Run(cfgFile string) { - // lal包中的nazalog - _ = nazalog.Init(func(option *nazalog.Option) { - option.IsToStdout = conf.C.Server.Debug - }) - - conf.Init(cfgFile) - logger.Init(util.IF(conf.C.Server.Debug, zapcore.DebugLevel, zapcore.InfoLevel).(zapcore.LevelEnabler)) - - zap.S().Infof("init server...") - if err := os.MkdirAll(conf.C.Server.Path, 0774); err != nil { - zap.S().Fatalw("failed to mkdir", "error", err) - } - - if err := db.Init(); err != nil { - zap.S().Fatalw("failed to init database", "error", err) - } - zap.S().Infof("init database succ...") - - zap.S().Infof("server runs on :%d,debug: %v", conf.C.Server.Port, conf.C.Server.Debug) - engine := router.Init() - err := engine.Run(fmt.Sprintf(":%d", conf.C.Server.Port)) - if err != nil { - zap.S().Fatalw("failed to run gin engine", "error", err, "port", conf.C.Server.Port) - return - } -} diff --git a/service/srv_fav/fav.go b/service/srv_fav/fav.go index 5916279..d349922 100644 --- a/service/srv_fav/fav.go +++ b/service/srv_fav/fav.go @@ -1,13 +1,13 @@ package srv_fav import ( - "github.com/iyear/pure-live/db" + "github.com/iyear/pure-live/global" "github.com/iyear/pure-live/model" ) func GetFav(id uint64) (*model.Favorite, error) { fav := model.Favorite{} - if err := db.SQLite.First(&fav, id).Limit(1).Error; err != nil { + if err := global.DB.First(&fav, id).Limit(1).Error; err != nil { return nil, err } return &fav, nil @@ -20,16 +20,16 @@ func AddFav(fid uint64, order int, plat string, room string, upper string) (*mod Room: room, Upper: upper, } - if err := db.SQLite.Create(&fav).Error; err != nil { + if err := global.DB.Create(&fav).Error; err != nil { return nil, err } return &fav, nil } func DelFav(id uint64) error { - if err := db.SQLite.First(&model.Favorite{ID: id}).Limit(1).Error; err != nil { + if err := global.DB.First(&model.Favorite{ID: id}).Limit(1).Error; err != nil { return err } - if err := db.SQLite.Where("id = ?", id).Delete(&model.Favorite{}).Error; err != nil { + if err := global.DB.Where("id = ?", id).Delete(&model.Favorite{}).Error; err != nil { return err } return nil @@ -37,10 +37,10 @@ func DelFav(id uint64) error { func EditFav(id uint64, order int, plat string, room string, upper string) (*model.Favorite, error) { r := model.Favorite{ID: id} - if err := db.SQLite.First(&r).Limit(1).Error; err != nil { + if err := global.DB.First(&r).Limit(1).Error; err != nil { return nil, err } - if err := db.SQLite.Model(&r).Updates(map[string]interface{}{"order": order, "plat": plat, "room": room, "upper": upper}).Error; err != nil { + if err := global.DB.Model(&r).Updates(map[string]interface{}{"order": order, "plat": plat, "room": room, "upper": upper}).Error; err != nil { return nil, err } return &r, nil diff --git a/service/srv_fav/list.go b/service/srv_fav/list.go index 4d8dcba..afa55b6 100644 --- a/service/srv_fav/list.go +++ b/service/srv_fav/list.go @@ -2,7 +2,7 @@ package srv_fav import ( "fmt" - "github.com/iyear/pure-live/db" + "github.com/iyear/pure-live/global" "github.com/iyear/pure-live/model" ) @@ -11,7 +11,7 @@ func AddFavList(title string, order int) (*model.FavoritesList, error) { Title: title, Order: order, } - if err := db.SQLite.Create(&result).Error; err != nil { + if err := global.DB.Create(&result).Error; err != nil { return nil, err } return &result, nil @@ -19,7 +19,7 @@ func AddFavList(title string, order int) (*model.FavoritesList, error) { func GetAllFavLists() ([]*model.FavoritesList, error) { var result []*model.FavoritesList - if err := db.SQLite.Find(&result).Error; err != nil { + if err := global.DB.Find(&result).Error; err != nil { return nil, err } return result, nil @@ -30,10 +30,10 @@ func DelFavList(id uint64) error { if id == 1 { return fmt.Errorf("default fav list cannot be deleted") } - if err := db.SQLite.First(&model.FavoritesList{ID: id}).Limit(1).Error; err != nil { + if err := global.DB.First(&model.FavoritesList{ID: id}).Limit(1).Error; err != nil { return err } - if err := db.SQLite.Delete(&model.FavoritesList{ID: id}).Error; err != nil { + if err := global.DB.Delete(&model.FavoritesList{ID: id}).Error; err != nil { return err } return nil @@ -41,10 +41,10 @@ func DelFavList(id uint64) error { func EditFavList(id uint64, title string, order int) (*model.FavoritesList, error) { r := model.FavoritesList{ID: id} - if err := db.SQLite.First(&r).Limit(1).Error; err != nil { + if err := global.DB.First(&r).Limit(1).Error; err != nil { return nil, err } - if err := db.SQLite.Model(&r).Updates(map[string]interface{}{"title": title, "order": order}).Error; err != nil { + if err := global.DB.Model(&r).Updates(map[string]interface{}{"title": title, "order": order}).Error; err != nil { return nil, err } return &r, nil @@ -55,10 +55,10 @@ func GetFavList(id uint64) (*model.FavoritesList, []*model.Favorite, error) { list = model.FavoritesList{} favs []*model.Favorite ) - if err := db.SQLite.First(&list, id).Limit(1).Error; err != nil { + if err := global.DB.First(&list, id).Limit(1).Error; err != nil { return nil, nil, err } - if err := db.SQLite.Where("fid = ?", id).Find(&favs).Error; err != nil { + if err := global.DB.Where("fid = ?", id).Find(&favs).Error; err != nil { return nil, nil, err } return &list, favs, nil diff --git a/service/srv_live/danmaku.go b/service/srv_live/danmaku.go index bc3436b..95118c7 100644 --- a/service/srv_live/danmaku.go +++ b/service/srv_live/danmaku.go @@ -1,19 +1,9 @@ package srv_live import ( - "github.com/iyear/pure-live/global" + "github.com/iyear/pure-live/model" ) -func SendDanmaku(id string, content string, tp int, color int64) error { - var ( - conn *global.Conn - err error - ) - if conn, err = global.GetConn(id); err != nil { - return err - } - if err = conn.Client.SendDanmaku(conn.Room, content, tp, color); err != nil { - return err - } - return nil +func SendDanmaku(client model.Client, room, content string, tp int, color int64) error { + return client.SendDanmaku(room, content, tp, color) } diff --git a/service/srv_live/live.go b/service/srv_live/live.go index 1b52305..2b54726 100644 --- a/service/srv_live/live.go +++ b/service/srv_live/live.go @@ -1,9 +1,9 @@ package srv_live import ( - "github.com/iyear/pure-live/conf" "github.com/iyear/pure-live/model" "github.com/iyear/pure-live/pkg/client" + "github.com/iyear/pure-live/pkg/conf" ) func GetRoomInfo(plat string, room string) (*model.RoomInfo, error) { diff --git a/service/srv_live/serve.go b/service/srv_live/serve.go index 55db747..c04a2ce 100644 --- a/service/srv_live/serve.go +++ b/service/srv_live/serve.go @@ -3,102 +3,50 @@ package srv_live import ( "context" "github.com/gorilla/websocket" - "github.com/iyear/pure-live/api" - "github.com/iyear/pure-live/conf" - "github.com/iyear/pure-live/global" "github.com/iyear/pure-live/model" - "github.com/iyear/pure-live/util" "go.uber.org/zap" "time" ) -func Serve(ctx context.Context) { +func Serve(ctx context.Context, dialer *websocket.Dialer, id string, client model.Client, room string) (chan *model.Transport, error) { // 过程 // conn -> enter(on entered) -> go receive() -> for{send msg to local} // -> go heartbeat() - - id := ctx.Value("id").(string) - conn, err := global.GetConn(id) + live, _, err := dialer.DialContext(ctx, client.Host(), nil) if err != nil { - return + return nil, err } - dialer := websocket.DefaultDialer - if conf.C.Socks5.Enable { - dialer.NetDial = util.MustGetSocks5(conf.C.Socks5.Host, conf.C.Socks5.Port, conf.C.Socks5.User, conf.C.Socks5.Password).Dial - } - - live, _, err := dialer.DialContext(ctx, conn.Client.Host(), nil) - if err != nil { - zap.S().Warnw("failed to connect live websocket server", "id", id, "error", err) - return - } - defer live.Close() - zap.S().Infow("connected to live danmaku server", "id", id) rev := make(chan *model.Transport) - tp, data, err := conn.Client.Enter(conn.Room) - // fmt.Println(hex.Dump(data)) + tp, data, err := client.Enter(room) + if err != nil { - zap.S().Warnw("failed to get enter room data", "id", id, "error", err) - return + return nil, err } for _, d := range data { if err = live.WriteMessage(tp, d); err != nil { - zap.S().Warnw("failed to write ws msg", "id", id, "error", err) - return + return nil, err } } zap.S().Infow("entered the room", "id", id) - hbCtx, hbCancel := context.WithCancel(ctx) - revCtx, revCancel := context.WithCancel(ctx) + go receive(ctx, id, client, live, rev) + go heartbeat(ctx, id, client, live) - go receive(revCtx, live, rev) - defer revCancel() - - go heartbeat(hbCtx, live) - defer hbCancel() - - // 客户端心跳检查 - health := time.NewTicker(5 * time.Second) - defer health.Stop() - for { - select { - // 5秒检查一次客户端存活 - case <-health.C: - if err = conn.Server.WriteMessage(websocket.TextMessage, api.MsgFmt(conf.EventCheck, nil)); err != nil { - zap.S().Warnw("failed to write ws message", "id", id, "error", err) - return - } - case tp := <-rev: - if tp.Error != nil { - zap.S().Warnw("receive transport error", "id", id, "error", err) - continue - } - if err = conn.Server.WriteMessage(websocket.TextMessage, api.MsgFmt(tp.Msg.Event(), tp.Msg)); err != nil { - zap.S().Warnw("failed to write ws message", "id", id, "error", err) - return - } - } - } + return rev, nil } -func heartbeat(ctx context.Context, live *websocket.Conn) { - id := ctx.Value("id").(string) - conn, err := global.GetConn(id) - if err != nil { - return - } - +func heartbeat(ctx context.Context, id string, client model.Client, live *websocket.Conn) { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() hb := func() { - tp, data, err := conn.Client.HeartBeat() + tp, data, err := client.HeartBeat() if err != nil { zap.S().Warnw("failed to get heartbeat data", "id", id, "error", err) return @@ -122,19 +70,10 @@ func heartbeat(ctx context.Context, live *websocket.Conn) { } } -func receive(ctx context.Context, live *websocket.Conn, rev chan *model.Transport) { + +func receive(ctx context.Context, id string, client model.Client, live *websocket.Conn, rev chan *model.Transport) { msgCtx, msgCancel := context.WithCancel(ctx) defer msgCancel() - - var ( - conn *global.Conn - err error - ) - id := ctx.Value("id").(string) - if conn, err = global.GetConn(id); err != nil { - return - } - for { select { case <-ctx.Done(): @@ -145,11 +84,11 @@ func receive(ctx context.Context, live *websocket.Conn, rev chan *model.Transpor if err != nil { continue } - msg, ok, err := conn.Client.Handle(t, data) + msg, ok, err := client.Handle(t, data) // 错误判断 if err != nil { - go push(msgCtx, &model.Transport{ + go push(msgCtx, id, &model.Transport{ Msg: nil, Error: err, }, rev) @@ -161,7 +100,7 @@ func receive(ctx context.Context, live *websocket.Conn, rev chan *model.Transpor } for _, m := range msg { - go push(msgCtx, &model.Transport{ + go push(msgCtx, id, &model.Transport{ Msg: m, Error: nil, }, rev) @@ -169,9 +108,7 @@ func receive(ctx context.Context, live *websocket.Conn, rev chan *model.Transpor } } } -func push(ctx context.Context, tp *model.Transport, rev chan *model.Transport) { - id := ctx.Value("id").(string) - +func push(ctx context.Context, id string, tp *model.Transport, rev chan *model.Transport) { t := time.NewTimer(5 * time.Second) defer t.Stop() diff --git a/service/srv_os/cpu.go b/service/srv_os/cpu.go new file mode 100644 index 0000000..749b355 --- /dev/null +++ b/service/srv_os/cpu.go @@ -0,0 +1,23 @@ +package srv_os + +import ( + "github.com/iyear/pure-live/model" + "github.com/iyear/pure-live/pkg/ps" + "time" +) + +func GetSysCPU() (*model.SysCPU, error) { + per, err := ps.GetSysCPU(25*time.Millisecond, false) + if err != nil { + return nil, err + } + return &model.SysCPU{Percent: per[0]}, nil +} + +func GetSelfCPU() (*model.SelfCPU, error) { + per, err := ps.GetSelfCPU() + if err != nil { + return nil, err + } + return &model.SelfCPU{Percent: per}, nil +} diff --git a/service/srv_os/mem.go b/service/srv_os/mem.go new file mode 100644 index 0000000..01948be --- /dev/null +++ b/service/srv_os/mem.go @@ -0,0 +1,31 @@ +package srv_os + +import ( + "github.com/iyear/pure-live/model" + "github.com/iyear/pure-live/pkg/ps" + "github.com/iyear/pure-live/pkg/util" +) + +func GetSysMem() (*model.SysMem, error) { + m, err := ps.GetSysMem() + if err != nil { + return nil, err + } + return &model.SysMem{ + Total: m.Total, + TotalStr: util.MemoryHuman(m.Total), + Avl: m.Available, + AvlStr: util.MemoryHuman(m.Available), + }, nil +} + +func GetSelfMem() (*model.SelfMem, error) { + m, err := ps.GetSelfMem() + if err != nil { + return nil, err + } + return &model.SelfMem{ + Mem: m.RSS, + MemStr: util.MemoryHuman(m.RSS), + }, nil +} diff --git a/service/srv_os/os.go b/service/srv_os/os.go new file mode 100644 index 0000000..a647095 --- /dev/null +++ b/service/srv_os/os.go @@ -0,0 +1,22 @@ +package srv_os + +import ( + "github.com/iyear/pure-live/model" + "github.com/iyear/pure-live/pkg/ps" +) + +func GetOSInfo() (*model.OSInfo, error) { + info, err := ps.GetOsInfo() + if err != nil { + return nil, err + } + + return &model.OSInfo{ + Uptime: info.Uptime, + OS: info.OS, + Platform: info.Platform, + PlatformVersion: info.PlatformVersion, + KernelVersion: info.KernelVersion, + KernelArch: info.KernelArch, + }, nil +} diff --git a/util/live.go b/util/live.go deleted file mode 100644 index 7cf77fe..0000000 --- a/util/live.go +++ /dev/null @@ -1,14 +0,0 @@ -package util - -import "github.com/iyear/pure-live/conf" - -var qn2desc = map[int]string{ - conf.QnBest: "原画", - conf.QnHigh: "蓝光", - conf.QnMid: "超清", - conf.QnLow: "流畅", -} - -func Qn2Desc(qn int) string { - return qn2desc[qn] -}