mirror of
https://github.com/AynaLivePlayer/AynaLivePlayer.git
synced 2025-12-11 20:58:13 +08:00
finish config_basic.go
This commit is contained in:
@@ -4,6 +4,30 @@
|
|||||||
"zh-CN"
|
"zh-CN"
|
||||||
],
|
],
|
||||||
"Messages": {
|
"Messages": {
|
||||||
|
"gui.config.basic.audio_device": {
|
||||||
|
"en": "Audio Device",
|
||||||
|
"zh-CN": "音频输出设备"
|
||||||
|
},
|
||||||
|
"gui.config.basic.description": {
|
||||||
|
"en": "Basic Configuration",
|
||||||
|
"zh-CN": "基础设置"
|
||||||
|
},
|
||||||
|
"gui.config.basic.random_playlist": {
|
||||||
|
"en": "Playlist Random",
|
||||||
|
"zh-CN": "播放列表随机设置(打勾表示随机播放)"
|
||||||
|
},
|
||||||
|
"gui.config.basic.random_playlist.system": {
|
||||||
|
"en": "System Playlist",
|
||||||
|
"zh-CN": "闲置歌单"
|
||||||
|
},
|
||||||
|
"gui.config.basic.random_playlist.user": {
|
||||||
|
"en": "User Playlist",
|
||||||
|
"zh-CN": "用户歌单"
|
||||||
|
},
|
||||||
|
"gui.config.basic.title": {
|
||||||
|
"en": "Basic",
|
||||||
|
"zh-CN": "基础设置"
|
||||||
|
},
|
||||||
"gui.player.button.lrc": {
|
"gui.player.button.lrc": {
|
||||||
"en": "lrc",
|
"en": "lrc",
|
||||||
"zh-CN": "歌词"
|
"zh-CN": "歌词"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
)
|
)
|
||||||
|
|
||||||
const VERSION = "alpha 0.6.5"
|
const VERSION = "alpha 0.6.7"
|
||||||
|
|
||||||
const CONFIG_PATH = "./config.ini"
|
const CONFIG_PATH = "./config.ini"
|
||||||
const Assests_PATH = "./assets"
|
const Assests_PATH = "./assets"
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ type _PlayerConfig struct {
|
|||||||
PlaylistsProvider []string
|
PlaylistsProvider []string
|
||||||
PlaylistIndex int
|
PlaylistIndex int
|
||||||
PlaylistRandom bool
|
PlaylistRandom bool
|
||||||
|
AudioDevice string
|
||||||
|
Volume float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *_PlayerConfig) Name() string {
|
func (c *_PlayerConfig) Name() string {
|
||||||
@@ -16,4 +18,6 @@ var Player = &_PlayerConfig{
|
|||||||
PlaylistsProvider: []string{"netease", "netease", "netease"},
|
PlaylistsProvider: []string{"netease", "netease", "netease"},
|
||||||
PlaylistIndex: 0,
|
PlaylistIndex: 0,
|
||||||
PlaylistRandom: true,
|
PlaylistRandom: true,
|
||||||
|
AudioDevice: "auto",
|
||||||
|
Volume: 100,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,99 +19,6 @@ func l() *logrus.Entry {
|
|||||||
return logger.Logger.WithField("Module", MODULE_CONTROLLER)
|
return logger.Logger.WithField("Module", MODULE_CONTROLLER)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PlayNext() {
|
|
||||||
l().Info("try to play next possible media")
|
|
||||||
if UserPlaylist.Size() == 0 && SystemPlaylist.Size() == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var media *player.Media
|
|
||||||
if UserPlaylist.Size() != 0 {
|
|
||||||
media = UserPlaylist.Pop()
|
|
||||||
} else if SystemPlaylist.Size() != 0 {
|
|
||||||
media = SystemPlaylist.Next()
|
|
||||||
}
|
|
||||||
Play(media)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Play(media *player.Media) {
|
|
||||||
l().Info("prepare media")
|
|
||||||
err := PrepareMedia(media)
|
|
||||||
if err != nil {
|
|
||||||
l().Warn("prepare media failed. try play next")
|
|
||||||
PlayNext()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
CurrentMedia = media
|
|
||||||
if err := MainPlayer.Play(media); err != nil {
|
|
||||||
l().Warn("play failed", err)
|
|
||||||
}
|
|
||||||
CurrentLyric.Reload(media.Lyric)
|
|
||||||
// reset
|
|
||||||
media.Url = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func Add(keyword string, user interface{}) {
|
|
||||||
medias, err := Search(keyword)
|
|
||||||
if err != nil {
|
|
||||||
l().Warnf("search for %s, got error %s", keyword, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(medias) == 0 {
|
|
||||||
l().Info("search for %s, got no result", keyword)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
media := medias[0]
|
|
||||||
media.User = user
|
|
||||||
l().Infof("add media %s (%s)", media.Title, media.Artist)
|
|
||||||
UserPlaylist.Insert(-1, media)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddWithProvider(keyword string, pname string, user interface{}) {
|
|
||||||
medias, err := provider.Search(pname, keyword)
|
|
||||||
if err != nil {
|
|
||||||
l().Warnf("search for %s, got error %s", keyword, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(medias) == 0 {
|
|
||||||
l().Info("search for %s, got no result", keyword)
|
|
||||||
}
|
|
||||||
media := medias[0]
|
|
||||||
media.User = user
|
|
||||||
l().Info("add media %s (%s)", media.Title, media.Artist)
|
|
||||||
UserPlaylist.Insert(-1, media)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Seek(position float64, absolute bool) {
|
|
||||||
if err := MainPlayer.Seek(position, absolute); err != nil {
|
|
||||||
l().Warnf("seek to position %f (%t) failed, %s", position, absolute, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Toggle() (b bool) {
|
|
||||||
var err error
|
|
||||||
if MainPlayer.IsPaused() {
|
|
||||||
err = MainPlayer.Unpause()
|
|
||||||
b = false
|
|
||||||
} else {
|
|
||||||
err = MainPlayer.Pause()
|
|
||||||
b = true
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
l().Warn("toggle failed", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetVolume(volume float64) {
|
|
||||||
if MainPlayer.SetVolume(volume) != nil {
|
|
||||||
l().Warnf("set mpv volume to %f failed", volume)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Destroy() {
|
|
||||||
MainPlayer.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetDanmuClient(roomId string) {
|
func SetDanmuClient(roomId string) {
|
||||||
ResetDanmuClient()
|
ResetDanmuClient()
|
||||||
l().Infof("setting live client for %s", roomId)
|
l().Infof("setting live client for %s", roomId)
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ var CurrentMedia *player.Media
|
|||||||
func Initialize() {
|
func Initialize() {
|
||||||
|
|
||||||
MainPlayer = player.NewPlayer()
|
MainPlayer = player.NewPlayer()
|
||||||
|
SetAudioDevice(config.Player.AudioDevice)
|
||||||
|
SetVolume(config.Player.Volume)
|
||||||
UserPlaylist = player.NewPlaylist("user", player.PlaylistConfig{RandomNext: false})
|
UserPlaylist = player.NewPlaylist("user", player.PlaylistConfig{RandomNext: false})
|
||||||
SystemPlaylist = player.NewPlaylist("system", player.PlaylistConfig{RandomNext: config.Player.PlaylistRandom})
|
SystemPlaylist = player.NewPlaylist("system", player.PlaylistConfig{RandomNext: config.Player.PlaylistRandom})
|
||||||
PlaylistManager = make([]*player.Playlist, 0)
|
PlaylistManager = make([]*player.Playlist, 0)
|
||||||
@@ -29,6 +31,7 @@ func Initialize() {
|
|||||||
UserPlaylist.Handler.RegisterA(player.EventPlaylistInsert, "controller.playnextwhenadd", handlePlaylistAdd)
|
UserPlaylist.Handler.RegisterA(player.EventPlaylistInsert, "controller.playnextwhenadd", handlePlaylistAdd)
|
||||||
MainPlayer.ObserveProperty("time-pos", handleLyricUpdate)
|
MainPlayer.ObserveProperty("time-pos", handleLyricUpdate)
|
||||||
MainPlayer.Start()
|
MainPlayer.Start()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadPlaylists() {
|
func loadPlaylists() {
|
||||||
|
|||||||
121
controller/player.go
Normal file
121
controller/player.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"AynaLivePlayer/config"
|
||||||
|
"AynaLivePlayer/player"
|
||||||
|
"AynaLivePlayer/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PlayNext() {
|
||||||
|
l().Info("try to play next possible media")
|
||||||
|
if UserPlaylist.Size() == 0 && SystemPlaylist.Size() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var media *player.Media
|
||||||
|
if UserPlaylist.Size() != 0 {
|
||||||
|
media = UserPlaylist.Pop()
|
||||||
|
} else if SystemPlaylist.Size() != 0 {
|
||||||
|
media = SystemPlaylist.Next()
|
||||||
|
}
|
||||||
|
Play(media)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Play(media *player.Media) {
|
||||||
|
l().Info("prepare media")
|
||||||
|
err := PrepareMedia(media)
|
||||||
|
if err != nil {
|
||||||
|
l().Warn("prepare media failed. try play next")
|
||||||
|
PlayNext()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
CurrentMedia = media
|
||||||
|
if err := MainPlayer.Play(media); err != nil {
|
||||||
|
l().Warn("play failed", err)
|
||||||
|
}
|
||||||
|
CurrentLyric.Reload(media.Lyric)
|
||||||
|
// reset
|
||||||
|
media.Url = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func Add(keyword string, user interface{}) {
|
||||||
|
medias, err := Search(keyword)
|
||||||
|
if err != nil {
|
||||||
|
l().Warnf("search for %s, got error %s", keyword, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(medias) == 0 {
|
||||||
|
l().Info("search for %s, got no result", keyword)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
media := medias[0]
|
||||||
|
media.User = user
|
||||||
|
l().Infof("add media %s (%s)", media.Title, media.Artist)
|
||||||
|
UserPlaylist.Insert(-1, media)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddWithProvider(keyword string, pname string, user interface{}) {
|
||||||
|
medias, err := provider.Search(pname, keyword)
|
||||||
|
if err != nil {
|
||||||
|
l().Warnf("search for %s, got error %s", keyword, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(medias) == 0 {
|
||||||
|
l().Info("search for %s, got no result", keyword)
|
||||||
|
}
|
||||||
|
media := medias[0]
|
||||||
|
media.User = user
|
||||||
|
l().Info("add media %s (%s)", media.Title, media.Artist)
|
||||||
|
UserPlaylist.Insert(-1, media)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Seek(position float64, absolute bool) {
|
||||||
|
if err := MainPlayer.Seek(position, absolute); err != nil {
|
||||||
|
l().Warnf("seek to position %f (%t) failed, %s", position, absolute, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Toggle() (b bool) {
|
||||||
|
var err error
|
||||||
|
if MainPlayer.IsPaused() {
|
||||||
|
err = MainPlayer.Unpause()
|
||||||
|
b = false
|
||||||
|
} else {
|
||||||
|
err = MainPlayer.Pause()
|
||||||
|
b = true
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
l().Warn("toggle failed", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetVolume(volume float64) {
|
||||||
|
if MainPlayer.SetVolume(volume) != nil {
|
||||||
|
l().Warnf("set mpv volume to %f failed", volume)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.Player.Volume = volume
|
||||||
|
}
|
||||||
|
|
||||||
|
func Destroy() {
|
||||||
|
MainPlayer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAudioDevices() []player.AudioDevice {
|
||||||
|
dl, err := MainPlayer.GetAudioDeviceList()
|
||||||
|
if err != nil {
|
||||||
|
return make([]player.AudioDevice, 0)
|
||||||
|
}
|
||||||
|
return dl
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetAudioDevice(device string) {
|
||||||
|
l().Infof("set audio device to %s", device)
|
||||||
|
if err := MainPlayer.SetAudioDevice(device); err != nil {
|
||||||
|
l().Warnf("set mpv audio device to %s failed, %s", device, err)
|
||||||
|
MainPlayer.SetAudioDevice("auto")
|
||||||
|
config.Player.AudioDevice = "auto"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.Player.AudioDevice = device
|
||||||
|
}
|
||||||
@@ -1,20 +1,54 @@
|
|||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"AynaLivePlayer/config"
|
||||||
|
"AynaLivePlayer/controller"
|
||||||
|
"AynaLivePlayer/i18n"
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/data/binding"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
)
|
)
|
||||||
|
|
||||||
type bascicConfig struct{}
|
type bascicConfig struct {
|
||||||
|
panel fyne.CanvasObject
|
||||||
func (b bascicConfig) Title() string {
|
|
||||||
return "Basic"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b bascicConfig) Description() string {
|
func (b *bascicConfig) Title() string {
|
||||||
return "Basic configuration"
|
return i18n.T("gui.config.basic.title")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b bascicConfig) Create() fyne.CanvasObject {
|
func (b *bascicConfig) Description() string {
|
||||||
//TODO implement me
|
return i18n.T("gui.config.basic.description")
|
||||||
panic("implement me")
|
}
|
||||||
|
|
||||||
|
func (b *bascicConfig) CreatePanel() fyne.CanvasObject {
|
||||||
|
if b.panel != nil {
|
||||||
|
return b.panel
|
||||||
|
}
|
||||||
|
randomPlaylist := container.NewHBox(
|
||||||
|
widget.NewLabel(i18n.T("gui.config.basic.random_playlist")),
|
||||||
|
widget.NewCheckWithData(
|
||||||
|
i18n.T("gui.config.basic.random_playlist.user"),
|
||||||
|
binding.BindBool(&controller.UserPlaylist.Config.RandomNext)),
|
||||||
|
widget.NewCheckWithData(
|
||||||
|
i18n.T("gui.config.basic.random_playlist.system"),
|
||||||
|
binding.BindBool(&controller.SystemPlaylist.Config.RandomNext)),
|
||||||
|
)
|
||||||
|
devices := controller.GetAudioDevices()
|
||||||
|
deviceDesc := make([]string, len(devices))
|
||||||
|
deviceDesc2Name := make(map[string]string)
|
||||||
|
for i, device := range devices {
|
||||||
|
deviceDesc[i] = device.Description
|
||||||
|
deviceDesc2Name[device.Description] = device.Name
|
||||||
|
}
|
||||||
|
deviceSel := widget.NewSelect(deviceDesc, func(s string) {
|
||||||
|
controller.SetAudioDevice(deviceDesc2Name[s])
|
||||||
|
})
|
||||||
|
deviceSel.Selected = config.Player.AudioDevice
|
||||||
|
outputDevice := container.NewBorder(nil, nil,
|
||||||
|
widget.NewLabel(i18n.T("gui.config.basic.audio_device")), nil,
|
||||||
|
deviceSel)
|
||||||
|
b.panel = container.NewVBox(randomPlaylist, outputDevice)
|
||||||
|
return b.panel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type ConfigLayout interface {
|
|||||||
|
|
||||||
var App fyne.App
|
var App fyne.App
|
||||||
var MainWindow fyne.Window
|
var MainWindow fyne.Window
|
||||||
var ConfigList = []ConfigLayout{}
|
var ConfigList = []ConfigLayout{&bascicConfig{}}
|
||||||
|
|
||||||
func l() *logrus.Entry {
|
func l() *logrus.Entry {
|
||||||
return logger.Logger.WithField("Module", MODULE_GUI)
|
return logger.Logger.WithField("Module", MODULE_GUI)
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ func registerPlayControllerHandler() {
|
|||||||
l().Error("fail to register handler for progress bar with property idle-active")
|
l().Error("fail to register handler for progress bar with property idle-active")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PlayController.Progress.Max = 0
|
||||||
PlayController.Progress.OnChanged = func(f float64) {
|
PlayController.Progress.OnChanged = func(f float64) {
|
||||||
controller.Seek(f/10, false)
|
controller.Seek(f/10, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func (b *playlistOperationButton) Tapped(e *fyne.PointEvent) {
|
|||||||
func newPlaylistOperationButton() *playlistOperationButton {
|
func newPlaylistOperationButton() *playlistOperationButton {
|
||||||
b := &playlistOperationButton{Index: 0}
|
b := &playlistOperationButton{Index: 0}
|
||||||
deleteItem := fyne.NewMenuItem(i18n.T("gui.player.playlist.op.delete"), func() {
|
deleteItem := fyne.NewMenuItem(i18n.T("gui.player.playlist.op.delete"), func() {
|
||||||
fmt.Println("delete", b.Index)
|
controller.UserPlaylist.Delete(b.Index)
|
||||||
})
|
})
|
||||||
topItem := fyne.NewMenuItem(i18n.T("gui.player.playlist.op.top"), func() {
|
topItem := fyne.NewMenuItem(i18n.T("gui.player.playlist.op.top"), func() {
|
||||||
controller.UserPlaylist.Move(b.Index, 0)
|
controller.UserPlaylist.Move(b.Index, 0)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"AynaLivePlayer/util"
|
"AynaLivePlayer/util"
|
||||||
"github.com/aynakeya/go-mpv"
|
"github.com/aynakeya/go-mpv"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
const MODULE_PLAYER = "Player.Player"
|
const MODULE_PLAYER = "Player.Player"
|
||||||
@@ -154,3 +155,32 @@ func (p *Player) ObserveProperty(property string, handler ...PropertyHandlerFunc
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AudioDevice struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAudioDeviceList get output device for mpv
|
||||||
|
// return format is []AudioDevice
|
||||||
|
func (p *Player) GetAudioDeviceList() ([]AudioDevice, error) {
|
||||||
|
p.l().Trace("getting audio device list for mpv")
|
||||||
|
property, err := p.libmpv.GetProperty("audio-device-list", mpv.FORMAT_STRING)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dl := make([]AudioDevice, 0)
|
||||||
|
gjson.Parse(property.(string)).ForEach(func(key, value gjson.Result) bool {
|
||||||
|
dl = append(dl, AudioDevice{
|
||||||
|
Name: value.Get("name").String(),
|
||||||
|
Description: value.Get("description").String(),
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return dl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) SetAudioDevice(device string) error {
|
||||||
|
p.l().Tracef("set audio device %s for mpv", device)
|
||||||
|
return p.libmpv.SetPropertyString("audio-device", device)
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,7 +58,14 @@ func (p *Playlist) Pop() *Media {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
p.Lock.Lock()
|
p.Lock.Lock()
|
||||||
media := p.Playlist[0]
|
index := 0
|
||||||
|
if p.Config.RandomNext {
|
||||||
|
index = rand.Intn(p.Size())
|
||||||
|
}
|
||||||
|
media := p.Playlist[index]
|
||||||
|
for i := index; i > 0; i-- {
|
||||||
|
p.Playlist[i] = p.Playlist[i-1]
|
||||||
|
}
|
||||||
p.Playlist = p.Playlist[1:]
|
p.Playlist = p.Playlist[1:]
|
||||||
p.Lock.Unlock()
|
p.Lock.Unlock()
|
||||||
defer p.Handler.CallA(EventPlaylistUpdate, PlaylistUpdateEvent{Playlist: p})
|
defer p.Handler.CallA(EventPlaylistUpdate, PlaylistUpdateEvent{Playlist: p})
|
||||||
|
|||||||
4
todo.txt
4
todo.txt
@@ -5,15 +5,17 @@
|
|||||||
- @5 delete optimization
|
- @5 delete optimization
|
||||||
|
|
||||||
- 歌词来源
|
- 歌词来源
|
||||||
|
|
||||||
- 文本输出
|
- 文本输出
|
||||||
- web输出
|
- web输出
|
||||||
|
- 历史记录
|
||||||
|
- 黑名单
|
||||||
- 进入beta版本
|
- 进入beta版本
|
||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
Finished
|
Finished
|
||||||
|
- 2022.6.26: i18n
|
||||||
- 2022.6.25: kuwo歌单
|
- 2022.6.25: kuwo歌单
|
||||||
- 2022.6.25: 设置界面
|
- 2022.6.25: 设置界面
|
||||||
- 2022.6.25: @6 bug, race condition, playlist size changed during playlist update.
|
- 2022.6.25: @6 bug, race condition, playlist size changed during playlist update.
|
||||||
|
|||||||
Reference in New Issue
Block a user