run: |
sudo apt-get update
sudo apt-get install libgtk-3-dev libcairo2-dev libglib2.0-dev
+ export r=213
go get .
go mod vendor
- CGO_ENABLED=0 go test -v --cover -coverprofile=coverage ./...
+ CGO_ENABLED=1 go test -v --cover -coverprofile=coverage -race -count=1 ./...
- name: Codecov
uses: codecov/codecov-action@v4
run: |
sudo apt-get update
sudo apt-get install libgtk-3-dev libcairo2-dev libglib2.0-dev
+ export r=213
go get .
go mod vendor
- CGO_ENABLED=0 go test -v --cover -coverprofile=coverage ./...
+ CGO_ENABLED=1 go test -v --cover -coverprofile=coverage -race -count=1 ./...
- name: Codecov
uses: codecov/codecov-action@v4
// 功能区
-// 显示营收
-func init() {
- if !IsOn("统计营收") {
- return
- }
- go func() {
- var ShowRev = make(map[int]float64)
-
- clog := c.C.Log.Base_add(`营收`)
- for {
- if _, ok := ShowRev[c.C.Roomid]; !ok && c.C.Roomid != 0 {
- ShowRev[c.C.Roomid] = 0
- }
- for room, rev := range ShowRev {
- if c.C.Roomid != room {
- clog.L(`I: `, fmt.Sprintf("%d ¥%.2f", room, c.C.Rev))
- delete(ShowRev, room)
- } else if c.C.Rev != rev {
- ShowRev[room] = c.C.Rev
- clog.L(`I: `, fmt.Sprintf("%d ¥%.2f", room, c.C.Rev))
- }
- }
- time.Sleep(time.Minute)
- }
- }()
-}
-
// 获取实例的Common
func StreamOCommon(roomid int) (array []*c.Common) {
if roomid != -1 { //返回特定房间
_ "github.com/qydysky/bili_danmu/Reply/F/danmuEmotes"
_ "github.com/qydysky/bili_danmu/Reply/F/danmuji"
_ "github.com/qydysky/bili_danmu/Reply/F/parseM3u8"
+ _ "github.com/qydysky/bili_danmu/Reply/F/rev"
_ "github.com/qydysky/bili_danmu/Reply/F/videoFastSeed"
comp "github.com/qydysky/part/component2"
log "github.com/qydysky/part/log"
)
+var Rev = comp.Get[interface {
+ Init(l *log.Log_interface)
+ ShowRev(roomid int, rev float64)
+}](`rev`)
+
var DanmuCountPerMin = comp.Get[interface {
// will WriteHeader
GetRec(savePath string, r *http.Request, w http.ResponseWriter) error
--- /dev/null
+package rev
+
+import (
+ "fmt"
+ "sync"
+ "time"
+
+ comp "github.com/qydysky/part/component2"
+ log "github.com/qydysky/part/log"
+)
+
+func init() {
+ comp.RegisterOrPanic[interface {
+ Init(l *log.Log_interface)
+ ShowRev(roomid int, rev float64)
+ }](`rev`, &rev{})
+}
+
+type rev struct {
+ l *log.Log_interface
+ currentRoom int
+ currentRev float64
+ lastShow time.Time
+ sync.Mutex
+}
+
+func (t *rev) Init(l *log.Log_interface) {
+ t.Lock()
+ defer t.Unlock()
+
+ t.l = l.Base(`营收`)
+}
+
+func (t *rev) ShowRev(roomid int, rev float64) {
+ t.Lock()
+ defer t.Unlock()
+
+ if t.l == nil {
+ return
+ }
+
+ if roomid != t.currentRoom {
+ if t.currentRoom != 0 {
+ t.l.L(`I: `, fmt.Sprintf("%d ¥%.2f", t.currentRoom, t.currentRev))
+ }
+ t.l.L(`I: `, fmt.Sprintf("%d ¥%.2f", roomid, rev))
+ } else if rev != t.currentRev && time.Since(t.lastShow).Minutes() > 1 {
+ t.lastShow = time.Now()
+ t.l.L(`I: `, fmt.Sprintf("%d ¥%.2f", roomid, rev))
+ }
+ t.currentRev = rev
+ t.currentRoom = roomid
+}
"compress/zlib"
"context"
"encoding/json"
- "errors"
"fmt"
"strconv"
"strings"
)
var reply_log = c.C.Log.Base(`Reply`)
-var ErrDecode = errors.New(`ErrDecode`)
// brotliDecoder
type brotliDecoder struct {
ActionGenFastSeedFmp4 pe.Action = `GenFastSeedFmp4`
ActionSeekFmp4 pe.Action = `SeekFmp4`
ActionOneFFmp4 pe.Action = `OneFFmp4`
+ ActionCheckTFail pe.Action = `CheckTFail`
)
var boxs map[string]bool
}
//is t error?
- check_set_maxT = func(ts timeStamp, equal func(ts timeStamp) error, larger func(ts timeStamp) error) (err error) {
+ checkAndSetMaxT = func(ts timeStamp) (err error) {
switch ts.handlerType {
case 'v':
if maxVT == 0 {
maxVT = ts.getT()
- } else if maxVT == ts.getT() && equal != nil {
- err = equal(ts)
- } else if maxVT > ts.getT() && larger != nil {
- err = larger(ts)
+ } else if maxVT == ts.getT() {
+ err = ActionCheckTFail.New("equal VT detect")
+ } else if maxVT > ts.getT() {
+ err = ActionCheckTFail.New("lower VT detect")
} else {
maxVT = ts.getT()
}
case 'a':
if maxAT == 0 {
maxAT = ts.getT()
- } else if maxAT == ts.getT() && equal != nil {
- err = equal(ts)
- } else if maxAT > ts.getT() && larger != nil {
- err = larger(ts)
+ } else if maxAT == ts.getT() {
+ err = ActionCheckTFail.New("equal AT detect")
+ } else if maxAT > ts.getT() {
+ err = ActionCheckTFail.New("lower AT detect")
} else {
maxAT = ts.getT()
}
}
return
}
+
+ dropKeyFrame = func(index int) {
+ t.buf.Reset()
+ haveKeyframe = false
+ cu = index
+ }
)
ies, e := decode(buf, "moof")
if ts.handlerType == 'v' {
if e := checkSampleEntries(m[5].i, m[6].i); e != nil {
//skip
- t.buf.Reset()
- haveKeyframe = false
- cu = m[0].i
- return pe.Join(ErrDealIESkip, e)
+ dropKeyFrame(m[0].e)
+ return pe.Join(ErrDecode, e)
}
}
- if e := check_set_maxT(ts, func(_ timeStamp) error {
- return errors.New("skip")
- }, func(_ timeStamp) error {
- t.buf.Reset()
- haveKeyframe = false
- cu = m[0].i
- return errors.New("skip")
- }); e != nil {
- return pe.Join(ErrDealIESkip, e)
+ if e := checkAndSetMaxT(ts); e != nil {
+ dropKeyFrame(m[0].e)
+ return pe.Join(ErrDecode, e)
}
}
if keyframeMoof {
if v, e := t.buf.HadModified(bufModified); e == nil && v && !t.buf.IsEmpty() {
if e := t.buf.AppendTo(keyframe); e != nil {
- return pe.Join(ErrDealIEBreak, e)
+ return e
}
- cu = m[0].i
- t.buf.Reset()
+ dropKeyFrame(m[0].i)
}
haveKeyframe = true
} else if !haveKeyframe {
}
if haveKeyframe {
if e := t.buf.Append(buf[m[0].i:m[6].e]); e != nil {
- return pe.Join(ErrDealIEBreak, e)
+ return e
}
}
return nil
if handlerType == 'v' {
if e := checkSampleEntries(m[5].i, m[6].i); e != nil {
//skip
- t.buf.Reset()
- haveKeyframe = false
- cu = m[0].i
- return pe.Join(ErrDealIESkip, e)
+ dropKeyFrame(m[0].e)
+ return pe.Join(ErrDecode, e)
}
}
switch handlerType {
case 's':
audio = ts
}
- if e := check_set_maxT(ts, func(_ timeStamp) error {
- return errors.New("skip")
- }, func(_ timeStamp) error {
- t.buf.Reset()
- haveKeyframe = false
- cu = m[0].i
- return errors.New("skip")
- }); e != nil {
- return pe.Join(ErrDealIESkip, e)
+ if e := checkAndSetMaxT(ts); e != nil {
+ dropKeyFrame(m[0].e)
+ return pe.Join(ErrDecode, e)
}
}
{
if handlerType == 'v' {
if e := checkSampleEntries(m[9].i, m[10].i); e != nil {
//skip
- t.buf.Reset()
- haveKeyframe = false
- cu = m[0].i
- return pe.Join(ErrDealIESkip, e)
+ dropKeyFrame(m[0].e)
+ return pe.Join(ErrDecode, e)
}
}
switch handlerType {
case 's':
audio = ts
}
- if e := check_set_maxT(ts, func(_ timeStamp) error {
- return errors.New("skip")
- }, func(_ timeStamp) error {
- t.buf.Reset()
- haveKeyframe = false
- cu = m[0].i
- return errors.New("skip")
- }); e != nil {
- return pe.Join(ErrDealIESkip, e)
+ if e := checkAndSetMaxT(ts); e != nil {
+ dropKeyFrame(m[0].e)
+ return pe.Join(ErrDecode, e)
}
}
t.AVTDiff = 0.1
}
if diff := math.Abs(video.getT() - audio.getT()); diff > t.AVTDiff {
- return pe.Join(ErrDealIEBreak, fmt.Errorf("时间戳不匹配 %v %v (或许应调整fmp4音视频时间戳容差s>%.2f)", video.timeStamp, audio.timeStamp, diff))
+ return pe.Join(ErrDecode, fmt.Errorf("时间戳不匹配 %v %v (或许应调整fmp4音视频时间戳容差s>%.2f)", video.timeStamp, audio.timeStamp, diff))
// copy(video.data, F.Itob64(int64(audio.getT()*float64(video.timescale))))
}
if keyframeMoof {
if v, e := t.buf.HadModified(bufModified); e == nil && v && !t.buf.IsEmpty() {
if e := t.buf.AppendTo(keyframe); e != nil {
- return pe.Join(ErrDealIEBreak, e)
+ return e
}
- cu = m[0].i
- t.buf.Reset()
+ dropKeyFrame(m[0].i)
}
haveKeyframe = true
} else if !haveKeyframe {
}
if haveKeyframe {
if e := t.buf.Append(buf[m[0].i:m[10].e]); e != nil {
- return pe.Join(ErrDealIEBreak, e)
+ return e
}
}
return nil
}
//is t error?
- check_set_maxT = func(ts timeStamp, equal func(ts timeStamp) error, larger func(ts timeStamp) error) (err error) {
+ checkAndSetMaxT = func(ts timeStamp) (err error) {
switch ts.handlerType {
case 'v':
if maxVT == 0 {
maxVT = ts.getT()
- } else if maxVT == ts.getT() && equal != nil {
- err = equal(ts)
- } else if maxVT > ts.getT() && larger != nil {
- err = larger(ts)
+ } else if maxVT == ts.getT() {
+ err = ActionCheckTFail.New("equal VT detect")
+ } else if maxVT > ts.getT() {
+ err = ActionCheckTFail.New("lower VT detect")
} else {
maxVT = ts.getT()
}
case 'a':
if maxAT == 0 {
maxAT = ts.getT()
- } else if maxAT == ts.getT() && equal != nil {
- err = equal(ts)
- } else if maxAT > ts.getT() && larger != nil {
- err = larger(ts)
+ } else if maxAT == ts.getT() {
+ err = ActionCheckTFail.New("equal AT detect")
+ } else if maxAT > ts.getT() {
+ err = ActionCheckTFail.New("lower AT detect")
} else {
maxAT = ts.getT()
}
}
return
}
+
+ dropKeyFrame = func(index int) {
+ t.buf.Reset()
+ haveKeyframe = false
+ cu = index
+ }
)
ies, e := decode(buf, "moof")
return 0, e
}
- var ErrNormal = pe.New("ErrNormal", "ErrNormal")
+ var ErrNormal pe.Action = "ErrNormal"
err = deals(ies,
[]dealIE{
if ts.handlerType == 'v' {
if e := checkSampleEntries(m[5].i, m[6].i); e != nil {
//skip
- t.buf.Reset()
- haveKeyframe = false
- cu = m[0].i
- return pe.Join(ErrDealIESkip.New(), e)
+ dropKeyFrame(m[0].e)
+ return pe.Join(ErrDecode, e)
}
}
if handlerType == 'v' {
video = ts
}
- if e := check_set_maxT(ts, func(_ timeStamp) error {
- return errors.New("skip")
- }, func(_ timeStamp) error {
- t.buf.Reset()
- haveKeyframe = false
- cu = m[0].i
- return errors.New("skip")
- }); e != nil {
- return pe.Join(ErrDealIESkip.New(), e)
+ if e := checkAndSetMaxT(ts); e != nil {
+ dropKeyFrame(m[0].e)
+ return pe.Join(ErrDecode, e)
}
}
//deal frame
if keyframeMoof {
if v, e := t.buf.HadModified(bufModified); e == nil && v && !t.buf.IsEmpty() {
- cu = m[0].i
if haveKeyframe && len(w) > 0 {
err = w[0](video.getT(), cu, t.buf)
- t.buf.Reset()
+ dropKeyFrame(m[0].i)
return ErrNormal
}
- t.buf.Reset()
+ dropKeyFrame(m[0].i)
}
haveKeyframe = true
} else if !haveKeyframe {
}
if haveKeyframe {
if e := t.buf.Append(buf[m[0].i:m[6].e]); e != nil {
- return pe.Join(ErrDealIEBreak.New(), e)
+ return e
}
}
return nil
if handlerType == 'v' {
if e := checkSampleEntries(m[5].i, m[6].i); e != nil {
//skip
- t.buf.Reset()
- haveKeyframe = false
- cu = m[0].i
- return pe.Join(ErrDealIESkip, e)
+ dropKeyFrame(m[0].e)
+ return pe.Join(ErrDecode, e)
}
}
switch handlerType {
case 's':
audio = ts
}
- if e := check_set_maxT(ts, func(_ timeStamp) error {
- return errors.New("skip")
- }, func(_ timeStamp) error {
- t.buf.Reset()
- haveKeyframe = false
- cu = m[0].i
- return errors.New("skip")
- }); e != nil {
- return pe.Join(ErrDealIESkip, e)
+ if e := checkAndSetMaxT(ts); e != nil {
+ dropKeyFrame(m[0].e)
+ return pe.Join(ErrDecode, e)
}
}
{
if handlerType == 'v' {
if e := checkSampleEntries(m[9].i, m[10].i); e != nil {
//skip
- t.buf.Reset()
- haveKeyframe = false
- cu = m[0].i
- return pe.Join(ErrDealIESkip, e)
+ dropKeyFrame(m[0].e)
+ return pe.Join(ErrDecode, e)
}
}
switch handlerType {
case 's':
audio = ts
}
- if e := check_set_maxT(ts, func(_ timeStamp) error {
- return errors.New("skip")
- }, func(_ timeStamp) error {
- t.buf.Reset()
- haveKeyframe = false
- cu = m[0].i
- return errors.New("skip")
- }); e != nil {
- return pe.Join(ErrDealIESkip, e)
+ if e := checkAndSetMaxT(ts); e != nil {
+ dropKeyFrame(m[0].e)
+ return pe.Join(ErrDecode, e)
}
}
t.AVTDiff = 0.1
}
if diff := math.Abs(video.getT() - audio.getT()); diff > t.AVTDiff {
- return pe.Join(ErrDealIEBreak, fmt.Errorf("时间戳不匹配 %v %v (或许应调整fmp4音视频时间戳容差s>%.2f)", video.timeStamp, audio.timeStamp, diff))
+ return pe.Join(ErrDecode, fmt.Errorf("时间戳不匹配 %v %v (或许应调整fmp4音视频时间戳容差s>%.2f)", video.timeStamp, audio.timeStamp, diff))
// copy(video.data, F.Itob64(int64(audio.getT()*float64(video.timescale))))
}
//deal frame
if keyframeMoof {
if v, e := t.buf.HadModified(bufModified); e == nil && v && !t.buf.IsEmpty() {
- cu = m[0].i
if haveKeyframe && len(w) > 0 {
err = w[0](video.getT(), cu, t.buf)
- t.buf.Reset()
+ dropKeyFrame(m[0].i)
return ErrNormal
}
- t.buf.Reset()
+ dropKeyFrame(m[0].i)
}
haveKeyframe = true
} else if !haveKeyframe {
}
if haveKeyframe {
if e := t.buf.Append(buf[m[0].i:m[10].e]); e != nil {
- return pe.Join(ErrDealIEBreak, e)
+ return e
}
}
return nil
return
}
-var (
- ErrDealIEBreak = pe.Action(`ErrDealIEBreak`)
- ErrDealIESkip = pe.Action(`ErrDealIESkip`)
-)
-
type dealIE struct {
matchCounts int
boxNames []string
func deals(ies []ie, dealIEs []dealIE) (err error) {
for cu := 0; cu < len(ies) && len(dealIEs) != 0; cu++ {
for i := 0; i < len(dealIEs); i++ {
- if e := dealIEs[i].deal(ies, cu); e != nil && !pe.Catch(e, ErrDealIESkip) {
+ if e := dealIEs[i].deal(ies, cu); e != nil {
return e
}
}
pstring "github.com/qydysky/part/strings"
pu "github.com/qydysky/part/util"
pweb "github.com/qydysky/part/web"
+ "slices"
)
const (
}
var (
- AEFDCTO perrors.Action = `ActionErrFmp4DownloadCareTO`
+ ActionErrFmp4DownloadCareTO perrors.Action = `ActionErrFmp4DownloadCareTO`
)
func (link *m4s_link_item) download(reqPool *pool.Buf[reqf.Req], reqConfig reqf.Rval) (err error) {
return e
} else {
if int64(reqConfig.Timeout) < r.UsedTime.Milliseconds()+3000 {
- err = perrors.New(fmt.Sprintf("fmp4切片下载超时s(%d)或许应该大于%d", reqConfig.Timeout/1000, (r.UsedTime.Milliseconds()+4000)/1000), AEFDCTO)
+ err = ActionErrFmp4DownloadCareTO.New(fmt.Sprintf("fmp4切片下载超时s(%d)或许应该大于%d", reqConfig.Timeout/1000, (r.UsedTime.Milliseconds()+4000)/1000))
}
link.status = 2 // 设置切片状态为下载完成
return
}
}
+var ErrDecode = perrors.Action("ErrDecode")
+
func (t *M4SStream) saveStream() (e error) {
// 清除初始值
t.first_buf = nil
`Connection`: `close`,
},
})
- if perrors.Catch(e, AEFDCTO) {
+ if ActionErrFmp4DownloadCareTO.Catch(e) {
t.log.L(`W: `, e.Error())
} else if e != nil {
downErr.Store(true)
}
}
t.putM4s(cu)
- download_seq = append(download_seq[:k], download_seq[k+1:]...)
+ download_seq = slices.Delete(download_seq, k, k+1)
k -= 1
continue
} else if t.first_buf == nil {
t.putM4s(cu)
- download_seq = append(download_seq[:k], download_seq[k+1:]...)
+ download_seq = slices.Delete(download_seq, k, k+1)
k -= 1
continue
}
t.log.L(`E: `, e)
}
t.putM4s(cu)
- download_seq = append(download_seq[:k], download_seq[k+1:]...)
+ download_seq = slices.Delete(download_seq, k, k+1)
k -= 1
buff, unlock := buf.GetPureBufRLock()
unlock()
if err != nil && !errors.Is(err, io.EOF) {
- t.log.L(`E: `, err)
- //丢弃所有数据
- buf.Reset()
- e = err
- if skipErrFrame {
+ if ErrDecode.Catch(err) && skipErrFrame {
+ t.log.L(`W: `, err)
// 将此切片服务器设置停用
// if u, e := url.Parse(cu.Url); e == nil {
t.common.DisableLiveAutoByUuid(cu.SerUuid)
// t.common.DisableLiveAuto(u.Host)
// }
} else {
- return
+ e = err
+ download_seq = download_seq[:0]
+ _ = pctx.CallCancel(t.Status)
+ break
}
}
//保存弹幕
go StartRecDanmu(ctx1, ms.GetSavePath())
+ //指定房间录制回调
+ if v, ok := ms.common.K_v.LoadV("指定房间录制回调").([]any); ok && len(v) > 0 {
+ l := l.Base(`录制回调`)
+ for i := 0; i < len(v); i++ {
+ if vm, ok := v[i].(map[string]any); ok {
+ if roomid, ok := vm["roomid"].(float64); ok && int(roomid) == ms.common.Roomid {
+ var (
+ durationS, _ = vm["durationS"].(float64)
+ start, _ = vm["start"].([]any)
+ )
+ if len(start) >= 2 && durationS >= 0 {
+ go func() {
+ ctx2, done2 := pctx.WaitCtx(ctx1)
+ defer done2()
+ select {
+ case <-ctx2.Done():
+ case <-time.After(time.Second * time.Duration(durationS)):
+ var cmds []string
+ for i := 0; i < len(start); i++ {
+ if cmd, ok := start[i].(string); ok && cmd != "" {
+ cmds = append(cmds, strings.ReplaceAll(cmd, "{type}", ms.GetStreamType()))
+ }
+ }
+
+ cmd := exec.Command(cmds[0], cmds[1:]...)
+ cmd.Dir = ms.GetSavePath()
+ l.L(`I: `, "启动", cmd.Args)
+ if e := cmd.Run(); e != nil {
+ l.L(`E: `, e)
+ }
+ l.L(`I: `, "结束")
+ }
+ }()
+ }
+ }
+ }
+ }
+ }
+
path := ms.GetSavePath() + `0.` + ms.GetStreamType()
startT := time.Now()
if e := ms.PusherToFile(ctx1, path, startf, stopf); e != nil {
ws "github.com/qydysky/part/websocket"
)
-func Start() {
+func Start(rootCtx context.Context) {
danmulog := c.C.Log.Base(`bilidanmu`)
danmulog.L(`I: `, `当前PID:`, c.C.PID)
danmulog.L(`I: `, "version: ", c.C.Version)
reply.KeepMedalLight(mainCtx, c.C)
//ass初始化
replyFunc.Ass.Init(c.C.K_v.LoadV("Ass"))
+ //rev初始化
+ if c.C.IsOn(`统计营收`) {
+ replyFunc.Rev.Init(danmulog)
+ }
// 指定房间录制区间
if _, err := recStartEnd.InitF.Run(mainCtx, c.C); err != nil {
danmulog.Base("功能", "指定房间录制区间").L(`E: `, err)
common, ok := c.Commons.LoadV(c.C.Roomid).(*c.Common)
if ok {
common.Rev += rev.Rev
+ // 显示营收
+ replyFunc.Rev.ShowRev(common.Roomid, common.Rev)
}
}
return false
common.Rev = 0.0 // 营收
}
- exitSign = entryRoom(mainCtx, danmulog.BaseAdd(common.Roomid), common)
+ exitSign = entryRoom(rootCtx, mainCtx, danmulog.BaseAdd(common.Roomid), common)
common.InIdle = true
}
}
-func entryRoom(mainCtx context.Context, danmulog *part.Log_interface, common *c.Common) (exitSign bool) {
+func entryRoom(rootCtx, mainCtx context.Context, danmulog *part.Log_interface, common *c.Common) (exitSign bool) {
//附加功能 自动发送即将过期礼物
go reply.AutoSend_silver_gift(common)
//获取热门榜
replyFunc.Danmuji.Danmuji_auto(mainCtx, c.C.K_v.LoadV(`自动弹幕机_内容`).([]any), c.C.K_v.LoadV(`自动弹幕机_发送间隔s`).(float64), reply.Msg_senddanmu)
}
{ //附加功能 进房间发送弹幕 直播流保存 每日签到
+ F.RoomEntryAction(common.Roomid)
// go F.Dosign()
- go reply.Entry_danmu(common)
+ reply.Entry_danmu(common)
if _, e := recStartEnd.RecStartCheck.Run(mainCtx, common); e == nil {
- go reply.StreamOStart(common, common.Roomid)
+ reply.StreamOStart(common, common.Roomid)
} else {
danmulog.Base("功能", "指定房间录制区间").L(`I: `, common.Roomid, e)
}
- go F.RoomEntryAction(common.Roomid)
}
//当前ws
},
})
+ danmulog.L(`T: `, "启动完成", common.Uname, `(`, common.Roomid, `)`)
+
{
cancel, c := wsmsg.Pull_tag_chan(`exit`, 1, mainCtx)
- <-c
+ select {
+ case <-c:
+ case <-rootCtx.Done():
+ common.Danmu_Main_mq.Push_tag(`interrupt`, nil)
+ <-c
+ }
cancel()
}
package main
import (
+ "context"
+
q "github.com/qydysky/bili_danmu"
)
func main() {
- q.Start()
+ q.Start(context.Background())
}
--- /dev/null
+package main
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ q "github.com/qydysky/bili_danmu"
+)
+
+// go test -run ^TestMain$ github.com/qydysky/bili_danmu/demo -race -count=1 -v -r xxx
+func TestMain(m *testing.T) {
+ ctx, c := context.WithTimeout(context.Background(), time.Second*40)
+ defer c()
+ q.Start(ctx)
+}
require (
github.com/gotk3/gotk3 v0.6.4
github.com/mdp/qrterminal/v3 v3.2.0
- github.com/qydysky/part v0.28.20250313160332
+ github.com/qydysky/part v0.28.20250330170611
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
require (
github.com/google/uuid v1.6.0
- github.com/qydysky/biliApi v0.0.0-20240725184407-15076dddb6fb
+ github.com/qydysky/biliApi v0.0.0-20250406112014-bf8c070170f6
github.com/qydysky/brotli v0.0.0-20240828134800-e9913a6e7ed9
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f
)
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/qydysky/biliApi v0.0.0-20240725184407-15076dddb6fb h1:dtSpNF9hLQa09TUfR+xbYFkHcx2breAFsXeU7e599gE=
-github.com/qydysky/biliApi v0.0.0-20240725184407-15076dddb6fb/go.mod h1:om024vfxALQ5vxsbaGoMm8IS0esLYBnEOpJI8FsGoDg=
+github.com/qydysky/biliApi v0.0.0-20250406112014-bf8c070170f6 h1:eWklz9YhqcLnJeHxWSlJZmL2V5rRyyEVqLKgLY3ipQs=
+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.20250313160332 h1:3YwhIZwtIrnULREneqmjdQnL7vB1SxYZGVAZUiJAVZU=
-github.com/qydysky/part v0.28.20250313160332/go.mod h1:RHYTy8EbqCP6OioVf6BkvFcfWLNO0S220zl0DDlY84Y=
+github.com/qydysky/part v0.28.20250330170611 h1:8ll4oVALYXi0wFce12r8BkYRdlw8U50VZs7FI6AZTog=
+github.com/qydysky/part v0.28.20250330170611/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=