Merge pull request #62 from AynaLivePlayer/dev

1.2.3
This commit is contained in:
Aynakeya
2026-02-19 22:22:41 +08:00
committed by GitHub
26 changed files with 1366 additions and 253 deletions

3
.gitignore vendored
View File

@@ -9,4 +9,5 @@ CMakeCache.txt
/release/
log.txt
config.ini
config.ini.bak
config.ini.bak
.gocache

View File

@@ -8,45 +8,65 @@ import (
)
var EventsMapping = map[string]any{
CmdLiveRoomAdd: CmdLiveRoomAddData{},
LiveRoomProviderUpdate: LiveRoomProviderUpdateEvent{},
CmdLiveRoomRemove: CmdLiveRoomRemoveData{},
UpdateLiveRoomRooms: UpdateLiveRoomRoomsData{},
UpdateLiveRoomStatus: UpdateLiveRoomStatusData{},
CmdLiveRoomConfigChange: CmdLiveRoomConfigChangeData{},
CmdLiveRoomOperation: CmdLiveRoomOperationData{},
PlayerVolumeChangeCmd: PlayerVolumeChangeCmdEvent{},
PlayerPlayCmd: PlayerPlayCmdEvent{},
PlayerPlayErrorUpdate: PlayerPlayErrorUpdateEvent{},
PlayerSeekCmd: PlayerSeekCmdEvent{},
PlayerToggleCmd: PlayerToggleCmdEvent{},
PlayerSetPauseCmd: PlayerSetPauseCmdEvent{},
PlayerPlayNextCmd: PlayerPlayNextCmdEvent{},
CmdGetCurrentLyric: CmdGetCurrentLyricData{},
UpdateCurrentLyric: UpdateCurrentLyricData{},
PlayerLyricPosUpdate: PlayerLyricPosUpdateEvent{},
PlayerPlayingUpdate: PlayerPlayingUpdateEvent{},
PlayerPropertyPauseUpdate: PlayerPropertyPauseUpdateEvent{},
PlayerPropertyPercentPosUpdate: PlayerPropertyPercentPosUpdateEvent{},
PlayerPropertyStateUpdate: PlayerPropertyStateUpdateEvent{},
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{},
CmdMiaosicSearch: CmdMiaosicSearchData{},
ReplyMiaosicSearch: ReplyMiaosicSearchData{},
GUISetPlayerWindowOpenCmd: GUISetPlayerWindowOpenCmdEvent{},
CmdLiveRoomAdd: CmdLiveRoomAddData{},
LiveRoomProviderUpdate: LiveRoomProviderUpdateEvent{},
CmdLiveRoomRemove: CmdLiveRoomRemoveData{},
UpdateLiveRoomRooms: UpdateLiveRoomRoomsData{},
UpdateLiveRoomStatus: UpdateLiveRoomStatusData{},
CmdLiveRoomConfigChange: CmdLiveRoomConfigChangeData{},
CmdLiveRoomOperation: CmdLiveRoomOperationData{},
PlayerVolumeChangeCmd: PlayerVolumeChangeCmdEvent{},
PlayerPlayCmd: PlayerPlayCmdEvent{},
PlayerPlayErrorUpdate: PlayerPlayErrorUpdateEvent{},
PlayerSeekCmd: PlayerSeekCmdEvent{},
PlayerToggleCmd: PlayerToggleCmdEvent{},
PlayerSetPauseCmd: PlayerSetPauseCmdEvent{},
PlayerPlayNextCmd: PlayerPlayNextCmdEvent{},
CmdGetCurrentLyric: CmdGetCurrentLyricData{},
UpdateCurrentLyric: UpdateCurrentLyricData{},
PlayerLyricPosUpdate: PlayerLyricPosUpdateEvent{},
PlayerPlayingUpdate: PlayerPlayingUpdateEvent{},
PlayerPropertyPauseUpdate: PlayerPropertyPauseUpdateEvent{},
PlayerPropertyPercentPosUpdate: PlayerPropertyPercentPosUpdateEvent{},
PlayerPropertyStateUpdate: PlayerPropertyStateUpdateEvent{},
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{},
CmdMiaosicListProviders: CmdMiaosicListProvidersData{},
ReplyMiaosicListProviders: ReplyMiaosicListProvidersData{},
CmdMiaosicMatchMediaByProvider: CmdMiaosicMatchMediaByProviderData{},
ReplyMiaosicMatchMediaByProvider: ReplyMiaosicMatchMediaByProviderData{},
CmdMiaosicSearch: CmdMiaosicSearchData{},
ReplyMiaosicSearch: ReplyMiaosicSearchData{},
CmdMiaosicGetMediaInfo: CmdMiaosicGetMediaInfoData{},
ReplyMiaosicGetMediaInfo: ReplyMiaosicGetMediaInfoData{},
CmdMiaosicGetMediaUrl: CmdMiaosicGetMediaUrlData{},
ReplyMiaosicGetMediaUrl: ReplyMiaosicGetMediaUrlData{},
CmdMiaosicQrLogin: CmdMiaosicQrLoginData{},
ReplyMiaosicQrLogin: ReplyMiaosicQrLoginData{},
CmdMiaosicQrLoginVerify: CmdMiaosicQrLoginVerifyData{},
ReplyMiaosicQrLoginVerify: ReplyMiaosicQrLoginVerifyData{},
CmdMiaosicLogoutByProvider: CmdMiaosicLogoutByProviderData{},
ReplyMiaosicLogoutByProvider: ReplyMiaosicLogoutByProviderData{},
CmdMiaosicIsLoginByProvider: CmdMiaosicIsLoginByProviderData{},
ReplyMiaosicIsLoginByProvider: ReplyMiaosicIsLoginByProviderData{},
CmdMiaosicRestoreSessionByProvider: CmdMiaosicRestoreSessionByProviderData{},
ReplyMiaosicRestoreSessionByProvider: ReplyMiaosicRestoreSessionByProviderData{},
CmdMiaosicSaveSessionByProvider: CmdMiaosicSaveSessionByProviderData{},
ReplyMiaosicSaveSessionByProvider: ReplyMiaosicSaveSessionByProviderData{},
GUISetPlayerWindowOpenCmd: GUISetPlayerWindowOpenCmdEvent{},
}
func init() {

View File

@@ -2,6 +2,35 @@ package events
import "github.com/AynaLivePlayer/miaosic"
const CmdMiaosicListProviders = "cmd.miaosic.listProviders"
type CmdMiaosicListProvidersData struct{}
const ReplyMiaosicListProviders = "reply.miaosic.listProviders"
type MiaosicProviderInfo struct {
Name string `json:"name"`
Loginable bool `json:"loginable"`
}
type ReplyMiaosicListProvidersData struct {
Providers []MiaosicProviderInfo `json:"providers"`
}
const CmdMiaosicMatchMediaByProvider = "cmd.miaosic.matchMediaByProvider"
type CmdMiaosicMatchMediaByProviderData struct {
Provider string `json:"provider"`
Keyword string `json:"keyword"`
}
const ReplyMiaosicMatchMediaByProvider = "reply.miaosic.matchMediaByProvider"
type ReplyMiaosicMatchMediaByProviderData struct {
Meta miaosic.MetaData `json:"meta"`
Found bool `json:"found"`
}
const CmdMiaosicGetMediaInfo = "cmd.miaosic.getMediaInfo"
type CmdMiaosicGetMediaInfoData struct {
@@ -55,3 +84,54 @@ type ReplyMiaosicQrLoginVerifyData struct {
Result miaosic.QrLoginResult `json:"result"`
Error error `json:"error"`
}
const CmdMiaosicLogoutByProvider = "cmd.miaosic.logoutByProvider"
type CmdMiaosicLogoutByProviderData struct {
Provider string `json:"provider"`
}
const ReplyMiaosicLogoutByProvider = "reply.miaosic.logoutByProvider"
type ReplyMiaosicLogoutByProviderData struct {
Error error `json:"error"`
}
const CmdMiaosicIsLoginByProvider = "cmd.miaosic.isLoginByProvider"
type CmdMiaosicIsLoginByProviderData struct {
Provider string `json:"provider"`
}
const ReplyMiaosicIsLoginByProvider = "reply.miaosic.isLoginByProvider"
type ReplyMiaosicIsLoginByProviderData struct {
IsLogin bool `json:"is_login"`
Error error `json:"error"`
}
const CmdMiaosicRestoreSessionByProvider = "cmd.miaosic.restoreSessionByProvider"
type CmdMiaosicRestoreSessionByProviderData struct {
Provider string `json:"provider"`
Session string `json:"session"`
}
const ReplyMiaosicRestoreSessionByProvider = "reply.miaosic.restoreSessionByProvider"
type ReplyMiaosicRestoreSessionByProviderData struct {
Error error `json:"error"`
}
const CmdMiaosicSaveSessionByProvider = "cmd.miaosic.saveSessionByProvider"
type CmdMiaosicSaveSessionByProviderData struct {
Provider string `json:"provider"`
}
const ReplyMiaosicSaveSessionByProvider = "reply.miaosic.saveSessionByProvider"
type ReplyMiaosicSaveSessionByProviderData struct {
Session string `json:"session"`
Error error `json:"error"`
}

View File

@@ -3,5 +3,6 @@ package events
const MediaProviderUpdate = "update.media.provider.update"
type MediaProviderUpdateEvent struct {
Providers []string
Providers []string
ProviderInfos []MiaosicProviderInfo
}

View File

@@ -15,4 +15,5 @@ const ReplyMiaosicSearch = "update.search_result"
type ReplyMiaosicSearchData struct {
Medias []model.Media
Error error
}

16
go.mod
View File

@@ -12,7 +12,7 @@ replace (
)
require (
fyne.io/fyne/v2 v2.7.1
fyne.io/fyne/v2 v2.7.2
github.com/AynaLivePlayer/liveroom-sdk v0.1.0
github.com/AynaLivePlayer/miaosic v0.2.5
github.com/adrg/libvlc-go/v3 v3.1.6
@@ -21,7 +21,8 @@ require (
github.com/aynakeya/go-mpv v0.0.8
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728
github.com/go-ole/go-ole v1.3.0
github.com/go-resty/resty/v2 v2.16.5
github.com/go-resty/resty/v2 v2.17.1
github.com/godbus/dbus/v5 v5.1.0
github.com/gorilla/websocket v1.5.3
github.com/mattn/go-colorable v0.1.14
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
@@ -33,13 +34,13 @@ require (
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
golang.org/x/sys v0.37.0
golang.org/x/sys v0.40.0
gopkg.in/ini.v1 v1.67.0
)
require (
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect
github.com/AynaLivePlayer/blivedm-go v0.0.0-20250629154348-690af765bfbc // indirect
fyne.io/systray v1.12.0 // indirect
github.com/AynaLivePlayer/blivedm-go v0.0.0-20251109134927-cc4a4ca07110 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/XiaoMengXinX/Music163Api-Go v0.1.30 // indirect
@@ -59,7 +60,6 @@ require (
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.0 // indirect
@@ -81,8 +81,8 @@ require (
github.com/yuin/goldmark v1.7.8 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

32
go.sum
View File

@@ -1,9 +1,9 @@
fyne.io/fyne/v2 v2.7.1 h1:ja7rNHWWEooha4XBIZNnPP8tVFwmTfwMJdpZmLxm2Zc=
fyne.io/fyne/v2 v2.7.1/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE=
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 h1:eA5/u2XRd8OUkoMqEv3IBlFYSruNlXD8bRHDiqm0VNI=
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/AynaLivePlayer/blivedm-go v0.0.0-20250629154348-690af765bfbc h1:t1fMdqUjB2lR9uuGQ9yWJ7LJ3h1hXhI+LhbTpElPueI=
github.com/AynaLivePlayer/blivedm-go v0.0.0-20250629154348-690af765bfbc/go.mod h1:u+JfexgX5pYrylIuC5zP3N/Ylp47K/xvl+ntpZtosuE=
fyne.io/fyne/v2 v2.7.2 h1:XiNpWkn0PzX43ZCjbb0QYGg1RCxVbugwfVgikWZBCMw=
fyne.io/fyne/v2 v2.7.2/go.mod h1:PXbqY3mQmJV3J1NRUR2VbVgUUx3vgvhuFJxyjRK/4Ug=
fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM=
fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/AynaLivePlayer/blivedm-go v0.0.0-20251109134927-cc4a4ca07110 h1:A6IBTHcv/Pisf65FAsM4oxN1OXpVjpIYlVXDugCIuiw=
github.com/AynaLivePlayer/blivedm-go v0.0.0-20251109134927-cc4a4ca07110/go.mod h1:CtiYF0MDMCzrPUuM96b2194ANTtRxnA/YdtJNdAdxuQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
@@ -60,8 +60,8 @@ github.com/go-musicfox/winrt-go v0.1.4 h1:xg+7VKsIozGK8S4X4zNQ/3HNhg5yHWYaTE+Zs4
github.com/go-musicfox/winrt-go v0.1.4/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
@@ -184,8 +184,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -211,8 +211,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -231,10 +231,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=

View File

@@ -36,3 +36,10 @@ func (s *SliderPlus) Dragged(e *fyne.DragEvent) {
s.Dragging = true
s.Slider.Dragged(e)
}
func (s *SliderPlus) Tapped(e *fyne.PointEvent) {
s.Slider.Tapped(e)
if s.OnDragEnd != nil {
s.OnDragEnd(s.Value)
}
}

View File

@@ -35,6 +35,7 @@ type PlayControllerContainer struct {
LrcWindowOpen bool
CurrentTime *component.LabelFixedSize
TotalTime *widget.Label
DurationSec float64
}
func (p *PlayControllerContainer) SetDefaultCover() {
@@ -84,19 +85,22 @@ func registerPlayControllerHandler() {
PlayController.ButtonSwitch.Refresh()
}))
global.EventBus.Subscribe(gctx.EventChannel, events.PlayerPropertyPercentPosUpdate, "gui.player.controller.percent_pos", func(event *eventbus.Event) {
global.EventBus.Subscribe(gctx.EventChannel, events.PlayerPropertyPercentPosUpdate, "gui.player.controller.percent_pos", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
if PlayController.Progress.Dragging {
return
}
// Keep this path async (Do, not DoAndWait) to avoid blocking eventbus handlers.
// Using ThreadSafeHandler here also avoids a second RunInFyneThread dispatch hop.
PlayController.Progress.Value = event.Data.(events.PlayerPropertyPercentPosUpdateEvent).PercentPos * 10
gutil.RunInFyneThread(PlayController.Progress.Refresh)
})
PlayController.Progress.Refresh()
}))
global.EventBus.Subscribe(gctx.EventChannel, events.PlayerPropertyStateUpdate, "gui.player.controller.idle_active", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
state := event.Data.(events.PlayerPropertyStateUpdateEvent).State
if state == model.PlayerStateIdle || state == model.PlayerStateLoading {
PlayController.Progress.Value = 0
PlayController.Progress.Max = 0
PlayController.DurationSec = 0
//PlayController.Title.SetText("Title")
//PlayController.Artist.SetText("Artist")
//PlayController.Username.SetText("Username")
@@ -108,6 +112,15 @@ func registerPlayControllerHandler() {
PlayController.Progress.Max = 0
PlayController.Progress.OnDragEnd = func(f float64) {
if PlayController.DurationSec > 0 {
target := (f / PlayController.Progress.Max) * PlayController.DurationSec
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
Position: target,
Absolute: true,
})
return
}
// Fallback for unknown duration.
_ = global.EventBus.PublishToChannel(gctx.EventChannel, events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
Position: f / 10,
Absolute: false,
@@ -119,7 +132,8 @@ func registerPlayControllerHandler() {
}))
global.EventBus.Subscribe(gctx.EventChannel, events.PlayerPropertyDurationUpdate, "gui.player.controller.duration", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
PlayController.TotalTime.SetText(util.FormatTime(int(event.Data.(events.PlayerPropertyDurationUpdateEvent).Duration)))
PlayController.DurationSec = event.Data.(events.PlayerPropertyDurationUpdateEvent).Duration
PlayController.TotalTime.SetText(util.FormatTime(int(PlayController.DurationSec)))
}))
global.EventBus.Subscribe(gctx.EventChannel, events.PlayerPropertyVolumeUpdate, "gui.player.controller.volume", gutil.ThreadSafeHandler(func(event *eventbus.Event) {
@@ -138,6 +152,7 @@ func registerPlayControllerHandler() {
if event.Data.(events.PlayerPlayingUpdateEvent).Removed {
PlayController.Progress.Value = 0
PlayController.Progress.Max = 0
PlayController.DurationSec = 0
PlayController.TotalTime.SetText("0:00")
PlayController.CurrentTime.SetText("0:00")
PlayController.Title.SetText("Title")

View File

@@ -11,9 +11,11 @@ func registerHandlers() {
global.EventBus.Subscribe(gctx.EventChannel, events.GUISetPlayerWindowOpenCmd, "gui.player.videoplayer.handleopen", func(event *eventbus.Event) {
data := event.Data.(events.GUISetPlayerWindowOpenCmdEvent)
if data.SetOpen {
playerWindow.Close()
} else {
showPlayerWindow()
} else {
if playerWindow != nil {
playerWindow.Hide()
}
}
})
}

View File

@@ -18,7 +18,7 @@ func SetupPlayer() {
func StopPlayer() {
if config.Experimental.PlayerCore == "vlc" {
//vlc.StopPlayer()
vlc.StopPlayer()
} else {
mpv.StopPlayer()
}

View File

@@ -7,6 +7,7 @@ import (
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/eventbus"
"AynaLivePlayer/pkg/logger"
"errors"
"fmt"
"github.com/AynaLivePlayer/miaosic"
"github.com/adrg/libvlc-go/v3"
@@ -26,43 +27,43 @@ var lock sync.Mutex
var prevPercentPos float64 = 0
var prevTimePos float64 = 0
var duration float64 = 0
var currentState = model.PlayerStateIdle
var currentMedia model.Media
var currentWindowHandle uintptr
var audioDevices []model.AudioDevice
var currentAudioDevice string
var videoOptions = map[string][]string{
"windows": {"--video-filter=adjust", "--directx-hwnd"},
"darwin": {"--vout=macosx"},
"linux": {"--vout=x11", "--x11-display=:0"},
}
func setWindowHandle(handle uintptr) error {
return nil
if player == nil {
return errors.New("player is not initialized")
}
if handle == 0 {
return errors.New("invalid window handle 0")
}
os := runtime.GOOS
switch os {
case "windows":
// Windows 平台使用 DirectX
player.SetHWND(uintptr(handle))
if err := player.SetHWND(handle); err != nil {
return err
}
case "darwin":
// macOS 平台使用 NSView
player.SetNSObject(handle)
if err := player.SetNSObject(handle); err != nil {
return err
}
case "linux":
// Linux 平台使用 XWindow
player.SetXWindow(uint32(handle))
if err := player.SetXWindow(uint32(handle)); err != nil {
return err
}
default:
return fmt.Errorf("unsupported platform: %s", os)
}
currentWindowHandle = handle
// 如果当前有媒体在播放,需要重新加载视频输出
if player.IsPlaying() {
player.Stop()
player.Play()
}
return nil
}
@@ -71,11 +72,10 @@ func SetupPlayer() {
config.LoadConfig(cfg)
log = global.Logger.WithPrefix("VLC Player")
opts := []string{"--no-video", "--quiet"}
//os := runtime.GOOS
//if platformOpts, ok := videoOptions[os]; ok {
// opts = append(opts, platformOpts...)
//}
opts := []string{"--quiet"}
if !cfg.DisplayMusicCover {
opts = append(opts, "--no-video")
}
// 初始化libvlc
if err := vlc.Init(opts...); err != nil {
@@ -113,6 +113,7 @@ func StopPlayer() {
log.Infof("save audio device config: %s", cfg.AudioDevice)
}
running = false
currentState = model.PlayerStateIdle
if player != nil {
err := player.Stop()
if err != nil {
@@ -133,9 +134,8 @@ func StopPlayer() {
func registerEvents() {
// 播放结束事件
_, err := eventManager.Attach(vlc.MediaPlayerEndReached, func(e vlc.Event, userData interface{}) {
_ = global.EventBus.Publish(events.PlayerPropertyStateUpdate, events.PlayerPropertyStateUpdateEvent{
State: model.PlayerStateIdle,
})
currentState = model.PlayerStateIdle
_ = global.EventBus.Publish(events.PlayerPropertyStateUpdate, events.PlayerPropertyStateUpdateEvent{State: currentState})
_ = global.EventBus.Publish(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{
Media: model.Media{},
Removed: true,
@@ -195,6 +195,10 @@ func registerEvents() {
_, err = eventManager.Attach(vlc.MediaPlayerPlaying, func(e vlc.Event, userData interface{}) {
log.Debug("VLC player playing")
currentState = currentState.NextState(model.PlayerStatePlaying)
_ = global.EventBus.Publish(events.PlayerPropertyStateUpdate, events.PlayerPropertyStateUpdateEvent{
State: currentState,
})
_ = global.EventBus.Publish(events.PlayerPropertyPauseUpdate, events.PlayerPropertyPauseUpdateEvent{
Paused: false,
})
@@ -203,6 +207,39 @@ func registerEvents() {
log.Error("register MediaPlayerPlaying event failed: ", err)
}
_, err = eventManager.Attach(vlc.MediaPlayerOpening, func(e vlc.Event, userData interface{}) {
currentState = currentState.NextState(model.PlayerStateLoading)
_ = global.EventBus.Publish(events.PlayerPropertyStateUpdate, events.PlayerPropertyStateUpdateEvent{
State: currentState,
})
}, nil)
if err != nil {
log.Error("register MediaPlayerOpening event failed: ", err)
}
_, err = eventManager.Attach(vlc.MediaPlayerStopped, func(e vlc.Event, userData interface{}) {
currentState = model.PlayerStateIdle
_ = global.EventBus.Publish(events.PlayerPropertyStateUpdate, events.PlayerPropertyStateUpdateEvent{
State: currentState,
})
}, nil)
if err != nil {
log.Error("register MediaPlayerStopped event failed: ", err)
}
_, err = eventManager.Attach(vlc.MediaPlayerEncounteredError, func(e vlc.Event, userData interface{}) {
currentState = model.PlayerStateIdle
_ = global.EventBus.Publish(events.PlayerPropertyStateUpdate, events.PlayerPropertyStateUpdateEvent{
State: currentState,
})
_ = global.EventBus.Publish(events.PlayerPlayErrorUpdate, events.PlayerPlayErrorUpdateEvent{
Error: errors.New("vlc encountered playback error"),
})
}, nil)
if err != nil {
log.Error("register MediaPlayerEncounteredError event failed: ", err)
}
_, err = eventManager.Attach(vlc.MediaPlayerAudioVolume, func(e vlc.Event, userData interface{}) {
volume, _ := player.Volume()
log.Debug("VLC player audio volume: ", volume)
@@ -214,14 +251,32 @@ func registerEvents() {
func registerCmdHandler() {
global.EventBus.Subscribe("", events.PlayerPlayCmd, "player.play", func(evnt *eventbus.Event) {
lock.Lock()
defer lock.Unlock()
mediaInfo := evnt.Data.(events.PlayerPlayCmdEvent).Media.Info
mediaData := evnt.Data.(events.PlayerPlayCmdEvent).Media
currentState = currentState.NextState(model.PlayerStateLoading)
_ = global.EventBus.Publish(events.PlayerPropertyStateUpdate, events.PlayerPropertyStateUpdateEvent{
State: currentState,
})
log.Infof("[VLC Player] Play media %s", mediaInfo.Title)
mediaUrls, err := miaosic.GetMediaUrl(mediaInfo.Meta, miaosic.QualityAny)
if err != nil || len(mediaUrls) == 0 {
respInfo, err := global.EventBus.Call(events.CmdMiaosicGetMediaInfo, events.ReplyMiaosicGetMediaInfo,
events.CmdMiaosicGetMediaInfoData{Meta: mediaData.Info.Meta})
if err == nil {
infoReply := respInfo.Data.(events.ReplyMiaosicGetMediaInfoData)
if infoReply.Error == nil {
mediaData.Info = infoReply.Info
}
}
_ = global.EventBus.Publish(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{
Media: mediaData,
Removed: false,
})
respURL, err := global.EventBus.Call(events.CmdMiaosicGetMediaUrl, events.ReplyMiaosicGetMediaUrl,
events.CmdMiaosicGetMediaUrlData{Meta: mediaData.Info.Meta, Quality: miaosic.QualityAny})
if err != nil {
log.Warn("[VLC PlayControl] get media url failed ", mediaInfo.Meta.ID(), err)
_ = global.EventBus.Publish(
events.PlayerPlayErrorUpdate,
@@ -230,49 +285,64 @@ func registerCmdHandler() {
})
return
}
// 创建媒体对象
var media *vlc.Media
log.Debugf("[VLC PlayControl] get player media %s", mediaUrls[0].Url)
if strings.HasPrefix(mediaUrls[0].Url, "http") {
media, err = vlc.NewMediaFromURL(mediaUrls[0].Url)
} else {
media, err = vlc.NewMediaFromPath(mediaUrls[0].Url)
}
if err != nil {
log.Error("create media failed: ", err)
mediaUrls := respURL.Data.(events.ReplyMiaosicGetMediaUrlData)
if mediaUrls.Error != nil || len(mediaUrls.Urls) == 0 {
replyErr := mediaUrls.Error
if replyErr == nil {
replyErr = errors.New("empty media url list")
}
log.Warn("[VLC PlayControl] get media url failed ", mediaInfo.Meta.ID(), replyErr)
_ = global.EventBus.Publish(
events.PlayerPlayErrorUpdate,
events.PlayerPlayErrorUpdateEvent{
Error: replyErr,
})
return
}
lock.Lock()
defer lock.Unlock()
// 创建媒体对象
var media *vlc.Media
mediaURL := mediaUrls.Urls[0]
log.Debugf("[VLC PlayControl] get player media %s", mediaURL.Url)
if strings.HasPrefix(mediaURL.Url, "http") {
media, err = vlc.NewMediaFromURL(mediaURL.Url)
} else {
media, err = vlc.NewMediaFromPath(mediaURL.Url)
}
if err != nil {
log.Error("create media failed: ", err)
_ = global.EventBus.Publish(events.PlayerPlayErrorUpdate, events.PlayerPlayErrorUpdateEvent{Error: err})
return
}
defer func() {
if err := media.Release(); err != nil {
log.Warn("release media failed: ", err)
}
}()
// 设置HTTP头
if val, ok := mediaUrls[0].Header["User-Agent"]; ok {
if val, ok := mediaURL.Header["User-Agent"]; ok {
err = media.AddOptions(":http-user-agent=" + val)
if err != nil {
log.Warn("add http-user-agent options failed: ", err)
}
}
if val, ok := mediaUrls[0].Header["Referer"]; ok {
if val, ok := mediaURL.Header["Referer"]; ok {
err = media.AddOptions(":http-referrer=" + val)
if err != nil {
log.Warn("add http-referrer options failed: ", err)
}
}
// 更新媒体信息
mediaData := evnt.Data.(events.PlayerPlayCmdEvent).Media
if m, err := miaosic.GetMediaInfo(mediaData.Info.Meta); err == nil {
mediaData.Info = m
}
currentMedia = mediaData
_ = global.EventBus.Publish(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{
Media: mediaData,
Removed: false,
})
// 播放
if err := player.SetMedia(media); err != nil {
log.Error("set media failed: ", err)
_ = global.EventBus.Publish(events.PlayerPlayErrorUpdate, events.PlayerPlayErrorUpdateEvent{Error: err})
return
}
@@ -284,6 +354,7 @@ func registerCmdHandler() {
if err := player.Play(); err != nil {
log.Error("play failed: ", err)
_ = global.EventBus.Publish(events.PlayerPlayErrorUpdate, events.PlayerPlayErrorUpdateEvent{Error: err})
return
}
@@ -296,9 +367,6 @@ func registerCmdHandler() {
_ = global.EventBus.Publish(events.PlayerPropertyPercentPosUpdate, events.PlayerPropertyPercentPosUpdateEvent{
PercentPos: 0,
})
_ = global.EventBus.Publish(events.PlayerPropertyStateUpdate, events.PlayerPropertyStateUpdateEvent{
State: model.PlayerStatePlaying,
})
})
global.EventBus.Subscribe("", events.PlayerToggleCmd, "player.toggle", func(evnt *eventbus.Event) {
@@ -326,10 +394,14 @@ func registerCmdHandler() {
lock.Lock()
defer lock.Unlock()
data := evnt.Data.(events.PlayerSeekCmdEvent)
var err error
if data.Absolute {
player.SetMediaTime(int(data.Position * 1000)) // 转换为毫秒
err = player.SetMediaTime(int(data.Position * 1000)) // 转换为毫秒
} else {
player.SetMediaPosition(float32(data.Position / 100))
err = player.SetMediaPosition(float32(data.Position / 100))
}
if err != nil {
log.Warn("seek failed", err)
}
})
@@ -345,7 +417,9 @@ func registerCmdHandler() {
global.EventBus.Subscribe("", events.PlayerVideoPlayerSetWindowHandleCmd, "player.set_window_handle", func(evnt *eventbus.Event) {
handle := evnt.Data.(events.PlayerVideoPlayerSetWindowHandleCmdEvent).Handle
setWindowHandle(handle)
if err := setWindowHandle(handle); err != nil {
log.Warn("set window handle failed", err)
}
})
global.EventBus.Subscribe("", events.PlayerSetAudioDeviceCmd, "player.set_audio_device", func(evnt *eventbus.Event) {
@@ -366,6 +440,10 @@ func setAudioDevice(deviceID string) error {
lock.Lock()
defer lock.Unlock()
if deviceID == "" {
return nil
}
log.Infof("set audio device to: %s", deviceID)
// 验证设备是否在列表中

View File

@@ -36,6 +36,7 @@ func Initialize() {
log = global.Logger.WithPrefix("MediaProvider")
loadMediaProvider()
handleProvider()
handleSearch()
handleInfo()
createLyricLoader()
@@ -43,6 +44,7 @@ func Initialize() {
_ = global.EventBus.Publish(
events.MediaProviderUpdate, events.MediaProviderUpdateEvent{
Providers: miaosic.ListAvailableProviders(),
Providers: miaosic.ListAvailableProviders(),
ProviderInfos: listProviderInfos(),
})
}

View File

@@ -12,28 +12,8 @@ func handleSourceLogin() {
events.CmdMiaosicQrLogin, "internal.media_provider.qrlogin_handler", func(event *eventbus.Event) {
data := event.Data.(events.CmdMiaosicQrLoginData)
log.Infof("trying login %s", data.Provider)
pvdr, ok := miaosic.GetProvider(data.Provider)
if !ok {
_ = global.EventBus.Reply(
event, events.ReplyMiaosicQrLogin,
events.ReplyMiaosicQrLoginData{
Session: miaosic.QrLoginSession{},
Error: miaosic.ErrorNoSuchProvider,
})
return
}
result, ok := pvdr.(miaosic.Loginable)
if !ok {
_ = global.EventBus.Reply(
event, events.ReplyMiaosicQrLogin,
events.ReplyMiaosicQrLoginData{
Session: miaosic.QrLoginSession{},
Error: miaosic.ErrNotImplemented,
})
return
}
var session miaosic.QrLoginSession
sess, err := result.QrLogin()
sess, err := miaosic.QrLoginByProvider(data.Provider)
if err == nil && sess != nil {
session = *sess
}
@@ -51,28 +31,8 @@ func handleSourceLogin() {
events.CmdMiaosicQrLoginVerify, "internal.media_provider.qrloginverify_handler", func(event *eventbus.Event) {
data := event.Data.(events.CmdMiaosicQrLoginVerifyData)
log.Infof("trying login verify %s", data.Provider)
pvdr, ok := miaosic.GetProvider(data.Provider)
if !ok {
_ = global.EventBus.Reply(
event, events.ReplyMiaosicQrLoginVerify,
events.ReplyMiaosicQrLoginVerifyData{
Result: miaosic.QrLoginResult{},
Error: miaosic.ErrorNoSuchProvider,
})
return
}
loginable, ok := pvdr.(miaosic.Loginable)
if !ok {
_ = global.EventBus.Reply(
event, events.ReplyMiaosicQrLoginVerify,
events.ReplyMiaosicQrLoginVerifyData{
Result: miaosic.QrLoginResult{},
Error: miaosic.ErrNotImplemented,
})
return
}
var result miaosic.QrLoginResult
res, err := loginable.QrLoginVerify(&data.Session)
res, err := miaosic.QrLoginVerifyByProvider(data.Provider, &data.Session)
if err == nil && res != nil {
result = *res
}
@@ -86,4 +46,60 @@ func handleSourceLogin() {
if err != nil {
log.ErrorW("Subscribe miaosic qrloginverify failed", "error", err)
}
err = global.EventBus.Subscribe("",
events.CmdMiaosicLogoutByProvider, "internal.media_provider.logout_by_provider", func(event *eventbus.Event) {
data := event.Data.(events.CmdMiaosicLogoutByProviderData)
_ = global.EventBus.Reply(
event, events.ReplyMiaosicLogoutByProvider,
events.ReplyMiaosicLogoutByProviderData{
Error: miaosic.LogoutByProvider(data.Provider),
})
})
if err != nil {
log.ErrorW("Subscribe miaosic logout failed", "error", err)
}
err = global.EventBus.Subscribe("",
events.CmdMiaosicIsLoginByProvider, "internal.media_provider.is_login_by_provider", func(event *eventbus.Event) {
data := event.Data.(events.CmdMiaosicIsLoginByProviderData)
isLogin, loginErr := miaosic.IsLoginByProvider(data.Provider)
_ = global.EventBus.Reply(
event, events.ReplyMiaosicIsLoginByProvider,
events.ReplyMiaosicIsLoginByProviderData{
IsLogin: isLogin,
Error: loginErr,
})
})
if err != nil {
log.ErrorW("Subscribe miaosic is login failed", "error", err)
}
err = global.EventBus.Subscribe("",
events.CmdMiaosicRestoreSessionByProvider, "internal.media_provider.restore_session_by_provider", func(event *eventbus.Event) {
data := event.Data.(events.CmdMiaosicRestoreSessionByProviderData)
_ = global.EventBus.Reply(
event, events.ReplyMiaosicRestoreSessionByProvider,
events.ReplyMiaosicRestoreSessionByProviderData{
Error: miaosic.RestoreSessionByProvider(data.Provider, data.Session),
})
})
if err != nil {
log.ErrorW("Subscribe miaosic restore session failed", "error", err)
}
err = global.EventBus.Subscribe("",
events.CmdMiaosicSaveSessionByProvider, "internal.media_provider.save_session_by_provider", func(event *eventbus.Event) {
data := event.Data.(events.CmdMiaosicSaveSessionByProviderData)
session, sessionErr := miaosic.SaveSessionByProvider(data.Provider)
_ = global.EventBus.Reply(
event, events.ReplyMiaosicSaveSessionByProvider,
events.ReplyMiaosicSaveSessionByProviderData{
Session: session,
Error: sessionErr,
})
})
if err != nil {
log.ErrorW("Subscribe miaosic save session failed", "error", err)
}
}

View File

@@ -0,0 +1,54 @@
package source
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/eventbus"
"github.com/AynaLivePlayer/miaosic"
)
func listProviderInfos() []events.MiaosicProviderInfo {
providers := make([]events.MiaosicProviderInfo, 0)
for _, providerName := range miaosic.ListAvailableProviders() {
p, ok := miaosic.GetProvider(providerName)
if !ok {
continue
}
_, loginable := p.(miaosic.Loginable)
providers = append(providers, events.MiaosicProviderInfo{
Name: providerName,
Loginable: loginable,
})
}
return providers
}
func handleProvider() {
err := global.EventBus.Subscribe("",
events.CmdMiaosicListProviders, "internal.media_provider.list_providers", func(event *eventbus.Event) {
providers := listProviderInfos()
_ = global.EventBus.Reply(
event, events.ReplyMiaosicListProviders,
events.ReplyMiaosicListProvidersData{
Providers: providers,
})
})
if err != nil {
log.ErrorW("Subscribe list providers event failed", "error", err)
}
err = global.EventBus.Subscribe("",
events.CmdMiaosicMatchMediaByProvider, "internal.media_provider.match_media_by_provider", func(event *eventbus.Event) {
data := event.Data.(events.CmdMiaosicMatchMediaByProviderData)
meta, found := miaosic.MatchMediaByProvider(data.Provider, data.Keyword)
_ = global.EventBus.Reply(
event, events.ReplyMiaosicMatchMediaByProvider,
events.ReplyMiaosicMatchMediaByProviderData{
Meta: meta,
Found: found,
})
})
if err != nil {
log.ErrorW("Subscribe match media by provider event failed", "error", err)
}
}

View File

@@ -16,6 +16,12 @@ func handleSearch() {
searchResult, err := miaosic.SearchByProvider(data.Provider, data.Keyword, 1, 10)
if err != nil {
log.Warnf("Search %s using %s failed: %s", data.Keyword, data.Provider, err)
_ = global.EventBus.Reply(
event, events.ReplyMiaosicSearch,
events.ReplyMiaosicSearchData{
Medias: make([]model.Media, 0),
Error: err,
})
return
}
medias := make([]model.Media, len(searchResult))
@@ -29,6 +35,7 @@ func handleSearch() {
event, events.ReplyMiaosicSearch,
events.ReplyMiaosicSearchData{
Medias: medias,
Error: nil,
})
})
if err != nil {

View File

@@ -1,9 +1,420 @@
package sysmediacontrol
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/eventbus"
"AynaLivePlayer/pkg/logger"
"fmt"
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/introspect"
"github.com/godbus/dbus/v5/prop"
"sync"
"time"
)
const (
mprisBusName = "org.mpris.MediaPlayer2.AynaLivePlayer"
mprisObjPath = dbus.ObjectPath("/org/mpris/MediaPlayer2")
mprisRootIF = "org.mpris.MediaPlayer2"
mprisPlayerIF = "org.mpris.MediaPlayer2.Player"
noTrackPath = dbus.ObjectPath("/org/mpris/MediaPlayer2/TrackList/NoTrack")
)
var (
linuxSMCLog logger.ILogger
linuxSMC *linuxMpris
)
type linuxMpris struct {
conn *dbus.Conn
props *prop.Properties
mu sync.Mutex
trackSeq uint64
trackPath dbus.ObjectPath
metadata map[string]dbus.Variant
positionUS int64
durationUS int64
volume float64
playback string
}
type mprisRoot struct{}
type mprisPlayer struct{}
func (m *linuxMpris) nextTrackPath() dbus.ObjectPath {
m.trackSeq++
return dbus.ObjectPath(fmt.Sprintf("/org/mpris/MediaPlayer2/track/%d", m.trackSeq))
}
func (m *linuxMpris) emitProps(changed map[string]dbus.Variant) {
_ = m.conn.Emit(
mprisObjPath,
"org.freedesktop.DBus.Properties.PropertiesChanged",
mprisPlayerIF,
changed,
[]string{},
)
}
func toMprisVolume(v float64) float64 {
if v < 0 {
v = 0
}
return v / 100.0
}
func fromMprisVolume(v float64) float64 {
if v < 0 {
return 0
}
return v * 100.0
}
func (m *linuxMpris) setPlayback(status string) {
m.mu.Lock()
m.playback = status
props := m.props
m.mu.Unlock()
if props != nil {
props.SetMust(mprisPlayerIF, "PlaybackStatus", status)
}
m.emitProps(map[string]dbus.Variant{
"PlaybackStatus": dbus.MakeVariant(status),
})
}
func (m *linuxMpris) setPosition(seconds float64) {
pos := int64(seconds * float64(time.Second/time.Microsecond))
if pos < 0 {
pos = 0
}
m.mu.Lock()
m.positionUS = pos
props := m.props
m.mu.Unlock()
// Keep Position up-to-date for Get(Position), but avoid emitting change signal.
// KDE/GNOME estimate progress locally while playing; frequent signal pushes cause jitter.
if props != nil {
props.SetMust(mprisPlayerIF, "Position", pos)
}
}
func (m *linuxMpris) setDuration(seconds float64) {
duration := int64(seconds * float64(time.Second/time.Microsecond))
if duration < 0 {
duration = 0
}
m.mu.Lock()
m.durationUS = duration
m.metadata["mpris:length"] = dbus.MakeVariant(duration)
md := m.metadata
props := m.props
m.mu.Unlock()
if props != nil {
props.SetMust(mprisPlayerIF, "Metadata", md)
}
m.emitProps(map[string]dbus.Variant{
"Metadata": dbus.MakeVariant(md),
})
}
func (m *linuxMpris) setVolume(v float64) {
mv := toMprisVolume(v)
m.mu.Lock()
m.volume = mv
props := m.props
m.mu.Unlock()
if props != nil {
props.SetMust(mprisPlayerIF, "Volume", mv)
}
m.emitProps(map[string]dbus.Variant{
"Volume": dbus.MakeVariant(mv),
})
}
func (m *linuxMpris) setPlaying(data events.PlayerPlayingUpdateEvent) {
m.mu.Lock()
defer m.mu.Unlock()
if data.Removed {
m.trackPath = noTrackPath
m.metadata = map[string]dbus.Variant{
"mpris:trackid": dbus.MakeVariant(noTrackPath),
}
m.positionUS = 0
m.playback = "Stopped"
props := m.props
if props != nil {
props.SetMust(mprisPlayerIF, "PlaybackStatus", m.playback)
props.SetMust(mprisPlayerIF, "Metadata", m.metadata)
props.SetMust(mprisPlayerIF, "Position", m.positionUS)
}
m.emitProps(map[string]dbus.Variant{
"PlaybackStatus": dbus.MakeVariant(m.playback),
"Metadata": dbus.MakeVariant(m.metadata),
})
return
}
m.trackPath = m.nextTrackPath()
metadata := map[string]dbus.Variant{
"mpris:trackid": dbus.MakeVariant(m.trackPath),
"xesam:title": dbus.MakeVariant(data.Media.Info.Title),
"xesam:album": dbus.MakeVariant(data.Media.Info.Album),
}
if data.Media.Info.Artist != "" {
metadata["xesam:artist"] = dbus.MakeVariant([]string{data.Media.Info.Artist})
}
if data.Media.Info.Cover.Url != "" {
metadata["mpris:artUrl"] = dbus.MakeVariant(data.Media.Info.Cover.Url)
}
if m.durationUS > 0 {
metadata["mpris:length"] = dbus.MakeVariant(m.durationUS)
}
m.metadata = metadata
m.positionUS = 0
m.playback = "Playing"
props := m.props
if props != nil {
props.SetMust(mprisPlayerIF, "PlaybackStatus", m.playback)
props.SetMust(mprisPlayerIF, "Metadata", m.metadata)
props.SetMust(mprisPlayerIF, "Position", m.positionUS)
}
m.emitProps(map[string]dbus.Variant{
"PlaybackStatus": dbus.MakeVariant(m.playback),
"Metadata": dbus.MakeVariant(m.metadata),
})
}
func (m *mprisRoot) Raise() *dbus.Error { return nil }
func (m *mprisRoot) Quit() *dbus.Error { return nil }
func (m *mprisPlayer) Next() *dbus.Error {
_ = global.EventBus.Publish(events.PlayerPlayNextCmd, events.PlayerPlayNextCmdEvent{})
return nil
}
func (m *mprisPlayer) Previous() *dbus.Error {
_ = global.EventBus.Publish(events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
Position: 0,
Absolute: true,
})
return nil
}
func (m *mprisPlayer) Pause() *dbus.Error {
_ = global.EventBus.Publish(events.PlayerSetPauseCmd, events.PlayerSetPauseCmdEvent{Pause: true})
return nil
}
func (m *mprisPlayer) PlayPause() *dbus.Error {
_ = global.EventBus.Publish(events.PlayerToggleCmd, events.PlayerToggleCmdEvent{})
return nil
}
func (m *mprisPlayer) Stop() *dbus.Error {
_ = global.EventBus.Publish(events.PlayerSetPauseCmd, events.PlayerSetPauseCmdEvent{Pause: true})
return nil
}
func (m *mprisPlayer) Play() *dbus.Error {
_ = global.EventBus.Publish(events.PlayerSetPauseCmd, events.PlayerSetPauseCmdEvent{Pause: false})
return nil
}
func (m *mprisPlayer) Seek(offset int64) *dbus.Error {
_ = global.EventBus.Publish(events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
Position: float64(offset) / float64(time.Second/time.Microsecond),
Absolute: false,
})
return nil
}
func (m *mprisPlayer) SetPosition(trackID dbus.ObjectPath, position int64) *dbus.Error {
if linuxSMC == nil {
return nil
}
linuxSMC.mu.Lock()
currentTrack := linuxSMC.trackPath
linuxSMC.mu.Unlock()
if currentTrack != noTrackPath && trackID != currentTrack {
return nil
}
_ = global.EventBus.Publish(events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
Position: float64(position) / float64(time.Second/time.Microsecond),
Absolute: true,
})
return nil
}
func (m *mprisPlayer) OpenUri(_ string) *dbus.Error { return nil }
func InitSystemMediaControl() {
// stub
linuxSMCLog = global.Logger.WithPrefix("SMTC-Linux")
conn, err := dbus.ConnectSessionBus()
if err != nil {
linuxSMCLog.Warnf("failed to connect session bus: %v", err)
return
}
reply, err := conn.RequestName(mprisBusName, dbus.NameFlagDoNotQueue)
if err != nil || reply != dbus.RequestNameReplyPrimaryOwner {
linuxSMCLog.Warnf("failed to own mpris bus name (%v, %v)", reply, err)
_ = conn.Close()
return
}
linuxSMC = &linuxMpris{
conn: conn,
trackPath: noTrackPath,
metadata: map[string]dbus.Variant{
"mpris:trackid": dbus.MakeVariant(noTrackPath),
},
positionUS: 0,
durationUS: 0,
volume: 0.5,
playback: "Stopped",
}
propsSpec := map[string]map[string]*prop.Prop{
mprisRootIF: {
"CanQuit": {Value: false, Writable: false, Emit: prop.EmitTrue},
"CanRaise": {Value: false, Writable: false, Emit: prop.EmitTrue},
"HasTrackList": {Value: false, Writable: false, Emit: prop.EmitTrue},
"Identity": {Value: config.ProgramName, Writable: false, Emit: prop.EmitTrue},
"DesktopEntry": {Value: "AynaLivePlayer", Writable: false, Emit: prop.EmitTrue},
"SupportedUriSchemes": {Value: []string{"file", "http", "https"}, Writable: false, Emit: prop.EmitTrue},
"SupportedMimeTypes": {Value: []string{}, Writable: false, Emit: prop.EmitTrue},
"Fullscreen": {Value: false, Writable: false, Emit: prop.EmitTrue},
"CanSetFullscreen": {Value: false, Writable: false, Emit: prop.EmitTrue},
},
mprisPlayerIF: {
"PlaybackStatus": {Value: linuxSMC.playback, Writable: false, Emit: prop.EmitFalse},
"Metadata": {Value: linuxSMC.metadata, Writable: false, Emit: prop.EmitFalse},
"Volume": {
Value: linuxSMC.volume,
Writable: true,
Emit: prop.EmitFalse,
Callback: func(c *prop.Change) *dbus.Error {
v, ok := c.Value.(float64)
if !ok {
return dbus.MakeFailedError(fmt.Errorf("invalid volume type %T", c.Value))
}
linuxSMC.setVolume(fromMprisVolume(v))
_ = global.EventBus.Publish(events.PlayerVolumeChangeCmd, events.PlayerVolumeChangeCmdEvent{
Volume: fromMprisVolume(v),
})
return nil
},
},
"Position": {Value: linuxSMC.positionUS, Writable: false, Emit: prop.EmitFalse},
"CanGoNext": {Value: true, Writable: false, Emit: prop.EmitTrue},
"CanGoPrevious": {Value: true, Writable: false, Emit: prop.EmitTrue},
"CanPlay": {Value: true, Writable: false, Emit: prop.EmitTrue},
"CanPause": {Value: true, Writable: false, Emit: prop.EmitTrue},
"CanSeek": {Value: true, Writable: false, Emit: prop.EmitTrue},
"CanControl": {Value: true, Writable: false, Emit: prop.EmitTrue},
},
}
linuxSMC.props = prop.New(conn, mprisObjPath, propsSpec)
_ = conn.Export(&mprisRoot{}, mprisObjPath, mprisRootIF)
_ = conn.Export(&mprisPlayer{}, mprisObjPath, mprisPlayerIF)
node := &introspect.Node{
Name: string(mprisObjPath),
Interfaces: []introspect.Interface{
introspect.IntrospectData,
prop.IntrospectData,
{
Name: mprisRootIF,
Methods: []introspect.Method{
{Name: "Raise"},
{Name: "Quit"},
},
},
{
Name: mprisPlayerIF,
Methods: []introspect.Method{
{Name: "Next"},
{Name: "Previous"},
{Name: "Pause"},
{Name: "PlayPause"},
{Name: "Stop"},
{Name: "Play"},
{
Name: "Seek",
Args: []introspect.Arg{{Name: "Offset", Type: "x", Direction: "in"}},
},
{
Name: "SetPosition",
Args: []introspect.Arg{
{Name: "TrackId", Type: "o", Direction: "in"},
{Name: "Position", Type: "x", Direction: "in"},
},
},
{
Name: "OpenUri",
Args: []introspect.Arg{{Name: "Uri", Type: "s", Direction: "in"}},
},
},
Signals: []introspect.Signal{
{
Name: "Seeked",
Args: []introspect.Arg{{Name: "Position", Type: "x"}},
},
},
},
},
}
_ = conn.Export(introspect.NewIntrospectable(node), mprisObjPath, "org.freedesktop.DBus.Introspectable")
_ = global.EventBus.Subscribe("", events.PlayerPlayingUpdate, "sysmediacontrol.linux.playing", func(event *eventbus.Event) {
linuxSMC.setPlaying(event.Data.(events.PlayerPlayingUpdateEvent))
})
_ = global.EventBus.Subscribe("", events.PlayerPropertyPauseUpdate, "sysmediacontrol.linux.pause", func(event *eventbus.Event) {
if event.Data.(events.PlayerPropertyPauseUpdateEvent).Paused {
linuxSMC.setPlayback("Paused")
} else {
linuxSMC.setPlayback("Playing")
}
})
_ = global.EventBus.Subscribe("", events.PlayerPropertyDurationUpdate, "sysmediacontrol.linux.duration", func(event *eventbus.Event) {
linuxSMC.setDuration(event.Data.(events.PlayerPropertyDurationUpdateEvent).Duration)
})
_ = global.EventBus.Subscribe("", events.PlayerPropertyTimePosUpdate, "sysmediacontrol.linux.timepos", func(event *eventbus.Event) {
linuxSMC.setPosition(event.Data.(events.PlayerPropertyTimePosUpdateEvent).TimePos)
})
_ = global.EventBus.Subscribe("", events.PlayerPropertyVolumeUpdate, "sysmediacontrol.linux.volume", func(event *eventbus.Event) {
linuxSMC.setVolume(event.Data.(events.PlayerPropertyVolumeUpdateEvent).Volume)
})
_ = global.EventBus.Subscribe("", events.PlayerPropertyStateUpdate, "sysmediacontrol.linux.state", func(event *eventbus.Event) {
state := event.Data.(events.PlayerPropertyStateUpdateEvent).State
if state == model.PlayerStateIdle {
linuxSMC.setPlayback("Stopped")
}
})
linuxSMCLog.Info("linux MPRIS media control initialized")
}
func Destroy() {
// stub
if linuxSMC == nil {
return
}
_ = global.EventBus.Unsubscribe(events.PlayerPlayingUpdate, "sysmediacontrol.linux.playing")
_ = global.EventBus.Unsubscribe(events.PlayerPropertyPauseUpdate, "sysmediacontrol.linux.pause")
_ = global.EventBus.Unsubscribe(events.PlayerPropertyDurationUpdate, "sysmediacontrol.linux.duration")
_ = global.EventBus.Unsubscribe(events.PlayerPropertyTimePosUpdate, "sysmediacontrol.linux.timepos")
_ = global.EventBus.Unsubscribe(events.PlayerPropertyVolumeUpdate, "sysmediacontrol.linux.volume")
_ = global.EventBus.Unsubscribe(events.PlayerPropertyStateUpdate, "sysmediacontrol.linux.state")
_, _ = linuxSMC.conn.ReleaseName(mprisBusName)
_ = linuxSMC.conn.Close()
linuxSMC = nil
}

View File

@@ -0,0 +1,153 @@
//go:build linux
package sysmediacontrol
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/eventbus"
"errors"
"sync"
"testing"
"github.com/godbus/dbus/v5"
"github.com/stretchr/testify/require"
)
type publishRecord struct {
id string
data interface{}
}
type mockBus struct {
mu sync.Mutex
published []publishRecord
}
func (m *mockBus) Start() error { return nil }
func (m *mockBus) Wait() error { return nil }
func (m *mockBus) Stop() error { return nil }
func (m *mockBus) Subscribe(string, string, string, eventbus.HandlerFunc) error {
return nil
}
func (m *mockBus) SubscribeAny(string, string, eventbus.HandlerFunc) error { return nil }
func (m *mockBus) SubscribeOnce(string, string, string, eventbus.HandlerFunc) error {
return nil
}
func (m *mockBus) Unsubscribe(string, string) error { return nil }
func (m *mockBus) Publish(eventID string, data interface{}) error {
m.mu.Lock()
defer m.mu.Unlock()
m.published = append(m.published, publishRecord{id: eventID, data: data})
return nil
}
func (m *mockBus) PublishToChannel(string, string, interface{}) error { return nil }
func (m *mockBus) PublishEvent(*eventbus.Event) error { return nil }
func (m *mockBus) Call(string, string, interface{}) (*eventbus.Event, error) {
return nil, errors.New("not implemented")
}
func (m *mockBus) Reply(*eventbus.Event, string, interface{}) error { return nil }
func (m *mockBus) snapshot() []publishRecord {
m.mu.Lock()
defer m.mu.Unlock()
out := make([]publishRecord, len(m.published))
copy(out, m.published)
return out
}
func setupSMCTest(t *testing.T) *mockBus {
t.Helper()
oldBus := global.EventBus
oldSMC := linuxSMC
mb := &mockBus{}
global.EventBus = mb
linuxSMC = nil
t.Cleanup(func() {
global.EventBus = oldBus
linuxSMC = oldSMC
})
return mb
}
func TestMprisPlayerPublishesControlEvents(t *testing.T) {
mb := setupSMCTest(t)
p := &mprisPlayer{}
require.Nil(t, p.Next())
require.Nil(t, p.Previous())
require.Nil(t, p.Pause())
require.Nil(t, p.Play())
require.Nil(t, p.PlayPause())
require.Nil(t, p.Stop())
require.Nil(t, p.Seek(3_000_000))
pubs := mb.snapshot()
require.Len(t, pubs, 7)
require.Equal(t, events.PlayerPlayNextCmd, pubs[0].id)
_, ok := pubs[0].data.(events.PlayerPlayNextCmdEvent)
require.True(t, ok)
require.Equal(t, events.PlayerSeekCmd, pubs[1].id)
prevSeek, ok := pubs[1].data.(events.PlayerSeekCmdEvent)
require.True(t, ok)
require.True(t, prevSeek.Absolute)
require.Equal(t, 0.0, prevSeek.Position)
require.Equal(t, events.PlayerSetPauseCmd, pubs[2].id)
pauseEvt, ok := pubs[2].data.(events.PlayerSetPauseCmdEvent)
require.True(t, ok)
require.True(t, pauseEvt.Pause)
require.Equal(t, events.PlayerSetPauseCmd, pubs[3].id)
playEvt, ok := pubs[3].data.(events.PlayerSetPauseCmdEvent)
require.True(t, ok)
require.False(t, playEvt.Pause)
require.Equal(t, events.PlayerToggleCmd, pubs[4].id)
_, ok = pubs[4].data.(events.PlayerToggleCmdEvent)
require.True(t, ok)
require.Equal(t, events.PlayerSetPauseCmd, pubs[5].id)
stopEvt, ok := pubs[5].data.(events.PlayerSetPauseCmdEvent)
require.True(t, ok)
require.True(t, stopEvt.Pause)
require.Equal(t, events.PlayerSeekCmd, pubs[6].id)
seekEvt, ok := pubs[6].data.(events.PlayerSeekCmdEvent)
require.True(t, ok)
require.False(t, seekEvt.Absolute)
require.InDelta(t, 3.0, seekEvt.Position, 1e-6)
}
func TestMprisPlayerSetPositionTrackGuard(t *testing.T) {
mb := setupSMCTest(t)
p := &mprisPlayer{}
linuxSMC = &linuxMpris{
trackPath: dbus.ObjectPath("/org/mpris/MediaPlayer2/track/1"),
}
require.Nil(t, p.SetPosition(dbus.ObjectPath("/org/mpris/MediaPlayer2/track/2"), 5_000_000))
require.Len(t, mb.snapshot(), 0)
require.Nil(t, p.SetPosition(dbus.ObjectPath("/org/mpris/MediaPlayer2/track/1"), 5_000_000))
pubs := mb.snapshot()
require.Len(t, pubs, 1)
require.Equal(t, events.PlayerSeekCmd, pubs[0].id)
seekEvt, ok := pubs[0].data.(events.PlayerSeekCmdEvent)
require.True(t, ok)
require.True(t, seekEvt.Absolute)
require.InDelta(t, 5.0, seekEvt.Position, 1e-6)
linuxSMC.trackPath = noTrackPath
require.Nil(t, p.SetPosition(dbus.ObjectPath("/org/mpris/MediaPlayer2/track/whatever"), 2_000_000))
pubs = mb.snapshot()
require.Len(t, pubs, 2)
lastEvt, ok := pubs[1].data.(events.PlayerSeekCmdEvent)
require.True(t, ok)
require.True(t, lastEvt.Absolute)
require.InDelta(t, 2.0, lastEvt.Position, 1e-6)
}

View File

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

View File

@@ -3,6 +3,9 @@ package eventbus
import (
"errors"
"fmt"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
@@ -51,6 +54,10 @@ type bus struct {
// logger
log Logger
// worker context markers for deadlock detection in Call()
workerCtxMu sync.RWMutex
workerCtx map[uint64]int // goroutine id -> worker idx
}
// New creates a new Bus.
@@ -75,22 +82,23 @@ func New(opts ...Option) Bus {
pending: make([]*Event, 0, 16),
echoWaiter: make(map[string]chan *Event),
log: option.log,
workerCtx: make(map[uint64]int),
}
for i := 0; i < option.maxWorkerSize; i++ {
b.addWorker()
b.addWorker(i)
}
return b
}
func (b *bus) addWorker() {
func (b *bus) addWorker(workerIdx int) {
b.mu.Lock()
q := make(chan task, b.queueSize)
b.queues = append(b.queues, q)
go b.workerLoop(q)
go b.workerLoop(workerIdx, q)
b.mu.Unlock()
}
func (b *bus) workerLoop(q chan task) {
func (b *bus) workerLoop(workerIdx int, q chan task) {
for {
select {
case <-b.stopCh:
@@ -105,7 +113,14 @@ func (b *bus) workerLoop(q chan task) {
}
case t := <-q:
func() {
gid := curGID()
b.workerCtxMu.Lock()
b.workerCtx[gid] = workerIdx
b.workerCtxMu.Unlock()
defer func() {
b.workerCtxMu.Lock()
delete(b.workerCtx, gid)
b.workerCtxMu.Unlock()
if r := recover(); r != nil {
b.log.Printf("handler panic recovered: event=%s handler=%s panic=%v", t.ev.Id, t.h.name, r)
}
@@ -301,6 +316,9 @@ func (b *bus) Call(eventId string, subEvtId string, data interface{}) (*Event, e
if eventId == "" {
return nil, errors.New("empty eventId")
}
if b.willDeadlockOnCall(eventId) {
return nil, fmt.Errorf("potential deadlock detected: sync Call(%s) from same worker shard", eventId)
}
echo := b.nextEchoId()
wait := make(chan *Event, 1)
@@ -328,6 +346,40 @@ func (b *bus) Call(eventId string, subEvtId string, data interface{}) (*Event, e
}
}
func (b *bus) willDeadlockOnCall(eventId string) bool {
gid := curGID()
b.workerCtxMu.RLock()
currentWorker, inWorker := b.workerCtx[gid]
b.workerCtxMu.RUnlock()
if !inWorker {
return false
}
b.mu.RLock()
targetWorker, hasWorker := b.workerIdxes[eventId]
b.mu.RUnlock()
if !hasWorker {
return false
}
return currentWorker == targetWorker
}
func curGID() uint64 {
var buf [64]byte
n := runtime.Stack(buf[:], false)
// first line format: "goroutine 123 [running]:\n"
line := strings.TrimPrefix(string(buf[:n]), "goroutine ")
space := strings.IndexByte(line, ' ')
if space <= 0 {
return 0
}
id, err := strconv.ParseUint(line[:space], 10, 64)
if err != nil {
return 0
}
return id
}
func (b *bus) Reply(req *Event, eventId string, data interface{}) error {
return b.PublishEvent(&Event{
Id: eventId,

View File

@@ -243,6 +243,37 @@ func TestCall(t *testing.T) {
require.Equal(t, "response to my-data", resp.Data)
}
func TestCall_FastFailOnPotentialDeadlock(t *testing.T) {
b := New(WithMaxWorkerSize(1), WithQueueSize(10))
err := b.Start()
require.NoError(t, err)
defer b.Stop()
// Ensure target event has a worker shard assignment.
err = b.Subscribe("", "inner-request", "inner-responder", func(event *Event) {
_ = b.Reply(event, "inner-response", "ok")
})
require.NoError(t, err)
done := make(chan error, 1)
err = b.Subscribe("", "outer-request", "outer-handler", func(event *Event) {
_, callErr := b.Call("inner-request", "inner-response", nil)
done <- callErr
})
require.NoError(t, err)
err = b.Publish("outer-request", nil)
require.NoError(t, err)
select {
case callErr := <-done:
require.Error(t, callErr)
require.Contains(t, callErr.Error(), "potential deadlock detected")
case <-time.After(2 * time.Second):
t.Fatal("outer handler did not finish in time")
}
}
// TestCall_StopDuringWait checks that Call returns an error if the bus is stopped while waiting.
func TestCall_StopDuringWait(t *testing.T) {
b := New(WithMaxWorkerSize(1), WithQueueSize(10))

View File

@@ -168,20 +168,24 @@ func (d *Diange) Enable() error {
}
d.isCurrentSystem = (!data.Media.IsLiveRoomUser()) && (data.Media.ToUser().Name == model.SystemUser.Name)
})
// check if all available source has a command in sourceConfigs, if not add this source to sourceConfigs
// actually, if default config exists, then this code does nothing.
prvdrs := miaosic.ListAvailableProviders()
for _, pvdr := range prvdrs {
// found pvdr in command list
if _, ok := d.sourceConfigs[pvdr]; ok {
continue
}
d.sourceConfigs[pvdr] = &sourceConfig{
Enable: true,
Command: "点" + pvdr + "歌",
Priority: len(d.sourceConfigs) + 1,
}
}
global.EventBus.Subscribe("",
events.MediaProviderUpdate,
"plugin.diange.provider.update",
func(event *eventbus.Event) {
// check if all available source has a command in sourceConfigs, if not add this source to sourceConfigs
// actually, if default config exists, then this code does nothing.
data := event.Data.(events.MediaProviderUpdateEvent)
for _, pvdr := range data.Providers {
if _, ok := d.sourceConfigs[pvdr]; ok {
continue
}
d.sourceConfigs[pvdr] = &sourceConfig{
Enable: true,
Command: "点" + pvdr + "歌",
Priority: len(d.sourceConfigs) + 1,
}
}
})
return nil
}
@@ -216,6 +220,22 @@ func (d *Diange) getSource(cmd string) []string {
return sources
}
func (d *Diange) searchByProvider(provider, keywords string) ([]model.Media, error) {
resp, err := global.EventBus.Call(
events.CmdMiaosicSearch,
events.ReplyMiaosicSearch,
events.CmdMiaosicSearchData{
Keyword: keywords,
Provider: provider,
},
)
if err != nil {
return nil, err
}
reply := resp.Data.(events.ReplyMiaosicSearchData)
return reply.Medias, reply.Error
}
func (d *Diange) handleMessage(event *eventbus.Event) {
message := event.Data.(events.LiveRoomMessageReceiveEvent).Message
msgs := strings.Split(message.Message, " ")
@@ -292,7 +312,20 @@ func (d *Diange) handleMessage(event *eventbus.Event) {
var mediaMeta miaosic.MetaData
found := false
for _, source := range sources {
mediaMeta, found = miaosic.MatchMediaByProvider(source, keywords)
resp, err := global.EventBus.Call(
events.CmdMiaosicMatchMediaByProvider,
events.ReplyMiaosicMatchMediaByProvider,
events.CmdMiaosicMatchMediaByProviderData{
Provider: source,
Keyword: keywords,
},
)
if err != nil {
continue
}
match := resp.Data.(events.ReplyMiaosicMatchMediaByProviderData)
mediaMeta = match.Meta
found = match.Found
if found {
break
}
@@ -302,22 +335,34 @@ func (d *Diange) handleMessage(event *eventbus.Event) {
if !found {
for _, source := range sources {
medias, err := miaosic.SearchByProvider(source, keywords, 1, 10)
medias, err := d.searchByProvider(source, keywords)
if len(medias) == 0 || err != nil {
continue
}
media = medias[0]
media = medias[0].Info
found = true
break
}
} else {
d.log.Info("Match media: ", mediaMeta)
m, err := miaosic.GetMediaInfo(mediaMeta)
resp, err := global.EventBus.Call(
events.CmdMiaosicGetMediaInfo,
events.ReplyMiaosicGetMediaInfo,
events.CmdMiaosicGetMediaInfoData{
Meta: mediaMeta,
},
)
if err != nil {
d.log.Error("Get media info failed: ", err)
found = false
} else {
reply := resp.Data.(events.ReplyMiaosicGetMediaInfoData)
if reply.Error != nil {
d.log.Error("Get media info failed: ", reply.Error)
found = false
}
media = reply.Info
}
media = m
}
if found {
@@ -410,11 +455,7 @@ func (d *Diange) CreatePanel() fyne.CanvasObject {
skipPlaylistCheck,
)
sourceCfgs := []fyne.CanvasObject{}
prvdrs := miaosic.ListAvailableProviders()
for source, cfg := range d.sourceConfigs {
if !slices.Contains(prvdrs, source) {
continue
}
sourceCfgs = append(
sourceCfgs, container.NewGridWithColumns(2,
widget.NewLabel(source),

View File

@@ -6,6 +6,7 @@ import (
"AynaLivePlayer/gui/component"
config2 "AynaLivePlayer/gui/views/config"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/eventbus"
"AynaLivePlayer/pkg/i18n"
"AynaLivePlayer/pkg/logger"
"AynaLivePlayer/resource"
@@ -55,15 +56,14 @@ func (w *SourceLogin) Enable() error {
func (w *SourceLogin) Disable() error {
w.log.Info("save session for all provider")
providers := miaosic.ListAvailableProviders()
for _, pname := range providers {
if p, ok := miaosic.GetProvider(pname); ok {
pl, ok2 := p.(miaosic.Loginable)
if ok2 {
w.log.Infof("save session for %s", pname)
w.sessions[pname] = pl.SaveSession()
}
for _, provider := range w.listLoginableProviders() {
w.log.Infof("save session for %s", provider)
session, err := w.saveSession(provider)
if err != nil {
w.log.Warnf("save session for %s failed: %v", provider, err)
continue
}
w.sessions[provider] = session
}
return nil
}
@@ -86,30 +86,16 @@ func (w *SourceLogin) CreatePanel() fyne.CanvasObject {
widget.NewLabel(i18n.T("plugin.sourcelogin.current_user")),
currentUser)
providers := miaosic.ListAvailableProviders()
loginableProviders := make([]string, 0)
loginables := make(map[string]miaosic.MediaProvider)
for _, pname := range providers {
if p, ok := miaosic.GetProvider(pname); ok {
pl, ok2 := p.(miaosic.Loginable)
if ok2 {
loginableProviders = append(loginableProviders, pname)
loginables[pname] = p
if session, ok3 := w.sessions[pname]; ok3 {
err := pl.RestoreSession(session)
if err != nil {
w.log.Error("failed to restore session for ", pname)
}
}
}
}
}
providerChoice := widget.NewSelect(loginableProviders, func(s string) {
providerChoice := widget.NewSelect([]string{}, func(s string) {
w.log.Info("switching provider to ", s)
if s != "" {
pvdr, _ := miaosic.GetProvider(s)
provider := pvdr.(miaosic.Loginable)
if provider.IsLogin() {
isLogin, err := w.isLogin(s)
if err != nil {
_ = global.EventBus.Publish(events.ErrorUpdate,
events.ErrorUpdateEvent{Error: err})
return
}
if isLogin {
currentUser.SetText(i18n.T("plugin.sourcelogin.current_user.loggedin"))
} else {
currentUser.SetText(i18n.T("plugin.sourcelogin.current_user.notlogin"))
@@ -119,11 +105,49 @@ func (w *SourceLogin) CreatePanel() fyne.CanvasObject {
sourcePanel := container.NewGridWithColumns(2,
providerChoice, currentStatus)
restoredSessions := make(map[string]bool)
_ = global.EventBus.Subscribe("",
events.MediaProviderUpdate,
"plugin.sourcelogin.providers",
func(event *eventbus.Event) {
data := event.Data.(events.MediaProviderUpdateEvent)
loginableProviders := make([]string, 0)
for _, providerInfo := range data.ProviderInfos {
if providerInfo.Loginable {
loginableProviders = append(loginableProviders, providerInfo.Name)
}
}
for _, provider := range loginableProviders {
if restoredSessions[provider] {
continue
}
session, ok := w.sessions[provider]
if !ok || session == "" {
continue
}
restoredSessions[provider] = true
go func(providerName string, providerSession string) {
if err := w.restoreSession(providerName, providerSession); err != nil {
w.log.Warnf("failed to restore session for %s: %v", providerName, err)
}
}(provider, session)
}
fyne.DoAndWait(func() {
providerChoice.Options = loginableProviders
providerChoice.Refresh()
if providerChoice.Selected == "" && len(loginableProviders) > 0 {
providerChoice.SetSelected(loginableProviders[0])
}
})
})
logoutBtn := component.NewAsyncButton(
i18n.T("plugin.sourcelogin.logout"),
func() {
err := loginables[providerChoice.Selected].(miaosic.Loginable).Logout()
if providerChoice.Selected == "" {
return
}
err := w.logout(providerChoice.Selected)
if err != nil {
_ = global.EventBus.Publish(events.ErrorUpdate,
events.ErrorUpdateEvent{Error: err})
@@ -153,14 +177,25 @@ func (w *SourceLogin) CreatePanel() fyne.CanvasObject {
qrStatus.SetText("")
})
w.log.Info("getting a new qr code for login")
pvdr, _ := miaosic.GetProvider(providerChoice.Selected)
provider := pvdr.(miaosic.Loginable)
currentLoginSession, err = provider.QrLogin()
resp, err := global.EventBus.Call(
events.CmdMiaosicQrLogin,
events.ReplyMiaosicQrLogin,
events.CmdMiaosicQrLoginData{
Provider: providerChoice.Selected,
},
)
if err != nil {
_ = global.EventBus.Publish(events.ErrorUpdate,
events.ErrorUpdateEvent{Error: err})
return
}
qrData := resp.Data.(events.ReplyMiaosicQrLoginData)
if qrData.Error != nil {
_ = global.EventBus.Publish(events.ErrorUpdate,
events.ErrorUpdateEvent{Error: qrData.Error})
return
}
currentLoginSession = &qrData.Session
w.log.Debugf("trying encode url %s to qrcode", currentLoginSession.Url)
data, err := qrcode.Encode(currentLoginSession.Url, qrcode.Medium, 256)
if err != nil {
@@ -186,26 +221,43 @@ func (w *SourceLogin) CreatePanel() fyne.CanvasObject {
if currentProvider == "" {
return
}
pvdr, _ := miaosic.GetProvider(currentProvider)
provider := pvdr.(miaosic.Loginable)
w.log.Info("checking qr status")
result, err := provider.QrLoginVerify(currentLoginSession)
resp, err := global.EventBus.Call(
events.CmdMiaosicQrLoginVerify,
events.ReplyMiaosicQrLoginVerify,
events.CmdMiaosicQrLoginVerifyData{
Provider: currentProvider,
Session: *currentLoginSession,
},
)
if err != nil {
_ = global.EventBus.Publish(events.ErrorUpdate,
events.ErrorUpdateEvent{Error: err})
return
}
resultData := resp.Data.(events.ReplyMiaosicQrLoginVerifyData)
if resultData.Error != nil {
_ = global.EventBus.Publish(events.ErrorUpdate,
events.ErrorUpdateEvent{Error: resultData.Error})
return
}
fyne.DoAndWait(func() {
qrStatus.SetText(result.Message)
qrStatus.SetText(resultData.Result.Message)
})
if result.Success {
if resultData.Result.Success {
currentLoginSession = nil
fyne.DoAndWait(func() {
qrcodeImg.Resource = resource.ImageEmptyQrCode
qrcodeImg.Refresh()
providerChoice.OnChanged(currentProvider)
})
w.sessions[currentProvider] = provider.SaveSession()
session, sessionErr := w.saveSession(currentProvider)
if sessionErr != nil {
_ = global.EventBus.Publish(events.ErrorUpdate,
events.ErrorUpdateEvent{Error: sessionErr})
return
}
w.sessions[currentProvider] = session
}
},
)
@@ -216,3 +268,84 @@ func (w *SourceLogin) CreatePanel() fyne.CanvasObject {
w.panel = container.NewVBox(sourcePanel, controlBox, qrImagePanel)
return w.panel
}
func (w *SourceLogin) listLoginableProviders() []string {
resp, err := global.EventBus.Call(
events.CmdMiaosicListProviders,
events.ReplyMiaosicListProviders,
events.CmdMiaosicListProvidersData{},
)
if err != nil {
w.log.Warnf("list providers failed: %v", err)
return []string{}
}
data := resp.Data.(events.ReplyMiaosicListProvidersData)
providers := make([]string, 0)
for _, provider := range data.Providers {
if provider.Loginable {
providers = append(providers, provider.Name)
}
}
return providers
}
func (w *SourceLogin) isLogin(provider string) (bool, error) {
resp, err := global.EventBus.Call(
events.CmdMiaosicIsLoginByProvider,
events.ReplyMiaosicIsLoginByProvider,
events.CmdMiaosicIsLoginByProviderData{
Provider: provider,
},
)
if err != nil {
return false, err
}
data := resp.Data.(events.ReplyMiaosicIsLoginByProviderData)
return data.IsLogin, data.Error
}
func (w *SourceLogin) logout(provider string) error {
resp, err := global.EventBus.Call(
events.CmdMiaosicLogoutByProvider,
events.ReplyMiaosicLogoutByProvider,
events.CmdMiaosicLogoutByProviderData{
Provider: provider,
},
)
if err != nil {
return err
}
data := resp.Data.(events.ReplyMiaosicLogoutByProviderData)
return data.Error
}
func (w *SourceLogin) restoreSession(provider, session string) error {
resp, err := global.EventBus.Call(
events.CmdMiaosicRestoreSessionByProvider,
events.ReplyMiaosicRestoreSessionByProvider,
events.CmdMiaosicRestoreSessionByProviderData{
Provider: provider,
Session: session,
},
)
if err != nil {
return err
}
data := resp.Data.(events.ReplyMiaosicRestoreSessionByProviderData)
return data.Error
}
func (w *SourceLogin) saveSession(provider string) (string, error) {
resp, err := global.EventBus.Call(
events.CmdMiaosicSaveSessionByProvider,
events.ReplyMiaosicSaveSessionByProvider,
events.CmdMiaosicSaveSessionByProviderData{
Provider: provider,
},
)
if err != nil {
return "", err
}
data := resp.Data.(events.ReplyMiaosicSaveSessionByProviderData)
return data.Session, data.Error
}

View File

@@ -3,18 +3,26 @@ package wshub
import (
"encoding/json"
"fmt"
"strings"
"unicode"
)
// capitalize is a helper function to safely capitalize the first letter of a string.
// It's robust against empty strings.
func capitalize(s string) string {
// toCamelCase converts under_score or lowerCamel to UpperCamel (PascalCase) for JSON key compatibility.
// It is robust against empty strings.
func toCamelCase(s string) string {
if s == "" {
return ""
}
r := []rune(s)
r[0] = unicode.ToUpper(r[0])
return string(r)
parts := strings.Split(s, "_")
for i := range parts {
if parts[i] == "" {
continue
}
r := []rune(parts[i])
r[0] = unicode.ToUpper(r[0])
parts[i] = string(r)
}
return strings.Join(parts, "")
}
// capitalizeKeys recursively traverses an interface{} and capitalizes the keys of any maps it finds.
@@ -26,7 +34,7 @@ func capitalizeKeys(data interface{}) interface{} {
newMap := make(map[string]interface{})
for k, v := range value {
// Capitalize the key and recursively process the value.
newMap[capitalize(k)] = capitalizeKeys(v)
newMap[toCamelCase(k)] = capitalizeKeys(v)
}
return newMap