diff --git a/core/events/mapping.go b/core/events/mapping.go index a042cdb..3927f40 100644 --- a/core/events/mapping.go +++ b/core/events/mapping.go @@ -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() { diff --git a/core/events/miaosic.go b/core/events/miaosic.go index d0037a5..613b74f 100644 --- a/core/events/miaosic.go +++ b/core/events/miaosic.go @@ -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"` +} diff --git a/core/events/provider.go b/core/events/provider.go index 16a0c41..57b495c 100644 --- a/core/events/provider.go +++ b/core/events/provider.go @@ -3,5 +3,6 @@ package events const MediaProviderUpdate = "update.media.provider.update" type MediaProviderUpdateEvent struct { - Providers []string + Providers []string + ProviderInfos []MiaosicProviderInfo } diff --git a/core/events/search.go b/core/events/search.go index c0a9e39..201a768 100644 --- a/core/events/search.go +++ b/core/events/search.go @@ -15,4 +15,5 @@ const ReplyMiaosicSearch = "update.search_result" type ReplyMiaosicSearchData struct { Medias []model.Media + Error error } diff --git a/go.mod b/go.mod index 7db0344..c20cfcd 100644 --- a/go.mod +++ b/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 @@ -38,7 +38,7 @@ require ( ) require ( - fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // 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 diff --git a/go.sum b/go.sum index 2191eb7..f7e8f1c 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -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= +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= diff --git a/internal/source/base.go b/internal/source/base.go index cc013c6..6f470db 100644 --- a/internal/source/base.go +++ b/internal/source/base.go @@ -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(), }) } diff --git a/internal/source/login.go b/internal/source/login.go index d140272..5e546fa 100644 --- a/internal/source/login.go +++ b/internal/source/login.go @@ -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) + } } diff --git a/internal/source/provider.go b/internal/source/provider.go new file mode 100644 index 0000000..9bc2cac --- /dev/null +++ b/internal/source/provider.go @@ -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) + } +} diff --git a/internal/source/search.go b/internal/source/search.go index 0409588..af83737 100644 --- a/internal/source/search.go +++ b/internal/source/search.go @@ -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 { diff --git a/pkg/eventbus/bus_impl.go b/pkg/eventbus/bus_impl.go index 0b4db15..fe6bf6e 100644 --- a/pkg/eventbus/bus_impl.go +++ b/pkg/eventbus/bus_impl.go @@ -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, diff --git a/pkg/eventbus/bus_impl_test.go b/pkg/eventbus/bus_impl_test.go index cf18053..e494bc7 100644 --- a/pkg/eventbus/bus_impl_test.go +++ b/pkg/eventbus/bus_impl_test.go @@ -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)) diff --git a/plugin/diange/diange.go b/plugin/diange/diange.go index 63b5ba2..0d219ec 100644 --- a/plugin/diange/diange.go +++ b/plugin/diange/diange.go @@ -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), diff --git a/plugin/sourcelogin/sourcelogin.go b/plugin/sourcelogin/sourcelogin.go index e104f0f..a4a5f80 100644 --- a/plugin/sourcelogin/sourcelogin.go +++ b/plugin/sourcelogin/sourcelogin.go @@ -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 +}