Merge pull request #9 from AynaLivePlayer/dev

merge 1.0.6
This commit is contained in:
Aynakeya
2024-05-06 09:50:38 +08:00
committed by GitHub
37 changed files with 901 additions and 154 deletions

4
.gitignore vendored
View File

@@ -6,4 +6,6 @@ music
/txtinfo/
CMakeCache.txt
/config/
/release/
/release/
log.txt
go.sum

View File

@@ -42,7 +42,9 @@ git submodule set-url pkg/liveroom-sdk https://github.com/AynaLivePlayer/liveroo
git submodule update
```
6. now you can build (please check makefile for more details)
```bash
```powershell
$env:CGO_LDFLAGS="-LC:\Users\vboxuser\Desktop\AynaLivePlayer\libmpv\lib"
$env:CGO_CFLAGS="-IC:\Users\vboxuser\Desktop\AynaLivePlayer\libmpv\include"
# ... more setup, see makefile
go build -o AynaLivePlayer -ldflags -H=windowsgui app/main.go
```

View File

@@ -37,7 +37,7 @@ var Log = &_LogConfig{
func setupGlobal() {
global.EventManager = event.NewManger(128, 16)
global.Logger = loggerRepo.NewZapColoredLogger()
global.Logger = loggerRepo.NewZapColoredLogger(Log.Path, !*dev)
global.Logger.SetLogLevel(Log.Level)
}

10
app/uwu/a.html Normal file
View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@@ -276,6 +276,10 @@
"en": "Show",
"zh-CN": "打开"
},
"gui.update.already_latest_version": {
"en": "no update available",
"zh-CN": "没有可用更新"
},
"gui.update.new_version": {
"en": "New Version Available",
"zh-CN": "有新版本可用"
@@ -317,8 +321,8 @@
"zh-CN": "点歌冷却"
},
"plugin.diange.custom_cmd": {
"en": "Custom Command (Default one still works)",
"zh-CN": "自定义命令 (默认的依然可用)"
"en": "Custom Command",
"zh-CN": "自定义命令"
},
"plugin.diange.description": {
"en": "Basic Diange Configuration",
@@ -484,53 +488,57 @@
"en": "Text Output",
"zh-CN": "文本输出"
},
"plugin.webinfo.autostart": {
"plugin.wshub.autostart": {
"en": "Auto start",
"zh-CN": "自动启用"
},
"plugin.webinfo.description": {
"en": "Web output configuration",
"zh-CN": "web输出设置"
"plugin.wshub.description": {
"en": "Websocket Hub Configuration",
"zh-CN": "Websocket服务器设置"
},
"plugin.webinfo.port": {
"plugin.wshub.local_host_only": {
"en": "only allow local host connection",
"zh-CN": "只允许本地连接"
},
"plugin.wshub.port": {
"en": "Port",
"zh-CN": "服务器端口"
},
"plugin.webinfo.server_control": {
"plugin.wshub.server_control": {
"en": "Control",
"zh-CN": "操作"
},
"plugin.webinfo.server_control.restart": {
"plugin.wshub.server_control.restart": {
"en": "Restart",
"zh-CN": "重启"
},
"plugin.webinfo.server_control.start": {
"plugin.wshub.server_control.start": {
"en": "Start",
"zh-CN": "启动"
},
"plugin.webinfo.server_control.stop": {
"plugin.wshub.server_control.stop": {
"en": "Stop",
"zh-CN": "停止"
},
"plugin.webinfo.server_preview": {
"en": "Server Preview",
"zh-CN": "效果预览"
"plugin.wshub.server_link": {
"en": "Websocket Server Link",
"zh-CN": "Websocket服务器链接"
},
"plugin.webinfo.server_status": {
"plugin.wshub.server_status": {
"en": "Server Status",
"zh-CN": "服务器状态"
},
"plugin.webinfo.server_status.running": {
"plugin.wshub.server_status.running": {
"en": "Running",
"zh-CN": "运行中"
},
"plugin.webinfo.server_status.stopped": {
"plugin.wshub.server_status.stopped": {
"en": "Stopped",
"zh-CN": "已停止"
},
"plugin.webinfo.title": {
"en": "Web Output",
"zh-CN": "Web输出"
"plugin.wshub.title": {
"en": "Websocket Hub",
"zh-CN": "Websocket服务器"
}
}
}

78
core/events/mapping.go Normal file
View File

@@ -0,0 +1,78 @@
package events
import (
"AynaLivePlayer/core/model"
"AynaLivePlayer/pkg/event"
"encoding/json"
"errors"
"reflect"
)
var EventsMapping = map[event.EventId]any{
LiveRoomAddCmd: LiveRoomAddCmdEvent{},
LiveRoomProviderUpdate: LiveRoomProviderUpdateEvent{},
LiveRoomRemoveCmd: LiveRoomRemoveCmdEvent{},
LiveRoomRoomsUpdate: LiveRoomRoomsUpdateEvent{},
LiveRoomStatusUpdate: LiveRoomStatusUpdateEvent{},
LiveRoomConfigChangeCmd: LiveRoomConfigChangeCmdEvent{},
LiveRoomOperationCmd: LiveRoomOperationCmdEvent{},
PlayerVolumeChangeCmd: PlayerVolumeChangeCmdEvent{},
PlayerPlayCmd: PlayerPlayCmdEvent{},
PlayerPlayErrorUpdate: PlayerPlayErrorUpdateEvent{},
PlayerSeekCmd: PlayerSeekCmdEvent{},
PlayerToggleCmd: PlayerToggleCmdEvent{},
PlayerSetPauseCmd: PlayerSetPauseCmdEvent{},
PlayerPlayNextCmd: PlayerPlayNextCmdEvent{},
PlayerLyricRequestCmd: PlayerLyricRequestCmdEvent{},
PlayerLyricReload: PlayerLyricReloadEvent{},
PlayerLyricPosUpdate: PlayerLyricPosUpdateEvent{},
PlayerPlayingUpdate: PlayerPlayingUpdateEvent{},
PlayerPropertyPauseUpdate: PlayerPropertyPauseUpdateEvent{},
PlayerPropertyPercentPosUpdate: PlayerPropertyPercentPosUpdateEvent{},
PlayerPropertyIdleActiveUpdate: PlayerPropertyIdleActiveUpdateEvent{},
PlayerPropertyTimePosUpdate: PlayerPropertyTimePosUpdateEvent{},
PlayerPropertyDurationUpdate: PlayerPropertyDurationUpdateEvent{},
PlayerPropertyVolumeUpdate: PlayerPropertyVolumeUpdateEvent{},
PlayerVideoPlayerSetWindowHandleCmd: PlayerVideoPlayerSetWindowHandleCmdEvent{},
PlayerSetAudioDeviceCmd: PlayerSetAudioDeviceCmdEvent{},
PlayerAudioDeviceUpdate: PlayerAudioDeviceUpdateEvent{},
PlaylistManagerSetSystemCmd: PlaylistManagerSetSystemCmdEvent{},
PlaylistManagerSystemUpdate: PlaylistManagerSystemUpdateEvent{},
PlaylistManagerRefreshCurrentCmd: PlaylistManagerRefreshCurrentCmdEvent{},
PlaylistManagerGetCurrentCmd: PlaylistManagerGetCurrentCmdEvent{},
PlaylistManagerCurrentUpdate: PlaylistManagerCurrentUpdateEvent{},
PlaylistManagerInfoUpdate: PlaylistManagerInfoUpdateEvent{},
PlaylistManagerAddPlaylistCmd: PlaylistManagerAddPlaylistCmdEvent{},
PlaylistManagerRemovePlaylistCmd: PlaylistManagerRemovePlaylistCmdEvent{},
MediaProviderUpdate: MediaProviderUpdateEvent{},
SearchCmd: SearchCmdEvent{},
SearchResultUpdate: SearchResultUpdateEvent{},
}
func init() {
for _, v := range []model.PlaylistID{model.PlaylistIDSystem, model.PlaylistIDPlayer, model.PlaylistIDHistory} {
EventsMapping[PlaylistDetailUpdate(v)] = PlaylistDetailUpdateEvent{}
EventsMapping[PlaylistMoveCmd(v)] = PlaylistMoveCmdEvent{}
EventsMapping[PlaylistSetIndexCmd(v)] = PlaylistSetIndexCmdEvent{}
EventsMapping[PlaylistDeleteCmd(v)] = PlaylistDeleteCmdEvent{}
EventsMapping[PlaylistInsertCmd(v)] = PlaylistInsertCmdEvent{}
EventsMapping[PlaylistInsertUpdate(v)] = PlaylistInsertUpdateEvent{}
EventsMapping[PlaylistNextCmd(v)] = PlaylistNextCmdEvent{}
EventsMapping[PlaylistNextUpdate(v)] = PlaylistNextUpdateEvent{}
EventsMapping[PlaylistModeChangeCmd(v)] = PlaylistModeChangeCmdEvent{}
EventsMapping[PlaylistModeChangeUpdate(v)] = PlaylistModeChangeUpdateEvent{}
}
}
func UnmarshalEventData(eventId event.EventId, data []byte) (any, error) {
val, ok := EventsMapping[eventId]
if !ok {
return nil, errors.New("event id not found")
}
newVal := reflect.New(reflect.TypeOf(val))
err := json.Unmarshal(data, newVal.Interface())
if err != nil {
return nil, err
}
return newVal.Elem().Interface(), nil
}

View File

@@ -0,0 +1,22 @@
package events
import (
"encoding/json"
"github.com/stretchr/testify/require"
"testing"
)
func TestUnmarshalEventData(t *testing.T) {
eventData := LiveRoomAddCmdEvent{
Title: "test",
Provider: "asdfasd",
RoomKey: "asdfasdf",
}
data, err := json.Marshal(eventData)
require.NoError(t, err)
val, err := UnmarshalEventData(LiveRoomAddCmd, data)
require.NoError(t, err)
resultData, ok := val.(LiveRoomAddCmdEvent)
require.True(t, ok)
require.Equal(t, eventData, resultData)
}

View File

@@ -16,6 +16,12 @@ type PlayerPlayCmdEvent struct {
Media model.Media
}
const PlayerPlayErrorUpdate = "update.player.play.error"
type PlayerPlayErrorUpdateEvent struct {
Error error
}
const PlayerSeekCmd = "cmd.player.op.seek"
type PlayerSeekCmdEvent struct {
@@ -31,6 +37,12 @@ const PlayerToggleCmd = "cmd.player.op.toggle"
type PlayerToggleCmdEvent struct {
}
const PlayerSetPauseCmd = "cmd.player.op.pause"
type PlayerSetPauseCmdEvent struct {
Pause bool
}
const PlayerPlayNextCmd = "cmd.player.op.next"
type PlayerPlayNextCmdEvent struct {

View File

@@ -22,6 +22,14 @@ type PlaylistMoveCmdEvent struct {
To int
}
func PlaylistSetIndexCmd(id model.PlaylistID) event.EventId {
return event.EventId("cmd.playlist.setindex." + id)
}
type PlaylistSetIndexCmdEvent struct {
Index int
}
func PlaylistDeleteCmd(id model.PlaylistID) event.EventId {
return event.EventId("cmd.playlist.delete." + id)
}

15
core/events/updater.go Normal file
View File

@@ -0,0 +1,15 @@
package events
import "AynaLivePlayer/core/model"
const CheckUpdateCmd = "cmd.update.check"
type CheckUpdateCmdEvent struct {
}
const CheckUpdateResultUpdate = "update.update.check"
type CheckUpdateResultUpdateEvent struct {
HasUpdate bool
Info model.VersionInfo
}

4
go.mod
View File

@@ -33,13 +33,13 @@ require (
require (
fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e // indirect
github.com/AynaLivePlayer/blivedm-go v0.0.0-20240408074929-6565ab41764b // indirect
github.com/AynaLivePlayer/blivedm-go v0.0.0-20240427041017-949a66917a81 // indirect
github.com/PuerkitoBio/goquery v1.7.1 // indirect
github.com/XiaoMengXinX/Music163Api-Go v0.1.30 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/andybalholm/cascadia v1.2.0 // indirect
github.com/aynakeya/deepcolor v1.0.2 // indirect
github.com/aynakeya/open-bilibili-live v0.0.5 // indirect
github.com/aynakeya/open-bilibili-live v0.0.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25 // indirect
github.com/fredbi/uri v1.0.0 // indirect

View File

@@ -4,6 +4,8 @@ import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/component"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"fyne.io/fyne/v2"
@@ -105,35 +107,23 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject {
outputDevice := container.NewBorder(nil, nil,
widget.NewLabel(i18n.T("gui.config.basic.audio_device")), nil,
deviceSel)
//skipWhenErr := container.NewHBox(
// widget.NewLabel(i18n.T("gui.config.basic.skip_when_error")),
// component.NewCheckOneWayBinding(
// i18n.T("gui.config.basic.skip_when_error.prompt"),
// &API.PlayControl().Config().AutoNextWhenFail,
// API.PlayControl().Config().AutoNextWhenFail),
//)
//checkUpdateBox := container.NewHBox(
// widget.NewLabel(i18n.T("gui.config.basic.auto_check_update")),
// component.NewCheckOneWayBinding(
// i18n.T("gui.config.basic.auto_check_update.prompt"),
// &config.General.AutoCheckUpdate,
// config.General.AutoCheckUpdate),
//)
//checkUpdateBtn := widget.NewButton(i18n.T("gui.config.basic.check_update"), func() {
// err := API.App().CheckUpdate()
// if err != nil {
// showDialogIfError(err)
// return
// }
// if API.App().LatestVersion().Version > API.App().Version().Version {
// dialog.ShowCustom(
// i18n.T("gui.update.new_version"),
// "OK",
// widget.NewRichTextFromMarkdown(API.App().LatestVersion().Info),
// MainWindow)
// }
//})
//b.panel = container.NewVBox(randomPlaylist, outputDevice, skipPlaylist, skipWhenErr, checkUpdateBox, checkUpdateBtn)
b.panel = container.NewVBox(randomPlaylist, outputDevice)
skipWhenErr := container.NewHBox(
widget.NewLabel(i18n.T("gui.config.basic.skip_when_error")),
component.NewCheckOneWayBinding(
i18n.T("gui.config.basic.skip_when_error.prompt"),
&config.General.PlayNextOnFail,
config.General.PlayNextOnFail),
)
checkUpdateBox := container.NewHBox(
widget.NewLabel(i18n.T("gui.config.basic.auto_check_update")),
component.NewCheckOneWayBinding(
i18n.T("gui.config.basic.auto_check_update.prompt"),
&config.General.AutoCheckUpdate,
config.General.AutoCheckUpdate),
)
checkUpdateBtn := widget.NewButton(i18n.T("gui.config.basic.check_update"), func() {
global.EventManager.CallA(events.CheckUpdateCmd, events.CheckUpdateCmdEvent{})
})
b.panel = container.NewVBox(randomPlaylist, skipWhenErr, outputDevice, checkUpdateBox, checkUpdateBtn)
return b.panel
}

View File

@@ -78,28 +78,9 @@ func Initialize() {
dialog.ShowError(err, MainWindow)
})
checkUpdate()
MainWindow.SetFixedSize(true)
if config.General.ShowSystemTray {
setupSysTray()
}
}
//
//func checkUpdate() {
// l().Info("checking updates...")
// err := API.App().CheckUpdate()
// if err != nil {
// showDialogIfError(err)
// l().Warnf("check update failed", err)
// return
// }
// l().Infof("latest version: v%s", API.App().LatestVersion().Version)
// if API.App().LatestVersion().Version > API.App().Version().Version {
// l().Info("new update available")
// dialog.ShowCustom(
// i18n.T("gui.update.new_version"),
// "OK",
// widget.NewRichTextFromMarkdown(API.App().LatestVersion().Info),
// MainWindow)
// }
//}

View File

@@ -44,12 +44,16 @@ func NewImageFromPlayerPicture(picture miaosic.Picture) (*canvas.Image, error) {
if uri == nil {
return nil, errors.New("fail to fail url")
}
// NewImageFromURI will return an image with empty resource and file
img = canvas.NewImageFromURI(uri)
if img == nil {
if img == nil || (img.File == "" && img.Resource == nil) {
// bug fix, return a new error to indicate fail to read an image
return nil, errors.New("fail to read image")
}
}
if img.Resource == nil {
return nil, errors.New("fail to read image")
}
// compress image, so it won't be too large
img.Resource = ResizeImage(img.Resource, 128, 128)
return img, nil

View File

@@ -166,7 +166,10 @@ func registerRoomHandlers() {
return
}
RoomTab.rooms[index] = room
// add lock to avoid race condition
RoomTab.lock.Lock()
RoomTab.Rooms.Refresh()
RoomTab.lock.Unlock()
if index == RoomTab.Index {
RoomTab.RoomTitle.SetText(room.DisplayName())
RoomTab.RoomID.SetText(room.LiveRoom.Identifier())

View File

@@ -130,6 +130,8 @@ func registerPlayControllerHandler() {
if event.Data.(events.PlayerPlayingUpdateEvent).Removed {
PlayController.Progress.Value = 0
PlayController.Progress.Max = 0
PlayController.TotalTime.SetText("0:00")
PlayController.CurrentTime.SetText("0:00")
PlayController.Title.SetText("Title")
PlayController.Artist.SetText("Artist")
PlayController.Username.SetText("Username")
@@ -160,6 +162,7 @@ func registerPlayControllerHandler() {
picture, err := gutil.NewImageFromPlayerPicture(media.Info.Cover)
if err != nil {
ch <- nil
logger.Errorf("fail to load cover: %v", err)
return
}
ch <- picture

31
gui/updater.go Normal file
View File

@@ -0,0 +1,31 @@
package gui
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
func checkUpdate() {
global.EventManager.RegisterA(
events.CheckUpdateResultUpdate, "gui.updater.check_update", func(event *event.Event) {
data := event.Data.(events.CheckUpdateResultUpdateEvent)
msg := data.Info.Version.String() + "\n\n\n" + data.Info.Info
if data.HasUpdate {
dialog.ShowCustom(
i18n.T("gui.update.new_version"),
"OK",
widget.NewRichTextFromMarkdown(msg),
MainWindow)
} else {
dialog.ShowCustom(
i18n.T("gui.update.already_latest_version"),
"OK",
widget.NewRichTextFromMarkdown(""),
MainWindow)
}
})
}

View File

@@ -5,6 +5,7 @@ import (
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/internal/playlist"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/event"
)
@@ -36,7 +37,17 @@ func handlePlayNext() {
global.EventManager.RegisterA(
events.PlaylistInsertUpdate(model.PlaylistIDPlayer),
"internal.controller.playcontrol.playnext_when_insert",
"internal.controller.playcontrol.playnext_when_insert.player",
func(event *event.Event) {
if isIdle {
global.EventManager.CallA(events.PlayerPlayNextCmd,
events.PlayerPlayNextCmdEvent{})
}
})
global.EventManager.RegisterA(
events.PlaylistInsertUpdate(model.PlaylistIDSystem),
"internal.controller.playcontrol.playnext_when_insert.system",
func(event *event.Event) {
if isIdle {
global.EventManager.CallA(events.PlayerPlayNextCmd,
@@ -49,17 +60,27 @@ func handlePlayNext() {
"internal.controller.playcontrol.playnext",
func(event *event.Event) {
if playlist.PlayerPlaylist.Size() > 0 {
log.Infof("Try to play next media in player playlist")
global.EventManager.CallA(events.PlaylistNextCmd(model.PlaylistIDPlayer),
events.PlaylistNextCmdEvent{
Remove: true,
})
} else {
log.Infof("Try to play next media in system playlist")
global.EventManager.CallA(events.PlaylistNextCmd(model.PlaylistIDSystem),
events.PlaylistNextCmdEvent{
Remove: true,
Remove: false,
})
}
})
global.EventManager.RegisterA(
events.PlayerPlayErrorUpdate,
"internal.controller.playcontrol.playnext_on_error",
func(event *event.Event) {
if isIdle && config.General.PlayNextOnFail {
global.EventManager.CallA(events.PlayerPlayNextCmd, events.PlayerPlayNextCmdEvent{})
}
})
global.EventManager.RegisterA(events.PlaylistNextUpdate(model.PlaylistIDPlayer),

View File

@@ -7,11 +7,13 @@ import (
"AynaLivePlayer/internal/playlist"
"AynaLivePlayer/internal/plugins"
"AynaLivePlayer/internal/source"
"AynaLivePlayer/internal/updater"
"AynaLivePlayer/plugin/diange"
"AynaLivePlayer/plugin/durationmgmt"
"AynaLivePlayer/plugin/qiege"
"AynaLivePlayer/plugin/sourcelogin"
"AynaLivePlayer/plugin/textinfo"
"AynaLivePlayer/plugin/wshub"
)
func Initialize() {
@@ -25,7 +27,9 @@ func Initialize() {
diange.NewDiange(), qiege.NewQiege(), sourcelogin.NewSourceLogin(),
textinfo.NewTextInfo(),
durationmgmt.NewMaxDuration(),
wshub.NewWsHub(),
)
updater.Initialize()
}
func Stop() {

View File

@@ -20,7 +20,10 @@ func (c *_cfg) OnLoad() {
}
func (c *_cfg) OnSave() {
_ = config.SaveJson(c.LiveRoomPath, &c.liveRooms)
err := config.SaveJson(c.LiveRoomPath, &c.liveRooms)
if err != nil {
log.Errorf("fail to save live rooms: %v", err)
}
}
var cfg = &_cfg{

View File

@@ -50,6 +50,15 @@ func StopAndSave() {
func addLiveRoom(roomModel model.LiveRoom) {
log.Info("Add live room")
room, err := liveroomsdk.CreateLiveRoom(roomModel.LiveRoom)
// handle failed to create liveroom
if err != nil {
log.Errorf("Create live room failed: %s", err)
global.EventManager.CallA(
events.ErrorUpdate, events.ErrorUpdateEvent{
Error: err,
})
return
}
if _, ok := liveRooms[room.Config().Identifier()]; ok {
log.Errorf("fail to add, room %s already exists", room.Config().Identifier())
global.EventManager.CallA(
@@ -118,6 +127,7 @@ func registerHandlers() {
}
_ = room.room.Disconnect()
room.room.OnStatusChange(nil)
room.room.OnMessage(nil)
delete(liveRooms, data.Identifier)
log.Infof("success remove live room %s", data.Identifier)
sendRoomsUpdateEvent()

View File

@@ -12,6 +12,7 @@ import (
"github.com/AynaLivePlayer/miaosic"
"github.com/aynakeya/go-mpv"
"github.com/tidwall/gjson"
"math"
)
var running bool = false
@@ -71,6 +72,9 @@ func StopPlayer() {
log.Info("mpv player stopped")
}
var prevPercentPos float64 = 0
var prevTimePos float64 = 0
var mpvPropertyHandler = map[string]func(value any){
"pause": func(value any) {
var data events.PlayerPropertyPauseUpdateEvent
@@ -89,6 +93,11 @@ var mpvPropertyHandler = map[string]func(value any){
if data.PercentPos < 0.1 {
return
}
// ignore small change
if math.Abs(data.PercentPos-prevPercentPos) < 0.5 {
return
}
prevPercentPos = data.PercentPos
global.EventManager.CallA(events.PlayerPropertyPercentPosUpdate, data)
},
@@ -120,6 +129,11 @@ var mpvPropertyHandler = map[string]func(value any){
if data.TimePos < 0.1 {
return
}
// ignore small change
if math.Abs(data.TimePos-prevTimePos) < 0.5 {
return
}
prevTimePos = data.TimePos
global.EventManager.CallA(events.PlayerPropertyTimePosUpdate, data)
},
"duration": func(value any) {
@@ -160,6 +174,11 @@ func registerCmdHandler() {
mediaUrls, err := miaosic.GetMediaUrl(mediaInfo.Meta, miaosic.QualityAny)
if err != nil || len(mediaUrls) == 0 {
log.Warn("[MPV PlayControl] get media url failed", err)
global.EventManager.CallA(
events.PlayerPlayErrorUpdate,
events.PlayerPlayErrorUpdateEvent{
Error: err,
})
return
}
mediaUrl := mediaUrls[0]
@@ -180,11 +199,6 @@ func registerCmdHandler() {
return
}
}
log.Debugf("mpv command load file %s %s", mediaInfo.Title, mediaUrl.Url)
if err := libmpv.Command([]string{"loadfile", mediaUrl.Url}); err != nil {
log.Warn("[MPV PlayControl] mpv load media failed", mediaInfo)
return
}
media := evnt.Data.(events.PlayerPlayCmdEvent).Media
if m, err := miaosic.GetMediaInfo(media.Info.Meta); err == nil {
media.Info = m
@@ -193,6 +207,22 @@ func registerCmdHandler() {
Media: media,
Removed: false,
})
log.Debugf("mpv command load file %s %s", mediaInfo.Title, mediaUrl.Url)
if err := libmpv.Command([]string{"loadfile", mediaUrl.Url}); err != nil {
log.Error("[MPV PlayControl] mpv load media failed", mediaInfo)
global.EventManager.CallA(
events.PlayerPlayErrorUpdate,
events.PlayerPlayErrorUpdateEvent{
Error: err,
})
return
}
global.EventManager.CallA(events.PlayerPropertyTimePosUpdate, events.PlayerPropertyTimePosUpdateEvent{
TimePos: 0,
})
global.EventManager.CallA(events.PlayerPropertyPercentPosUpdate, events.PlayerPropertyPercentPosUpdateEvent{
PercentPos: 0,
})
})
global.EventManager.RegisterA(events.PlayerToggleCmd, "player.toggle", func(evnt *event.Event) {
property, err := libmpv.GetProperty("pause", mpv.FORMAT_FLAG)
@@ -205,6 +235,13 @@ func registerCmdHandler() {
log.Warn("[MPV PlayControl] toggle pause failed", err)
}
})
global.EventManager.RegisterA(events.PlayerSetPauseCmd, "player.set_paused", func(evnt *event.Event) {
data := evnt.Data.(events.PlayerSetPauseCmdEvent)
err := libmpv.SetProperty("pause", mpv.FORMAT_FLAG, data.Pause)
if err != nil {
log.Warn("[MPV PlayControl] set pause failed", err)
}
})
global.EventManager.RegisterA(events.PlayerSeekCmd, "player.seek", func(evnt *event.Event) {
data := evnt.Data.(events.PlayerSeekCmdEvent)
log.Debugf("seek to %f (absolute=%t)", data.Position, data.Absolute)

View File

@@ -37,15 +37,26 @@ func newPlaylist(id model.PlaylistID) *playlist {
pl.Delete(e.Index)
})
global.EventManager.RegisterA(events.PlaylistNextCmd(id), "internal.playlist.next", func(event *event.Event) {
log.Infof("Playlist %s recieve next", id)
pl.Next(event.Data.(events.PlaylistNextCmdEvent).Remove)
})
global.EventManager.RegisterA(events.PlaylistModeChangeCmd(id), "internal.playlist.mode", func(event *event.Event) {
if pl.mode == model.PlaylistModeRandom {
pl.Index = 0
}
pl.mode = event.Data.(events.PlaylistModeChangeCmdEvent).Mode
log.Infof("Playlist %s mode changed to %d", id, pl.mode)
global.EventManager.CallA(events.PlaylistModeChangeUpdate(id), events.PlaylistModeChangeUpdateEvent{
Mode: pl.mode,
})
})
global.EventManager.RegisterA(events.PlaylistSetIndexCmd(id), "internal.playlist.setindex", func(event *event.Event) {
index := event.Data.(events.PlaylistSetIndexCmdEvent).Index
if index >= pl.Size() || index < 0 {
index = 0
}
pl.Index = index
})
return pl
}
@@ -137,13 +148,19 @@ func (p *playlist) Move(src int, dst int) {
}
func (p *playlist) Next(delete bool) {
p.Lock.Lock()
if p.Size() == 0 {
// no media in the playlist
// do not issue any event
p.Lock.Unlock()
return
}
var index int
index = p.Index
// add guard
if index >= p.Size() {
index = 0
}
if p.mode == model.PlaylistModeRandom {
p.Index = rand.Intn(p.Size())
} else if p.mode == model.PlaylistModeNormal {
@@ -152,17 +169,26 @@ func (p *playlist) Next(delete bool) {
p.Index = index
}
m := p.Medias[index]
global.EventManager.CallA(events.PlaylistNextUpdate(p.playlistId), events.PlaylistNextUpdateEvent{
Media: m,
})
// fix race condition
currentSize := p.Size() - 1
if delete {
p.Delete(index)
if p.mode == model.PlaylistModeRandom {
p.Index = rand.Intn(p.Size())
if currentSize == 0 {
p.Index = 0
} else {
p.Index = rand.Intn(currentSize)
}
} else if p.mode == model.PlaylistModeNormal {
p.Index = index
} else {
p.Index = index
}
}
p.Lock.Unlock()
global.EventManager.CallA(events.PlaylistNextUpdate(p.playlistId), events.PlaylistNextUpdateEvent{
Media: m,
})
if delete {
p.Delete(index)
}
}

View File

@@ -1,41 +0,0 @@
package todo
import (
"AynaLivePlayer/core/model"
"AynaLivePlayer/pkg/config"
"errors"
"fmt"
"github.com/go-resty/resty/v2"
"github.com/tidwall/gjson"
)
type AppBilibiliChannel struct {
latestVersion model.Version
}
func (app *AppBilibiliChannel) Version() model.VersionInfo {
return model.VersionInfo{
model.Version(config.Version), "",
}
}
func (app *AppBilibiliChannel) LatestVersion() model.VersionInfo {
return model.VersionInfo{
app.latestVersion,
fmt.Sprintf("v%s\n\n[https://play-live.bilibili.com/details/1661006726438](https://play-live.bilibili.com/details/1661006726438)", app.latestVersion),
}
}
func (app *AppBilibiliChannel) CheckUpdate() error {
uri := "https://api.live.bilibili.com/xlive/virtual-interface/v1/app/detail?app_id=1661006726438"
resp, err := resty.New().R().Get(uri)
if err != nil {
return err
}
lv := model.VersionFromString(gjson.ParseBytes(resp.Body()).Get("data.version").String())
if lv == 0 {
return errors.New("failed to get latest version")
}
app.latestVersion = lv
return nil
}

View File

@@ -0,0 +1,63 @@
package updater
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/logger"
"github.com/go-resty/resty/v2"
"github.com/tidwall/gjson"
"strconv"
)
var log logger.ILogger = nil
func Initialize() {
log = global.Logger.WithPrefix("internal.updater")
if config.General.AutoCheckUpdate {
go func() {
info, hasUpdate := CheckUpdate()
if !hasUpdate {
return
}
global.EventManager.CallA(
events.CheckUpdateResultUpdate,
events.CheckUpdateResultUpdateEvent{
HasUpdate: hasUpdate,
Info: info,
})
}()
}
global.EventManager.RegisterA(
events.CheckUpdateCmd, "internal.updater.handle",
func(evt *event.Event) {
info, hasUpdate := CheckUpdate()
global.EventManager.CallA(
events.CheckUpdateResultUpdate,
events.CheckUpdateResultUpdateEvent{
HasUpdate: hasUpdate,
Info: info,
})
})
}
func CheckUpdate() (model.VersionInfo, bool) {
uri := config.General.InfoApiServer + "/api/version/check_update"
resp, err := resty.New().R().SetQueryParam("client_version", strconv.Itoa(int(config.Version))).Get(uri)
if err != nil {
log.Errorf("failed to check update: %s", err.Error())
return model.VersionInfo{}, false
}
result := gjson.ParseBytes(resp.Body())
if !result.Get("data.has_update").Bool() {
log.Infof("no update available")
return model.VersionInfo{}, false
}
log.Infof("new version available: %s", model.Version(result.Get("data.latest.version").Uint()).String())
return model.VersionInfo{
Version: model.Version(result.Get("data.latest.version").Uint()),
Info: result.Get("data.latest.note").String(),
}, true
}

View File

@@ -10,7 +10,7 @@ import (
const (
ProgramName = "卡西米尔唱片机"
Version uint32 = 0x010002
Version uint32 = 0x010006
)
const (

View File

@@ -5,8 +5,10 @@ type _GeneralConfig struct {
Width float32
Height float32
Language string
InfoApiServer string
AutoCheckUpdate bool
ShowSystemTray bool
PlayNextOnFail bool
}
func (c *_GeneralConfig) Name() string {
@@ -16,7 +18,9 @@ func (c *_GeneralConfig) Name() string {
var General = &_GeneralConfig{
Language: "zh-CN",
ShowSystemTray: false,
InfoApiServer: "http://localhost:9090",
AutoCheckUpdate: true,
Width: 960,
Height: 480,
PlayNextOnFail: false,
}

View File

@@ -15,6 +15,51 @@ type LogrusLogger struct {
module string
}
func (l *LogrusLogger) DebugW(message string, keysAndValues ...interface{}) {
//TODO implement me
panic("implement me")
}
func (l *LogrusLogger) DebugS(message string, fields logger.LogField) {
//TODO implement me
panic("implement me")
}
func (l *LogrusLogger) InfoW(message string, keysAndValues ...interface{}) {
//TODO implement me
panic("implement me")
}
func (l *LogrusLogger) InfoS(message string, fields logger.LogField) {
//TODO implement me
panic("implement me")
}
func (l *LogrusLogger) WarnW(message string, keysAndValues ...interface{}) {
//TODO implement me
panic("implement me")
}
func (l *LogrusLogger) WarnS(message string, fields logger.LogField) {
//TODO implement me
panic("implement me")
}
func (l *LogrusLogger) ErrorW(message string, keysAndValues ...interface{}) {
//TODO implement me
panic("implement me")
}
func (l *LogrusLogger) ErrorS(message string, fields logger.LogField) {
//TODO implement me
panic("implement me")
}
func (l *LogrusLogger) WithPrefix(prefix string) logger.ILogger {
//TODO implement me
panic("implement me")
}
func (l *LogrusLogger) SetLogLevel(level logger.LogLevel) {
switch level {
case logger.LogLevelDebug:

View File

@@ -3,6 +3,7 @@ package repository
import (
"AynaLivePlayer/pkg/logger"
"github.com/mattn/go-colorable"
"github.com/virtuald/go-paniclog"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"time"
@@ -98,7 +99,8 @@ func NewZapLogger() logger.ILogger {
return &zapLoggerImpl{SugaredLogger: sugar}
}
func NewZapColoredLogger() logger.ILogger {
func NewZapColoredLogger(outPath string, redirectPanic bool) logger.ILogger {
f, err := getLogOut(outPath, 5)
cfg := zap.NewProductionEncoderConfig()
level := zap.NewAtomicLevel()
level.SetLevel(zapcore.DebugLevel)
@@ -106,11 +108,30 @@ func NewZapColoredLogger() logger.ILogger {
cfg.EncodeTime = syslogTimeEncoder
cfg.EncodeName = customNamedEncoder
cfg.ConsoleSeparator = " "
zapLog := zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(cfg),
zapcore.AddSync(colorable.NewColorableStdout()),
level,
))
var zapLog *zap.Logger
if err == nil {
zapLog = zap.New(
zapcore.NewTee(zapcore.NewCore(
zapcore.NewConsoleEncoder(cfg),
zapcore.AddSync(colorable.NewColorableStdout()),
level),
zapcore.NewCore(
zapcore.NewConsoleEncoder(cfg),
zapcore.AddSync(f),
level),
),
)
} else {
zapLog = zap.New(
zapcore.NewCore(
zapcore.NewConsoleEncoder(cfg),
zapcore.AddSync(colorable.NewColorableStdout()),
level),
)
}
if redirectPanic {
_, _ = paniclog.RedirectStderr(f)
}
sugar := zapLog.Sugar()
return &zapLoggerImpl{SugaredLogger: sugar, level: level}
}

View File

@@ -86,7 +86,7 @@ func NewDiange() *Diange {
},
},
cooldowns: make(map[string]int),
log: global.Logger.WithPrefix("Plugin.Logger"),
log: global.Logger.WithPrefix("Plugin.Diange"),
}
return diange
}
@@ -215,7 +215,14 @@ func (d *Diange) handleMessage(event *event.Event) {
// match media first
mediaMeta, found := miaosic.MatchMedia(keywords)
var mediaMeta miaosic.MetaData
found := false
for _, source := range sources {
mediaMeta, found = miaosic.MatchMediaByProvider(source, keywords)
if found {
break
}
}
var media miaosic.MediaInfo
@@ -225,22 +232,12 @@ func (d *Diange) handleMessage(event *event.Event) {
if len(medias) == 0 || err != nil {
continue
}
// double check blacklist
for _, item := range d.blacklist {
if item.Exact && item.Value == medias[0].Title {
d.log.Warnf("User %s(%s) diange %s is in blacklist %s, ignore", message.User.Username, message.User.Uid, keywords, item.Value)
return
}
if !item.Exact && strings.Contains(medias[0].Title, item.Value) {
d.log.Warnf("User %s(%s) diange %s is in blacklist %s, ignore", message.User.Username, message.User.Uid, keywords, item.Value)
return
}
}
media = medias[0]
found = true
break
}
} else {
d.log.Info("Match media: ", mediaMeta)
m, err := miaosic.GetMediaInfo(mediaMeta)
if err != nil {
d.log.Error("Get media info failed: ", err)
@@ -250,6 +247,17 @@ func (d *Diange) handleMessage(event *event.Event) {
}
if found {
// double check blacklist
for _, item := range d.blacklist {
if item.Exact && item.Value == media.Title {
d.log.Warnf("User %s(%s) diange %s is in blacklist %s, ignore", message.User.Username, message.User.Uid, keywords, item.Value)
return
}
if !item.Exact && strings.Contains(media.Title, item.Value) {
d.log.Warnf("User %s(%s) diange %s is in blacklist %s, ignore", message.User.Username, message.User.Uid, keywords, item.Value)
return
}
}
if d.SkipSystemPlaylist && d.isCurrentSystem {
global.EventManager.CallA(
events.PlayerPlayCmd,

View File

@@ -1,7 +1,6 @@
package webinfo
import (
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/pkg/config"
"context"
"encoding/json"

16
plugin/wshub/events.go Normal file
View File

@@ -0,0 +1,16 @@
package wshub
import (
"AynaLivePlayer/pkg/event"
"encoding/json"
)
type EventData struct {
EventID event.EventId
Data interface{}
}
type EventDataReceived struct {
EventID event.EventId
Data json.RawMessage
}

176
plugin/wshub/server.go Normal file
View File

@@ -0,0 +1,176 @@
package wshub
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/logger"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/websocket"
"net/http"
"sync"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type wsClient struct {
conn *websocket.Conn
Data chan []byte
Close chan byte
}
func (c *wsClient) start() {
for {
msgType, val, err := c.conn.ReadMessage()
if err != nil {
c.Close <- 1
return
}
if msgType != websocket.TextMessage {
return
}
var data EventDataReceived
err = json.Unmarshal(val, &data)
if err != nil {
global.Logger.Warn("unmarshal event data failed", err)
return
}
actualEventData, err := events.UnmarshalEventData(data.EventID, data.Data)
if err != nil {
global.Logger.Warn("unmarshal event data failed", err)
return
}
global.EventManager.CallA(data.EventID, actualEventData)
}
}
type wsServer struct {
Running bool
Server *http.Server
clients map[*wsClient]bool
mux *http.ServeMux
lock sync.RWMutex
port *int
localhostOnly *bool
log logger.ILogger
}
func newWsServer(port *int, localhostOnly *bool) *wsServer {
mux := http.NewServeMux()
s := &wsServer{
Running: false,
clients: make(map[*wsClient]bool),
mux: mux,
port: port,
localhostOnly: localhostOnly,
log: global.Logger.WithPrefix("plugin.wshub.server"),
}
mux.HandleFunc("/wsinfo", s.handleWsInfo)
return s
}
func (s *wsServer) broadcast(data []byte) {
s.lock.RLock()
defer s.lock.RUnlock()
for client := range s.clients {
client.Data <- data
}
}
func (s *wsServer) register(client *wsClient) {
s.lock.Lock()
s.clients[client] = true
s.lock.Unlock()
}
func (s *wsServer) unregister(client *wsClient) {
s.lock.Lock()
delete(s.clients, client)
s.lock.Unlock()
}
func (s *wsServer) handleWsInfo(w http.ResponseWriter, r *http.Request) {
s.log.Debug("connection start")
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
s.log.Warnf("upgrade error: %s", err)
return
}
client := &wsClient{
conn: conn,
Data: make(chan []byte, 16),
Close: make(chan byte, 1),
}
s.register(client)
defer s.unregister(client)
go client.start()
for {
select {
case data := <-client.Data:
err := client.conn.WriteMessage(websocket.TextMessage, data)
if err != nil {
s.log.Warn("write message failed", err)
return
}
case _ = <-client.Close:
s.log.Infof("client %s close", client.conn.RemoteAddr().String())
if err := client.conn.Close(); err != nil {
s.log.Warnf("close connection encouter an error: %s", err)
}
return
}
}
}
func (s *wsServer) Start() {
s.log.Debug("WebInfoServer starting...")
s.Running = true
go func() {
var addr string
if *s.localhostOnly {
addr = fmt.Sprintf("localhost:%d", *s.port)
} else {
addr = fmt.Sprintf("0.0.0.0:%d", *s.port)
}
s.Server = &http.Server{
Addr: addr,
Handler: s.mux,
}
err := s.Server.ListenAndServe()
s.Running = false
if errors.Is(err, http.ErrServerClosed) {
s.log.Info("WebInfoServer closed")
return
}
if err != nil {
s.log.Errorf("Failed to start webinfo server: %s", err)
return
}
}()
}
func (s *wsServer) Stop() error {
s.log.Debug("WebInfoServer stopping...")
s.lock.Lock()
s.clients = make(map[*wsClient]bool)
s.lock.Unlock()
if s.Server != nil {
return s.Server.Shutdown(context.TODO())
}
return nil
}
func (s *wsServer) getWsUrl() string {
if *s.localhostOnly {
return fmt.Sprintf("ws://localhost:%d/wsinfo", *s.port)
}
return fmt.Sprintf("ws://0.0.0.0:%d/wsinfo", *s.port)
}

180
plugin/wshub/wshub.go Normal file
View File

@@ -0,0 +1,180 @@
package wshub
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/gui"
"AynaLivePlayer/gui/component"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"AynaLivePlayer/pkg/logger"
"encoding/json"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
type WsHub struct {
config.BaseConfig
Enabled bool
Port int
LocalHostOnly bool
panel fyne.CanvasObject
server *wsServer
log logger.ILogger
}
func NewWsHub() *WsHub {
return &WsHub{
Enabled: false,
Port: 29629,
LocalHostOnly: true,
log: global.Logger.WithPrefix("plugin.wshub"),
}
}
func (w *WsHub) Enable() error {
config.LoadConfig(w)
w.server = newWsServer(&w.Port, &w.LocalHostOnly)
gui.AddConfigLayout(w)
w.registerEvents()
w.log.Info("webinfo loaded")
if w.Enabled {
w.log.Info("starting web backend server")
w.server.Start()
}
return nil
}
func (w *WsHub) Disable() error {
if w.server.Running {
err := w.server.Stop()
if err != nil {
w.log.Warnf("stop server have error: %s", err)
}
}
return nil
}
func (w *WsHub) Name() string {
return "WsHub"
}
func (w *WsHub) Title() string {
return i18n.T("plugin.wshub.title")
}
func (w *WsHub) Description() string {
return i18n.T("plugin.wshub.description")
}
func (w *WsHub) CreatePanel() fyne.CanvasObject {
if w.panel != nil {
return w.panel
}
statusText := widget.NewLabel("")
freshStatusText := func() {
if w.server.Running {
statusText.SetText(i18n.T("plugin.wshub.server_status.running"))
return
} else {
statusText.SetText(i18n.T("plugin.wshub.server_status.stopped"))
}
}
serverStatus := container.NewHBox(
widget.NewLabel(i18n.T("plugin.wshub.server_status")),
statusText,
)
autoStart := container.NewHBox(
widget.NewLabel(i18n.T("plugin.wshub.autostart")),
component.NewCheckOneWayBinding("", &w.Enabled, w.Enabled))
localHostOnly := container.NewHBox(
widget.NewLabel(i18n.T("plugin.wshub.local_host_only")),
component.NewCheckOneWayBinding("", &w.LocalHostOnly, w.LocalHostOnly))
freshStatusText()
serverPort := container.NewBorder(nil, nil,
widget.NewLabel(i18n.T("plugin.wshub.port")), nil,
widget.NewEntryWithData(binding.IntToString(binding.BindInt(&w.Port))),
)
serverUrl := widget.NewEntry()
serverUrl.SetText(w.server.getWsUrl())
serverUrl.Disable()
serverPreview := container.NewBorder(nil, nil,
widget.NewLabel(i18n.T("plugin.wshub.server_link")), nil,
serverUrl,
)
refreshServerUrl := func() {
serverUrl.SetText(w.server.getWsUrl())
}
stopBtn := component.NewAsyncButtonWithIcon(
i18n.T("plugin.wshub.server_control.stop"),
theme.MediaStopIcon(),
func() {
if !w.server.Running {
return
}
w.log.Info("User try stop webinfo server")
err := w.server.Stop()
if err != nil {
w.log.Warnf("stop server have error: %s", err)
}
freshStatusText()
},
)
startBtn := component.NewAsyncButtonWithIcon(
i18n.T("plugin.wshub.server_control.start"),
theme.MediaPlayIcon(),
func() {
if w.server.Running {
return
}
w.log.Infof("User try start webinfo server with port %d", w.Port)
w.server.Start()
freshStatusText()
refreshServerUrl()
},
)
restartBtn := component.NewAsyncButtonWithIcon(
i18n.T("plugin.wshub.server_control.restart"),
theme.MediaReplayIcon(),
func() {
w.log.Infof("User try restart webinfo server with port %d", w.Port)
if w.server.Running {
if err := w.server.Stop(); err != nil {
w.log.Warnf("stop server have error: %s", err)
return
}
}
w.server.Start()
freshStatusText()
refreshServerUrl()
},
)
ctrlBtns := container.NewHBox(
widget.NewLabel(i18n.T("plugin.wshub.server_control")),
startBtn, stopBtn, restartBtn,
)
w.panel = container.NewVBox(serverStatus, autoStart, localHostOnly, serverPreview, serverPort, ctrlBtns)
return nil
}
func (w *WsHub) registerEvents() {
for eid, _ := range events.EventsMapping {
global.EventManager.RegisterA(eid,
"plugin.wshub.event."+string(eid),
func(e *event.Event) {
val, err := json.Marshal(EventData{
EventID: e.Id,
Data: e.Data,
})
if err != nil {
w.log.Errorf("failed to marshal event data %v", err)
return
}
w.server.broadcast(val)
})
}
}

View File

@@ -7,6 +7,7 @@
- 适配歌词服务器
- 媒体源 - 歌单信息获取
- mpris, SMTC
- web弹幕协议的断线handler (web 重连)
- 歌词event发送全部歌词前端处理不同版本
- 网页输出重写,使用网页版本,不绑定在点歌机内(点歌机不需要启动网页服务)
@@ -14,7 +15,12 @@
----
Finished
- 2024.04.22 : 文本输出, 歌曲最长时长控制bug修复, 网易云登录(歌曲来源统一登录)
- 2024.05.06@1.0.6 : 修复若干bug
- 2024.04.30 : 完成websocket hub
- 2024.04.28 : 修复id点歌匹配失败的问题, 修复黑名单会被id绕过的bug
- 2024.04.26@1.0.5: 修复直播间长连接重复连接导致点歌重复点的问题,修复直播间添加失败时候会触发闪退的问题,更新依赖导致的闪退问题,修复webdm断开再连接之后无法获取到弹幕的问题
- 2024.04.24@1.0.4: 添加log修复闪退
- 2024.04.22@1.0.2: 文本输出, 歌曲最长时长控制bug修复, 网易云登录(歌曲来源统一登录)
- 2024.04.17 : 1. 弹幕拿不到的问题,尽量使用身份码,网页协议随时可能爆炸
2. 酷我音乐复活
3. 黑名单