44 Commits

Author SHA1 Message Date
Aynakeya
1e18ca1ff2 ui界面优化,event handler优化-新增任务池模式,歌词加载优化,新房间管理(可以自动连接) 本地音频搜索算法优化, 2022-12-25 01:27:56 -08:00
Aynakeya
9ec4057412 重写controller部分,修改search界面,添加歌词滚动效果,部分资源添加到bundle,修复拖动进度条时产生的噪音 2022-12-24 03:51:21 -08:00
Aynakeya
c47d338a9e rewrite using IoC and DI 2022-12-23 05:06:57 -08:00
Aynakeya
0498d2dbf3 new room gui 2022-11-28 18:39:12 -08:00
Aynakeya
eac8b7b775 local search algorithm optimization/diange medal permission/ 2022-08-24 15:13:13 -07:00
Aynakeya
d20c39ace3 fix config not saving 2022-08-20 10:10:12 -07:00
Aynakeya
a8d5e9d772 0.9.2 2022-08-19 13:32:42 -07:00
Aynakeya
b6645cc575 update dependency. 2022-08-19 13:16:40 -07:00
Aynakeya
4c0b407475 fix playlist gui not refresh when switching 2022-07-21 23:30:28 -07:00
Aynakeya
c0c83ef82a fix local playlist bug/to 0.9.1 2022-07-21 23:20:19 -07:00
Aynakeya
f4b080da25 fix local playlist bug 2022-07-21 23:07:41 -07:00
Aynakeya
e305e0da6e fix logger 2022-07-19 20:34:11 -07:00
Aynakeya
666ca2a16e update webinfo frontend 2022-07-19 20:27:55 -07:00
Aynakeya
bb24c54d46 update webinfo frontend 2022-07-19 20:25:58 -07:00
Aynakeya
5ea45858f7 update webinfo frontend 2022-07-19 20:24:58 -07:00
Aynakeya
90e5c8a9bf update webinfo frontend 2022-07-19 20:19:57 -07:00
Aynakeya
db2030b3c0 update webinfo frontend 2022-07-19 19:57:25 -07:00
Aynakeya
05c44079e2 fix bug/webinfo-add custom template 2022-07-19 18:09:44 -07:00
Aynakeya
f6b58a3e9d fix bug 2022-07-18 23:10:40 -07:00
Aynakeya
a385b341f7 fix bug/web template init 2022-07-18 22:58:52 -07:00
Aynakeya
f69327d53d Merge pull request #1 from aynakeya/add-license-1
Create LICENSE.md
2022-07-18 20:52:42 -07:00
Aynakeya
313e48cf21 Create LICENSE.md 2022-07-18 20:52:19 -07:00
Aynakeya
2b7151e1d2 web info first version/fix some lyric not update/update translation.json/fix some media in history list cann't be played again 2022-07-18 20:37:00 -07:00
Aynakeya
24457e0acd add stderr redirection, add logo 2022-07-15 16:30:50 -07:00
Aynakeya
8f2b975455 change puase icon 2022-07-14 01:04:09 -07:00
Aynakeya
9e7b062790 update webinfo control panel 2022-07-14 00:51:26 -07:00
Aynakeya
8602e6470a update todo 2022-07-13 19:20:25 -07:00
Aynakeya
41e2a4775a add netease login plugin 2022-07-13 18:59:04 -07:00
Aynakeya
6f5cfc9028 update frontend requirement 2022-07-13 15:35:14 -07:00
Aynakeya
f47cc14151 fix typo 2022-07-12 23:12:44 -07:00
Aynakeya
78cf40ff26 update frontend requirement 2022-07-12 23:09:58 -07:00
Aynakeya
d90311acfe add frontend requirement 2022-07-12 21:48:06 -07:00
Aynakeya
94615265bf add web output backend 2022-07-12 21:06:55 -07:00
Aynakeya
6e4c78daf2 add empty image output 2022-07-07 20:37:06 -07:00
Aynakeya
632f531cdb update to 0.8.0 2022-07-07 20:30:35 -07:00
Aynakeya
7bf9372898 add local provider, add cover output in textinfo 2022-07-07 20:30:12 -07:00
Aynakeya
fd91b1e130 add search for bilivideo 2022-07-07 00:44:10 -07:00
Aynakeya
3431b5cafe experiment ui 2022-07-05 09:26:19 -07:00
Aynakeya
3907ff96d5 experiment ui 2022-07-03 20:52:44 -07:00
Aynakeya
dfce89f96e add diange source command & fix bilibili video 2022-07-03 13:54:01 -07:00
Aynakeya
dc3ab46ad0 fix gui text error 2022-07-02 22:03:20 -07:00
Aynakeya
6bdb0acf93 fix bilibili video source header & fix player header setting 2022-07-02 22:01:11 -07:00
Aynakeya
6e18df9b41 add bilibili video 2022-07-02 10:09:19 -07:00
Aynakeya
e0849f0d65 music id match up 2022-07-02 08:41:31 -07:00
135 changed files with 6074 additions and 2101 deletions

4
.gitignore vendored
View File

