From 8b0be3ef3bd72b3a98f68d797beab46e7459e0aa Mon Sep 17 00:00:00 2001 From: qydysky Date: Tue, 8 Apr 2025 20:56:26 +0800 Subject: [PATCH] =?utf8?q?Improve=20=E7=BB=93=E6=9D=9F=E5=BD=95=E5=88=B6?= =?utf8?q?=E6=97=B6=E4=BF=9D=E5=AD=98=E8=A1=A8=E6=83=85=E5=8C=85=20(#186)?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * Improve 结束录制时保存表情包 * Improve 优化 * Improve 添加说明 * Fix golangci-lint check * Improve 优化 --- README.md | 8 ++- Reply/F.go | 41 +++++++++--- Reply/F/comp.go | 3 + Reply/F/danmuEmotes/danmuEmotes.go | 101 +++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- 6 files changed, 143 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d593b57..4ec80d9 100644 --- a/README.md +++ b/README.md @@ -304,9 +304,7 @@ curl -s http://{主机名}:11000/ip/ | awk '/240:?/' #### 直播回放显示表情 配置文件中添加配置项`弹幕表情`(>v0.14.9)。默认为true,当为true时,将会保存弹幕中的表情png到emots目录下,并在回放时显示表情。 -为了能顺利保存,会将某些字符进行转换(<=v0.14.18),如:[dog?]=>[dog?].png。 - -但之后(>v0.14.18)存储通过md5作为文件名,如:[dog?]=>md5("[dog?]")+".png"=>0c8427bdb9854d85e9cfe59c45d70583.png +为了能顺利保存,存储通过md5作为文件名,如:[dog?]=>md5("[dog?]")+".png"=>0c8427bdb9854d85e9cfe59c45d70583.png 从而避免对原表情产生修改,且避免不同环境对文件名支持的差异。 @@ -314,6 +312,10 @@ curl -s http://{主机名}:11000/ip/ | awk '/240:?/' 保存的表情可以通过`http://{Web服务地址}{直播Web服务路径}emots/{md5}.png`获取。 +在(>v0.16.6)中,在录制结束时,本场录制出现的表情将会从emots目录复制到录制文件夹下的`emotes.zip`中。 +当请求`http://{Web服务地址}{直播Web服务路径}emots/{md5}.png`时,如请求头`Referer`中的`ref`url参数为有效的录播文件夹名,并且该文件夹下有`emotes.zip`。 +则在回放中使用`emotes.zip`中的表情,这确保了录播文件夹迁移到其他环境时,即使emots目录没有对应表情,也可以正常回放表情。 + #### 直播流停用服务器 配置文件中添加配置项`直播流停用服务器`(>v0.14.3)。默认为空,编写正则字符串,当获取到的服务器链接与字符串匹配时,将会停用。 diff --git a/Reply/F.go b/Reply/F.go index de06fab..60c2c8b 100644 --- a/Reply/F.go +++ b/Reply/F.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "io/fs" "net" "net/http" "net/http/pprof" @@ -961,19 +962,34 @@ func init() { return } - f := file.New("emots/"+strings.TrimPrefix(r.URL.Path, spath+"emots/"), 0, true).CheckRoot("emots/") - if !f.IsExist() { - w.WriteHeader(http.StatusNotFound) - return + ref := "" + if u, e := url.Parse(r.Header.Get("Referer")); e == nil { + ref = u.Query().Get("ref") + if ref != "now" { + if s, ok := c.C.K_v.LoadV(`直播流保存位置`).(string); ok && s != "" { + if strings.HasSuffix(s, "/") || strings.HasSuffix(s, "\\") { + ref = s + ref + "/" + } else { + ref = s + "/" + ref + "/" + } + } + } else { + ref = "" + } } + emoteDir := replyFunc.DanmuEmotes.GetEmotesDir(ref) - // mod - if info, e := f.Stat(); e == nil && pweb.NotModified(r, w, info.ModTime()) { - return + if f, e := emoteDir.Open(strings.TrimPrefix(r.URL.Path, spath+"emots/")); e != nil { + if errors.Is(e, fs.ErrNotExist) { + w.WriteHeader(http.StatusNotFound) + } else { + w.WriteHeader(http.StatusBadRequest) + } + } else if info, e := f.Stat(); e != nil { + w.WriteHeader(http.StatusNotFound) + } else if !pweb.NotModified(r, w, info.ModTime()) { + _, _ = io.Copy(w, f) } - - b, _ := f.ReadAll(humanize.KByte, humanize.MByte) - _, _ = w.Write(b) }) // 直播流播放器 @@ -1437,6 +1453,11 @@ func StartRecDanmu(ctx context.Context, filePath string) { // Ass replyFunc.Ass.ToAss(filePath) + // emots + if e := replyFunc.DanmuEmotes.PackEmotes(filePath); e != nil { + msglog.L(`E: `, e) + } + Recoder.Stop() } diff --git a/Reply/F/comp.go b/Reply/F/comp.go index 06cb920..2b923e5 100644 --- a/Reply/F/comp.go +++ b/Reply/F/comp.go @@ -2,6 +2,7 @@ package f import ( "context" + "io/fs" "iter" "net/http" "time" @@ -68,4 +69,6 @@ var DanmuEmotes = comp.Get[interface { Hashr(s string) (r string) SetLayerN(n int) IsErrNoEmote(e error) bool + PackEmotes(dir string) error + GetEmotesDir(dir string) fs.FS }](`danmuEmotes`) diff --git a/Reply/F/danmuEmotes/danmuEmotes.go b/Reply/F/danmuEmotes/danmuEmotes.go index 4ad1720..9a08fc9 100644 --- a/Reply/F/danmuEmotes/danmuEmotes.go +++ b/Reply/F/danmuEmotes/danmuEmotes.go @@ -1,11 +1,19 @@ package danmuemotes import ( + "archive/zip" + "bytes" "context" "encoding/json" "errors" + "io" + "io/fs" + "iter" + "regexp" + "strconv" "strings" + "github.com/dustin/go-humanize" c "github.com/qydysky/bili_danmu/CV" comp "github.com/qydysky/part/component2" file "github.com/qydysky/part/file" @@ -23,6 +31,8 @@ type TargetInterface interface { Hashr(s string) (r string) SetLayerN(n int) IsErrNoEmote(e error) bool + PackEmotes(dir string) error + GetEmotesDir(dir string) fs.FS } func init() { @@ -165,3 +175,94 @@ func (t *danmuEmotes) Hashr(s string) (r string) { } return string(rr) } + +func (t *danmuEmotes) PackEmotes(dir string) error { + var ( + w *zip.Writer + set = make(map[string]struct{}) + r, _ = regexp.Compile(`\[.*?\]`) + ) + + for line := range loadCsv(dir, "0.csv") { + for _, v := range r.FindAllString(line.Text, -1) { + key := t.Hashr(v) + if _, ok := set[key]; ok { + continue + } else { + set[key] = struct{}{} + } + + f := file.New(t.Dir+key+".png", 0, false) + if f.IsExist() { + if w == nil { + f := file.Open(dir + "emotes.zip") + if f.IsExist() { + _ = f.Delete() + } + w = zip.NewWriter(f.File()) + defer w.Close() + } + if iw, e := w.Create(key + ".png"); e != nil { + return e + } else if _, e := io.Copy(iw, f); e != nil { + return e + } + } + } + } + return nil +} + +func (t *danmuEmotes) GetEmotesDir(dir string) fs.FS { + if dir != "" && file.IsExist(dir+"emotes.zip") { + if rc, e := zip.OpenReader(dir + "emotes.zip"); e == nil { + return rc + } + } + return file.DirFS(t.Dir) +} + +func loadCsv(savePath string, filename ...string) iter.Seq[Data] { + return func(yield func(Data) bool) { + csvf := file.New(savePath+append(filename, "0.csv")[0], 0, false) + defer csvf.Close() + + if !csvf.IsExist() { + return + } + + var data = Data{} + for i := 0; true; i += 1 { + if line, e := csvf.ReadUntil([]byte{'\n'}, humanize.KByte, humanize.MByte); len(line) != 0 { + lined := bytes.SplitN(line, []byte{','}, 3) + if len(lined) == 3 { + if t, e := strconv.ParseFloat(string(lined[0]), 64); e == nil { + if e := json.Unmarshal(lined[2], &data); e == nil { + data.Time = t + if data.Style.Color == "" { + data.Style.Color = "#FFFFFF" + } + if !yield(data) { + return + } + } + } + } + } else if e != nil { + break + } + } + } +} + +type DataStyle struct { + Color string `json:"color"` + Border bool `json:"border"` + Mode int `json:"mode"` +} + +type Data struct { + Text string `json:"text"` + Style DataStyle `json:"style"` + Time float64 `json:"time"` +} diff --git a/go.mod b/go.mod index 18d2b8f..8b6d52c 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24 require ( github.com/gotk3/gotk3 v0.6.4 github.com/mdp/qrterminal/v3 v3.2.0 - github.com/qydysky/part v0.28.20250330170611 + github.com/qydysky/part v0.28.20250406180726 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 golang.org/x/text v0.23.0 // indirect diff --git a/go.sum b/go.sum index add27d5..e8beba5 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/qydysky/biliApi v0.0.0-20250406112014-bf8c070170f6 h1:eWklz9YhqcLnJeH github.com/qydysky/biliApi v0.0.0-20250406112014-bf8c070170f6/go.mod h1:1FbgCj+aOwIvuRRuX/l5uTLb3JIwWyJSa0uEfwpYV/8= github.com/qydysky/brotli v0.0.0-20240828134800-e9913a6e7ed9 h1:k451T+bpsLr+Dq9Ujo+Qtx0iomRA1XXS5ttlEojvfuQ= github.com/qydysky/brotli v0.0.0-20240828134800-e9913a6e7ed9/go.mod h1:cI8/gy/wjy2Eb+p2IUj2ZuDnC8R5Vrx3O0VMPvMvphA= -github.com/qydysky/part v0.28.20250330170611 h1:8ll4oVALYXi0wFce12r8BkYRdlw8U50VZs7FI6AZTog= -github.com/qydysky/part v0.28.20250330170611/go.mod h1:RHYTy8EbqCP6OioVf6BkvFcfWLNO0S220zl0DDlY84Y= +github.com/qydysky/part v0.28.20250406180726 h1:uCHzGPpH3USZR7tilD5H0h07DjZNMF4XU2K+U+Q7TIc= +github.com/qydysky/part v0.28.20250406180726/go.mod h1:RHYTy8EbqCP6OioVf6BkvFcfWLNO0S220zl0DDlY84Y= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= -- 2.39.2