mirror of
https://github.com/AynaLivePlayer/AynaLivePlayer.git
synced 2026-03-15 14:03:17 +08:00
add web output backend
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"AynaLivePlayer/plugin/diange"
|
||||
"AynaLivePlayer/plugin/qiege"
|
||||
"AynaLivePlayer/plugin/textinfo"
|
||||
"AynaLivePlayer/plugin/webinfo"
|
||||
"fmt"
|
||||
"github.com/mitchellh/panicwrap"
|
||||
"os"
|
||||
@@ -24,11 +25,12 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
var plugins = []controller.Plugin{diange.NewDiange(), qiege.NewQiege(), textinfo.NewTextInfo(), webinfo.NewWebInfo()}
|
||||
|
||||
func main() {
|
||||
fmt.Printf("BiliAudioBot Revive %s\n", config.VERSION)
|
||||
//logger.Logger.SetLevel(logrus.DebugLevel)
|
||||
controller.Initialize()
|
||||
controller.LoadPlugins(diange.NewDiange(), qiege.NewQiege(), textinfo.NewTextInfo())
|
||||
controller.LoadPlugins(plugins...)
|
||||
defer func() {
|
||||
controller.Destroy()
|
||||
config.SaveToConfigFile(config.CONFIG_PATH)
|
||||
@@ -36,4 +38,5 @@ func main() {
|
||||
}()
|
||||
gui.Initialize()
|
||||
gui.MainWindow.ShowAndRun()
|
||||
controller.ClosePlugins(plugins...)
|
||||
}
|
||||
|
||||
124
app/webs/main.go
Normal file
124
app/webs/main.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
type WebInfo struct {
|
||||
A string
|
||||
B string
|
||||
}
|
||||
|
||||
type WebInfoServer struct {
|
||||
ServeMux http.ServeMux
|
||||
Clients map[*Client]int
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
conn *websocket.Conn
|
||||
Data chan []byte
|
||||
Close chan byte
|
||||
}
|
||||
|
||||
func NewWebInfoServer() *WebInfoServer {
|
||||
server := &WebInfoServer{
|
||||
Clients: map[*Client]int{},
|
||||
}
|
||||
server.ServeMux.Handle("/", http.FileServer(http.Dir("./assets/webinfo")))
|
||||
server.ServeMux.HandleFunc("/ws/info", server.handleInfo)
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *WebInfoServer) handleInfo(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("connection start")
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
fmt.Println("upgrade error", 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 {
|
||||
fmt.Println("waiting for message")
|
||||
select {
|
||||
case data := <-client.Data:
|
||||
writer, err := client.conn.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
fmt.Println("get writer error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = writer.Write(data); err != nil {
|
||||
fmt.Println("send error:", err)
|
||||
return
|
||||
}
|
||||
if err = writer.Close(); err != nil {
|
||||
fmt.Println("can't close writer")
|
||||
return
|
||||
}
|
||||
case _ = <-client.Close:
|
||||
fmt.Println("client close", client.conn.Close())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebInfoServer) sendInfo(info *WebInfo) {
|
||||
for client := range s.Clients {
|
||||
d, _ := json.Marshal(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()
|
||||
}
|
||||
|
||||
var info WebInfo = WebInfo{A: "asdf", B: "ffff"}
|
||||
|
||||
func main() {
|
||||
server := NewWebInfoServer()
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Second * 5)
|
||||
server.sendInfo(&info)
|
||||
}
|
||||
}()
|
||||
http.ListenAndServe("localhost:8080", &server.ServeMux)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/data/binding"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var app = app.New()
|
||||
|
||||
var (
|
||||
labelText = ""
|
||||
bindedLabelText = binding.BindString(&labelText)
|
||||
|
||||
label = widget.NewLabelWithData(bindedLabelText)
|
||||
)
|
||||
|
||||
var window = app.NewWindow("Canvas")
|
||||
|
||||
var verticalBox = container.NewVBox(label)
|
||||
window.SetContent(verticalBox)
|
||||
|
||||
go func() {
|
||||
for i := 0; ; i++ {
|
||||
var newLabelText = strconv.Itoa(i)
|
||||
if err := bindedLabelText.Set(newLabelText); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
time.Sleep(time.Microsecond)
|
||||
|
||||
// NOTE: the only thing, that helps prevent UI updates from freezes, except for the direct manipulation with window size, e.g. update window size from 499x499 -> 500x500 and vice-versa for each iteration
|
||||
canvas.Refresh(label)
|
||||
}
|
||||
}()
|
||||
|
||||
window.ShowAndRun()
|
||||
}
|
||||
@@ -92,14 +92,14 @@
|
||||
"en": "ID/URL",
|
||||
"zh-CN": "ID/网址"
|
||||
},
|
||||
"gui.playlist.add.source": {
|
||||
"en": "Source",
|
||||
"zh-CN": "来源"
|
||||
},
|
||||
"gui.playlist.add.prompt": {
|
||||
"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": "添加歌单"
|
||||
@@ -220,10 +220,6 @@
|
||||
"en": "Custom Command (Default one still works)",
|
||||
"zh-CN": "自定义命令 (默认的依然可用)"
|
||||
},
|
||||
"plugin.diange.source_cmd": {
|
||||
"en": "Source Command",
|
||||
"zh-CN": "来源点歌命令"
|
||||
},
|
||||
"plugin.diange.description": {
|
||||
"en": "Basic Diange Configuration",
|
||||
"zh-CN": "点歌基本设置"
|
||||
@@ -240,6 +236,10 @@
|
||||
"en": "Max Queue",
|
||||
"zh-CN": "最大点歌数"
|
||||
},
|
||||
"plugin.diange.source_cmd": {
|
||||
"en": "Source Command",
|
||||
"zh-CN": "来源点歌命令"
|
||||
},
|
||||
"plugin.diange.title": {
|
||||
"en": "Diange",
|
||||
"zh-CN": "点歌"
|
||||
@@ -291,6 +291,14 @@
|
||||
"plugin.textinfo.title": {
|
||||
"en": "Text Output",
|
||||
"zh-CN": "文本输出"
|
||||
},
|
||||
"plugin.webinfo.description": {
|
||||
"en": "Web output configuration",
|
||||
"zh-CN": "web输出设置"
|
||||
},
|
||||
"plugin.webinfo.title": {
|
||||
"en": "Web Output",
|
||||
"zh-CN": "Web输出"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
assets/webinfo/index.html
Normal file
11
assets/webinfo/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>LOL</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>123123</h1>
|
||||
<p>ABDCCC</p>
|
||||
</body>
|
||||
</html>
|
||||
3
assets/webinfo/js/main.js
Normal file
3
assets/webinfo/js/main.js
Normal file
@@ -0,0 +1,3 @@
|
||||
function aaa() {
|
||||
return 0;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"path"
|
||||
)
|
||||
|
||||
const VERSION = "alpha 0.8.1"
|
||||
const VERSION = "alpha 0.8.3"
|
||||
|
||||
const CONFIG_PATH = "./config.ini"
|
||||
const Assests_PATH = "./assets"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -10,12 +10,14 @@ require (
|
||||
github.com/aynakeya/go-mpv v0.0.4
|
||||
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 // indirect
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/mitchellh/panicwrap v1.0.0
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/cast v1.3.1
|
||||
github.com/tidwall/gjson v1.14.1
|
||||
gopkg.in/ini.v1 v1.66.4
|
||||
nhooyr.io/websocket v1.8.7
|
||||
)
|
||||
|
||||
replace (
|
||||
|
||||
51
go.sum
51
go.sum
@@ -20,32 +20,70 @@ github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8
|
||||
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f h1:s0O46d8fPwk9kU4k1jj76wBquMVETx7uveQD9MCIQoU=
|
||||
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be h1:Z28GdQBfKOL8tNHjvaDn3wHDO7AzTRkmAXvHvnopp98=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
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/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8=
|
||||
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
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/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/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
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/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE=
|
||||
github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
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=
|
||||
@@ -67,6 +105,8 @@ github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCE
|
||||
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk=
|
||||
@@ -78,6 +118,10 @@ 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/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.3.8 h1:Nw158Q8QN+CPgTmVRByhVwapp8Mm1e2blinhmx4wx5E=
|
||||
@@ -99,6 +143,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -107,14 +152,18 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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-20191204190536-9bdfabe68543/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=
|
||||
@@ -125,3 +174,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||
|
||||
@@ -60,6 +60,10 @@ func (d *Diange) Enable() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Diange) Disable() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Diange) initCMD() {
|
||||
if len(d.SourceCMD) == len(config.Provider.Priority) {
|
||||
return
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -106,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)
|
||||
@@ -228,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
33
plugin/webinfo/info.go
Normal 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
|
||||
}
|
||||
144
plugin/webinfo/server.go
Normal file
144
plugin/webinfo/server.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package webinfo
|
||||
|
||||
import (
|
||||
"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
|
||||
Server *http.Server
|
||||
Clients map[*Client]int
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
conn *websocket.Conn
|
||||
Data chan []byte
|
||||
Close chan byte
|
||||
}
|
||||
|
||||
func NewWebInfoServer(port int) *WebInfoServer {
|
||||
server := &WebInfoServer{
|
||||
Clients: map[*Client]int{},
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", http.FileServer(http.Dir("./assets/webinfo")))
|
||||
mux.HandleFunc("/ws/info", server.handleInfo)
|
||||
mux.HandleFunc("/api/info", server.getInfo)
|
||||
server.Server = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", port),
|
||||
Handler: mux,
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *WebInfoServer) getInfo(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
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() {
|
||||
go func() {
|
||||
err := s.Server.ListenAndServe()
|
||||
if err == http.ErrServerClosed {
|
||||
lg.Info("server closed")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
lg.Warnf("Failed to start webinfo server: %s", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *WebInfoServer) Stop() error {
|
||||
return s.Server.Shutdown(context.Background())
|
||||
}
|
||||
140
plugin/webinfo/webinfo.go
Normal file
140
plugin/webinfo/webinfo.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package webinfo
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/config"
|
||||
"AynaLivePlayer/controller"
|
||||
"AynaLivePlayer/event"
|
||||
"AynaLivePlayer/gui"
|
||||
"AynaLivePlayer/i18n"
|
||||
"AynaLivePlayer/logger"
|
||||
"AynaLivePlayer/player"
|
||||
"fyne.io/fyne/v2"
|
||||
"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
|
||||
}
|
||||
|
||||
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) CreatePanel() fyne.CanvasObject {
|
||||
return widget.NewLabel("adf")
|
||||
}
|
||||
Reference in New Issue
Block a user