MeteorLight/api/api.go

387 lines
9.0 KiB
Go

package api
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/queue"
config2 "git.gammaspectra.live/S.O.N.G/MeteorLight/config"
queue2 "git.gammaspectra.live/S.O.N.G/MeteorLight/queue"
"git.gammaspectra.live/S.O.N.G/MeteorLight/queue/track"
"io"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
type API struct {
config *config2.Config
queue *queue2.Queue
wg sync.WaitGroup
nr *track.Entry
}
func NewAPI(config *config2.Config, queue *queue2.Queue) *API {
api := &API{
config: config,
queue: queue,
}
api.listen()
api.handleQueue()
return api
}
func (a *API) Wait() {
a.wg.Wait()
}
func (a *API) getQueueEntryFromBody(body []byte) (*track.Entry, error) {
entry := &track.Entry{}
err := json.Unmarshal(body, &entry.Original)
if err != nil {
return nil, err
}
err = json.Unmarshal(body, &entry.Metadata)
if err != nil {
return nil, err
}
var val interface{}
var strVal string
var ok bool
if val, ok = entry.Original["hash"]; ok && a.config.Queue.SongFetchUrl != "" {
if strVal, ok = val.(string); ok {
entry.Path = a.config.Queue.SongFetchUrl + strVal
}
} else if val, ok = entry.Original["path"]; ok {
if strVal, ok = val.(string); ok {
entry.Path = strVal
}
}
if len(entry.Path) > 0 {
return entry, nil
}
return nil, errors.New("could not create queue entry")
}
func (a *API) getFallbackTrack() (*track.Entry, error) {
m := make(map[string]interface{})
m["path"] = a.config.Queue.FallbackPath
return &track.Entry{
Path: a.config.Queue.FallbackPath,
Original: m,
}, nil
}
func (a *API) getRandomTrack() (*track.Entry, error) {
response, err := (&http.Client{
Timeout: time.Second * 60,
}).Get(a.config.Queue.RandomSongApi)
if err != nil {
return nil, err
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
return a.getQueueEntryFromBody(body)
}
func (a *API) setNowRandom(nr *track.Entry) {
a.nr = nr
if a.config.Queue.NowRandom != "" {
jsonData, _ := json.Marshal(nr.Original)
response, err := http.DefaultClient.Post(a.config.Queue.NowRandom, "application/json; charset=utf-8", bytes.NewReader(jsonData))
if err != nil {
log.Print(err)
}
if response != nil {
defer response.Body.Close()
}
}
}
func (a *API) handleQueue() {
a.wg.Add(1)
go func() {
defer a.wg.Done()
defer close(a.queue.QueueEmpty)
//TODO: close properly
for {
failed := true
//give three tries for a random track to succeed
for i := 0; i < 3; i++ {
if e, err := a.getRandomTrack(); e != nil {
//preload
if err = e.Load(); err != nil {
log.Printf("random track loading error for %s: \"%s\"", e.Path, err)
continue
}
failed = false
a.setNowRandom(e)
a.queue.QueueEmpty <- e
break
} else {
log.Printf("random track error: \"%s\"", err)
}
}
if failed {
if e, err := a.getFallbackTrack(); e != nil {
//preload
if err = e.Load(); err != nil {
log.Printf("fallback track loading error for %s: \"%s\"", e.Path, err)
continue
}
a.setNowRandom(e)
a.queue.QueueEmpty <- e
} else {
log.Printf("fallback track error: \"%s\"", err)
}
}
}
}()
a.wg.Add(1)
go func() {
defer a.wg.Done()
for np := range a.queue.NowPlaying {
jsonData, _ := json.Marshal(np.Original)
response, err := http.DefaultClient.Post(a.config.Queue.NowPlaying, "application/json; charset=utf-8", bytes.NewReader(jsonData))
if err != nil {
log.Print(err)
}
if response != nil {
response.Body.Close()
}
}
}()
//insert first track
a.queue.HandleQueue()
}
func (a *API) listen() {
type resultResponse struct {
Success bool `json:"success"`
Reason *string `json:"reason"`
}
type queueResultResponse struct {
resultResponse
QueueId queue.Identifier `json:"queue_id"`
}
a.wg.Add(1)
go func() {
defer a.wg.Done()
server := http.Server{
Addr: fmt.Sprintf("%s:%d", a.config.Api.Host, a.config.Api.Port),
Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Server", "MeteorLight/api")
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
pathSegments := strings.Split(request.URL.Path, "/")
if len(pathSegments) > 1 {
switch pathSegments[1] {
case "listeners":
if len(pathSegments) > 2 {
if request.Method != "DELETE" {
return
}
result := resultResponse{
Success: a.queue.RemoveListener(pathSegments[2]),
}
if !result.Success {
resultErr := fmt.Sprintf("listener %s not found", pathSegments[2])
result.Reason = &resultErr
}
jsonData, _ := json.Marshal(result)
writer.Write(jsonData)
return
} else {
jsonData, _ := json.Marshal(a.queue.GetListeners())
writer.Write(jsonData)
}
return
case "np":
if e := a.queue.GetNowPlaying(); e != nil {
jsonData, _ := json.Marshal(e.Original)
writer.Write(jsonData)
} else {
writer.Write([]byte{'{', '}'})
}
return
case "random":
if a.nr != nil {
jsonData, _ := json.Marshal(a.nr.Original)
writer.Write(jsonData)
} else {
writer.Write([]byte{'{', '}'})
}
return
case "queue":
if len(pathSegments) == 2 {
if request.Method != "GET" {
return
}
var blobs = make([]map[string]interface{}, 0, 1)
for _, e := range a.queue.GetQueue() {
blobs = append(blobs, e.Original)
}
jsonData, _ := json.Marshal(blobs)
writer.Write(jsonData)
return
} else {
switch pathSegments[2] {
case "head":
if request.Method == "POST" {
result := queueResultResponse{}
if body, err := io.ReadAll(request.Body); err == nil {
if e, err := a.getQueueEntryFromBody(body); e != nil {
if err = a.queue.AddTrack(e, false); err == nil {
result.Success = true
result.QueueId = e.Identifier
} else {
resultErr := err.Error()
result.Reason = &resultErr
log.Printf("track addition error for %s: \"%s\"", e.Path, err)
}
} else {
resultErr := err.Error()
result.Reason = &resultErr
log.Printf("track addition error: \"%s\"", err)
}
}
jsonData, _ := json.Marshal(result)
writer.Write(jsonData)
return
} else if request.Method == "DELETE" {
result := resultResponse{}
if head := a.queue.GetHead(); head != nil {
result.Success = a.queue.Remove(head.Identifier)
}
jsonData, _ := json.Marshal(result)
writer.Write(jsonData)
return
}
case "tail":
if request.Method == "POST" {
result := queueResultResponse{}
if body, err := io.ReadAll(request.Body); err == nil {
if e, err := a.getQueueEntryFromBody(body); e != nil {
if err = a.queue.AddTrack(e, true); err == nil {
result.Success = true
result.QueueId = e.Identifier
} else {
resultErr := err.Error()
result.Reason = &resultErr
log.Printf("track addition error for %s: \"%s\"", e.Path, err)
}
} else {
resultErr := err.Error()
result.Reason = &resultErr
log.Printf("track addition error: \"%s\"", err)
}
}
jsonData, _ := json.Marshal(result)
writer.Write(jsonData)
return
} else if request.Method == "DELETE" {
result := resultResponse{}
if head := a.queue.GetTail(); head != nil {
result.Success = a.queue.Remove(head.Identifier)
}
jsonData, _ := json.Marshal(result)
writer.Write(jsonData)
return
}
case "clear":
if request.Method != "POST" {
return
}
result := resultResponse{}
for _, e := range a.queue.GetQueue() {
a.queue.Remove(e.Identifier)
}
result.Success = true
jsonData, _ := json.Marshal(result)
writer.Write(jsonData)
return
default:
if request.Method != "POST" {
return
}
if i, err := strconv.ParseInt(pathSegments[2], 10, 0); err == nil {
result := resultResponse{}
result.Success = a.queue.Remove(queue.Identifier(i))
jsonData, _ := json.Marshal(result)
writer.Write(jsonData)
return
}
}
}
case "skip":
if request.Method != "POST" {
return
}
result := resultResponse{}
result.Success = a.queue.SkipNowPlaying()
jsonData, _ := json.Marshal(result)
writer.Write(jsonData)
return
}
}
writer.WriteHeader(http.StatusNotFound)
writer.Write([]byte{'{', '}'})
}),
}
if err := server.ListenAndServe(); err != nil {
log.Panic(err)
}
}()
}