diff --git a/go.mod b/go.mod index 6da156f..eab1971 100644 --- a/go.mod +++ b/go.mod @@ -1,36 +1,39 @@ module AynaLivePlayer -go 1.19 +go 1.23.0 + +toolchain go1.24.4 replace ( github.com/AynaLivePlayer/liveroom-sdk v0.1.0 => ./pkg/liveroom-sdk // submodule - github.com/AynaLivePlayer/miaosic v0.1.5 => ./pkg/miaosic // submodule + github.com/AynaLivePlayer/miaosic v0.1.7 => ./pkg/miaosic // submodule github.com/saltosystems/winrt-go => github.com/go-musicfox/winrt-go v0.1.4 // winrt with media foundation ) require ( - fyne.io/fyne/v2 v2.5.4 + fyne.io/fyne/v2 v2.6.1 github.com/AynaLivePlayer/liveroom-sdk v0.1.0 - github.com/AynaLivePlayer/miaosic v0.1.5 + github.com/AynaLivePlayer/miaosic v0.1.7 + github.com/adrg/libvlc-go/v3 v3.1.6 github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b github.com/antonfisher/nested-logrus-formatter v1.3.1 - github.com/aynakeya/go-mpv v0.0.6 - github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a + github.com/aynakeya/go-mpv v0.0.7 + 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.7.0 + github.com/go-resty/resty/v2 v2.16.5 github.com/gorilla/websocket v1.5.3 - github.com/mattn/go-colorable v0.1.12 + github.com/mattn/go-colorable v0.1.14 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 - github.com/saltosystems/winrt-go v0.0.0-20240320184339-289d313a74b7 + github.com/saltosystems/winrt-go v0.0.0-20241223121953-98e32661f6ff github.com/sirupsen/logrus v1.9.3 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e - github.com/stretchr/testify v1.8.4 - github.com/tidwall/gjson v1.17.3 + github.com/stretchr/testify v1.10.0 + github.com/tidwall/gjson v1.18.0 github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459 - go.uber.org/zap v1.26.0 - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 - golang.org/x/sys v0.25.0 + go.uber.org/zap v1.27.0 + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b + golang.org/x/sys v0.33.0 gopkg.in/ini.v1 v1.67.0 ) @@ -38,45 +41,46 @@ require ( fyne.io/systray v1.11.0 // indirect github.com/AynaLivePlayer/blivedm-go v0.0.0-20250527143915-74cc4b2603bc // indirect github.com/BurntSushi/toml v1.4.0 // indirect - github.com/PuerkitoBio/goquery v1.7.1 // indirect + github.com/PuerkitoBio/goquery v1.10.3 // indirect github.com/XiaoMengXinX/Music163Api-Go v0.1.30 // indirect github.com/andybalholm/brotli v1.1.0 // indirect - github.com/andybalholm/cascadia v1.2.0 // indirect + github.com/andybalholm/cascadia v1.3.3 // indirect github.com/aynakeya/deepcolor v1.0.3 // indirect github.com/aynakeya/open-bilibili-live v0.0.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25 // indirect + github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 // indirect github.com/fredbi/uri v1.1.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect - github.com/fyne-io/glfw-js v0.0.0-20241126112943-313d8a0fe1d0 // indirect - github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect - github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect + github.com/fyne-io/gl-js v0.1.0 // indirect + github.com/fyne-io/glfw-js v0.2.0 // indirect + github.com/fyne-io/image v0.1.1 // indirect + github.com/fyne-io/oksvg v0.1.0 // indirect + 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.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.5.0 // indirect - github.com/gopherjs/gopherjs v1.17.2 // indirect - github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 // indirect + github.com/hack-pad/go-indexeddb v0.3.2 // indirect + github.com/hack-pad/safejs v0.1.0 // indirect + github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect github.com/jinzhu/copier v0.4.0 // indirect - github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect + github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rymdport/portal v0.3.0 // indirect - github.com/sahilm/fuzzy v0.1.0 // indirect + github.com/rymdport/portal v0.4.1 // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect - github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/cast v1.9.2 // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - github.com/yuin/goldmark v1.7.1 // indirect + github.com/yuin/goldmark v1.7.8 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/image v0.18.0 // indirect - golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect - golang.org/x/net v0.25.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/image v0.24.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/text v0.26.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/gui/component/button.go b/gui/component/button.go index 4245b99..8e77113 100644 --- a/gui/component/button.go +++ b/gui/component/button.go @@ -37,7 +37,8 @@ func (b *AsyncButton) SetOnTapped(f func()) { b.Disable() go func() { f() - b.Enable() + //time.Sleep(3 * time.Second) + fyne.Do(b.Enable) }() } } diff --git a/gui/config_basic.go b/gui/config_basic.go index def8a64..ee66ae1 100644 --- a/gui/config_basic.go +++ b/gui/config_basic.go @@ -5,6 +5,7 @@ import ( "AynaLivePlayer/core/model" "AynaLivePlayer/global" "AynaLivePlayer/gui/component" + "AynaLivePlayer/gui/gutil" "AynaLivePlayer/pkg/config" "AynaLivePlayer/pkg/event" "AynaLivePlayer/pkg/i18n" @@ -44,10 +45,10 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject { }) global.EventManager.RegisterA(events.PlaylistModeChangeUpdate(model.PlaylistIDPlayer), "gui.config.basic.random_playlist.player", - func(event *event.Event) { + gutil.ThreadSafeHandler(func(event *event.Event) { data := event.Data.(events.PlaylistModeChangeUpdateEvent) playerRandomCheck.SetChecked(data.Mode == model.PlaylistModeRandom) - }) + })) systemRandomCheck := widget.NewCheck(i18n.T("gui.config.basic.random_playlist.system"), func(b bool) { @@ -63,10 +64,10 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject { global.EventManager.RegisterA(events.PlaylistModeChangeUpdate(model.PlaylistIDSystem), "gui.config.basic.random_playlist.system", - func(event *event.Event) { + gutil.ThreadSafeHandler(func(event *event.Event) { data := event.Data.(events.PlaylistModeChangeUpdateEvent) systemRandomCheck.SetChecked(data.Mode == model.PlaylistModeRandom) - }) + })) randomPlaylist := container.NewHBox( widget.NewLabel(i18n.T("gui.config.basic.random_playlist")), @@ -86,7 +87,7 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject { global.EventManager.RegisterA( events.PlayerAudioDeviceUpdate, "gui.config.basic.audio_device.update", - func(event *event.Event) { + gutil.ThreadSafeHandler(func(event *event.Event) { data := event.Data.(events.PlayerAudioDeviceUpdateEvent) devices := make([]string, len(data.Devices)) deviceDesc2Name = make(map[string]string) @@ -102,7 +103,7 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject { deviceSel.Options = devices deviceSel.Selected = currentDevice deviceSel.Refresh() - }) + })) outputDevice := container.NewBorder(nil, nil, widget.NewLabel(i18n.T("gui.config.basic.audio_device")), nil, diff --git a/gui/gui.go b/gui/gui.go index 13b32ef..c027acf 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -4,6 +4,7 @@ import ( "AynaLivePlayer/core/events" "AynaLivePlayer/core/model" "AynaLivePlayer/global" + "AynaLivePlayer/gui/gutil" "AynaLivePlayer/pkg/config" "AynaLivePlayer/pkg/event" "AynaLivePlayer/pkg/i18n" @@ -14,6 +15,7 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/widget" + "os" _logger "AynaLivePlayer/pkg/logger" ) @@ -33,7 +35,9 @@ func Initialize() { logger = global.Logger.WithPrefix("GUI") black_magic() logger.Info("Initializing GUI") - //os.Setenv("FYNE_FONT", config.GetAssetPath("msyh.ttc")) + if config.General.CustomFonts != "" { + _ = os.Setenv("FYNE_FONT", config.GetAssetPath(config.General.CustomFonts)) + } App = app.NewWithID(config.ProgramName) //App.Settings().SetTheme(&myTheme{}) MainWindow = App.NewWindow(fmt.Sprintf("%s Ver %s", config.ProgramName, model.Version(config.Version))) @@ -65,18 +69,20 @@ func Initialize() { //MainWindow.Resize(fyne.NewSize(1280, 720)) MainWindow.Resize(fyne.NewSize(config.General.Width, config.General.Height)) - setupPlayerWindow() + // todo: fix, window were created even if not show. this block gui from closing + // i can't create sub window before the main window shows. + // setupPlayerWindow() // register error global.EventManager.RegisterA( - events.ErrorUpdate, "gui.show_error", func(e *event.Event) { + events.ErrorUpdate, "gui.show_error", gutil.ThreadSafeHandler(func(e *event.Event) { err := e.Data.(events.ErrorUpdateEvent).Error logger.Warnf("gui received error event: %v, %v", err, err == nil) if err == nil { return } dialog.ShowError(err, MainWindow) - }) + })) checkUpdate() MainWindow.SetFixedSize(config.General.FixedSize) @@ -91,7 +97,9 @@ func Initialize() { }) } MainWindow.SetOnClosed(func() { + logger.Infof("GUI closing") if playerWindow != nil { + logger.Infof("player window closing") playerWindow.Close() } }) diff --git a/gui/gutil/fyne.go b/gui/gutil/fyne.go new file mode 100644 index 0000000..985bdb8 --- /dev/null +++ b/gui/gutil/fyne.go @@ -0,0 +1,15 @@ +package gutil + +import ( + "AynaLivePlayer/pkg/event" + "fyne.io/fyne/v2" +) + +// since 2.6.1, calls to fyne API from other go routine must be wrapped in fyne.Do +func ThreadSafeHandler(fn func(e *event.Event)) func(e *event.Event) { + return func(e *event.Event) { + fyne.Do(func() { + fn(e) + }) + } +} diff --git a/gui/history_list.go b/gui/history_list.go index 8a6f807..5b69829 100644 --- a/gui/history_list.go +++ b/gui/history_list.go @@ -4,6 +4,7 @@ import ( "AynaLivePlayer/core/events" "AynaLivePlayer/core/model" "AynaLivePlayer/global" + "AynaLivePlayer/gui/gutil" "AynaLivePlayer/pkg/event" "AynaLivePlayer/pkg/i18n" "fmt" @@ -76,10 +77,10 @@ func createHistoryList() fyne.CanvasObject { func registerHistoryHandler() { global.EventManager.RegisterA( events.PlaylistDetailUpdate(model.PlaylistIDHistory), - "gui.history.update", func(event *event.Event) { + "gui.history.update", gutil.ThreadSafeHandler(func(event *event.Event) { History.mux.Lock() History.Medias = event.Data.(events.PlaylistDetailUpdateEvent).Medias History.List.Refresh() History.mux.Unlock() - }) + })) } diff --git a/gui/liverooms.go b/gui/liverooms.go index bf583d1..e92a6e3 100644 --- a/gui/liverooms.go +++ b/gui/liverooms.go @@ -4,6 +4,7 @@ import ( "AynaLivePlayer/core/events" "AynaLivePlayer/core/model" "AynaLivePlayer/global" + "AynaLivePlayer/gui/gutil" "AynaLivePlayer/gui/xfyne" "AynaLivePlayer/pkg/event" "AynaLivePlayer/pkg/i18n" @@ -135,14 +136,14 @@ func registerRoomHandlers() { global.EventManager.RegisterA( events.LiveRoomProviderUpdate, "gui.liveroom.provider_update", - func(event *event.Event) { + gutil.ThreadSafeHandler(func(event *event.Event) { RoomTab.providers = event.Data.(events.LiveRoomProviderUpdateEvent).Providers RoomTab.Rooms.Refresh() - }) + })) global.EventManager.RegisterA( events.LiveRoomRoomsUpdate, "gui.liveroom.rooms_update", - func(event *event.Event) { + gutil.ThreadSafeHandler(func(event *event.Event) { logger.Infof("Update rooms") data := event.Data.(events.LiveRoomRoomsUpdateEvent) RoomTab.lock.Lock() @@ -150,11 +151,11 @@ func registerRoomHandlers() { RoomTab.Rooms.Select(0) RoomTab.Rooms.Refresh() RoomTab.lock.Unlock() - }) + })) global.EventManager.RegisterA( events.LiveRoomStatusUpdate, "gui.liveroom.room_status_update", - func(event *event.Event) { + gutil.ThreadSafeHandler(func(event *event.Event) { room := event.Data.(events.LiveRoomStatusUpdateEvent).Room index := -1 for i := 0; i < len(RoomTab.rooms); i++ { @@ -182,7 +183,7 @@ func registerRoomHandlers() { } RoomTab.Status.Refresh() } - }) + })) } @@ -216,10 +217,10 @@ func createRoomController() fyne.CanvasObject { global.EventManager.RegisterA( events.LiveRoomOperationFinish, "gui.liveroom.operation_finish", - func(event *event.Event) { + gutil.ThreadSafeHandler(func(event *event.Event) { RoomTab.ConnectBtn.Enable() RoomTab.DisConnectBtn.Enable() - }) + })) RoomTab.Status = widget.NewLabel(i18n.T("gui.room.waiting")) RoomTab.RoomTitle = widget.NewLabel("") RoomTab.RoomID = widget.NewLabel("") diff --git a/gui/player_controller.go b/gui/player_controller.go index a2934b9..40f5971 100644 --- a/gui/player_controller.go +++ b/gui/player_controller.go @@ -59,7 +59,7 @@ func registerPlayControllerHandler() { PlayController.ButtonLrc.OnTapped = func() { if !PlayController.LrcWindowOpen { PlayController.LrcWindowOpen = true - createLyricWindow().Show() + createLyricWindow().Close() } } @@ -67,26 +67,25 @@ func registerPlayControllerHandler() { showPlayerWindow() } - global.EventManager.RegisterA(events.PlayerPropertyPauseUpdate, "gui.player.controller.paused", func(event *event.Event) { + global.EventManager.RegisterA(events.PlayerPropertyPauseUpdate, "gui.player.controller.paused", gutil.ThreadSafeHandler(func(event *event.Event) { if event.Data.(events.PlayerPropertyPauseUpdateEvent).Paused { PlayController.ButtonSwitch.Icon = theme.MediaPlayIcon() } else { PlayController.ButtonSwitch.Icon = theme.MediaPauseIcon() } PlayController.ButtonSwitch.Refresh() - }) + })) - global.EventManager.RegisterA(events.PlayerPropertyPercentPosUpdate, "gui.player.controller.percent_pos", func(event *event.Event) { + global.EventManager.RegisterA(events.PlayerPropertyPercentPosUpdate, "gui.player.controller.percent_pos", gutil.ThreadSafeHandler(func(event *event.Event) { if PlayController.Progress.Dragging { return } PlayController.Progress.Value = event.Data.(events.PlayerPropertyPercentPosUpdateEvent).PercentPos * 10 PlayController.Progress.Refresh() - }) + })) - global.EventManager.RegisterA(events.PlayerPropertyIdleActiveUpdate, "gui.player.controller.idle_active", func(event *event.Event) { + global.EventManager.RegisterA(events.PlayerPropertyIdleActiveUpdate, "gui.player.controller.idle_active", gutil.ThreadSafeHandler(func(event *event.Event) { isIdle := event.Data.(events.PlayerPropertyIdleActiveUpdateEvent).IsIdle - // todo: @3 if isIdle { PlayController.Progress.Value = 0 PlayController.Progress.Max = 0 @@ -97,7 +96,7 @@ func registerPlayControllerHandler() { } else { PlayController.Progress.Max = 1000 } - }) + })) PlayController.Progress.Max = 0 PlayController.Progress.OnDragEnd = func(f float64) { @@ -107,18 +106,18 @@ func registerPlayControllerHandler() { }) } - global.EventManager.RegisterA(events.PlayerPropertyTimePosUpdate, "gui.player.controller.time_pos", func(event *event.Event) { + global.EventManager.RegisterA(events.PlayerPropertyTimePosUpdate, "gui.player.controller.time_pos", gutil.ThreadSafeHandler(func(event *event.Event) { PlayController.CurrentTime.SetText(util.FormatTime(int(event.Data.(events.PlayerPropertyTimePosUpdateEvent).TimePos))) - }) + })) - global.EventManager.RegisterA(events.PlayerPropertyDurationUpdate, "gui.player.controller.duration", func(event *event.Event) { + global.EventManager.RegisterA(events.PlayerPropertyDurationUpdate, "gui.player.controller.duration", gutil.ThreadSafeHandler(func(event *event.Event) { PlayController.TotalTime.SetText(util.FormatTime(int(event.Data.(events.PlayerPropertyDurationUpdateEvent).Duration))) - }) + })) - global.EventManager.RegisterA(events.PlayerPropertyVolumeUpdate, "gui.player.controller.volume", func(event *event.Event) { + global.EventManager.RegisterA(events.PlayerPropertyVolumeUpdate, "gui.player.controller.volume", gutil.ThreadSafeHandler(func(event *event.Event) { PlayController.Volume.Value = event.Data.(events.PlayerPropertyVolumeUpdateEvent).Volume PlayController.Volume.Refresh() - }) + })) PlayController.Volume.OnChanged = func(f float64) { global.EventManager.CallA(events.PlayerVolumeChangeCmd, events.PlayerVolumeChangeCmdEvent{ @@ -126,7 +125,8 @@ func registerPlayControllerHandler() { }) } - global.EventManager.RegisterA(events.PlayerPlayingUpdate, "gui.player.updateinfo", func(event *event.Event) { + // todo: double check cover loading for new thread model + global.EventManager.RegisterA(events.PlayerPlayingUpdate, "gui.player.updateinfo", gutil.ThreadSafeHandler(func(event *event.Event) { if event.Data.(events.PlayerPlayingUpdateEvent).Removed { PlayController.Progress.Value = 0 PlayController.Progress.Max = 0 @@ -176,12 +176,12 @@ func registerPlayControllerHandler() { return } PlayController.Cover.Resource = pic.Resource - PlayController.Cover.Refresh() + fyne.Do(PlayController.Cover.Refresh) } }() } - }) + })) } func createPlayControllerV2() fyne.CanvasObject { diff --git a/gui/player_lyric.go b/gui/player_lyric.go index da85d38..deec266 100644 --- a/gui/player_lyric.go +++ b/gui/player_lyric.go @@ -3,6 +3,7 @@ package gui import ( "AynaLivePlayer/core/events" "AynaLivePlayer/global" + "AynaLivePlayer/gui/gutil" "AynaLivePlayer/pkg/event" "AynaLivePlayer/pkg/i18n" "fyne.io/fyne/v2" @@ -41,8 +42,9 @@ func createLyricWindow() fyne.Window { w.CenterOnScreen() // register handlers + // todo: lyric not update correctly, known bug https://github.com/fyne-io/fyne/pull/5783 global.EventManager.RegisterA( - events.PlayerLyricPosUpdate, "player.lyric.current_lyric", func(event *event.Event) { + events.PlayerLyricPosUpdate, "player.lyric.current_lyric", gutil.ThreadSafeHandler(func(event *event.Event) { e := event.Data.(events.PlayerLyricPosUpdateEvent) logger.Debug("lyric update", e) if prevIndex >= len(fullLrc.Objects) || e.CurrentIndex >= len(fullLrc.Objects) { @@ -66,13 +68,13 @@ func createLyricWindow() fyne.Window { }, }) fullLrc.Refresh() - }) + })) - global.EventManager.RegisterA(events.PlayerLyricReload, "player.lyric.current_lyric", func(event *event.Event) { + global.EventManager.RegisterA(events.PlayerLyricReload, "player.lyric.current_lyric", gutil.ThreadSafeHandler(func(event *event.Event) { e := event.Data.(events.PlayerLyricReloadEvent) fullLrc.Objects = createLyricObj(&e.Lyrics) lrcWindow.Refresh() - }) + })) global.EventManager.CallA(events.PlayerLyricRequestCmd, events.PlayerLyricRequestCmdEvent{}) diff --git a/gui/player_playlist.go b/gui/player_playlist.go index 3deb7ff..84f3293 100644 --- a/gui/player_playlist.go +++ b/gui/player_playlist.go @@ -4,6 +4,7 @@ import ( "AynaLivePlayer/core/events" "AynaLivePlayer/core/model" "AynaLivePlayer/global" + "AynaLivePlayer/gui/gutil" "AynaLivePlayer/pkg/event" "AynaLivePlayer/pkg/i18n" "fmt" @@ -74,12 +75,12 @@ func createPlaylist() fyne.CanvasObject { object.(*fyne.Container).Objects[1].(*widget.Label).SetText(fmt.Sprintf("%d", id)) object.(*fyne.Container).Objects[2].(*playlistOperationButton).Index = id }) - global.EventManager.RegisterA(events.PlaylistDetailUpdate(model.PlaylistIDPlayer), "gui.player.playlist.update", func(event *event.Event) { + global.EventManager.RegisterA(events.PlaylistDetailUpdate(model.PlaylistIDPlayer), "gui.player.playlist.update", gutil.ThreadSafeHandler(func(event *event.Event) { UserPlaylist.mux.Lock() UserPlaylist.Medias = event.Data.(events.PlaylistDetailUpdateEvent).Medias UserPlaylist.List.Refresh() UserPlaylist.mux.Unlock() - }) + })) return container.NewBorder( container.NewBorder(nil, nil, widget.NewLabel("#"), widget.NewLabel(i18n.T("gui.player.playlist.ops")), diff --git a/gui/player_videoplayer.go b/gui/player_videoplayer.go index 96bcb13..540887f 100644 --- a/gui/player_videoplayer.go +++ b/gui/player_videoplayer.go @@ -17,6 +17,9 @@ func setupPlayerWindow() { } func showPlayerWindow() { + if playerWindow == nil { + setupPlayerWindow() + } playerWindow.Show() if playerWindowHandle == 0 { playerWindowHandle = xfyne.GetWindowHandle(playerWindow) diff --git a/gui/playlists.go b/gui/playlists.go index 500724d..4935a14 100644 --- a/gui/playlists.go +++ b/gui/playlists.go @@ -4,6 +4,7 @@ import ( "AynaLivePlayer/core/events" "AynaLivePlayer/core/model" "AynaLivePlayer/global" + "AynaLivePlayer/gui/gutil" "AynaLivePlayer/gui/xfyne" "AynaLivePlayer/pkg/event" "AynaLivePlayer/pkg/i18n" @@ -97,14 +98,14 @@ func createPlaylists() fyne.CanvasObject { }) } global.EventManager.RegisterA(events.MediaProviderUpdate, - "gui.playlists.provider.update", func(event *event.Event) { + "gui.playlists.provider.update", gutil.ThreadSafeHandler(func(event *event.Event) { providers := event.Data.(events.MediaProviderUpdateEvent) s := make([]string, len(providers.Providers)) copy(s, providers.Providers) PlaylistManager.providers = s - }) + })) global.EventManager.RegisterA(events.PlaylistManagerInfoUpdate, - "gui.playlists.info.update", func(event *event.Event) { + "gui.playlists.info.update", gutil.ThreadSafeHandler(func(event *event.Event) { data := event.Data.(events.PlaylistManagerInfoUpdateEvent) prevLen := len(PlaylistManager.currentPlaylists) PlaylistManager.currentPlaylists = data.Playlists @@ -113,12 +114,12 @@ func createPlaylists() fyne.CanvasObject { if prevLen != len(PlaylistManager.currentPlaylists) { PlaylistManager.Playlists.Select(0) } - }) + })) global.EventManager.RegisterA(events.PlaylistManagerSystemUpdate, - "gui.playlists.system.update", func(event *event.Event) { + "gui.playlists.system.update", gutil.ThreadSafeHandler(func(event *event.Event) { data := event.Data.(events.PlaylistManagerSystemUpdateEvent) PlaylistManager.CurrentSystemPlaylist.SetText(i18n.T("gui.playlist.current") + data.Info.DisplayName()) - }) + })) return container.NewHBox( container.NewBorder( nil, container.NewCenter(container.NewHBox(PlaylistManager.AddBtn, PlaylistManager.RemoveBtn)), @@ -190,12 +191,12 @@ func createPlaylistMedias() fyne.CanvasObject { } }) global.EventManager.RegisterA(events.PlaylistManagerCurrentUpdate, - "gui.playlists.current.update", func(event *event.Event) { + "gui.playlists.current.update", gutil.ThreadSafeHandler(func(event *event.Event) { logger.Infof("receive current playlist update, try to refresh playlist medias") data := event.Data.(events.PlaylistManagerCurrentUpdateEvent) PlaylistManager.currentMedias = data.Medias PlaylistManager.PlaylistMedia.Refresh() - }) + })) return container.NewBorder( container.NewHBox(PlaylistManager.RefreshBtn, PlaylistManager.SetAsSystemBtn, PlaylistManager.CurrentSystemPlaylist), nil, nil, nil, diff --git a/gui/search_bar.go b/gui/search_bar.go index 11ae422..9abc539 100644 --- a/gui/search_bar.go +++ b/gui/search_bar.go @@ -5,6 +5,7 @@ import ( "AynaLivePlayer/core/model" "AynaLivePlayer/global" "AynaLivePlayer/gui/component" + "AynaLivePlayer/gui/gutil" "AynaLivePlayer/pkg/event" "AynaLivePlayer/pkg/i18n" "fyne.io/fyne/v2" @@ -14,7 +15,7 @@ import ( var SearchBar = &struct { Input *component.Entry - Button *component.AsyncButton + Button *widget.Button UseSource *widget.Select }{} @@ -26,7 +27,7 @@ func createSearchBar() fyne.CanvasObject { SearchBar.Button.OnTapped() } } - SearchBar.Button = component.NewAsyncButton(i18n.T("gui.search.search"), func() { + SearchBar.Button = widget.NewButton(i18n.T("gui.search.search"), func() { keyword := SearchBar.Input.Text pr := SearchBar.UseSource.Selected logger.Debugf("Search keyword: %s, provider: %s", keyword, pr) @@ -41,7 +42,7 @@ func createSearchBar() fyne.CanvasObject { }) global.EventManager.RegisterA(events.MediaProviderUpdate, - "gui.search.provider.update", func(event *event.Event) { + "gui.search.provider.update", gutil.ThreadSafeHandler(func(event *event.Event) { providers := event.Data.(events.MediaProviderUpdateEvent) s := make([]string, len(providers.Providers)) copy(s, providers.Providers) @@ -49,7 +50,7 @@ func createSearchBar() fyne.CanvasObject { if len(s) > 0 { SearchBar.UseSource.SetSelected(s[0]) } - }) + })) SearchBar.UseSource = widget.NewSelect([]string{}, func(s string) { }) diff --git a/gui/search_list.go b/gui/search_list.go index 59c267f..5aa074c 100644 --- a/gui/search_list.go +++ b/gui/search_list.go @@ -4,6 +4,7 @@ import ( "AynaLivePlayer/core/events" "AynaLivePlayer/core/model" "AynaLivePlayer/global" + "AynaLivePlayer/gui/gutil" "AynaLivePlayer/pkg/event" "AynaLivePlayer/pkg/i18n" "fmt" @@ -61,13 +62,13 @@ func createSearchList() fyne.CanvasObject { }) } }) - global.EventManager.RegisterA(events.SearchResultUpdate, "gui.search.update_result", func(event *event.Event) { + global.EventManager.RegisterA(events.SearchResultUpdate, "gui.search.update_result", gutil.ThreadSafeHandler(func(event *event.Event) { items := event.Data.(events.SearchResultUpdateEvent).Medias SearchResult.Items = items SearchResult.mux.Lock() SearchResult.List.Refresh() SearchResult.mux.Unlock() - }) + })) return container.NewBorder( container.NewBorder(nil, nil, widget.NewLabel("#"), widget.NewLabel(i18n.T("gui.search.operation")), diff --git a/gui/updater.go b/gui/updater.go index 16b8dc6..ad62d4a 100644 --- a/gui/updater.go +++ b/gui/updater.go @@ -3,6 +3,7 @@ package gui import ( "AynaLivePlayer/core/events" "AynaLivePlayer/global" + "AynaLivePlayer/gui/gutil" "AynaLivePlayer/pkg/event" "AynaLivePlayer/pkg/i18n" "fyne.io/fyne/v2/dialog" @@ -11,7 +12,7 @@ import ( func checkUpdate() { global.EventManager.RegisterA( - events.CheckUpdateResultUpdate, "gui.updater.check_update", func(event *event.Event) { + events.CheckUpdateResultUpdate, "gui.updater.check_update", gutil.ThreadSafeHandler(func(event *event.Event) { data := event.Data.(events.CheckUpdateResultUpdateEvent) msg := data.Info.Version.String() + "\n\n\n" + data.Info.Info if data.HasUpdate { @@ -27,5 +28,5 @@ func checkUpdate() { widget.NewRichTextFromMarkdown(""), MainWindow) } - }) + })) } diff --git a/gui/xfyne/window.go b/gui/xfyne/window.go index 124d4c5..4bb6eda 100644 --- a/gui/xfyne/window.go +++ b/gui/xfyne/window.go @@ -11,6 +11,7 @@ import ( // getGlfwWindow returns the glfw.Window pointer from a fyne.Window. // very unsafe and ugly hacks. but it works. +// todo: replace with LifeCycle https://github.com/fyne-io/fyne/issues/4483 func getGlfwWindow(window fyne.Window) *glfw.Window { rv := reflect.ValueOf(window) if rv.Type().String() != "*glfw.window" { diff --git a/internal/player/player.go b/internal/player/player.go index 1ee4f6c..b8e6674 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -1,11 +1,24 @@ package player -import "AynaLivePlayer/internal/player/mpv" +import ( + "AynaLivePlayer/internal/player/mpv" + "AynaLivePlayer/internal/player/vlc" + "AynaLivePlayer/pkg/config" +) func SetupMpvPlayer() { - mpv.SetupPlayer() + if config.Experimental.PlayerCore == "vlc" { + vlc.SetupPlayer() + } else { + mpv.SetupPlayer() + } + } func StopMpvPlayer() { - mpv.StopPlayer() + if config.Experimental.PlayerCore == "vlc" { + vlc.StopPlayer() + } else { + mpv.StopPlayer() + } } diff --git a/internal/player/vlc/config.go b/internal/player/vlc/config.go new file mode 100644 index 0000000..84a8713 --- /dev/null +++ b/internal/player/vlc/config.go @@ -0,0 +1,46 @@ +package vlc + +import ( + "AynaLivePlayer/core/events" + "AynaLivePlayer/global" + "AynaLivePlayer/pkg/event" +) + +type playerConfig struct { + Volume float64 + AudioDevice string + DisplayMusicCover bool +} + +func (p *playerConfig) Name() string { + return "Player" +} + +func (p *playerConfig) OnLoad() { + return +} + +func (p *playerConfig) OnSave() { + return +} + +var cfg = &playerConfig{ + Volume: 100, + DisplayMusicCover: true, +} + +func restoreConfig() { + global.EventManager.CallA(events.PlayerVolumeChangeCmd, events.PlayerVolumeChangeCmdEvent{ + Volume: cfg.Volume, + }) + global.EventManager.RegisterA(events.PlayerPropertyVolumeUpdate, "player.config.volume", func(evnt *event.Event) { + data := evnt.Data.(events.PlayerPropertyVolumeUpdateEvent) + if data.Volume < 0 { + return + } + cfg.Volume = data.Volume + }) + global.EventManager.CallA(events.PlayerSetAudioDeviceCmd, events.PlayerSetAudioDeviceCmdEvent{ + Device: cfg.AudioDevice, + }) +} diff --git a/internal/player/vlc/vlc.go b/internal/player/vlc/vlc.go new file mode 100644 index 0000000..0201c0e --- /dev/null +++ b/internal/player/vlc/vlc.go @@ -0,0 +1,443 @@ +package vlc + +import ( + "AynaLivePlayer/core/events" + "AynaLivePlayer/core/model" + "AynaLivePlayer/global" + "AynaLivePlayer/pkg/config" + "AynaLivePlayer/pkg/event" + "AynaLivePlayer/pkg/logger" + "fmt" + "github.com/AynaLivePlayer/miaosic" + "github.com/adrg/libvlc-go/v3" + "math" + "runtime" + "strings" + "sync" +) + +var running bool = false +var log logger.ILogger = nil +var player *vlc.Player +var eventManager *vlc.EventManager +var lock sync.Mutex + +// 状态变量 +var prevPercentPos float64 = 0 +var prevTimePos float64 = 0 +var duration float64 = 0 +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 + os := runtime.GOOS + switch os { + case "windows": + // Windows 平台使用 DirectX + player.SetHWND(uintptr(handle)) + case "darwin": + // macOS 平台使用 NSView + player.SetNSObject(handle) + case "linux": + // Linux 平台使用 XWindow + player.SetXWindow(uint32(handle)) + default: + return fmt.Errorf("unsupported platform: %s", os) + } + + currentWindowHandle = handle + + // 如果当前有媒体在播放,需要重新加载视频输出 + if player.IsPlaying() { + player.Stop() + player.Play() + } + + return nil +} + +func SetupPlayer() { + running = true + 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...) + //} + + // 初始化libvlc + if err := vlc.Init(opts...); err != nil { + log.Error("initialize libvlc failed: ", err) + return + } + + // 创建播放器 + var err error + player, err = vlc.NewPlayer() + if err != nil { + log.Error("create player failed: ", err) + return + } + + // 获取事件管理器 + eventManager, err = player.EventManager() + if err != nil { + log.Error("get event manager failed: ", err) + return + } + + // 注册事件 + registerEvents() + registerCmdHandler() + updateAudioDeviceList() + restoreConfig() + log.Info("VLC player initialized") +} + +func StopPlayer() { + log.Info("stopping VLC player") + if currentAudioDevice != "" { + cfg.AudioDevice = currentAudioDevice + log.Infof("save audio device config: %s", cfg.AudioDevice) + } + running = false + if player != nil { + err := player.Stop() + if err != nil { + log.Error("stop player failed: ", err) + } + err = player.Release() + if err != nil { + log.Error("release player failed: ", err) + } + } + err := vlc.Release() + if err != nil { + log.Error("release player failed: ", err) + } + log.Info("VLC player stopped") +} + +func registerEvents() { + // 播放结束事件 + _, err := eventManager.Attach(vlc.MediaPlayerEndReached, func(e vlc.Event, userData interface{}) { + global.EventManager.CallA(events.PlayerPropertyIdleActiveUpdate, events.PlayerPropertyIdleActiveUpdateEvent{ + IsIdle: true, + }) + global.EventManager.CallA(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{ + Media: model.Media{}, + Removed: true, + }) + }, nil) + if err != nil { + log.Error("register MediaPlayerEndReached event failed: ", err) + } + + // 播放位置改变事件 + _, err = eventManager.Attach(vlc.MediaPlayerPositionChanged, func(e vlc.Event, userData interface{}) { + pos32, _ := player.MediaPosition() + pos := float64(pos32) + if duration > 0 { + timePos := pos * duration + percentPos := pos * 100 + // 忽略小变化 + if math.Abs(timePos-prevTimePos) < 0.5 && math.Abs(percentPos-prevPercentPos) < 0.5 { + return + } + prevTimePos = timePos + prevPercentPos = percentPos + global.EventManager.CallA(events.PlayerPropertyTimePosUpdate, events.PlayerPropertyTimePosUpdateEvent{ + TimePos: timePos, + }) + global.EventManager.CallA(events.PlayerPropertyPercentPosUpdate, events.PlayerPropertyPercentPosUpdateEvent{ + PercentPos: percentPos, + }) + } + }, nil) + if err != nil { + log.Error("register MediaPlayerPositionChanged event failed: ", err) + } + + // 时间改变事件(获取时长) + _, err = eventManager.Attach(vlc.MediaPlayerTimeChanged, func(e vlc.Event, userData interface{}) { + dur, _ := player.MediaLength() + duration = float64(dur) / 1000.0 // 转换为秒 + global.EventManager.CallA(events.PlayerPropertyDurationUpdate, events.PlayerPropertyDurationUpdateEvent{ + Duration: duration, + }) + }, nil) + if err != nil { + log.Error("register MediaPlayerTimeChanged event failed: ", err) + } + + // 暂停状态改变 + _, err = eventManager.Attach(vlc.MediaPlayerPaused, func(e vlc.Event, userData interface{}) { + log.Debug("VLC player paused") + global.EventManager.CallA(events.PlayerPropertyPauseUpdate, events.PlayerPropertyPauseUpdateEvent{ + Paused: true, + }) + }, nil) + if err != nil { + log.Error("register MediaPlayerPaused event failed: ", err) + } + + _, err = eventManager.Attach(vlc.MediaPlayerPlaying, func(e vlc.Event, userData interface{}) { + log.Debug("VLC player playing") + global.EventManager.CallA(events.PlayerPropertyPauseUpdate, events.PlayerPropertyPauseUpdateEvent{ + Paused: false, + }) + }, nil) + if err != nil { + log.Error("register MediaPlayerPlaying event failed: ", err) + } + + _, err = eventManager.Attach(vlc.MediaPlayerAudioVolume, func(e vlc.Event, userData interface{}) { + volume, _ := player.Volume() + log.Debug("VLC player audio volume: ", volume) + global.EventManager.CallA(events.PlayerPropertyVolumeUpdate, events.PlayerPropertyVolumeUpdateEvent{ + Volume: float64(volume), + }) + }, nil) +} + +func registerCmdHandler() { + global.EventManager.RegisterA(events.PlayerPlayCmd, "player.play", func(evnt *event.Event) { + lock.Lock() + defer lock.Unlock() + + mediaInfo := evnt.Data.(events.PlayerPlayCmdEvent).Media.Info + log.Infof("[VLC Player] Play media %s", mediaInfo.Title) + + mediaUrls, err := miaosic.GetMediaUrl(mediaInfo.Meta, miaosic.QualityAny) + if err != nil || len(mediaUrls) == 0 { + log.Warn("[VLC PlayControl] get media url failed ", mediaInfo.Meta.ID(), err) + global.EventManager.CallA( + events.PlayerPlayErrorUpdate, + events.PlayerPlayErrorUpdateEvent{ + Error: err, + }) + return + } + + // 创建媒体对象 + var media *vlc.Media + 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) + return + } + + // 设置HTTP头 + if val, ok := mediaUrls[0].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 { + 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.EventManager.CallA(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{ + Media: mediaData, + Removed: false, + }) + + // 播放 + if err := player.SetMedia(media); err != nil { + log.Error("set media failed: ", err) + return + } + + if currentWindowHandle != 0 { + if err := setWindowHandle(currentWindowHandle); err != nil { + log.Error("apply window handle failed: ", err) + } + } + + if err := player.Play(); err != nil { + log.Error("play failed: ", err) + return + } + + // 重置位置信息 + prevPercentPos = 0 + prevTimePos = 0 + global.EventManager.CallA(events.PlayerPropertyTimePosUpdate, events.PlayerPropertyTimePosUpdateEvent{ + TimePos: 0, + }) + global.EventManager.CallA(events.PlayerPropertyPercentPosUpdate, events.PlayerPropertyPercentPosUpdateEvent{ + PercentPos: 0, + }) + global.EventManager.CallA(events.PlayerPropertyIdleActiveUpdate, events.PlayerPropertyIdleActiveUpdateEvent{ + IsIdle: false, + }) + }) + + global.EventManager.RegisterA(events.PlayerToggleCmd, "player.toggle", func(evnt *event.Event) { + lock.Lock() + defer lock.Unlock() + err := player.TogglePause() + if err != nil { + log.Errorf("[VLC Player] Toggle pause failed: %v", err) + return + } + }) + + global.EventManager.RegisterA(events.PlayerSetPauseCmd, "player.set_paused", func(evnt *event.Event) { + lock.Lock() + defer lock.Unlock() + data := evnt.Data.(events.PlayerSetPauseCmdEvent) + err := player.SetPause(data.Pause) + if err != nil { + log.Errorf("[VLC Player] SetPause failed: %v", err) + return + } + }) + + global.EventManager.RegisterA(events.PlayerSeekCmd, "player.seek", func(evnt *event.Event) { + lock.Lock() + defer lock.Unlock() + data := evnt.Data.(events.PlayerSeekCmdEvent) + if data.Absolute { + player.SetMediaTime(int(data.Position * 1000)) // 转换为毫秒 + } else { + player.SetMediaPosition(float32(data.Position / 100)) + } + }) + + global.EventManager.RegisterA(events.PlayerVolumeChangeCmd, "player.volume", func(evnt *event.Event) { + lock.Lock() + defer lock.Unlock() + data := evnt.Data.(events.PlayerVolumeChangeCmdEvent) + err := player.SetVolume(int(data.Volume)) + if err != nil { + log.Errorf("[VLC Player] SetVolume failed: %v", err) + } + }) + + global.EventManager.RegisterA(events.PlayerVideoPlayerSetWindowHandleCmd, "player.set_window_handle", func(evnt *event.Event) { + handle := evnt.Data.(events.PlayerVideoPlayerSetWindowHandleCmdEvent).Handle + setWindowHandle(handle) + }) + + global.EventManager.RegisterA(events.PlayerSetAudioDeviceCmd, "player.set_audio_device", func(evnt *event.Event) { + device := evnt.Data.(events.PlayerSetAudioDeviceCmdEvent).Device + if err := setAudioDevice(device); err != nil { + log.Warn("set audio device failed", err) + global.EventManager.CallA( + events.ErrorUpdate, + events.ErrorUpdateEvent{ + Error: err, + }) + } + }) +} + +// setAudioDevice 设置音频输出设备 +func setAudioDevice(deviceID string) error { + lock.Lock() + defer lock.Unlock() + + log.Infof("set audio device to: %s", deviceID) + + // 验证设备是否在列表中 + found := false + for _, dev := range audioDevices { + if dev.Name == deviceID { + found = true + break + } + } + + if !found { + return fmt.Errorf("audio device not found: %s", deviceID) + } + + // 设置音频设备 + if err := player.SetAudioOutputDevice(deviceID, ""); err != nil { + log.Error("set audio device failed: ", err) + return err + } + + currentAudioDevice = deviceID + + // 更新配置 + cfg.AudioDevice = deviceID + + // 发送更新事件 + global.EventManager.CallA(events.PlayerAudioDeviceUpdate, events.PlayerAudioDeviceUpdateEvent{ + Current: currentAudioDevice, + Devices: audioDevices, + }) + + return nil +} + +// updateAudioDeviceList 获取并更新音频设备列表 +func updateAudioDeviceList() { + lock.Lock() + defer lock.Unlock() + + // 获取所有音频设备 + devices, err := player.AudioOutputDevices() + if err != nil { + log.Error("get audio device list failed: ", err) + return + } + + // 获取当前音频设备 + currentDevice, err := player.AudioOutputDevice() + if err != nil { + log.Warn("get current audio device failed: ", err) + currentDevice = "" + } + log.Debugf("current audio device list: %s", devices) + log.Debugf("current audio device: %s", currentDevice) + + // 转换设备格式 + audioDevices = make([]model.AudioDevice, 0, len(devices)) + for _, device := range devices { + audioDevices = append(audioDevices, model.AudioDevice{ + Name: device.Name, + Description: device.Description, + }) + } + + currentAudioDevice = currentDevice + + log.Infof("update audio device list: %d devices, current: %s", + len(audioDevices), currentAudioDevice) + + // 发送事件通知 + global.EventManager.CallA(events.PlayerAudioDeviceUpdate, events.PlayerAudioDeviceUpdateEvent{ + Current: currentAudioDevice, + Devices: audioDevices, + }) +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 26bc51b..a62126d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -10,7 +10,7 @@ import ( const ( ProgramName = "卡西米尔唱片机" - Version uint32 = 0x010103 + Version uint32 = 0x010200 ) const ( diff --git a/pkg/config/config_experimental.go b/pkg/config/config_experimental.go index c6e1906..6bc635e 100644 --- a/pkg/config/config_experimental.go +++ b/pkg/config/config_experimental.go @@ -2,7 +2,8 @@ package config type _ExperimentalConfig struct { BaseConfig - Headless bool + Headless bool + PlayerCore string } func (c *_ExperimentalConfig) Name() string { @@ -10,5 +11,6 @@ func (c *_ExperimentalConfig) Name() string { } var Experimental = &_ExperimentalConfig{ - Headless: false, + Headless: false, + PlayerCore: "mpv", } diff --git a/pkg/config/config_general.go b/pkg/config/config_general.go index 1ecc5da..fcb2e49 100644 --- a/pkg/config/config_general.go +++ b/pkg/config/config_general.go @@ -11,7 +11,8 @@ type _GeneralConfig struct { PlayNextOnFail bool UseSystemPlaylist bool FixedSize bool - EnableSMC bool // enable system media control + EnableSMC bool // enable system media control + CustomFonts string // use custom fonts, under ./assets file } @@ -30,4 +31,5 @@ var General = &_GeneralConfig{ UseSystemPlaylist: true, FixedSize: true, EnableSMC: true, + CustomFonts: "", } diff --git a/pkg/miaosic b/pkg/miaosic index 6a9b5d6..5f59bad 160000 --- a/pkg/miaosic +++ b/pkg/miaosic @@ -1 +1 @@ -Subproject commit 6a9b5d60c3f93ceff74b8f98c7aa23651822a726 +Subproject commit 5f59badfe1a151dfd2748a57cb73b1f7e0b03ffd diff --git a/plugin/diange/diange.go b/plugin/diange/diange.go index a09c414..0cf05a9 100644 --- a/plugin/diange/diange.go +++ b/plugin/diange/diange.go @@ -135,6 +135,7 @@ func (d *Diange) Enable() error { "plugin.diange.queue.update", func(event *event.Event) { d.currentQueueLength = len(event.Data.(events.PlaylistDetailUpdateEvent).Medias) + d.log.Debugf("current queue length: %d", d.currentQueueLength) medias := event.Data.(events.PlaylistDetailUpdateEvent).Medias tmpUserCount := make(map[string]int) for _, media := range medias { @@ -144,8 +145,11 @@ func (d *Diange) Enable() error { } tmpUserCount[media.ToUser().Name]++ } + // clear user count + d.userCount.Clear() for user, count := range tmpUserCount { d.userCount.Store(user, count) + d.log.Debugf("user media count in player playlist %s: %d", user, count) } }) global.EventManager.RegisterA( diff --git a/todo.txt b/todo.txt index e0d39e3..6ea4fb3 100644 --- a/todo.txt +++ b/todo.txt @@ -1,21 +1,21 @@ - long text wrap in list - went idle and insert new item race condition -- @3 fix handler execution (maybe priority) - @4 list refresh - @5 delete optimization - race condition in RichText. i dont care fu - 适配歌词服务器 - 媒体源 - 歌单信息获取 -- mpris, SMTC -- 歌词event发送全部歌词,前端处理不同版本 +- mpris - optimize local music -- 从搜索里添加的歌不能被切 -- 随机歌单 +- 下一首为非版权歌时自动切歌不生效 +- 网易云电台节目 +- 自定义弹幕服务器 ---- Finished +- 2024.06.30 : 添加vlc核心,修复若干bug,gui框架更新,修复点歌限制为1时可能出现的无法点歌的问题 - 2024.05.27 : 修复web弹幕获取到0个host的时候闪退的问题 - 2024.03.27 : 添加b站合集新链接的格式 - 2024.03.22 : 添加酷狗歌单新链接的格式