@@ -1 +1,3 @@
.idea
.idea
assets/webinfo/*.html
assets/webinfo/assets

21
LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Aynakeya
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

60
Makefile Normal file
View File

@@ -0,0 +1,60 @@
NAME = AynaLivePlayer
ifeq ($(OS),Windows_NT)
RM = del /Q /F
RRM = rmdir /Q /S
else
RM = rm -f
RRM = rm -rf
endif
ifeq ($(OS), Windows_NT)
EXECUTABLE=$(NAME).exe
SCRIPTPATH = .\assets\scripts\windows
else
EXECUTABLE=$(NAME)
SCRIPTPATH = ./assets/scripts/linux
endif
gui: bundle
go build -o $(EXECUTABLE) -ldflags -H=windowsgui ./app/gui/main.go
run: bundle
go run ./app/gui/main.go
clear:
$(RM) config.ini log.txt playlists.txt liverooms.json
bundle:
fyne bundle --name resImageEmpty --package resource ./assets/empty.png > ./resource/bundle.go
fyne bundle --append --name resImageIcon --package resource ./assets/icon.jpg >> ./resource/bundle.go
fyne bundle --append --name resFontMSYaHei --package resource ./assets/msyh.ttc >> ./resource/bundle.go
fyne bundle --append --name resFontMSYaHeiBold --package resource ./assets/msyhbd.ttc >> ./resource/bundle.go
release: gui
-mkdir release
ifeq ($(OS), Windows_NT)
COPY .\$(EXECUTABLE) .\release\$(EXECUTABLE)
COPY .\webtemplates.json .\release\webtemplates.json
COPY .\assets\translation.json .\release\assets\translation.json
COPY LICENSE.md .\release\LICENSE.md
XCOPY .\assets\scripts\windows\* .\release\ /k /i /y /q
XCOPY .\assets\webinfo .\release\assets\webinfo /s /e /i /y /q
XCOPY .\music .\release\music /s /e /i /y /q
XCOPY .\template .\release\template /s /e /i /y /q
else
cp ./$(EXECUTABLE) ./release/$(EXECUTABLE)
cp ./webtemplates.json ./release/webtemplates.json
cp ./assets/translation.json ./release/assets/translation.json
cp LICENSE.md ./release/LICENSE.md
cp ./assets/scripts/linux/* ./release/
cp -r ./assets/webinfo ./release/assest/webinfo
cp -r ./music ./release/music
cp -r ./template ./release/template
endif
clean:
$(RM) $(EXECUTABLE) config.ini log.txt playlists.txt liverooms.json
$(RRM) release
.PHONY: ${EXECUTABLE}

View File

@@ -9,6 +9,10 @@ QQ group: 621035845
## build
```
go build -ldflags -H=windowsgui app/gui/main.go
go build -o AynaLivePlayer.exe -ldflags -H=windowsgui app/gui/main.go
```
## packaging
```
fyne package --src path_to_gui --exe AynaLivePlayer.exe --appVersion 0.8.4 --icon path_to_icon
```

View File

@@ -1,23 +0,0 @@
package main
import (
"AynaLivePlayer/config"
"AynaLivePlayer/controller"
"AynaLivePlayer/logger"
"fmt"
"github.com/sirupsen/logrus"
)
func main() {
fmt.Printf("BiliAudioBot Revive %s\n", config.VERSION)
logger.Logger.SetLevel(logrus.DebugLevel)
fmt.Println("Please enter room id")
var roomid string
// Taking input from user
fmt.Scanln(&roomid)
controller.Initialize()
controller.SetDanmuClient(roomid)
ch := make(chan int)
<-ch
}

View File

@@ -240,6 +240,7 @@ This styled row should also wrap as expected, but only *when required*.
grid := makeTextGrid()
return container.NewBorder(fixed, grid, nil, nil,
container.NewGridWithRows(2, rich, entryLoremIpsum))
}
func makeInputTab(_ fyne.Window) fyne.CanvasObject {

View File

@@ -1,41 +1,51 @@
package main
import (
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/common/logger"
"AynaLivePlayer/config"
"AynaLivePlayer/controller"
"AynaLivePlayer/controller/core"
"AynaLivePlayer/gui"
"AynaLivePlayer/i18n"
"AynaLivePlayer/logger"
"AynaLivePlayer/player"
"AynaLivePlayer/plugin/diange"
"AynaLivePlayer/plugin/qiege"
"AynaLivePlayer/plugin/textinfo"
"fmt"
"github.com/mitchellh/panicwrap"
"github.com/sirupsen/logrus"
"os"
"AynaLivePlayer/plugin/webinfo"
"AynaLivePlayer/plugin/wylogin"
"flag"
)
func init() {
exitStatus, _ := panicwrap.BasicWrap(func(s string) {
logger.Logger.Panic(s)
os.Exit(1)
return
})
if exitStatus >= 0 {
os.Exit(exitStatus)
}
var dev = flag.Bool("dev", false, "generate new translation file")
func createController() controller.IController {
liveroom := core.NewLiveRoomController()
lyric := core.NewLyricLoader()
provider := core.NewProviderController()
playlist := core.NewPlaylistController(provider)
plugin := core.NewPluginController()
mpvPlayer := player.NewMpvPlayer()
playControl := core.NewPlayerController(mpvPlayer, playlist, lyric, provider)
ctr := core.NewController(liveroom, playControl, playlist, provider, plugin)
return ctr
}
func main() {
fmt.Printf("BiliAudioBot Revive %s\n", config.VERSION)
logger.Logger.SetLevel(logrus.DebugLevel)
controller.Initialize()
controller.LoadPlugins(diange.NewDiange(), qiege.NewQiege(), textinfo.NewTextInfo())
defer func() {
controller.Destroy()
config.SaveToConfigFile(config.CONFIG_PATH)
i18n.SaveTranslation()
}()
flag.Parse()
logger.Logger.Info("================Program Start================")
logger.Logger.Infof("================Current Version: %s================", config.Version)
mainController := createController()
controller.Instance = mainController
gui.Initialize()
plugins := []controller.Plugin{diange.NewDiange(mainController), qiege.NewQiege(mainController),
textinfo.NewTextInfo(mainController), webinfo.NewWebInfo(mainController),
wylogin.NewWYLogin()}
mainController.LoadPlugins(plugins...)
gui.MainWindow.ShowAndRun()
mainController.CloseAndSave()
if *dev {
i18n.SaveTranslation()
}
_ = config.SaveToConfigFile(config.ConfigPath)
logger.Logger.Info("================Program End================")
}

27
app/wrapwordbug/main.go Normal file
View File

@@ -0,0 +1,27 @@
package main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func main() {
a := app.New()
w := a.NewWindow("Hello World")
texts := make([]fyne.CanvasObject, 1)
for i := 0; i < len(texts); i++ {
l := widget.NewLabelWithStyle(
" AAAA",
fyne.TextAlignCenter, fyne.TextStyle{})
l.Wrapping = fyne.TextWrapWord
texts[i] = l
}
vbox := container.NewVBox(texts...)
scroll := container.NewScroll(vbox)
w.SetContent(scroll)
w.Resize(fyne.NewSize(360, 540))
w.ShowAndRun()
}

View File

@@ -1,11 +0,0 @@
package main
import (
"AynaLivePlayer/plugin/textinfo"
)
func main() {
x := &textinfo.TextInfo{}
x.Enable()
x.RenderTemplates()
}

BIN
assets/icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
assets/msyhbd.ttc Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
taskkill /IM "AynaLivePlayer.exe" /F

View File

@@ -0,0 +1 @@
netsh winsock reset

View File

@@ -96,6 +96,10 @@
"en": "Please enter the ID or URL of the song you want to add.",
"zh-CN": "输入歌单ID或者歌单网址。"
},
"gui.playlist.add.source": {
"en": "Source",
"zh-CN": "来源"
},
"gui.playlist.add.title": {
"en": "Add Playlist",
"zh-CN": "添加歌单"
@@ -124,6 +128,30 @@
"en": "Current: None",
"zh-CN": "当前为: 无"
},
"gui.room.add.cancel": {
"en": "Cancel",
"zh-CN": "取消"
},
"gui.room.add.client_name": {
"en": "Client Name",
"zh-CN": "直播平台"
},
"gui.room.add.confirm": {
"en": "Confirm",
"zh-CN": "确定"
},
"gui.room.add.id_url": {
"en": "Room ID",
"zh-CN": "房间号"
},
"gui.room.add.prompt": {
"en": "enter room id",
"zh-CN": "填入房间号"
},
"gui.room.add.title": {
"en": "Add Room",
"zh-CN": "添加房间"
},
"gui.room.btn.connect": {
"en": "Connect",
"zh-CN": "连接"
@@ -132,6 +160,18 @@
"en": "Disconnect",
"zh-CN": "断开"
},
"gui.room.button.add": {
"en": "Add",
"zh-CN": "新增"
},
"gui.room.button.remove": {
"en": "Remove",
"zh-CN": "删除"
},
"gui.room.check.autoconnect": {
"en": "Auto Connection",
"zh-CN": "自动连接"
},
"gui.room.id": {
"en": "Room ID: ",
"zh-CN": "房间号: "
@@ -220,6 +260,18 @@
"en": "Basic Diange Configuration",
"zh-CN": "点歌基本设置"
},
"plugin.diange.medal.level": {
"en": "Level",
"zh-CN": "等级"
},
"plugin.diange.medal.name": {
"en": "Name",
"zh-CN": "牌子名"
},
"plugin.diange.medal.perm": {
"en": "Medal Permission",
"zh-CN": "牌子点歌权限"
},
"plugin.diange.permission": {
"en": "Permission",
"zh-CN": "点歌权限"
@@ -232,6 +284,10 @@
"en": "Max Queue",
"zh-CN": "最大点歌数"
},
"plugin.diange.source_cmd": {
"en": "Source Command",
"zh-CN": "来源点歌命令"
},
"plugin.diange.title": {
"en": "Diange",
"zh-CN": "点歌"
@@ -240,6 +296,38 @@
"en": "User",
"zh-CN": "普通用户"
},
"plugin.neteaselogin.current_user": {
"en": "Current User:",
"zh-CN": "当前用户:"
},
"plugin.neteaselogin.current_user.notlogin": {
"en": "Not Login",
"zh-CN": "未登录"
},
"plugin.neteaselogin.description": {
"en": "Netease User Login",
"zh-CN": "网易云登录"
},
"plugin.neteaselogin.logout": {
"en": "Logout",
"zh-CN": "登出"
},
"plugin.neteaselogin.qr.finish": {
"en": "Finish Scan",
"zh-CN": "完成扫描后按我"
},
"plugin.neteaselogin.qr.new": {
"en": "Get a new qr code",
"zh-CN": "获取新二维码"
},
"plugin.neteaselogin.refresh": {
"en": "Refresh",
"zh-CN": "刷新状态"
},
"plugin.neteaselogin.title": {
"en": "Netease Login",
"zh-CN": "网易云登录"
},
"plugin.qiege.admin": {
"en": "Admin",
"zh-CN": "管理员"
@@ -283,6 +371,54 @@
"plugin.textinfo.title": {
"en": "Text Output",
"zh-CN": "文本输出"
},
"plugin.webinfo.autostart": {
"en": "Auto start",
"zh-CN": "自动启用"
},
"plugin.webinfo.description": {
"en": "Web output configuration",
"zh-CN": "web输出设置"
},
"plugin.webinfo.port": {
"en": "Port",
"zh-CN": "服务器端口"
},
"plugin.webinfo.server_control": {
"en": "Control",
"zh-CN": "操作"
},
"plugin.webinfo.server_control.restart": {
"en": "Restart",
"zh-CN": "重启"
},
"plugin.webinfo.server_control.start": {
"en": "Start",
"zh-CN": "启动"
},
"plugin.webinfo.server_control.stop": {
"en": "Stop",
"zh-CN": "停止"
},
"plugin.webinfo.server_preview": {
"en": "Server Preview",
"zh-CN": "效果预览"
},
"plugin.webinfo.server_status": {
"en": "Server Status",
"zh-CN": "服务器状态"
},
"plugin.webinfo.server_status.running": {
"en": "Running",
"zh-CN": "运行中"
},
"plugin.webinfo.server_status.stopped": {
"en": "Stopped",
"zh-CN": "已停止"
},
"plugin.webinfo.title": {
"en": "Web Output",
"zh-CN": "Web输出"
}
}
}

0
assets/webinfo/.gitkeep Normal file
View File

126
common/event/event.go Normal file
View File

@@ -0,0 +1,126 @@
package event
import (
"sync"
)
type EventId string
type Event struct {
Id EventId
Cancelled bool
Data interface{}
}
type HandlerFunc func(event *Event)
type Handler struct {
EventId EventId
Name string
Handler HandlerFunc
}
type Manager struct {
handlers map[EventId]map[string]*Handler
queue chan func()
stopSig chan int
queueSize int
workerSize int
lock sync.RWMutex
}
func NewManger(queueSize int, workerSize int) *Manager {
manager := &Manager{
handlers: make(map[EventId]map[string]*Handler),
queue: make(chan func(), queueSize),
stopSig: make(chan int, workerSize),
queueSize: queueSize,
workerSize: workerSize,
}
for i := 0; i < workerSize; i++ {
go func() {
for {
select {
case <-manager.stopSig:
return
case f := <-manager.queue:
f()
}
}
}()
}
return manager
}
func (h *Manager) NewChildManager() *Manager {
return &Manager{
handlers: make(map[EventId]map[string]*Handler),
queue: h.queue,
stopSig: h.stopSig,
queueSize: h.queueSize,
workerSize: h.workerSize,
}
}
func (h *Manager) Stop() {
for i := 0; i < h.workerSize; i++ {
h.stopSig <- 0
}
}
func (h *Manager) Register(handler *Handler) {
h.lock.Lock()
defer h.lock.Unlock()
m, ok := h.handlers[handler.EventId]
if !ok {
m = make(map[string]*Handler)
h.handlers[handler.EventId] = m
}
m[handler.Name] = handler
}
func (h *Manager) RegisterA(id EventId, name string, handler HandlerFunc) {
h.Register(&Handler{
EventId: id,
Name: name,
Handler: handler,
})
}
func (h *Manager) UnregisterAll() {
h.lock.Lock()
defer h.lock.Unlock()
h.handlers = make(map[EventId]map[string]*Handler)
}
func (h *Manager) Unregister(name string) {
h.lock.Lock()
defer h.lock.Unlock()
for _, m := range h.handlers {
if _, ok := m[name]; ok {
delete(m, name)
}
}
}
func (h *Manager) Call(event *Event) {
h.lock.RLock()
handlers, ok := h.handlers[event.Id]
h.lock.RUnlock()
if !ok {
return
}
for _, eh := range handlers {
handler := eh.Handler
h.queue <- func() {
handler(event)
}
}
}
func (h *Manager) CallA(id EventId, data interface{}) {
h.Call(&Event{
Id: id,
Data: data,
})
}

10
common/event/pool.go Normal file
View File

@@ -0,0 +1,10 @@
package event
var MAX_QUEUE_SIZE = 128
var MAX_WORKER_SIZE = 16
var MainManager *Manager
func init() {
MainManager = NewManger(MAX_QUEUE_SIZE, MAX_WORKER_SIZE)
}

View File

@@ -1,10 +1,10 @@
package i18n
import (
"AynaLivePlayer/common/util"
"AynaLivePlayer/config"
"AynaLivePlayer/util"
"encoding/json"
"io/ioutil"
"os"
)
const FILENAME = "translation.json"
@@ -28,7 +28,7 @@ var CurrentLanguage string
func init() {
TranslationMap = Translation{make([]string, 0), make(map[string]map[string]string)}
file, err := ioutil.ReadFile(config.GetAssetPath(FILENAME))
file, err := os.ReadFile(config.GetAssetPath(FILENAME))
if err == nil {
_ = json.Unmarshal([]byte(file), &TranslationMap)
}
@@ -59,5 +59,5 @@ func T(id string) string {
func SaveTranslation() {
content, _ := util.MarshalIndentUnescape(TranslationMap, "", " ")
_ = ioutil.WriteFile(config.GetAssetPath(FILENAME), []byte(content), 0666)
_ = os.WriteFile(config.GetAssetPath(FILENAME), []byte(content), 0666)
}

View File

@@ -4,6 +4,7 @@ import (
"AynaLivePlayer/config"
nested "github.com/antonfisher/nested-logrus-formatter"
"github.com/sirupsen/logrus"
"github.com/virtuald/go-paniclog"
"io"
"os"
)
@@ -13,15 +14,22 @@ var Logger *logrus.Logger
func init() {
Logger = logrus.New()
Logger.SetLevel(config.Log.Level)
file, err := os.OpenFile(config.Log.Path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err == nil {
Logger.Out = io.MultiWriter(file, os.Stdout)
} else {
Logger.Info("Failed to log to file, using default stdout")
}
Logger.SetFormatter(&nested.Formatter{
FieldsOrder: []string{"Module"},
HideKeys: true,
NoColors: true,
})
_ = os.Truncate(config.Log.Path, 0)
file, err := os.OpenFile(config.Log.Path, os.O_CREATE|os.O_WRONLY, 0666)
if err == nil {
Logger.Out = io.MultiWriter(file, os.Stdout)
} else {
Logger.Info("Failed to log to file, using default stdout")
}
if config.Log.RedirectStderr {
Logger.Info("panic/stderr redirect to log file")
if _, err = paniclog.RedirectStderr(file); err != nil {
Logger.Infof("Failed to redirect stderr to to file: %s", err)
}
}
}

69
common/util/generic.go Normal file
View File

@@ -0,0 +1,69 @@
package util
import (
"golang.org/x/exp/constraints"
)
func Slice[T any](arr []T, from int, to int) []T {
l := len(arr)
to = Min(to, l)
from = Min(from, l)
if to <= 0 {
to = l + to
if to <= 0 {
return []T{}
}
}
if from < 0 {
from = l + from
if from < 0 {
from = 0
}
}
if to <= from {
return []T{}
}
return arr[from:to]
}
func SliceCopy[T any](src []T) []T {
x := make([]T, len(src))
copy(x, src)
return x
}
func SliceContains[T comparable](s []T, e T) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func TernaryOp[T any](cond bool, a T, b T) T {
if cond {
return a
}
return b
}
func Min[T constraints.Ordered](arr ...T) T {
min := arr[0]
for _, a := range arr {
if a < min {
min = a
}
}
return min
}
func Max[T constraints.Ordered](arr ...T) T {
max := arr[0]
for _, a := range arr {
if a > max {
max = a
}
}
return max
}

119
common/util/string.go Normal file
View File

@@ -0,0 +1,119 @@
package util
import (
"fmt"
"strconv"
)
func StrLen(str string) int {
return len([]rune(str))
}
func StringNormalize(str string, min int, max int) string {
fmtStr := fmt.Sprintf("%%-%d.%ds", min, max)
return fmt.Sprintf(fmtStr, str)
}
func Atoi(s string) int {
i, _ := strconv.Atoi(s)
return i
}
func GetOrDefault(s string, def string) string {
if s == "" {
return def
}
return s
}
func LevenshteinDistance(s1 string, s2 string) int {
// support unicode
r1 := []rune(s1)
r2 := []rune(s2)
r1l := len(r1)
r2l := len(r2)
if r1l == 0 || r2l == 0 {
return Max(r1l, r2l)
}
previous := make([]int, r2l+1)
current := make([]int, r2l+1)
for i := 0; i <= r2l; i++ {
previous[i] = i
}
for i := 1; i <= r1l; i++ {
current[0] = i
for j := 1; j <= r2l; j++ {
subCost := 1
if r1[i-1] == r2[j-1] {
subCost = 0
}
// current[j] = min( insertCost,deleteCost, subCost)
current[j] = Min(current[j-1]+1, previous[j]+1, previous[j-1]+subCost)
}
current, previous = previous, current
}
return previous[r2l]
}
func WeightedLevenshteinDistance(s1 string, s2 string, ins, del, repl int) int {
// support unicode
r1 := []rune(s1)
r2 := []rune(s2)
r1l := len(r1)
r2l := len(r2)
if r1l == 0 || r2l == 0 {
return Max(r1l, r2l)
}
previous := make([]int, r2l+1)
current := make([]int, r2l+1)
for i := 0; i <= r2l; i++ {
previous[i] = i
}
for i := 1; i <= r1l; i++ {
current[0] = i
for j := 1; j <= r2l; j++ {
subCost := 1
if r1[i-1] == r2[j-1] {
subCost = 0
}
// current[j] = min( insertCost,deleteCost, subCost)
current[j] = Min(current[j-1]+1*ins, previous[j]+1*del, previous[j-1]+subCost*repl)
}
current, previous = previous, current
}
return previous[r2l]
}
func LongestCommonString(s1 string, s2 string) string {
// support unicode
r1 := []rune(s1)
r2 := []rune(s2)
r1l := len(r1)
r2l := len(r2)
if r1l == 0 || r2l == 0 {
return ""
}
previous := make([]int, r2l+1)
current := make([]int, r2l+1)
max := 0
maxIndex := 0
for i := 1; i <= r1l; i++ {
for j := 1; j <= r2l; j++ {
if r1[i-1] == r2[j-1] {
current[j] = previous[j-1] + 1
if current[j] > max {
max = current[j]
maxIndex = i
}
} else {
current[j] = 0
}
}
current, previous = previous, current
}
return string(r1[maxIndex-max : maxIndex])
}

View File

@@ -0,0 +1,32 @@
package util
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestLevenshteinDistance(t *testing.T) {
assert.Equal(t, 3, LevenshteinDistance("kitten", "sitting"))
assert.Equal(t, 0, LevenshteinDistance("kitten", "kitten"))
assert.Equal(t, 1, LevenshteinDistance("kitten", "kittens"))
assert.Equal(t, 2, LevenshteinDistance("kitten", "kitt"))
assert.Greater(t, LevenshteinDistance("夜曲 周杰伦/方文山", "夜曲 周杰伦"), LevenshteinDistance("夜曲 翻唱A", "夜曲 周杰伦"))
assert.Greater(t,
WeightedLevenshteinDistance("Mojito Tommy Hong", "Mojito 周杰伦", 1, 1, 3),
WeightedLevenshteinDistance("Mojito 周杰伦", "Mojito 周杰伦", 1, 1, 3))
assert.Greater(t,
WeightedLevenshteinDistance("默 (Live) 李荣浩/周杰伦", "Mojito 周杰伦", 1, 1, 3),
WeightedLevenshteinDistance("Mojito 周杰伦", "Mojito 周杰伦", 1, 1, 3))
assert.Greater(t,
WeightedLevenshteinDistance("布拉格广场 周杰伦", "Mojito 周杰伦", 1, 1, 3),
WeightedLevenshteinDistance("Mojito 周杰伦", "Mojito 周杰伦", 1, 1, 3))
assert.Greater(t,
WeightedLevenshteinDistance("Mojito翻自 cover 周杰伦)野猪佩奇", "Mojito 周杰伦", 1, 1, 3),
WeightedLevenshteinDistance("Mojito 周杰伦", "Mojito 周杰伦", 1, 1, 3))
//assert.Less(t, WeightedLevenshteinDistance("夜曲 周杰伦/方文山", "夜曲 周杰伦",1,1,3), WeightedLevenshteinDistance("夜曲 翻唱A", "夜曲 周杰伦",1,1,3))
}
func TestLongestCommonString(t *testing.T) {
assert.Equal(t, "itt", LongestCommonString("kitten", "sitting"))
assert.Equal(t, "布拉格广场", LongestCommonString("布拉格广场 周杰伦", "布拉格广场"))
}

8
common/util/url.go Normal file
View File

@@ -0,0 +1,8 @@
package util
import "net/url"
func UrlMustParse(rawurl string) *url.URL {
u, _ := url.Parse(rawurl)
return u
}

View File

@@ -6,17 +6,33 @@ import (
"path"
)
const VERSION = "alpha 0.7.0"
const (
ProgramName = "卡西米尔唱片机"
Version = "beta 0.9.5"
)
const CONFIG_PATH = "./config.ini"
const Assests_PATH = "./assets"
const (
ConfigPath = "./config.ini"
AssetsPath = "./assets"
)
func GetAssetPath(name string) string {
return path.Join(Assests_PATH, name)
return path.Join(AssetsPath, name)
}
type Config interface {
Name() string
OnLoad()
OnSave()
}
type BaseConfig struct {
}
func (c *BaseConfig) OnLoad() {
}
func (c *BaseConfig) OnSave() {
}
var ConfigFile *ini.File
@@ -27,18 +43,19 @@ func LoadConfig(cfg Config) {
if err == nil {
_ = sec.MapTo(cfg)
}
cfg.OnLoad()
Configs = append(Configs, cfg)
return
}
func init() {
var err error
ConfigFile, err = ini.Load(CONFIG_PATH)
ConfigFile, err = ini.Load(ConfigPath)
if err != nil {
fmt.Println("config not found, using default config")
ConfigFile = ini.Empty()
}
for _, cfg := range []Config{Log, LiveRoom, Player, Provider, General} {
for _, cfg := range []Config{Log, General} {
LoadConfig(cfg)
}
}
@@ -46,6 +63,7 @@ func init() {
func SaveToConfigFile(filename string) error {
cfgFile := ini.Empty()
for _, cfg := range Configs {
cfg.OnSave()
if err := cfgFile.Section(cfg.Name()).ReflectFrom(cfg); err != nil {
fmt.Println(err)
}

View File

@@ -1,6 +1,7 @@
package config
type _GeneralConfig struct {
BaseConfig
Language string
}

View File

@@ -1,11 +0,0 @@
package config
type _LiveRoomConfig struct {
History []string
}
func (c *_LiveRoomConfig) Name() string {
return "LiveRoom"
}
var LiveRoom = &_LiveRoomConfig{History: []string{"9076804", "3819533"}}

View File

@@ -3,8 +3,15 @@ package config
import "github.com/sirupsen/logrus"
type _LogConfig struct {
Path string
Level logrus.Level
Path string
Level logrus.Level
RedirectStderr bool
}
func (c *_LogConfig) OnLoad() {
}
func (c *_LogConfig) OnSave() {
}
func (c *_LogConfig) Name() string {
@@ -12,6 +19,7 @@ func (c *_LogConfig) Name() string {
}
var Log = &_LogConfig{
Path: "./log.txt",
Level: logrus.InfoLevel,
Path: "./log.txt",
Level: logrus.InfoLevel,
RedirectStderr: false, // this should be true if it is in production mode.
}

View File

@@ -1,25 +0,0 @@
package config
type _PlayerConfig struct {
Playlists []string
PlaylistsProvider []string
PlaylistIndex int
PlaylistRandom bool
AudioDevice string
Volume float64
SkipPlaylist bool
}
func (c *_PlayerConfig) Name() string {
return "Player"
}
var Player = &_PlayerConfig{
Playlists: []string{"2382819181", "4987059624", "646548465"},
PlaylistsProvider: []string{"netease", "netease", "netease"},
PlaylistIndex: 0,
PlaylistRandom: true,
AudioDevice: "auto",
Volume: 100,
SkipPlaylist: false,
}

View File

@@ -1,15 +0,0 @@
package config
type _ProviderConfig struct {
Priority []string
LocalDir string
}
func (c *_ProviderConfig) Name() string {
return "Provider"
}
var Provider = &_ProviderConfig{
Priority: []string{"local", "netease", "kuwo", "bilibili"},
LocalDir: "./music",
}

View File

@@ -6,7 +6,7 @@ import (
)
func TestCreate(t *testing.T) {
fmt.Println(SaveToConfigFile(CONFIG_PATH))
fmt.Println(SaveToConfigFile(ConfigPath))
}
func TestLoad(t *testing.T) {

23
config/jsonconfig.go Normal file
View File

@@ -0,0 +1,23 @@
package config
import (
"encoding/json"
"os"
)
func LoadJson(path string, dst any) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
return json.Unmarshal(data, dst)
}
func SaveJson(path string, dst any) error {
data, err := json.MarshalIndent(dst, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0666)
}

View File

@@ -1,31 +0,0 @@
package controller
import (
"AynaLivePlayer/event"
"AynaLivePlayer/liveclient"
"strings"
)
var Commands []DanmuCommandExecutor
type DanmuCommandExecutor interface {
Match(command string) bool
Execute(command string, args []string, danmu *liveclient.DanmuMessage)
}
func AddCommand(executors ...DanmuCommandExecutor) {
Commands = append(Commands, executors...)
}
func danmuCommandHandler(event *event.Event) {
danmu := event.Data.(*liveclient.DanmuMessage)
args := strings.Split(danmu.Message, " ")
if len(args[0]) == 0 {
return
}
for _, cmd := range Commands {
if cmd.Match(args[0]) {
cmd.Execute(args[0], args[1:], danmu)
}
}
}

View File

@@ -1,122 +1,13 @@
package controller
import (
"AynaLivePlayer/config"
"AynaLivePlayer/event"
"AynaLivePlayer/liveclient"
"AynaLivePlayer/logger"
"AynaLivePlayer/player"
"AynaLivePlayer/provider"
"AynaLivePlayer/util"
"fmt"
"github.com/sirupsen/logrus"
"strconv"
)
var Instance IController = nil
const MODULE_CONTROLLER = "Controller"
func l() *logrus.Entry {
return logger.Logger.WithField("Module", MODULE_CONTROLLER)
}
func SetDanmuClient(roomId string) {
ResetDanmuClient()
l().Infof("setting live client for %s", roomId)
room, err := strconv.Atoi(roomId)
if err != nil {
l().Warn("parse room id error", err)
return
}
if !util.StringSliceContains(config.LiveRoom.History, roomId) {
config.LiveRoom.History = append(config.LiveRoom.History, roomId)
}
LiveClient = liveclient.NewBilibili(room)
LiveClient.Handler().Register(&event.EventHandler{
EventId: liveclient.EventMessageReceive,
Name: "controller.commandexecutor",
Handler: danmuCommandHandler,
})
LiveClient.Handler().RegisterA(
liveclient.EventMessageReceive,
"controller.danmu.handler",
danmuHandler)
l().Infof("setting live client for %s success", roomId)
}
func StartDanmuClient() {
LiveClient.Connect()
}
func ResetDanmuClient() {
if LiveClient != nil {
l().Infof("disconnect from current live client %s", LiveClient.ClientName())
LiveClient.Disconnect()
LiveClient.Handler().UnregisterAll()
LiveClient = nil
}
}
func AddPlaylist(pname string, uri string) *player.Playlist {
l().Infof("try add playlist %s with provider %s", uri, pname)
id, err := provider.FormatPlaylistUrl(pname, uri)
if err != nil || id == "" {
l().Warnf("fail to format %s playlist id for %s", uri, pname)
return nil
}
p := player.NewPlaylist(fmt.Sprintf("%s-%s", pname, id), player.PlaylistConfig{})
p.Meta = provider.Meta{
Name: pname,
Id: id,
}
PlaylistManager = append(PlaylistManager, p)
config.Player.Playlists = append(config.Player.Playlists, id)
config.Player.PlaylistsProvider = append(config.Player.PlaylistsProvider, pname)
return p
}
func RemovePlaylist(index int) {
l().Infof("Try to remove playlist.index=%d", index)
if index < 0 || index >= len(PlaylistManager) {
l().Warnf("playlist.index=%d not found", index)
return
}
if index == config.Player.PlaylistIndex {
l().Info("Delete current system playlist, reset system playlist to index = 0")
SetSystemPlaylist(0)
}
if index < config.Player.PlaylistIndex {
l().Debugf("Delete playlist before system playlist (index=%d), reduce system playlist index by 1", config.Player.PlaylistIndex)
config.Player.PlaylistIndex = config.Player.PlaylistIndex - 1
}
PlaylistManager = append(PlaylistManager[:index], PlaylistManager[index+1:]...)
config.Player.Playlists = append(config.Player.Playlists[:index], config.Player.Playlists[index+1:]...)
config.Player.PlaylistsProvider = append(config.Player.PlaylistsProvider[:index], config.Player.PlaylistsProvider[index+1:]...)
}
func SetSystemPlaylist(index int) {
l().Infof("try set system playlist to playlist.id=%d", index)
if index < 0 || index >= len(PlaylistManager) {
l().Warn("playlist.index=%d not found", index)
return
}
err := PreparePlaylist(PlaylistManager[index])
if err != nil {
return
}
medias := PlaylistManager[index].Playlist
config.Player.PlaylistIndex = index
ApplyUser(medias, player.PlaylistUser)
SystemPlaylist.Replace(medias)
}
func PreparePlaylistByIndex(index int) {
l().Infof("try prepare playlist.id=%d", index)
if index < 0 || index >= len(PlaylistManager) {
l().Warn("playlist.id=%d not found", index)
return
}
err := PreparePlaylist(PlaylistManager[index])
if err != nil {
return
}
type IController interface {
LiveRooms() ILiveRoomController
PlayControl() IPlayController
Playlists() IPlaylistController
Provider() IProviderController
Plugin() IPluginController
LoadPlugins(plugins ...Plugin)
CloseAndSave()
}

View File

@@ -1,10 +0,0 @@
package controller
import (
"fmt"
"testing"
)
func TestController(t *testing.T) {
fmt.Println(LiveClient == nil)
}

View File

@@ -0,0 +1,59 @@
package core
import (
"AynaLivePlayer/common/logger"
"AynaLivePlayer/controller"
)
var lg = logger.Logger.WithField("Module", "CoreController")
type Controller struct {
liveroom controller.ILiveRoomController `ini:"-"`
player controller.IPlayController `ini:"-"`
lyric controller.ILyricLoader `ini:"-"`
playlist controller.IPlaylistController `ini:"-"`
provider controller.IProviderController `ini:"-"`
plugin controller.IPluginController `ini:"-"`
}
func NewController(
liveroom controller.ILiveRoomController, player controller.IPlayController,
playlist controller.IPlaylistController,
provider controller.IProviderController, plugin controller.IPluginController) controller.IController {
cc := &Controller{
liveroom: liveroom,
player: player,
playlist: playlist,
provider: provider,
plugin: plugin,
}
return cc
}
func (c *Controller) LiveRooms() controller.ILiveRoomController {
return c.liveroom
}
func (c *Controller) PlayControl() controller.IPlayController {
return c.player
}
func (c *Controller) Playlists() controller.IPlaylistController {
return c.playlist
}
func (c *Controller) Provider() controller.IProviderController {
return c.provider
}
func (c *Controller) Plugin() controller.IPluginController {
return c.plugin
}
func (c *Controller) LoadPlugins(plugins ...controller.Plugin) {
c.plugin.LoadPlugins(plugins...)
}
func (c *Controller) CloseAndSave() {
c.plugin.ClosePlugins()
}

201
controller/core/liveroom.go Normal file
View File

@@ -0,0 +1,201 @@
package core
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/config"
"AynaLivePlayer/controller"
"AynaLivePlayer/liveclient"
"AynaLivePlayer/model"
"errors"
"strings"
)
type coreLiveRoom struct {
model.LiveRoom
client liveclient.LiveClient
}
func (r *coreLiveRoom) Model() *model.LiveRoom {
return &r.LiveRoom
}
func (r *coreLiveRoom) Status() bool {
return r.client.Status()
}
func (r *coreLiveRoom) EventManager() *event.Manager {
return r.client.EventManager()
}
func (r *coreLiveRoom) init(msgHandler event.HandlerFunc) (err error) {
if r.client != nil {
return nil
}
r.client, err = liveclient.NewLiveClient(r.ClientName, r.ID)
if err != nil {
return
}
r.client.EventManager().RegisterA(
liveclient.EventMessageReceive,
"controller.danmu.command",
msgHandler)
return nil
}
type LiveRoomController struct {
LiveRoomPath string
liveRooms []*coreLiveRoom
danmuCommands []controller.DanmuCommandExecutor
}
func NewLiveRoomController() controller.ILiveRoomController {
lr := &LiveRoomController{
LiveRoomPath: "liverooms.json",
liveRooms: []*coreLiveRoom{
{LiveRoom: model.LiveRoom{
ClientName: "bilibili",
ID: "9076804",
AutoConnect: false,
}},
{LiveRoom: model.LiveRoom{
ClientName: "bilibili",
ID: "3819533",
AutoConnect: false,
}},
},
danmuCommands: make([]controller.DanmuCommandExecutor, 0),
}
config.LoadConfig(lr)
lr.initialize()
return lr
}
func (lr *LiveRoomController) danmuCommandHandler(event *event.Event) {
danmu := event.Data.(*liveclient.DanmuMessage)
args := strings.Split(danmu.Message, " ")
if len(args[0]) == 0 {
return
}
for _, cmd := range lr.danmuCommands {
if cmd.Match(args[0]) {
cmd.Execute(args[0], args[1:], danmu)
}
}
}
func (lr *LiveRoomController) initialize() {
for i := 0; i < len(lr.liveRooms); i++ {
if lr.liveRooms[i].client == nil {
_ = lr.liveRooms[i].init(lr.danmuCommandHandler)
}
}
go func() {
for i := 0; i < len(lr.liveRooms); i++ {
if lr.liveRooms[i].AutoConnect {
lr.liveRooms[i].client.Connect()
}
}
}()
}
func (lr *LiveRoomController) Name() string {
return "LiveRooms"
}
func (lr *LiveRoomController) Size() int {
return len(lr.liveRooms)
}
func (lr *LiveRoomController) OnLoad() {
rooms := make([]model.LiveRoom, 0)
_ = config.LoadJson(lr.LiveRoomPath, &lr.liveRooms)
if len(rooms) == 0 {
return
}
lr.liveRooms = make([]*coreLiveRoom, len(rooms))
for i := 0; i < len(rooms); i++ {
lr.liveRooms[i] = &coreLiveRoom{LiveRoom: rooms[i]}
}
}
func (lr *LiveRoomController) OnSave() {
rooms := make([]model.LiveRoom, len(lr.liveRooms))
for i := 0; i < len(lr.liveRooms); i++ {
rooms[i] = lr.liveRooms[i].LiveRoom
}
_ = config.SaveJson(lr.LiveRoomPath, &rooms)
}
func (lr *LiveRoomController) Get(index int) controller.ILiveRoom {
if index < 0 || index >= len(lr.liveRooms) {
return nil
}
return lr.liveRooms[index]
}
func (lr *LiveRoomController) GetRoomStatus(index int) bool {
if index < 0 || index >= len(lr.liveRooms) {
return false
}
return lr.liveRooms[index].client.Status()
}
func (lr *LiveRoomController) Connect(index int) error {
lg.Infof("[LiveRooms] Try to start LiveRooms.index=%d", index)
if index < 0 || index >= len(lr.liveRooms) {
lg.Errorf("[LiveRooms] LiveRooms.index=%d not found", index)
return errors.New("index out of range")
}
lr.liveRooms[index].client.Connect()
return nil
}
func (lr *LiveRoomController) Disconnect(index int) error {
lg.Infof("[LiveRooms] Try to Disconnect LiveRooms.index=%d", index)
if index < 0 || index >= len(lr.liveRooms) {
lg.Errorf("[LiveRooms] LiveRooms.index=%d not found", index)
return errors.New("index out of range")
}
lr.liveRooms[index].client.Disconnect()
return nil
}
func (lr *LiveRoomController) AddRoom(clientName, roomId string) (*model.LiveRoom, error) {
rm := &coreLiveRoom{
LiveRoom: model.LiveRoom{
ClientName: clientName,
ID: roomId,
AutoConnect: false,
},
}
lg.Infof("[LiveRooms] add live room %s", &rm.LiveRoom)
err := rm.init(lr.danmuCommandHandler)
if err != nil {
return nil, err
}
lg.Infof("[LiveRooms] %s init failed: %s", &rm.LiveRoom, err)
if err != nil {
return nil, err
}
lr.liveRooms = append(lr.liveRooms, rm)
return &rm.LiveRoom, nil
}
func (lr *LiveRoomController) DeleteRoom(index int) error {
lg.Infof("Try to remove LiveRooms.index=%d", index)
if index < 0 || index >= len(lr.liveRooms) {
lg.Warnf("LiveRooms.index=%d not found", index)
return errors.New("index out of range")
}
if len(lr.liveRooms) == 1 {
return errors.New("can't delete last room")
}
_ = lr.liveRooms[index].client.Disconnect()
lr.liveRooms[index].EventManager().UnregisterAll()
lr.liveRooms = append(lr.liveRooms[:index], lr.liveRooms[index+1:]...)
return nil
}
func (lr *LiveRoomController) AddDanmuCommand(executor controller.DanmuCommandExecutor) {
lr.danmuCommands = append(lr.danmuCommands, executor)
}

56
controller/core/lyric.go Normal file
View File

@@ -0,0 +1,56 @@
package core
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/model"
)
type LyricLoader struct {
Lyric *model.Lyric
Handler *event.Manager
prev float64
}
func NewLyricLoader() *LyricLoader {
return &LyricLoader{
Lyric: model.LoadLyric(""),
Handler: event.MainManager.NewChildManager(),
prev: -1,
}
}
func (l *LyricLoader) EventManager() *event.Manager {
return l.Handler
}
func (l *LyricLoader) Get() *model.Lyric {
return l.Lyric
}
func (l *LyricLoader) Reload(lyric string) {
l.Lyric = model.LoadLyric(lyric)
l.Handler.CallA(
model.EventLyricReload,
model.LyricReloadEvent{
Lyrics: l.Lyric,
})
}
func (l *LyricLoader) Update(time float64) {
lrc := l.Lyric.FindContext(time, 1, 3)
if lrc == nil {
return
}
if l.prev == lrc.Now.Time {
return
}
l.prev = lrc.Now.Time
l.Handler.CallA(
model.EventLyricUpdate,
model.LyricUpdateEvent{
Lyrics: l.Lyric,
Time: time,
Lyric: lrc,
})
return
}

View File

@@ -0,0 +1,248 @@
package core
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/config"
"AynaLivePlayer/controller"
"AynaLivePlayer/model"
"AynaLivePlayer/player"
"AynaLivePlayer/repo/provider"
"errors"
)
type PlayController struct {
eventManager *event.Manager `ini:"-"`
player player.IPlayer `ini:"-"`
playlist controller.IPlaylistController `ini:"-"`
provider controller.IProviderController `ini:"-"`
lyric controller.ILyricLoader `ini:"-"`
playing *model.Media `ini:"-"`
AudioDevice string
Volume float64
SkipPlaylist bool
}
func (pc *PlayController) GetSkipPlaylist() bool {
return pc.SkipPlaylist
}
func (pc *PlayController) SetSkipPlaylist(b bool) {
pc.SkipPlaylist = b
}
func (pc *PlayController) Name() string {
return "PlayController"
}
func (pc *PlayController) OnLoad() {
return
}
func (pc *PlayController) OnSave() {
return
}
func NewPlayerController(
player player.IPlayer,
playlist controller.IPlaylistController,
lyric controller.ILyricLoader,
provider controller.IProviderController) controller.IPlayController {
pc := &PlayController{
eventManager: event.MainManager.NewChildManager(),
player: player,
playlist: playlist,
lyric: lyric,
provider: provider,
playing: &model.Media{},
AudioDevice: "auto",
Volume: 100,
SkipPlaylist: false,
}
config.LoadConfig(pc)
pc.SetVolume(pc.Volume)
pc.SetAudioDevice(pc.AudioDevice)
pc.player.ObserveProperty(model.PlayerPropIdleActive, "controller.playcontrol.idleplaynext", pc.handleMpvIdlePlayNext)
pc.playlist.GetCurrent().EventManager().RegisterA(model.EventPlaylistInsert, "controller.playcontrol.playlistadd", pc.handlePlaylistAdd)
pc.player.ObserveProperty(model.PlayerPropTimePos, "controller.playcontrol.updatelyric", pc.handleLyricUpdate)
return pc
}
func (pc *PlayController) handleMpvIdlePlayNext(event *event.Event) {
isIdle := event.Data.(model.PlayerPropertyUpdateEvent).Value.(bool)
if isIdle {
lg.Info("[Controller] mpv went idle, try play next")
pc.PlayNext()
}
}
func (pc *PlayController) handlePlaylistAdd(event *event.Event) {
if pc.player.IsIdle() {
pc.PlayNext()
return
}
lg.Debugf("[PlayController] playlist add event, SkipPlaylist=%t", pc.SkipPlaylist)
if pc.SkipPlaylist && pc.playing != nil && pc.playing.User == controller.PlaylistUser {
pc.PlayNext()
return
}
}
func (pc *PlayController) handleLyricUpdate(event *event.Event) {
data := event.Data.(model.PlayerPropertyUpdateEvent).Value
if data == nil {
return
}
pc.lyric.Update(data.(float64))
}
func (pc *PlayController) EventManager() *event.Manager {
return pc.eventManager
}
func (pc *PlayController) GetPlaying() *model.Media {
return pc.playing
}
func (pc *PlayController) GetPlayer() player.IPlayer {
return pc.player
}
func (pc *PlayController) GetLyric() controller.ILyricLoader {
return pc.lyric
}
func (pc *PlayController) PlayNext() {
lg.Infof("[PlayController] try to play next possible media")
if pc.playlist.GetCurrent().Size() == 0 && pc.playlist.GetDefault().Size() == 0 {
return
}
var media *model.Media
if pc.playlist.GetCurrent().Size() != 0 {
media = pc.playlist.GetCurrent().Pop().Copy()
} else if pc.playlist.GetDefault().Size() != 0 {
media = pc.playlist.GetDefault().Next().Copy()
media.User = controller.PlaylistUser
}
_ = pc.Play(media)
}
func (pc *PlayController) Play(media *model.Media) error {
lg.Infof("[PlayController] prepare media %s", media.Title)
err := pc.provider.PrepareMedia(media)
if err != nil {
lg.Warn("[PlayController] prepare media failed, try play next")
//pc.PlayNext()
return errors.New("prepare media failed")
}
pc.eventManager.CallA(model.EventPlay, model.PlayEvent{
Media: media,
})
pc.playing = media
pc.playlist.AddToHistory(media)
if err := pc.player.Play(media); err != nil {
lg.Warn("[PlayController] play failed", err)
return errors.New("player play failed")
}
pc.eventManager.CallA(model.EventPlayed, model.PlayEvent{
Media: media,
})
pc.lyric.Reload(media.Lyric)
// reset
media.Url = ""
return nil
}
func (pc *PlayController) Add(keyword string, user interface{}) {
media := pc.provider.MediaMatch(keyword)
if media == nil {
medias, err := pc.provider.Search(keyword)
if err != nil {
lg.Warnf("[PlayController] search for %s, got error %s", keyword, err)
return
}
if len(medias) == 0 {
lg.Info("[PlayController] search for %s, got no result", keyword)
return
}
media = medias[0]
}
media.User = user
lg.Infof("[PlayController] add media %s (%s)", media.Title, media.Artist)
pc.playlist.GetCurrent().Insert(-1, media)
}
func (pc *PlayController) AddWithProvider(keyword string, pname string, user interface{}) {
media := provider.MatchMedia(pname, keyword)
if media == nil {
medias, err := provider.Search(pname, keyword)
if err != nil {
lg.Warnf("[PlayController] search for %s, got error %s", keyword, err)
return
}
if len(medias) == 0 {
lg.Infof("[PlayController] search for %s, got no result", keyword)
return
}
media = medias[0]
}
media.User = user
lg.Infof("[PlayController] add media %s (%s)", media.Title, media.Artist)
pc.playlist.GetCurrent().Insert(-1, media)
}
func (pc *PlayController) Seek(position float64, absolute bool) {
if err := pc.player.Seek(position, absolute); err != nil {
lg.Warnf("[PlayController] seek to position %f (%t) failed, %s", position, absolute, err)
}
}
func (pc *PlayController) Toggle() (b bool) {
var err error
if pc.player.IsPaused() {
err = pc.player.Unpause()
b = false
} else {
err = pc.player.Pause()
b = true
}
if err != nil {
lg.Warn("[PlayController] toggle failed", err)
}
return
}
func (pc *PlayController) SetVolume(volume float64) {
if pc.player.SetVolume(volume) != nil {
lg.Warnf("[PlayController] set mpv volume to %f failed", volume)
return
}
pc.Volume = volume
}
func (pc *PlayController) Destroy() {
pc.player.Stop()
}
func (pc *PlayController) GetCurrentAudioDevice() string {
return pc.AudioDevice
}
func (pc *PlayController) GetAudioDevices() []model.AudioDevice {
dl, err := pc.player.GetAudioDeviceList()
if err != nil {
return make([]model.AudioDevice, 0)
}
return dl
}
func (pc *PlayController) SetAudioDevice(device string) {
lg.Infof("[PlayController] set audio device to %s", device)
if err := pc.player.SetAudioDevice(device); err != nil {
lg.Warnf("[PlayController] set mpv audio device to %s failed, %s", device, err)
_ = pc.player.SetAudioDevice("auto")
pc.AudioDevice = "auto"
return
}
pc.AudioDevice = device
}

397
controller/core/playlist.go Normal file
View File

@@ -0,0 +1,397 @@
package core
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/config"
"AynaLivePlayer/controller"
"AynaLivePlayer/model"
"AynaLivePlayer/repo/provider"
"errors"
"fmt"
"math/rand"
"sync"
)
type PlaylistController struct {
PlaylistPath string
provider controller.IProviderController
History controller.IPlaylist `ini:"-"`
Current controller.IPlaylist `ini:"-"`
Default controller.IPlaylist `ini:"-"`
Playlists []controller.IPlaylist `ini:"-"`
DefaultIndex int
CurrentPlaylistRandom bool
DefaultPlaylistRandom bool
}
func NewPlaylistController(
provider controller.IProviderController) controller.IPlaylistController {
pc := &PlaylistController{
PlaylistPath: "playlists.json",
provider: provider,
History: NewPlaylist("history"),
Default: NewPlaylist("default"),
Current: NewPlaylist("current"),
Playlists: make([]controller.IPlaylist, 0),
DefaultIndex: 0,
CurrentPlaylistRandom: false,
DefaultPlaylistRandom: true,
}
config.LoadConfig(pc)
if pc.DefaultIndex < 0 || pc.DefaultIndex >= len(pc.Playlists) {
pc.DefaultIndex = 0
lg.Warn("playlist index did not find")
}
go func() {
_ = pc.SetDefault(pc.DefaultIndex)
}()
return pc
}
func (pc *PlaylistController) Name() string {
return "Playlists"
}
func (pc *PlaylistController) OnLoad() {
var metas = []model.Meta{
{
"netease",
"2382819181",
},
{"netease",
"4987059624",
},
{"local",
"list1",
},
}
_ = config.LoadJson(pc.PlaylistPath, &metas)
for _, m := range metas {
p := NewPlaylist(fmt.Sprintf("%s-%s", m.Name, m.Id))
p.Model().Meta = m
pc.Playlists = append(pc.Playlists, p)
}
if pc.CurrentPlaylistRandom {
pc.Current.Model().Mode = model.PlaylistModeRandom
}
if pc.DefaultPlaylistRandom {
pc.Default.Model().Mode = model.PlaylistModeRandom
}
}
func (pc *PlaylistController) OnSave() {
var metas = make([]model.Meta, 0)
for _, pl := range pc.Playlists {
metas = append(metas, pl.Model().Meta)
}
_ = config.SaveJson(pc.PlaylistPath, &metas)
if pc.Current.Model().Mode == model.PlaylistModeRandom {
pc.CurrentPlaylistRandom = true
} else {
pc.CurrentPlaylistRandom = false
}
if pc.Default.Model().Mode == model.PlaylistModeRandom {
pc.DefaultPlaylistRandom = true
} else {
pc.DefaultPlaylistRandom = false
}
}
func (pc *PlaylistController) Size() int {
return len(pc.Playlists)
}
func (pc *PlaylistController) GetHistory() controller.IPlaylist {
return pc.History
}
func (pc *PlaylistController) GetDefault() controller.IPlaylist {
return pc.Default
}
func (pc *PlaylistController) GetCurrent() controller.IPlaylist {
return pc.Current
}
func (pc *PlaylistController) AddToHistory(media *model.Media) {
lg.Tracef("add media %s (%s) to history", media.Title, media.Artist)
media = media.Copy()
// reset url for future use
media.Url = ""
if pc.History.Size() >= 1024 {
pc.History.Replace([]*model.Media{})
}
media.User = controller.HistoryUser
pc.History.Push(media)
return
}
func (pc *PlaylistController) Get(index int) controller.IPlaylist {
if index < 0 || index >= len(pc.Playlists) {
lg.Warnf("playlist.index=%d not found", index)
return nil
}
return pc.Playlists[index]
}
func (pc *PlaylistController) Add(pname string, uri string) controller.IPlaylist {
lg.Infof("try add playlist %s with provider %s", uri, pname)
id, err := provider.FormatPlaylistUrl(pname, uri)
if err != nil || id == "" {
lg.Warnf("fail to format %s playlist id for %s", uri, pname)
return nil
}
p := NewPlaylist(fmt.Sprintf("%s-%s", pname, id))
p.Model().Meta = model.Meta{
Name: pname,
Id: id,
}
pc.Playlists = append(pc.Playlists, p)
return p
}
func (pc *PlaylistController) Remove(index int) controller.IPlaylist {
lg.Infof("Try to remove playlist.index=%d", index)
if index < 0 || index >= len(pc.Playlists) {
lg.Warnf("playlist.index=%d not found", index)
return nil
}
if index == pc.DefaultIndex {
lg.Info("Delete current system playlist, reset system playlist to index = 0")
_ = pc.SetDefault(0)
}
if index < pc.DefaultIndex {
lg.Debugf("Delete playlist before system playlist (index=%d), reduce system playlist index by 1", pc.DefaultIndex)
pc.DefaultIndex = pc.DefaultIndex - 1
}
pl := pc.Playlists[index]
pc.Playlists = append(pc.Playlists[:index], pc.Playlists[index+1:]...)
return pl
}
func (pc *PlaylistController) SetDefault(index int) error {
lg.Infof("try set system playlist to playlist.id=%d", index)
if index < 0 || index >= len(pc.Playlists) {
lg.Warn("playlist.index=%d not found", index)
return errors.New("playlist.index not found")
}
err := pc.provider.PreparePlaylist(pc.Playlists[index])
if err != nil {
return err
}
pl := pc.Playlists[index].Model().Copy()
pc.DefaultIndex = index
controller.ApplyUser(pl.Medias, controller.PlaylistUser)
pc.Default.Replace(pl.Medias)
pc.Default.Model().Name = pl.Name
return nil
}
func (pc *PlaylistController) PreparePlaylistByIndex(index int) error {
lg.Infof("try prepare playlist.id=%d", index)
if index < 0 || index >= len(pc.Playlists) {
lg.Warn("playlist.id=%d not found", index)
return nil
}
return pc.provider.PreparePlaylist(pc.Playlists[index])
}
type corePlaylist struct {
model.Playlist
Index int
Lock sync.RWMutex
eventManager *event.Manager
}
func NewPlaylist(name string) controller.IPlaylist {
return &corePlaylist{
Index: 0,
Playlist: model.Playlist{
Name: name,
Medias: make([]*model.Media, 0),
Mode: model.PlaylistModeNormal,
Meta: model.Meta{},
},
eventManager: event.MainManager.NewChildManager(),
}
}
func (p *corePlaylist) Model() *model.Playlist {
return &p.Playlist
}
func (p *corePlaylist) EventManager() *event.Manager {
return p.eventManager
}
func (p *corePlaylist) Name() string {
return p.Playlist.Name
}
func (p *corePlaylist) Size() int {
return p.Playlist.Size()
}
func (p *corePlaylist) Get(index int) *model.Media {
if index < 0 || index >= p.Playlist.Size() {
return nil
}
return p.Playlist.Medias[index]
}
func (p *corePlaylist) Pop() *model.Media {
lg.Info("[Playlists] %s pop first media", p.Playlist)
if p.Size() == 0 {
return nil
}
p.Lock.Lock()
index := 0
if p.Mode == model.PlaylistModeRandom {
index = rand.Intn(p.Size())
}
m := p.Medias[index]
for i := index; i > 0; i-- {
p.Medias[i] = p.Medias[i-1]
}
p.Medias = p.Medias[1:]
p.Lock.Unlock()
if m == nil {
lg.Warn("[Playlists] pop first media failed, no media left in the playlist")
return nil
}
p.eventManager.CallA(
model.EventPlaylistUpdate,
model.PlaylistUpdateEvent{Playlist: p.Playlist.Copy()},
)
return m
}
func (p *corePlaylist) Replace(medias []*model.Media) {
lg.Infof("[Playlists] %s replace all media", &p.Playlist)
p.Lock.Lock()
p.Playlist.Medias = medias
p.Index = 0
p.Lock.Unlock()
p.eventManager.CallA(
model.EventPlaylistUpdate,
model.PlaylistUpdateEvent{Playlist: p.Playlist.Copy()},
)
}
func (p *corePlaylist) Push(media *model.Media) {
p.Insert(-1, media)
}
func (p *corePlaylist) Insert(index int, media *model.Media) {
lg.Infof("[Playlists]insert media into new index %d at %s ", index, p.Playlist)
lg.Debugf("media=%s %v", media.Title, media.Meta)
e := event.Event{
Id: model.EventPlaylistPreInsert,
Cancelled: false,
Data: model.PlaylistInsertEvent{
Playlist: p.Playlist.Copy(),
Index: index,
Media: media,
},
}
p.eventManager.Call(&e)
if e.Cancelled {
lg.Info("[Playlists] media insertion has been cancelled by handler")
return
}
p.Lock.Lock()
if index > p.Size() {
index = p.Size()
}
if index < 0 {
index = p.Size() + index + 1
}
p.Medias = append(p.Medias, nil)
for i := p.Size() - 1; i > index; i-- {
p.Medias[i] = p.Medias[i-1]
}
p.Medias[index] = media
p.Lock.Unlock()
p.eventManager.CallA(
model.EventPlaylistUpdate,
model.PlaylistUpdateEvent{Playlist: p.Playlist.Copy()},
)
p.eventManager.CallA(
model.EventPlaylistInsert,
model.PlaylistInsertEvent{
Playlist: p.Playlist.Copy(),
Index: index,
Media: media,
},
)
}
func (p *corePlaylist) Delete(index int) *model.Media {
lg.Infof("from media at index %d from %s", index, p.Playlist)
if index >= p.Size() || index < 0 {
p.Lock.Unlock()
return nil
}
m := p.Medias[index]
p.Lock.Lock()
// todo: @5 delete optimization
p.Medias = append(p.Medias[:index], p.Medias[index+1:]...)
p.Lock.Unlock()
if m == nil {
lg.Warnf("media at index %d does not exist", index)
}
p.eventManager.CallA(
model.EventPlaylistUpdate,
model.PlaylistUpdateEvent{Playlist: p.Playlist.Copy()})
return m
}
func (p *corePlaylist) Move(src int, dst int) {
lg.Infof("from media from index %d to %d", src, dst)
if src >= p.Size() || src < 0 {
lg.Warnf("media at index %d does not exist", src)
return
}
p.Lock.Lock()
if dst >= p.Size() {
dst = p.Size() - 1
}
if dst < 0 {
dst = 0
}
if dst == src {
p.Lock.Unlock()
return
}
step := 1
if dst < src {
step = -1
}
tmp := p.Medias[src]
for i := src; i != dst; i += step {
p.Medias[i] = p.Medias[i+step]
}
p.Medias[dst] = tmp
p.Lock.Unlock()
p.eventManager.CallA(
model.EventPlaylistUpdate,
model.PlaylistUpdateEvent{Playlist: p.Playlist.Copy()})
}
func (p *corePlaylist) Next() *model.Media {
lg.Infof("[Playlists] %s get next media with random=%t", p, p.Mode == model.PlaylistModeRandom)
if p.Size() == 0 {
lg.Warn("[Playlists] get next media failed, no media left in the playlist")
return nil
}
var index int
index = p.Index
if p.Mode == model.PlaylistModeRandom {
p.Index = rand.Intn(p.Size())
} else {
p.Index = (p.Index + 1) % p.Size()
}
m := p.Medias[index]
return m
}

44
controller/core/plugin.go Normal file
View File

@@ -0,0 +1,44 @@
package core
import (
"AynaLivePlayer/controller"
"github.com/sirupsen/logrus"
)
type PluginController struct {
plugins map[string]controller.Plugin
}
func NewPluginController() controller.IPluginController {
return &PluginController{
plugins: make(map[string]controller.Plugin),
}
}
func (p *PluginController) LoadPlugin(plugin controller.Plugin) {
lg.Info("[Plugin] Loading plugin: " + plugin.Name())
if _, ok := p.plugins[plugin.Name()]; ok {
logrus.Warnf("[Plugin] plugin with same name already exists, skip")
return
}
if err := plugin.Enable(); err != nil {
lg.Warnf("Failed to load plugin: %s, %s", plugin.Name(), err)
return
}
p.plugins[plugin.Name()] = plugin
}
func (p *PluginController) LoadPlugins(plugins ...controller.Plugin) {
for _, plugin := range plugins {
p.LoadPlugin(plugin)
}
}
func (p *PluginController) ClosePlugins() {
for _, plugin := range p.plugins {
if err := plugin.Disable(); err != nil {
lg.Warnf("Failed to close plugin: %s, %s", plugin.Name(), err)
continue
}
}
}

114
controller/core/provider.go Normal file
View File

@@ -0,0 +1,114 @@
package core
import (
"AynaLivePlayer/config"
"AynaLivePlayer/controller"
"AynaLivePlayer/model"
"AynaLivePlayer/repo/provider"
)
type ProviderController struct {
config.BaseConfig
Priority []string
LocalDir string
}
func (pc *ProviderController) Name() string {
return "Provider"
}
func NewProviderController() controller.IProviderController {
p := &ProviderController{
Priority: []string{"netease", "kuwo", "bilibili", "local", "bilibili-video"},
LocalDir: "./music",
}
config.LoadConfig(p)
provider.NewLocal(p.LocalDir)
return p
}
func (pc *ProviderController) GetPriority() []string {
return pc.Priority
}
func (pc *ProviderController) PrepareMedia(media *model.Media) error {
var err error
if media.Title == "" || !media.Cover.Exists() {
lg.Trace("fetching media info")
if err = provider.UpdateMedia(media); err != nil {
lg.Warn("fail to prepare media when fetch info", err)
return err
}
}
if media.Url == "" {
lg.Trace("fetching media url")
if err = provider.UpdateMediaUrl(media); err != nil {
lg.Warn("fail to prepare media when url", err)
return err
}
}
if media.Lyric == "" {
lg.Trace("fetching media lyric")
if err = provider.UpdateMediaLyric(media); err != nil {
lg.Warn("fail to prepare media when lyric", err)
}
}
return nil
}
func (pc *ProviderController) MediaMatch(keyword string) *model.Media {
lg.Infof("Match media for %s", keyword)
for _, p := range pc.Priority {
if pr, ok := provider.Providers[p]; ok {
m := pr.MatchMedia(keyword)
if m == nil {
continue
}
if err := provider.UpdateMedia(m); err == nil {
return m
}
} else {
lg.Warnf("Provider %s not exist", p)
}
}
return nil
}
func (pc *ProviderController) Search(keyword string) ([]*model.Media, error) {
lg.Infof("Search for %s", keyword)
for _, p := range pc.Priority {
if pr, ok := provider.Providers[p]; ok {
r, err := pr.Search(keyword)
if err != nil {
lg.Warn("Provider %s return err", err)
continue
}
return r, err
} else {
lg.Warnf("Provider %s not exist", p)
}
}
return nil, provider.ErrorNoSuchProvider
}
func (pc *ProviderController) SearchWithProvider(keyword string, p string) ([]*model.Media, error) {
lg.Infof("Search for %s using %s", keyword, p)
if pr, ok := provider.Providers[p]; ok {
r, err := pr.Search(keyword)
return r, err
}
lg.Warnf("Provider %s not exist", p)
return nil, provider.ErrorNoSuchProvider
}
func (pc *ProviderController) PreparePlaylist(playlist controller.IPlaylist) error {
lg.Debug("Prepare playlist ", playlist.Name())
medias, err := provider.GetPlaylist(&playlist.Model().Meta)
if err != nil {
lg.Warn("prepare playlist failed ", err)
return err
}
controller.ApplyUser(medias, controller.SystemUser)
playlist.Replace(medias)
return nil
}

View File

@@ -1,23 +0,0 @@
package controller
import (
"AynaLivePlayer/event"
"AynaLivePlayer/liveclient"
)
var DanmuHandlers []DanmuHandler
type DanmuHandler interface {
Execute(anmu *liveclient.DanmuMessage)
}
func AddDanmuHandler(handlers ...DanmuHandler) {
DanmuHandlers = append(DanmuHandlers, handlers...)
}
func danmuHandler(event *event.Event) {
danmu := event.Data.(*liveclient.DanmuMessage)
for _, cmd := range DanmuHandlers {
cmd.Execute(danmu)
}
}

View File

@@ -1,70 +0,0 @@
package controller
import (
"AynaLivePlayer/config"
"AynaLivePlayer/liveclient"
"AynaLivePlayer/player"
"AynaLivePlayer/provider"
"fmt"
)
var MainPlayer *player.Player
var UserPlaylist *player.Playlist
var History *player.Playlist
var HistoryUser *player.User
var SystemPlaylist *player.Playlist
var LiveClient liveclient.LiveClient
var PlaylistManager []*player.Playlist
var CurrentLyric *player.Lyric
var CurrentMedia *player.Media
func Initialize() {
MainPlayer = player.NewPlayer()
SetAudioDevice(config.Player.AudioDevice)
SetVolume(config.Player.Volume)
UserPlaylist = player.NewPlaylist("user", player.PlaylistConfig{RandomNext: false})
SystemPlaylist = player.NewPlaylist("system", player.PlaylistConfig{RandomNext: config.Player.PlaylistRandom})
PlaylistManager = make([]*player.Playlist, 0)
CurrentLyric = player.NewLyric("")
loadPlaylists()
History = player.NewPlaylist("history", player.PlaylistConfig{RandomNext: false})
HistoryUser = &player.User{Name: "History"}
MainPlayer.ObserveProperty("idle-active", handleMpvIdlePlayNext)
UserPlaylist.Handler.RegisterA(player.EventPlaylistInsert, "controller.playnextwhenadd", handlePlaylistAdd)
MainPlayer.ObserveProperty("time-pos", handleLyricUpdate)
MainPlayer.Start()
}
func loadPlaylists() {
l().Info("Loading playlists ", config.Player.Playlists, config.Player.PlaylistsProvider)
if len(config.Player.Playlists) != len(config.Player.Playlists) {
l().Warn("playlist id and provider does not have same length")
return
}
for i := 0; i < len(config.Player.Playlists); i++ {
pname := config.Player.PlaylistsProvider[i]
id := config.Player.Playlists[i]
p := player.NewPlaylist(fmt.Sprintf("%s-%s", pname, id), player.PlaylistConfig{})
p.Meta = provider.Meta{
Name: pname,
Id: id,
}
PlaylistManager = append(PlaylistManager, p)
}
if config.Player.PlaylistIndex < 0 || config.Player.PlaylistIndex >= len(config.Player.Playlists) {
l().Warn("playlist index did not find")
return
}
go func() {
c := config.Player.PlaylistIndex
err := PreparePlaylist(PlaylistManager[c])
if err != nil {
return
}
SetSystemPlaylist(c)
}()
}

View File

@@ -1,35 +0,0 @@
package controller
import (
"AynaLivePlayer/config"
"AynaLivePlayer/event"
"AynaLivePlayer/player"
"github.com/aynakeya/go-mpv"
)
func handleMpvIdlePlayNext(property *mpv.EventProperty) {
isIdle := property.Data.(mpv.Node).Value.(bool)
if isIdle {
l().Info("mpv went idle, try play next")
PlayNext()
}
}
func handlePlaylistAdd(event *event.Event) {
if MainPlayer.IsIdle() {
PlayNext()
return
}
if config.Player.SkipPlaylist && CurrentMedia != nil && CurrentMedia.User == player.PlaylistUser {
PlayNext()
return
}
}
func handleLyricUpdate(property *mpv.EventProperty) {
if property.Data == nil {
return
}
t := property.Data.(mpv.Node).Value.(float64)
CurrentLyric.Update(t)
}

30
controller/liveroom.go Normal file
View File

@@ -0,0 +1,30 @@
package controller
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/liveclient"
"AynaLivePlayer/model"
)
type DanmuCommandExecutor interface {
Match(command string) bool
Execute(command string, args []string, danmu *liveclient.DanmuMessage)
}
type ILiveRoomController interface {
Size() int
Get(index int) ILiveRoom
GetRoomStatus(index int) bool
Connect(index int) error
Disconnect(index int) error
AddRoom(clientName, roomId string) (*model.LiveRoom, error)
DeleteRoom(index int) error
AddDanmuCommand(executor DanmuCommandExecutor)
}
type ILiveRoom interface {
Model() *model.LiveRoom // should return mutable model (not a copy)
Title() string // should be same as Model().Title
Status() bool
EventManager() *event.Manager
}

34
controller/playcontrol.go Normal file
View File

@@ -0,0 +1,34 @@
package controller
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/model"
"AynaLivePlayer/player"
)
type IPlayController interface {
EventManager() *event.Manager
GetPlaying() *model.Media
GetPlayer() player.IPlayer
PlayNext()
Play(media *model.Media) error
Add(keyword string, user interface{})
AddWithProvider(keyword string, provider string, user interface{})
Seek(position float64, absolute bool)
Toggle() bool
SetVolume(volume float64)
Destroy()
GetCurrentAudioDevice() string
GetAudioDevices() []model.AudioDevice
SetAudioDevice(device string)
GetLyric() ILyricLoader
GetSkipPlaylist() bool
SetSkipPlaylist(b bool)
}
type ILyricLoader interface {
EventManager() *event.Manager
Get() *model.Lyric
Reload(lyric string)
Update(time float64)
}

View File

@@ -1,122 +0,0 @@
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
AddToHistory(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
}

View File

@@ -1,22 +1,34 @@
package controller
import "AynaLivePlayer/player"
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/model"
)
func AddToHistory(media *player.Media) {
l().Tracef("add media %s (%s) to history", media.Title, media.Artist)
media = media.Copy()
History.Push(media)
return
type IPlaylistController interface {
Size() int
GetHistory() IPlaylist
AddToHistory(media *model.Media)
GetDefault() IPlaylist
GetCurrent() IPlaylist
Get(index int) IPlaylist
Add(pname string, uri string) IPlaylist
Remove(index int) IPlaylist
SetDefault(index int) error
PreparePlaylistByIndex(index int) error
}
func ToHistoryMedia(media *player.Media) *player.Media {
media = media.Copy()
media.User = HistoryUser
return media
}
func ToSystemMedia(media *player.Media) *player.Media {
media = media.Copy()
media.User = player.SystemUser
return media
type IPlaylist interface {
Model() *model.Playlist // mutable model (not a copy)
EventManager() *event.Manager
Name() string
Size() int
Get(index int) *model.Media
Pop() *model.Media
Replace(medias []*model.Media)
Push(media *model.Media)
Insert(index int, media *model.Media)
Delete(index int) *model.Media
Move(src int, dst int)
Next() *model.Media
}

View File

@@ -3,17 +3,11 @@ package controller
type Plugin interface {
Name() string
Enable() error
Disable() error
}
func LoadPlugin(plugin Plugin) {
l().Info("Loading plugin: " + plugin.Name())
if err := plugin.Enable(); err != nil {
l().Warnf("Failed to load plugin: %s, %s", plugin.Name(), err)
}
}
func LoadPlugins(plugins ...Plugin) {
for _, plugin := range plugins {
LoadPlugin(plugin)
}
type IPluginController interface {
LoadPlugin(plugin Plugin)
LoadPlugins(plugins ...Plugin)
ClosePlugins()
}

View File

@@ -1,77 +1,30 @@
package controller
import (
"AynaLivePlayer/config"
"AynaLivePlayer/player"
"AynaLivePlayer/provider"
"AynaLivePlayer/model"
)
func PrepareMedia(media *player.Media) error {
var err error
if media.Title == "" || media.Cover == "" {
l().Trace("fetching media info")
if err = provider.UpdateMedia(media); err != nil {
l().Warn("fail to prepare media when fetch info", err)
}
}
if media.Url == "" {
l().Trace("fetching media url")
if err = provider.UpdateMediaUrl(media); err != nil {
l().Warn("fail to prepare media when url", err)
}
var PlaylistUser = &model.User{Name: "Playlists"}
var SystemUser = &model.User{Name: "System"}
var HistoryUser = &model.User{Name: "History"}
}
if media.Lyric == "" {
l().Trace("fetching media lyric")
if err = provider.UpdateMediaLyric(media); err != nil {
l().Warn("fail to prepare media when lyric", err)
}
}
return err
type IProviderController interface {
GetPriority() []string
PrepareMedia(media *model.Media) error
MediaMatch(keyword string) *model.Media
Search(keyword string) ([]*model.Media, error)
SearchWithProvider(keyword string, provider string) ([]*model.Media, error)
PreparePlaylist(playlist IPlaylist) error
}
func Search(keyword string) ([]*player.Media, error) {
l().Infof("Search for %s", keyword)
for _, p := range config.Provider.Priority {
if pr, ok := provider.Providers[p]; ok {
r, err := pr.Search(keyword)
if err != nil {
l().Warn("Provider %s return err", err)
continue
}
return r, err
} else {
l().Warnf("Provider %s not exist", p)
}
}
return nil, provider.ErrorNoSuchProvider
}
func SearchWithProvider(keyword string, p string) ([]*player.Media, error) {
l().Infof("Search for %s using %s", keyword, p)
if pr, ok := provider.Providers[p]; ok {
r, err := pr.Search(keyword)
return r, err
}
l().Warnf("Provider %s not exist", p)
return nil, provider.ErrorNoSuchProvider
}
func ApplyUser(medias []*player.Media, user interface{}) {
func ApplyUser(medias []*model.Media, user interface{}) {
for _, m := range medias {
m.User = user
}
}
func PreparePlaylist(playlist *player.Playlist) error {
l().Debug("Prepare playlist ", playlist.Meta.(provider.Meta))
medias, err := provider.GetPlaylist(playlist.Meta.(provider.Meta))
if err != nil {
l().Warn("prepare playlist failed ", err)
return err
}
ApplyUser(medias, player.SystemUser)
playlist.Replace(medias)
return nil
func ToSpMedia(media *model.Media, user *model.User) *model.Media {
media = media.Copy()
media.User = user
return media
}

View File

@@ -1,88 +0,0 @@
package event
import (
"AynaLivePlayer/logger"
"github.com/sirupsen/logrus"
"sync"
)
type EventId string
const MODULE_HANDLER = "EventHandler"
var eventLogger = logger.Logger.WithFields(logrus.Fields{
"Module": MODULE_HANDLER,
})
type Event struct {
Id EventId
Cancelled bool
Data interface{}
}
type EventHandlerFunc func(event *Event)
type EventHandler struct {
EventId EventId
Name string
Handler EventHandlerFunc
}
type Handler struct {
handlers map[string]*EventHandler
lock sync.RWMutex
}
func NewHandler() *Handler {
return &Handler{
handlers: make(map[string]*EventHandler),
}
}
func (h *Handler) Register(handler *EventHandler) {
h.lock.Lock()
defer h.lock.Unlock()
eventLogger.Tracef("register new handler id=%s,name=%s", handler.EventId, handler.Name)
h.handlers[handler.Name] = handler
}
func (h *Handler) RegisterA(id EventId, name string, handler EventHandlerFunc) {
h.Register(&EventHandler{
EventId: id,
Name: name,
Handler: handler,
})
}
func (h *Handler) UnregisterAll() {
h.lock.Lock()
defer h.lock.Unlock()
eventLogger.Trace("clear all handler")
h.handlers = make(map[string]*EventHandler)
}
func (h *Handler) Unregister(name string) {
h.lock.Lock()
defer h.lock.Unlock()
eventLogger.Tracef("unregister handler name=%s", name)
delete(h.handlers, name)
}
func (h *Handler) Call(event *Event) {
h.lock.RLock()
defer h.lock.RUnlock()
for _, eh := range h.handlers {
if eh.EventId == event.Id {
eventLogger.Tracef("handler name=%s called by event_id = %s", event.Id, eh.Name)
// todo: @3
go eh.Handler(event)
}
}
}
func (h *Handler) CallA(id EventId, data interface{}) {
h.Call(&Event{
Id: id,
Data: data,
})
}

72
go.mod
View File

@@ -1,35 +1,61 @@
module AynaLivePlayer
go 1.16
go 1.19
require (
fyne.io/fyne/v2 v2.1.4
github.com/BurntSushi/toml v0.4.1
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9
github.com/XiaoMengXinX/Music163Api-Go v0.1.26
fyne.io/fyne/v2 v2.2.4
github.com/XiaoMengXinX/Music163Api-Go v0.1.29
github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/aynakeya/blivedm v0.1.3
github.com/aynakeya/go-mpv v0.0.4
github.com/go-ole/go-ole v1.2.6
github.com/aynakeya/blivedm v0.1.6
github.com/aynakeya/go-mpv v0.0.6
github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086
github.com/go-resty/resty/v2 v2.7.0
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526
github.com/gorilla/websocket v1.5.0
github.com/jinzhu/copier v0.3.5
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca
github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b
github.com/mitchellh/panicwrap v1.0.0
github.com/pkg/errors v0.9.1
github.com/magiconair/properties v1.8.5
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cast v1.3.1
github.com/stretchr/testify v1.5.1
github.com/tidwall/gjson v1.14.1
github.com/urfave/cli/v2 v2.3.0
golang.org/x/mod v0.4.2
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
golang.org/x/tools v0.1.5
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/cast v1.5.0
github.com/stretchr/testify v1.7.2
github.com/tidwall/gjson v1.14.3
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15
gopkg.in/ini.v1 v1.66.4
)
replace (
github.com/aynakeya/blivedm => D:\Repository\blivedm
github.com/aynakeya/go-mpv => D:\Repository\go-mpv
require (
fyne.io/systray v1.10.1-0.20220621085403-9a2652634e93 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect
github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 // 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/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 // indirect
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect
github.com/tevino/abool v1.2.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/yuin/goldmark v1.4.0 // indirect
golang.org/x/image v0.0.0-20220601225756-64ec528b34cd // indirect
golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee // indirect
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
)
//replace (
// github.com/aynakeya/blivedm => D:\Repository\blivedm
// github.com/aynakeya/go-mpv => D:\Repository\go-mpv
//)

670
go.sum
View File

@@ -1,139 +1,703 @@
fyne.io/fyne/v2 v2.1.4 h1:bt1+28++kAzRzPB0GM2EuSV4cnl8rXNX4cjfd8G06Rc=
fyne.io/fyne/v2 v2.1.4/go.mod h1:p+E/Dh+wPW8JwR2DVcsZ9iXgR9ZKde80+Y+40Is54AQ=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
fyne.io/fyne/v2 v2.2.4 h1:izyiDUjJYAB7B/MST7M9GDs+mQ0CwDgRZTiVJZQoEe4=
fyne.io/fyne/v2 v2.2.4/go.mod h1:MBoGuHzLLSXdQOWFAwWhIhYTEMp33zqtGCReSWhaQTA=
fyne.io/systray v1.10.1-0.20220621085403-9a2652634e93 h1:V2IC9t0Zj9Ur6qDbfhUuzVmIvXKFyxZXRJyigUvovs4=
fyne.io/systray v1.10.1-0.20220621085403-9a2652634e93/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9 h1:1ltqoej5GtaWF8jaiA49HwsZD459jqm9YFz9ZtMFpQA=
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
github.com/XiaoMengXinX/Music163Api-Go v0.1.26 h1:Nybor5okI8C0jzAiRvGfpLHdDrPqUbjx5kXWIZDX6pw=
github.com/XiaoMengXinX/Music163Api-Go v0.1.26/go.mod h1:kLU/CkLxKnEJFCge0URvQ0lHt6ImoG1/2aVeNbgV2RQ=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/XiaoMengXinX/Music163Api-Go v0.1.29 h1:c7ekfgo4qgEJ3Wjm9rMhGm7ggN8XqbD1idQka4unJ+Q=
github.com/XiaoMengXinX/Music163Api-Go v0.1.29/go.mod h1:kLU/CkLxKnEJFCge0URvQ0lHt6ImoG1/2aVeNbgV2RQ=
github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antonfisher/nested-logrus-formatter v1.3.1 h1:NFJIr+pzwv5QLHTPyKz9UMEoHck02Q9L0FP13b/xSbQ=
github.com/antonfisher/nested-logrus-formatter v1.3.1/go.mod h1:6WTfyWFkBc9+zyBaKIqRrg/KwMqBbodBjgbHjDz7zjA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aynakeya/blivedm v0.1.6 h1:fnjyyHYnXAArcLqMRNLQCAdvRFiEVGA/a/54UatZF0k=
github.com/aynakeya/blivedm v0.1.6/go.mod h1:g7cA6/BfDcrsD4v9w+P6B9Z+gANi4jPlGkZ1Oyuu/i4=
github.com/aynakeya/go-mpv v0.0.6 h1:WCBwHrzl700C1J3f+aXR+URw/OKYPjwUjDW9diOsXYY=
github.com/aynakeya/go-mpv v0.0.6/go.mod h1:do6ImaEyt9dlQ7JRS/8ke+P9q4kGW8+Bf6j3faBQOfE=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086 h1:ORubSQoKnncsBnR4zD9CuYFJCPOCuSNEpWEZrDdBXkc=
github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086/go.mod h1:Z3Lomva4pyMWYezjMAU5QWRh0p1VvO4199OHlFnyKkM=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA=
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f h1:s0O46d8fPwk9kU4k1jj76wBquMVETx7uveQD9MCIQoU=
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be h1:Z28GdQBfKOL8tNHjvaDn3wHDO7AzTRkmAXvHvnopp98=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe h1:A/wiwvQ0CAjPkuJytaD+SsXkPU0asQ+guQEIg1BJGX4=
github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe/go.mod h1:d4clgH0/GrRwWjRzJJQXxT/h1TyuNSfF/X64zb/3Ggg=
github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 h1:+31CdF/okdokeFNoy9L/2PccG3JFidQT3ev64/r4pYU=
github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504/go.mod h1:gLRWYfYnMA9TONeppRSikMdXlHQ97xVsPojddUv3b/E=
github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 h1:hnLq+55b7Zh7/2IRzWCpiTcAvjv/P8ERF+N7+xXbZhk=
github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2/go.mod h1:eO7W361vmlPOrykIg+Rsh1SZ3tQBaOsfzZhsIOb/Lm0=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk=
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec h1:3FLiRYO6PlQFDpUU7OEFlWgjGD1jnBIVSJ5SYRWk+9c=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8=
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20211219123610-ec9572f70e60/go.mod h1:cz9oNYuRUWGdHmLF2IodMLkAhcPtXeULvcBNagUrxTI=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526 h1:NfuKjkj/Xc2z1xZIj+EmNCm5p1nKJPyw3F4E20usXvg=
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/goxjs/gl v0.0.0-20210104184919-e3fafc6f8f2a/go.mod h1:dy/f2gjY09hwVfIyATps4G2ai7/hLwLkc5TrPqONuXY=
github.com/goxjs/glfw v0.0.0-20191126052801-d2efb5f20838/go.mod h1:oS8P8gVOT4ywTcjV6wZlOU4GuVFQ8F5328KY3MJ79CY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jackmordaunt/icns/v2 v2.2.1/go.mod h1:6aYIB9eSzyfHHMKqDf17Xrs1zetQPReAkiUSHzdw4cI=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca h1:ozPUX9TKQZVek4lZWYRsQo7uS8vJ+q4OOHvRhHiCLfU=
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e h1:LvL4XsI70QxOGHed6yhQtAU34Kx3Qq2wwBzGFKY8zKk=
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b h1:tLSDWcFhT0WRlnsFszh4iaFTexWF8mmccGTk88Siq7Q=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc=
github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE=
github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 h1:HunZiaEKNGVdhTRQOVpMmj5MQnGnv+e8uZNu3xFLgyM=
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCEzJNy71UkeF4XIx2EVmL9KLwDQdmM=
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg=
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459 h1:x9pIfbdIjnw+Ylb2vE27Gtqb7BDmfR+nLcJwvbJh98U=
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459/go.mod h1:nFvuG3SWu3VWqobG3cX8nt57wXU0OOFapeCs/8axIuM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.3.8 h1:Nw158Q8QN+CPgTmVRByhVwapp8Mm1e2blinhmx4wx5E=
github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0 h1:OtISOGfH6sOWa1/qXqqAiOIAO6Z5J3AEAE18WAq6BiQ=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 h1:5oN1Pz/eDhCpbMbLstvIPa0b/BEQo6g6nwV3pLjfM6w=
golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220601225756-64ec528b34cd h1:9NbNcTg//wfC5JskFW4Z3sqwVnjmJKHxLAol1bW2qgw=
golang.org/x/image v0.0.0-20220601225756-64ec528b34cd/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee h1:/tShaw8UTf0XzI8DOZwQHzC7d6Vi3EtrBnftiZ4vAvU=
golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 h1:oomkgU6VaQDsV6qZby2uz1Lap0eXmku8+2em3A/l700=
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2/go.mod h1:sUMDUKNB2ZcVjt92UnLy3cdGs+wDAcrPdV3JP6sVgA4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

43
gui/component/button.go Normal file
View File

@@ -0,0 +1,43 @@
package component
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
)
type AsyncButton struct {
widget.Button
}
func NewAsyncButton(label string, tapped func()) *AsyncButton {
b := &AsyncButton{
Button: widget.Button{
Text: label,
},
}
b.ExtendBaseWidget(b)
b.SetOnTapped(tapped)
return b
}
func NewAsyncButtonWithIcon(label string, icon fyne.Resource, tapped func()) *AsyncButton {
b := &AsyncButton{
Button: widget.Button{
Text: label,
Icon: icon,
},
}
b.ExtendBaseWidget(b)
b.SetOnTapped(tapped)
return b
}
func (b *AsyncButton) SetOnTapped(f func()) {
b.Button.OnTapped = func() {
b.Disable()
go func() {
f()
b.Enable()
}()
}
}

32
gui/component/entry.go Normal file
View File

@@ -0,0 +1,32 @@
package component
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
)
type Entry struct {
widget.Entry
OnKeyUp func(key *fyne.KeyEvent)
OnKeyDown func(key *fyne.KeyEvent)
}
func NewEntry() *Entry {
e := &Entry{}
e.ExtendBaseWidget(e)
return e
}
func (m *Entry) KeyUp(key *fyne.KeyEvent) {
m.Entry.KeyUp(key)
if m.OnKeyUp != nil {
m.OnKeyUp(key)
}
}
func (m *Entry) KeyDown(key *fyne.KeyEvent) {
m.Entry.KeyDown(key)
if m.OnKeyDown != nil {
m.OnKeyDown(key)
}
}

38
gui/component/slider.go Normal file
View File

@@ -0,0 +1,38 @@
package component
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
)
type SliderPlus struct {
widget.Slider
OnDragEnd func(value float64)
Dragging bool // during dragging
}
func NewSliderPlus(min, max float64) *SliderPlus {
slider := &SliderPlus{
Slider: widget.Slider{
Value: 0,
Min: min,
Max: max,
Step: 1,
Orientation: widget.Horizontal,
},
}
slider.ExtendBaseWidget(slider)
return slider
}
func (s *SliderPlus) DragEnd() {
if s.OnDragEnd != nil {
s.OnDragEnd(s.Value)
}
s.Dragging = false
}
func (s *SliderPlus) Dragged(e *fyne.DragEvent) {
s.Dragging = true
s.Slider.Dragged(e)
}

166
gui/component/split.go Normal file
View File

@@ -0,0 +1,166 @@
package component
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
type FixedSplit struct {
widget.BaseWidget
Offset float64
Horizontal bool
SeparatorThickness float32
Leading fyne.CanvasObject
Trailing fyne.CanvasObject
}
func NewFixedHSplitContainer(leading, trailing fyne.CanvasObject, offset float64) *FixedSplit {
return NewFixedSplitContainer(leading, trailing, true, offset)
}
func NewFixedVSplitContainer(top, bottom fyne.CanvasObject, offset float64) *FixedSplit {
return NewFixedSplitContainer(top, bottom, false, offset)
}
func NewFixedSplitContainer(leading, trailing fyne.CanvasObject, horizontal bool, offset float64) *FixedSplit {
s := &FixedSplit{
Offset: offset, // Sensible default, can be overridden with SetOffset
SeparatorThickness: theme.SeparatorThicknessSize(),
Horizontal: horizontal,
Leading: leading,
Trailing: trailing,
}
s.BaseWidget.ExtendBaseWidget(s)
return s
}
func (s *FixedSplit) CreateRenderer() fyne.WidgetRenderer {
s.BaseWidget.ExtendBaseWidget(s)
d := widget.NewSeparator()
return &fixedSplitContainerRenderer{
split: s,
divider: d,
objects: []fyne.CanvasObject{s.Leading, d, s.Trailing},
}
}
func (s *FixedSplit) SetOffset(offset float64) {
if s.Offset == offset {
return
}
s.Offset = offset
s.Refresh()
}
func (s *FixedSplit) SetSepThickness(thickness float32) {
if s.SeparatorThickness == thickness {
return
}
s.SeparatorThickness = thickness
s.Refresh()
}
type fixedSplitContainerRenderer struct {
split *FixedSplit
divider *widget.Separator
objects []fyne.CanvasObject
}
func (r *fixedSplitContainerRenderer) Destroy() {
}
func (r *fixedSplitContainerRenderer) Layout(size fyne.Size) {
var dividerPos, leadingPos, trailingPos fyne.Position
var dividerSize, leadingSize, trailingSize fyne.Size
if r.split.Horizontal {
lw, tw := r.computeSplitLengths(size.Width, r.split.Leading.MinSize().Width, r.split.Trailing.MinSize().Width)
leadingPos.X = 0
leadingSize.Width = lw
leadingSize.Height = size.Height
dividerPos.X = lw
dividerSize.Width = r.split.SeparatorThickness
dividerSize.Height = size.Height
trailingPos.X = lw + dividerSize.Width
trailingSize.Width = tw
trailingSize.Height = size.Height
} else {
lh, th := r.computeSplitLengths(size.Height, r.split.Leading.MinSize().Height, r.split.Trailing.MinSize().Height)
leadingPos.Y = 0
leadingSize.Width = size.Width
leadingSize.Height = lh
dividerPos.Y = lh
dividerSize.Width = size.Width
dividerSize.Height = r.split.SeparatorThickness
trailingPos.Y = lh + dividerSize.Height
trailingSize.Width = size.Width
trailingSize.Height = th
}
r.divider.Move(dividerPos)
r.divider.Resize(dividerSize)
r.split.Leading.Move(leadingPos)
r.split.Leading.Resize(leadingSize)
r.split.Trailing.Move(trailingPos)
r.split.Trailing.Resize(trailingSize)
canvas.Refresh(r.divider)
canvas.Refresh(r.split.Leading)
canvas.Refresh(r.split.Trailing)
}
func (r *fixedSplitContainerRenderer) MinSize() fyne.Size {
s := fyne.NewSize(0, 0)
for _, o := range r.objects {
min := o.MinSize()
if r.split.Horizontal {
s.Width += min.Width
s.Height = fyne.Max(s.Height, min.Height)
} else {
s.Width = fyne.Max(s.Width, min.Width)
s.Height += min.Height
}
}
return s
}
func (r *fixedSplitContainerRenderer) Objects() []fyne.CanvasObject {
return r.objects
}
func (r *fixedSplitContainerRenderer) Refresh() {
r.objects[0] = r.split.Leading
// [1] is divider which doesn't change
r.objects[2] = r.split.Trailing
r.Layout(r.split.Size())
canvas.Refresh(r.split)
}
func (r *fixedSplitContainerRenderer) computeSplitLengths(total, lMin, tMin float32) (float32, float32) {
available := float64(total - r.split.SeparatorThickness)
if available <= 0 {
return 0, 0
}
ld := float64(lMin)
tr := float64(tMin)
offset := r.split.Offset
min := ld / available
max := 1 - tr/available
if min <= max {
if offset < min {
offset = min
}
if offset > max {
offset = max
}
} else {
offset = ld / (ld + tr)
}
ld = offset * available
tr = available - ld
return float32(ld), float32(tr)
}

View File

@@ -1,12 +1,11 @@
package gui
import (
"AynaLivePlayer/config"
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/controller"
"AynaLivePlayer/i18n"
"AynaLivePlayer/model"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/widget"
)
@@ -26,16 +25,33 @@ 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(
newCheckInit(
i18n.T("gui.config.basic.random_playlist.user"),
binding.BindBool(&controller.UserPlaylist.Config.RandomNext)),
widget.NewCheckWithData(
func(b bool) {
l().Infof("Set random playlist for user: %t", b)
if b {
controller.Instance.Playlists().GetCurrent().Model().Mode = model.PlaylistModeRandom
} else {
controller.Instance.Playlists().GetCurrent().Model().Mode = model.PlaylistModeNormal
}
},
controller.Instance.Playlists().GetCurrent().Model().Mode == model.PlaylistModeRandom),
newCheckInit(
i18n.T("gui.config.basic.random_playlist.system"),
binding.BindBool(&controller.SystemPlaylist.Config.RandomNext)),
func(b bool) {
l().Infof("Set random playlist for system: %t", b)
if b {
controller.Instance.Playlists().GetDefault().Model().Mode = model.PlaylistModeRandom
} else {
controller.Instance.Playlists().GetDefault().Model().Mode = model.PlaylistModeNormal
}
},
controller.Instance.Playlists().GetDefault().Model().Mode == model.PlaylistModeRandom),
)
devices := controller.GetAudioDevices()
devices := controller.Instance.PlayControl().GetAudioDevices()
deviceDesc := make([]string, len(devices))
deviceDesc2Name := make(map[string]string)
for i, device := range devices {
@@ -43,17 +59,20 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject {
deviceDesc2Name[device.Description] = device.Name
}
deviceSel := widget.NewSelect(deviceDesc, func(s string) {
controller.SetAudioDevice(deviceDesc2Name[s])
controller.Instance.PlayControl().SetAudioDevice(deviceDesc2Name[s])
})
deviceSel.Selected = config.Player.AudioDevice
deviceSel.Selected = controller.Instance.PlayControl().GetCurrentAudioDevice()
outputDevice := container.NewBorder(nil, nil,
widget.NewLabel(i18n.T("gui.config.basic.audio_device")), nil,
deviceSel)
skipPlaylist := container.NewHBox(
widget.NewLabel(i18n.T("gui.config.basic.skip_playlist")),
widget.NewCheckWithData(
newCheckInit(
i18n.T("gui.config.basic.skip_playlist.prompt"),
binding.BindBool(&config.Player.SkipPlaylist),
func(b bool) {
controller.Instance.PlayControl().SetSkipPlaylist(b)
},
controller.Instance.PlayControl().GetSkipPlaylist(),
),
)
b.panel = container.NewVBox(randomPlaylist, outputDevice, skipPlaylist)

View File

@@ -1,34 +1,24 @@
package gui
import (
"AynaLivePlayer/gui/component"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
type TestConfig struct {
}
func (t *TestConfig) Title() string {
return "Test Title"
}
func (T *TestConfig) Description() string {
return "Test Description"
}
func (t *TestConfig) CreatePanel() fyne.CanvasObject {
return widget.NewLabel("asdf")
}
func createConfigLayout() fyne.CanvasObject {
// initialize config panels
for _, c := range ConfigList {
c.CreatePanel()
}
content := container.NewMax()
entryList := widget.NewList(
func() int {
return len(ConfigList)
},
func() fyne.CanvasObject {
return widget.NewLabel("AAAAAAAAAAAAAAAA")
return widget.NewLabel("")
},
func(id widget.ListItemID, object fyne.CanvasObject) {
object.(*widget.Label).SetText(ConfigList[id].Title())
@@ -40,16 +30,11 @@ func createConfigLayout() fyne.CanvasObject {
seg.Style.Alignment = fyne.TextAlignCenter
}
}
a := container.NewVScroll(ConfigList[id].CreatePanel())
content.Objects = []fyne.CanvasObject{
container.NewBorder(container.NewVBox(desc, widget.NewSeparator()), nil, nil, nil,
a),
container.NewVScroll(container.NewVBox(desc, widget.NewSeparator(), ConfigList[id].CreatePanel())),
}
content.Refresh()
}
return container.NewBorder(
nil, nil,
container.NewHBox(entryList, widget.NewSeparator()), nil,
content)
return component.NewFixedSplitContainer(entryList, content, true, 0.23)
}

View File

@@ -1,67 +1,74 @@
package gui
import (
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/common/logger"
"AynaLivePlayer/config"
"AynaLivePlayer/i18n"
"AynaLivePlayer/logger"
"AynaLivePlayer/controller"
"AynaLivePlayer/resource"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/widget"
"github.com/sirupsen/logrus"
"os"
)
const MODULE_GUI = "GUI"
type ConfigLayout interface {
Title() string
Description() string
CreatePanel() fyne.CanvasObject
}
var App fyne.App
var MainWindow fyne.Window
var ConfigList = []ConfigLayout{&bascicConfig{}}
func l() *logrus.Entry {
return logger.Logger.WithField("Module", MODULE_GUI)
}
func black_magic() {
widget.RichTextStyleStrong.TextStyle.Bold = false
}
func Initialize() {
os.Setenv("FYNE_FONT", config.GetAssetPath("msyh.ttc"))
//black_magic()
l().Info("Initializing GUI")
//os.Setenv("FYNE_FONT", config.GetAssetPath("msyh.ttc"))
App = app.New()
MainWindow = App.NewWindow(fmt.Sprintf("AynaLivePlayer Ver.%s", config.VERSION))
App.Settings().SetTheme(&myTheme{})
MainWindow = App.NewWindow(fmt.Sprintf("%s Ver.%s", config.ProgramName, config.Version))
tabs := container.NewAppTabs(
container.NewTabItem(i18n.T("gui.tab.player"),
newPaddedBoarder(nil, createPlayController(), nil, nil, createPlaylist()),
container.NewBorder(nil, createPlayControllerV2(), nil, nil, createPlaylist()),
),
container.NewTabItem(i18n.T("gui.tab.search"),
newPaddedBoarder(createSearchBar(), nil, nil, nil, createSearchList()),
container.NewBorder(createSearchBar(), nil, nil, nil, createSearchList()),
),
container.NewTabItem(i18n.T("gui.tab.room"),
newPaddedBoarder(createRoomController(), nil, nil, nil, createRoomLogger()),
container.NewBorder(nil, nil, createRoomSelector(), nil, createRoomController()),
),
container.NewTabItem(i18n.T("gui.tab.playlist"),
newPaddedBoarder(nil, nil, createPlaylists(), nil, createPlaylistMedias()),
container.NewBorder(nil, nil, createPlaylists(), nil, createPlaylistMedias()),
),
container.NewTabItem(i18n.T("gui.tab.history"),
newPaddedBoarder(nil, nil, nil, nil, createHistoryList()),
container.NewBorder(nil, nil, nil, nil, createHistoryList()),
),
container.NewTabItem(i18n.T("gui.tab.config"),
newPaddedBoarder(nil, nil, nil, nil, createConfigLayout()),
createConfigLayout(),
),
)
tabs.SetTabLocation(container.TabLocationTop)
MainWindow.SetIcon(resource.ImageIcon)
MainWindow.SetContent(tabs)
//MainWindow.Resize(fyne.NewSize(1280, 720))
MainWindow.Resize(fyne.NewSize(960, 480))
//MainWindow.SetFixedSize(true)
}
func AddConfigLayout(cfgs ...ConfigLayout) {
ConfigList = append(ConfigList, cfgs...)
func addShortCut() {
key := &desktop.CustomShortcut{KeyName: fyne.KeyRight, Modifier: fyne.KeyModifierControl | fyne.KeyModifierShift}
MainWindow.Canvas().AddShortcut(key, func(shortcut fyne.Shortcut) {
l().Info("Shortcut pressed: Ctrl+Shift+Right")
controller.Instance.PlayControl().PlayNext()
})
}

56
gui/gutil/resize.go Normal file
View File

@@ -0,0 +1,56 @@
package gutil
import (
"AynaLivePlayer/model"
"bytes"
"errors"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/storage"
"github.com/nfnt/resize"
"image"
"image/png"
)
func ResizeImage(resource fyne.Resource, width int, height int) fyne.Resource {
data := resource.Content()
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return resource
}
img = resize.Thumbnail(uint(width), uint(height), img, resize.Lanczos3)
buf := bytes.NewBuffer([]byte{})
err = png.Encode(buf, img)
if err != nil {
return resource
}
return fyne.NewStaticResource(resource.Name(), buf.Bytes())
}
func NewImageFromPlayerPicture(picture model.Picture) (*canvas.Image, error) {
var img *canvas.Image
if picture.Data != nil {
img = canvas.NewImageFromReader(bytes.NewReader(picture.Data), "cover")
// return an error when img is nil
if img == nil {
return nil, errors.New("fail to read image")
}
} else {
uri, err := storage.ParseURI(picture.Url)
if err != nil {
return nil, err
}
if uri == nil {
return nil, errors.New("fail to fail url")
}
img = canvas.NewImageFromURI(uri)
if img == nil {
// bug fix, return a new error to indicate fail to read an image
return nil, errors.New("fail to read image")
}
}
// compress image, so it won't be too large
img.Resource = ResizeImage(img.Resource, 128, 128)
return img, nil
}

View File

@@ -3,6 +3,7 @@ package gui
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
@@ -17,21 +18,6 @@ func newLabelWithWrapping(text string, wrapping fyne.TextWrap) *widget.Label {
return w
}
func createAsyncOnTapped(btn *widget.Button, f func()) func() {
return func() {
btn.Disable()
go func() {
f()
btn.Enable()
}()
}
}
func createAsyncButton(btn *widget.Button, tapped func()) *widget.Button {
btn.OnTapped = createAsyncOnTapped(btn, tapped)
return btn
}
type ContextMenuButton struct {
widget.Button
menu *fyne.Menu
@@ -49,28 +35,14 @@ func newContextMenuButton(label string, menu *fyne.Menu) *ContextMenuButton {
return b
}
type FixedSplitContainer struct {
*container.Split
}
func (f *FixedSplitContainer) Dragged(event *fyne.DragEvent) {
// do nothing
}
func (f *FixedSplitContainer) DragEnd() {
// do nothing
}
func newFixedSplitContainer(horizontal bool, leading, trailing fyne.CanvasObject) *FixedSplitContainer {
s := &container.Split{
Offset: 0.5, // Sensible default, can be overridden with SetOffset
Horizontal: horizontal,
Leading: leading,
Trailing: trailing,
func showDialogIfError(err error) {
if err != nil {
dialog.ShowError(err, MainWindow)
}
fs := &FixedSplitContainer{
s,
}
fs.Split.BaseWidget.ExtendBaseWidget(s)
return fs
}
func newCheckInit(name string, changed func(bool), checked bool) *widget.Check {
check := widget.NewCheck(name, changed)
check.SetChecked(checked)
return check
}

View File

@@ -1,26 +1,26 @@
package gui
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/controller"
"AynaLivePlayer/event"
"AynaLivePlayer/i18n"
"AynaLivePlayer/player"
"AynaLivePlayer/model"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"sync"
)
type HistoryContainer struct {
Playlist *player.Playlist
var History = &struct {
Playlist *model.Playlist
List *widget.List
}
var History = &PlaylistContainer{}
mux sync.RWMutex
}{}
func createHistoryList() fyne.CanvasObject {
History.Playlist = controller.History
History.Playlist = controller.Instance.Playlists().GetHistory().Model().Copy()
History.List = widget.NewList(
func() int {
return History.Playlist.Size()
@@ -38,7 +38,7 @@ func createHistoryList() fyne.CanvasObject {
newLabelWithWrapping("user", fyne.TextTruncate)))
},
func(id widget.ListItemID, object fyne.CanvasObject) {
m := History.Playlist.Playlist[History.Playlist.Size()-id-1]
m := History.Playlist.Medias[History.Playlist.Size()-id-1].Copy()
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[0].(*widget.Label).SetText(
m.Title)
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[1].(*widget.Label).SetText(
@@ -47,11 +47,12 @@ func createHistoryList() fyne.CanvasObject {
m.ToUser().Name)
object.(*fyne.Container).Objects[1].(*widget.Label).SetText(fmt.Sprintf("%d", id))
btns := object.(*fyne.Container).Objects[2].(*fyne.Container).Objects
m.User = controller.HistoryUser
btns[0].(*widget.Button).OnTapped = func() {
controller.Play(controller.ToHistoryMedia(m))
showDialogIfError(controller.Instance.PlayControl().Play(m))
}
btns[1].(*widget.Button).OnTapped = func() {
controller.UserPlaylist.Push(controller.ToHistoryMedia(m))
controller.Instance.Playlists().GetCurrent().Push(m)
}
})
registerHistoryHandler()
@@ -68,9 +69,10 @@ func createHistoryList() fyne.CanvasObject {
}
func registerHistoryHandler() {
History.Playlist.Handler.RegisterA(player.EventPlaylistUpdate, "gui.history.update", func(event *event.Event) {
History.Playlist.Lock.RLock()
controller.Instance.Playlists().GetHistory().EventManager().RegisterA(model.EventPlaylistUpdate, "gui.history.update", func(event *event.Event) {
History.mux.RLock()
History.Playlist = event.Data.(model.PlaylistUpdateEvent).Playlist
History.List.Refresh()
History.Playlist.Lock.RUnlock()
History.mux.RUnlock()
})
}

15
gui/interface.go Normal file
View File

@@ -0,0 +1,15 @@
package gui
import "fyne.io/fyne/v2"
var ConfigList = []ConfigLayout{&bascicConfig{}}
type ConfigLayout interface {
Title() string
Description() string
CreatePanel() fyne.CanvasObject
}
func AddConfigLayout(cfgs ...ConfigLayout) {
ConfigList = append(ConfigList, cfgs...)
}

135
gui/liverooms.go Normal file
View File

@@ -0,0 +1,135 @@
package gui
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/controller"
"AynaLivePlayer/liveclient"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
)
var RoomTab = &struct {
Rooms *widget.List
Index int
AddBtn *widget.Button
RemoveBtn *widget.Button
RoomTitle *widget.Label
Status *widget.Label
AutoConnect *widget.Check
ConnectBtn *widget.Button
DisConnectBtn *widget.Button
}{}
func createRoomSelector() fyne.CanvasObject {
RoomTab.Rooms = widget.NewList(
func() int {
return controller.Instance.LiveRooms().Size()
},
func() fyne.CanvasObject {
return widget.NewLabel("AAAAAAAAAAAAAAAA")
},
func(id widget.ListItemID, object fyne.CanvasObject) {
object.(*widget.Label).SetText(
controller.Instance.LiveRooms().Get(id).Title())
})
RoomTab.AddBtn = widget.NewButton(i18n.T("gui.room.button.add"), func() {
clientNameEntry := widget.NewSelect(liveclient.GetAllClientNames(), nil)
idEntry := widget.NewEntry()
dia := dialog.NewCustomConfirm(
i18n.T("gui.room.add.title"),
i18n.T("gui.room.add.confirm"),
i18n.T("gui.room.add.cancel"),
container.NewVBox(
container.New(
layout.NewFormLayout(),
widget.NewLabel(i18n.T("gui.room.add.client_name")),
clientNameEntry,
widget.NewLabel(i18n.T("gui.room.add.id_url")),
idEntry,
),
widget.NewLabel(i18n.T("gui.room.add.prompt")),
),
func(b bool) {
if b && len(clientNameEntry.Selected) > 0 && len(idEntry.Text) > 0 {
_, err := controller.Instance.LiveRooms().AddRoom(clientNameEntry.Selected, idEntry.Text)
showDialogIfError(err)
RoomTab.Rooms.Refresh()
}
},
MainWindow,
)
dia.Resize(fyne.NewSize(512, 256))
dia.Show()
})
RoomTab.RemoveBtn = widget.NewButton(i18n.T("gui.room.button.remove"), func() {
showDialogIfError(controller.Instance.LiveRooms().DeleteRoom(PlaylistManager.Index))
RoomTab.Rooms.Select(0)
RoomTab.Rooms.Refresh()
})
RoomTab.Rooms.OnSelected = func(id widget.ListItemID) {
rom := controller.Instance.LiveRooms().Get(PlaylistManager.Index)
if rom != nil {
rom.EventManager().Unregister("gui.liveroom.status")
}
RoomTab.Index = id
rom = controller.Instance.LiveRooms().Get(RoomTab.Index)
rom.EventManager().RegisterA(liveclient.EventStatusChange, "gui.liveroom.status", func(event *event.Event) {
d := event.Data.(liveclient.StatusChangeEvent)
if d.Connected {
RoomTab.Status.SetText(i18n.T("gui.room.status.connected"))
} else {
RoomTab.Status.SetText(i18n.T("gui.room.status.disconnected"))
}
RoomTab.Status.Refresh()
})
RoomTab.RoomTitle.SetText(rom.Title())
RoomTab.AutoConnect.SetChecked(rom.Model().AutoConnect)
if controller.Instance.LiveRooms().GetRoomStatus(RoomTab.Index) {
RoomTab.Status.SetText(i18n.T("gui.room.status.connected"))
} else {
RoomTab.Status.SetText(i18n.T("gui.room.status.disconnected"))
}
RoomTab.Status.Refresh()
}
return container.NewHBox(
container.NewBorder(
nil, container.NewCenter(container.NewHBox(RoomTab.AddBtn, RoomTab.RemoveBtn)),
nil, nil,
RoomTab.Rooms,
),
widget.NewSeparator(),
)
}
func createRoomController() fyne.CanvasObject {
RoomTab.ConnectBtn = widget.NewButton(i18n.T("gui.room.btn.connect"), func() {
RoomTab.ConnectBtn.Disable()
go func() {
_ = controller.Instance.LiveRooms().Connect(RoomTab.Index)
RoomTab.ConnectBtn.Enable()
}()
})
RoomTab.DisConnectBtn = widget.NewButton(i18n.T("gui.room.btn.disconnect"), func() {
_ = controller.Instance.LiveRooms().Disconnect(RoomTab.Index)
})
RoomTab.Status = widget.NewLabel(i18n.T("gui.room.waiting"))
RoomTab.RoomTitle = widget.NewLabel("")
RoomTab.AutoConnect = widget.NewCheck(i18n.T("gui.room.check.autoconnect"), func(b bool) {
rom := controller.Instance.LiveRooms().Get(RoomTab.Index)
if rom != nil {
rom.Model().AutoConnect = b
}
return
})
RoomTab.Rooms.Select(0)
return container.NewVBox(
RoomTab.RoomTitle,
RoomTab.Status,
container.NewHBox(widget.NewLabel(i18n.T("gui.room.check.autoconnect")), RoomTab.AutoConnect),
container.NewHBox(RoomTab.ConnectBtn, RoomTab.DisConnectBtn),
)
}

View File

@@ -1,19 +1,20 @@
package gui
import (
"AynaLivePlayer/config"
"AynaLivePlayer/common/event"
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/common/util"
"AynaLivePlayer/controller"
"AynaLivePlayer/event"
"AynaLivePlayer/i18n"
"AynaLivePlayer/player"
"AynaLivePlayer/util"
"AynaLivePlayer/gui/component"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/model"
"AynaLivePlayer/resource"
"context"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/aynakeya/go-mpv"
)
type PlayControllerContainer struct {
@@ -21,10 +22,11 @@ type PlayControllerContainer struct {
Artist *widget.Label
Username *widget.Label
Cover *canvas.Image
coverLoader context.CancelFunc
ButtonPrev *widget.Button
ButtonSwitch *widget.Button
ButtonNext *widget.Button
Progress *widget.Slider
Progress *component.SliderPlus
Volume *widget.Slider
ButtonLrc *widget.Button
LrcWindowOpen bool
@@ -33,62 +35,21 @@ type PlayControllerContainer struct {
}
func (p *PlayControllerContainer) SetDefaultCover() {
p.Cover.Resource = nil
p.Cover.File = config.GetAssetPath("empty.png")
p.Cover.Resource = resource.ImageEmpty
p.Cover.Refresh()
}
var PlayController = &PlayControllerContainer{}
func createPlayController() fyne.CanvasObject {
PlayController.Cover = canvas.NewImageFromFile(config.GetAssetPath("empty.png"))
PlayController.Cover.SetMinSize(fyne.NewSize(128, 128))
PlayController.Cover.FillMode = canvas.ImageFillContain
PlayController.ButtonPrev = widget.NewButtonWithIcon("", theme.MediaSkipPreviousIcon(), func() {})
PlayController.ButtonSwitch = widget.NewButtonWithIcon("", theme.MediaPlayIcon(), func() {})
PlayController.ButtonNext = widget.NewButtonWithIcon("", theme.MediaSkipNextIcon(), func() {})
buttonsBox := container.NewCenter(
container.NewHBox(PlayController.ButtonPrev, PlayController.ButtonSwitch, PlayController.ButtonNext))
PlayController.Progress = widget.NewSlider(0, 1000)
PlayController.CurrentTime = widget.NewLabel("0:00")
PlayController.TotalTime = widget.NewLabel("0:00")
progressItem := container.NewBorder(nil, nil, PlayController.CurrentTime, PlayController.TotalTime, PlayController.Progress)
PlayController.Title = widget.NewLabel("Title")
PlayController.Artist = widget.NewLabel("Artist")
PlayController.Username = widget.NewLabel("Username")
playInfo := container.NewVBox(PlayController.Title, PlayController.Artist, PlayController.Username)
PlayController.Volume = widget.NewSlider(0, 100)
volumeIcon := widget.NewIcon(theme.VolumeMuteIcon())
PlayController.ButtonLrc = widget.NewButton(i18n.T("gui.player.button.lrc"), func() {})
volumeControl := container.NewBorder(nil, nil, container.NewHBox(widget.NewLabel(" "), volumeIcon), nil,
container.NewGridWithColumns(3, container.NewMax(PlayController.Volume), PlayController.ButtonLrc))
registerPlayControllerHandler()
return container.NewBorder(nil, nil, container.NewHBox(PlayController.Cover, playInfo, widget.NewSeparator()), nil,
container.NewVBox(buttonsBox, progressItem, volumeControl))
}
func registerPlayControllerHandler() {
PlayController.ButtonPrev.OnTapped = func() {
controller.Seek(0, true)
controller.Instance.PlayControl().Seek(0, true)
}
PlayController.ButtonSwitch.OnTapped = func() {
if controller.Toggle() {
PlayController.ButtonSwitch.Icon = theme.MediaPlayIcon()
} else {
PlayController.ButtonSwitch.Icon = theme.MediaStopIcon()
}
controller.Instance.PlayControl().Toggle()
}
PlayController.ButtonNext.OnTapped = func() {
controller.PlayNext()
controller.Instance.PlayControl().PlayNext()
}
PlayController.ButtonLrc.OnTapped = func() {
@@ -98,118 +59,197 @@ func registerPlayControllerHandler() {
}
}
if controller.MainPlayer.ObserveProperty("pause", func(property *mpv.EventProperty) {
if property.Data == nil {
PlayController.ButtonSwitch.Icon = theme.MediaPlayIcon()
return
}
if property.Data.(mpv.Node).Value.(bool) {
PlayController.ButtonSwitch.Icon = theme.MediaPlayIcon()
} else {
PlayController.ButtonSwitch.Icon = theme.MediaStopIcon()
}
}) != nil {
if controller.Instance.PlayControl().GetPlayer().ObserveProperty(
model.PlayerPropPause, "gui.play_controller.pause", func(ev *event.Event) {
data := ev.Data.(model.PlayerPropertyUpdateEvent).Value
if data == nil {
PlayController.ButtonSwitch.Icon = theme.MediaPlayIcon()
return
}
if data.(bool) {
PlayController.ButtonSwitch.Icon = theme.MediaPlayIcon()
} else {
PlayController.ButtonSwitch.Icon = theme.MediaPauseIcon()
}
}) != nil {
l().Error("fail to register handler for switch button with property pause")
}
if controller.MainPlayer.ObserveProperty("percent-pos", func(property *mpv.EventProperty) {
if property.Data == nil {
PlayController.Progress.Value = 0
} else {
PlayController.Progress.Value = property.Data.(mpv.Node).Value.(float64) * 10
}
PlayController.Progress.Refresh()
}) != nil {
if controller.Instance.PlayControl().GetPlayer().ObserveProperty(
model.PlayerPropPercentPos, "gui.play_controller.percent_pos", func(ev *event.Event) {
if PlayController.Progress.Dragging {
return
}
data := ev.Data.(model.PlayerPropertyUpdateEvent).Value
if data == nil {
PlayController.Progress.Value = 0
} else {
PlayController.Progress.Value = data.(float64) * 10
}
PlayController.Progress.Refresh()
}) != nil {
l().Error("fail to register handler for progress bar with property percent-pos")
}
if controller.MainPlayer.ObserveProperty("idle-active", func(property *mpv.EventProperty) {
isIdle := property.Data.(mpv.Node).Value.(bool)
l().Debug("receive idle active ", isIdle, " set/reset info")
// todo: @3
if isIdle {
PlayController.Progress.Value = 0
PlayController.Progress.Max = 0
//PlayController.Title.SetText("Title")
//PlayController.Artist.SetText("Artist")
//PlayController.Username.SetText("Username")
//PlayController.SetDefaultCover()
} else {
PlayController.Progress.Max = 1000
}
}) != nil {
if controller.Instance.PlayControl().GetPlayer().ObserveProperty(
model.PlayerPropIdleActive, "gui.play_controller.idle_active", func(ev *event.Event) {
isIdle := ev.Data.(model.PlayerPropertyUpdateEvent).Value.(bool)
l().Debug("receive idle active ", isIdle, " set/reset info")
// todo: @3
if isIdle {
PlayController.Progress.Value = 0
PlayController.Progress.Max = 0
//PlayController.Title.SetText("Title")
//PlayController.Artist.SetText("Artist")
//PlayController.Username.SetText("Username")
//PlayController.SetDefaultCover()
} else {
PlayController.Progress.Max = 1000
}
}) != nil {
l().Error("fail to register handler for progress bar with property idle-active")
}
PlayController.Progress.Max = 0
PlayController.Progress.OnChanged = func(f float64) {
controller.Seek(f/10, false)
PlayController.Progress.OnDragEnd = func(f float64) {
controller.Instance.PlayControl().Seek(f/10, false)
}
if controller.MainPlayer.ObserveProperty("time-pos", func(property *mpv.EventProperty) {
if property.Data == nil {
PlayController.CurrentTime.SetText("0:00")
return
}
PlayController.CurrentTime.SetText(util.FormatTime(int(property.Data.(mpv.Node).Value.(float64))))
}) != nil {
if controller.Instance.PlayControl().GetPlayer().ObserveProperty(
model.PlayerPropTimePos, "gui.play_controller.time_pos", func(ev *event.Event) {
data := ev.Data.(model.PlayerPropertyUpdateEvent).Value
if data == nil {
PlayController.CurrentTime.SetText("0:00")
return
}
PlayController.CurrentTime.SetText(util.FormatTime(int(data.(float64))))
}) != nil {
l().Error("fail to register handler for current time with property time-pos")
}
if controller.MainPlayer.ObserveProperty("duration", func(property *mpv.EventProperty) {
if property.Data == nil {
PlayController.TotalTime.SetText("0:00")
return
}
PlayController.TotalTime.SetText(util.FormatTime(int(property.Data.(mpv.Node).Value.(float64))))
}) != nil {
if controller.Instance.PlayControl().GetPlayer().ObserveProperty(
model.PlayerPropDuration, "gui.play_controller.duration", func(ev *event.Event) {
data := ev.Data.(model.PlayerPropertyUpdateEvent).Value
if data == nil {
PlayController.TotalTime.SetText("0:00")
return
}
PlayController.TotalTime.SetText(util.FormatTime(int(data.(float64))))
}) != nil {
l().Error("fail to register handler for total time with property duration")
}
if controller.MainPlayer.ObserveProperty("volume", func(property *mpv.EventProperty) {
l().Trace("receive volume change event", *property)
if property.Data == nil {
PlayController.Volume.Value = 0
} else {
PlayController.Volume.Value = property.Data.(mpv.Node).Value.(float64)
}
if controller.Instance.PlayControl().GetPlayer().ObserveProperty(
model.PlayerPropVolume, "gui.play_controller.volume", func(ev *event.Event) {
data := ev.Data.(model.PlayerPropertyUpdateEvent).Value
if data == nil {
PlayController.Volume.Value = 0
} else {
PlayController.Volume.Value = data.(float64)
}
PlayController.Volume.Refresh()
}) != nil {
PlayController.Volume.Refresh()
}) != nil {
l().Error("fail to register handler for progress bar with property percent-pos")
}
PlayController.Volume.OnChanged = func(f float64) {
controller.SetVolume(f)
controller.Instance.PlayControl().SetVolume(f)
}
controller.MainPlayer.EventHandler.RegisterA(player.EventPlay, "gui.player.updateinfo", func(event *event.Event) {
controller.Instance.PlayControl().EventManager().RegisterA(model.EventPlay, "gui.player.updateinfo", func(event *event.Event) {
l().Debug("receive EventPlay update player info")
media := event.Data.(player.PlayEvent).Media
media := event.Data.(model.PlayEvent).Media
//PlayController.Title.SetText(
// util.StringNormalize(media.Title, 16, 16))
//PlayController.Artist.SetText(
// util.StringNormalize(media.Artist, 16, 16))
PlayController.Title.SetText(
util.StringNormalize(media.Title, 16, 16))
media.Title)
PlayController.Artist.SetText(
util.StringNormalize(media.Artist, 16, 16))
media.Artist)
PlayController.Username.SetText(media.ToUser().Name)
if media.Cover == "" {
if !media.Cover.Exists() {
PlayController.SetDefaultCover()
} else {
uri, err := storage.ParseURI(media.Cover)
if err != nil {
l().Warn("fail to load parse cover url", media.Cover)
if PlayController.coverLoader != nil {
PlayController.coverLoader()
}
// async update
var ctx context.Context
ctx, PlayController.coverLoader = context.WithCancel(context.Background())
go func() {
img := canvas.NewImageFromURI(uri)
if img == nil {
l().Warn("fail to load parse cover url", media.Cover)
PlayController.SetDefaultCover()
ch := make(chan *canvas.Image)
go func() {
picture, err := gutil.NewImageFromPlayerPicture(media.Cover)
if err != nil {
ch <- nil
return
}
ch <- picture
}()
select {
case <-ctx.Done():
return
case pic := <-ch:
if pic == nil {
PlayController.SetDefaultCover()
return
}
PlayController.Cover.Resource = pic.Resource
PlayController.Cover.Refresh()
}
PlayController.Cover.Resource = img.Resource
PlayController.Cover.Refresh()
}()
}
})
return
}
func createPlayControllerV2() fyne.CanvasObject {
PlayController.Cover = canvas.NewImageFromResource(resource.ImageEmpty)
PlayController.Cover.SetMinSize(fyne.NewSize(128, 128))
PlayController.Cover.FillMode = canvas.ImageFillContain
PlayController.ButtonPrev = widget.NewButtonWithIcon("", theme.MediaSkipPreviousIcon(), func() {})
PlayController.ButtonSwitch = widget.NewButtonWithIcon("", theme.MediaPlayIcon(), func() {})
PlayController.ButtonNext = widget.NewButtonWithIcon("", theme.MediaSkipNextIcon(), func() {})
buttonsBox := container.NewHBox(PlayController.ButtonPrev, PlayController.ButtonSwitch, PlayController.ButtonNext)
PlayController.Volume = widget.NewSlider(0, 100)
PlayController.ButtonLrc = widget.NewButton(i18n.T("gui.player.button.lrc"), func() {})
controls := container.NewPadded(container.NewBorder(nil, nil,
buttonsBox, nil,
container.NewGridWithColumns(
2,
container.NewMax(),
container.NewBorder(nil, nil, widget.NewIcon(theme.VolumeMuteIcon()), PlayController.ButtonLrc,
PlayController.Volume)),
))
PlayController.Progress = component.NewSliderPlus(0, 1000)
PlayController.CurrentTime = widget.NewLabel("0:00")
PlayController.TotalTime = widget.NewLabel("0:00")
progressItem := container.NewBorder(nil, nil,
PlayController.CurrentTime,
PlayController.TotalTime,
PlayController.Progress)
PlayController.Title = widget.NewLabel("Title")
PlayController.Title.Wrapping = fyne.TextTruncate
PlayController.Artist = widget.NewLabel("Artist")
PlayController.Username = widget.NewLabel("Username")
titleUser := component.NewFixedHSplitContainer(
PlayController.Title, PlayController.Artist, 0.32)
titleUser.SetSepThickness(0)
playInfo := container.NewBorder(nil, nil, nil, PlayController.Username,
titleUser)
registerPlayControllerHandler()
return container.NewBorder(nil, nil, container.NewHBox(PlayController.Cover, widget.NewSeparator()), nil,
container.NewVBox(playInfo, progressItem, controls))
}

View File

@@ -1,60 +1,89 @@
package gui
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/controller"
"AynaLivePlayer/event"
"AynaLivePlayer/player"
"AynaLivePlayer/model"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
"strings"
)
func createLyricObj(lyric *model.Lyric) []fyne.CanvasObject {
lrcs := make([]fyne.CanvasObject, len(lyric.Lyrics))
for i := 0; i < len(lrcs); i++ {
lr := widget.NewLabelWithStyle(
lyric.Lyrics[i].Lyric,
fyne.TextAlignCenter, fyne.TextStyle{Italic: true})
//lr.Wrapping = fyne.TextWrapWord
// todo fix fyne bug
lr.Wrapping = fyne.TextWrapBreak
lrcs[i] = lr
}
return lrcs
}
func createLyricWindow() fyne.Window {
// create widgets
w := App.NewWindow("Lyric")
currentLrc := newLabelWithWrapping("", fyne.TextWrapBreak)
currentLrc.Alignment = fyne.TextAlignCenter
lrcs := make([]string, len(controller.CurrentLyric.Lyrics))
for i := 0; i < len(lrcs); i++ {
lrcs[i] = controller.CurrentLyric.Lyrics[i].Lyric
}
fullLrc := widget.NewRichTextWithText(strings.Join(lrcs, "\n\n"))
fullLrc.Scroll = container.ScrollVerticalOnly
fullLrc.Wrapping = fyne.TextWrapWord
fullLrc := container.NewVBox(createLyricObj(controller.Instance.PlayControl().GetLyric().Get())...)
lrcWindow := container.NewVScroll(fullLrc)
prevIndex := 0
w.SetContent(container.NewBorder(nil,
container.NewVBox(widget.NewSeparator(), currentLrc),
nil, nil,
fullLrc))
lrcWindow))
w.Resize(fyne.NewSize(360, 540))
w.CenterOnScreen()
// register handlers
controller.CurrentLyric.Handler.RegisterA(player.EventLyricUpdate, "player.lyric.current_lyric", func(event *event.Event) {
e := event.Data.(player.LyricUpdateEvent)
if e.Lyric == nil {
currentLrc.SetText("")
return
}
currentLrc.SetText(e.Lyric.Lyric)
})
controller.CurrentLyric.Handler.RegisterA(player.EventLyricReload, "player.lyric.new_media", func(event *event.Event) {
e := event.Data.(player.LyricReloadEvent)
lrcs := make([]string, len(e.Lyrics.Lyrics))
for i := 0; i < len(lrcs); i++ {
lrcs[i] = e.Lyrics.Lyrics[i].Lyric
}
fullLrc.Segments[0] = &widget.TextSegment{
Style: widget.RichTextStyleInline,
Text: strings.Join(lrcs, "\n\n"),
}
fullLrc.Refresh()
})
controller.Instance.PlayControl().GetLyric().EventManager().RegisterA(
model.EventLyricUpdate, "player.lyric.current_lyric", func(event *event.Event) {
e := event.Data.(model.LyricUpdateEvent)
if prevIndex >= len(fullLrc.Objects) || e.Lyric.Index >= len(fullLrc.Objects) {
// fix race condition
return
}
if e.Lyric == nil {
currentLrc.SetText("")
return
}
fullLrc.Objects[prevIndex].(*widget.Label).TextStyle.Bold = false
fullLrc.Objects[prevIndex].Refresh()
fullLrc.Objects[e.Lyric.Index].(*widget.Label).TextStyle.Bold = true
fullLrc.Objects[e.Lyric.Index].Refresh()
prevIndex = e.Lyric.Index
currentLrc.SetText(e.Lyric.Now.Lyric)
lrcWindow.Scrolled(&fyne.ScrollEvent{
Scrolled: fyne.Delta{
DX: 0,
DY: lrcWindow.Offset.Y - float32(e.Lyric.Index-2)/float32(e.Lyric.Total)*lrcWindow.Content.Size().Height,
},
})
fullLrc.Refresh()
})
controller.Instance.PlayControl().GetLyric().EventManager().RegisterA(
model.EventLyricReload, "player.lyric.new_media", func(event *event.Event) {
e := event.Data.(model.LyricReloadEvent)
lrcs := make([]string, len(e.Lyrics.Lyrics))
for i := 0; i < len(lrcs); i++ {
lrcs[i] = e.Lyrics.Lyrics[i].Lyric
}
fullLrc.Objects = createLyricObj(e.Lyrics)
//fullLrc.SetText(strings.Join(lrcs, "\n"))
//fullLrc.Segments[0] = &widget.TextSegment{
// Style: widget.RichTextStyleInline,
// Text: strings.Join(lrcs, "\n\n"),
//}
lrcWindow.Refresh()
})
w.SetOnClosed(func() {
controller.CurrentLyric.Handler.Unregister("player.lyric.current_lyric")
controller.CurrentLyric.Handler.Unregister("player.lyric.new_media")
controller.Instance.PlayControl().GetLyric().EventManager().Unregister("player.lyric.current_lyric")
controller.Instance.PlayControl().GetLyric().EventManager().Unregister("player.lyric.new_media")
PlayController.LrcWindowOpen = false
})
return w

View File

@@ -1,15 +1,16 @@
package gui
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/controller"
"AynaLivePlayer/event"
"AynaLivePlayer/i18n"
"AynaLivePlayer/player"
"AynaLivePlayer/model"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"sync"
)
type playlistOperationButton struct {
@@ -25,10 +26,10 @@ func (b *playlistOperationButton) Tapped(e *fyne.PointEvent) {
func newPlaylistOperationButton() *playlistOperationButton {
b := &playlistOperationButton{Index: 0}
deleteItem := fyne.NewMenuItem(i18n.T("gui.player.playlist.op.delete"), func() {
controller.UserPlaylist.Delete(b.Index)
controller.Instance.Playlists().GetCurrent().Delete(b.Index)
})
topItem := fyne.NewMenuItem(i18n.T("gui.player.playlist.op.top"), func() {
controller.UserPlaylist.Move(b.Index, 0)
controller.Instance.Playlists().GetCurrent().Move(b.Index, 0)
})
m := fyne.NewMenu("", deleteItem, topItem)
b.menu = m
@@ -38,18 +39,16 @@ func newPlaylistOperationButton() *playlistOperationButton {
return b
}
type PlaylistContainer struct {
Playlist *player.Playlist
var UserPlaylist = &struct {
Playlist *model.Playlist
List *widget.List
}
var UserPlaylist = &PlaylistContainer{}
mux sync.RWMutex
}{}
func createPlaylist() fyne.CanvasObject {
UserPlaylist.Playlist = controller.UserPlaylist
UserPlaylist.Playlist = controller.Instance.Playlists().GetCurrent().Model().Copy()
UserPlaylist.List = widget.NewList(
func() int {
//debug.PrintStack()
//todo: @4
return UserPlaylist.Playlist.Size()
},
@@ -62,11 +61,11 @@ func createPlaylist() fyne.CanvasObject {
},
func(id widget.ListItemID, object fyne.CanvasObject) {
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[0].(*widget.Label).SetText(
UserPlaylist.Playlist.Playlist[id].Title)
UserPlaylist.Playlist.Medias[id].Title)
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[1].(*widget.Label).SetText(
UserPlaylist.Playlist.Playlist[id].Artist)
UserPlaylist.Playlist.Medias[id].Artist)
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[2].(*widget.Label).SetText(
UserPlaylist.Playlist.Playlist[id].ToUser().Name)
UserPlaylist.Playlist.Medias[id].ToUser().Name)
object.(*fyne.Container).Objects[1].(*widget.Label).SetText(fmt.Sprintf("%d", id))
object.(*fyne.Container).Objects[2].(*playlistOperationButton).Index = id
})
@@ -85,10 +84,12 @@ func createPlaylist() fyne.CanvasObject {
}
func registerPlaylistHandler() {
UserPlaylist.Playlist.Handler.RegisterA(player.EventPlaylistUpdate, "gui.playlist.update", func(event *event.Event) {
// @6 Read lock Playlist when updating free after updating.
UserPlaylist.Playlist.Lock.RLock()
controller.Instance.Playlists().GetCurrent().EventManager().RegisterA(model.EventPlaylistUpdate, "gui.playlist.update", func(event *event.Event) {
// Read lock Playlists when updating free after updating.
l().Tracef("Playlist update event received: %s", event.Data.(model.PlaylistUpdateEvent).Playlist)
UserPlaylist.mux.RLock()
UserPlaylist.Playlist = event.Data.(model.PlaylistUpdateEvent).Playlist
UserPlaylist.List.Refresh()
UserPlaylist.Playlist.Lock.RUnlock()
UserPlaylist.mux.RUnlock()
})
}

View File

@@ -1,9 +1,9 @@
package gui
import (
"AynaLivePlayer/config"
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/controller"
"AynaLivePlayer/i18n"
"AynaLivePlayer/gui/component"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
@@ -13,40 +13,37 @@ import (
"fyne.io/fyne/v2/widget"
)
type PlaylistManagerContainer struct {
type PlaylistsTab struct {
Playlists *widget.List
PlaylistMedia *widget.List
Index int
AddBtn *widget.Button
RemoveBtn *widget.Button
SetAsSystemBtn *widget.Button
RefreshBtn *widget.Button
SetAsSystemBtn *component.AsyncButton
RefreshBtn *component.AsyncButton
CurrentSystemPlaylist *widget.Label
}
func (p *PlaylistManagerContainer) UpdateCurrentSystemPlaylist() {
if config.Player.PlaylistIndex >= len(controller.PlaylistManager) {
p.CurrentSystemPlaylist.SetText(i18n.T("gui.playlist.current.none"))
}
p.CurrentSystemPlaylist.SetText(i18n.T("gui.playlist.current") + controller.PlaylistManager[config.Player.PlaylistIndex].Name)
func (p *PlaylistsTab) UpdateCurrentSystemPlaylist() {
p.CurrentSystemPlaylist.SetText(i18n.T("gui.playlist.current") + controller.Instance.Playlists().GetDefault().Name())
}
var PlaylistManager = &PlaylistManagerContainer{}
var PlaylistManager = &PlaylistsTab{}
func createPlaylists() fyne.CanvasObject {
PlaylistManager.Playlists = widget.NewList(
func() int {
return len(controller.PlaylistManager)
return controller.Instance.Playlists().Size()
},
func() fyne.CanvasObject {
return widget.NewLabel("AAAAAAAAAAAAAAAA")
},
func(id widget.ListItemID, object fyne.CanvasObject) {
object.(*widget.Label).SetText(
controller.PlaylistManager[id].Name)
controller.Instance.Playlists().Get(id).Name())
})
PlaylistManager.AddBtn = widget.NewButton(i18n.T("gui.playlist.button.add"), func() {
providerEntry := widget.NewSelect(config.Provider.Priority, nil)
providerEntry := widget.NewSelect(controller.Instance.Provider().GetPriority(), nil)
idEntry := widget.NewEntry()
dia := dialog.NewCustomConfirm(
i18n.T("gui.playlist.add.title"),
@@ -55,7 +52,7 @@ func createPlaylists() fyne.CanvasObject {
container.NewVBox(
container.New(
layout.NewFormLayout(),
widget.NewLabel(i18n.T("gui.playlist.add.confirm")),
widget.NewLabel(i18n.T("gui.playlist.add.source")),
providerEntry,
widget.NewLabel(i18n.T("gui.playlist.add.id_url")),
idEntry,
@@ -64,7 +61,7 @@ func createPlaylists() fyne.CanvasObject {
),
func(b bool) {
if b && len(providerEntry.Selected) > 0 && len(idEntry.Text) > 0 {
controller.AddPlaylist(providerEntry.Selected, idEntry.Text)
controller.Instance.Playlists().Add(providerEntry.Selected, idEntry.Text)
PlaylistManager.Playlists.Refresh()
PlaylistManager.PlaylistMedia.Refresh()
}
@@ -75,7 +72,7 @@ func createPlaylists() fyne.CanvasObject {
dia.Show()
})
PlaylistManager.RemoveBtn = widget.NewButton(i18n.T("gui.playlist.button.remove"), func() {
controller.RemovePlaylist(PlaylistManager.Index)
controller.Instance.Playlists().Remove(PlaylistManager.Index)
//PlaylistManager.Index = 0
PlaylistManager.Playlists.Select(0)
PlaylistManager.Playlists.Refresh()
@@ -83,6 +80,7 @@ func createPlaylists() fyne.CanvasObject {
})
PlaylistManager.Playlists.OnSelected = func(id widget.ListItemID) {
PlaylistManager.Index = id
PlaylistManager.PlaylistMedia.Refresh()
}
return container.NewHBox(
container.NewBorder(
@@ -95,27 +93,28 @@ func createPlaylists() fyne.CanvasObject {
}
func createPlaylistMedias() fyne.CanvasObject {
PlaylistManager.RefreshBtn = createAsyncButton(
widget.NewButtonWithIcon(i18n.T("gui.playlist.button.refresh"), theme.ViewRefreshIcon(), nil),
PlaylistManager.RefreshBtn = component.NewAsyncButtonWithIcon(
i18n.T("gui.playlist.button.refresh"), theme.ViewRefreshIcon(),
func() {
controller.PreparePlaylistByIndex(PlaylistManager.Index)
showDialogIfError(controller.Instance.Playlists().PreparePlaylistByIndex(PlaylistManager.Index))
PlaylistManager.PlaylistMedia.Refresh()
})
PlaylistManager.SetAsSystemBtn = createAsyncButton(
widget.NewButton(i18n.T("gui.playlist.button.set_as_system"), nil),
PlaylistManager.SetAsSystemBtn = component.NewAsyncButton(
i18n.T("gui.playlist.button.set_as_system"),
func() {
controller.SetSystemPlaylist(PlaylistManager.Index)
showDialogIfError(controller.Instance.Playlists().SetDefault(PlaylistManager.Index))
PlaylistManager.PlaylistMedia.Refresh()
PlaylistManager.UpdateCurrentSystemPlaylist()
})
PlaylistManager.CurrentSystemPlaylist = widget.NewLabel("Current: ")
PlaylistManager.UpdateCurrentSystemPlaylist()
PlaylistManager.PlaylistMedia = widget.NewList(
func() int {
if len(controller.PlaylistManager) == 0 {
if controller.Instance.Playlists().Size() == 0 {
return 0
}
return controller.PlaylistManager[PlaylistManager.Index].Size()
return controller.Instance.Playlists().Get(PlaylistManager.Index).Size()
},
func() fyne.CanvasObject {
return container.NewBorder(nil, nil,
@@ -129,18 +128,19 @@ func createPlaylistMedias() fyne.CanvasObject {
newLabelWithWrapping("artist", fyne.TextTruncate)))
},
func(id widget.ListItemID, object fyne.CanvasObject) {
m := controller.PlaylistManager[PlaylistManager.Index].Playlist[id]
m := controller.Instance.Playlists().Get(PlaylistManager.Index).Get(id).Copy()
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[0].(*widget.Label).SetText(
m.Title)
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[1].(*widget.Label).SetText(
m.Artist)
object.(*fyne.Container).Objects[1].(*widget.Label).SetText(fmt.Sprintf("%d", id))
btns := object.(*fyne.Container).Objects[2].(*fyne.Container).Objects
m.User = controller.SystemUser
btns[0].(*widget.Button).OnTapped = func() {
controller.Play(controller.ToSystemMedia(m))
showDialogIfError(controller.Instance.PlayControl().Play(m))
}
btns[1].(*widget.Button).OnTapped = func() {
controller.UserPlaylist.Push(controller.ToSystemMedia(m))
controller.Instance.Playlists().GetCurrent().Push(m)
}
})
return container.NewBorder(

View File

@@ -1,58 +0,0 @@
package gui
import (
"AynaLivePlayer/config"
"AynaLivePlayer/controller"
"AynaLivePlayer/event"
"AynaLivePlayer/i18n"
"AynaLivePlayer/liveclient"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
type RoomControllerContainer struct {
Input *widget.SelectEntry
ConnectBtn *widget.Button
DisConnectBtn *widget.Button
Status *widget.Label
}
var RoomController = &RoomControllerContainer{}
func createRoomController() fyne.CanvasObject {
RoomController.Input = widget.NewSelectEntry(config.LiveRoom.History)
RoomController.ConnectBtn = widget.NewButton(i18n.T("gui.room.btn.connect"), func() {
RoomController.ConnectBtn.Disable()
controller.SetDanmuClient(RoomController.Input.Text)
if controller.LiveClient == nil {
RoomController.Status.SetText(i18n.T("gui.room.status.failed"))
RoomController.ConnectBtn.Enable()
RoomController.Status.Refresh()
return
}
RoomController.Input.SetOptions(config.LiveRoom.History)
controller.LiveClient.Handler().RegisterA(liveclient.EventStatusChange, "gui.liveclient.status", func(event *event.Event) {
d := event.Data.(liveclient.StatusChangeEvent)
if d.Connected {
RoomController.Status.SetText(i18n.T("gui.room.status.connected"))
} else {
RoomController.Status.SetText(i18n.T("gui.room.status.disconnected"))
}
RoomController.Status.Refresh()
})
go func() {
controller.StartDanmuClient()
RoomController.ConnectBtn.Enable()
}()
})
RoomController.DisConnectBtn = widget.NewButton(i18n.T("gui.room.btn.disconnect"), func() {
controller.ResetDanmuClient()
})
RoomController.Status = widget.NewLabel(i18n.T("gui.room.waiting"))
return container.NewBorder(
nil, nil,
widget.NewLabel(i18n.T("gui.room.id")), container.NewHBox(RoomController.ConnectBtn, RoomController.DisConnectBtn),
container.NewBorder(nil, nil, nil, RoomController.Status, RoomController.Input),
)
}

View File

@@ -1,15 +0,0 @@
package gui
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
)
type RoomLoggerContainer struct {
}
var RoomLogger = &RoomLoggerContainer{}
func createRoomLogger() fyne.CanvasObject {
return widget.NewLabel("广告位招租")
}

View File

@@ -1,54 +1,52 @@
package gui
import (
"AynaLivePlayer/config"
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/controller"
"AynaLivePlayer/i18n"
"AynaLivePlayer/player"
"AynaLivePlayer/gui/component"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
type SearchBarContainer struct {
Input *widget.Entry
Button *widget.Button
UseSource *widget.CheckGroup
Items []*player.Media
}
var SearchBar = &SearchBarContainer{}
var SearchBar = &struct {
Input *component.Entry
Button *component.AsyncButton
UseSource *widget.Select
}{}
func createSearchBar() fyne.CanvasObject {
SearchBar.Input = widget.NewEntry()
SearchBar.Input = component.NewEntry()
SearchBar.Input.SetPlaceHolder(i18n.T("gui.search.placeholder"))
SearchBar.Button = widget.NewButton(i18n.T("gui.search.search"), nil)
SearchBar.Button.OnTapped = createAsyncOnTapped(SearchBar.Button, func() {
keyword := SearchBar.Input.Text
s := make([]string, len(SearchBar.UseSource.Selected))
copy(s, SearchBar.UseSource.Selected)
items := make([]*player.Media, 0)
for _, p := range s {
if r, err := controller.SearchWithProvider(keyword, p); err == nil {
items = append(items, r...)
}
SearchBar.Input.OnKeyUp = func(key *fyne.KeyEvent) {
if key.Name == fyne.KeyReturn {
SearchBar.Button.OnTapped()
}
controller.ApplyUser(items, player.SystemUser)
}
SearchBar.Button = component.NewAsyncButton(i18n.T("gui.search.search"), func() {
keyword := SearchBar.Input.Text
pr := SearchBar.UseSource.Selected
l().Debugf("Search keyword: %s, provider: %s", keyword, pr)
items, err := controller.Instance.Provider().SearchWithProvider(keyword, pr)
if err != nil {
dialog.ShowError(err, MainWindow)
}
controller.ApplyUser(items, controller.SystemUser)
SearchResult.Items = items
SearchResult.List.Refresh()
})
s := make([]string, len(config.Provider.Priority))
copy(s, config.Provider.Priority)
s := make([]string, len(controller.Instance.Provider().GetPriority()))
copy(s, controller.Instance.Provider().GetPriority())
SearchBar.UseSource = widget.NewCheckGroup(s, nil)
SearchBar.UseSource.Horizontal = true
SearchBar.UseSource.SetSelected(s)
SearchBar.UseSource = widget.NewSelect(s, func(s string) {})
if len(s) > 0 {
SearchBar.UseSource.SetSelected(s[0])
}
searchInput := container.NewBorder(
nil, nil, widget.NewLabel(i18n.T("gui.search.search")), SearchBar.Button,
SearchBar.Input)
container.NewBorder(nil, nil, SearchBar.UseSource, nil, SearchBar.Input))
return container.NewVBox(
searchInput,
container.NewHBox(widget.NewLabel(i18n.T("gui.search.filter")), SearchBar.UseSource),
widget.NewSeparator())
}

View File

@@ -1,10 +1,9 @@
package gui
import (
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/controller"
"AynaLivePlayer/i18n"
"AynaLivePlayer/player"
"AynaLivePlayer/provider"
"AynaLivePlayer/model"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
@@ -12,13 +11,13 @@ import (
"fyne.io/fyne/v2/widget"
)
type SearchResultContainer struct {
var SearchResult = &struct {
List *widget.List
Items []*player.Media
Items []*model.Media
}{
Items: []*model.Media{},
}
var SearchResult = &SearchResultContainer{Items: []*player.Media{}}
func createSearchList() fyne.CanvasObject {
SearchResult.List = widget.NewList(
func() int {
@@ -42,14 +41,14 @@ func createSearchList() fyne.CanvasObject {
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[1].(*widget.Label).SetText(
SearchResult.Items[id].Artist)
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[2].(*widget.Label).SetText(
SearchResult.Items[id].Meta.(provider.Meta).Name)
SearchResult.Items[id].Meta.(model.Meta).Name)
object.(*fyne.Container).Objects[1].(*widget.Label).SetText(fmt.Sprintf("%d", id))
btns := object.(*fyne.Container).Objects[2].(*fyne.Container).Objects
btns[0].(*widget.Button).OnTapped = func() {
controller.Play(SearchResult.Items[id])
showDialogIfError(controller.Instance.PlayControl().Play(SearchResult.Items[id]))
}
btns[1].(*widget.Button).OnTapped = func() {
controller.UserPlaylist.Push(SearchResult.Items[id])
controller.Instance.Playlists().GetCurrent().Push(SearchResult.Items[id])
}
})
return container.NewBorder(

42
gui/theme.go Normal file
View File

@@ -0,0 +1,42 @@
package gui
import (
"AynaLivePlayer/resource"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
"image/color"
)
type myTheme struct{}
var _ fyne.Theme = (*myTheme)(nil)
// return bundled font resource
func (*myTheme) Font(s fyne.TextStyle) fyne.Resource {
if s.Monospace {
return resource.FontMSYaHei
}
if s.Bold {
if s.Italic {
//return theme.DefaultTheme().Font(s)
return resource.FontMSYaHeiBold
}
return resource.FontMSYaHei
}
if s.Italic {
return resource.FontMSYaHei
}
return resource.FontMSYaHei
}
func (*myTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
return theme.DefaultTheme().Color(n, v)
}
func (*myTheme) Icon(n fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(n)
}
func (*myTheme) Size(n fyne.ThemeSizeName) float32 {
return theme.DefaultTheme().Size(n)
}

View File

@@ -1,8 +1,9 @@
package liveclient
import (
"AynaLivePlayer/event"
"AynaLivePlayer/logger"
"AynaLivePlayer/common/event"
"AynaLivePlayer/common/logger"
"errors"
"github.com/aynakeya/blivedm"
"github.com/sirupsen/logrus"
"strconv"
@@ -10,18 +11,32 @@ import (
)
type Bilibili struct {
client *blivedm.BLiveWsClient
handlers *event.Handler
client *blivedm.BLiveWsClient
eventManager *event.Manager
roomName string
status bool
}
func init() {
LiveClients["bilibili"] = func(id string) (LiveClient, error) {
room, err := strconv.Atoi(id)
if err != nil {
return nil, errors.New("room id for bilibili should be a integer")
}
return NewBilibili(room), nil
}
}
func NewBilibili(roomId int) LiveClient {
cl := &Bilibili{
client: &blivedm.BLiveWsClient{ShortId: roomId, Account: blivedm.DanmuAccount{UID: 0}, HearbeatInterval: 10 * time.Second},
handlers: event.NewHandler(),
client: &blivedm.BLiveWsClient{ShortId: roomId, Account: blivedm.DanmuAccount{UID: 0}, HearbeatInterval: 10 * time.Second},
eventManager: event.MainManager.NewChildManager(),
roomName: "Unknown",
}
cl.client.OnDisconnect = func(client *blivedm.BLiveWsClient) {
cl.l().Warn("disconnect from websocket connection, maybe try reconnect")
cl.Handler().CallA(EventStatusChange, StatusChangeEvent{Connected: false, Client: cl})
cl.status = false
cl.eventManager.CallA(EventStatusChange, StatusChangeEvent{Connected: false, Client: cl})
}
cl.client.RegHandler(blivedm.CmdDanmaku, cl.handleMsg)
return cl
@@ -31,14 +46,27 @@ func (b *Bilibili) ClientName() string {
return "bilibili"
}
func (b *Bilibili) Handler() *event.Handler {
return b.handlers
func (b *Bilibili) RoomName() string {
return b.roomName
}
func (b *Bilibili) Status() bool {
return b.status
}
func (b *Bilibili) EventManager() *event.Manager {
return b.eventManager
}
func (b *Bilibili) Connect() bool {
if b.status {
return true
}
b.l().Info("Trying Connect Danmu Server")
if b.client.InitRoom() && b.client.ConnectDanmuServer() {
b.Handler().CallA(EventStatusChange, StatusChangeEvent{Connected: true, Client: b})
b.roomName = b.client.RoomInfo.Title
b.status = true
b.eventManager.CallA(EventStatusChange, StatusChangeEvent{Connected: true, Client: b})
b.l().Info("Connect Success")
return true
}
@@ -48,8 +76,11 @@ func (b *Bilibili) Connect() bool {
func (b *Bilibili) Disconnect() bool {
b.l().Info("Disconnect from danmu server")
if b.client == nil {
return true
}
b.client.Disconnect()
b.Handler().CallA(EventStatusChange, StatusChangeEvent{Connected: false, Client: b})
b.eventManager.CallA(EventStatusChange, StatusChangeEvent{Connected: false, Client: b})
return true
}
@@ -82,7 +113,7 @@ func (b *Bilibili) handleMsg(context *blivedm.Context) {
}
b.l().Debug("receive message", dmsg)
go func() {
b.handlers.Call(&event.Event{
b.eventManager.Call(&event.Event{
Id: EventMessageReceive,
Cancelled: false,
Data: &dmsg,

View File

@@ -1,8 +1,8 @@
package liveclient
import (
"AynaLivePlayer/event"
"AynaLivePlayer/logger"
"AynaLivePlayer/common/event"
"AynaLivePlayer/common/logger"
"fmt"
"github.com/sirupsen/logrus"
"testing"
@@ -11,9 +11,9 @@ import (
func TestBilibili_Client(t *testing.T) {
logger.Logger.SetLevel(logrus.DebugLevel)
lc := NewBilibili(7777)
lc := NewBilibili(7777, event.NewManger())
//lc := NewBilibili(8524916587)
lc.Handler().Register(&event.EventHandler{
lc.Handler().Register(&event.Handler{
EventId: EventMessageReceive,
Name: "test.receivemsg",
Handler: func(event *event.Event) {

View File

@@ -1,7 +1,7 @@
package liveclient
import (
"AynaLivePlayer/event"
"AynaLivePlayer/common/event"
)
const (

View File

@@ -1,6 +1,9 @@
package liveclient
import "AynaLivePlayer/event"
import (
"AynaLivePlayer/common/event"
"errors"
)
const MODULE_NAME = "LiveClient"
@@ -21,10 +24,31 @@ type DanmuMessage struct {
User DanmuUser
Message string
}
type LiveClient interface {
ClientName() string
RoomName() string
Connect() bool
Disconnect() bool
Handler() *event.Handler
Status() bool
EventManager() *event.Manager
}
type LiveClientCtor func(id string) (LiveClient, error)
var LiveClients map[string]LiveClientCtor = map[string]LiveClientCtor{}
func GetAllClientNames() []string {
names := make([]string, 0)
for key, _ := range LiveClients {
names = append(names, key)
}
return names
}
func NewLiveClient(clientName, id string) (LiveClient, error) {
ctor, ok := LiveClients[clientName]
if !ok {
return nil, errors.New("no such client")
}
return ctor(id)
}

View File

@@ -1,11 +1,12 @@
package player
package model
import (
"AynaLivePlayer/event"
"AynaLivePlayer/common/event"
)
const (
EventPlay event.EventId = "player.play"
EventPlayed event.EventId = "player.played"
EventPlaylistPreInsert event.EventId = "playlist.insert.pre"
EventPlaylistInsert event.EventId = "playlist.insert.after"
EventPlaylistUpdate event.EventId = "playlist.update"
@@ -13,6 +14,10 @@ const (
EventLyricReload event.EventId = "lyric.reload"
)
func EventPlayerPropertyUpdate(property PlayerProperty) event.EventId {
return event.EventId("player.property.update." + string(property))
}
type PlaylistInsertEvent struct {
Playlist *Playlist
Index int
@@ -20,13 +25,7 @@ type PlaylistInsertEvent struct {
}
type PlaylistUpdateEvent struct {
Playlist *Playlist
}
func newPlaylistUpdateEvent(playlist *Playlist) PlaylistUpdateEvent {
return PlaylistUpdateEvent{
Playlist: playlist,
}
Playlist *Playlist // Playlist is a copy of the playlist
}
type PlayEvent struct {
@@ -36,9 +35,19 @@ type PlayEvent struct {
type LyricUpdateEvent struct {
Lyrics *Lyric
Time float64
Lyric *LyricLine
Lyric *LyricContext
}
type LyricReloadEvent struct {
Lyrics *Lyric
}
type PlayerPropertyUpdateEvent struct {
Property PlayerProperty
Value PlayerPropertyValue
}
type LiveRoomStatusUpdateEvent struct {
RoomTitle string
Status bool
}

17
model/liveroom.go Normal file
View File

@@ -0,0 +1,17 @@
package model
import "fmt"
type LiveRoom struct {
ClientName string
ID string
AutoConnect bool
}
func (r *LiveRoom) String() string {
return fmt.Sprintf("<LiveRooms %s:%s>", r.ClientName, r.ID)
}
func (r *LiveRoom) Title() string {
return fmt.Sprintf("%s-%s", r.ClientName, r.ID)
}

117
model/lyric.go Normal file
View File

@@ -0,0 +1,117 @@
package model
import (
"github.com/spf13/cast"
"regexp"
"sort"
"strings"
)
var timeTagRegex = regexp.MustCompile("\\[[0-9]+:[0-9]+(\\.[0-9]+)?\\]")
type LyricLine struct {
Time float64 // in seconds
Lyric string
Translation string
}
type LyricContext struct {
Now *LyricLine
Index int
Total int
Prev []*LyricLine
Next []*LyricLine
}
type Lyric struct {
Lyrics []*LyricLine
}
func LoadLyric(lyric string) *Lyric {
tmp := make(map[float64]*LyricLine)
times := make([]float64, 0)
for _, line := range strings.Split(lyric, "\n") {
lrc := timeTagRegex.ReplaceAllString(line, "")
if len(lrc) > 0 && lrc[len(lrc)-1] == '\r' {
lrc = lrc[:len(lrc)-1]
}
for _, time := range timeTagRegex.FindAllString(line, -1) {
ts := strings.Split(time[1:len(time)-1], ":")
t := cast.ToFloat64(ts[0])*60 + cast.ToFloat64(ts[1])
times = append(times, t)
tmp[t] = &LyricLine{
Time: t,
Lyric: lrc,
}
}
}
sort.Float64s(times)
lrcs := make([]*LyricLine, len(times))
for index, time := range times {
lrcs[index] = tmp[time]
}
if len(lrcs) == 0 {
lrcs = append(lrcs, &LyricLine{Time: 0, Lyric: ""})
}
lrcs = append(lrcs, &LyricLine{
Time: 99999999999,
Lyric: "",
})
return &Lyric{Lyrics: lrcs}
}
func (l *Lyric) findIndexV1(time float64) int {
for i := 0; i < len(l.Lyrics)-1; i++ {
if l.Lyrics[i].Time <= time && time < l.Lyrics[i+1].Time {
return i
}
}
return -1
}
func (l *Lyric) findIndex(time float64) int {
start := 0
end := len(l.Lyrics) - 1
mid := (start + end) / 2
for start < end {
if l.Lyrics[mid].Time <= time && time < l.Lyrics[mid+1].Time {
return mid
}
if l.Lyrics[mid].Time > time {
end = mid
} else {
start = mid
}
mid = (start + end) / 2
}
return -1
}
func (l *Lyric) Find(time float64) *LyricLine {
idx := l.findIndex(time)
if idx == -1 {
return nil
}
return l.Lyrics[idx]
}
func (l *Lyric) FindContext(time float64, prev int, next int) *LyricContext {
prev = -prev
idx := l.findIndex(time)
if idx == -1 {
return nil
}
if (idx + prev) < 0 {
prev = -idx
}
if (idx + 1 + next) > len(l.Lyrics) {
next = len(l.Lyrics) - idx - 1
}
return &LyricContext{
Now: l.Lyrics[idx],
Index: idx,
Total: len(l.Lyrics),
Prev: l.Lyrics[idx+prev : idx],
Next: l.Lyrics[idx+1 : idx+1+next],
}
}

View File

@@ -1,23 +1,31 @@
package player
package model
import (
"fmt"
"github.com/magiconair/properties/assert"
"testing"
)
var testLyric = "[ti:双截棍]\n[ar:周杰伦]\n[al:范特西]\n[00:03.85]双截棍\n[00:07.14]\n[00:30.13]岩烧店的烟味弥漫隔壁是国术馆\n[00:32.57]店里面的妈妈桑茶道有三段\n[00:34.61]教拳脚武术的老板练铁沙掌耍杨家枪\n[00:37.34]硬底子功夫最擅长还会金钟罩铁步衫\n[00:39.67]他们儿子我习惯从小就耳濡目染\n[00:41.96]什么刀枪跟棍棒我都耍的有模有样\n[00:44.22]什么兵器最喜欢双截棍柔中带刚\n[00:46.73]想要去河南嵩山学少林跟武当\n[00:49.24]干什么(客)干什么(客)呼吸吐纳心自在\n[00:51.28]干什么(客)干什么(客)气沉丹田手心开\n[00:53.44]干什么(客)干什么(客)日行千里系沙袋\n[00:56.13]飞檐走壁莫奇怪去去就来\n[00:58.35]一个马步向前一记左钩拳右钩拳\n[01:01.26]一句惹毛我的人有危险一再重演\n[01:04.02]一根我不抽的菸一放好多年它一直在身边\n[01:07.28]干什么(客)干什么(客)我打开任督二脉\n[01:10.27]干什么(客)干什么(客)东亚病夫的招牌\n[01:12.75]干什么(客)干什么(客)已被我一脚踢开\n[02:32.62][01:54.69][01:15.40]快使用双截棍哼哼哈兮\n[02:34.52][01:56.63][01:18.40]快使用双截棍哼哼哈兮\n[02:36.88][01:58.98][01:20.71]习武之人切记仁者无敌\n[02:39.45][02:01.66][01:23.27]是谁在练太极风生水起\n[02:41.97][02:03.93][01:25.74]快使用双截棍哼哼哈兮\n[02:44.42][02:06.11][01:27.75]快使用双截棍哼哼哈兮\n[02:47.01][02:08.54][01:30.13]如果我有轻功飞檐走壁\n[02:49.36][02:11.03][01:32.67]为人耿直不屈一身正气\n[02:53.81]快使用双截棍哼\n[02:56.30]我用手刀防御哼\n[02:58.52]漂亮的回旋踢\n[02:59.52]"
func TestLyric(t *testing.T) {
lryic := NewLyric(testLyric)
lryic := LoadLyric(testLyric)
for _, lrc := range lryic.Lyrics {
fmt.Println(lrc)
}
}
func TestLyricFind(t *testing.T) {
lryic := NewLyric(testLyric)
lryic := LoadLyric(testLyric)
fmt.Println(lryic.Find(90.4))
for _, l := range lryic.FindContext(90.4, -2, 2) {
for _, l := range lryic.FindContext(90.4, -2, 2).Next {
fmt.Println(l)
}
}
func TestLyricFindV2(t *testing.T) {
lryic := LoadLyric(testLyric)
for i := 0.0; i < 170; i += 0.01 {
assert.Equal(t, lryic.FindV1(i), lryic.Find(i))
}
}

View File

@@ -1,14 +1,23 @@
package player
package model
import (
"AynaLivePlayer/liveclient"
"github.com/jinzhu/copier"
)
type Picture struct {
Url string
Data []byte
}
func (p Picture) Exists() bool {
return p.Url != "" || p.Data != nil
}
type Media struct {
Title string
Artist string
Cover string
Cover Picture
Album string
Lyric string
Url string

View File

@@ -1,4 +1,4 @@
package player
package model
import (
"fmt"

18
model/player.go Normal file
View File

@@ -0,0 +1,18 @@
package model
type AudioDevice struct {
Name string
Description string
}
type PlayerPropertyValue any
type PlayerProperty string
const (
PlayerPropIdleActive PlayerProperty = "idle-active"
PlayerPropTimePos PlayerProperty = "time-pos"
PlayerPropDuration PlayerProperty = "duration"
PlayerPropPercentPos PlayerProperty = "percent-pos"
PlayerPropPause PlayerProperty = "pause"
PlayerPropVolume PlayerProperty = "volume"
)

36
model/playlist.go Normal file
View File

@@ -0,0 +1,36 @@
package model
import "fmt"
type PlaylistMode int
const (
PlaylistModeNormal PlaylistMode = iota
PlaylistModeRandom
)
type Playlist struct {
Name string
Medias []*Media
Mode PlaylistMode
Meta Meta
}
func (p Playlist) String() string {
return fmt.Sprintf("<Playlist %s>", p.Name)
}
func (p *Playlist) Size() int {
return len(p.Medias)
}
func (p *Playlist) Copy() *Playlist {
medias := make([]*Media, len(p.Medias))
copy(medias, p.Medias)
return &Playlist{
Name: p.Name,
Medias: medias,
Mode: p.Mode,
Meta: p.Meta,
}
}

6
model/provider.go Normal file
View File

@@ -0,0 +1,6 @@
package model
type Meta struct {
Name string
Id string
}

5
model/user.go Normal file
View File

@@ -0,0 +1,5 @@
package model
type User struct {
Name string
}

Binary file not shown.

View File

@@ -1,98 +0,0 @@
package player
import (
"AynaLivePlayer/event"
"github.com/spf13/cast"
"regexp"
"sort"
"strings"
)
var timeTagRegex = regexp.MustCompile("\\[[0-9]+:[0-9]+(\\.[0-9]+)?\\]")
type LyricLine struct {
Time float64 // in seconds
Lyric string
}
type Lyric struct {
Lyrics []LyricLine
Handler *event.Handler
prev float64
}
func (l *Lyric) Reload(lyric string) {
tmp := make(map[float64]LyricLine)
times := make([]float64, 0)
for _, line := range strings.Split(lyric, "\n") {
lrc := timeTagRegex.ReplaceAllString(line, "")
for _, time := range timeTagRegex.FindAllString(line, -1) {
ts := strings.Split(time[1:len(time)-1], ":")
t := cast.ToFloat64(ts[0])*60 + cast.ToFloat64(ts[1])
times = append(times, t)
tmp[t] = LyricLine{
Time: t,
Lyric: lrc,
}
}
}
sort.Float64s(times)
lrcs := make([]LyricLine, len(times))
for index, time := range times {
lrcs[index] = tmp[time]
}
lrcs = append(lrcs, LyricLine{
Time: 99999999999,
Lyric: "",
})
l.Lyrics = lrcs
l.Handler.CallA(EventLyricReload, LyricReloadEvent{Lyrics: l})
return
}
func (l *Lyric) Update(time float64) {
lrc := l.Find(time)
if lrc == nil {
return
}
if l.prev == lrc.Time {
return
}
l.prev = lrc.Time
l.Handler.CallA(EventLyricUpdate, LyricUpdateEvent{
Lyrics: l,
Time: time,
Lyric: lrc,
})
return
}
func (l *Lyric) Find(time float64) *LyricLine {
for i := 0; i < len(l.Lyrics)-1; i++ {
if l.Lyrics[i].Time <= time && time < l.Lyrics[i+1].Time {
return &l.Lyrics[i]
}
}
return nil
}
func (l *Lyric) FindContext(time float64, prev int, next int) []LyricLine {
for i := 0; i < len(l.Lyrics)-1; i++ {
if l.Lyrics[i].Time <= time && time < l.Lyrics[i+1].Time {
if (i + prev) < 0 {
prev = -i
}
if (i + 1 + next) > len(l.Lyrics) {
next = len(l.Lyrics) - i - 1
}
return l.Lyrics[i+prev : i+1+next]
}
}
return nil
}
func NewLyric(lyric string) *Lyric {
l := &Lyric{Handler: event.NewHandler(), prev: -1}
l.Reload(lyric)
return l
}

View File

@@ -1,186 +1,24 @@
package player
import (
"AynaLivePlayer/event"
"AynaLivePlayer/logger"
"AynaLivePlayer/util"
"github.com/aynakeya/go-mpv"
"github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"AynaLivePlayer/common/event"
"AynaLivePlayer/common/logger"
"AynaLivePlayer/model"
)
const MODULE_PLAYER = "Player.Player"
var lg = logger.Logger.WithField("Module", "PlayControl")
type PropertyHandlerFunc func(property *mpv.EventProperty)
type Player struct {
running bool
libmpv *mpv.Mpv
Playing *Media
PropertyHandler map[string][]PropertyHandlerFunc
EventHandler *event.Handler
}
func NewPlayer() *Player {
player := &Player{
running: true,
libmpv: mpv.Create(),
PropertyHandler: make(map[string][]PropertyHandlerFunc),
EventHandler: event.NewHandler(),
}
err := player.libmpv.Initialize()
if err != nil {
player.l().Error("initialize libmpv failed")
return nil
}
player.libmpv.SetOptionString("vo", "null")
player.l().Info("initialize libmpv success")
return player
}
func (p *Player) Start() {
p.l().Info("starting mpv player")
go func() {
for p.running {
e := p.libmpv.WaitEvent(1)
if e == nil {
p.l().Warn("event loop got nil event")
}
p.l().Trace("new event", e)
if e.EventId == mpv.EVENT_PROPERTY_CHANGE {
property := e.Property()
p.l().Trace("receive property change event", property)
for _, handler := range p.PropertyHandler[property.Name] {
// todo: @3
go handler(&property)
}
}
if e.EventId == mpv.EVENT_SHUTDOWN {
p.l().Info("libmpv shutdown")
p.Stop()
}
}
}()
}
func (p *Player) Stop() {
p.l().Info("stopping mpv player")
p.running = false
p.libmpv.TerminateDestroy()
}
func (p *Player) l() *logrus.Entry {
return logger.Logger.WithField("Module", MODULE_PLAYER)
}
func (p *Player) Play(media *Media) error {
p.l().Infof("Play media %s", media.Url)
p.l().Trace("set user-agent for mpv player")
if val, ok := media.Header["user-agent"]; ok {
err := p.libmpv.SetPropertyString("user-agent", val)
if err != nil {
p.l().Warn("set player user-agent failed", err)
return err
}
}
p.l().Trace("set referrer for mpv player")
if val, ok := media.Header["referrer"]; ok {
err := p.libmpv.SetPropertyString("referrer", val)
if err != nil {
p.l().Warn("set player referrer failed", err)
return err
}
}
p.l().Debugf("mpv command load file %s %s", media.Title, media.Url)
if err := p.libmpv.Command([]string{"loadfile", media.Url}); err != nil {
p.l().Warn("mpv load media failed", media)
return err
}
p.Playing = media
p.EventHandler.CallA(EventPlay, PlayEvent{Media: media})
return nil
}
func (p *Player) IsPaused() bool {
property, err := p.libmpv.GetProperty("pause", mpv.FORMAT_FLAG)
if err != nil {
p.l().Warn("get property pause failed", err)
return false
}
return property.(bool)
}
func (p *Player) Pause() error {
p.l().Tracef("pause")
return p.libmpv.SetProperty("pause", mpv.FORMAT_FLAG, true)
}
func (p *Player) Unpause() error {
p.l().Tracef("unpause")
return p.libmpv.SetProperty("pause", mpv.FORMAT_FLAG, false)
}
// SetVolume set mpv volume, from 0.0 - 100.0
func (p *Player) SetVolume(volume float64) error {
p.l().Tracef("set volume to %f", volume)
return p.libmpv.SetProperty("volume", mpv.FORMAT_DOUBLE, volume)
}
func (p *Player) IsIdle() bool {
property, err := p.libmpv.GetProperty("idle-active", mpv.FORMAT_FLAG)
if err != nil {
p.l().Warn("get property idle-active failed", err)
return false
}
return property.(bool)
}
// Seek change position for current file
// absolute = true : position is the time in second
// absolute = false: position is in percentage eg 0.1 0.2
func (p *Player) Seek(position float64, absolute bool) error {
p.l().Tracef("seek to %f (absolute=%t)", position, absolute)
if absolute {
return p.libmpv.SetProperty("time-pos", mpv.FORMAT_DOUBLE, position)
} else {
return p.libmpv.SetProperty("percent-pos", mpv.FORMAT_DOUBLE, position)
}
}
func (p *Player) ObserveProperty(property string, handler ...PropertyHandlerFunc) error {
p.l().Trace("add property observer for mpv")
p.PropertyHandler[property] = append(p.PropertyHandler[property], handler...)
if len(p.PropertyHandler[property]) == 1 {
return p.libmpv.ObserveProperty(util.Hash64(property), property, mpv.FORMAT_NODE)
}
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)
type IPlayer interface {
Start()
Stop()
Play(media *model.Media) error
IsPaused() bool
Pause() error
Unpause() error
SetVolume(volume float64) error
IsIdle() bool
Seek(position float64, absolute bool) error
ObserveProperty(property model.PlayerProperty, name string, handler event.HandlerFunc) error
GetAudioDeviceList() ([]model.AudioDevice, error)
SetAudioDevice(device string) error
}

204
player/player_mpv.go Normal file
View File

@@ -0,0 +1,204 @@
package player
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/common/util"
"AynaLivePlayer/model"
"github.com/aynakeya/go-mpv"
"github.com/tidwall/gjson"
)
var mpvPropertyMap = map[model.PlayerProperty]string{
model.PlayerPropDuration: "duration",
model.PlayerPropTimePos: "time-pos",
model.PlayerPropIdleActive: "idle-active",
model.PlayerPropPercentPos: "percent-pos",
model.PlayerPropPause: "pause",
model.PlayerPropVolume: "volume",
}
var mpvPropertyMapInv = map[string]model.PlayerProperty{
"duration": model.PlayerPropDuration,
"time-pos": model.PlayerPropTimePos,
"idle-active": model.PlayerPropIdleActive,
"percent-pos": model.PlayerPropPercentPos,
"pause": model.PlayerPropPause,
"volume": model.PlayerPropVolume,
}
type MpvPlayer struct {
running bool
libmpv *mpv.Mpv
Playing *model.Media
propertyWatchedFlag map[model.PlayerProperty]int
eventManager *event.Manager
}
func NewMpvPlayer() IPlayer {
player := &MpvPlayer{
running: true,
libmpv: mpv.Create(),
propertyWatchedFlag: make(map[model.PlayerProperty]int),
eventManager: event.MainManager.NewChildManager(),
}
err := player.libmpv.Initialize()
if err != nil {
lg.Error("[MPV PlayControl] initialize libmpv failed")
return nil
}
_ = player.libmpv.SetOptionString("vo", "null")
lg.Info("[MPV PlayControl] initialize libmpv success")
player.Start()
return player
}
func (p *MpvPlayer) Start() {
lg.Info("[MPV PlayControl] starting mpv player")
go func() {
for p.running {
e := p.libmpv.WaitEvent(1)
if e == nil {
lg.Warn("[MPV PlayControl] event loop got nil event")
}
lg.Trace("[MPV PlayControl] new event", e)
if e.EventId == mpv.EVENT_PROPERTY_CHANGE {
eventProperty := e.Property()
property, ok := mpvPropertyMapInv[eventProperty.Name]
if !ok {
continue
}
var value interface{} = nil
if eventProperty.Data != nil {
value = eventProperty.Data.(mpv.Node).Value
}
p.eventManager.CallA(
model.EventPlayerPropertyUpdate(property),
model.PlayerPropertyUpdateEvent{
Property: property,
Value: value,
})
}
if e.EventId == mpv.EVENT_SHUTDOWN {
lg.Info("[MPV PlayControl] libmpv shutdown")
p.Stop()
}
}
}()
}
func (p *MpvPlayer) Stop() {
lg.Info("[MPV PlayControl] stopping mpv player")
p.running = false
p.libmpv.TerminateDestroy()
}
func (p *MpvPlayer) Play(media *model.Media) error {
lg.Infof("[MPV PlayControl] Play media %s", media.Url)
if val, ok := media.Header["User-Agent"]; ok {
lg.Debug("[MPV PlayControl] set user-agent for mpv player")
err := p.libmpv.SetPropertyString("user-agent", val)
if err != nil {
lg.Warn("[MPV PlayControl] set player user-agent failed", err)
return err
}
}
if val, ok := media.Header["Referer"]; ok {
lg.Debug("[MPV PlayControl] set referrer for mpv player")
err := p.libmpv.SetPropertyString("referrer", val)
if err != nil {
lg.Warn("[MPV PlayControl] set player referrer failed", err)
return err
}
}
lg.Debugf("mpv command load file %s %s", media.Title, media.Url)
if err := p.libmpv.Command([]string{"loadfile", media.Url}); err != nil {
lg.Warn("[MPV PlayControl] mpv load media failed", media)
return err
}
p.Playing = media
return nil
}
func (p *MpvPlayer) IsPaused() bool {
property, err := p.libmpv.GetProperty("pause", mpv.FORMAT_FLAG)
if err != nil {
lg.Warn("[MPV PlayControl] get property pause failed", err)
return false
}
return property.(bool)
}
func (p *MpvPlayer) Pause() error {
lg.Tracef("[MPV PlayControl] pause")
return p.libmpv.SetProperty("pause", mpv.FORMAT_FLAG, true)
}
func (p *MpvPlayer) Unpause() error {
lg.Tracef("[MPV PlayControl] unpause")
return p.libmpv.SetProperty("pause", mpv.FORMAT_FLAG, false)
}
// SetVolume set mpv volume, from 0.0 - 100.0
func (p *MpvPlayer) SetVolume(volume float64) error {
lg.Tracef("[MPV PlayControl] set volume to %f", volume)
return p.libmpv.SetProperty("volume", mpv.FORMAT_DOUBLE, volume)
}
func (p *MpvPlayer) IsIdle() bool {
property, err := p.libmpv.GetProperty("idle-active", mpv.FORMAT_FLAG)
if err != nil {
lg.Warn("[MPV PlayControl] get property idle-active failed", err)
return false
}
return property.(bool)
}
// Seek change position for current file
// absolute = true : position is the time in second
// absolute = false: position is in percentage eg 0.1 0.2
func (p *MpvPlayer) Seek(position float64, absolute bool) error {
lg.Tracef("[MPV PlayControl] seek to %f (absolute=%t)", position, absolute)
if absolute {
return p.libmpv.SetProperty("time-pos", mpv.FORMAT_DOUBLE, position)
} else {
return p.libmpv.SetProperty("percent-pos", mpv.FORMAT_DOUBLE, position)
}
}
func (p *MpvPlayer) ObserveProperty(property model.PlayerProperty, name string, handler event.HandlerFunc) error {
lg.Trace("[MPV PlayControl] add property observer for mpv")
p.eventManager.RegisterA(
model.EventPlayerPropertyUpdate(property),
name, handler)
if _, ok := p.propertyWatchedFlag[property]; !ok {
p.propertyWatchedFlag[property] = 1
return p.libmpv.ObserveProperty(util.Hash64(mpvPropertyMap[property]), mpvPropertyMap[property], mpv.FORMAT_NODE)
}
return nil
}
// GetAudioDeviceList get output device for mpv
// return format is []AudioDevice
func (p *MpvPlayer) GetAudioDeviceList() ([]model.AudioDevice, error) {
lg.Trace("[MPV PlayControl] 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([]model.AudioDevice, 0)
gjson.Parse(property.(string)).ForEach(func(key, value gjson.Result) bool {
dl = append(dl, model.AudioDevice{
Name: value.Get("name").String(),
Description: value.Get("description").String(),
})
return true
})
return dl, nil
}
func (p *MpvPlayer) SetAudioDevice(device string) error {
lg.Tracef("[MPV PlayControl] set audio device %s for mpv", device)
return p.libmpv.SetPropertyString("audio-device", device)
}

View File

@@ -1,6 +1,7 @@
package player
import (
"AynaLivePlayer/model"
"fmt"
"github.com/aynakeya/go-mpv"
"testing"
@@ -18,7 +19,7 @@ func TestPlayer(t *testing.T) {
player.ObserveProperty("percent-pos", func(property *mpv.EventProperty) {
fmt.Println(2, property.Data)
})
player.Play(&Media{
player.Play(&model.Media{
Url: "https://ia600809.us.archive.org/19/items/VillagePeopleYMCAOFFICIALMusicVideo1978/Village%20People%20-%20YMCA%20OFFICIAL%20Music%20Video%201978.mp4",
})
time.Sleep(time.Second * 15)

Some files were not shown because too many files have changed in this diff Show More