This commit is contained in:
aynakeya
2024-04-10 00:42:33 -07:00
parent 8d73a3c284
commit f926f15606
145 changed files with 2852 additions and 4296 deletions

6
.gitignore vendored
View File

@@ -1,3 +1,7 @@
.idea
assets/webinfo/*.html
assets/webinfo/assets
assets/webinfo/assets
music
liverooms.json
playlists.json
textinfo

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "pkg/miaosic"]
path = pkg/miaosic
url = git@github.com:AynaLivePlayer/miaosic.git
[submodule "pkg/liveroom-sdk"]
path = pkg/liveroom-sdk
url = git@github.com:AynaLivePlayer/liveroom-sdk.git

View File

@@ -1,24 +0,0 @@
package adapters
import (
"AynaLivePlayer/adapters/liveclient"
"AynaLivePlayer/adapters/logger"
"AynaLivePlayer/adapters/player"
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/adapter"
)
var Logger = &logger.LoggerFactory{}
var LiveClient = &liveclient.LiveClientFactory{
LiveClients: map[string]adapter.LiveClientCtor{
"bilibili": liveclient.BilibiliCtor,
},
EventManager: event.MainManager,
Logger: &logger.EmptyLogger{},
}
var Player = &player.PlayerFactory{
EventManager: event.MainManager,
Logger: &logger.EmptyLogger{},
}

View File

@@ -1,141 +0,0 @@
package liveclient
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"encoding/json"
"errors"
"github.com/aynakeya/blivedm"
"github.com/go-resty/resty/v2"
"github.com/tidwall/gjson"
"strconv"
"time"
)
type Bilibili struct {
client *blivedm.BLiveWsClient
eventManager *event.Manager
roomName string
status bool
log adapter.ILogger
}
func BilibiliCtor(id string, em *event.Manager, log adapter.ILogger) (adapter.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, em, log), nil
}
func NewBilibili(roomId int, em *event.Manager, log adapter.ILogger) adapter.LiveClient {
cl := &Bilibili{
client: &blivedm.BLiveWsClient{ShortId: roomId, Account: blivedm.DanmuAccount{UID: 0}, HearbeatInterval: 10 * time.Second},
eventManager: em,
roomName: "",
log: log,
}
cl.client.OnDisconnect = func(client *blivedm.BLiveWsClient) {
cl.log.Warn("[Bilibili LiveChatSDK] disconnect from websocket connection, maybe try reconnect")
cl.status = false
cl.eventManager.CallA(events.LiveRoomStatusChange, events.StatusChangeEvent{Connected: false, Client: cl})
}
cl.client.RegHandler(blivedm.CmdDanmaku, cl.handleMsg)
return cl
}
func (b *Bilibili) ClientName() string {
return "bilibili"
}
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.log.Info("[Bilibili LiveChatSDK] Trying Connect Danmu Server")
if !b.client.GetRoomInfo() {
b.log.Info("[Bilibili LiveChatSDK] Connect Failed")
return false
}
resp, err := resty.New().R().
SetQueryParam("room_id", strconv.Itoa(b.client.RoomId)).
Get("https://scene.aynakeya.com:3000/bilisrv/dminfo")
if err != nil {
b.log.Info("[Bilibili LiveChatSDK] Connect Failed")
return false
}
gjresult := gjson.Parse(resp.String())
if gjresult.Get("code").Int() != 0 {
b.log.Info("[Bilibili LiveChatSDK] Connect Failed")
return false
}
b.client.DanmuInfo = blivedm.DanmuInfoData{
Token: gjresult.Get("data.token").String(),
}
b.client.Account.UID = int(gjresult.Get("data.uid").Int())
if err := json.Unmarshal([]byte(gjresult.Get("data.host_list").String()), &b.client.DanmuInfo.HostList); err != nil {
return false
}
if b.client.ConnectDanmuServer() {
b.roomName = b.client.RoomInfo.Title
b.status = true
b.eventManager.CallA(events.LiveRoomStatusChange, events.StatusChangeEvent{Connected: true, Client: b})
b.log.Info("[Bilibili LiveChatSDK] Connect Success")
return true
}
b.log.Info("[Bilibili LiveChatSDK] Connect Failed")
return false
}
func (b *Bilibili) Disconnect() bool {
b.log.Info("[Bilibili LiveChatSDK] Disconnect from danmu server")
if b.client == nil {
return true
}
b.client.Disconnect()
b.eventManager.CallA(events.LiveRoomStatusChange, events.StatusChangeEvent{Connected: false, Client: b})
return true
}
func (b *Bilibili) handleMsg(context *blivedm.Context) {
msg, ok := context.ToDanmakuMessage()
if !ok {
b.log.Warn("[Bilibili LiveChatSDK] handle message failed, can't convert context to danmu message")
return
}
dmsg := model.DanmuMessage{
User: model.DanmuUser{
Uid: strconv.FormatInt(msg.Uid, 10),
Username: msg.Uname,
Medal: model.UserMedal{
Name: msg.MedalName,
Level: int(msg.MedalLevel),
},
Admin: msg.Admin,
Privilege: int(msg.PrivilegeType),
},
Message: msg.Msg,
}
b.log.Debug("[Bilibili LiveChatSDK] receive message", dmsg)
go func() {
b.eventManager.Call(&event.Event{
Id: events.LiveRoomMessageReceive,
Cancelled: false,
Data: &dmsg,
})
}()
}

View File

@@ -1,29 +0,0 @@
package liveclient
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/adapter"
"errors"
)
type LiveClientFactory struct {
LiveClients map[string]adapter.LiveClientCtor
EventManager *event.Manager
Logger adapter.ILogger
}
func (f *LiveClientFactory) GetAllClientNames() []string {
names := make([]string, 0)
for key, _ := range f.LiveClients {
names = append(names, key)
}
return names
}
func (f *LiveClientFactory) NewLiveClient(clientName, id string) (adapter.LiveClient, error) {
ctor, ok := f.LiveClients[clientName]
if !ok {
return nil, errors.New("no such client")
}
return ctor(id, f.EventManager.NewChildManager(), f.Logger)
}

View File

@@ -1,48 +0,0 @@
package logger
import (
"AynaLivePlayer/core/adapter"
)
type EmptyLogger struct {
}
func (e EmptyLogger) Debug(args ...interface{}) {
return
}
func (e EmptyLogger) Debugf(format string, args ...interface{}) {
return
}
func (e EmptyLogger) Info(args ...interface{}) {
return
}
func (e EmptyLogger) Infof(format string, args ...interface{}) {
return
}
func (e EmptyLogger) Warn(args ...interface{}) {
return
}
func (e EmptyLogger) Warnf(format string, args ...interface{}) {
return
}
func (e EmptyLogger) Error(args ...interface{}) {
return
}
func (e EmptyLogger) Errorf(format string, args ...interface{}) {
return
}
func (e EmptyLogger) WithModule(prefix string) adapter.ILogger {
return &EmptyLogger{}
}
func (e EmptyLogger) SetLogLevel(level adapter.LogLevel) {
return
}

View File

@@ -1,13 +0,0 @@
package logger
import (
"AynaLivePlayer/core/adapter"
)
type LoggerFactory struct {
LiveClients map[string]adapter.LiveClientCtor
}
func (f *LoggerFactory) NewLogrus(filename string, redirectStderr bool, maxSize int64) adapter.ILogger {
return NewLogrusLogger(filename, redirectStderr, maxSize)
}

View File

@@ -1,73 +0,0 @@
package logger
import (
"AynaLivePlayer/core/adapter"
nested "github.com/antonfisher/nested-logrus-formatter"
"github.com/sirupsen/logrus"
"github.com/virtuald/go-paniclog"
"io"
"os"
)
type LogrusLogger struct {
*logrus.Entry
module string
}
func (l *LogrusLogger) SetLogLevel(level adapter.LogLevel) {
switch level {
case adapter.LogLevelDebug:
l.Logger.SetLevel(logrus.DebugLevel)
case adapter.LogLevelInfo:
l.Logger.SetLevel(logrus.InfoLevel)
case adapter.LogLevelWarn:
l.Logger.SetLevel(logrus.WarnLevel)
case adapter.LogLevelError:
l.Logger.SetLevel(logrus.ErrorLevel)
default:
l.Logger.SetLevel(logrus.InfoLevel)
}
}
func NewLogrusLogger(fileName string, redirectStderr bool, maxSize int64) *LogrusLogger {
l := logrus.New()
l.SetFormatter(
&nested.Formatter{
FieldsOrder: []string{"Module"},
HideKeys: true,
NoColors: true,
})
var file *os.File
var err error
if fileName != "" {
fi, err := os.Stat(fileName)
if err != nil {
file, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY, 0666)
} else if fi.Size() > maxSize*1024*1024 {
file, err = os.OpenFile(fileName, os.O_TRUNC|os.O_WRONLY, 0666)
} else {
file, err = os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND, 0666)
}
if err == nil {
l.Out = io.MultiWriter(file, os.Stdout)
} else {
l.Info("Failed to log to file, using default stdout")
}
}
if redirectStderr && file != nil {
l.Info("panic/stderr redirect to log file")
if _, err = paniclog.RedirectStderr(file); err != nil {
l.Infof("Failed to redirect stderr to to file: %s", err)
}
}
return &LogrusLogger{
Entry: logrus.NewEntry(l),
}
}
func (l *LogrusLogger) WithModule(prefix string) adapter.ILogger {
return &LogrusLogger{
Entry: l.Entry.WithField("Module", prefix),
module: prefix,
}
}

View File

@@ -1,15 +0,0 @@
package player
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/adapter"
)
type PlayerFactory struct {
EventManager *event.Manager
Logger adapter.ILogger
}
func (f *PlayerFactory) NewMPV() adapter.IPlayer {
return NewMpvPlayer(f.EventManager, f.Logger)
}

View File

@@ -1,226 +0,0 @@
package player
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"fmt"
"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
log adapter.ILogger
}
func NewMpvPlayer(em *event.Manager, log adapter.ILogger) adapter.IPlayer {
player := &MpvPlayer{
running: true,
libmpv: mpv.Create(),
propertyWatchedFlag: make(map[model.PlayerProperty]int),
eventManager: em,
log: log,
}
err := player.libmpv.Initialize()
if err != nil {
player.log.Error("[MPV Player] initialize libmpv failed")
return nil
}
_ = player.libmpv.SetOptionString("vo", "null")
player.log.Info("[MPV Player] initialize libmpv success")
_ = player.ObserveProperty(model.PlayerPropIdleActive, "player.setplaying", func(evnt *event.Event) {
isIdle := evnt.Data.(events.PlayerPropertyUpdateEvent).Value.(bool)
if isIdle {
player.Playing = nil
}
})
player.Start()
return player
}
func (p *MpvPlayer) Start() {
p.log.Info("[MPV Player] starting mpv player")
go func() {
for p.running {
e := p.libmpv.WaitEvent(1)
if e == nil {
p.log.Warn("[MPV Player] event loop got nil event")
}
//p.log.Trace("[MPV Player] 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(
events.EventPlayerPropertyUpdate(property),
events.PlayerPropertyUpdateEvent{
Property: property,
Value: value,
})
}
if e.EventId == mpv.EVENT_SHUTDOWN {
p.log.Info("[MPV Player] libmpv shutdown")
p.Stop()
}
}
}()
return
}
func (p *MpvPlayer) Stop() {
p.log.Info("[MPV Player] stopping mpv player")
p.running = false
p.libmpv.TerminateDestroy()
}
func (p *MpvPlayer) GetPlaying() *model.Media {
return p.Playing
}
func (p *MpvPlayer) SetWindowHandle(handle uintptr) error {
p.log.Infof("[MPV Player] set window handle %d", handle)
_ = p.libmpv.SetOptionString("wid", fmt.Sprintf("%d", handle))
return p.libmpv.SetOptionString("vo", "gpu")
}
func (p *MpvPlayer) Play(media *model.Media) error {
p.log.Infof("[MPV Player] Play media %s", media.Url)
if val, ok := media.Header["User-Agent"]; ok {
p.log.Debug("[MPV PlayControl] set user-agent for mpv player")
err := p.libmpv.SetPropertyString("user-agent", val)
if err != nil {
p.log.Warn("[MPV PlayControl] set player user-agent failed", err)
return err
}
}
if val, ok := media.Header["Referer"]; ok {
p.log.Debug("[MPV PlayControl] set referrer for mpv player")
err := p.libmpv.SetPropertyString("referrer", val)
if err != nil {
p.log.Warn("[MPV PlayControl] set player referrer failed", err)
return err
}
}
p.log.Debugf("mpv command load file %s %s", media.Title, media.Url)
if err := p.libmpv.Command([]string{"loadfile", media.Url}); err != nil {
p.log.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 {
p.log.Warn("[MPV PlayControl] get property pause failed", err)
return false
}
return property.(bool)
}
func (p *MpvPlayer) Pause() error {
p.log.Debugf("[MPV Player] pause")
return p.libmpv.SetProperty("pause", mpv.FORMAT_FLAG, true)
}
func (p *MpvPlayer) Unpause() error {
p.log.Debugf("[MPV Player] 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 {
p.log.Debugf("[MPV Player] 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 {
p.log.Warn("[MPV Player] 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 {
p.log.Debugf("[MPV Player] 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 {
p.log.Debugf("[MPV Player] add property observer for mpv")
p.eventManager.RegisterA(
events.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) {
p.log.Debugf("[MPV Player] 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 {
p.log.Debugf("[MPV Player] set audio device %s for mpv", device)
return p.libmpv.SetPropertyString("audio-device", device)
}

View File

@@ -1,27 +0,0 @@
package player
import (
"AynaLivePlayer/adapters/logger"
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/model"
"fmt"
"testing"
"time"
)
func TestPlayer(t *testing.T) {
player := NewMpvPlayer(event.MainManager, &logger.EmptyLogger{})
player.Start()
defer player.Stop()
player.ObserveProperty("time-pos", "testplayer.timepos", func(evnt *event.Event) {
fmt.Println(1, evnt.Data)
})
player.ObserveProperty("percent-pos", "testplayer.percentpos", func(evnt *event.Event) {
fmt.Println(2, evnt.Data)
})
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)
}

View File

@@ -1,139 +0,0 @@
package provider
import (
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"fmt"
"github.com/tidwall/gjson"
"net/url"
"regexp"
)
type Bilibili struct {
InfoApi string
FileApi string
SearchApi string
IdRegex0 *regexp.Regexp
IdRegex1 *regexp.Regexp
}
func NewBilibili(config adapter.MediaProviderConfig) adapter.MediaProvider {
return &Bilibili{
InfoApi: "https://www.bilibili.com/audio/music-service-c/web/song/info?sid=%s",
FileApi: "https://api.bilibili.com/audio/music-service-c/url?device=phone&mid=8047632&mobi_app=iphone&platform=ios&privilege=2&songid=%s&quality=2",
SearchApi: "https://api.bilibili.com/audio/music-service-c/s?search_type=music&keyword=%s&page=1&pagesize=100",
IdRegex0: regexp.MustCompile("^[0-9]+"),
IdRegex1: regexp.MustCompile("^au[0-9]+"),
}
}
func _newBilibili() *Bilibili {
return &Bilibili{
InfoApi: "https://www.bilibili.com/audio/music-service-c/web/song/info?sid=%s",
FileApi: "https://api.bilibili.com/audio/music-service-c/url?device=phone&mid=8047632&mobi_app=iphone&platform=ios&privilege=2&songid=%s&quality=2",
SearchApi: "https://api.bilibili.com/audio/music-service-c/s?search_type=music&keyword=%s&page=1&pagesize=100",
IdRegex0: regexp.MustCompile("^[0-9]+"),
IdRegex1: regexp.MustCompile("^au[0-9]+"),
}
}
var BilibiliAPI *Bilibili
func init() {
BilibiliAPI = _newBilibili()
Providers[BilibiliAPI.GetName()] = BilibiliAPI
}
func (b *Bilibili) GetName() string {
return "bilibili"
}
func (b *Bilibili) MatchMedia(keyword string) *model.Media {
if id := b.IdRegex0.FindString(keyword); id != "" {
return &model.Media{
Meta: model.Meta{
Name: b.GetName(),
Id: id,
},
}
}
if id := b.IdRegex1.FindString(keyword); id != "" {
return &model.Media{
Meta: model.Meta{
Name: b.GetName(),
Id: id[2:],
},
}
}
return nil
}
func (b *Bilibili) FormatPlaylistUrl(uri string) string {
return ""
}
func (b *Bilibili) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
return nil, ErrorExternalApi
}
func (b *Bilibili) Search(keyword string) ([]*model.Media, error) {
resp := httpGetString(fmt.Sprintf(b.SearchApi, url.QueryEscape(keyword)), map[string]string{
"user-agent": "BiliMusic/2.233.3",
})
if resp == "" {
return nil, ErrorExternalApi
}
result := make([]*model.Media, 0)
gjson.Get(resp, "data.result").ForEach(func(key, value gjson.Result) bool {
result = append(result, &model.Media{
Title: value.Get("title").String(),
Cover: model.Picture{Url: value.Get("cover").String()},
Artist: value.Get("author").String(),
Meta: model.Meta{
Name: b.GetName(),
Id: value.Get("id").String(),
},
})
return true
})
return result, nil
}
func (b *Bilibili) UpdateMedia(media *model.Media) error {
resp := httpGetString(fmt.Sprintf(b.InfoApi, media.Meta.(model.Meta).Id), map[string]string{
"user-agent": "BiliMusic/2.233.3",
})
if resp == "" {
return ErrorExternalApi
}
if gjson.Get(resp, "data.title").String() == "" {
return ErrorExternalApi
}
media.Title = gjson.Get(resp, "data.title").String()
media.Cover.Url = gjson.Get(resp, "data.cover").String()
media.Artist = gjson.Get(resp, "data.author").String()
media.Album = media.Title
return nil
}
func (b *Bilibili) UpdateMediaUrl(media *model.Media) error {
resp := httpGetString(fmt.Sprintf(b.FileApi, media.Meta.(model.Meta).Id), map[string]string{
"user-agent": "BiliMusic/2.233.3",
})
if resp == "" {
return ErrorExternalApi
}
media.Header = map[string]string{
"user-agent": "BiliMusic/2.233.3",
}
uri := gjson.Get(resp, "data.cdns.0").String()
if uri == "" {
return ErrorExternalApi
}
media.Url = uri
return nil
}
func (k *Bilibili) UpdateMediaLyric(media *model.Media) error {
return nil
}

View File

@@ -1,60 +0,0 @@
package provider
import (
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"fmt"
"testing"
)
func TestBilibili_Search(t *testing.T) {
var api adapter.MediaProvider = BilibiliAPI
result, err := api.Search("染 reol")
if err != nil {
fmt.Println(1, err)
return
}
fmt.Println(result)
media := result[0]
fmt.Println(*media)
err = api.UpdateMediaUrl(media)
fmt.Println(err)
fmt.Println(media.Url)
}
func TestBilibili_GetMusicMeta(t *testing.T) {
var api adapter.MediaProvider = BilibiliAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "1560601",
},
}
err := api.UpdateMedia(&media)
fmt.Println(err)
if err != nil {
return
}
fmt.Println(media)
}
func TestBilibili_GetMusic(t *testing.T) {
var api adapter.MediaProvider = BilibiliAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "1560601",
},
}
err := api.UpdateMedia(&media)
if err != nil {
return
}
err = api.UpdateMediaUrl(&media)
if err != nil {
return
}
fmt.Println(media)
fmt.Println(media.Url)
}

View File

@@ -1,174 +0,0 @@
package provider
import (
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"fmt"
"github.com/jinzhu/copier"
"github.com/tidwall/gjson"
"net/url"
"regexp"
)
type BilibiliVideo struct {
InfoApi string
FileApi string
SearchApi string
BVRegex *regexp.Regexp
IdRegex *regexp.Regexp
PageRegex *regexp.Regexp
header map[string]string
}
func NewBilibiliVideo(config adapter.MediaProviderConfig) adapter.MediaProvider {
return &BilibiliVideo{
InfoApi: "https://api.bilibili.com/x/web-interface/view/detail?bvid=%s&aid=&jsonp=jsonp",
FileApi: "https://api.bilibili.com/x/player/playurl?type=&otype=json&fourk=1&qn=32&avid=&bvid=%s&cid=%s",
SearchApi: "https://api.bilibili.com/x/web-interface/search/type?search_type=video&page=1&keyword=%s",
BVRegex: regexp.MustCompile("^BV[0-9A-Za-z]+"),
IdRegex: regexp.MustCompile("^BV[0-9A-Za-z]+(\\?p=[0-9]+)?"),
PageRegex: regexp.MustCompile("p=[0-9]+"),
header: map[string]string{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
"Referer": "https://www.bilibili.com/",
"Origin": "https://www.bilibili.com",
},
}
}
func _newBilibiliVideo() *BilibiliVideo {
return &BilibiliVideo{
InfoApi: "https://api.bilibili.com/x/web-interface/view/detail?bvid=%s&aid=&jsonp=jsonp",
FileApi: "https://api.bilibili.com/x/player/playurl?type=&otype=json&fourk=1&qn=32&avid=&bvid=%s&cid=%s",
SearchApi: "https://api.bilibili.com/x/web-interface/search/type?search_type=video&page=1&keyword=%s",
BVRegex: regexp.MustCompile("^BV[0-9A-Za-z]+"),
IdRegex: regexp.MustCompile("^BV[0-9A-Za-z]+(\\?p=[0-9]+)?"),
PageRegex: regexp.MustCompile("p=[0-9]+"),
header: map[string]string{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0",
"Referer": "https://www.bilibili.com/",
"Origin": "https://www.bilibili.com",
"Cookie": "buvid3=9A8B3564-BDA9-407F-B45F-D5C40786CA49167618infoc;",
},
}
}
var BilibiliVideoAPI *BilibiliVideo
func init() {
BilibiliVideoAPI = _newBilibiliVideo()
Providers[BilibiliVideoAPI.GetName()] = BilibiliVideoAPI
}
func (b *BilibiliVideo) getPage(bv string) int {
if page := b.PageRegex.FindString(bv); page != "" {
return util.Atoi(page[2:])
}
return 0
}
func (b *BilibiliVideo) getBv(bv string) string {
return b.BVRegex.FindString(bv)
}
func (b *BilibiliVideo) GetName() string {
return "bilibili-video"
}
func (b *BilibiliVideo) MatchMedia(keyword string) *model.Media {
if id := b.IdRegex.FindString(keyword); id != "" {
return &model.Media{
Meta: model.Meta{
Name: b.GetName(),
Id: id,
},
}
}
return nil
}
func (b *BilibiliVideo) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
return nil, ErrorExternalApi
}
func (b *BilibiliVideo) FormatPlaylistUrl(uri string) string {
return ""
}
func (b *BilibiliVideo) Search(keyword string) ([]*model.Media, error) {
resp := httpGetString(fmt.Sprintf(b.SearchApi, url.QueryEscape(keyword)), b.header)
if resp == "" {
return nil, ErrorExternalApi
}
jresp := gjson.Parse(resp)
if jresp.Get("code").String() != "0" {
return nil, ErrorExternalApi
}
result := make([]*model.Media, 0)
r := regexp.MustCompile("</?em[^>]*>")
jresp.Get("data.result").ForEach(func(key, value gjson.Result) bool {
result = append(result, &model.Media{
Title: r.ReplaceAllString(value.Get("title").String(), ""),
Cover: model.Picture{Url: "https:" + value.Get("pic").String()},
Artist: value.Get("author").String(),
Meta: model.Meta{
Name: b.GetName(),
Id: value.Get("bvid").String(),
},
})
return true
})
return result, nil
}
func (b *BilibiliVideo) UpdateMedia(media *model.Media) error {
resp := httpGetString(fmt.Sprintf(b.InfoApi, b.getBv(media.Meta.(model.Meta).Id)), nil)
if resp == "" {
return ErrorExternalApi
}
jresp := gjson.Parse(resp)
if jresp.Get("data.View.title").String() == "" {
return ErrorExternalApi
}
media.Title = jresp.Get("data.View.title").String()
media.Artist = jresp.Get("data.View.owner.name").String()
media.Cover.Url = jresp.Get("data.View.pic").String()
media.Album = media.Title
return nil
}
func (b *BilibiliVideo) UpdateMediaUrl(media *model.Media) error {
resp := httpGetString(fmt.Sprintf(b.InfoApi, b.getBv(media.Meta.(model.Meta).Id)), nil)
if resp == "" {
return ErrorExternalApi
}
jresp := gjson.Parse(resp)
page := b.getPage(media.Meta.(model.Meta).Id) - 1
cid := jresp.Get(fmt.Sprintf("data.View.pages.%d.cid", page)).String()
if cid == "" {
cid = jresp.Get("data.View.cid").String()
}
if cid == "" {
return ErrorExternalApi
}
resp = httpGetString(fmt.Sprintf(b.FileApi, b.getBv(media.Meta.(model.Meta).Id), cid), b.header)
if resp == "" {
return ErrorExternalApi
}
jresp = gjson.Parse(resp)
uri := jresp.Get("data.durl.0.url").String()
if uri == "" {
return ErrorExternalApi
}
media.Url = uri
header := make(map[string]string)
_ = copier.Copy(&header, &b.header)
header["Referer"] = fmt.Sprintf("https://www.bilibili.com/video/%s", b.getBv(media.Meta.(model.Meta).Id))
media.Header = b.header
return nil
}
func (b *BilibiliVideo) UpdateMediaLyric(media *model.Media) error {
return nil
}

View File

@@ -1,100 +0,0 @@
package provider
import (
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"fmt"
"regexp"
"testing"
)
func TestBV_GetMusicMeta(t *testing.T) {
var api adapter.MediaProvider = BilibiliVideoAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "BV1434y1q71P",
},
}
err := api.UpdateMedia(&media)
fmt.Println(err)
if err != nil {
return
}
fmt.Println(media)
}
func TestBV_GetMusic(t *testing.T) {
var api adapter.MediaProvider = BilibiliVideoAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "BV1434y1q71P",
},
}
err := api.UpdateMedia(&media)
if err != nil {
return
}
err = api.UpdateMediaUrl(&media)
if err != nil {
return
}
//fmt.Println(media)
fmt.Println(media.Url)
}
func TestBV_Regex(t *testing.T) {
fmt.Println(regexp.MustCompile("^BV[0-9A-Za-z]+(\\?p=[0-9]+)?").FindString("BV1gA411P7ir?p=3"))
}
func TestBV_GetMusicMeta2(t *testing.T) {
var api adapter.MediaProvider = BilibiliVideoAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "BV1gA411P7ir?p=3",
},
}
err := api.UpdateMedia(&media)
fmt.Println(err)
if err != nil {
return
}
fmt.Println(media)
}
func TestBV_GetMusic2(t *testing.T) {
var api adapter.MediaProvider = BilibiliVideoAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "BV1gA411P7ir?p=3",
},
}
err := api.UpdateMedia(&media)
if err != nil {
return
}
err = api.UpdateMediaUrl(&media)
if err != nil {
return
}
//fmt.Println(media)
fmt.Println(media.Url)
}
func TestBV_Search(t *testing.T) {
var api adapter.MediaProvider = BilibiliVideoAPI
result, err := api.Search("家有女友")
if err != nil {
fmt.Println(1, err)
return
}
fmt.Println(len(result))
for _, r := range result {
fmt.Println(r.Artist)
}
}

View File

@@ -1,8 +0,0 @@
package provider
import "errors"
var (
ErrorExternalApi = errors.New("external api error")
ErrorNoSuchProvider = errors.New("not such provider")
)

View File

@@ -1,29 +0,0 @@
package provider
import (
"github.com/go-resty/resty/v2"
"time"
)
func httpGet(url string, header map[string]string) (*resty.Response, error) {
resp, err := resty.New().
SetTimeout(time.Second * 3).R().
SetHeaders(header).
Get(url)
return resp, err
}
func httpGetString(url string, header map[string]string) string {
resp, err := httpGet(url, header)
if err != nil {
return ""
}
return resp.String()
}
func httpHead(url string, header map[string]string) (*resty.Response, error) {
resp, err := resty.New().
SetTimeout(time.Second * 3).R().
SetHeaders(header).
Head(url)
return resp, err
}

View File

@@ -1,230 +0,0 @@
package provider
import (
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"fmt"
"github.com/tidwall/gjson"
"html"
"net/url"
"regexp"
"strings"
)
type Kuwo struct {
InfoApi string
FileApi string
SearchCookie string
SearchApi string
LyricApi string
PlaylistApi string
PlaylistRegex0 *regexp.Regexp
PlaylistRegex1 *regexp.Regexp
IdRegex0 *regexp.Regexp
IdRegex1 *regexp.Regexp
}
func NewKuwo(config adapter.MediaProviderConfig) adapter.MediaProvider {
return &Kuwo{
InfoApi: "http://www.kuwo.cn/api/www/music/musicInfo?mid=%s&httpsStatus=1",
//FileApi: "http://www.kuwo.cn/api/v1/www/music/playUrl?mid=%d&type=music&httpsStatus=1",
FileApi: "http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_%s",
SearchCookie: "http://kuwo.cn/search/list?key=%s",
SearchApi: "http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key=%s&pn=%d&rn=%d",
LyricApi: "http://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=%s",
PlaylistApi: "http://www.kuwo.cn/api/www/playlist/playListInfo?pid=%s&pn=%d&rn=%d&httpsStatus=1",
PlaylistRegex0: regexp.MustCompile("[0-9]+"),
PlaylistRegex1: regexp.MustCompile("playlist/[0-9]+"),
IdRegex0: regexp.MustCompile("^[0-9]+"),
IdRegex1: regexp.MustCompile("^kw[0-9]+"),
}
}
func _newKuwo() *Kuwo {
return &Kuwo{
InfoApi: "http://www.kuwo.cn/api/www/music/musicInfo?mid=%s&httpsStatus=1",
//FileApi: "http://www.kuwo.cn/api/v1/www/music/playUrl?mid=%d&type=music&httpsStatus=1",
FileApi: "http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_%s",
SearchCookie: "http://kuwo.cn/search/list?key=%s",
SearchApi: "http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key=%s&pn=%d&rn=%d",
LyricApi: "http://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=%s",
PlaylistApi: "http://www.kuwo.cn/api/www/playlist/playListInfo?pid=%s&pn=%d&rn=%d&httpsStatus=1",
PlaylistRegex0: regexp.MustCompile("[0-9]+"),
PlaylistRegex1: regexp.MustCompile("playlist/[0-9]+"),
IdRegex0: regexp.MustCompile("^[0-9]+"),
IdRegex1: regexp.MustCompile("^kw[0-9]+"),
}
}
var KuwoAPI *Kuwo
func init() {
KuwoAPI = _newKuwo()
Providers[KuwoAPI.GetName()] = KuwoAPI
}
func (k *Kuwo) GetName() string {
return "kuwo"
}
func (k *Kuwo) MatchMedia(keyword string) *model.Media {
if id := k.IdRegex0.FindString(keyword); id != "" {
return &model.Media{
Meta: model.Meta{
Name: k.GetName(),
Id: id,
},
}
}
if id := k.IdRegex1.FindString(keyword); id != "" {
return &model.Media{
Meta: model.Meta{
Name: k.GetName(),
Id: id[2:],
},
}
}
return nil
}
func (k *Kuwo) FormatPlaylistUrl(uri string) string {
var id string
id = k.PlaylistRegex0.FindString(uri)
if id != "" {
return id
}
id = k.PlaylistRegex1.FindString(uri)
if id != "" {
return id[9:]
}
return ""
}
//func (k *Kuwo) _kuwoGet(url string) string {
// searchCookie, err := httpHead(fmt.Sprintf(k.SearchCookie, "any"), nil)
// if err != nil {
// return ""
// }
// kwToken, ok := util.SliceString(regexp.MustCompile("kw_token=([^;])*;").FindString(searchCookie.Header().Get("set-cookie")), 9, -1)
// if !ok {
// return ""
// }
// return httpGetString(url, map[string]string{
// "cookie": "kw_token=" + kwToken,
// "csrf": kwToken,
// "referer": "http://www.kuwo.cn/",
// })
//}
func (k *Kuwo) _kuwoGet(url string) string {
return httpGetString(url, map[string]string{
"cookie": "kw_token=" + "95MWTYC4FP",
"csrf": "95MWTYC4FP",
"referer": "http://www.kuwo.cn/",
})
}
func (k *Kuwo) Search(keyword string) ([]*model.Media, error) {
resp := k._kuwoGet(fmt.Sprintf(k.SearchApi, url.QueryEscape(keyword), 1, 64))
if resp == "" {
return nil, ErrorExternalApi
}
result := make([]*model.Media, 0)
gjson.Parse(resp).Get("data.list").ForEach(func(key, value gjson.Result) bool {
result = append(result, &model.Media{
Title: html.UnescapeString(value.Get("name").String()),
Cover: model.Picture{Url: value.Get("pic").String()},
Artist: value.Get("artist").String(),
Album: value.Get("album").String(),
Meta: model.Meta{
Name: k.GetName(),
Id: value.Get("rid").String(),
},
})
return true
})
return result, nil
}
func (k *Kuwo) UpdateMedia(media *model.Media) error {
resp := k._kuwoGet(fmt.Sprintf(k.InfoApi, media.Meta.(model.Meta).Id))
if resp == "" {
return ErrorExternalApi
}
jresp := gjson.Parse(resp)
if jresp.Get("data.musicrid").String() == "" {
return ErrorExternalApi
}
media.Title = html.UnescapeString(jresp.Get("data.name").String())
media.Cover.Url = jresp.Get("data.pic").String()
media.Artist = jresp.Get("data.artist").String()
media.Album = jresp.Get("data.album").String()
return nil
}
func (k *Kuwo) UpdateMediaUrl(media *model.Media) error {
result := httpGetString(fmt.Sprintf(k.FileApi, media.Meta.(model.Meta).Id), nil)
if result == "" {
return ErrorExternalApi
}
media.Url = result
return nil
}
func (k *Kuwo) UpdateMediaLyric(media *model.Media) error {
result := httpGetString(fmt.Sprintf(k.LyricApi, media.Meta.(model.Meta).Id), nil)
if result == "" {
return ErrorExternalApi
}
lrcs := make([]string, 0)
gjson.Parse(result).Get("data.lrclist").ForEach(func(key, value gjson.Result) bool {
lrcs = append(lrcs, fmt.Sprintf("[00:%s]%s", value.Get("time").String(), value.Get("lineLyric").String()))
return true
})
media.Lyric = strings.Join(lrcs, "\n")
return nil
}
func (k *Kuwo) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
medias := make([]*model.Media, 0)
var resp string
var jresp gjson.Result
for i := 1; i <= 20; i++ {
resp = k._kuwoGet(fmt.Sprintf(k.PlaylistApi, playlist.Id, i, 128))
if resp == "" {
break
}
//fmt.Println(resp[:100])
jresp = gjson.Parse(resp)
//fmt.Println(jresp.Get("code").String())
if jresp.Get("code").String() != "200" {
break
}
cnt := int(jresp.Get("data.total").Int())
//fmt.Println(cnt)
//fmt.Println(len(jresp.Get("data.musicList").Array()))
jresp.Get("data.musicList").ForEach(func(key, value gjson.Result) bool {
medias = append(
medias,
&model.Media{
Title: html.UnescapeString(value.Get("name").String()),
Artist: value.Get("artist").String(),
Cover: model.Picture{Url: value.Get("pic").String()},
Album: value.Get("album").String(),
Meta: model.Meta{
Name: k.GetName(),
Id: value.Get("rid").String(),
},
})
return true
})
if cnt <= i*100 {
break
}
}
if len(medias) == 0 {
return nil, ErrorExternalApi
}
return medias, nil
}

View File

@@ -1,89 +0,0 @@
package provider
import (
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"fmt"
"testing"
)
func TestKuwo_Search(t *testing.T) {
var api adapter.MediaProvider = KuwoAPI
result, err := api.Search("染 reol")
if err != nil {
return
}
fmt.Println(result)
media := result[0]
err = api.UpdateMediaUrl(media)
fmt.Println(err)
fmt.Println(media.Url)
}
func TestKuwo_GetMusicMeta(t *testing.T) {
var api adapter.MediaProvider = KuwoAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "22804772",
},
}
err := api.UpdateMedia(&media)
fmt.Println(err)
if err != nil {
return
}
fmt.Println(media)
}
func TestKuwo_GetMusic(t *testing.T) {
var api adapter.MediaProvider = KuwoAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "22804772",
},
}
err := api.UpdateMedia(&media)
if err != nil {
return
}
err = api.UpdateMediaUrl(&media)
if err != nil {
return
}
fmt.Println(media)
fmt.Println(media.Url)
}
func TestKuwo_UpdateMediaLyric(t *testing.T) {
var api adapter.MediaProvider = KuwoAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "22804772",
},
}
err := api.UpdateMediaLyric(&media)
fmt.Println(err)
fmt.Println(media.Lyric)
}
func TestKuwo_GetPlaylist(t *testing.T) {
var api adapter.MediaProvider = KuwoAPI
playlist, err := api.GetPlaylist(&model.Meta{
Name: api.GetName(),
//Id: "1082685104",
Id: "2959147566",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(len(playlist))
for _, media := range playlist {
fmt.Println(media.Title, media.Artist, media.Album)
}
}

View File

@@ -1,165 +0,0 @@
package provider
import (
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"os"
"sort"
"strings"
)
type _LocalPlaylist struct {
Name string
Medias []*model.Media
}
type Local struct {
localDir string
Playlists []*_LocalPlaylist
}
func NewLocalCtor(config adapter.MediaProviderConfig) adapter.MediaProvider {
localDir, ok := config["local_dir"]
if !ok {
localDir = "./local"
}
l := &Local{Playlists: make([]*_LocalPlaylist, 0), localDir: localDir}
if err := os.MkdirAll(localDir, 0755); err != nil {
return l
}
for _, n := range getPlaylistNames(localDir) {
l.Playlists = append(l.Playlists, &_LocalPlaylist{Name: n})
}
for i, _ := range l.Playlists {
_ = readLocalPlaylist(localDir, l.Playlists[i])
}
LocalAPI = l
Providers[LocalAPI.GetName()] = LocalAPI
return l
}
var LocalAPI *Local
func NewLocal(localdir string) *Local {
l := &Local{Playlists: make([]*_LocalPlaylist, 0), localDir: localdir}
if err := os.MkdirAll(localdir, 0755); err != nil {
return l
}
for _, n := range getPlaylistNames(localdir) {
l.Playlists = append(l.Playlists, &_LocalPlaylist{Name: n})
}
for i, _ := range l.Playlists {
_ = readLocalPlaylist(localdir, l.Playlists[i])
}
LocalAPI = l
Providers[LocalAPI.GetName()] = LocalAPI
return l
}
func (l *Local) GetName() string {
return "local"
}
func (l *Local) MatchMedia(keyword string) *model.Media {
return nil
}
func (l *Local) UpdateMediaLyric(media *model.Media) error {
// already update in UpdateMedia, do nothing
return nil
}
func (l *Local) FormatPlaylistUrl(uri string) string {
return uri
}
func (l *Local) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
var pl *_LocalPlaylist = nil
for _, p := range l.Playlists {
if p.Name == playlist.Id {
pl = p
}
}
if pl == nil {
l.Playlists = append(l.Playlists, &_LocalPlaylist{Name: playlist.Id})
pl = l.Playlists[len(l.Playlists)-1]
}
if readLocalPlaylist(l.localDir, pl) != nil {
return nil, ErrorExternalApi
}
return pl.Medias, nil
}
func (l *Local) Search(keyword string) ([]*model.Media, error) {
allMedias := make([]*model.Media, 0)
for _, p := range l.Playlists {
for _, m := range p.Medias {
allMedias = append(allMedias, m)
}
}
MediaSort(keyword, allMedias)
c := util.Min(len(allMedias), 32)
medias := make([]*model.Media, c)
for i := 0; i < c; i++ {
medias[i] = allMedias[i].Copy()
}
return medias, nil
}
func (l *Local) SearchV1(keyword string) ([]*model.Media, error) {
result := make([]struct {
M *model.Media
N int
}, 0)
keywords := strings.Split(keyword, " ")
for _, p := range l.Playlists {
for _, m := range p.Medias {
title := strings.ToLower(m.Title)
artist := strings.ToLower(m.Artist)
n := 0
for _, k := range keywords {
kw := strings.ToLower(k)
if strings.Contains(title, kw) || strings.Contains(artist, kw) {
n++
}
if kw == title {
n += 3
}
}
if n > 0 {
result = append(result, struct {
M *model.Media
N int
}{M: m, N: n})
}
}
}
sort.Slice(result, func(i, j int) bool {
return result[i].N > result[j].N
})
medias := make([]*model.Media, len(result))
for i, r := range result {
medias[i] = r.M.Copy()
}
return medias, nil
}
func (l *Local) UpdateMedia(media *model.Media) error {
mediaPath := media.Meta.(model.Meta).Id
_, err := os.Stat(mediaPath)
if err != nil {
return err
}
return readMediaFile(media)
}
func (l *Local) UpdateMediaUrl(media *model.Media) error {
mediaPath := media.Meta.(model.Meta).Id
_, err := os.Stat(mediaPath)
if err != nil {
return err
}
media.Url = mediaPath
return nil
}

View File

@@ -1,72 +0,0 @@
package provider
import (
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/model"
"github.com/dhowden/tag"
"io/ioutil"
"os"
"path/filepath"
)
func getPlaylistNames(localdir string) []string {
names := make([]string, 0)
items, _ := ioutil.ReadDir(localdir)
for _, item := range items {
if item.IsDir() {
names = append(names, item.Name())
}
}
return names
}
// readLocalPlaylist read files under a directory
// and return a _LocalPlaylist object.
// This function assume this directory exists
func readLocalPlaylist(localdir string, playlist *_LocalPlaylist) error {
p1th := playlist.Name
playlist.Medias = make([]*model.Media, 0)
fullPath := filepath.Join(localdir, p1th)
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
return err
}
items, _ := ioutil.ReadDir(fullPath)
for _, item := range items {
// if item is a file, read file
if !item.IsDir() {
fn := item.Name()
media := model.Media{
Meta: model.Meta{
Name: LocalAPI.GetName(),
Id: filepath.Join(fullPath, fn),
},
}
if readMediaFile(&media) != nil {
continue
}
playlist.Medias = append(playlist.Medias, &media)
}
}
return nil
}
func readMediaFile(media *model.Media) error {
p := media.Meta.(model.Meta).Id
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close()
meta, err := tag.ReadFrom(f)
if err != nil {
return err
}
media.Title = util.GetOrDefault(meta.Title(), filepath.Base(p))
media.Artist = util.GetOrDefault(meta.Artist(), "Unknown")
media.Album = util.GetOrDefault(meta.Album(), "Unknown")
media.Lyric = meta.Lyrics()
if meta.Picture() != nil {
media.Cover.Data = meta.Picture().Data
}
return nil
}

View File

@@ -1,25 +0,0 @@
package provider
import (
"fmt"
"io/ioutil"
"testing"
)
func TestLocal_Read(t *testing.T) {
items, _ := ioutil.ReadDir(".")
for _, item := range items {
if item.IsDir() {
subitems, _ := ioutil.ReadDir(item.Name())
for _, subitem := range subitems {
if !subitem.IsDir() {
// handle file there
fmt.Println(item.Name() + "/" + subitem.Name())
}
}
} else {
// handle file there
fmt.Println(item.Name())
}
}
}

View File

@@ -1,239 +0,0 @@
package provider
import (
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
neteaseApi "github.com/XiaoMengXinX/Music163Api-Go/api"
neteaseTypes "github.com/XiaoMengXinX/Music163Api-Go/types"
neteaseUtil "github.com/XiaoMengXinX/Music163Api-Go/utils"
"regexp"
"strconv"
"strings"
)
type Netease struct {
PlaylistRegex0 *regexp.Regexp
PlaylistRegex1 *regexp.Regexp
ReqData neteaseUtil.RequestData
IdRegex0 *regexp.Regexp
IdRegex1 *regexp.Regexp
loginStatus neteaseTypes.LoginStatusData
}
func NewNetease(config adapter.MediaProviderConfig) adapter.MediaProvider {
return &Netease{
PlaylistRegex0: regexp.MustCompile("^[0-9]+$"),
// https://music.163.com/playlist?id=2382819181&userid=95906480
PlaylistRegex1: regexp.MustCompile("playlist\\?id=[0-9]+"),
ReqData: neteaseUtil.RequestData{
Headers: neteaseUtil.Headers{
{
"X-Real-IP",
"118.88.88.88",
},
},
},
IdRegex0: regexp.MustCompile("^[0-9]+"),
IdRegex1: regexp.MustCompile("^wy[0-9]+"),
}
}
func _newNetease() *Netease {
return &Netease{
PlaylistRegex0: regexp.MustCompile("^[0-9]+$"),
// https://music.163.com/playlist?id=2382819181&userid=95906480
PlaylistRegex1: regexp.MustCompile("playlist\\?id=[0-9]+"),
ReqData: neteaseUtil.RequestData{
Headers: neteaseUtil.Headers{
{
"X-Real-IP",
"118.88.88.88",
},
},
},
IdRegex0: regexp.MustCompile("^[0-9]+"),
IdRegex1: regexp.MustCompile("^wy[0-9]+"),
}
}
var NeteaseAPI *Netease
func init() {
NeteaseAPI = _newNetease()
Providers[NeteaseAPI.GetName()] = NeteaseAPI
}
// Netease private helper method
func _neteaseGetArtistNames(data neteaseTypes.SongDetailData) string {
artists := make([]string, 0)
for _, a := range data.Ar {
artists = append(artists, a.Name)
}
return strings.Join(artists, ",")
}
// MediaProvider implementation
func (n *Netease) GetName() string {
return "netease"
}
func (n *Netease) MatchMedia(keyword string) *model.Media {
if id := n.IdRegex0.FindString(keyword); id != "" {
return &model.Media{
Meta: model.Meta{
Name: n.GetName(),
Id: id,
},
}
}
if id := n.IdRegex1.FindString(keyword); id != "" {
return &model.Media{
Meta: model.Meta{
Name: n.GetName(),
Id: id[2:],
},
}
}
return nil
}
func (n *Netease) FormatPlaylistUrl(uri string) string {
var id string
id = n.PlaylistRegex0.FindString(uri)
if id != "" {
return id
}
id = n.PlaylistRegex1.FindString(uri)
if id != "" {
return id[12:]
}
return ""
}
func (n *Netease) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
result, err := neteaseApi.GetPlaylistDetail(
n.ReqData, util.Atoi(playlist.Id))
if err != nil || result.Code != 200 {
return nil, ErrorExternalApi
}
cnt := len(result.Playlist.TrackIds)
if cnt == 0 {
return nil, ErrorExternalApi
}
ids := make([]int, len(result.Playlist.TrackIds))
for i := 0; i < cnt; i++ {
ids[i] = result.Playlist.TrackIds[i].Id
}
medias := make([]*model.Media, 0, cnt)
for index := 0; index < len(ids); index += 1000 {
result2, err := neteaseApi.GetSongDetail(
n.ReqData,
ids[index:util.IntMin(index+1000, len(ids))])
if err != nil || result2.Code != 200 {
break
}
cnt = len(result2.Songs)
if cnt == 0 {
break
}
for i := 0; i < cnt; i++ {
medias = append(medias, &model.Media{
Title: result2.Songs[i].Name,
Artist: _neteaseGetArtistNames(result2.Songs[i]),
Cover: model.Picture{Url: result2.Songs[i].Al.PicUrl},
Album: result2.Songs[i].Al.Name,
Url: "",
Header: nil,
User: nil,
Meta: model.Meta{
Name: n.GetName(),
Id: strconv.Itoa(result2.Songs[i].Id),
},
})
}
}
if len(medias) == 0 {
return nil, ErrorExternalApi
}
return medias, nil
}
func (n *Netease) Search(keyword string) ([]*model.Media, error) {
rawResult, err := neteaseApi.SearchSong(
n.ReqData,
neteaseApi.SearchSongConfig{
Keyword: keyword,
Limit: 30,
Offset: 0,
})
if err != nil || rawResult.Code != 200 {
return nil, ErrorExternalApi
}
medias := make([]*model.Media, 0)
for _, song := range rawResult.Result.Songs {
artists := make([]string, 0)
for _, a := range song.Artists {
artists = append(artists, a.Name)
}
medias = append(medias, &model.Media{
Title: song.Name,
Artist: strings.Join(artists, ","),
Cover: model.Picture{},
Album: song.Album.Name,
Url: "",
Header: nil,
Meta: model.Meta{
Name: n.GetName(),
Id: strconv.Itoa(song.Id),
},
})
}
return medias, nil
}
func (n *Netease) UpdateMedia(media *model.Media) error {
result, err := neteaseApi.GetSongDetail(
n.ReqData,
[]int{util.Atoi(media.Meta.(model.Meta).Id)})
if err != nil || result.Code != 200 {
return ErrorExternalApi
}
if len(result.Songs) == 0 {
return ErrorExternalApi
}
media.Title = result.Songs[0].Name
media.Cover.Url = result.Songs[0].Al.PicUrl
media.Album = result.Songs[0].Al.Name
media.Artist = _neteaseGetArtistNames(result.Songs[0])
return nil
}
func (n *Netease) UpdateMediaUrl(media *model.Media) error {
result, err := neteaseApi.GetSongURL(
n.ReqData,
neteaseApi.SongURLConfig{Ids: []int{util.Atoi(media.Meta.(model.Meta).Id)}})
if err != nil || result.Code != 200 {
return ErrorExternalApi
}
if len(result.Data) == 0 {
return ErrorExternalApi
}
if result.Data[0].Code != 200 {
return ErrorExternalApi
}
media.Url = result.Data[0].Url
return nil
}
func (n *Netease) UpdateMediaLyric(media *model.Media) error {
result, err := neteaseApi.GetSongLyric(n.ReqData, util.Atoi(media.Meta.(model.Meta).Id))
if err != nil || result.Code != 200 {
return ErrorExternalApi
}
media.Lyric = result.Lrc.Lyric
return nil
}

View File

@@ -1,62 +0,0 @@
package provider
import (
"fmt"
neteaseApi "github.com/XiaoMengXinX/Music163Api-Go/api"
"net/http"
)
// Netease other method
func (n *Netease) UpdateStatus() {
status, _ := neteaseApi.GetLoginStatus(n.ReqData)
n.loginStatus = status
}
// IsLogin check if current cookie is a login user
func (n *Netease) IsLogin() bool {
return n.loginStatus.Profile.UserId != 0
}
func (n *Netease) Nickname() string {
return n.loginStatus.Profile.Nickname
}
func (n *Netease) GetQrLoginKey() string {
unikey, err := neteaseApi.GetQrUnikey(n.ReqData)
if err != nil {
return ""
}
return unikey.Unikey
}
func (n *Netease) GetQrLoginUrl(key string) string {
return fmt.Sprintf("https://music.163.com/login?codekey=%s", key)
}
func (n *Netease) CheckQrLogin(key string) (bool, string) {
login, h, err := neteaseApi.CheckQrLogin(n.ReqData, key)
if err != nil {
return false, ""
}
// if login.Code == 800 || login.Code == 803. login success
if login.Code != 800 && login.Code != 803 {
return false, login.Message
}
cookies := make([]*http.Cookie, 0)
for _, c := range (&http.Response{Header: h}).Cookies() {
if c.Name == "MUSIC_U" || c.Name == "__csrf" {
cookies = append(cookies, c)
}
}
n.ReqData.Cookies = cookies
return true, login.Message
}
func (n *Netease) Logout() {
n.ReqData.Cookies = []*http.Cookie{
{Name: "MUSIC_U", Value: ""},
{Name: "__csrf", Value: ""},
}
return
}

View File

@@ -1,104 +0,0 @@
package provider
import (
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"fmt"
"testing"
)
func TestNetease_Search(t *testing.T) {
var api adapter.MediaProvider = NeteaseAPI
result, err := api.Search("染 reol")
if err != nil {
return
}
fmt.Println(result)
media := result[0]
fmt.Println(media)
err = api.UpdateMediaUrl(media)
fmt.Println(err)
fmt.Println(media.Url)
}
func TestNetease_Search2(t *testing.T) {
var api adapter.MediaProvider = NeteaseAPI
result, err := api.Search("出山")
if err != nil {
return
}
t.Log(result)
media := result[0]
t.Log(media)
err = api.UpdateMediaUrl(media)
t.Log(err)
t.Log(media.Url)
}
func TestNetease_GetMusicMeta(t *testing.T) {
var api adapter.MediaProvider = NeteaseAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "33516503",
},
}
err := api.UpdateMedia(&media)
fmt.Println(err)
if err != nil {
return
}
fmt.Println(media)
}
func TestNetease_GetMusic(t *testing.T) {
var api adapter.MediaProvider = NeteaseAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "33516503",
},
}
err := api.UpdateMedia(&media)
if err != nil {
return
}
err = api.UpdateMediaUrl(&media)
if err != nil {
return
}
fmt.Println(media)
fmt.Println(media.Url)
}
func TestNetease_GetPlaylist(t *testing.T) {
var api adapter.MediaProvider = NeteaseAPI
playlist, err := api.GetPlaylist(&model.Meta{
Name: api.GetName(),
//Id: "2520739691",
Id: "2382819181",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(len(playlist))
for _, media := range playlist {
fmt.Println(media.Title, media.Artist, media.Album)
}
}
func TestNetease_UpdateMediaLyric(t *testing.T) {
var api adapter.MediaProvider = NeteaseAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "33516503",
},
}
err := api.UpdateMediaLyric(&media)
fmt.Println(err)
fmt.Println(media.Lyric)
}

View File

@@ -1,65 +0,0 @@
package provider
import (
"AynaLivePlayer/core/adapter"
model "AynaLivePlayer/core/model"
)
var RegisteredProviders = map[string]adapter.MediaProviderCtor{
"netease": NewNetease,
"local": NewLocalCtor,
"kuwo": NewKuwo,
"bilibili": NewBilibili,
"bilibili-video": NewBilibiliVideo,
}
var Providers map[string]adapter.MediaProvider = make(map[string]adapter.MediaProvider)
func GetPlaylist(meta *model.Meta) ([]*model.Media, error) {
if v, ok := Providers[meta.Name]; ok {
return v.GetPlaylist(meta)
}
return nil, ErrorNoSuchProvider
}
func FormatPlaylistUrl(pname, uri string) (string, error) {
if v, ok := Providers[pname]; ok {
return v.FormatPlaylistUrl(uri), nil
}
return "", ErrorNoSuchProvider
}
func MatchMedia(provider string, keyword string) *model.Media {
if v, ok := Providers[provider]; ok {
return v.MatchMedia(keyword)
}
return nil
}
func Search(provider string, keyword string) ([]*model.Media, error) {
if v, ok := Providers[provider]; ok {
return v.Search(keyword)
}
return nil, ErrorNoSuchProvider
}
func UpdateMedia(media *model.Media) error {
if v, ok := Providers[media.Meta.(model.Meta).Name]; ok {
return v.UpdateMedia(media)
}
return ErrorNoSuchProvider
}
func UpdateMediaUrl(media *model.Media) error {
if v, ok := Providers[media.Meta.(model.Meta).Name]; ok {
return v.UpdateMediaUrl(media)
}
return ErrorNoSuchProvider
}
func UpdateMediaLyric(media *model.Media) error {
if v, ok := Providers[media.Meta.(model.Meta).Name]; ok {
return v.UpdateMediaLyric(media)
}
return ErrorNoSuchProvider
}

View File

@@ -1,26 +0,0 @@
package provider
import (
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/model"
"sort"
)
func MediaSort(keyword string, medias []*model.Media) {
mediaDist := make([]struct {
media *model.Media
dist int
}, len(medias))
for i, media := range medias {
mediaDist[i].media = media
mediaDist[i].dist = util.StrLen(util.LongestCommonString(keyword, media.Title)) +
util.StrLen(util.LongestCommonString(keyword, media.Artist))
}
sort.Slice(mediaDist, func(i, j int) bool {
return mediaDist[i].dist > mediaDist[j].dist
})
for i, media := range mediaDist {
medias[i] = media.media
}
return
}

112
app/main.go Normal file
View File

@@ -0,0 +1,112 @@
package main
import (
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui"
"AynaLivePlayer/internal"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"AynaLivePlayer/pkg/logger"
loggerRepo "AynaLivePlayer/pkg/logger/repository"
"flag"
)
var dev = flag.Bool("dev", false, "dev")
type _LogConfig struct {
config.BaseConfig
Path string
Level logger.LogLevel
RedirectStderr bool
MaxSize int64
}
func (c *_LogConfig) Name() string {
return "Log"
}
var Log = &_LogConfig{
Path: "./log.txt",
Level: logger.LogLevelInfo,
RedirectStderr: false, // this should be true if it is in production mode.
MaxSize: 5,
}
//func createController(log adapter.ILogger) adapter.IControlBridge {
// logbridge := log.WithModule("ControlBridge")
// em := event.MainManager.NewChildManager()
// liveroom := internal.NewLiveRoomController(
// logbridge)
// lyric := internal.NewLyricLoader()
// provider := internal.NewProviderController(logbridge)
// playlist := internal.NewPlaylistController(em, logbridge, provider)
// plugin := internal.NewPluginController(logbridge)
// mpvPlayer := player.NewMpvPlayer(em, logbridge)
// playControl := internal.NewPlayerController(mpvPlayer, playlist, lyric, provider, logbridge)
// ctr := internal.NewController(
// liveroom, playControl, playlist, provider, plugin,
// logbridge,
// )
// return ctr
//}
func setupGlobal() {
global.EventManager = event.NewManger(128, 16)
global.Logger = loggerRepo.NewZapColoredLogger()
global.Logger.SetLogLevel(Log.Level)
}
func main() {
flag.Parse()
config.LoadFromFile(config.ConfigPath)
config.LoadConfig(Log)
i18n.LoadLanguage(config.General.Language)
setupGlobal()
global.Logger.Info("================Program Start================")
global.Logger.Infof("================Current Version: %s================", model.Version(config.Version))
//mainController := createController(log)
internal.Initialize()
gui.Initialize()
global.EventManager.Start()
//plugins := []adapter.Plugin{diange.NewDiange(mainController), qiege.NewQiege(mainController),
// textinfo.NewTextInfo(mainController), webinfo.NewWebInfo(mainController),
// wylogin.NewWYLogin(mainController)}
//mainController.LoadPlugins(plugins...)
gui.MainWindow.ShowAndRun()
internal.Stop()
global.EventManager.Stop()
if *dev {
i18n.SaveTranslation()
}
_ = config.SaveToConfigFile(config.ConfigPath)
global.Logger.Info("================Program End================")
}
////go:embed all:../webgui/frontend/dist
//var assets embed.FS
//
//func main() {
// // Create an instance of the app structure
// app := webgui.NewApp()
//
// // Create application with options
// err := wails.Run(&options.App{
// Title: "AynaLivePlayer",
// Width: 1024,
// Height: 768,
// AssetServer: &assetserver.Options{
// Assets: assets,
// },
// BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
// OnStartup: app.Startup,
// Bind: []interface{}{
// app,
// },
// })
//
// if err != nil {
// println("Error:", err.Error())
// }
//}

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#First released as C++ program by Hiroyuki Tsutsumi as part of the free software suite “Beer”
#I thought porting it to Python could be both a challenge and useful
from sys import argv, exit, getsizeof
from struct import pack_into, unpack_from
def ceil4(n):
"""returns the next integer which is a multiple of 4"""
return (n + 3) & ~3
if len(argv)!=2:
print("Usage: %s FontCollection.ttc" % argv)
exit(2)
filename = argv[1]
in_file = open(filename, "rb")
buf = in_file.read()
in_file.close()
if filename.lower().endswith(".ttc"):
filename = filename[:-4]
if buf[:4] != b"ttcf":
out_filename = "%s.ttf" % filename
out_file = open(out_filename, "wb")
out_file.write(buf)
#end, so we dont have to close the files or call exit() here
else:
ttf_count = unpack_from("!L", buf, 0x08)[0]
print("Anzahl enthaltener TTF-Dateien: %s" % ttf_count)
ttf_offset_array = unpack_from("!"+ttf_count*"L", buf, 0x0C)
for i in range(ttf_count):
print("Extrahiere TTF #%s:" % (i+1))
table_header_offset = ttf_offset_array[i]
print("\tHeader beginnt bei Byte %s" % table_header_offset)
table_count = unpack_from("!H", buf, table_header_offset+0x04)[0]
header_length = 0x0C + table_count * 0x10
print("\tHeaderlänge: %s Byte" % header_length)
table_length = 0
for j in range(table_count):
length = unpack_from("!L", buf, table_header_offset+0x0C+0x0C+j*0x10)[0]
table_length += ceil4(length)
total_length = header_length + table_length
new_buf = bytearray(total_length)
header = unpack_from(header_length*"c", buf, table_header_offset)
pack_into(header_length*"c", new_buf, 0, *header)
current_offset = header_length
for j in range(table_count):
offset = unpack_from("!L", buf, table_header_offset+0x0C+0x08+j*0x10)[0]
length = unpack_from("!L", buf, table_header_offset+0x0C+0x0C+j*0x10)[0]
pack_into("!L", new_buf, 0x0C+0x08+j*0x10, current_offset)
current_table = unpack_from(length*"c", buf, offset)
pack_into(length*"c", new_buf, current_offset, *current_table)
#table_checksum = sum(unpack_from("!"+("L"*length), new_buf, current_offset))
#pack_into("!L", new_buf, 0x0C+0x04+j*0x10, table_checksum)
current_offset += ceil4(length)
out_file = open("%s%d.ttf"%(filename, i), "wb")
out_file.write(new_buf)

View File

@@ -4,6 +4,10 @@
"zh-CN"
],
"Messages": {
"default web protocol. enter room id to connect.": {
"en": "default web protocol. enter room id to connect.",
"zh-CN": "网页弹幕协议,请输入房间号"
},
"gui.config.basic.audio_device": {
"en": "Audio Device",
"zh-CN": "音频输出设备"
@@ -72,6 +76,10 @@
"en": "User",
"zh-CN": "用户"
},
"gui.lyric.title": {
"en": "Lyric",
"zh-CN": "歌词"
},
"gui.player.button.lrc": {
"en": "lrc",
"zh-CN": "歌词"
@@ -168,6 +176,10 @@
"en": "Room ID",
"zh-CN": "房间号"
},
"gui.room.add.name": {
"en": "Display Name",
"zh-CN": "显示名"
},
"gui.room.add.prompt": {
"en": "enter room id",
"zh-CN": "填入房间号"
@@ -272,6 +284,10 @@
"en": "New Version Available",
"zh-CN": "有新版本可用"
},
"open bilibili live protocol. enter client key to connect.": {
"en": "open bilibili live protocol. enter client key to connect.",
"zh-CN": "新版b站协议输入身份码"
},
"plugin.diange.admin": {
"en": "Admin",
"zh-CN": "管理员"

View File

@@ -1,146 +0,0 @@
package event
import (
"sync"
)
type EventId string
type Event struct {
Id EventId
Cancelled bool
Data interface{}
Outdated bool
lock sync.RWMutex
}
type HandlerFunc func(event *Event)
type Handler struct {
EventId EventId
Name string
Handler HandlerFunc
SkipOutdated bool
}
type Manager struct {
handlers map[EventId]map[string]*Handler
prevEvent map[EventId]*Event
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),
prevEvent: make(map[EventId]*Event),
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),
prevEvent: make(map[EventId]*Event),
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,
SkipOutdated: true,
})
}
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.Lock()
handlers, ok := h.handlers[event.Id]
if e := h.prevEvent[event.Id]; e != nil {
e.lock.Lock()
e.Outdated = true
e.lock.Unlock()
}
h.prevEvent[event.Id] = event
h.lock.Unlock()
if !ok {
return
}
for _, eh := range handlers {
eventHandler := eh
h.queue <- func() {
event.lock.Lock()
if eventHandler.SkipOutdated && event.Outdated {
event.lock.Unlock()
return
}
eventHandler.Handler(event)
event.lock.Unlock()
}
}
}
func (h *Manager) CallA(id EventId, data interface{}) {
h.Call(&Event{
Id: id,
Data: data,
})
}

View File

@@ -1,35 +0,0 @@
package event
import (
"fmt"
"testing"
"time"
)
func TestEventSeq(t *testing.T) {
m := NewManger(128, 16)
m.RegisterA("ceshi", "asdf1", func(event *Event) {
fmt.Println("Num:", event.Data)
})
go func() {
for i := 0; i < 1000; i++ {
m.CallA("ceshi", fmt.Sprintf("a%d", i))
}
}()
for i := 0; i < 1000; i++ {
m.CallA("ceshi", i)
}
}
func TestEventWeired(t *testing.T) {
m := NewManger(128, 2)
m.RegisterA("playlist.update", "asdf1", func(event *Event) {
fmt.Printf("%d %p, outdated: %t\n", event.Data, event, event.Outdated)
})
for i := 0; i < 2; i++ {
fmt.Println("asdfsafasfasfasfasfasf")
m.CallA("playlist.update", i)
fmt.Println("asdfsafasfasfasfasfasf")
}
time.Sleep(1 * time.Second)
}

View File

@@ -1,10 +0,0 @@
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,26 +0,0 @@
package logger
//var Logger *logrus.Logger
//
//func init() {
// Logger = logrus.New()
// Logger.SetLevel(config.Log.Level)
// 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|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")
// }
// 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)
// }
// }
//}

View File

@@ -2,6 +2,7 @@ package util
import "fmt"
func FormatTime(time int) string {
return fmt.Sprintf("%01d:%02d", time/60, time%60)
// FormatTime formats time in seconds to string in format "m:ss"
func FormatTime(sec int) string {
return fmt.Sprintf("%01d:%02d", sec/60, sec%60)
}

View File

@@ -1,5 +1,6 @@
package util
func GetWindowHandle(title string) uintptr {
return getWindowHandle(title)
//return getWindowHandle(title)
return 0
}

View File

@@ -1,8 +1,8 @@
package adapter
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/model"
"AynaLivePlayer/pkg/event"
)
type LiveClientCtor func(id string, ev *event.Manager, log ILogger) (LiveClient, error)

View File

@@ -1,8 +1,8 @@
package adapter
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/model"
"AynaLivePlayer/pkg/event"
)
type PlayerCtor func(ev *event.Manager, log ILogger) IPlayer

View File

@@ -1,8 +1,8 @@
package adapter
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/model"
"AynaLivePlayer/pkg/event"
)
type IPlaylist interface {

View File

@@ -1,8 +1,8 @@
package adapter
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/model"
"AynaLivePlayer/pkg/event"
)
// IControlBridge is the interface for all controller and

View File

@@ -1,54 +1,56 @@
package events
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/model"
)
//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"
// EventLyricUpdate event.EventId = "lyric.update"
// EventLyricReload event.EventId = "lyric.reload"
//)
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"
EventLyricUpdate event.EventId = "lyric.update"
EventLyricReload event.EventId = "lyric.reload"
)
const ErrorUpdate = "update.error"
func EventPlayerPropertyUpdate(property model.PlayerProperty) event.EventId {
return event.EventId("player.property.update." + string(property))
type ErrorUpdateEvent struct {
Error error
}
type PlaylistInsertEvent struct {
Playlist *model.Playlist
Index int
Media *model.Media
}
type PlaylistUpdateEvent struct {
Playlist *model.Playlist // Playlist is a copy of the playlist
}
type PlayEvent struct {
Media *model.Media
}
type LyricUpdateEvent struct {
Lyrics *model.Lyric
Time float64
Lyric *model.LyricContext
}
type LyricReloadEvent struct {
Lyrics *model.Lyric
}
type PlayerPropertyUpdateEvent struct {
Property model.PlayerProperty
Value model.PlayerPropertyValue
}
type LiveRoomStatusUpdateEvent struct {
RoomTitle string
Status bool
}
//
//func EventPlayerPropertyUpdate(property model.PlayerProperty) event.EventId {
// return event.EventId("player.property.update." + string(property))
//}
//
//type PlaylistInsertEvent struct {
// Playlist *model.Playlist
// Index int
// Media *model.Media
//}
//
//type PlaylistUpdateEvent struct {
// Playlist *model.Playlist // Playlist is a copy of the playlist
//}
//
//type PlayEvent struct {
// Media *model.Media
//}
//
//type LyricUpdateEvent struct {
// Lyrics *model.Lyric
// Time float64
// Lyric *model.LyricContext
//}
//
//type LyricReloadEvent struct {
// Lyrics *model.Lyric
//}
//
//type PlayerPropertyUpdateEvent struct {
// Property model.PlayerProperty
// Value model.PlayerPropertyValue
//}
//
//type LiveRoomStatusUpdateEvent struct {
// RoomTitle string
// Status bool
//}

View File

@@ -1,16 +1,73 @@
package events
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
liveroomsdk "github.com/AynaLivePlayer/liveroom-sdk"
)
const (
LiveRoomStatusChange event.EventId = "liveclient.status.change"
LiveRoomMessageReceive event.EventId = "liveclient.message.receive"
)
//const (
// LiveRoomStatusChange event.EventId = "liveclient.status.change"
// LiveRoomMessageReceive event.EventId = "liveclient.message.receive"
//)
//
//type StatusChangeEvent struct {
// Connected bool
// Client adapter.LiveClient
//}
type StatusChangeEvent struct {
Connected bool
Client adapter.LiveClient
const LiveRoomAddCmd = "cmd.liveroom.add"
type LiveRoomAddCmdEvent struct {
Title string
Provider string
RoomKey string
}
const LiveRoomProviderUpdate = "update.liveroom.provider"
type LiveRoomProviderUpdateEvent struct {
Providers []model.LiveRoomProviderInfo
}
const LiveRoomRemoveCmd = "cmd.liveroom.remove"
type LiveRoomRemoveCmdEvent struct {
Identifier string
}
const LiveRoomRoomsUpdate = "update.liveroom.rooms"
type LiveRoomRoomsUpdateEvent struct {
Rooms []model.LiveRoom
}
const LiveRoomStatusUpdate = "update.liveroom.status"
type LiveRoomStatusUpdateEvent struct {
Room model.LiveRoom
}
const LiveRoomConfigChangeCmd = "cmd.liveroom.config.change"
type LiveRoomConfigChangeCmdEvent struct {
Identifier string
Config model.LiveRoomConfig
}
const LiveRoomOperationCmd = "cmd.liveroom.operation"
type LiveRoomOperationCmdEvent struct {
Identifier string
SetConnect bool // connect or disconnect
}
const LiveRoomOperationFinish = "update.liveroom.operation"
type LiveRoomOperationFinishEvent struct {
}
const LiveRoomMessageReceive = "update.liveroom.message"
type LiveRoomMessageReceiveEvent struct {
Message *liveroomsdk.Message
}

View File

@@ -0,0 +1,37 @@
package events
import (
"AynaLivePlayer/core/model"
)
const PlayerVolumeChangeCmd = "cmd.player.op.change_volume"
type PlayerVolumeChangeCmdEvent struct {
Volume float64 // Volume from 0-100
}
const PlayerPlayCmd = "cmd.player.op.play"
type PlayerPlayCmdEvent struct {
Media model.Media
}
const PlayerSeekCmd = "cmd.player.op.seek"
type PlayerSeekCmdEvent struct {
Position float64
// Absolute is the seek mode.
// if absolute = true : position is the time in second
// if absolute = false: position is in percentage eg 0.1 0.2
Absolute bool
}
const PlayerToggleCmd = "cmd.player.op.toggle"
type PlayerToggleCmdEvent struct {
}
const PlayerPlayNextCmd = "cmd.player.op.next"
type PlayerPlayNextCmdEvent struct {
}

View File

@@ -0,0 +1,23 @@
package events
import "github.com/AynaLivePlayer/miaosic"
const PlayerLyricRequestCmd = "cmd.player.lyric.request"
type PlayerLyricRequestCmdEvent struct {
}
const PlayerLyricReload = "update.player.lyric.reload"
type PlayerLyricReloadEvent struct {
Lyrics miaosic.Lyrics
}
const PlayerLyricPosUpdate = "update.player.lyric.pos"
type PlayerLyricPosUpdateEvent struct {
Time float64
CurrentIndex int // -1 means no lyric
CurrentLine miaosic.LyricLine
Total int // total lyric count
}

View File

@@ -0,0 +1,46 @@
package events
import "AynaLivePlayer/core/model"
const PlayerPlayingUpdate = "update.player.playing"
type PlayerPlayingUpdateEvent struct {
Media model.Media
Removed bool // if no media is playing, removed is true
}
const PlayerPropertyPauseUpdate = "update.player.property.pause"
type PlayerPropertyPauseUpdateEvent struct {
Paused bool
}
const PlayerPropertyPercentPosUpdate = "update.player.property.percent_pos"
type PlayerPropertyPercentPosUpdateEvent struct {
PercentPos float64
}
const PlayerPropertyIdleActiveUpdate = "update.player.property.idle_active"
type PlayerPropertyIdleActiveUpdateEvent struct {
IsIdle bool
}
const PlayerPropertyTimePosUpdate = "update.player.property.time_pos"
type PlayerPropertyTimePosUpdateEvent struct {
TimePos float64 // Time in seconds
}
const PlayerPropertyDurationUpdate = "update.player.property.duration"
type PlayerPropertyDurationUpdateEvent struct {
Duration float64 // Duration in seconds
}
const PlayerPropertyVolumeUpdate = "update.player.property.volume"
type PlayerPropertyVolumeUpdateEvent struct {
Volume float64 // Volume from 0-100
}

View File

@@ -0,0 +1,7 @@
package events
const PlayerVideoPlayerSetWindowHandleCmd = "cmd.player.videoplayer.set_window_handle"
type PlayerVideoPlayerSetWindowHandleCmdEvent struct {
Handle uintptr
}

81
core/events/playlist.go Normal file
View File

@@ -0,0 +1,81 @@
package events
import (
"AynaLivePlayer/core/model"
"AynaLivePlayer/pkg/event"
)
func PlaylistDetailUpdate(id model.PlaylistID) event.EventId {
return event.EventId("update.playlist.detail." + id)
}
type PlaylistDetailUpdateEvent struct {
Medias []model.Media
}
func PlaylistMoveCmd(id model.PlaylistID) event.EventId {
return event.EventId("cmd.playlist.move." + id)
}
type PlaylistMoveCmdEvent struct {
From int
To int
}
func PlaylistDeleteCmd(id model.PlaylistID) event.EventId {
return event.EventId("cmd.playlist.delete." + id)
}
type PlaylistDeleteCmdEvent struct {
Index int
}
func PlaylistInsertCmd(id model.PlaylistID) event.EventId {
return event.EventId("cmd.playlist.insert." + id)
}
type PlaylistInsertCmdEvent struct {
Position int // position to insert, -1 means last one
Media model.Media
}
func PlaylistInsertUpdate(id model.PlaylistID) event.EventId {
return event.EventId("update.playlist.insert." + id)
}
type PlaylistInsertUpdateEvent struct {
Position int // position to insert, -1 means last one
Media model.Media
}
func PlaylistNextCmd(id model.PlaylistID) event.EventId {
return event.EventId("cmd.playlist.next." + id)
}
type PlaylistNextCmdEvent struct {
Remove bool // remove the media after next
}
func PlaylistNextUpdate(id model.PlaylistID) event.EventId {
return event.EventId("update.playlist.next." + id)
}
type PlaylistNextUpdateEvent struct {
Media model.Media
}
func PlaylistModeChangeCmd(id model.PlaylistID) event.EventId {
return event.EventId("cmd.playlist.mode." + id)
}
type PlaylistModeChangeCmdEvent struct {
Mode model.PlaylistMode
}
func PlaylistModeChangeUpdate(id model.PlaylistID) event.EventId {
return event.EventId("update.playlist.mode." + id)
}
type PlaylistModeChangeUpdateEvent struct {
Mode model.PlaylistMode
}

24
core/events/search.go Normal file
View File

@@ -0,0 +1,24 @@
package events
import (
"AynaLivePlayer/core/model"
)
const SearchCmd = "cmd.search"
type SearchCmdEvent struct {
Keyword string
Provider string
}
const SearchResultUpdate = "update.search_result"
type SearchResultUpdateEvent struct {
Medias []model.Media
}
const SearchProviderUpdate = "update.search.provider.update"
type SearchProviderUpdateEvent struct {
Providers []string
}

View File

@@ -1,40 +1,26 @@
package model
import (
"fmt"
)
import "github.com/AynaLivePlayer/liveroom-sdk"
type LiveRoomConfig struct {
AutoConnect bool `json:"auto_connect"`
}
type LiveRoom struct {
ClientName string
ID string
Title string
AutoConnect bool
AutoReconnect bool
LiveRoom liveroom.LiveRoom `json:"live_room"`
Config LiveRoomConfig `json:"config"`
Title string `json:"title"`
Status bool `json:"-"`
}
func (r *LiveRoom) String() string {
return fmt.Sprintf("<LiveRooms %s:%s>", r.ClientName, r.ID)
func (r *LiveRoom) DisplayName() string {
if r.Title != "" {
return r.Title
}
return r.LiveRoom.Identifier()
}
func (r *LiveRoom) Identifier() string {
return fmt.Sprintf("%s_%s", r.ClientName, r.ID)
}
type UserMedal struct {
Name string
Level int
RoomID string
}
type DanmuUser struct {
Uid string
Username string
Medal UserMedal
Admin bool
Privilege int
}
type DanmuMessage struct {
User DanmuUser
Message string
type LiveRoomProviderInfo struct {
Name string
Description string
}

View File

@@ -1,117 +0,0 @@
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,31 +0,0 @@
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 := LoadLyric(testLyric)
for _, lrc := range lryic.Lyrics {
fmt.Println(lrc)
}
}
func TestLyricFind(t *testing.T) {
lryic := LoadLyric(testLyric)
fmt.Println(lryic.Find(90.4))
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.Find(i), lryic.Find(i))
}
}

View File

@@ -1,46 +1,38 @@
package model
import (
"github.com/jinzhu/copier"
"github.com/AynaLivePlayer/liveroom-sdk"
"github.com/AynaLivePlayer/miaosic"
)
type Picture struct {
Url string
Data []byte
type User struct {
Name string
}
func (p Picture) Exists() bool {
return p.Url != "" || p.Data != nil
}
var PlaylistUser = User{Name: "Playlists"}
var SystemUser = User{Name: "System"}
var HistoryUser = User{Name: "History"}
type Media struct {
Title string
Artist string
Cover Picture
Album string
Lyric string
Url string
Header map[string]string
User interface{}
Meta interface{}
Info miaosic.MediaInfo
User interface{}
}
func (m *Media) ToUser() *User {
if u, ok := m.User.(*User); ok {
func (m *Media) IsLiveRoomUser() bool {
_, ok := m.User.(liveroom.User)
return ok
}
func (m *Media) ToUser() User {
if u, ok := m.User.(User); ok {
return u
}
return &User{Name: m.DanmuUser().Username}
return User{Name: m.DanmuUser().Username}
}
func (m *Media) DanmuUser() *DanmuUser {
if u, ok := m.User.(*DanmuUser); ok {
func (m *Media) DanmuUser() liveroom.User {
if u, ok := m.User.(liveroom.User); ok {
return u
}
return nil
}
func (m *Media) Copy() *Media {
newMedia := &Media{}
copier.Copy(newMedia, m)
return newMedia
return liveroom.User{}
}

View File

@@ -1,30 +0,0 @@
package model
import (
"fmt"
"testing"
)
type A struct {
A string
}
type B struct {
B string
}
func TestStruct(t *testing.T) {
var x interface{} = &A{A: "123"}
y, ok := x.(*A)
fmt.Println(y, ok)
z, ok := x.(*B)
fmt.Println(z, ok)
}
func TestMedia_Copy(t *testing.T) {
m := &Media{Title: "asdf", User: &User{Name: "123"}}
m2 := m.Copy()
fmt.Println(m, m2)
m2.User.(*User).Name = "456"
fmt.Println(m.User.(*User).Name, m2)
}

View File

@@ -1,7 +1,5 @@
package model
import "fmt"
type PlaylistMode int
const (
@@ -10,28 +8,11 @@ const (
PlaylistModeRepeat
)
type Playlist struct {
Title string // can be same, display name
Medias []*Media
Mode PlaylistMode
Meta Meta
}
type PlaylistID string
func (p Playlist) String() string {
return fmt.Sprintf("<Playlist %s len:%d>", p.Title, len(p.Medias))
}
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{
Title: p.Title,
Medias: medias,
Mode: p.Mode,
Meta: p.Meta,
}
}
const (
PlaylistIDPlayer PlaylistID = "player"
PlaylistIDSystem PlaylistID = "system"
PlaylistIDHistory PlaylistID = "history"
PlaylistIDPlaylists PlaylistID = "playlists"
)

View File

@@ -1,16 +0,0 @@
package model
import "fmt"
type Meta struct {
Name string
Id string
}
func (m Meta) String() string {
return fmt.Sprintf("<Meta %s:%s>", m.Name, m.Id)
}
func (m Meta) Identifier() string {
return fmt.Sprintf("%s_%s", m.Name, m.Id)
}

View File

@@ -1,17 +0,0 @@
package model
type User struct {
Name string
}
func ApplyUser(medias []*Media, user interface{}) {
for _, m := range medias {
m.User = user
}
}
func ToSpMedia(media *Media, user *User) *Media {
media = media.Copy()
media.User = user
return media
}

10
global/global.go Normal file
View File

@@ -0,0 +1,10 @@
package global
import (
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/logger"
)
var Logger logger.ILogger = nil
var EventManager *event.Manager = nil

83
go.mod
View File

@@ -2,55 +2,70 @@ module AynaLivePlayer
go 1.19
require (
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.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/gorilla/websocket v1.5.0
github.com/jinzhu/copier v0.3.5
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/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/cast v1.5.0
github.com/stretchr/testify v1.8.0
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
golang.org/x/sys v0.1.0
gopkg.in/ini.v1 v1.66.4
replace (
github.com/AynaLivePlayer/liveroom-sdk v0.1.0 => ./pkg/liveroom-sdk // submodule
github.com/AynaLivePlayer/miaosic v0.1.5 => ./pkg/miaosic // submodule
)
require (
fyne.io/systray v1.10.1-0.20230207085535-4a244dbb9d03 // indirect
fyne.io/fyne/v2 v2.4.4
fyne.io/x/fyne v0.0.0-20240326131024-3ba9170cc3be
github.com/AynaLivePlayer/liveroom-sdk v0.1.0
github.com/AynaLivePlayer/miaosic v0.1.5
github.com/XiaoMengXinX/Music163Api-Go v0.1.30
github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/aynakeya/go-mpv v0.0.6
github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b
github.com/go-resty/resty/v2 v2.7.0
github.com/gorilla/websocket v1.5.0
github.com/jinzhu/copier v0.4.0
github.com/mattn/go-colorable v0.1.12
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/sirupsen/logrus v1.9.3
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.8.4
github.com/tidwall/gjson v1.16.0
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
go.uber.org/zap v1.26.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
golang.org/x/sys v0.13.0
gopkg.in/ini.v1 v1.67.0
)
require (
fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e // indirect
github.com/PuerkitoBio/goquery v1.7.1 // indirect
github.com/andybalholm/cascadia v1.2.0 // indirect
github.com/aynakeya/deepcolor v1.0.2 // indirect
github.com/aynakeya/open-bilibili-live v0.0.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v0.1.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fredbi/uri v1.0.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // 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-20221017161538-93cebf72946b // indirect
github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 // indirect
github.com/go-text/typesetting v0.1.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/srwiley/oksvg v0.0.0-20220731023508-a61f04f16b76 // indirect
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // 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.13 // 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/text v0.3.7 // indirect
github.com/yuin/goldmark v1.5.5 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/image v0.11.0 // indirect
golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/text v0.13.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
)

173
go.sum
View File

@@ -37,27 +37,32 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
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/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE=
fyne.io/systray v1.10.1-0.20230207085535-4a244dbb9d03 h1:zb84y6X6lIi4Aeo8ehu6Qtu2fipBZ6Wzp41Ki/F4qdg=
fyne.io/systray v1.10.1-0.20230207085535-4a244dbb9d03/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE=
fyne.io/fyne/v2 v2.4.4 h1:4efSRpoikcGbqQN83yzC9WmF8UNq9olsaJQ/Ejme6Z8=
fyne.io/fyne/v2 v2.4.4/go.mod h1:VyrxAOZ3NRZRWBvNIJbfqoKOG4DdbewoPk7ozqJKNPY=
fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e h1:Hvs+kW2VwCzNToF3FmnIAzmivNgrclwPgoUdVSrjkP8=
fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE=
fyne.io/x/fyne v0.0.0-20240326131024-3ba9170cc3be h1:HBAwKsmfTO4r1Ndlksy5oXIxgWiazj0gu2qmhOmkRFU=
fyne.io/x/fyne v0.0.0-20240326131024-3ba9170cc3be/go.mod h1:1pa3ZVIopRWNvfSG4ZrSkcZ3mJ8qoHPZv4PT8/zpn1o=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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/PuerkitoBio/goquery v1.7.1 h1:oE+T06D+1T7LNrn91B4aERsRIeCLJ/oPSa6xB9FPnz4=
github.com/PuerkitoBio/goquery v1.7.1/go.mod h1:XY0pP4kfraEmmV1O7Uf6XyjoslwsneBbgeDjLYuN8xY=
github.com/XiaoMengXinX/Music163Api-Go v0.1.30 h1:MqRItDFtX1J0JTlFtwN2RwjsYMA7/g/+cTjcOJXy19s=
github.com/XiaoMengXinX/Music163Api-Go v0.1.30/go.mod h1:kLU/CkLxKnEJFCge0URvQ0lHt6ImoG1/2aVeNbgV2RQ=
github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
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/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/deepcolor v1.0.2 h1:ldmVMweZTX3uLFpRQBtNyapGMXZ+UqkDp2S08ROD0rE=
github.com/aynakeya/deepcolor v1.0.2/go.mod h1:9wdFsi0G4uAQlu58B2/eHBlGoQ8VkmSyPsK+bDZ+6dQ=
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/aynakeya/open-bilibili-live v0.0.5 h1:SJX+MA7nn3R30+zWJzpUH3E2SrDcDA8C6ZE2QhzT1U0=
github.com/aynakeya/open-bilibili-live v0.0.5/go.mod h1:8aYl0767J4wgBkJ6kgOpOuj6FcFFL8GN3HRHGGjYpSk=
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=
@@ -71,12 +76,11 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht
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/dhowden/tag v0.0.0-20230630033851-978a0926ee25 h1:simG0vMYFvNriGhaaat7QVVkaVkXzvqcohaBoLZl9Hg=
github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25/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=
@@ -85,13 +89,12 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
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/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8=
github.com/fredbi/uri v0.1.0 h1:8XBBD74STBLcWJ5smjEkKCZivSxSKMhFB0FbQUKeNyM=
github.com/fredbi/uri v0.1.0/go.mod h1:1xC40RnIOGCaQzswaOvrzvG/3M3F0hyDVb3aO/1iGy0=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/fredbi/uri v1.0.0 h1:s4QwUAZ8fz+mbTsukND+4V5f+mJ/wjaTokwstGUAemg=
github.com/fredbi/uri v1.0.0/go.mod h1:1xC40RnIOGCaQzswaOvrzvG/3M3F0hyDVb3aO/1iGy0=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
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=
@@ -107,17 +110,17 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/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/go-text/render v0.0.0-20230619120952-35bccb6164b8 h1:VkKnvzbvHqgEfm351rfr8Uclu5fnwq8HP2ximUzJsBM=
github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8/go.mod h1:h29xCucjNsDcYb7+0rJokxVwYAq+9kQ19WiFuBKkYtc=
github.com/go-text/typesetting v0.1.0 h1:vioSaLPYcHwPEPLT7gsjCGDCoYSbljxoHJzMnKwVvHw=
github.com/go-text/typesetting v0.1.0/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
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/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c h1:JGCm/+tJ9gC6THUxooTldS+CUDsba0qvkvU3DHklqW8=
github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c/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=
@@ -161,7 +164,7 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
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/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
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=
@@ -179,15 +182,14 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe
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/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/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=
@@ -216,10 +218,8 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
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 v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
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=
@@ -230,16 +230,18 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
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/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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/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-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
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/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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=
@@ -256,8 +258,6 @@ github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJE
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/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=
@@ -268,35 +268,32 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
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/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
github.com/srwiley/oksvg v0.0.0-20220731023508-a61f04f16b76 h1:Ga2uagHhDeGysCixLAzH0mS2TU+CrbQavmsHUNkEEVA=
github.com/srwiley/oksvg v0.0.0-20220731023508-a61f04f16b76/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 h1:oDMiXaTMyBEuZMU53atpxqYsSB3U1CHkeAu2zr6wTeY=
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -306,23 +303,19 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
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/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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.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/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg=
github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
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/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
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=
@@ -331,8 +324,9 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
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.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU=
github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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=
@@ -344,8 +338,13 @@ 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/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
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=
@@ -354,6 +353,7 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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/crypto v0.0.0-20210921155107-089bfa567519/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=
@@ -365,12 +365,12 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
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/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
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/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
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=
@@ -385,8 +385,9 @@ golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPI
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/mobile v0.0.0-20230531173138-3c911d8e3eda h1:O+EUvnBNPwI4eLthn8W5K+cS8zQZfgTABPLNm6Bna34=
golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
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=
@@ -396,6 +397,9 @@ 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
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=
@@ -432,10 +436,13 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
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-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
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=
@@ -459,6 +466,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
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/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/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=
@@ -470,10 +479,8 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
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=
@@ -506,10 +513,18 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
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/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
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=
@@ -518,8 +533,11 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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=
@@ -576,6 +594,8 @@ 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/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -683,13 +703,12 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -1,107 +0,0 @@
package gui
import (
"AynaLivePlayer/common/config"
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/core/model"
"AynaLivePlayer/gui/component"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
type bascicConfig struct {
panel fyne.CanvasObject
}
func (b *bascicConfig) Title() string {
return i18n.T("gui.config.basic.title")
}
func (b *bascicConfig) Description() string {
return i18n.T("gui.config.basic.description")
}
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")),
newCheckInit(
i18n.T("gui.config.basic.random_playlist.user"),
func(b bool) {
l().Infof("Set random playlist for user: %t", b)
if b {
API.Playlists().GetCurrent().Model().Mode = model.PlaylistModeRandom
} else {
API.Playlists().GetCurrent().Model().Mode = model.PlaylistModeNormal
}
},
API.Playlists().GetCurrent().Model().Mode == model.PlaylistModeRandom),
newCheckInit(
i18n.T("gui.config.basic.random_playlist.system"),
func(b bool) {
l().Infof("Set random playlist for system: %t", b)
if b {
API.Playlists().GetDefault().Model().Mode = model.PlaylistModeRandom
} else {
API.Playlists().GetDefault().Model().Mode = model.PlaylistModeNormal
}
},
API.Playlists().GetDefault().Model().Mode == model.PlaylistModeRandom),
)
devices := API.PlayControl().GetAudioDevices()
deviceDesc := make([]string, len(devices))
deviceDesc2Name := make(map[string]string)
for i, device := range devices {
deviceDesc[i] = device.Description
deviceDesc2Name[device.Description] = device.Name
}
deviceSel := widget.NewSelect(deviceDesc, func(s string) {
API.PlayControl().SetAudioDevice(deviceDesc2Name[s])
})
deviceSel.Selected = API.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")),
component.NewCheckOneWayBinding(
i18n.T("gui.config.basic.skip_playlist.prompt"),
&API.PlayControl().Config().SkipPlaylist,
API.PlayControl().Config().SkipPlaylist),
)
skipWhenErr := container.NewHBox(
widget.NewLabel(i18n.T("gui.config.basic.skip_when_error")),
component.NewCheckOneWayBinding(
i18n.T("gui.config.basic.skip_when_error.prompt"),
&API.PlayControl().Config().AutoNextWhenFail,
API.PlayControl().Config().AutoNextWhenFail),
)
checkUpdateBox := container.NewHBox(
widget.NewLabel(i18n.T("gui.config.basic.auto_check_update")),
component.NewCheckOneWayBinding(
i18n.T("gui.config.basic.auto_check_update.prompt"),
&config.General.AutoCheckUpdate,
config.General.AutoCheckUpdate),
)
checkUpdateBtn := widget.NewButton(i18n.T("gui.config.basic.check_update"), func() {
err := API.App().CheckUpdate()
if err != nil {
showDialogIfError(err)
return
}
if API.App().LatestVersion().Version > API.App().Version().Version {
dialog.ShowCustom(
i18n.T("gui.update.new_version"),
"OK",
widget.NewRichTextFromMarkdown(API.App().LatestVersion().Info),
MainWindow)
}
})
b.panel = container.NewVBox(randomPlaylist, outputDevice, skipPlaylist, skipWhenErr, checkUpdateBox, checkUpdateBtn)
return b.panel
}

View File

@@ -1,40 +0,0 @@
package gui
import (
"AynaLivePlayer/gui/component"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
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("")
},
func(id widget.ListItemID, object fyne.CanvasObject) {
object.(*widget.Label).SetText(ConfigList[id].Title())
})
entryList.OnSelected = func(id widget.ListItemID) {
desc := widget.NewRichTextFromMarkdown("## " + ConfigList[id].Title() + " \n\n" + ConfigList[id].Description())
for i := range desc.Segments {
if seg, ok := desc.Segments[i].(*widget.TextSegment); ok {
seg.Style.Alignment = fyne.TextAlignCenter
}
}
content.Objects = []fyne.CanvasObject{
container.NewVScroll(container.NewVBox(desc, widget.NewSeparator(), ConfigList[id].CreatePanel())),
}
content.Refresh()
}
return component.NewFixedSplitContainer(entryList, content, true, 0.23)
}

View File

@@ -1,42 +1,42 @@
package gui
import (
"AynaLivePlayer/common/config"
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
config2 "AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"AynaLivePlayer/resource"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/widget"
_logger "AynaLivePlayer/pkg/logger"
)
var API adapter.IControlBridge
var App fyne.App
var MainWindow fyne.Window
var playerWindow fyne.Window
var playerWindowHandle uintptr
func l() adapter.ILogger {
return API.Logger().WithModule("GUI")
}
var logger _logger.ILogger = nil
func black_magic() {
widget.RichTextStyleStrong.TextStyle.Bold = false
}
func Initialize() {
logger = global.Logger.WithPrefix("GUI")
black_magic()
l().Info("Initializing GUI")
logger.Info("Initializing GUI")
//os.Setenv("FYNE_FONT", config.GetAssetPath("msyh.ttc"))
App = app.New()
App.Settings().SetTheme(&myTheme{})
MainWindow = App.NewWindow(fmt.Sprintf("%s Ver.%s", config.ProgramName, model.Version(config.Version)))
MainWindow = App.NewWindow(fmt.Sprintf("%s Ver %s", config2.ProgramName, model.Version(config2.Version)))
tabs := container.NewAppTabs(
container.NewTabItem(i18n.T("gui.tab.player"),
@@ -48,15 +48,15 @@ func Initialize() {
container.NewTabItem(i18n.T("gui.tab.room"),
container.NewBorder(nil, nil, createRoomSelector(), nil, createRoomController()),
),
container.NewTabItem(i18n.T("gui.tab.playlist"),
container.NewBorder(nil, nil, createPlaylists(), nil, createPlaylistMedias()),
),
container.NewTabItem(i18n.T("gui.tab.history"),
container.NewBorder(nil, nil, nil, nil, createHistoryList()),
),
container.NewTabItem(i18n.T("gui.tab.config"),
createConfigLayout(),
),
//container.NewTabItem(i18n.T("gui.tab.playlist"),
// container.NewBorder(nil, nil, createPlaylists(), nil, createPlaylistMedias()),
//),
//container.NewTabItem(i18n.T("gui.tab.history"),
// container.NewBorder(nil, nil, nil, nil, createHistoryList()),
//),
//container.NewTabItem(i18n.T("gui.tab.config"),
// createConfigLayout(),
//),
)
tabs.SetTabLocation(container.TabLocationTop)
@@ -65,56 +65,41 @@ func Initialize() {
//MainWindow.Resize(fyne.NewSize(1280, 720))
MainWindow.Resize(fyne.NewSize(960, 480))
playerWindow = App.NewWindow("CorePlayerPreview")
playerWindow.Resize(fyne.NewSize(480, 240))
playerWindow.SetCloseIntercept(func() {
playerWindow.Hide()
})
MainWindow.SetOnClosed(func() {
playerWindow.Close()
})
playerWindow.Hide()
setupPlayerWindow()
// register error
global.EventManager.RegisterA(
events.ErrorUpdate, "gui.show_error", func(e *event.Event) {
err := e.Data.(events.ErrorUpdateEvent).Error
logger.Warnf("gui received error event: %v, %v", err, err == nil)
if err == nil {
return
}
dialog.ShowError(err, MainWindow)
})
//MainWindow.SetFixedSize(true)
if config.General.AutoCheckUpdate {
go checkUpdate()
}
//if config2.General.AutoCheckUpdate {
// go checkUpdate()
//}
}
func showPlayerWindow() {
playerWindow.Show()
if playerWindowHandle == 0 {
playerWindowHandle = util.GetWindowHandle("CorePlayerPreview")
l().Infof("video output window handle: %d", playerWindowHandle)
if playerWindowHandle != 0 {
_ = API.PlayControl().GetPlayer().SetWindowHandle(playerWindowHandle)
}
}
}
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")
API.PlayControl().PlayNext()
})
}
func checkUpdate() {
l().Info("checking updates...")
err := API.App().CheckUpdate()
if err != nil {
showDialogIfError(err)
l().Warnf("check update failed", err)
return
}
l().Infof("latest version: v%s", API.App().LatestVersion().Version)
if API.App().LatestVersion().Version > API.App().Version().Version {
l().Info("new update available")
dialog.ShowCustom(
i18n.T("gui.update.new_version"),
"OK",
widget.NewRichTextFromMarkdown(API.App().LatestVersion().Info),
MainWindow)
}
}
//
//func checkUpdate() {
// l().Info("checking updates...")
// err := API.App().CheckUpdate()
// if err != nil {
// showDialogIfError(err)
// l().Warnf("check update failed", err)
// return
// }
// l().Infof("latest version: v%s", API.App().LatestVersion().Version)
// if API.App().LatestVersion().Version > API.App().Version().Version {
// l().Info("new update available")
// dialog.ShowCustom(
// i18n.T("gui.update.new_version"),
// "OK",
// widget.NewRichTextFromMarkdown(API.App().LatestVersion().Info),
// MainWindow)
// }
//}

View File

@@ -1,12 +1,12 @@
package gutil
import (
"AynaLivePlayer/core/model"
"bytes"
"errors"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/storage"
"github.com/AynaLivePlayer/miaosic"
"github.com/nfnt/resize"
"image"
"image/png"
@@ -27,7 +27,7 @@ func ResizeImage(resource fyne.Resource, width int, height int) fyne.Resource {
return fyne.NewStaticResource(resource.Name(), buf.Bytes())
}
func NewImageFromPlayerPicture(picture model.Picture) (*canvas.Image, error) {
func NewImageFromPlayerPicture(picture miaosic.Picture) (*canvas.Image, error) {
var img *canvas.Image
if picture.Data != nil {
img = canvas.NewImageFromReader(bytes.NewReader(picture.Data), "cover")

View File

@@ -3,7 +3,6 @@ package gui
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
@@ -35,12 +34,6 @@ func newContextMenuButton(label string, menu *fyne.Menu) *ContextMenuButton {
return b
}
func showDialogIfError(err error) {
if err != nil {
dialog.ShowError(err, MainWindow)
}
}
func newCheckInit(name string, changed func(bool), checked bool) *widget.Check {
check := widget.NewCheck(name, changed)
check.SetChecked(checked)

View File

@@ -1,79 +0,0 @@
package gui
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/internal"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"sync"
)
var History = &struct {
Playlist *model.Playlist
List *widget.List
mux sync.RWMutex
}{}
func createHistoryList() fyne.CanvasObject {
History.Playlist = API.Playlists().GetHistory().Model().Copy()
History.List = widget.NewList(
func() int {
return History.Playlist.Size()
},
func() fyne.CanvasObject {
return container.NewBorder(nil, nil,
widget.NewLabel("index"),
container.NewHBox(
widget.NewButtonWithIcon("", theme.MediaPlayIcon(), nil),
widget.NewButtonWithIcon("", theme.ContentAddIcon(), nil),
),
container.NewGridWithColumns(3,
newLabelWithWrapping("title", fyne.TextTruncate),
newLabelWithWrapping("artist", fyne.TextTruncate),
newLabelWithWrapping("user", fyne.TextTruncate)))
},
func(id widget.ListItemID, object fyne.CanvasObject) {
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(
m.Artist)
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[2].(*widget.Label).SetText(
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 = internal.HistoryUser
btns[0].(*widget.Button).OnTapped = func() {
showDialogIfError(API.PlayControl().Play(m))
}
btns[1].(*widget.Button).OnTapped = func() {
API.Playlists().GetCurrent().Push(m)
}
})
registerHistoryHandler()
return container.NewBorder(
container.NewBorder(nil, nil,
widget.NewLabel("#"), widget.NewLabel(i18n.T("gui.history.operation")),
container.NewGridWithColumns(3,
widget.NewLabel(i18n.T("gui.history.title")),
widget.NewLabel(i18n.T("gui.history.artist")),
widget.NewLabel(i18n.T("gui.history.user")))),
nil, nil, nil,
History.List,
)
}
func registerHistoryHandler() {
API.Playlists().GetHistory().EventManager().RegisterA(events.EventPlaylistUpdate, "gui.history.update", func(event *event.Event) {
History.mux.RLock()
History.Playlist = event.Data.(events.PlaylistUpdateEvent).Playlist
History.List.Refresh()
History.mux.RUnlock()
})
}

View File

@@ -1,15 +0,0 @@
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...)
}

View File

@@ -1,14 +1,17 @@
package gui
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"sync"
)
var RoomTab = &struct {
@@ -22,23 +25,40 @@ var RoomTab = &struct {
AutoConnect *widget.Check
ConnectBtn *widget.Button
DisConnectBtn *widget.Button
providers []model.LiveRoomProviderInfo
rooms []model.LiveRoom
lock sync.RWMutex
}{}
func createRoomSelector() fyne.CanvasObject {
RoomTab.Rooms = widget.NewList(
func() int {
return API.LiveRooms().Size()
return len(RoomTab.rooms)
},
func() fyne.CanvasObject {
return widget.NewLabel("AAAAAAAAAAAAAAAA")
},
func(id widget.ListItemID, object fyne.CanvasObject) {
object.(*widget.Label).SetText(
API.LiveRooms().Get(id).DisplayName())
RoomTab.rooms[id].DisplayName())
})
RoomTab.AddBtn = widget.NewButton(i18n.T("gui.room.button.add"), func() {
clientNameEntry := widget.NewSelect(API.LiveRooms().GetAllClientNames(), nil)
providerNames := make([]string, len(RoomTab.providers))
for i := 0; i < len(RoomTab.providers); i++ {
providerNames[i] = RoomTab.providers[i].Name
}
descriptionLabel := widget.NewLabel(i18n.T("gui.room.add.prompt"))
clientNameEntry := widget.NewSelect(providerNames, func(s string) {
for i := 0; i < len(RoomTab.providers); i++ {
if RoomTab.providers[i].Name == s {
descriptionLabel.SetText(i18n.T(RoomTab.providers[i].Description))
break
}
descriptionLabel.SetText("")
}
})
idEntry := widget.NewEntry()
nameEntry := widget.NewEntry()
dia := dialog.NewCustomConfirm(
i18n.T("gui.room.add.title"),
i18n.T("gui.room.add.confirm"),
@@ -46,18 +66,25 @@ func createRoomSelector() fyne.CanvasObject {
container.NewVBox(
container.New(
layout.NewFormLayout(),
widget.NewLabel(i18n.T("gui.room.add.name")),
nameEntry,
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")),
descriptionLabel,
),
func(b bool) {
if b && len(clientNameEntry.Selected) > 0 && len(idEntry.Text) > 0 {
_, err := API.LiveRooms().AddRoom(clientNameEntry.Selected, idEntry.Text)
showDialogIfError(err)
RoomTab.Rooms.Refresh()
logger.Infof("Add room %s %s", clientNameEntry.Selected, idEntry.Text)
global.EventManager.CallA(
events.LiveRoomAddCmd,
events.LiveRoomAddCmdEvent{
Title: nameEntry.Text,
Provider: clientNameEntry.Selected,
RoomKey: idEntry.Text,
})
}
},
MainWindow,
@@ -66,36 +93,32 @@ func createRoomSelector() fyne.CanvasObject {
dia.Show()
})
RoomTab.RemoveBtn = widget.NewButton(i18n.T("gui.room.button.remove"), func() {
showDialogIfError(API.LiveRooms().DeleteRoom(PlaylistManager.Index))
RoomTab.Rooms.Select(0)
RoomTab.Rooms.Refresh()
if len(RoomTab.rooms) == 0 {
return
}
global.EventManager.CallA(
events.LiveRoomRemoveCmd,
events.LiveRoomRemoveCmdEvent{
Identifier: RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier(),
})
})
RoomTab.Rooms.OnSelected = func(id widget.ListItemID) {
rom := API.LiveRooms().Get(PlaylistManager.Index)
if rom != nil {
rom.EventManager().Unregister("gui.liveroom.status")
if id >= len(RoomTab.rooms) {
return
}
RoomTab.Index = id
rom = API.LiveRooms().Get(RoomTab.Index)
rom.EventManager().RegisterA(events.LiveRoomStatusChange, "gui.liveroom.status", func(event *event.Event) {
d := event.Data.(events.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.DisplayName())
RoomTab.RoomID.SetText(rom.Identifier())
RoomTab.AutoConnect.SetChecked(rom.Model().AutoConnect)
if API.LiveRooms().GetRoomStatus(RoomTab.Index) {
room := RoomTab.rooms[RoomTab.Index]
RoomTab.RoomTitle.SetText(room.DisplayName())
RoomTab.RoomID.SetText(room.LiveRoom.Identifier())
RoomTab.AutoConnect.SetChecked(room.Config.AutoConnect)
if room.Status {
RoomTab.Status.SetText(i18n.T("gui.room.status.connected"))
} else {
RoomTab.Status.SetText(i18n.T("gui.room.status.disconnected"))
}
RoomTab.Status.Refresh()
}
registerRoomHandlers()
return container.NewHBox(
container.NewBorder(
nil, container.NewCenter(container.NewHBox(RoomTab.AddBtn, RoomTab.RemoveBtn)),
@@ -106,25 +129,107 @@ func createRoomSelector() fyne.CanvasObject {
)
}
func registerRoomHandlers() {
global.EventManager.RegisterA(
events.LiveRoomProviderUpdate,
"gui.liveroom.provider_update",
func(event *event.Event) {
RoomTab.providers = event.Data.(events.LiveRoomProviderUpdateEvent).Providers
RoomTab.Rooms.Refresh()
})
global.EventManager.RegisterA(
events.LiveRoomRoomsUpdate,
"gui.liveroom.rooms_update",
func(event *event.Event) {
data := event.Data.(events.LiveRoomRoomsUpdateEvent)
RoomTab.lock.Lock()
RoomTab.rooms = data.Rooms
RoomTab.Rooms.Select(0)
RoomTab.Rooms.Refresh()
RoomTab.lock.Unlock()
})
global.EventManager.RegisterA(
events.LiveRoomStatusUpdate,
"gui.liveroom.room_status_update",
func(event *event.Event) {
room := event.Data.(events.LiveRoomStatusUpdateEvent).Room
index := -1
for i := 0; i < len(RoomTab.rooms); i++ {
if RoomTab.rooms[i].LiveRoom.Identifier() == room.LiveRoom.Identifier() {
index = i
break
}
}
if index == -1 {
return
}
RoomTab.rooms[index] = room
RoomTab.Rooms.Refresh()
if index == RoomTab.Index {
RoomTab.RoomTitle.SetText(room.DisplayName())
RoomTab.RoomID.SetText(room.LiveRoom.Identifier())
RoomTab.AutoConnect.SetChecked(room.Config.AutoConnect)
if room.Status {
RoomTab.Status.SetText(i18n.T("gui.room.status.connected"))
} else {
RoomTab.Status.SetText(i18n.T("gui.room.status.disconnected"))
}
RoomTab.Status.Refresh()
}
})
}
func createRoomController() fyne.CanvasObject {
RoomTab.ConnectBtn = widget.NewButton(i18n.T("gui.room.btn.connect"), func() {
if RoomTab.Index >= len(RoomTab.rooms) {
return
}
RoomTab.ConnectBtn.Disable()
go func() {
_ = API.LiveRooms().Connect(RoomTab.Index)
RoomTab.ConnectBtn.Enable()
}()
logger.Infof("Connect to room %s", RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier())
global.EventManager.CallA(
events.LiveRoomOperationCmd,
events.LiveRoomOperationCmdEvent{
Identifier: RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier(),
SetConnect: true,
})
})
RoomTab.DisConnectBtn = widget.NewButton(i18n.T("gui.room.btn.disconnect"), func() {
_ = API.LiveRooms().Disconnect(RoomTab.Index)
if RoomTab.Index >= len(RoomTab.rooms) {
return
}
RoomTab.DisConnectBtn.Disable()
logger.Infof("Disconnect from room %s", RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier())
global.EventManager.CallA(
events.LiveRoomOperationCmd,
events.LiveRoomOperationCmdEvent{
Identifier: RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier(),
SetConnect: false,
})
})
global.EventManager.RegisterA(
events.LiveRoomOperationFinish,
"gui.liveroom.operation_finish",
func(event *event.Event) {
RoomTab.ConnectBtn.Enable()
RoomTab.DisConnectBtn.Enable()
})
RoomTab.Status = widget.NewLabel(i18n.T("gui.room.waiting"))
RoomTab.RoomTitle = widget.NewLabel("")
RoomTab.RoomID = widget.NewLabel("")
RoomTab.AutoConnect = widget.NewCheck(i18n.T("gui.room.check.autoconnect"), func(b bool) {
rom := API.LiveRooms().Get(RoomTab.Index)
if rom != nil {
rom.Model().AutoConnect = b
if RoomTab.Index >= len(RoomTab.rooms) {
return
}
logger.Infof("Change room %s autoconnect to %v", RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier(), b)
global.EventManager.CallA(
events.LiveRoomConfigChangeCmd,
events.LiveRoomConfigChangeCmdEvent{
Identifier: RoomTab.rooms[RoomTab.Index].LiveRoom.Identifier(),
Config: model.LiveRoomConfig{
AutoConnect: b,
},
})
return
})
RoomTab.Rooms.Select(0)

View File

@@ -1,13 +1,13 @@
package gui
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/component"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"AynaLivePlayer/resource"
"context"
"fyne.io/fyne/v2"
@@ -44,13 +44,16 @@ var PlayController = &PlayControllerContainer{}
func registerPlayControllerHandler() {
PlayController.ButtonPrev.OnTapped = func() {
API.PlayControl().Seek(0, true)
global.EventManager.CallA(events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
Position: 0,
Absolute: true,
})
}
PlayController.ButtonSwitch.OnTapped = func() {
API.PlayControl().Toggle()
global.EventManager.CallA(events.PlayerToggleCmd, events.PlayerToggleCmdEvent{})
}
PlayController.ButtonNext.OnTapped = func() {
API.PlayControl().PlayNext()
global.EventManager.CallA(events.PlayerPlayNextCmd, events.PlayerPlayNextCmdEvent{})
}
PlayController.ButtonLrc.OnTapped = func() {
@@ -64,117 +67,86 @@ func registerPlayControllerHandler() {
showPlayerWindow()
}
if API.PlayControl().GetPlayer().ObserveProperty(
model.PlayerPropPause, "gui.play_controller.pause", func(ev *event.Event) {
data := ev.Data.(events.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")
}
global.EventManager.RegisterA(events.PlayerPropertyPauseUpdate, "gui.player.controller.paused", func(event *event.Event) {
if event.Data.(events.PlayerPropertyPauseUpdateEvent).Paused {
PlayController.ButtonSwitch.Icon = theme.MediaPlayIcon()
} else {
PlayController.ButtonSwitch.Icon = theme.MediaPauseIcon()
}
PlayController.ButtonSwitch.Refresh()
})
if API.PlayControl().GetPlayer().ObserveProperty(
model.PlayerPropPercentPos, "gui.play_controller.percent_pos", func(ev *event.Event) {
if PlayController.Progress.Dragging {
return
}
data := ev.Data.(events.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")
}
global.EventManager.RegisterA(events.PlayerPropertyPercentPosUpdate, "gui.player.controller.percent_pos", func(event *event.Event) {
if PlayController.Progress.Dragging {
return
}
PlayController.Progress.Value = event.Data.(events.PlayerPropertyPercentPosUpdateEvent).PercentPos * 10
PlayController.Progress.Refresh()
})
if API.PlayControl().GetPlayer().ObserveProperty(
model.PlayerPropIdleActive, "gui.play_controller.idle_active", func(ev *event.Event) {
isIdle := ev.Data.(events.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")
}
global.EventManager.RegisterA(events.PlayerPropertyIdleActiveUpdate, "gui.player.controller.idle_active", func(event *event.Event) {
isIdle := event.Data.(events.PlayerPropertyIdleActiveUpdateEvent).IsIdle
// 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
}
})
PlayController.Progress.Max = 0
PlayController.Progress.OnDragEnd = func(f float64) {
API.PlayControl().Seek(f/10, false)
global.EventManager.CallA(events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
Position: f / 10,
Absolute: false,
})
}
if API.PlayControl().GetPlayer().ObserveProperty(
model.PlayerPropTimePos, "gui.play_controller.time_pos", func(ev *event.Event) {
data := ev.Data.(events.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")
}
global.EventManager.RegisterA(events.PlayerPropertyTimePosUpdate, "gui.player.controller.time_pos", func(event *event.Event) {
PlayController.CurrentTime.SetText(util.FormatTime(int(event.Data.(events.PlayerPropertyTimePosUpdateEvent).TimePos)))
})
if API.PlayControl().GetPlayer().ObserveProperty(
model.PlayerPropDuration, "gui.play_controller.duration", func(ev *event.Event) {
data := ev.Data.(events.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")
}
global.EventManager.RegisterA(events.PlayerPropertyDurationUpdate, "gui.player.controller.duration", func(event *event.Event) {
PlayController.TotalTime.SetText(util.FormatTime(int(event.Data.(events.PlayerPropertyDurationUpdateEvent).Duration)))
})
if API.PlayControl().GetPlayer().ObserveProperty(
model.PlayerPropVolume, "gui.play_controller.volume", func(ev *event.Event) {
data := ev.Data.(events.PlayerPropertyUpdateEvent).Value
if data == nil {
PlayController.Volume.Value = 0
} else {
PlayController.Volume.Value = data.(float64)
}
PlayController.Volume.Refresh()
}) != nil {
l().Error("fail to register handler for progress bar with property percent-pos")
}
global.EventManager.RegisterA(events.PlayerPropertyVolumeUpdate, "gui.player.controller.volume", func(event *event.Event) {
PlayController.Volume.Value = event.Data.(events.PlayerPropertyVolumeUpdateEvent).Volume
PlayController.Volume.Refresh()
})
PlayController.Volume.OnChanged = func(f float64) {
API.PlayControl().SetVolume(f)
global.EventManager.CallA(events.PlayerVolumeChangeCmd, events.PlayerVolumeChangeCmdEvent{
Volume: f,
})
}
API.PlayControl().EventManager().RegisterA(events.EventPlay, "gui.player.updateinfo", func(event *event.Event) {
l().Debug("receive EventPlay update player info")
media := event.Data.(events.PlayEvent).Media
global.EventManager.RegisterA(events.PlayerPlayingUpdate, "gui.player.updateinfo", func(event *event.Event) {
if event.Data.(events.PlayerPlayingUpdateEvent).Removed {
PlayController.Progress.Value = 0
PlayController.Progress.Max = 0
PlayController.Title.SetText("Title")
PlayController.Artist.SetText("Artist")
PlayController.Username.SetText("Username")
PlayController.SetDefaultCover()
return
}
media := event.Data.(events.PlayerPlayingUpdateEvent).Media
//PlayController.Title.SetText(
// util.StringNormalize(media.Title, 16, 16))
//PlayController.Artist.SetText(
// util.StringNormalize(media.Artist, 16, 16))
PlayController.Title.SetText(
media.Title)
media.Info.Title)
PlayController.Artist.SetText(
media.Artist)
media.Info.Artist)
PlayController.Username.SetText(media.ToUser().Name)
if !media.Cover.Exists() {
if !media.Info.Cover.Exists() {
PlayController.SetDefaultCover()
} else {
if PlayController.coverLoader != nil {
@@ -185,7 +157,7 @@ func registerPlayControllerHandler() {
go func() {
ch := make(chan *canvas.Image)
go func() {
picture, err := gutil.NewImageFromPlayerPicture(media.Cover)
picture, err := gutil.NewImageFromPlayerPicture(media.Info.Cover)
if err != nil {
ch <- nil
return
@@ -207,7 +179,6 @@ func registerPlayControllerHandler() {
}()
}
})
return
}
func createPlayControllerV2() fyne.CanvasObject {
@@ -233,7 +204,7 @@ func createPlayControllerV2() fyne.CanvasObject {
volumeControl := component.NewFixedHSplitContainer(
widget.NewLabel(""),
container.NewBorder(nil, nil,
widget.NewIcon(theme.VolumeMuteIcon()),
widget.NewIcon(theme.VolumeUpIcon()),
widget.NewLabel(" "),
PlayController.Volume), 0.05)
volumeControl.SeparatorThickness = 0
@@ -244,15 +215,6 @@ func createPlayControllerV2() fyne.CanvasObject {
0.4)
controls.SeparatorThickness = 0
//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")

View File

@@ -1,19 +1,21 @@
package gui
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
"github.com/AynaLivePlayer/miaosic"
)
func createLyricObj(lyric *model.Lyric) []fyne.CanvasObject {
lrcs := make([]fyne.CanvasObject, len(lyric.Lyrics))
func createLyricObj(lyric *miaosic.Lyrics) []fyne.CanvasObject {
lrcs := make([]fyne.CanvasObject, len(lyric.Content))
for i := 0; i < len(lrcs); i++ {
lr := widget.NewLabelWithStyle(
lyric.Lyrics[i].Lyric,
lyric.Content[i].Lyric,
fyne.TextAlignCenter, fyne.TextStyle{Italic: true})
//lr.Wrapping = fyne.TextWrapWord
// todo fix fyne bug
@@ -26,10 +28,10 @@ func createLyricObj(lyric *model.Lyric) []fyne.CanvasObject {
func createLyricWindow() fyne.Window {
// create widgets
w := App.NewWindow("Lyric")
w := App.NewWindow(i18n.T("gui.lyric.title"))
currentLrc := newLabelWithWrapping("", fyne.TextWrapBreak)
currentLrc.Alignment = fyne.TextAlignCenter
fullLrc := container.NewVBox(createLyricObj(API.PlayControl().GetLyric().Get())...)
fullLrc := container.NewVBox()
lrcWindow := container.NewVScroll(fullLrc)
prevIndex := 0
w.SetContent(container.NewBorder(nil,
@@ -40,50 +42,44 @@ func createLyricWindow() fyne.Window {
w.CenterOnScreen()
// register handlers
API.PlayControl().GetLyric().EventManager().RegisterA(
events.EventLyricUpdate, "player.lyric.current_lyric", func(event *event.Event) {
e := event.Data.(events.LyricUpdateEvent)
if prevIndex >= len(fullLrc.Objects) || e.Lyric.Index >= len(fullLrc.Objects) {
global.EventManager.RegisterA(
events.PlayerLyricPosUpdate, "player.lyric.current_lyric", func(event *event.Event) {
e := event.Data.(events.PlayerLyricPosUpdateEvent)
logger.Debugf("lyric update", e)
if prevIndex >= len(fullLrc.Objects) || e.CurrentIndex >= len(fullLrc.Objects) {
// fix race condition
return
}
if e.Lyric == nil {
if e.CurrentIndex == -1 {
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)
fullLrc.Objects[e.CurrentIndex].(*widget.Label).TextStyle.Bold = true
fullLrc.Objects[e.CurrentIndex].Refresh()
prevIndex = e.CurrentIndex
currentLrc.SetText(e.CurrentLine.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,
DY: lrcWindow.Offset.Y - float32(e.CurrentIndex-2)/float32(e.Total)*lrcWindow.Content.Size().Height,
},
})
fullLrc.Refresh()
})
API.PlayControl().GetLyric().EventManager().RegisterA(
events.EventLyricReload, "player.lyric.new_media", func(event *event.Event) {
e := event.Data.(events.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()
})
global.EventManager.RegisterA(events.PlayerLyricReload, "player.lyric.current_lyric", func(event *event.Event) {
e := event.Data.(events.PlayerLyricReloadEvent)
fullLrc.Objects = createLyricObj(&e.Lyrics)
lrcWindow.Refresh()
})
global.EventManager.CallA(events.PlayerLyricRequestCmd, events.PlayerLyricRequestCmdEvent{})
w.SetOnClosed(func() {
API.PlayControl().GetLyric().EventManager().Unregister("player.lyric.current_lyric")
API.PlayControl().GetLyric().EventManager().Unregister("player.lyric.new_media")
global.EventManager.Unregister("player.lyric.current_lyric")
global.EventManager.Unregister("player.lyric.new_media")
PlayController.LrcWindowOpen = false
})
return w

View File

@@ -1,10 +1,11 @@
package gui
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
@@ -26,10 +27,15 @@ 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() {
API.Playlists().GetCurrent().Delete(b.Index)
global.EventManager.CallA(events.PlaylistDeleteCmd(model.PlaylistIDPlayer), events.PlaylistDeleteCmdEvent{
Index: b.Index,
})
})
topItem := fyne.NewMenuItem(i18n.T("gui.player.playlist.op.top"), func() {
API.Playlists().GetCurrent().Move(b.Index, 0)
global.EventManager.CallA(events.PlaylistMoveCmd(model.PlaylistIDPlayer), events.PlaylistMoveCmdEvent{
From: b.Index,
To: 0,
})
})
m := fyne.NewMenu("", deleteItem, topItem)
b.menu = m
@@ -40,17 +46,16 @@ func newPlaylistOperationButton() *playlistOperationButton {
}
var UserPlaylist = &struct {
Playlist *model.Playlist
List *widget.List
mux sync.RWMutex
Medias []model.Media
List *widget.List
mux sync.RWMutex
}{}
func createPlaylist() fyne.CanvasObject {
UserPlaylist.Playlist = API.Playlists().GetCurrent().Model().Copy()
UserPlaylist.List = widget.NewList(
func() int {
//todo: @4
return UserPlaylist.Playlist.Size()
return len(UserPlaylist.Medias)
},
func() fyne.CanvasObject {
return container.NewBorder(nil, nil, widget.NewLabel("index"), newPlaylistOperationButton(),
@@ -60,18 +65,21 @@ func createPlaylist() fyne.CanvasObject {
newLabelWithWrapping("user", fyne.TextTruncate)))
},
func(id widget.ListItemID, object fyne.CanvasObject) {
l().Debugf("Update playlist item: %d", id)
l().Debugf("Update playlist event during update %d", UserPlaylist.Playlist.Size())
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[0].(*widget.Label).SetText(
UserPlaylist.Playlist.Medias[id].Title)
UserPlaylist.Medias[id].Info.Title)
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[1].(*widget.Label).SetText(
UserPlaylist.Playlist.Medias[id].Artist)
UserPlaylist.Medias[id].Info.Artist)
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[2].(*widget.Label).SetText(
UserPlaylist.Playlist.Medias[id].ToUser().Name)
UserPlaylist.Medias[id].ToUser().Name)
object.(*fyne.Container).Objects[1].(*widget.Label).SetText(fmt.Sprintf("%d", id))
object.(*fyne.Container).Objects[2].(*playlistOperationButton).Index = id
})
registerPlaylistHandler()
global.EventManager.RegisterA(events.PlaylistDetailUpdate(model.PlaylistIDPlayer), "gui.player.playlist.update", func(event *event.Event) {
UserPlaylist.mux.Lock()
UserPlaylist.Medias = event.Data.(events.PlaylistDetailUpdateEvent).Medias
UserPlaylist.List.Refresh()
UserPlaylist.mux.Unlock()
})
return container.NewBorder(
container.NewBorder(nil, nil,
widget.NewLabel("#"), widget.NewLabel(i18n.T("gui.player.playlist.ops")),
@@ -84,12 +92,3 @@ func createPlaylist() fyne.CanvasObject {
UserPlaylist.List,
)
}
func registerPlaylistHandler() {
API.Playlists().GetCurrent().EventManager().RegisterA(events.EventPlaylistUpdate, "gui.playlist.update", func(event *event.Event) {
UserPlaylist.mux.Lock()
UserPlaylist.Playlist = event.Data.(events.PlaylistUpdateEvent).Playlist
UserPlaylist.List.Refresh()
UserPlaylist.mux.Unlock()
})
}

32
gui/player_videoplayer.go Normal file
View File

@@ -0,0 +1,32 @@
package gui
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/xfyne"
"fyne.io/fyne/v2"
)
func setupPlayerWindow() {
playerWindow = App.NewWindow("CorePlayerPreview")
playerWindow.Resize(fyne.NewSize(480, 240))
playerWindow.SetCloseIntercept(func() {
playerWindow.Hide()
})
MainWindow.SetOnClosed(func() {
playerWindow.Close()
})
playerWindow.Hide()
}
func showPlayerWindow() {
playerWindow.Show()
if playerWindowHandle == 0 {
playerWindowHandle = xfyne.GetWindowHandle(playerWindow)
logger.Infof("video output window handle: %d", playerWindowHandle)
if playerWindowHandle != 0 {
global.EventManager.CallA(events.PlayerVideoPlayerSetWindowHandleCmd,
events.PlayerVideoPlayerSetWindowHandleCmdEvent{Handle: playerWindowHandle})
}
}
}

View File

@@ -1,150 +0,0 @@
package gui
import (
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/gui/component"
"AynaLivePlayer/internal"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
type PlaylistsTab struct {
Playlists *widget.List
PlaylistMedia *widget.List
Index int
AddBtn *widget.Button
RemoveBtn *widget.Button
SetAsSystemBtn *component.AsyncButton
RefreshBtn *component.AsyncButton
CurrentSystemPlaylist *widget.Label
}
func (p *PlaylistsTab) UpdateCurrentSystemPlaylist() {
p.CurrentSystemPlaylist.SetText(i18n.T("gui.playlist.current") + API.Playlists().GetDefault().DisplayName())
}
var PlaylistManager = &PlaylistsTab{}
func createPlaylists() fyne.CanvasObject {
PlaylistManager.Playlists = widget.NewList(
func() int {
return API.Playlists().Size()
},
func() fyne.CanvasObject {
return widget.NewLabel("AAAAAAAAAAAAAAAA")
},
func(id widget.ListItemID, object fyne.CanvasObject) {
object.(*widget.Label).SetText(
API.Playlists().Get(id).DisplayName())
})
PlaylistManager.AddBtn = widget.NewButton(i18n.T("gui.playlist.button.add"), func() {
providerEntry := widget.NewSelect(API.Provider().GetPriority(), nil)
idEntry := widget.NewEntry()
dia := dialog.NewCustomConfirm(
i18n.T("gui.playlist.add.title"),
i18n.T("gui.playlist.add.confirm"),
i18n.T("gui.playlist.add.cancel"),
container.NewVBox(
container.New(
layout.NewFormLayout(),
widget.NewLabel(i18n.T("gui.playlist.add.source")),
providerEntry,
widget.NewLabel(i18n.T("gui.playlist.add.id_url")),
idEntry,
),
widget.NewLabel(i18n.T("gui.playlist.add.prompt")),
),
func(b bool) {
if b && len(providerEntry.Selected) > 0 && len(idEntry.Text) > 0 {
API.Playlists().Add(providerEntry.Selected, idEntry.Text)
PlaylistManager.Playlists.Refresh()
PlaylistManager.PlaylistMedia.Refresh()
}
},
MainWindow,
)
dia.Resize(fyne.NewSize(512, 256))
dia.Show()
})
PlaylistManager.RemoveBtn = widget.NewButton(i18n.T("gui.playlist.button.remove"), func() {
API.Playlists().Remove(PlaylistManager.Index)
//PlaylistManager.Index = 0
PlaylistManager.Playlists.Select(0)
PlaylistManager.Playlists.Refresh()
PlaylistManager.PlaylistMedia.Refresh()
})
PlaylistManager.Playlists.OnSelected = func(id widget.ListItemID) {
PlaylistManager.Index = id
PlaylistManager.PlaylistMedia.Refresh()
}
return container.NewHBox(
container.NewBorder(
nil, container.NewCenter(container.NewHBox(PlaylistManager.AddBtn, PlaylistManager.RemoveBtn)),
nil, nil,
PlaylistManager.Playlists,
),
widget.NewSeparator(),
)
}
func createPlaylistMedias() fyne.CanvasObject {
PlaylistManager.RefreshBtn = component.NewAsyncButtonWithIcon(
i18n.T("gui.playlist.button.refresh"), theme.ViewRefreshIcon(),
func() {
showDialogIfError(API.Playlists().PreparePlaylistByIndex(PlaylistManager.Index))
PlaylistManager.PlaylistMedia.Refresh()
})
PlaylistManager.SetAsSystemBtn = component.NewAsyncButton(
i18n.T("gui.playlist.button.set_as_system"),
func() {
showDialogIfError(API.Playlists().SetDefault(PlaylistManager.Index))
PlaylistManager.PlaylistMedia.Refresh()
PlaylistManager.UpdateCurrentSystemPlaylist()
})
PlaylistManager.CurrentSystemPlaylist = widget.NewLabel("Current: ")
PlaylistManager.UpdateCurrentSystemPlaylist()
PlaylistManager.PlaylistMedia = widget.NewList(
func() int {
if API.Playlists().Size() == 0 {
return 0
}
return API.Playlists().Get(PlaylistManager.Index).Size()
},
func() fyne.CanvasObject {
return container.NewBorder(nil, nil,
widget.NewLabel("index"),
container.NewHBox(
widget.NewButtonWithIcon("", theme.MediaPlayIcon(), nil),
widget.NewButtonWithIcon("", theme.ContentAddIcon(), nil),
),
container.NewGridWithColumns(2,
newLabelWithWrapping("title", fyne.TextTruncate),
newLabelWithWrapping("artist", fyne.TextTruncate)))
},
func(id widget.ListItemID, object fyne.CanvasObject) {
m := API.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 = internal.SystemUser
btns[0].(*widget.Button).OnTapped = func() {
showDialogIfError(API.PlayControl().Play(m))
}
btns[1].(*widget.Button).OnTapped = func() {
API.Playlists().GetCurrent().Push(m)
}
})
return container.NewBorder(
container.NewHBox(PlaylistManager.RefreshBtn, PlaylistManager.SetAsSystemBtn, PlaylistManager.CurrentSystemPlaylist), nil,
nil, nil,
PlaylistManager.PlaylistMedia)
}

View File

@@ -1,13 +1,14 @@
package gui
import (
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/component"
"AynaLivePlayer/internal"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
@@ -28,22 +29,31 @@ func createSearchBar() fyne.CanvasObject {
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 := API.Provider().SearchWithProvider(keyword, pr)
if err != nil {
dialog.ShowError(err, MainWindow)
}
model.ApplyUser(items, internal.SystemUser)
SearchResult.Items = items
logger.Debugf("Search keyword: %s, provider: %s", keyword, pr)
SearchResult.mux.Lock()
SearchResult.Items = make([]model.Media, 0)
SearchResult.List.Refresh()
SearchResult.mux.Unlock()
global.EventManager.CallA(events.SearchCmd, events.SearchCmdEvent{
Keyword: keyword,
Provider: pr,
})
})
global.EventManager.RegisterA(events.SearchProviderUpdate,
"gui.search.provider.update", func(event *event.Event) {
providers := event.Data.(events.SearchProviderUpdateEvent)
s := make([]string, len(providers.Providers))
copy(s, providers.Providers)
SearchBar.UseSource.Options = s
if len(s) > 0 {
SearchBar.UseSource.SetSelected(s[0])
}
})
SearchBar.UseSource = widget.NewSelect([]string{}, func(s string) {
})
s := make([]string, len(API.Provider().GetPriority()))
copy(s, API.Provider().GetPriority())
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,
container.NewBorder(nil, nil, SearchBar.UseSource, nil, SearchBar.Input))

View File

@@ -1,20 +1,26 @@
package gui
import (
"AynaLivePlayer/common/i18n"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"sync"
)
var SearchResult = &struct {
List *widget.List
Items []*model.Media
Items []model.Media
mux sync.Mutex
}{
Items: []*model.Media{},
Items: []model.Media{},
mux: sync.Mutex{},
}
func createSearchList() fyne.CanvasObject {
@@ -36,20 +42,32 @@ func createSearchList() fyne.CanvasObject {
},
func(id widget.ListItemID, object fyne.CanvasObject) {
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[0].(*widget.Label).SetText(
SearchResult.Items[id].Title)
SearchResult.Items[id].Info.Title)
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[1].(*widget.Label).SetText(
SearchResult.Items[id].Artist)
SearchResult.Items[id].Info.Artist)
object.(*fyne.Container).Objects[0].(*fyne.Container).Objects[2].(*widget.Label).SetText(
SearchResult.Items[id].Meta.(model.Meta).Name)
SearchResult.Items[id].Info.Meta.Provider)
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() {
showDialogIfError(API.PlayControl().Play(SearchResult.Items[id]))
global.EventManager.CallA(events.PlayerPlayCmd, events.PlayerPlayCmdEvent{
Media: SearchResult.Items[id],
})
}
btns[1].(*widget.Button).OnTapped = func() {
API.Playlists().GetCurrent().Push(SearchResult.Items[id])
global.EventManager.CallA(events.PlaylistInsertCmd(model.PlaylistIDPlayer), events.PlaylistInsertCmdEvent{
Media: SearchResult.Items[id],
Position: -1,
})
}
})
global.EventManager.RegisterA(events.SearchResultUpdate, "gui.search.update_result", func(event *event.Event) {
items := event.Data.(events.SearchResultUpdateEvent).Medias
SearchResult.Items = items
SearchResult.mux.Lock()
SearchResult.List.Refresh()
SearchResult.mux.Unlock()
})
return container.NewBorder(
container.NewBorder(nil, nil,
widget.NewLabel("#"), widget.NewLabel(i18n.T("gui.search.operation")),

View File

@@ -3,8 +3,9 @@ package gui
import (
"AynaLivePlayer/resource"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
"image/color"
xtheme "fyne.io/x/fyne/theme"
)
type myTheme struct{}
@@ -18,7 +19,6 @@ func (*myTheme) Font(s fyne.TextStyle) fyne.Resource {
}
if s.Bold {
if s.Italic {
//return theme.DefaultTheme().Font(s)
return resource.FontMSYaHeiBold
}
return resource.FontMSYaHei
@@ -30,13 +30,13 @@ func (*myTheme) Font(s fyne.TextStyle) fyne.Resource {
}
func (*myTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
return theme.DefaultTheme().Color(n, v)
return xtheme.AdwaitaTheme().Color(n, v)
}
func (*myTheme) Icon(n fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(n)
return xtheme.AdwaitaTheme().Icon(n)
}
func (*myTheme) Size(n fyne.ThemeSizeName) float32 {
return theme.DefaultTheme().Size(n)
return xtheme.AdwaitaTheme().Size(n)
}

23
gui/xfyne/window.go Normal file
View File

@@ -0,0 +1,23 @@
package xfyne
import (
"fyne.io/fyne/v2"
"github.com/go-gl/glfw/v3.3/glfw"
"reflect"
"unsafe"
)
// getGlfwWindow returns the glfw.Window pointer from a fyne.Window.
// very unsafe and ugly hacks. but it works.
func getGlfwWindow(window fyne.Window) *glfw.Window {
rv := reflect.ValueOf(window)
if rv.Type().String() != "*glfw.window" {
return nil
}
rv = rv.Elem()
var glfwWindowPtr uintptr = rv.FieldByName("viewport").Pointer()
for glfwWindowPtr == 0 {
glfwWindowPtr = rv.FieldByName("viewport").Pointer()
}
return (*glfw.Window)(unsafe.Pointer(glfwWindowPtr))
}

View File

@@ -0,0 +1,12 @@
//go:build darwin
// +build darwin
package xfyne
func GetWindowHandle(window fyne.Window) uintptr {
glfwWindow := getGlfwWindow(window)
if glfwWindow == nil {
return 0
}
return uintptr(glfwWindow.GetCocoaWindow())
}

16
gui/xfyne/window_linux.go Normal file
View File

@@ -0,0 +1,16 @@
//go:build linux
// +build linux
package xfyne
import (
"fyne.io/fyne/v2"
)
func GetWindowHandle(window fyne.Window) uintptr {
glfwWindow := getGlfwWindow(window)
if glfwWindow == nil {
return 0
}
return uintptr(glfwWindow.GetX11Window())
}

10
gui/xfyne/window_other.go Normal file
View File

@@ -0,0 +1,10 @@
//go:build !darwin && !windows && !linux
// +build !darwin,!windows,!linux
package xfyne
import "fyne.io/fyne/v2"
func GetWindowHandle(window fyne.Window) uintptr {
return 0
}

View File

@@ -0,0 +1,14 @@
//go:build windows
// +build windows
package xfyne
import "fyne.io/fyne/v2"
func GetWindowHandle(window fyne.Window) uintptr {
glfwWindow := getGlfwWindow(window)
if glfwWindow == nil {
return 0
}
return uintptr(glfwWindow.GetWin32Window())
}

View File

@@ -1,69 +0,0 @@
package internal
import (
"AynaLivePlayer/core/adapter"
)
type Controller struct {
app adapter.IApplication `ini:"-"`
liveroom adapter.ILiveRoomController `ini:"-"`
player adapter.IPlayController `ini:"-"`
lyric adapter.ILyricLoader `ini:"-"`
playlist adapter.IPlaylistController `ini:"-"`
provider adapter.IProviderController `ini:"-"`
plugin adapter.IPluginController `ini:"-"`
log adapter.ILogger `ini:"-"`
}
func (c *Controller) Logger() adapter.ILogger {
return c.log
}
func NewController(
liveroom adapter.ILiveRoomController, player adapter.IPlayController,
playlist adapter.IPlaylistController,
provider adapter.IProviderController, plugin adapter.IPluginController,
log adapter.ILogger) adapter.IControlBridge {
cc := &Controller{
app: &AppBilibiliChannel{},
liveroom: liveroom,
player: player,
playlist: playlist,
provider: provider,
plugin: plugin,
log: log,
}
return cc
}
func (c *Controller) App() adapter.IApplication {
return c.app
}
func (c *Controller) LiveRooms() adapter.ILiveRoomController {
return c.liveroom
}
func (c *Controller) PlayControl() adapter.IPlayController {
return c.player
}
func (c *Controller) Playlists() adapter.IPlaylistController {
return c.playlist
}
func (c *Controller) Provider() adapter.IProviderController {
return c.provider
}
func (c *Controller) Plugin() adapter.IPluginController {
return c.plugin
}
func (c *Controller) LoadPlugins(plugins ...adapter.Plugin) {
c.plugin.LoadPlugins(plugins...)
}
func (c *Controller) CloseAndSave() {
c.plugin.ClosePlugins()
}

View File

@@ -0,0 +1,65 @@
package controller
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/event"
)
func Initialize() {
handleSearch()
createLyricLoader()
handlePlayNext()
}
func Stop() {
}
func handlePlayNext() {
log := global.Logger.WithPrefix("Controller")
isIdle := false
global.EventManager.RegisterA(
events.PlayerPropertyIdleActiveUpdate,
"internal.controller.playcontrol.idleplaynext",
func(event *event.Event) {
data := event.Data.(events.PlayerPropertyIdleActiveUpdateEvent)
isIdle = data.IsIdle
if data.IsIdle {
log.Info("mpv went idle, try play next")
global.EventManager.CallA(events.PlayerPlayNextCmd,
events.PlayerPlayNextCmdEvent{})
}
})
global.EventManager.RegisterA(
events.PlaylistInsertUpdate(model.PlaylistIDPlayer),
"internal.controller.playcontrol.playnext_when_insert",
func(event *event.Event) {
if isIdle {
global.EventManager.CallA(events.PlayerPlayNextCmd,
events.PlayerPlayNextCmdEvent{})
}
})
global.EventManager.RegisterA(
events.PlayerPlayNextCmd,
"internal.controller.playcontrol.playnext",
func(event *event.Event) {
global.EventManager.CallA(events.PlaylistNextCmd(model.PlaylistIDPlayer),
events.PlaylistNextCmdEvent{
Remove: true,
})
})
global.EventManager.RegisterA(events.PlaylistNextUpdate(model.PlaylistIDPlayer),
"internal.controller.playcontrol.play_when_next", func(event *event.Event) {
data := event.Data.(events.PlaylistNextUpdateEvent)
global.EventManager.CallA(
events.PlayerPlayCmd,
events.PlayerPlayCmdEvent{
Media: data.Media,
})
})
}

View File

@@ -0,0 +1,60 @@
package controller
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/event"
"github.com/AynaLivePlayer/miaosic"
)
type lyricLoader struct {
Lyric miaosic.Lyrics
prev float64
prevIndex int
}
var lyricManager = &lyricLoader{}
func createLyricLoader() {
log := global.Logger.WithPrefix("LyricLoader")
global.EventManager.RegisterA(events.PlayerPlayingUpdate, "internal.lyric.update", func(event *event.Event) {
data := event.Data.(events.PlayerPlayingUpdateEvent)
if data.Removed {
log.Debugf("current media removed, clear lyric")
lyricManager.Lyric = miaosic.ParseLyrics("", "")
return
}
log.Infof("update lyric for %s", data.Media.Info.Title)
lyric, err := miaosic.GetMediaLyric(data.Media.Info.Meta)
if err == nil && len(lyric) > 0 {
lyricManager.Lyric = lyric[0]
} else {
log.Errorf("failed to get lyric for %s (%s): %s", data.Media.Info.Title, data.Media.Info.Meta.ID(), err)
}
global.EventManager.CallA(events.PlayerLyricReload, events.PlayerLyricReloadEvent{
Lyrics: lyricManager.Lyric,
})
})
global.EventManager.RegisterA(events.PlayerPropertyTimePosUpdate, "internal.lyric.update_current", func(event *event.Event) {
time := event.Data.(events.PlayerPropertyTimePosUpdateEvent).TimePos
idx := lyricManager.Lyric.FindIndex(time)
if idx == lyricManager.prevIndex {
return
}
lyricManager.prevIndex = idx
global.EventManager.CallA(
events.PlayerLyricPosUpdate,
events.PlayerLyricPosUpdateEvent{
CurrentIndex: idx,
Time: time,
CurrentLine: lyricManager.Lyric.Find(time),
Total: len(lyricManager.Lyric.Content),
})
return
})
global.EventManager.RegisterA(events.PlayerLyricRequestCmd, "internal.lyric.request", func(event *event.Event) {
global.EventManager.CallA(events.PlayerLyricReload, events.PlayerLyricReloadEvent{
Lyrics: lyricManager.Lyric,
})
})
}

View File

@@ -0,0 +1,38 @@
package controller
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/event"
"github.com/AynaLivePlayer/miaosic"
)
func handleSearch() {
log := global.Logger.WithPrefix("Search")
global.EventManager.RegisterA(
events.SearchCmd, "internal.controller.search.handleSearchCmd", func(event *event.Event) {
data := event.Data.(events.SearchCmdEvent)
log.Infof("Search %s using %s", data.Keyword, data.Provider)
searchResult, err := miaosic.SearchByProvider(data.Provider, data.Keyword, 1, 10)
if err != nil {
log.Warnf("Search %s using %s failed: %s", data.Keyword, data.Provider, err)
return
}
medias := make([]model.Media, len(searchResult))
for i, v := range searchResult {
medias[i] = model.Media{
Info: v,
User: model.SystemUser,
}
}
global.EventManager.CallA(
events.SearchResultUpdate, events.SearchResultUpdateEvent{
Medias: medias,
})
})
global.EventManager.CallA(
events.SearchProviderUpdate, events.SearchProviderUpdateEvent{
Providers: miaosic.ListAvailableProviders(),
})
}

6
internal/init.go Normal file
View File

@@ -0,0 +1,6 @@
package internal
import (
_ "github.com/AynaLivePlayer/miaosic/providers/bilivideo"
_ "github.com/AynaLivePlayer/miaosic/providers/netease"
)

View File

@@ -1 +1,20 @@
package internal
import (
"AynaLivePlayer/internal/controller"
"AynaLivePlayer/internal/liveroom"
"AynaLivePlayer/internal/player"
"AynaLivePlayer/internal/playlist"
)
func Initialize() {
player.SetupMpvPlayer()
playlist.Initialize()
controller.Initialize()
liveroom.Initialize()
}
func Stop() {
liveroom.StopAndSave()
player.StopMpvPlayer()
}

View File

@@ -1,224 +0,0 @@
package internal
import (
"AynaLivePlayer/adapters"
"AynaLivePlayer/common/config"
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"errors"
"strings"
)
type liveRoomImpl struct {
model.LiveRoom
client adapter.LiveClient
}
func (r *liveRoomImpl) Status() bool {
return r.client.Status()
}
func (r *liveRoomImpl) EventManager() *event.Manager {
return r.client.EventManager()
}
func (r *liveRoomImpl) Model() *model.LiveRoom {
return &r.LiveRoom
}
func (r *liveRoomImpl) Client() adapter.LiveClient {
return r.client
}
func (r *liveRoomImpl) DisplayName() string {
// todo need to be fixed
r.LiveRoom.Title = r.client.RoomName()
if r.LiveRoom.Title != "" {
return r.LiveRoom.Title
}
return r.LiveRoom.Identifier()
}
func (r *liveRoomImpl) init(msgHandler event.HandlerFunc) (err error) {
if r.client != nil {
return nil
}
r.client, err = adapters.LiveClient.NewLiveClient(r.ClientName, r.ID)
if err != nil {
return
}
r.LiveRoom.Title = r.client.RoomName()
r.client.EventManager().RegisterA(
events.LiveRoomMessageReceive,
"adapter.danmu.command",
msgHandler)
return nil
}
type LiveRoomController struct {
LiveRoomPath string
liveRooms []*liveRoomImpl
danmuCommands []adapter.LiveRoomExecutor
log adapter.ILogger
}
func (lr *LiveRoomController) GetAllClientNames() []string {
return adapters.LiveClient.GetAllClientNames()
}
func NewLiveRoomController(
log adapter.ILogger,
) adapter.ILiveRoomController {
lr := &LiveRoomController{
LiveRoomPath: "liverooms.json",
liveRooms: []*liveRoomImpl{
{LiveRoom: model.LiveRoom{
ClientName: "bilibili",
ID: "9076804",
AutoConnect: false,
}},
{LiveRoom: model.LiveRoom{
ClientName: "bilibili",
ID: "3819533",
AutoConnect: false,
}},
},
danmuCommands: make([]adapter.LiveRoomExecutor, 0),
log: log,
}
config.LoadConfig(lr)
lr.initialize()
return lr
}
func (lr *LiveRoomController) danmuCommandHandler(event *event.Event) {
danmu := event.Data.(*model.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([]*liveRoomImpl, len(rooms))
for i := 0; i < len(rooms); i++ {
lr.liveRooms[i] = &liveRoomImpl{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) adapter.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 {
lr.log.Infof("[LiveRooms] Try to start LiveRooms.index=%d", index)
if index < 0 || index >= len(lr.liveRooms) {
lr.log.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 {
lr.log.Infof("[LiveRooms] Try to Disconnect LiveRooms.index=%d", index)
if index < 0 || index >= len(lr.liveRooms) {
lr.log.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 := &liveRoomImpl{
LiveRoom: model.LiveRoom{
ClientName: clientName,
ID: roomId,
AutoConnect: false,
},
}
lr.log.Infof("[LiveRooms] add live room %s", &rm.LiveRoom)
err := rm.init(lr.danmuCommandHandler)
if err != nil {
return nil, err
}
lr.log.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 {
lr.log.Infof("Try to remove LiveRooms.index=%d", index)
if index < 0 || index >= len(lr.liveRooms) {
lr.log.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].Client().EventManager().UnregisterAll()
lr.liveRooms = append(lr.liveRooms[:index], lr.liveRooms[index+1:]...)
return nil
}
func (lr *LiveRoomController) AddDanmuCommand(executor adapter.LiveRoomExecutor) {
lr.danmuCommands = append(lr.danmuCommands, executor)
}

View File

@@ -0,0 +1,30 @@
package liveroom
import (
"AynaLivePlayer/core/model"
"AynaLivePlayer/pkg/config"
)
type _cfg struct {
ApiServer string
LiveRoomPath string
liveRooms []model.LiveRoom
}
func (c *_cfg) Name() string {
return "LiveRoom"
}
func (c *_cfg) OnLoad() {
_ = config.LoadJson(c.LiveRoomPath, &c.liveRooms)
}
func (c *_cfg) OnSave() {
_ = config.SaveJson(c.LiveRoomPath, &c.liveRooms)
}
var cfg = &_cfg{
ApiServer: "http://localhost:9090",
LiveRoomPath: "liveroom.json",
liveRooms: make([]model.LiveRoom, 0),
}

View File

@@ -0,0 +1,217 @@
package liveroom
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/logger"
"errors"
liveroomsdk "github.com/AynaLivePlayer/liveroom-sdk"
"github.com/AynaLivePlayer/liveroom-sdk/provider/openblive"
)
type liveroom struct {
room liveroomsdk.ILiveRoom
model model.LiveRoom
}
var liveRooms = map[string]*liveroom{}
var log logger.ILogger
func Initialize() {
log = global.Logger.WithPrefix("LiveRoom")
config.LoadConfig(cfg)
liveroomsdk.RegisterProvider(openblive.NewOpenBLiveClientProvider(cfg.ApiServer, 1661006726438))
// ignore web danmu client
//liveroomsdk.RegisterProvider(webdm.NewWebDanmuClientProvider(cfg.ApiServer))
liveRooms = make(map[string]*liveroom, 0)
callEvents()
registerHandlers()
}
func StopAndSave() {
log.Infof("Stop and save live rooms")
for _, r := range liveRooms {
log.Infof("Disconnect room %s: %v", r.room.Config().Identifier(), r.room.Disconnect())
}
liveroomConfigs := make([]model.LiveRoom, 0)
for _, r := range liveRooms {
liveroomConfigs = append(liveroomConfigs, r.model)
}
cfg.liveRooms = liveroomConfigs
}
func addLiveRoom(roomModel model.LiveRoom) {
log.Info("Add live room")
room, err := liveroomsdk.CreateLiveRoom(roomModel.LiveRoom)
if _, ok := liveRooms[room.Config().Identifier()]; ok {
log.Errorf("fail to add, room %s already exists", room.Config().Identifier())
global.EventManager.CallA(
events.ErrorUpdate, events.ErrorUpdateEvent{
Error: errors.New("room already exists"),
})
return
}
if err != nil {
log.Errorf("Create live room failed: %s", err)
global.EventManager.CallA(
events.ErrorUpdate, events.ErrorUpdateEvent{
Error: err,
})
return
}
lr := &liveroom{
room: room,
model: roomModel,
}
liveRooms[room.Config().Identifier()] = lr
room.OnStatusChange(func(connected bool) {
log.Infof("room %s status change to %t", room.Config().Identifier(), connected)
lr.model.Status = connected
sendRoomStatusUpdateEvent(room.Config().Identifier())
})
room.OnMessage(func(message *liveroomsdk.Message) {
log.Debugf("room %s receive message: %s", room.Config().Identifier(), message.Message)
global.EventManager.CallA(
events.LiveRoomMessageReceive,
events.LiveRoomMessageReceiveEvent{
Message: message,
})
})
log.Infof("success add live room %s", room.Config().Identifier())
sendRoomsUpdateEvent()
}
func registerHandlers() {
global.EventManager.RegisterA(
events.LiveRoomAddCmd, "internal.liveroom.add", func(event *event.Event) {
data := event.Data.(events.LiveRoomAddCmdEvent)
addLiveRoom(model.LiveRoom{
LiveRoom: liveroomsdk.LiveRoom{
Provider: data.Provider,
Room: data.RoomKey,
},
Config: model.LiveRoomConfig{
AutoConnect: false,
},
Title: data.Title,
Status: false,
})
})
global.EventManager.RegisterA(
events.LiveRoomRemoveCmd, "internal.liveroom.remove", func(event *event.Event) {
data := event.Data.(events.LiveRoomRemoveCmdEvent)
room, ok := liveRooms[data.Identifier]
if !ok {
log.Errorf("remove room failed, room %s not found", data.Identifier)
return
}
_ = room.room.Disconnect()
room.room.OnStatusChange(nil)
delete(liveRooms, data.Identifier)
log.Infof("success remove live room %s", data.Identifier)
sendRoomsUpdateEvent()
})
global.EventManager.RegisterA(
events.LiveRoomConfigChangeCmd, "internal.liveroom.config.change", func(event *event.Event) {
data := event.Data.(events.LiveRoomConfigChangeCmdEvent)
if room, ok := liveRooms[data.Identifier]; ok {
room.model.Config = data.Config
sendRoomStatusUpdateEvent(data.Identifier)
}
})
global.EventManager.RegisterA(
events.LiveRoomOperationCmd, "internal.liveroom.operation", func(event *event.Event) {
data := event.Data.(events.LiveRoomOperationCmdEvent)
log.Infof("Live room operation SetConnect %v", data.SetConnect)
room, ok := liveRooms[data.Identifier]
if !ok {
log.Errorf("Room %s not found", data.Identifier)
return
}
var err error
if data.SetConnect {
err = room.room.Connect()
} else {
err = room.room.Disconnect()
}
if err != nil {
log.Errorf("Room %s operation failed: %s", data.Identifier, err)
global.EventManager.CallA(
events.ErrorUpdate, events.ErrorUpdateEvent{
Error: err,
})
}
global.EventManager.CallA(
events.LiveRoomOperationFinish, events.LiveRoomOperationFinishEvent{})
sendRoomStatusUpdateEvent(data.Identifier)
})
}
func sendRoomStatusUpdateEvent(roomId string) {
room, ok := liveRooms[roomId]
if !ok {
log.Errorf("send room status update event failed, room %s not found", roomId)
return
}
log.Infof("send room status update event, room %s", roomId)
global.EventManager.CallA(
events.LiveRoomStatusUpdate,
events.LiveRoomStatusUpdateEvent{
Room: room.model,
})
}
func sendRoomsUpdateEvent() {
rooms := make([]model.LiveRoom, 0)
for _, r := range liveRooms {
rooms = append(rooms, r.model)
}
global.EventManager.CallA(
events.LiveRoomRoomsUpdate,
events.LiveRoomRoomsUpdateEvent{
Rooms: rooms,
})
}
func callEvents() {
providers := liveroomsdk.ListAvailableProviders()
providerInfo := make([]model.LiveRoomProviderInfo, 0)
for _, p := range providers {
provider, _ := liveroomsdk.GetProvider(p)
providerInfo = append(providerInfo, model.LiveRoomProviderInfo{
Name: provider.GetName(),
Description: provider.GetDescription(),
})
}
for _, roomCfg := range cfg.liveRooms {
addLiveRoom(roomCfg)
}
global.EventManager.CallA(
events.LiveRoomProviderUpdate,
events.LiveRoomProviderUpdateEvent{
Providers: providerInfo,
})
sendRoomsUpdateEvent()
for _, r := range liveRooms {
if r.model.Config.AutoConnect {
global.EventManager.CallA(
events.LiveRoomOperationCmd,
events.LiveRoomOperationCmdEvent{
Identifier: r.room.Config().Identifier(),
SetConnect: true,
})
}
}
}

View File

@@ -1,57 +0,0 @@
package internal
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/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(
events.EventLyricReload,
events.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(
events.EventLyricUpdate,
events.LyricUpdateEvent{
Lyrics: l.Lyric,
Time: time,
Lyric: lrc,
})
return
}

View File

@@ -0,0 +1,37 @@
package mpv
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/event"
)
type playerConfig struct {
Volume float64
}
func (p *playerConfig) Name() string {
return "Player"
}
func (p *playerConfig) OnLoad() {
return
}
func (p *playerConfig) OnSave() {
return
}
var cfg = &playerConfig{
Volume: 100,
}
func restoreConfig() {
global.EventManager.CallA(events.PlayerVolumeChangeCmd, events.PlayerVolumeChangeCmdEvent{
Volume: cfg.Volume,
})
global.EventManager.RegisterA(events.PlayerPropertyVolumeUpdate, "player.config.volume", func(evnt *event.Event) {
data := evnt.Data.(events.PlayerPropertyVolumeUpdateEvent)
cfg.Volume = data.Volume
})
}

272
internal/player/mpv/mpv.go Normal file
View File

@@ -0,0 +1,272 @@
package mpv
import (
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/logger"
"fmt"
"github.com/AynaLivePlayer/miaosic"
"github.com/aynakeya/go-mpv"
)
var running bool = false
var libmpv *mpv.Mpv = nil
var log logger.ILogger = nil
func SetupPlayer() {
running = true
config.LoadConfig(cfg)
libmpv = mpv.Create()
log = global.Logger.WithPrefix("MPV Player")
err := libmpv.Initialize()
if err != nil {
log.Error("initialize libmpv failed")
return
}
_ = libmpv.SetOptionString("vo", "null")
log.Info("initialize libmpv success")
registerHandler()
registerCmdHandler()
restoreConfig()
log.Info("starting mpv player")
go func() {
for running {
e := libmpv.WaitEvent(1)
if e == nil {
log.Warn("[MPV Player] event loop got nil event")
}
if e.EventId == mpv.EVENT_PROPERTY_CHANGE {
eventProperty := e.Property()
handler, ok := mpvPropertyHandler[eventProperty.Name]
if !ok {
continue
}
var value interface{} = nil
if eventProperty.Data != nil {
value = eventProperty.Data.(mpv.Node).Value
}
//log.Debugf("[MPV Player] property update %s %v", eventProperty.Name, value)
handler(value)
}
if e.EventId == mpv.EVENT_SHUTDOWN {
log.Info("[MPV Player] libmpv shutdown")
StopPlayer()
}
}
}()
}
func StopPlayer() {
log.Info("stopping mpv player")
running = false
// stop player async, should be closed in the end anyway
go libmpv.TerminateDestroy()
log.Info("mpv player stopped")
}
var mpvPropertyHandler = map[string]func(value any){
"pause": func(value any) {
var data events.PlayerPropertyPauseUpdateEvent
log.Debugf("pause property update %v %T", value, value)
data.Paused = value.(bool)
global.EventManager.CallA(events.PlayerPropertyPauseUpdate, data)
},
"percent-pos": func(value any) {
var data events.PlayerPropertyPercentPosUpdateEvent
if value == nil {
data.PercentPos = 0
} else {
data.PercentPos = value.(float64)
}
// ignore bug value
if data.PercentPos < 0.1 {
return
}
global.EventManager.CallA(events.PlayerPropertyPercentPosUpdate, data)
},
"idle-active": func(value any) {
var data events.PlayerPropertyIdleActiveUpdateEvent
if value == nil {
data.IsIdle = true
} else {
data.IsIdle = value.(bool)
}
// if is idle, remove playing media
if data.IsIdle {
global.EventManager.CallA(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{
Media: model.Media{},
Removed: true,
})
}
global.EventManager.CallA(events.PlayerPropertyIdleActiveUpdate, data)
},
"time-pos": func(value any) {
var data events.PlayerPropertyTimePosUpdateEvent
if value == nil {
data.TimePos = 0
} else {
data.TimePos = value.(float64)
}
// ignore bug value
if data.TimePos < 0.1 {
return
}
global.EventManager.CallA(events.PlayerPropertyTimePosUpdate, data)
},
"duration": func(value any) {
var data events.PlayerPropertyDurationUpdateEvent
if value == nil {
data.Duration = 0
} else {
data.Duration = value.(float64)
}
global.EventManager.CallA(events.PlayerPropertyDurationUpdate, data)
},
"volume": func(value any) {
var data events.PlayerPropertyVolumeUpdateEvent
if value == nil {
data.Volume = 0
} else {
data.Volume = value.(float64)
}
global.EventManager.CallA(events.PlayerPropertyVolumeUpdate, data)
},
}
func registerHandler() {
var err error
for property, _ := range mpvPropertyHandler {
log.Infof("register handler for mpv property %s", property)
err = libmpv.ObserveProperty(util.Hash64(property), property, mpv.FORMAT_NODE)
if err != nil {
log.Errorf("register handler for mpv property %s failed: %s", property, err)
}
}
}
func registerCmdHandler() {
global.EventManager.RegisterA(events.PlayerPlayCmd, "player.play", func(evnt *event.Event) {
mediaInfo := evnt.Data.(events.PlayerPlayCmdEvent).Media.Info
log.Infof("[MPV Player] Play media %s", mediaInfo.Title)
mediaUrls, err := miaosic.GetMediaUrl(mediaInfo.Meta, miaosic.QualityAny)
if err != nil || len(mediaUrls) == 0 {
log.Warn("[MPV PlayControl] get media url failed", err)
return
}
mediaUrl := mediaUrls[0]
if val, ok := mediaUrl.Header["User-Agent"]; ok {
log.Debug("[MPV PlayControl] set user-agent for mpv player")
err := libmpv.SetPropertyString("user-agent", val)
if err != nil {
log.Warn("[MPV PlayControl] set player user-agent failed", err)
return
}
}
if val, ok := mediaUrl.Header["Referer"]; ok {
log.Debug("[MPV PlayControl] set referrer for mpv player")
err := libmpv.SetPropertyString("referrer", val)
if err != nil {
log.Warn("[MPV PlayControl] set player referrer failed", err)
return
}
}
log.Debugf("mpv command load file %s %s", mediaInfo.Title, mediaUrl.Url)
if err := libmpv.Command([]string{"loadfile", mediaUrl.Url}); err != nil {
log.Warn("[MPV PlayControl] mpv load media failed", mediaInfo)
return
}
media := evnt.Data.(events.PlayerPlayCmdEvent).Media
if m, err := miaosic.GetMediaInfo(media.Info.Meta); err == nil {
media.Info = m
}
global.EventManager.CallA(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{
Media: media,
Removed: false,
})
})
global.EventManager.RegisterA(events.PlayerToggleCmd, "player.toggle", func(evnt *event.Event) {
property, err := libmpv.GetProperty("pause", mpv.FORMAT_FLAG)
if err != nil {
log.Warn("[MPV PlayControl] get property pause failed", err)
return
}
err = libmpv.SetProperty("pause", mpv.FORMAT_FLAG, !property.(bool))
if err != nil {
log.Warn("[MPV PlayControl] toggle pause failed", err)
}
})
global.EventManager.RegisterA(events.PlayerSeekCmd, "player.seek", func(evnt *event.Event) {
data := evnt.Data.(events.PlayerSeekCmdEvent)
log.Debugf("seek to %f (absolute=%t)", data.Position, data.Absolute)
var err error
if data.Absolute {
err = libmpv.SetProperty("time-pos", mpv.FORMAT_DOUBLE, data.Position)
} else {
err = libmpv.SetProperty("percent-pos", mpv.FORMAT_DOUBLE, data.Position)
}
if err != nil {
log.Warn("seek failed", err)
}
})
global.EventManager.RegisterA(events.PlayerVolumeChangeCmd, "player.volume", func(evnt *event.Event) {
data := evnt.Data.(events.PlayerVolumeChangeCmdEvent)
err := libmpv.SetProperty("volume", mpv.FORMAT_DOUBLE, data.Volume)
if err != nil {
log.Warn("set volume failed", err)
}
})
global.EventManager.RegisterA(events.PlayerVideoPlayerSetWindowHandleCmd, "player.next", func(evnt *event.Event) {
handle := evnt.Data.(events.PlayerVideoPlayerSetWindowHandleCmdEvent).Handle
err := SetWindowHandle(handle)
if err != nil {
log.Warn("set window handle failed", err)
}
})
}
func SetWindowHandle(handle uintptr) error {
log.Infof("set window handle %d", handle)
_ = libmpv.SetOptionString("wid", fmt.Sprintf("%d", handle))
return libmpv.SetOptionString("vo", "gpu")
}
//func (p *MpvPlayer) IsIdle() bool {
// property, err := p.libmpv.GetProperty("idle-active", mpv.FORMAT_FLAG)
// if err != nil {
// p.log.Warn("[MPV Player] get property idle-active failed", err)
// return false
// }
// return property.(bool)
//}
//// GetAudioDeviceList get output device for mpv
//// return format is []AudioDevice
//func (p *MpvPlayer) GetAudioDeviceList() ([]model.AudioDevice, error) {
// p.log.Debugf("[MPV Player] 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 {
// p.log.Debugf("[MPV Player] set audio device %s for mpv", device)
// return p.libmpv.SetPropertyString("audio-device", device)
//}

11
internal/player/player.go Normal file
View File

@@ -0,0 +1,11 @@
package player
import "AynaLivePlayer/internal/player/mpv"
func SetupMpvPlayer() {
mpv.SetupPlayer()
}
func StopMpvPlayer() {
mpv.StopPlayer()
}

167
internal/playlist/model.go Normal file
View File

@@ -0,0 +1,167 @@
package playlist
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/event"
"math/rand"
"sync"
)
type playlist struct {
Index int
playlistId model.PlaylistID
mode model.PlaylistMode
Medias []model.Media
Lock sync.RWMutex
}
func newPlaylist(id model.PlaylistID) *playlist {
pl := &playlist{
playlistId: id,
Medias: make([]model.Media, 0),
Lock: sync.RWMutex{},
Index: 0,
}
global.EventManager.RegisterA(events.PlaylistMoveCmd(id), "internal.playlist.move", func(event *event.Event) {
e := event.Data.(events.PlaylistMoveCmdEvent)
pl.Move(e.From, e.To)
})
global.EventManager.RegisterA(events.PlaylistInsertCmd(id), "internal.playlist.insert", func(event *event.Event) {
e := event.Data.(events.PlaylistInsertCmdEvent)
pl.Insert(e.Position, e.Media)
})
global.EventManager.RegisterA(events.PlaylistDeleteCmd(id), "internal.playlist.delete", func(event *event.Event) {
e := event.Data.(events.PlaylistDeleteCmdEvent)
pl.Delete(e.Index)
})
global.EventManager.RegisterA(events.PlaylistNextCmd(id), "internal.playlist.next", func(event *event.Event) {
pl.Next(event.Data.(events.PlaylistNextCmdEvent).Remove)
})
global.EventManager.RegisterA(events.PlaylistModeChangeCmd(id), "internal.playlist.mode", func(event *event.Event) {
pl.mode = event.Data.(events.PlaylistModeChangeUpdateEvent).Mode
global.EventManager.CallA(events.PlaylistModeChangeUpdate(id), events.PlaylistModeChangeUpdateEvent{
Mode: pl.mode,
})
})
return pl
}
func (p *playlist) CopyMedia() []model.Media {
medias := make([]model.Media, len(p.Medias))
copy(medias, p.Medias)
return medias
}
func (p *playlist) Size() int {
return len(p.Medias)
}
func (p *playlist) Replace(medias []model.Media) {
p.Lock.Lock()
p.Medias = medias
p.Index = 0
p.Lock.Unlock()
global.EventManager.CallA(events.PlaylistDetailUpdate(p.playlistId), events.PlaylistDetailUpdateEvent{
Medias: p.CopyMedia(),
})
}
func (p *playlist) Insert(index int, media model.Media) {
p.Lock.Lock()
if index > p.Size() {
index = p.Size()
}
if index < 0 {
index = p.Size() + index + 1
}
p.Medias = append(p.Medias, model.Media{})
for i := p.Size() - 1; i > index; i-- {
p.Medias[i] = p.Medias[i-1]
}
p.Medias[index] = media
p.Lock.Unlock()
global.EventManager.CallA(events.PlaylistInsertUpdate(p.playlistId), events.PlaylistInsertUpdateEvent{
Position: index,
Media: media,
})
global.EventManager.CallA(events.PlaylistDetailUpdate(p.playlistId), events.PlaylistDetailUpdateEvent{
Medias: p.CopyMedia(),
})
}
func (p *playlist) Delete(index int) {
p.Lock.Lock()
if index >= p.Size() || index < 0 {
p.Lock.Unlock()
return
}
// todo: @5 delete optimization
p.Medias = append(p.Medias[:index], p.Medias[index+1:]...)
p.Lock.Unlock()
global.EventManager.CallA(events.PlaylistDetailUpdate(p.playlistId), events.PlaylistDetailUpdateEvent{
Medias: p.CopyMedia(),
})
}
func (p *playlist) Move(src int, dst int) {
if src >= p.Size() || src < 0 {
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()
global.EventManager.CallA(events.PlaylistDetailUpdate(p.playlistId), events.PlaylistDetailUpdateEvent{
Medias: p.CopyMedia(),
})
}
func (p *playlist) Next(delete bool) {
if p.Size() == 0 {
// no media in the playlist
// do not issue any event
return
}
var index int
index = p.Index
if p.mode == model.PlaylistModeRandom {
p.Index = rand.Intn(p.Size())
} else if p.mode == model.PlaylistModeNormal {
p.Index = (p.Index + 1) % p.Size()
} else {
p.Index = index
}
m := p.Medias[index]
global.EventManager.CallA(events.PlaylistNextUpdate(p.playlistId), events.PlaylistNextUpdateEvent{
Media: m,
})
if delete {
p.Delete(index)
if p.mode == model.PlaylistModeRandom {
p.Index = rand.Intn(p.Size())
} else if p.mode == model.PlaylistModeNormal {
p.Index = index
} else {
p.Index = index
}
}
}

View File

@@ -0,0 +1,48 @@
package playlist
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/event"
)
var PlayerPlaylist *playlist = nil
var HistoryPlaylist *playlist = nil
var SystemPlaylist *playlist = nil
var PlaylistsPlaylist *playlist = nil
type playlistConfig struct {
SystemPlaylistMode model.PlaylistMode
}
func (p *playlistConfig) Name() string {
return "playlist"
}
func (p *playlistConfig) OnLoad() {
return
}
func (p *playlistConfig) OnSave() {
return
}
var cfg = &playlistConfig{}
func Initialize() {
PlayerPlaylist = newPlaylist(model.PlaylistIDPlayer)
SystemPlaylist = newPlaylist(model.PlaylistIDSystem)
HistoryPlaylist = newPlaylist(model.PlaylistIDHistory)
PlaylistsPlaylist = newPlaylist(model.PlaylistIDPlaylists)
config.LoadConfig(cfg)
global.EventManager.RegisterA(events.PlaylistModeChangeCmd(model.PlaylistIDSystem), "internal.playlist.system_init", func(event *event.Event) {
cfg.SystemPlaylistMode = event.Data.(events.PlaylistModeChangeUpdateEvent).Mode
})
global.EventManager.CallA(events.PlaylistModeChangeUpdate(model.PlaylistIDSystem), events.PlaylistModeChangeUpdateEvent{
Mode: cfg.SystemPlaylistMode,
})
}

View File

@@ -0,0 +1,3 @@
package playlist
// todo: implement the playlist controller

View File

@@ -1,8 +1,8 @@
package internal
package todo
import (
"AynaLivePlayer/common/config"
"AynaLivePlayer/core/model"
"AynaLivePlayer/pkg/config"
"errors"
"fmt"
"github.com/go-resty/resty/v2"

View File

@@ -1,12 +1,12 @@
package internal
package todo
import (
"AynaLivePlayer/adapters/provider"
"AynaLivePlayer/common/config"
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/pkg/config"
event2 "AynaLivePlayer/pkg/event"
"errors"
)
@@ -15,7 +15,7 @@ type PlayController struct {
AudioDevice string
Volume float64
Cfg adapter.IPlayControlConfig
eventManager *event.Manager `ini:"-"`
eventManager *event2.Manager `ini:"-"`
player adapter.IPlayer `ini:"-"`
playlist adapter.IPlaylistController `ini:"-"`
provider adapter.IProviderController `ini:"-"`
@@ -46,7 +46,7 @@ func NewPlayerController(
provider adapter.IProviderController,
log adapter.ILogger) adapter.IPlayController {
pc := &PlayController{
eventManager: event.MainManager.NewChildManager(),
eventManager: event2.MainManager.NewChildManager(),
player: player,
playlist: playlist,
lyric: lyric,
@@ -69,7 +69,7 @@ func NewPlayerController(
return pc
}
func (pc *PlayController) handleMpvIdlePlayNext(event *event.Event) {
func (pc *PlayController) handleMpvIdlePlayNext(event *event2.Event) {
isIdle := event.Data.(events.PlayerPropertyUpdateEvent).Value.(bool)
if isIdle {
pc.log.Info("[Controller] mpv went idle, try play next")
@@ -77,7 +77,7 @@ func (pc *PlayController) handleMpvIdlePlayNext(event *event.Event) {
}
}
func (pc *PlayController) handlePlaylistAdd(event *event.Event) {
func (pc *PlayController) handlePlaylistAdd(event *event2.Event) {
if pc.player.IsIdle() {
pc.PlayNext()
return
@@ -89,7 +89,7 @@ func (pc *PlayController) handlePlaylistAdd(event *event.Event) {
}
}
func (pc *PlayController) handleLyricUpdate(event *event.Event) {
func (pc *PlayController) handleLyricUpdate(event *event2.Event) {
data := event.Data.(events.PlayerPropertyUpdateEvent).Value
if data == nil {
return
@@ -97,7 +97,7 @@ func (pc *PlayController) handleLyricUpdate(event *event.Event) {
pc.lyric.Update(data.(float64))
}
func (pc *PlayController) EventManager() *event.Manager {
func (pc *PlayController) EventManager() *event2.Manager {
return pc.eventManager
}

View File

@@ -1,16 +1,13 @@
package internal
package todo
import (
"AynaLivePlayer/adapters/provider"
"AynaLivePlayer/common/config"
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/event"
"errors"
"fmt"
"math/rand"
"sync"
)
type PlaylistController struct {
@@ -214,221 +211,3 @@ func (pc *PlaylistController) PreparePlaylistByIndex(index int) error {
}
return pc.provider.PreparePlaylist(pc.Playlists[index])
}
type playlistImpl struct {
model.Playlist
Index int
Lock sync.RWMutex
eventManager *event.Manager
log adapter.ILogger
}
func newPlaylistImpl(title string, em *event.Manager, log adapter.ILogger) adapter.IPlaylist {
return &playlistImpl{
Index: 0,
Playlist: model.Playlist{
Title: title,
Medias: make([]*model.Media, 0),
Mode: model.PlaylistModeNormal,
Meta: model.Meta{},
},
eventManager: em,
log: log,
}
}
func (p *playlistImpl) Model() *model.Playlist {
return &p.Playlist
}
func (p *playlistImpl) EventManager() *event.Manager {
return p.eventManager
}
func (p *playlistImpl) Identifier() string {
return p.Playlist.Meta.Identifier()
}
func (p *playlistImpl) DisplayName() string {
if p.Playlist.Title != "" {
return p.Playlist.Title
}
return p.Playlist.Meta.Identifier()
}
func (p *playlistImpl) Size() int {
return p.Playlist.Size()
}
func (p *playlistImpl) Get(index int) *model.Media {
if index < 0 || index >= p.Playlist.Size() {
return nil
}
return p.Playlist.Medias[index]
}
func (p *playlistImpl) Pop() *model.Media {
p.log.Debugf("[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 {
p.log.Warn("[Playlists] pop first media failed, no media left in the playlist")
return nil
}
p.eventManager.CallA(
events.EventPlaylistUpdate,
events.PlaylistUpdateEvent{
Playlist: p.Playlist.Copy(),
},
)
return m
}
func (p *playlistImpl) Replace(medias []*model.Media) {
p.log.Infof("[Playlists] %s replace all media", &p.Playlist)
p.Lock.Lock()
p.Playlist.Medias = medias
p.Index = 0
p.Lock.Unlock()
p.eventManager.CallA(
events.EventPlaylistUpdate,
events.PlaylistUpdateEvent{
Playlist: p.Playlist.Copy(),
},
)
}
func (p *playlistImpl) Push(media *model.Media) {
p.Insert(-1, media)
}
func (p *playlistImpl) Insert(index int, media *model.Media) {
p.log.Infof("[Playlists] insert media into new index %d at %s ", index, p.Playlist)
p.log.Debugf("media=%s %v", media.Title, media.Meta)
e := event.Event{
Id: events.EventPlaylistPreInsert,
Cancelled: false,
Data: events.PlaylistInsertEvent{
Playlist: p.Playlist.Copy(),
Index: index,
Media: media,
},
}
p.eventManager.Call(&e)
if e.Cancelled {
p.log.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(
events.EventPlaylistUpdate,
events.PlaylistUpdateEvent{
Playlist: p.Playlist.Copy(),
},
)
p.eventManager.CallA(
events.EventPlaylistInsert,
events.PlaylistInsertEvent{
Playlist: p.Playlist.Copy(),
Index: index,
Media: media,
},
)
}
func (p *playlistImpl) Delete(index int) *model.Media {
p.log.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 {
p.log.Warnf("[Playlists] media at index %d does not exist", index)
}
p.eventManager.CallA(
events.EventPlaylistUpdate,
events.PlaylistUpdateEvent{
Playlist: p.Playlist.Copy(),
})
return m
}
func (p *playlistImpl) Move(src int, dst int) {
p.log.Infof("[Playlists] from media from index %d to %d", src, dst)
if src >= p.Size() || src < 0 {
p.log.Warnf("[Playlists] 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(
events.EventPlaylistUpdate,
events.PlaylistUpdateEvent{
Playlist: p.Playlist.Copy(),
})
}
func (p *playlistImpl) Next() *model.Media {
p.log.Infof("[Playlists] %s get next media with random=%t", p, p.Mode == model.PlaylistModeRandom)
if p.Size() == 0 {
p.log.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
}

View File

@@ -1,4 +1,4 @@
package internal
package todo
import (
"AynaLivePlayer/core/adapter"

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