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) } }() }