mirror of
https://github.com/AynaLivePlayer/AynaLivePlayer.git
synced 2026-03-15 05:53:17 +08:00
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,4 +9,5 @@ CMakeCache.txt
|
||||
/release/
|
||||
log.txt
|
||||
config.ini
|
||||
config.ini.bak
|
||||
config.ini.bak
|
||||
.gocache
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ package events
|
||||
const MediaProviderUpdate = "update.media.provider.update"
|
||||
|
||||
type MediaProviderUpdateEvent struct {
|
||||
Providers []string
|
||||
Providers []string
|
||||
ProviderInfos []MiaosicProviderInfo
|
||||
}
|
||||
|
||||
@@ -15,4 +15,5 @@ const ReplyMiaosicSearch = "update.search_result"
|
||||
|
||||
type ReplyMiaosicSearchData struct {
|
||||
Medias []model.Media
|
||||
Error error
|
||||
}
|
||||
|
||||
16
go.mod
16
go.mod
@@ -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
32
go.sum
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func SetupPlayer() {
|
||||
|
||||
func StopPlayer() {
|
||||
if config.Experimental.PlayerCore == "vlc" {
|
||||
//vlc.StopPlayer()
|
||||
vlc.StopPlayer()
|
||||
} else {
|
||||
mpv.StopPlayer()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
// 验证设备是否在列表中
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
54
internal/source/provider.go
Normal file
54
internal/source/provider.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
153
internal/sysmediacontrol/smc_linux_test.go
Normal file
153
internal/sysmediacontrol/smc_linux_test.go
Normal 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)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
const (
|
||||
ProgramName = "卡西米尔唱片机"
|
||||
Version uint32 = 0x010202
|
||||
Version uint32 = 0x010203
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
Submodule pkg/liveroom-sdk updated: 7e498e64e5...6db40cc038
Submodule pkg/miaosic updated: 2e615a15d7...f8fee56393
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user