33 Commits

Author SHA1 Message Date
Aynakeya
e305e0da6e fix logger 2022-07-19 20:34:11 -07:00
Aynakeya
666ca2a16e update webinfo frontend 2022-07-19 20:27:55 -07:00
Aynakeya
bb24c54d46 update webinfo frontend 2022-07-19 20:25:58 -07:00
Aynakeya
5ea45858f7 update webinfo frontend 2022-07-19 20:24:58 -07:00
Aynakeya
90e5c8a9bf update webinfo frontend 2022-07-19 20:19:57 -07:00
Aynakeya
db2030b3c0 update webinfo frontend 2022-07-19 19:57:25 -07:00
Aynakeya
05c44079e2 fix bug/webinfo-add custom template 2022-07-19 18:09:44 -07:00
Aynakeya
f6b58a3e9d fix bug 2022-07-18 23:10:40 -07:00
Aynakeya
a385b341f7 fix bug/web template init 2022-07-18 22:58:52 -07:00
Aynakeya
f69327d53d Merge pull request #1 from aynakeya/add-license-1
Create LICENSE.md
2022-07-18 20:52:42 -07:00
Aynakeya
313e48cf21 Create LICENSE.md 2022-07-18 20:52:19 -07:00
Aynakeya
2b7151e1d2 web info first version/fix some lyric not update/update translation.json/fix some media in history list cann't be played again 2022-07-18 20:37:00 -07:00
Aynakeya
24457e0acd add stderr redirection, add logo 2022-07-15 16:30:50 -07:00
Aynakeya
8f2b975455 change puase icon 2022-07-14 01:04:09 -07:00
Aynakeya
9e7b062790 update webinfo control panel 2022-07-14 00:51:26 -07:00
Aynakeya
8602e6470a update todo 2022-07-13 19:20:25 -07:00
Aynakeya
41e2a4775a add netease login plugin 2022-07-13 18:59:04 -07:00
Aynakeya
6f5cfc9028 update frontend requirement 2022-07-13 15:35:14 -07:00
Aynakeya
f47cc14151 fix typo 2022-07-12 23:12:44 -07:00
Aynakeya
78cf40ff26 update frontend requirement 2022-07-12 23:09:58 -07:00
Aynakeya
d90311acfe add frontend requirement 2022-07-12 21:48:06 -07:00
Aynakeya
94615265bf add web output backend 2022-07-12 21:06:55 -07:00
Aynakeya
6e4c78daf2 add empty image output 2022-07-07 20:37:06 -07:00
Aynakeya
632f531cdb update to 0.8.0 2022-07-07 20:30:35 -07:00
Aynakeya
7bf9372898 add local provider, add cover output in textinfo 2022-07-07 20:30:12 -07:00
Aynakeya
fd91b1e130 add search for bilivideo 2022-07-07 00:44:10 -07:00
Aynakeya
3431b5cafe experiment ui 2022-07-05 09:26:19 -07:00
Aynakeya
3907ff96d5 experiment ui 2022-07-03 20:52:44 -07:00
Aynakeya
dfce89f96e add diange source command & fix bilibili video 2022-07-03 13:54:01 -07:00
Aynakeya
dc3ab46ad0 fix gui text error 2022-07-02 22:03:20 -07:00
Aynakeya
6bdb0acf93 fix bilibili video source header & fix player header setting 2022-07-02 22:01:11 -07:00
Aynakeya
6e18df9b41 add bilibili video 2022-07-02 10:09:19 -07:00
Aynakeya
e0849f0d65 music id match up 2022-07-02 08:41:31 -07:00
59 changed files with 2173 additions and 189 deletions

4
.gitignore vendored
View File

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

21
LICENSE.md Normal file
View File

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

View File

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

View File

@@ -9,7 +9,7 @@ import (
)
func main() {
fmt.Printf("BiliAudioBot Revive %s\n", config.VERSION)
fmt.Printf("BiliAudioBot Revive %s\n", config.Version)
logger.Logger.SetLevel(logrus.DebugLevel)
fmt.Println("Please enter room id")
var roomid string

View File

@@ -4,38 +4,26 @@ import (
"AynaLivePlayer/config"
"AynaLivePlayer/controller"
"AynaLivePlayer/gui"
"AynaLivePlayer/i18n"
"AynaLivePlayer/logger"
"AynaLivePlayer/plugin/diange"
"AynaLivePlayer/plugin/qiege"
"AynaLivePlayer/plugin/textinfo"
"fmt"
"github.com/mitchellh/panicwrap"
"github.com/sirupsen/logrus"
"os"
"AynaLivePlayer/plugin/webinfo"
"AynaLivePlayer/plugin/wylogin"
)
func init() {
exitStatus, _ := panicwrap.BasicWrap(func(s string) {
logger.Logger.Panic(s)
os.Exit(1)
return
})
if exitStatus >= 0 {
os.Exit(exitStatus)
}
}
var plugins = []controller.Plugin{diange.NewDiange(), qiege.NewQiege(), textinfo.NewTextInfo(), webinfo.NewWebInfo(),
wylogin.NewWYLogin()}
func main() {
fmt.Printf("BiliAudioBot Revive %s\n", config.VERSION)
logger.Logger.SetLevel(logrus.DebugLevel)
logger.Logger.Info("================Program Start================")
logger.Logger.Infof("================Current Version: %s================", config.Version)
controller.Initialize()
controller.LoadPlugins(diange.NewDiange(), qiege.NewQiege(), textinfo.NewTextInfo())
defer func() {
controller.Destroy()
config.SaveToConfigFile(config.CONFIG_PATH)
i18n.SaveTranslation()
}()
controller.LoadPlugins(plugins...)
gui.Initialize()
gui.MainWindow.ShowAndRun()
controller.ClosePlugins(plugins...)
controller.Destroy()
_ = config.SaveToConfigFile(config.ConfigPath)
logger.Logger.Info("================Program End================")
}

View File

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

BIN
assets/icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -96,6 +96,10 @@
"en": "Please enter the ID or URL of the song you want to add.",
"zh-CN": "输入歌单ID或者歌单网址。"
},
"gui.playlist.add.source": {
"en": "Source",
"zh-CN": "来源"
},
"gui.playlist.add.title": {
"en": "Add Playlist",
"zh-CN": "添加歌单"
@@ -232,6 +236,10 @@
"en": "Max Queue",
"zh-CN": "最大点歌数"
},
"plugin.diange.source_cmd": {
"en": "Source Command",
"zh-CN": "来源点歌命令"
},
"plugin.diange.title": {
"en": "Diange",
"zh-CN": "点歌"
@@ -240,6 +248,38 @@
"en": "User",
"zh-CN": "普通用户"
},
"plugin.neteaselogin.current_user": {
"en": "Current User:",
"zh-CN": "当前用户:"
},
"plugin.neteaselogin.current_user.notlogin": {
"en": "Not Login",
"zh-CN": "未登录"
},
"plugin.neteaselogin.description": {
"en": "Netease User Login",
"zh-CN": "网易云登录"
},
"plugin.neteaselogin.logout": {
"en": "Logout",
"zh-CN": "登出"
},
"plugin.neteaselogin.qr.finish": {
"en": "Finish Scan",
"zh-CN": "完成扫描后按我"
},
"plugin.neteaselogin.qr.new": {
"en": "Get a new qr code",
"zh-CN": "获取新二维码"
},
"plugin.neteaselogin.refresh": {
"en": "Refresh",
"zh-CN": "刷新状态"
},
"plugin.neteaselogin.title": {
"en": "Netease Login",
"zh-CN": "网易云登录"
},
"plugin.qiege.admin": {
"en": "Admin",
"zh-CN": "管理员"
@@ -283,6 +323,50 @@
"plugin.textinfo.title": {
"en": "Text Output",
"zh-CN": "文本输出"
},
"plugin.webinfo.description": {
"en": "Web output configuration",
"zh-CN": "web输出设置"
},
"plugin.webinfo.port": {
"en": "Port",
"zh-CN": "服务器端口"
},
"plugin.webinfo.server_control": {
"en": "Control",
"zh-CN": "操作"
},
"plugin.webinfo.server_control.restart": {
"en": "Restart",
"zh-CN": "重启"
},
"plugin.webinfo.server_control.start": {
"en": "Start",
"zh-CN": "启动"
},
"plugin.webinfo.server_control.stop": {
"en": "Stop",
"zh-CN": "停止"
},
"plugin.webinfo.server_status": {
"en": "Server Status",
"zh-CN": "服务器状态"
},
"plugin.webinfo.server_status.running": {
"en": "Running",
"zh-CN": "运行中"
},
"plugin.webinfo.server_status.stopped": {
"en": "Stopped",
"zh-CN": "已停止"
},
"plugin.webinfo.title": {
"en": "Web Output",
"zh-CN": "Web输出"
},
"plugin.webinfo.server_preview": {
"en": "Server Preview",
"zh-CN":"效果预览"
}
}
}

0
assets/webinfo/.gitkeep Normal file
View File

View File

@@ -6,13 +6,18 @@ import (
"path"
)
const VERSION = "alpha 0.7.0"
const (
ProgramName = "卡西米尔唱片机"
Version = "beta 0.9.0"
)
const CONFIG_PATH = "./config.ini"
const Assests_PATH = "./assets"
const (
ConfigPath = "./config.ini"
AssetsPath = "./assets"
)
func GetAssetPath(name string) string {
return path.Join(Assests_PATH, name)
return path.Join(AssetsPath, name)
}
type Config interface {
@@ -33,7 +38,7 @@ func LoadConfig(cfg Config) {
func init() {
var err error
ConfigFile, err = ini.Load(CONFIG_PATH)
ConfigFile, err = ini.Load(ConfigPath)
if err != nil {
fmt.Println("config not found, using default config")
ConfigFile = ini.Empty()

View File

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

View File

@@ -15,8 +15,8 @@ func (c *_PlayerConfig) Name() string {
}
var Player = &_PlayerConfig{
Playlists: []string{"2382819181", "4987059624", "646548465"},
PlaylistsProvider: []string{"netease", "netease", "netease"},
Playlists: []string{"2382819181", "4987059624", "list1"},
PlaylistsProvider: []string{"netease", "netease", "local"},
PlaylistIndex: 0,
PlaylistRandom: true,
AudioDevice: "auto",

View File

@@ -10,6 +10,6 @@ func (c *_ProviderConfig) Name() string {
}
var Provider = &_ProviderConfig{
Priority: []string{"local", "netease", "kuwo", "bilibili"},
Priority: []string{"netease", "kuwo", "bilibili", "local", "bilibili-video"},
LocalDir: "./music",
}

View File

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

View File

@@ -21,7 +21,7 @@ func PlayNext() {
}
func Play(media *player.Media) {
l().Info("prepare media")
l().Infof("prepare media %s", media.Title)
err := PrepareMedia(media)
if err != nil {
l().Warn("prepare media failed. try play next")
@@ -32,6 +32,7 @@ func Play(media *player.Media) {
AddToHistory(media)
if err := MainPlayer.Play(media); err != nil {
l().Warn("play failed", err)
return
}
CurrentLyric.Reload(media.Lyric)
// reset
@@ -39,33 +40,40 @@ func Play(media *player.Media) {
}
func Add(keyword string, user interface{}) {
medias, err := Search(keyword)
if err != nil {
l().Warnf("search for %s, got error %s", keyword, err)
return
media := MediaMatch(keyword)
if media == nil {
medias, err := Search(keyword)
if err != nil {
l().Warnf("search for %s, got error %s", keyword, err)
return
}
if len(medias) == 0 {
l().Info("search for %s, got no result", keyword)
return
}
media = medias[0]
}
if len(medias) == 0 {
l().Info("search for %s, got no result", keyword)
return
}
media := medias[0]
media.User = user
l().Infof("add media %s (%s)", media.Title, media.Artist)
UserPlaylist.Insert(-1, media)
}
func AddWithProvider(keyword string, pname string, user interface{}) {
medias, err := provider.Search(pname, keyword)
if err != nil {
l().Warnf("search for %s, got error %s", keyword, err)
return
media := provider.MatchMedia(pname, keyword)
if media == nil {
medias, err := provider.Search(pname, keyword)
if err != nil {
l().Warnf("search for %s, got error %s", keyword, err)
return
}
if len(medias) == 0 {
l().Infof("search for %s, got no result", keyword)
return
}
media = medias[0]
}
if len(medias) == 0 {
l().Info("search for %s, got no result", keyword)
}
media := medias[0]
media.User = user
l().Info("add media %s (%s)", media.Title, media.Artist)
l().Infof("add media %s (%s)", media.Title, media.Artist)
UserPlaylist.Insert(-1, media)
}

View File

@@ -5,6 +5,11 @@ import "AynaLivePlayer/player"
func AddToHistory(media *player.Media) {
l().Tracef("add media %s (%s) to history", media.Title, media.Artist)
media = media.Copy()
// reset url for future use
media.Url = ""
if History.Size() >= 1024 {
History.Replace([]*player.Media{})
}
History.Push(media)
return
}

View File

@@ -3,6 +3,7 @@ package controller
type Plugin interface {
Name() string
Enable() error
Disable() error
}
func LoadPlugin(plugin Plugin) {
@@ -17,3 +18,13 @@ func LoadPlugins(plugins ...Plugin) {
LoadPlugin(plugin)
}
}
func ClosePlugins(plugins ...Plugin) {
for _, plugin := range plugins {
err := plugin.Disable()
if err != nil {
l().Warnf("Failed to close plugin: %s, %s", plugin.Name(), err)
return
}
}
}

View File

@@ -8,27 +8,45 @@ import (
func PrepareMedia(media *player.Media) error {
var err error
if media.Title == "" || media.Cover == "" {
if media.Title == "" || !media.Cover.Exists() {
l().Trace("fetching media info")
if err = provider.UpdateMedia(media); err != nil {
l().Warn("fail to prepare media when fetch info", err)
return err
}
}
if media.Url == "" {
l().Trace("fetching media url")
if err = provider.UpdateMediaUrl(media); err != nil {
l().Warn("fail to prepare media when url", err)
return err
}
}
if media.Lyric == "" {
l().Trace("fetching media lyric")
if err = provider.UpdateMediaLyric(media); err != nil {
l().Warn("fail to prepare media when lyric", err)
}
}
return err
return nil
}
func MediaMatch(keyword string) *player.Media {
l().Infof("Match media for %s", keyword)
for _, p := range config.Provider.Priority {
if pr, ok := provider.Providers[p]; ok {
m := pr.MatchMedia(keyword)
if m == nil {
continue
}
if err := provider.UpdateMedia(m); err == nil {
return m
}
} else {
l().Warnf("Provider %s not exist", p)
}
}
return nil
}
func Search(keyword string) ([]*player.Media, error) {

17
go.mod
View File

@@ -4,28 +4,19 @@ go 1.16
require (
fyne.io/fyne/v2 v2.1.4
github.com/BurntSushi/toml v0.4.1
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9
github.com/XiaoMengXinX/Music163Api-Go v0.1.26
github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/aynakeya/blivedm v0.1.3
github.com/aynakeya/go-mpv v0.0.4
github.com/go-ole/go-ole v1.2.6
github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086
github.com/go-resty/resty/v2 v2.7.0
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526
github.com/gorilla/websocket v1.5.0
github.com/jinzhu/copier v0.3.5
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca
github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b
github.com/mitchellh/panicwrap v1.0.0
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.8.1
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/cast v1.3.1
github.com/stretchr/testify v1.5.1
github.com/tidwall/gjson v1.14.1
github.com/urfave/cli/v2 v2.3.0
golang.org/x/mod v0.4.2
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
golang.org/x/tools v0.1.5
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
gopkg.in/ini.v1 v1.66.4
)

27
go.sum
View File

@@ -1,21 +1,21 @@
fyne.io/fyne/v2 v2.1.4 h1:bt1+28++kAzRzPB0GM2EuSV4cnl8rXNX4cjfd8G06Rc=
fyne.io/fyne/v2 v2.1.4/go.mod h1:p+E/Dh+wPW8JwR2DVcsZ9iXgR9ZKde80+Y+40Is54AQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9 h1:1ltqoej5GtaWF8jaiA49HwsZD459jqm9YFz9ZtMFpQA=
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
github.com/XiaoMengXinX/Music163Api-Go v0.1.26 h1:Nybor5okI8C0jzAiRvGfpLHdDrPqUbjx5kXWIZDX6pw=
github.com/XiaoMengXinX/Music163Api-Go v0.1.26/go.mod h1:kLU/CkLxKnEJFCge0URvQ0lHt6ImoG1/2aVeNbgV2RQ=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/antonfisher/nested-logrus-formatter v1.3.1 h1:NFJIr+pzwv5QLHTPyKz9UMEoHck02Q9L0FP13b/xSbQ=
github.com/antonfisher/nested-logrus-formatter v1.3.1/go.mod h1:6WTfyWFkBc9+zyBaKIqRrg/KwMqBbodBjgbHjDz7zjA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/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/itl v0.0.0-20170329215456-9fbe21093131/go.mod h1:eVWQJVQ67aMvYhpkDwaH2Goy2vo6v8JCMfGXfQ9sPtw=
github.com/dhowden/plist v0.0.0-20141002110153-5db6e0d9931a/go.mod h1:sLjdR6uwx3L6/Py8F+QgAfeiuY87xuYGwCDqRFrvCzw=
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/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA=
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
@@ -24,7 +24,6 @@ github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f h1:s0O46d8fPwk9kU4k1jj76w
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be h1:Z28GdQBfKOL8tNHjvaDn3wHDO7AzTRkmAXvHvnopp98=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/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=
@@ -35,36 +34,32 @@ github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1E
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526 h1:NfuKjkj/Xc2z1xZIj+EmNCm5p1nKJPyw3F4E20usXvg=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca h1:ozPUX9TKQZVek4lZWYRsQo7uS8vJ+q4OOHvRhHiCLfU=
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b h1:tLSDWcFhT0WRlnsFszh4iaFTexWF8mmccGTk88Siq7Q=
github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc=
github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE=
github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
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/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
@@ -86,8 +81,9 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.3.8 h1:Nw158Q8QN+CPgTmVRByhVwapp8Mm1e2blinhmx4wx5E=
github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
@@ -95,7 +91,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -122,11 +117,9 @@ golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=

View File

@@ -22,6 +22,10 @@ func (t *TestConfig) CreatePanel() fyne.CanvasObject {
}
func createConfigLayout() fyne.CanvasObject {
// initialize config panels
for _, c := range ConfigList {
c.CreatePanel()
}
content := container.NewMax()
entryList := widget.NewList(
func() int {

View File

@@ -4,6 +4,7 @@ import (
"AynaLivePlayer/config"
"AynaLivePlayer/i18n"
"AynaLivePlayer/logger"
"AynaLivePlayer/resource"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
@@ -29,13 +30,14 @@ func l() *logrus.Entry {
}
func Initialize() {
l().Info("Initializing GUI")
os.Setenv("FYNE_FONT", config.GetAssetPath("msyh.ttc"))
App = app.New()
MainWindow = App.NewWindow(fmt.Sprintf("AynaLivePlayer Ver.%s", config.VERSION))
MainWindow = App.NewWindow(fmt.Sprintf("%s Ver.%s", config.ProgramName, config.Version))
tabs := container.NewAppTabs(
container.NewTabItem(i18n.T("gui.tab.player"),
newPaddedBoarder(nil, createPlayController(), nil, nil, createPlaylist()),
newPaddedBoarder(nil, createPlayControllerV2(), nil, nil, createPlaylist()),
),
container.NewTabItem(i18n.T("gui.tab.search"),
newPaddedBoarder(createSearchBar(), nil, nil, nil, createSearchList()),
@@ -55,7 +57,7 @@ func Initialize() {
)
tabs.SetTabLocation(container.TabLocationTop)
MainWindow.SetIcon(fyne.NewStaticResource("icon", resource.ProgramIcon))
MainWindow.SetContent(tabs)
//MainWindow.Resize(fyne.NewSize(1280, 720))
MainWindow.Resize(fyne.NewSize(960, 480))

View File

@@ -1,8 +1,13 @@
package gui
import (
"AynaLivePlayer/player"
"bytes"
"errors"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/widget"
)
@@ -74,3 +79,28 @@ func newFixedSplitContainer(horizontal bool, leading, trailing fyne.CanvasObject
fs.Split.BaseWidget.ExtendBaseWidget(s)
return fs
}
func newImageFromPlayerPicture(picture player.Picture) (*canvas.Image, error) {
if picture.Data != nil {
img := canvas.NewImageFromReader(bytes.NewReader(picture.Data), "cover")
// return an error when img is nil
if img == nil {
return nil, errors.New("fail to read image")
}
return img, nil
} else {
uri, err := storage.ParseURI(picture.Url)
if err != nil {
return nil, err
}
if uri == nil {
return nil, errors.New("fail to fail url")
}
img := canvas.NewImageFromURI(uri)
if img == nil {
// bug fix, return a new error to indicate fail to read an image
return nil, errors.New("fail to read image")
}
return img, nil
}
}

View File

@@ -0,0 +1,52 @@
package gui
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
)
// AsyncButton is a Button that handle OnTapped handler asynchronously.
type AsyncButton struct {
widget.Button
anim *fyne.Animation
}
func NewAsyncButton(label string, tapped func()) *AsyncButton {
button := &AsyncButton{
Button: widget.Button{
Text: label,
OnTapped: tapped,
},
}
button.ExtendBaseWidget(button)
return button
}
func NewAsyncButtonWithIcon(label string, icon fyne.Resource, tapped func()) *AsyncButton {
button := &AsyncButton{
Button: widget.Button{
Text: label,
Icon: icon,
OnTapped: tapped,
},
}
button.ExtendBaseWidget(button)
return button
}
func (b *AsyncButton) Tapped(e *fyne.PointEvent) {
if b.Disabled() {
return
}
// missing animation
b.Refresh()
if b.OnTapped != nil {
b.Disable()
go func() {
b.OnTapped()
b.Enable()
}()
}
}

View File

@@ -10,7 +10,6 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/aynakeya/go-mpv"
@@ -33,8 +32,7 @@ type PlayControllerContainer struct {
}
func (p *PlayControllerContainer) SetDefaultCover() {
p.Cover.Resource = nil
p.Cover.File = config.GetAssetPath("empty.png")
p.Cover.Resource = ResEmptyImage
p.Cover.Refresh()
}
@@ -81,11 +79,7 @@ func registerPlayControllerHandler() {
controller.Seek(0, true)
}
PlayController.ButtonSwitch.OnTapped = func() {
if controller.Toggle() {
PlayController.ButtonSwitch.Icon = theme.MediaPlayIcon()
} else {
PlayController.ButtonSwitch.Icon = theme.MediaStopIcon()
}
controller.Toggle()
}
PlayController.ButtonNext.OnTapped = func() {
controller.PlayNext()
@@ -106,7 +100,7 @@ func registerPlayControllerHandler() {
if property.Data.(mpv.Node).Value.(bool) {
PlayController.ButtonSwitch.Icon = theme.MediaPlayIcon()
} else {
PlayController.ButtonSwitch.Icon = theme.MediaStopIcon()
PlayController.ButtonSwitch.Icon = theme.MediaPauseIcon()
}
}) != nil {
l().Error("fail to register handler for switch button with property pause")
@@ -186,30 +180,73 @@ func registerPlayControllerHandler() {
controller.MainPlayer.EventHandler.RegisterA(player.EventPlay, "gui.player.updateinfo", func(event *event.Event) {
l().Debug("receive EventPlay update player info")
media := event.Data.(player.PlayEvent).Media
//PlayController.Title.SetText(
// util.StringNormalize(media.Title, 16, 16))
//PlayController.Artist.SetText(
// util.StringNormalize(media.Artist, 16, 16))
PlayController.Title.SetText(
util.StringNormalize(media.Title, 16, 16))
media.Title)
PlayController.Artist.SetText(
util.StringNormalize(media.Artist, 16, 16))
media.Artist)
PlayController.Username.SetText(media.ToUser().Name)
if media.Cover == "" {
if !media.Cover.Exists() {
PlayController.SetDefaultCover()
} else {
uri, err := storage.ParseURI(media.Cover)
if err != nil {
l().Warn("fail to load parse cover url", media.Cover)
}
// async update
go func() {
img := canvas.NewImageFromURI(uri)
if img == nil {
picture, err := newImageFromPlayerPicture(media.Cover)
if err != nil {
l().Warn("fail to load parse cover url", media.Cover)
PlayController.SetDefaultCover()
return
}
PlayController.Cover.Resource = img.Resource
PlayController.Cover.Resource = picture.Resource
PlayController.Cover.Refresh()
}()
}
})
return
}
func createPlayControllerV2() fyne.CanvasObject {
PlayController.Cover = canvas.NewImageFromResource(ResEmptyImage)
PlayController.Cover.SetMinSize(fyne.NewSize(128, 128))
PlayController.Cover.FillMode = canvas.ImageFillContain
PlayController.ButtonPrev = widget.NewButtonWithIcon("", theme.MediaSkipPreviousIcon(), func() {})
PlayController.ButtonSwitch = widget.NewButtonWithIcon("", theme.MediaPlayIcon(), func() {})
PlayController.ButtonNext = widget.NewButtonWithIcon("", theme.MediaSkipNextIcon(), func() {})
buttonsBox := container.NewHBox(PlayController.ButtonPrev, PlayController.ButtonSwitch, PlayController.ButtonNext)
PlayController.Volume = widget.NewSlider(0, 100)
PlayController.ButtonLrc = widget.NewButton(i18n.T("gui.player.button.lrc"), func() {})
controls := container.NewPadded(container.NewBorder(nil, nil,
buttonsBox, nil,
container.NewGridWithColumns(
2,
container.NewMax(),
container.NewBorder(nil, nil, widget.NewIcon(theme.VolumeMuteIcon()), PlayController.ButtonLrc,
PlayController.Volume)),
))
PlayController.Progress = widget.NewSlider(0, 1000)
PlayController.CurrentTime = widget.NewLabel("0:00")
PlayController.TotalTime = widget.NewLabel("0:00")
progressItem := container.NewBorder(nil, nil,
PlayController.CurrentTime,
PlayController.TotalTime,
PlayController.Progress)
PlayController.Title = widget.NewLabel("Title")
PlayController.Artist = widget.NewLabel("Artist")
PlayController.Username = widget.NewLabel("Username")
playInfo := container.NewBorder(nil, nil, nil, PlayController.Username,
container.NewHBox(PlayController.Title, PlayController.Artist))
registerPlayControllerHandler()
return container.NewBorder(nil, nil, container.NewHBox(PlayController.Cover, widget.NewSeparator()), nil,
container.NewVBox(playInfo, progressItem, controls))
}

View File

@@ -55,7 +55,7 @@ func createPlaylists() fyne.CanvasObject {
container.NewVBox(
container.New(
layout.NewFormLayout(),
widget.NewLabel(i18n.T("gui.playlist.add.confirm")),
widget.NewLabel(i18n.T("gui.playlist.add.source")),
providerEntry,
widget.NewLabel(i18n.T("gui.playlist.add.id_url")),
idEntry,

8
gui/resource.go Normal file
View File

@@ -0,0 +1,8 @@
package gui
import (
"AynaLivePlayer/resource"
"fyne.io/fyne/v2"
)
var ResEmptyImage = fyne.NewStaticResource("empty", resource.EmptyImage)

View File

@@ -2,6 +2,7 @@ package gui
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
@@ -11,5 +12,10 @@ type RoomLoggerContainer struct {
var RoomLogger = &RoomLoggerContainer{}
func createRoomLogger() fyne.CanvasObject {
return widget.NewLabel("广告位招租")
//b := NewAsyncButton("ceshi", func() {
// time.Sleep(time.Second * 5)
//})
return container.NewVBox(
widget.NewLabel("广告位招租"),
)
}

View File

@@ -4,6 +4,7 @@ import (
"AynaLivePlayer/config"
nested "github.com/antonfisher/nested-logrus-formatter"
"github.com/sirupsen/logrus"
"github.com/virtuald/go-paniclog"
"io"
"os"
)
@@ -13,15 +14,21 @@ 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,
})
file, err := os.OpenFile(config.Log.Path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err == nil {
Logger.Out = io.MultiWriter(file, os.Stdout)
} else {
Logger.Info("Failed to log to file, using default stdout")
}
Logger.SetFormatter(&nested.Formatter{
FieldsOrder: []string{"Module"},
HideKeys: true,
NoColors: true,
})
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)
}
}
}

Binary file not shown.

View File

@@ -41,6 +41,9 @@ func (l *Lyric) Reload(lyric string) {
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: "",

View File

@@ -5,10 +5,19 @@ import (
"github.com/jinzhu/copier"
)
type Picture struct {
Url string
Data []byte
}
func (p Picture) Exists() bool {
return p.Url != "" || p.Data != nil
}
type Media struct {
Title string
Artist string
Cover string
Cover Picture
Album string
Lyric string
Url string

View File

@@ -75,16 +75,17 @@ func (p *Player) l() *logrus.Entry {
func (p *Player) Play(media *Media) error {
p.l().Infof("Play media %s", media.Url)
p.l().Trace("set user-agent for mpv player")
if val, ok := media.Header["user-agent"]; ok {
if val, ok := media.Header["User-Agent"]; ok {
p.l().Debug("set user-agent for mpv player")
err := p.libmpv.SetPropertyString("user-agent", val)
if err != nil {
p.l().Warn("set player user-agent failed", err)
return err
}
}
p.l().Trace("set referrer for mpv player")
if val, ok := media.Header["referrer"]; ok {
if val, ok := media.Header["Referer"]; ok {
p.l().Debug("set referrer for mpv player")
err := p.libmpv.SetPropertyString("referrer", val)
if err != nil {
p.l().Warn("set player referrer failed", err)

View File

@@ -7,6 +7,7 @@ import (
"AynaLivePlayer/i18n"
"AynaLivePlayer/liveclient"
"AynaLivePlayer/logger"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
@@ -29,6 +30,7 @@ type Diange struct {
QueueMax int
UserCoolDown int
CustomCMD string
SourceCMD []string
cooldowns map[string]int
panel fyne.CanvasObject
}
@@ -41,6 +43,7 @@ func NewDiange() *Diange {
QueueMax: 128,
UserCoolDown: -1,
CustomCMD: "add",
SourceCMD: make([]string, 0),
cooldowns: make(map[string]int),
}
}
@@ -51,21 +54,50 @@ func (d *Diange) Name() string {
func (d *Diange) Enable() error {
config.LoadConfig(d)
d.initCMD()
controller.AddCommand(d)
gui.AddConfigLayout(d)
return nil
}
func (d *Diange) Match(command string) bool {
for _, c := range []string{"点歌", d.CustomCMD} {
if command == c {
return true
func (d *Diange) Disable() error {
return nil
}
func (d *Diange) initCMD() {
if len(d.SourceCMD) == len(config.Provider.Priority) {
return
}
if len(d.SourceCMD) > len(config.Provider.Priority) {
d.SourceCMD = d.SourceCMD[:len(config.Provider.Priority)]
return
}
for i := len(d.SourceCMD); i < len(config.Provider.Priority); i++ {
d.SourceCMD = append(d.SourceCMD, "点歌"+config.Provider.Priority[i])
}
}
// isCMD return int if the commmand name matches our command
// -1 = not match, 0 = normal command, 1+ = source command
func (d *Diange) isCMD(cmd string) int {
if cmd == "点歌" || cmd == d.CustomCMD {
return 0
}
fmt.Println(d.SourceCMD)
for index, c := range d.SourceCMD {
if cmd == c {
return index + 1
}
}
return false
return -1
}
func (d *Diange) Match(command string) bool {
return d.isCMD(command) >= 0
}
func (d *Diange) Execute(command string, args []string, danmu *liveclient.DanmuMessage) {
l().Infof("%s(%s) Execute command: %s %s", danmu.User.Username, danmu.User.Uid, command, args)
// if queue is full, return
if controller.UserPlaylist.Size() >= d.QueueMax {
l().Info("Queue is full, ignore diange")
@@ -77,6 +109,7 @@ func (d *Diange) Execute(command string, args []string, danmu *liveclient.DanmuM
l().Infof("User %s(%s) still in cool down period, diange failed", danmu.User.Username, danmu.User.Uid)
return
}
cmdType := d.isCMD(command)
keyword := strings.Join(args, " ")
perm := d.UserPermission
l().Trace("user permission check: ", perm)
@@ -84,10 +117,15 @@ func (d *Diange) Execute(command string, args []string, danmu *liveclient.DanmuM
l().Trace("privilege permission check: ", perm)
perm = perm || (d.AdminPermission && (danmu.User.Admin))
l().Trace("admin permission check: ", perm)
if perm {
// reset cool down
d.cooldowns[danmu.User.Uid] = ct
if !perm {
return
}
// reset cool down
d.cooldowns[danmu.User.Uid] = ct
if cmdType == 0 {
controller.Add(keyword, &danmu.User)
} else {
controller.AddWithProvider(keyword, config.Provider.Priority[cmdType-1], &danmu.User)
}
}
@@ -121,6 +159,17 @@ func (d *Diange) CreatePanel() fyne.CanvasObject {
widget.NewLabel(i18n.T("plugin.diange.custom_cmd")), nil,
widget.NewEntryWithData(binding.BindString(&d.CustomCMD)),
)
d.panel = container.NewVBox(dgPerm, dgQueue, dgCoolDown, dgShortCut)
sourceCmds := []fyne.CanvasObject{}
for i, _ := range d.SourceCMD {
sourceCmds = append(
sourceCmds,
container.NewBorder(
nil, nil, widget.NewLabel(config.Provider.Priority[i]), nil,
widget.NewEntryWithData(binding.BindString(&d.SourceCMD[i]))))
}
dgSourceCMD := container.NewBorder(
nil, nil, widget.NewLabel(i18n.T("plugin.diange.source_cmd")), nil,
container.NewVBox(sourceCmds...))
d.panel = container.NewVBox(dgPerm, dgQueue, dgCoolDown, dgShortCut, dgSourceCMD)
return d.panel
}

View File

@@ -48,6 +48,10 @@ func (d *Qiege) Enable() error {
return nil
}
func (d *Qiege) Disable() error {
return nil
}
func (d *Qiege) Match(command string) bool {
for _, c := range []string{"切歌", d.CustomCMD} {
if command == c {

View File

@@ -13,6 +13,7 @@ import (
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/widget"
"github.com/aynakeya/go-mpv"
"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
"io/ioutil"
"os"
@@ -41,6 +42,7 @@ type MediaInfo struct {
Artist string
Album string
Username string
Cover player.Picture
}
type OutInfo struct {
@@ -53,14 +55,16 @@ type OutInfo struct {
}
type TextInfo struct {
Rendering bool
info OutInfo
templates []*Template
panel fyne.CanvasObject
Rendering bool
info OutInfo
templates []*Template
emptyCover []byte
panel fyne.CanvasObject
}
func NewTextInfo() *TextInfo {
return &TextInfo{Rendering: true}
b, _ := ioutil.ReadFile(config.GetAssetPath("empty.png"))
return &TextInfo{Rendering: true, emptyCover: b}
}
func (t *TextInfo) Title() string {
@@ -102,6 +106,10 @@ func (t *TextInfo) Enable() (err error) {
return nil
}
func (d *TextInfo) Disable() error {
return nil
}
func (t *TextInfo) reloadTemplates() {
var err error
t.templates = make([]*Template, 0)
@@ -159,6 +167,38 @@ func (t *TextInfo) RenderTemplates() {
}
}
func (t *TextInfo) OutputCover() {
if !t.Rendering {
return
}
if !t.info.Current.Cover.Exists() {
err := ioutil.WriteFile(filepath.Join(Out_Path, "cover.jpg"), t.emptyCover, 0666)
if err != nil {
l().Warnf("write cover file failed: %s", err)
}
return
}
if t.info.Current.Cover.Data != nil {
err := ioutil.WriteFile(filepath.Join(Out_Path, "cover.jpg"), t.info.Current.Cover.Data, 0666)
if err != nil {
l().Warnf("write cover file failed: %s", err)
}
return
}
go func() {
resp, err := resty.New().R().
Get(t.info.Current.Cover.Url)
if err != nil {
l().Warnf("get cover %s content failed: %s", t.info.Current.Cover.Url, err)
return
}
err = ioutil.WriteFile(filepath.Join(Out_Path, "cover.jpg"), resp.Body(), 0666)
if err != nil {
l().Warnf("write cover file failed: %s", err)
}
}()
}
func (t *TextInfo) registerHandlers() {
controller.MainPlayer.EventHandler.RegisterA(player.EventPlay, "plugin.textinfo.current", func(event *event.Event) {
t.info.Current = MediaInfo{
@@ -166,9 +206,11 @@ func (t *TextInfo) registerHandlers() {
Title: event.Data.(player.PlayEvent).Media.Title,
Artist: event.Data.(player.PlayEvent).Media.Artist,
Album: event.Data.(player.PlayEvent).Media.Album,
Cover: event.Data.(player.PlayEvent).Media.Cover,
Username: event.Data.(player.PlayEvent).Media.ToUser().Name,
}
t.RenderTemplates()
t.OutputCover()
})
if controller.MainPlayer.ObserveProperty("time-pos", func(property *mpv.EventProperty) {
if property.Data == nil {
@@ -190,6 +232,7 @@ func (t *TextInfo) registerHandlers() {
return
}
t.info.TotalTime = int(property.Data.(mpv.Node).Value.(float64))
t.RenderTemplates()
}) != nil {
l().Error("fail to register handler for total time with property duration")
}

33
plugin/webinfo/info.go Normal file
View File

@@ -0,0 +1,33 @@
package webinfo
import "AynaLivePlayer/player"
type MediaInfo struct {
Index int
Title string
Artist string
Album string
Username string
Cover player.Picture
}
type OutInfo struct {
Current MediaInfo
CurrentTime int
TotalTime int
Lyric string
Playlist []MediaInfo
}
const (
OutInfoC = "Current"
OutInfoCT = "CurrentTime"
OutInfoTT = "TotalTime"
OutInfoL = "Lyric"
OutInfoPL = "Playlist"
)
type WebsocketData struct {
Update string
Data OutInfo
}

229
plugin/webinfo/server.go Normal file
View File

@@ -0,0 +1,229 @@
package webinfo
import (
"AynaLivePlayer/config"
"context"
"encoding/json"
"fmt"
"github.com/gorilla/websocket"
"net/http"
"sync"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type WebInfoServer struct {
Info OutInfo
Port int
ServerMux *http.ServeMux
Server *http.Server
Clients map[*Client]int
Running bool
Store *TemplateStore
lock sync.Mutex
}
type Client struct {
conn *websocket.Conn
Data chan []byte
Close chan byte
}
func NewWebInfoServer(port int) *WebInfoServer {
server := &WebInfoServer{
Store: newTemplateStore(WebTemplateStorePath),
Port: port,
Info: OutInfo{Playlist: make([]MediaInfo, 0)},
Clients: map[*Client]int{},
}
mux := http.NewServeMux()
mux.Handle("/", http.FileServer(http.Dir(config.GetAssetPath("webinfo"))))
mux.HandleFunc("/ws/info", server.handleInfo)
mux.HandleFunc("/api/info", server.getInfo)
mux.HandleFunc("/api/template/list", server.tmplList)
mux.HandleFunc("/api/template/get", server.tmplGet)
mux.HandleFunc("/api/template/save", server.tmplSave)
server.ServerMux = mux
return server
}
func (s *WebInfoServer) tmplList(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
//w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
d, _ := json.Marshal(s.Store.List())
_, err := w.Write(d)
if err != nil {
lg.Warnf("/api/template/list error: %s", err)
return
}
}
func (s *WebInfoServer) tmplGet(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
name := r.URL.Query().Get("name")
if name == "" {
name = "default"
}
d, _ := json.Marshal(s.Store.Get(name))
_, err := w.Write(d)
if err != nil {
lg.Warnf("/api/template/get error: %s", err)
return
}
}
func (s *WebInfoServer) tmplSave(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Method", "*")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Expose-Headers", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
lg.Info(r.Method)
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if err := r.ParseMultipartForm(1 << 16); err != nil {
lg.Warnf("ParseForm() err: %v", err)
return
}
name := r.FormValue("name")
tmpl := r.FormValue("template")
if name == "" {
name = "default"
}
lg.Infof("change template %s", name)
s.Store.Modify(name, tmpl)
d, _ := json.Marshal(s.Store.Get(name))
_, err := w.Write(d)
if err != nil {
lg.Warnf("/api/template/save error: %s", err)
return
}
s.Store.Save(WebTemplateStorePath)
}
func (s *WebInfoServer) getInfo(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
//w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
d, _ := json.Marshal(s.Info)
_, err := w.Write(d)
if err != nil {
lg.Warnf("api get info error: %s", err)
return
}
}
func (s *WebInfoServer) handleInfo(w http.ResponseWriter, r *http.Request) {
lg.Debug("connection start")
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
lg.Warnf("upgrade error: %s", err)
return
}
client := &Client{
conn: conn,
Data: make(chan []byte, 16),
Close: make(chan byte, 1),
}
s.addClient(client)
defer s.removeClient(client)
go func() {
for {
_, _, err := client.conn.ReadMessage()
if err != nil {
client.Close <- 1
}
}
}()
for {
lg.Trace("waiting for message")
select {
case data := <-client.Data:
writer, err := client.conn.NextWriter(websocket.TextMessage)
if err != nil {
lg.Warn("get writer error", err)
return
}
if _, err = writer.Write(data); err != nil {
lg.Warn("send error:", err)
return
}
if err = writer.Close(); err != nil {
lg.Warnf("can't close writer: %s", err)
return
}
case _ = <-client.Close:
lg.Debug("client close")
if err := client.conn.Close(); err != nil {
lg.Warnf("close connection encouter an error: %s", err)
}
return
}
}
}
func (s *WebInfoServer) SendInfo(update string, info OutInfo) {
for client := range s.Clients {
d, _ := json.Marshal(WebsocketData{Update: update, Data: info})
client.Data <- d
}
}
func (s *WebInfoServer) addClient(c *Client) {
s.lock.Lock()
s.Clients[c] = 1
s.lock.Unlock()
}
func (s *WebInfoServer) removeClient(c *Client) {
s.lock.Lock()
close(c.Data)
delete(s.Clients, c)
s.lock.Unlock()
}
func (s *WebInfoServer) Start() {
lg.Debug("WebInfoServer starting...")
s.Running = true
go func() {
s.Server = &http.Server{
Addr: fmt.Sprintf("localhost:%d", s.Port),
Handler: s.ServerMux,
}
err := s.Server.ListenAndServe()
s.Running = false
if err == http.ErrServerClosed {
lg.Info("WebInfoServer closed")
return
}
if err != nil {
lg.Warnf("Failed to start webinfo server: %s", err)
return
}
}()
}
func (s *WebInfoServer) Stop() error {
lg.Debug("WebInfoServer stopping...")
s.lock.Lock()
s.Clients = map[*Client]int{}
s.lock.Unlock()
return s.Server.Shutdown(context.TODO())
}

View File

@@ -0,0 +1,76 @@
package webinfo
import (
"AynaLivePlayer/util"
"encoding/json"
"io/ioutil"
)
const WebTemplateStorePath = "./webtemplates.json"
type WebTemplate struct {
Name string
Template string
}
type TemplateStore struct {
Templates map[string]*WebTemplate
}
func newTemplateStore(filename string) *TemplateStore {
s := &TemplateStore{Templates: map[string]*WebTemplate{}}
var templates []*WebTemplate
file, err := ioutil.ReadFile(filename)
if err == nil {
_ = json.Unmarshal(file, &templates)
}
for _, tmpl := range templates {
s.Templates[tmpl.Name] = tmpl
}
return s
}
func (s *TemplateStore) Save(filename string) {
templates := make([]WebTemplate, 0)
for _, tmp := range s.Templates {
templates = append(templates, *tmp)
}
unescape, err := util.MarshalIndentUnescape(templates, "", " ")
if err != nil {
lg.Warnf("save web templates to %s failed: %s", filename, err)
return
}
if err := ioutil.WriteFile(filename, []byte(unescape), 0666); err != nil {
lg.Warnf("save web templates to %s failed: %s", filename, err)
return
}
}
func (s *TemplateStore) Get(name string) *WebTemplate {
if t, ok := s.Templates[name]; ok {
return t
}
t := &WebTemplate{Name: name, Template: "<p>Empty</p>"}
s.Templates[name] = t
return t
}
func (s *TemplateStore) Modify(name string, content string) {
if _, ok := s.Templates[name]; ok {
s.Templates[name].Template = content
return
}
}
func (s *TemplateStore) List() []string {
names := make([]string, 0)
for name, _ := range s.Templates {
names = append(names, name)
}
return names
}
func (s *TemplateStore) Delete(name string) {
delete(s.Templates, name)
}

View File

@@ -0,0 +1,22 @@
package webinfo
import (
"fmt"
"testing"
)
func TestTemplateStore_Create(t *testing.T) {
s := newTemplateStore(WebTemplateStorePath)
s.Get("A")
s.Get("B")
s.Modify("A", "33333")
s.Save(WebTemplateStorePath)
}
func TestTemplateStore_Load(t *testing.T) {
s := newTemplateStore(WebTemplateStorePath)
fmt.Println(s.List())
for name, tmpl := range s.Templates {
fmt.Println(name, tmpl.Template)
}
}

229
plugin/webinfo/webinfo.go Normal file
View File

@@ -0,0 +1,229 @@
package webinfo
import (
"AynaLivePlayer/config"
"AynaLivePlayer/controller"
"AynaLivePlayer/event"
"AynaLivePlayer/gui"
"AynaLivePlayer/i18n"
"AynaLivePlayer/logger"
"AynaLivePlayer/player"
"AynaLivePlayer/util"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/aynakeya/go-mpv"
)
const MODULE_PLGUIN_WEBINFO = "plugin.webinfo"
var lg = logger.Logger.WithField("Module", MODULE_PLGUIN_WEBINFO)
type WebInfo struct {
Port int
server *WebInfoServer
panel fyne.CanvasObject
}
func NewWebInfo() *WebInfo {
return &WebInfo{
Port: 4000,
}
}
func (w *WebInfo) Name() string {
return "WebInfo"
}
func (w *WebInfo) Title() string {
return i18n.T("plugin.webinfo.title")
}
func (w *WebInfo) Description() string {
return i18n.T("plugin.webinfo.description")
}
func (w *WebInfo) Enable() error {
config.LoadConfig(w)
w.server = NewWebInfoServer(w.Port)
lg.Info("starting web backend server")
w.server.Start()
w.registerHandlers()
gui.AddConfigLayout(w)
lg.Info("webinfo loaded")
return nil
}
func (w *WebInfo) Disable() error {
lg.Info("closing webinfo backend server")
if err := w.server.Stop(); err != nil {
lg.Warnf("stop webinfo server encouter an error: %s", err)
}
return nil
}
func (t *WebInfo) registerHandlers() {
controller.MainPlayer.EventHandler.RegisterA(player.EventPlay, "plugin.webinfo.current", func(event *event.Event) {
t.server.Info.Current = MediaInfo{
Index: 0,
Title: event.Data.(player.PlayEvent).Media.Title,
Artist: event.Data.(player.PlayEvent).Media.Artist,
Album: event.Data.(player.PlayEvent).Media.Album,
Cover: event.Data.(player.PlayEvent).Media.Cover,
Username: event.Data.(player.PlayEvent).Media.ToUser().Name,
}
t.server.SendInfo(
OutInfoC,
OutInfo{Current: t.server.Info.Current},
)
})
if controller.MainPlayer.ObserveProperty("time-pos", func(property *mpv.EventProperty) {
if property.Data == nil {
t.server.Info.CurrentTime = 0
return
}
ct := int(property.Data.(mpv.Node).Value.(float64))
if ct == t.server.Info.CurrentTime {
return
}
t.server.Info.CurrentTime = ct
t.server.SendInfo(
OutInfoCT,
OutInfo{CurrentTime: t.server.Info.CurrentTime},
)
}) != nil {
lg.Error("register time-pos handler failed")
}
if controller.MainPlayer.ObserveProperty("duration", func(property *mpv.EventProperty) {
if property.Data == nil {
t.server.Info.TotalTime = 0
return
}
t.server.Info.TotalTime = int(property.Data.(mpv.Node).Value.(float64))
t.server.SendInfo(
OutInfoTT,
OutInfo{TotalTime: t.server.Info.TotalTime},
)
}) != nil {
lg.Error("fail to register handler for total time with property duration")
}
controller.UserPlaylist.Handler.RegisterA(player.EventPlaylistUpdate, "plugin.webinfo.playlist", func(event *event.Event) {
pl := make([]MediaInfo, 0)
e := event.Data.(player.PlaylistUpdateEvent)
e.Playlist.Lock.RLock()
for index, m := range e.Playlist.Playlist {
pl = append(pl, MediaInfo{
Index: index,
Title: m.Title,
Artist: m.Artist,
Album: m.Album,
Username: m.ToUser().Name,
})
}
e.Playlist.Lock.RUnlock()
t.server.Info.Playlist = pl
t.server.SendInfo(
OutInfoPL,
OutInfo{Playlist: t.server.Info.Playlist},
)
})
controller.CurrentLyric.Handler.RegisterA(player.EventLyricUpdate, "plugin.webinfo.lyric", func(event *event.Event) {
lrcLine := event.Data.(player.LyricUpdateEvent).Lyric
t.server.Info.Lyric = lrcLine.Lyric
t.server.SendInfo(
OutInfoL,
OutInfo{Lyric: t.server.Info.Lyric},
)
})
}
func (w *WebInfo) getServerStatusText() string {
if w.server.Running {
return i18n.T("plugin.webinfo.server_status.running")
} else {
return i18n.T("plugin.webinfo.server_status.stopped")
}
}
func (w *WebInfo) getServerUrl() string {
return fmt.Sprintf("http://localhost:%d/#/previewV2", w.Port)
}
func (w *WebInfo) CreatePanel() fyne.CanvasObject {
if w.panel != nil {
return w.panel
}
statusText := widget.NewLabel("")
serverStatus := container.NewHBox(
widget.NewLabel(i18n.T("plugin.webinfo.server_status")),
statusText,
)
statusText.SetText(w.getServerStatusText())
serverPort := container.NewBorder(nil, nil,
widget.NewLabel(i18n.T("plugin.webinfo.port")), nil,
widget.NewEntryWithData(binding.IntToString(binding.BindInt(&w.Port))),
)
serverUrl := widget.NewHyperlink(w.getServerUrl(), util.UrlMustParse(w.getServerUrl()))
serverPreview := container.NewHBox(
widget.NewLabel(i18n.T("plugin.webinfo.server_preview")),
serverUrl,
)
stopBtn := gui.NewAsyncButtonWithIcon(
i18n.T("plugin.webinfo.server_control.stop"),
theme.MediaStopIcon(),
func() {
if !w.server.Running {
return
}
lg.Info("User try stop webinfo server")
err := w.server.Stop()
if err != nil {
lg.Warnf("stop server have error: %s", err)
return
}
statusText.SetText(w.getServerStatusText())
},
)
startBtn := gui.NewAsyncButtonWithIcon(
i18n.T("plugin.webinfo.server_control.start"),
theme.MediaPlayIcon(),
func() {
if w.server.Running {
return
}
lg.Infof("User try start webinfo server with port %d", w.Port)
w.server.Port = w.Port
w.server.Start()
statusText.SetText(w.getServerStatusText())
serverUrl.SetText(w.getServerUrl())
_ = serverUrl.SetURLFromString(w.getServerUrl())
},
)
restartBtn := gui.NewAsyncButtonWithIcon(
i18n.T("plugin.webinfo.server_control.restart"),
theme.MediaReplayIcon(),
func() {
lg.Infof("User try restart webinfo server with port %d", w.Port)
if w.server.Running {
if err := w.server.Stop(); err != nil {
lg.Warnf("stop server have error: %s", err)
return
}
}
w.server.Port = w.Port
w.server.Start()
statusText.SetText(w.getServerStatusText())
serverUrl.SetText(w.getServerUrl())
_ = serverUrl.SetURLFromString(w.getServerUrl())
},
)
ctrlBtns := container.NewHBox(
widget.NewLabel(i18n.T("plugin.webinfo.server_control")),
startBtn, stopBtn, restartBtn,
)
w.panel = container.NewVBox(serverStatus, serverPreview, serverPort, ctrlBtns)
return w.panel
}

164
plugin/wylogin/wylogin.go Normal file
View File

@@ -0,0 +1,164 @@
package wylogin
import (
"AynaLivePlayer/config"
"AynaLivePlayer/gui"
"AynaLivePlayer/i18n"
"AynaLivePlayer/logger"
"AynaLivePlayer/provider"
"bytes"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
qrcode "github.com/skip2/go-qrcode"
"net/http"
)
const MODULE_PLGUIN_NETEASELOGIN = "plugin.neteaselogin"
var lg = logger.Logger.WithField("Module", MODULE_PLGUIN_NETEASELOGIN)
type WYLogin struct {
MusicU string
CSRF string
panel fyne.CanvasObject
}
func NewWYLogin() *WYLogin {
return &WYLogin{
MusicU: "MUSIC_U=;",
CSRF: "__csrf=;",
}
}
func (w *WYLogin) Name() string {
return "NeteaseLogin"
}
func (w *WYLogin) Enable() error {
config.LoadConfig(w)
w.loadCookie()
gui.AddConfigLayout(w)
go func() {
lg.Info("updating netease status")
provider.NeteaseAPI.UpdateStatus()
lg.Info("finish updating netease status")
}()
return nil
}
func (w *WYLogin) Disable() error {
w.saveCookie()
return nil
}
func (w *WYLogin) loadCookie() {
provider.NeteaseAPI.ReqData.Cookies = (&http.Response{
Header: map[string][]string{
"Set-Cookie": []string{w.MusicU, w.CSRF},
},
}).Cookies()
}
func (w *WYLogin) saveCookie() {
for _, c := range provider.NeteaseAPI.ReqData.Cookies {
if c.Name == "MUSIC_U" {
w.MusicU = c.String()
}
if c.Name == "__csrf" {
w.CSRF = c.String()
}
}
}
func (w *WYLogin) Title() string {
return i18n.T("plugin.neteaselogin.title")
}
func (w *WYLogin) Description() string {
return i18n.T("plugin.neteaselogin.description")
}
func (w *WYLogin) CreatePanel() fyne.CanvasObject {
if w.panel != nil {
return w.panel
}
currentUser := widget.NewLabel(i18n.T("plugin.neteaselogin.current_user.notlogin"))
currentStatus := container.NewHBox(
widget.NewLabel(i18n.T("plugin.neteaselogin.current_user")),
currentUser)
refreshBtn := gui.NewAsyncButton(
i18n.T("plugin.neteaselogin.refresh"),
func() {
provider.NeteaseAPI.UpdateStatus()
if provider.NeteaseAPI.IsLogin() {
currentUser.SetText(provider.NeteaseAPI.Nickname())
} else {
currentUser.SetText(i18n.T("plugin.neteaselogin.current_user.notlogin"))
}
},
)
logoutBtn := gui.NewAsyncButton(
i18n.T("plugin.neteaselogin.logout"),
func() {
provider.NeteaseAPI.Logout()
currentUser.SetText(i18n.T("plugin.neteaselogin.current_user.notlogin"))
},
)
controlBtns := container.NewHBox(refreshBtn, logoutBtn)
qrcodeImg := canvas.NewImageFromResource(gui.ResEmptyImage)
qrcodeImg.SetMinSize(fyne.NewSize(200, 200))
qrcodeImg.FillMode = canvas.ImageFillContain
var key string
qrStatus := widget.NewLabel("AAAAAAAA")
qrStatus.SetText("")
newQrBtn := gui.NewAsyncButton(
i18n.T("plugin.neteaselogin.qr.new"),
func() {
qrStatus.SetText("")
lg.Info("getting a new qr code for login")
key = provider.NeteaseAPI.GetQrLoginKey()
if key == "" {
lg.Warn("fail to get qr code key")
return
}
lg.Debugf("trying encode url %s to qrcode", provider.NeteaseAPI.GetQrLoginUrl(key))
data, err := qrcode.Encode(provider.NeteaseAPI.GetQrLoginUrl(key), qrcode.Medium, 256)
if err != nil {
lg.Warnf("generate qr code failed: %s", err)
return
}
lg.Debug("create img from raw data")
pic := canvas.NewImageFromReader(bytes.NewReader(data), "qrcode")
qrcodeImg.Resource = pic.Resource
qrcodeImg.Refresh()
},
)
finishQrBtn := gui.NewAsyncButton(
i18n.T("plugin.neteaselogin.qr.finish"),
func() {
if key == "" {
return
}
lg.Info("checking qr status")
ok, msg := provider.NeteaseAPI.CheckQrLogin(key)
qrStatus.SetText(msg)
if ok {
key = ""
qrcodeImg.Resource = gui.ResEmptyImage
qrcodeImg.Refresh()
}
},
)
loginPanel := container.NewCenter(
container.NewVBox(
qrcodeImg,
container.NewHBox(newQrBtn, finishQrBtn, qrStatus),
),
)
w.panel = container.NewVBox(controlBtns, currentStatus, loginPanel)
return w.panel
}

View File

@@ -5,12 +5,15 @@ import (
"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() *Bilibili {
@@ -18,6 +21,8 @@ func _newBilibili() *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]+"),
}
}
@@ -32,6 +37,26 @@ func (b *Bilibili) GetName() string {
return "bilibili"
}
func (b *Bilibili) MatchMedia(keyword string) *player.Media {
if id := b.IdRegex0.FindString(keyword); id != "" {
return &player.Media{
Meta: Meta{
Name: b.GetName(),
Id: id,
},
}
}
if id := b.IdRegex1.FindString(keyword); id != "" {
return &player.Media{
Meta: Meta{
Name: b.GetName(),
Id: id[2:],
},
}
}
return nil
}
func (b *Bilibili) FormatPlaylistUrl(uri string) string {
return ""
}
@@ -51,7 +76,7 @@ func (b *Bilibili) Search(keyword string) ([]*player.Media, error) {
gjson.Get(resp, "data.result").ForEach(func(key, value gjson.Result) bool {
result = append(result, &player.Media{
Title: value.Get("title").String(),
Cover: value.Get("cover").String(),
Cover: player.Picture{Url: value.Get("cover").String()},
Artist: value.Get("author").String(),
Meta: Meta{
Name: b.GetName(),
@@ -74,7 +99,7 @@ func (b *Bilibili) UpdateMedia(media *player.Media) error {
return ErrorExternalApi
}
media.Title = gjson.Get(resp, "data.title").String()
media.Cover = gjson.Get(resp, "data.cover").String()
media.Cover.Url = gjson.Get(resp, "data.cover").String()
media.Artist = gjson.Get(resp, "data.author").String()
media.Album = media.Title
return nil
@@ -91,7 +116,6 @@ func (b *Bilibili) UpdateMediaUrl(media *player.Media) error {
media.Header = map[string]string{
"user-agent": "BiliMusic/2.233.3",
}
fmt.Println(fmt.Sprintf(b.InfoApi, media.Meta.(Meta).Id))
uri := gjson.Get(resp, "data.cdns.0").String()
if uri == "" {
return ErrorExternalApi

156
provider/bilivideo.go Normal file
View File

@@ -0,0 +1,156 @@
package provider
import (
"AynaLivePlayer/player"
"AynaLivePlayer/util"
"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() *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",
},
}
}
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.StringToInt(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) *player.Media {
if id := b.IdRegex.FindString(keyword); id != "" {
return &player.Media{
Meta: Meta{
Name: b.GetName(),
Id: id,
},
}
}
return nil
}
func (b *BilibiliVideo) GetPlaylist(playlist Meta) ([]*player.Media, error) {
return nil, ErrorExternalApi
}
func (b *BilibiliVideo) FormatPlaylistUrl(uri string) string {
return ""
}
func (b *BilibiliVideo) Search(keyword string) ([]*player.Media, error) {
resp := httpGetString(fmt.Sprintf(b.SearchApi, url.QueryEscape(keyword)), nil)
if resp == "" {
return nil, ErrorExternalApi
}
jresp := gjson.Parse(resp)
if jresp.Get("code").String() != "0" {
return nil, ErrorExternalApi
}
result := make([]*player.Media, 0)
r := regexp.MustCompile("</?em[^>]*>")
jresp.Get("data.result").ForEach(func(key, value gjson.Result) bool {
result = append(result, &player.Media{
Title: r.ReplaceAllString(value.Get("title").String(), ""),
Cover: player.Picture{Url: "https:" + value.Get("pic").String()},
Artist: value.Get("author").String(),
Meta: Meta{
Name: b.GetName(),
Id: value.Get("bvid").String(),
},
})
return true
})
return result, nil
}
func (b *BilibiliVideo) UpdateMedia(media *player.Media) error {
resp := httpGetString(fmt.Sprintf(b.InfoApi, b.getBv(media.Meta.(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 *player.Media) error {
resp := httpGetString(fmt.Sprintf(b.InfoApi, b.getBv(media.Meta.(Meta).Id)), nil)
if resp == "" {
return ErrorExternalApi
}
jresp := gjson.Parse(resp)
page := b.getPage(media.Meta.(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.(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.(Meta).Id))
media.Header = b.header
return nil
}
func (b *BilibiliVideo) UpdateMediaLyric(media *player.Media) error {
return nil
}

View File

@@ -0,0 +1,99 @@
package provider
import (
"AynaLivePlayer/player"
"fmt"
"regexp"
"testing"
)
func TestBV_GetMusicMeta(t *testing.T) {
var api MediaProvider = BilibiliVideoAPI
media := player.Media{
Meta: 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 MediaProvider = BilibiliVideoAPI
media := player.Media{
Meta: 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 MediaProvider = BilibiliVideoAPI
media := player.Media{
Meta: 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 MediaProvider = BilibiliVideoAPI
media := player.Media{
Meta: 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 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

@@ -19,6 +19,8 @@ type Kuwo struct {
PlaylistApi string
PlaylistRegex0 *regexp.Regexp
PlaylistRegex1 *regexp.Regexp
IdRegex0 *regexp.Regexp
IdRegex1 *regexp.Regexp
}
func _newKuwo() *Kuwo {
@@ -32,6 +34,8 @@ func _newKuwo() *Kuwo {
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]+"),
}
}
@@ -46,6 +50,26 @@ func (k *Kuwo) GetName() string {
return "kuwo"
}
func (k *Kuwo) MatchMedia(keyword string) *player.Media {
if id := k.IdRegex0.FindString(keyword); id != "" {
return &player.Media{
Meta: Meta{
Name: k.GetName(),
Id: id,
},
}
}
if id := k.IdRegex1.FindString(keyword); id != "" {
return &player.Media{
Meta: Meta{
Name: k.GetName(),
Id: id[2:],
},
}
}
return nil
}
func (k *Kuwo) FormatPlaylistUrl(uri string) string {
var id string
id = k.PlaylistRegex0.FindString(uri)
@@ -92,7 +116,7 @@ func (k *Kuwo) Search(keyword string) ([]*player.Media, error) {
gjson.Parse(resp).Get("data.list").ForEach(func(key, value gjson.Result) bool {
result = append(result, &player.Media{
Title: html.UnescapeString(value.Get("name").String()),
Cover: value.Get("pic").String(),
Cover: player.Picture{Url: value.Get("pic").String()},
Artist: value.Get("artist").String(),
Album: value.Get("album").String(),
Meta: Meta{
@@ -115,7 +139,7 @@ func (k *Kuwo) UpdateMedia(media *player.Media) error {
return ErrorExternalApi
}
media.Title = html.UnescapeString(jresp.Get("data.name").String())
media.Cover = jresp.Get("data.pic").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
@@ -169,7 +193,7 @@ func (k *Kuwo) GetPlaylist(meta Meta) ([]*player.Media, error) {
&player.Media{
Title: html.UnescapeString(value.Get("name").String()),
Artist: value.Get("artist").String(),
Cover: value.Get("pic").String(),
Cover: player.Picture{Url: value.Get("pic").String()},
Album: value.Get("album").String(),
Meta: Meta{
Name: k.GetName(),

View File

@@ -1,45 +1,127 @@
package provider
import "AynaLivePlayer/player"
import (
"AynaLivePlayer/config"
"AynaLivePlayer/player"
"os"
"sort"
"strings"
)
type _LocalPlaylist struct {
Name string
Medias []*player.Media
}
type Local struct {
Playlists []_LocalPlaylist
}
var LocalAPI *Local
func init() {
LocalAPI = _newLocal()
//Providers[LocalAPI.GetName()] = LocalAPI
Providers[LocalAPI.GetName()] = LocalAPI
}
func _newLocal() *Local {
return &Local{}
l := &Local{Playlists: make([]_LocalPlaylist, 0)}
if err := os.MkdirAll(config.Provider.LocalDir, 0755); err != nil {
return l
}
for _, n := range getPlaylistNames() {
l.Playlists = append(l.Playlists, _LocalPlaylist{Name: n})
}
for i, _ := range l.Playlists {
_ = readLocalPlaylist(&l.Playlists[i])
}
return l
}
func (l *Local) GetName() string {
return "local"
}
func (l *Local) FormatPlaylistUrl(uri string) string {
return ""
func (l *Local) MatchMedia(keyword string) *player.Media {
return nil
}
func (l *Local) GetPlaylist(playlist string) ([]*player.Media, error) {
//TODO implement me
panic("implement me")
func (l *Local) UpdateMediaLyric(media *player.Media) error {
// already update in UpdateMedia, do nothing
return nil
}
func (l *Local) FormatPlaylistUrl(uri string) string {
return uri
}
func (l *Local) GetPlaylist(playlist Meta) ([]*player.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(pl) != nil {
return nil, ErrorExternalApi
}
return pl.Medias, nil
}
func (l *Local) Search(keyword string) ([]*player.Media, error) {
//TODO implement me
panic("implement me")
result := make([]struct {
M *player.Media
N int
}, 0)
keywords := strings.Split(keyword, " ")
for _, p := range l.Playlists {
for _, m := range p.Medias {
n := 0
for _, k := range keywords {
if strings.Contains(m.Title, k) || strings.Contains(m.Artist, k) {
n++
}
if k == m.Title {
n += 2
}
}
if n > 0 {
result = append(result, struct {
M *player.Media
N int
}{M: m, N: n})
}
}
}
sort.Slice(result, func(i, j int) bool {
return result[i].N > result[j].N
})
medias := make([]*player.Media, len(result))
for i, r := range result {
medias[i] = r.M.Copy()
}
return medias, nil
}
func (l *Local) UpdateMedia(media *player.Media) error {
//TODO implement me
panic("implement me")
mediaPath := media.Meta.(Meta).Id
_, err := os.Stat(mediaPath)
if err != nil {
return err
}
return readMediaFile(media)
}
func (l *Local) UpdateMediaUrl(media *player.Media) error {
//TODO implement me
panic("implement me")
mediaPath := media.Meta.(Meta).Id
_, err := os.Stat(mediaPath)
if err != nil {
return err
}
media.Url = mediaPath
return nil
}

73
provider/local_helper.go Normal file
View File

@@ -0,0 +1,73 @@
package provider
import (
"AynaLivePlayer/config"
"AynaLivePlayer/player"
"AynaLivePlayer/util"
"github.com/dhowden/tag"
"io/ioutil"
"os"
"path/filepath"
)
func getPlaylistNames() []string {
names := make([]string, 0)
items, _ := ioutil.ReadDir(config.Provider.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(playlist *_LocalPlaylist) error {
p1th := playlist.Name
playlist.Medias = make([]*player.Media, 0)
fullPath := filepath.Join(config.Provider.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 := player.Media{
Meta: 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 *player.Media) error {
p := media.Meta.(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
}

25
provider/local_test.go Normal file
View File

@@ -0,0 +1,25 @@
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

@@ -4,7 +4,7 @@ import (
"AynaLivePlayer/player"
"AynaLivePlayer/util"
neteaseApi "github.com/XiaoMengXinX/Music163Api-Go/api"
"github.com/XiaoMengXinX/Music163Api-Go/types"
neteaseTypes "github.com/XiaoMengXinX/Music163Api-Go/types"
neteaseUtil "github.com/XiaoMengXinX/Music163Api-Go/utils"
"regexp"
"strconv"
@@ -15,6 +15,9 @@ type Netease struct {
PlaylistRegex0 *regexp.Regexp
PlaylistRegex1 *regexp.Regexp
ReqData neteaseUtil.RequestData
IdRegex0 *regexp.Regexp
IdRegex1 *regexp.Regexp
loginStatus neteaseTypes.LoginStatusData
}
func _newNetease() *Netease {
@@ -30,6 +33,8 @@ func _newNetease() *Netease {
},
},
},
IdRegex0: regexp.MustCompile("^[0-9]+"),
IdRegex1: regexp.MustCompile("^wy[0-9]+"),
}
}
@@ -40,7 +45,9 @@ func init() {
Providers[NeteaseAPI.GetName()] = NeteaseAPI
}
func _neteaseGetArtistNames(data types.SongDetailData) string {
// Netease private helper method
func _neteaseGetArtistNames(data neteaseTypes.SongDetailData) string {
artists := make([]string, 0)
for _, a := range data.Ar {
artists = append(artists, a.Name)
@@ -48,10 +55,32 @@ func _neteaseGetArtistNames(data types.SongDetailData) string {
return strings.Join(artists, ",")
}
// MediaProvider implementation
func (n *Netease) GetName() string {
return "netease"
}
func (n *Netease) MatchMedia(keyword string) *player.Media {
if id := n.IdRegex0.FindString(keyword); id != "" {
return &player.Media{
Meta: Meta{
Name: n.GetName(),
Id: id,
},
}
}
if id := n.IdRegex1.FindString(keyword); id != "" {
return &player.Media{
Meta: Meta{
Name: n.GetName(),
Id: id[2:],
},
}
}
return nil
}
func (n *Netease) FormatPlaylistUrl(uri string) string {
var id string
id = n.PlaylistRegex0.FindString(uri)
@@ -96,7 +125,7 @@ func (n *Netease) GetPlaylist(meta Meta) ([]*player.Media, error) {
medias = append(medias, &player.Media{
Title: result2.Songs[i].Name,
Artist: _neteaseGetArtistNames(result2.Songs[i]),
Cover: result2.Songs[i].Al.PicUrl,
Cover: player.Picture{Url: result2.Songs[i].Al.PicUrl},
Album: result2.Songs[i].Al.Name,
Url: "",
Header: nil,
@@ -134,7 +163,7 @@ func (n *Netease) Search(keyword string) ([]*player.Media, error) {
medias = append(medias, &player.Media{
Title: song.Name,
Artist: strings.Join(artists, ","),
Cover: "",
Cover: player.Picture{},
Album: song.Album.Name,
Url: "",
Header: nil,
@@ -158,7 +187,7 @@ func (n *Netease) UpdateMedia(media *player.Media) error {
return ErrorExternalApi
}
media.Title = result.Songs[0].Name
media.Cover = result.Songs[0].Al.PicUrl
media.Cover.Url = result.Songs[0].Al.PicUrl
media.Album = result.Songs[0].Al.Name
media.Artist = _neteaseGetArtistNames(result.Songs[0])
return nil

62
provider/netease_extra.go Normal file
View File

@@ -0,0 +1,62 @@
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

@@ -19,6 +19,7 @@ type Meta struct {
type MediaProvider interface {
GetName() string
MatchMedia(keyword string) *player.Media
GetPlaylist(playlist Meta) ([]*player.Media, error)
FormatPlaylistUrl(uri string) string
Search(keyword string) ([]*player.Media, error)
@@ -43,6 +44,13 @@ func FormatPlaylistUrl(pname, uri string) (string, error) {
return "", ErrorNoSuchProvider
}
func MatchMedia(provider string, keyword string) *player.Media {
if v, ok := Providers[provider]; ok {
return v.MatchMedia(keyword)
}
return nil
}
func Search(provider string, keyword string) ([]*player.Media, error) {
if v, ok := Providers[provider]; ok {
return v.Search(keyword)

20
resource/resource.go Normal file
View File

@@ -0,0 +1,20 @@
package resource
import (
"AynaLivePlayer/config"
"io/ioutil"
)
var ProgramIcon = []byte{}
var EmptyImage = []byte{}
func init() {
loadResource(config.GetAssetPath("icon.jpg"), &ProgramIcon)
loadResource(config.GetAssetPath("empty.png"), &EmptyImage)
}
func loadResource(path string, res *[]byte) {
if file, err := ioutil.ReadFile(path); err == nil {
*res = file
}
}

View File

@@ -5,20 +5,29 @@
- @5 delete optimization
- 文本输出
- web输出
- 黑名单
- 进入beta版本
- web输出 (前端)
beta
- web 重连
- 黑名单
- bilibili 歌词来源
----
Finished
- 2022.7.01: 历史记录
- 2022.6.29: 跳过闲置歌单
- 2022.6.26: i18n
- 2022.6.25: kuwo歌单
- 2022.6.25: 设置界面
- 2022.6.25: @6 bug, race condition, playlist size changed during playlist update.
- 2022.6.23: 用户歌单操作
- 2022.7.18@0.9.0: Fix bug/网页第二版跟新,加入自定义模板/修复图片加载不出来导致的闪退bug
- 2022.7.18 : Fix bug
- 2022.7.16@0.8.6: 网页输出第一版更新/修复历史列表部分歌曲放不出来的bug/修复部分歌词不更新
- 2022.7.15: 更新stderr重定向/添加logo/
- 2022.7.13@0.8.4: 网易云登录
- 2022.7.10: Local Provider
- 2022.7.03: 多来源点歌
- 2022.7.01: 文本输出
- 2022.7.01: 历史记录
- 2022.6.29: 跳过闲置歌单
- 2022.6.26: i18n
- 2022.6.25: kuwo歌单
- 2022.6.25: 设置界面
- 2022.6.25: @6 bug, race condition, playlist size changed during playlist update.
- 2022.6.23: 用户歌单操作

View File

@@ -38,3 +38,16 @@ func StringToInt(s string) int {
i, _ := strconv.Atoi(s)
return i
}
func StringSliceCopy(src []string) []string {
x := make([]string, len(src))
copy(x, src)
return x
}
func GetOrDefault(s string, def string) string {
if s == "" {
return def
}
return s
}

8
util/url.go Normal file
View File

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

14
webtemplates.json Normal file
View File

@@ -0,0 +1,14 @@
[
{
"Name": "时间例子",
"Template": "<p><media-title /></p>\r\n\r\n<p>秒显示:<current-time></current-time>\r\n/<total-time></total-time></p>\r\n\r\n<p>\r\n分秒显示: \r\n<current-time format=\"m:s\"/></current-time>\r\n/\r\n<total-time format=\"m:s\"/></total-time>\r\n</p>\r\n"
},
{
"Name": "播放列表例子",
"Template": "<p>默认</p>\r\n<playlist-container></playlist-container>\r\n<p>自定义格式</p>\r\n<playlist-container>\r\n <template v-slot=\"v\">\r\n <p>序号: <playlist-index :index=\"v.index\"></playlist-index></p>\r\n <p>\r\n 歌名: <playlist-title :index=\"v.index\"></playlist-title>\r\n </p>\r\n <p>\r\n 歌手: <playlist-artist :index=\"v.index\"></playlist-artist>\r\n </p>\r\n <p>\r\n 专辑名: <playlist-album :index=\"v.index\"></playlist-album>\r\n </p>\r\n <p>\r\n 点歌用户: <playlist-username :index=\"v.index\"></playlist-username>\r\n </p>\r\n </template>\r\n </playlist-container>"
},
{
"Name": "default",
"Template": "\r\n <div class=\"current-playing\">\r\n <media-title></media-title>\r\n ---\r\n <media-artist></media-artist>\r\n ---\r\n <media-username></media-username>\r\n </div>\r\n"
}
]

209
前端要求.md Normal file
View File

@@ -0,0 +1,209 @@
# IDK what to write
后端已经写好了!!!
只需要负责前端的部分就行了。
开源地址: [https://github.com/aynakeya/AynaLivePlayer](https://github.com/aynakeya/AynaLivePlayer)
# 要求
- 开源项目.jpg
- 给OBS用的用来显示信息
- 访问的时候先获取全部的数据渲染然后用websocket获取更新的数据
- 能够单个输出所有的数据 @1
- 能自定义输出的模板(?暂定) @2
- 最好用vue
### @1 单个输出所有的数据
类似[https://github.com/aynakeya/BiliAudioBot](https://github.com/aynakeya/BiliAudioBot)里的frontend(用vue写的)可以单个输出所有的数据。release有打包好发布的测试用例。
- 适合简单的输出
- 得有一个css的class这样子用户可以自己在obs自定义css虽然大部分用户不知道怎么搞
- 如果个可以的话最好能有个页面能可视化的修改css
比如GET `/info/CurrentTitle` 或者 `info?target=Current.Title` 返回
```
<p class=".current-title">外婆桥</p>
```
比如 GET `info?target=Current.Cover` 返回
```
<img class=".current-title" src="..." />
```
比如 GET `info?target=Playlist` 返回
```
<ol>
<li v-for="item in playlist">
<h2 class="playlist-info">#{{ playlist.indexOf(item) }} - {{ item.title }} - {{ item.artist }} - {{ item.username }}</h2>
</li>
</ol>
```
### @2 自定义输出的模板
- **第一目标** 不整了/没想好怎么整
- **第二目标** 最简单的还是和TextInfo一样用户给一个模板(通过get?会不会太长了,或者本地读取?这样的话我后端获取还得加一个api来返回模板) 然后渲染出对应的html的代码当然也要有css这样子用户可以自定义
```
Title: {{ .Current.Title }}
Artist: {{ .Current.Artist }}
Album: {{ .Current.Album}}
Username: {{ .Current.Username }}
Progress(in seconds): {{.CurrentTime}} / {{.TotalTime}}
Progress(in minutes:seconds): {{ GetMinutes .CurrentTime}}:{{ GetSeconds .CurrentTime}} / {{ GetMinutes .TotalTime}}:{{ GetSeconds .TotalTime}}
Lyric: {{.Lyric}}
{{range .Playlist}}
Index: # {{ .Index}}
Title: {{ .Title }}
Artist: {{ .Artist }}
Album: {{ .Album}}
Username: {{ .Username }}
{{end}}
```
- **终极目标**不知道能不能想做的像[https://github.com/xfgryujk/blivechat](https://github.com/xfgryujk/blivechat) / [https://link.bilibili.com/ctool/vtuber/index.html](https://link.bilibili.com/ctool/vtuber/index.html), 但是感觉挺麻烦的
# 后端接口
前端的页面在/地址下
两个接口
```
mux.Handle("/", http.FileServer(http.Dir(config.GetAssetPath("webinfo")))) # ./assets/webinfo
mux.HandleFunc("/ws/info", server.handleInfo)
mux.HandleFunc("/api/info", server.getInfo)
```
## api详情
### GET /api/info
**http://127.0.0.1:4000/api/info**
返回一个`OutInfo`, 当前的所有数据
```
{
"Current": {
"Index": 0,
"Title": "外婆桥",
"Artist": "任然",
"Album": "外婆桥",
"Username": "System",
"Cover": {
"Url": "https://p1.music.126.net/Ep-CjAsRL5yvZkDreiWsMQ==/109951164390004861.jpg",
"Data": null
}
},
"CurrentTime": 6,
"TotalTime": 261,
"Lyric": " 编曲 : 闫津",
"Playlist": [
{
"Index": 0,
"Title": "Melody",
"Artist": "ZIV,KIPES",
"Album": "倒叙爱情",
"Username": "System",
"Cover": {
"Url": "",
"Data": null
}
},
{
"Index": 1,
"Title": "Cure For Me",
"Artist": "AURORA",
"Album": "Cure For Me",
"Username": "System",
"Cover": {
"Url": "",
"Data": null
}
},
{
"Index": 2,
"Title": "填满",
"Artist": "苏星婕",
"Album": "填满",
"Username": "System",
"Cover": {
"Url": "",
"Data": null
}
}
]
}
```
### websocket /ws/info
**ws://127.0.0.1:4000/ws/info**
返回一个`WebsocketData`, 更新的数据
```
{
"Update": "Lyric",
"Data": {
"Current": {
"Index": 0,
"Title": "",
"Artist": "",
"Album": "",
"Username": "",
"Cover": {
"Url": "",
"Data": null
}
},
"CurrentTime": 0,
"TotalTime": 0,
"Lyric": " 混音 : KIPES",
"Playlist": null
}
}
```
## Structures
```
# Url == "" && Data == nil 或者Url != "" || Data != nil
type Picture struct {
Url string # 如果是url就会给url
Data []byte # 如果是二进制数据就是base64
}
type MediaInfo struct {
Index int
Title string
Artist string
Album string
Username string
Cover player.Picture
}
type OutInfo struct {
Current MediaInfo
CurrentTime int
TotalTime int
Lyric string
Playlist []MediaInfo
}
type WebsocketData struct {
Update string
Data OutInfo
}
```