Skip to content

Commit

Permalink
feat: add web-api passthrough
Browse files Browse the repository at this point in the history
* add web-api passthrough

* fix comment

* add error log like in the original

* following naming convention

* move Body.Close below error condition

* fix and update statuscode

* refactor: spclient request

* add type ApiResponseMessage

* convert response message into ApiResponseMessage

* chore: cleanup

---------

Closes #65 

Co-authored-by: devgianlu <altomanigianluca@gmail.com>
  • Loading branch information
momozahara and devgianlu committed Aug 17, 2024
1 parent b9bc8c8 commit 84b3c3f
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 12 deletions.
68 changes: 58 additions & 10 deletions cmd/daemon/api_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (
log "github.com/sirupsen/logrus"
"net"
"net/http"
"net/url"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
"strings"
"sync"
"time"
)
Expand All @@ -30,11 +32,19 @@ type ApiServer struct {
clientsLock sync.RWMutex
}

var ErrNoSession = errors.New("no session")
var (
ErrNoSession = errors.New("no session")
ErrBadRequest = errors.New("bad request")
ErrForbidden = errors.New("forbidden")
ErrNotFound = errors.New("not found")
ErrMethodNotAllowed = errors.New("method not allowed")
ErrTooManyRequests = errors.New("the app has exceeded its rate limits")
)

type ApiRequestType string

const (
ApiRequestTypeWebApi ApiRequestType = "web_api"
ApiRequestTypeStatus ApiRequestType = "status"
ApiRequestTypeResume ApiRequestType = "resume"
ApiRequestTypePause ApiRequestType = "pause"
Expand Down Expand Up @@ -79,6 +89,12 @@ func (r *ApiRequest) Reply(data any, err error) {
r.resp <- apiResponse{data, err}
}

type ApiRequestDataWebApi struct {
Method string
Path string
Query url.Values
}

type ApiRequestDataPlay struct {
Uri string `json:"uri"`
SkipToUri string `json:"skip_to_uri"`
Expand Down Expand Up @@ -251,17 +267,39 @@ func (s *ApiServer) handleRequest(req ApiRequest, w http.ResponseWriter) {
req.resp = make(chan apiResponse, 1)
s.requests <- req
resp := <-req.resp
if errors.Is(resp.err, ErrNoSession) {
w.WriteHeader(http.StatusNoContent)
return
} else if resp.err != nil {
log.WithError(resp.err).Error("failed handling status request")
w.WriteHeader(http.StatusInternalServerError)
return

if resp.err != nil {
switch {
case errors.Is(resp.err, ErrNoSession):
w.WriteHeader(http.StatusNoContent)
return
case errors.Is(resp.err, ErrForbidden):
w.WriteHeader(http.StatusForbidden)
return
case errors.Is(resp.err, ErrNotFound):
w.WriteHeader(http.StatusNotFound)
return
case errors.Is(resp.err, ErrMethodNotAllowed):
w.WriteHeader(http.StatusMethodNotAllowed)
return
case errors.Is(resp.err, ErrTooManyRequests):
w.WriteHeader(http.StatusTooManyRequests)
return
default:
log.WithError(resp.err).Errorf("failed handling request %s", req.Type)
w.WriteHeader(http.StatusInternalServerError)
return
}
}

w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp.data)
switch respData := resp.data.(type) {
case []byte:
w.Header().Set("Content-Type", "application/octet-stream")
_, _ = w.Write(respData)
default:
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(respData)
}
}

func (s *ApiServer) allowOriginMiddleware(next http.Handler) http.Handler {
Expand All @@ -280,6 +318,16 @@ func (s *ApiServer) serve() {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte("{}"))
})
m.Handle("/web-api/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.handleRequest(ApiRequest{
Type: ApiRequestTypeWebApi,
Data: ApiRequestDataWebApi{
Method: r.Method,
Path: strings.TrimPrefix(r.URL.Path, "/web-api/"),
Query: r.URL.Query(),
},
}, w)
}))
m.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
Expand Down
42 changes: 42 additions & 0 deletions cmd/daemon/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
librespot "github.com/devgianlu/go-librespot"
Expand All @@ -14,6 +15,7 @@ import (
"github.com/devgianlu/go-librespot/tracks"
log "github.com/sirupsen/logrus"
"google.golang.org/protobuf/proto"
"io"
"math"
"strings"
"sync"
Expand Down Expand Up @@ -314,6 +316,46 @@ func (p *AppPlayer) handleDealerRequest(req dealer.Request) error {

func (p *AppPlayer) handleApiRequest(req ApiRequest) (any, error) {
switch req.Type {
case ApiRequestTypeWebApi:
data := req.Data.(ApiRequestDataWebApi)
resp, err := p.sess.WebApi(data.Method, data.Path, data.Query, nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to send web api request: %w", err)
}

defer func() { _ = resp.Body.Close() }()

// this is the status we want to return to client not just 500
switch resp.StatusCode {
case 400:
return nil, ErrBadRequest
case 403:
return nil, ErrForbidden
case 404:
return nil, ErrNotFound
case 405:
return nil, ErrMethodNotAllowed
case 429:
return nil, ErrTooManyRequests
}

// check for content type if not application/json
if !strings.Contains(resp.Header.Get("Content-Type"), "application/json") {
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}

return respBody, nil
}

// decode and return json
var respJson any
if err = json.NewDecoder(resp.Body).Decode(&respJson); err != nil {
return nil, fmt.Errorf("failed to decode response body: %w", err)
}

return respJson, nil
case ApiRequestTypeStatus:
resp := &ApiResponseStatus{
Username: p.sess.Username(),
Expand Down
7 changes: 7 additions & 0 deletions session/getters.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package session

import (
"net/http"
"net/url"

"github.com/devgianlu/go-librespot/ap"
"github.com/devgianlu/go-librespot/audio"
"github.com/devgianlu/go-librespot/dealer"
Expand Down Expand Up @@ -30,3 +33,7 @@ func (s *Session) Dealer() *dealer.Dealer {
func (s *Session) Accesspoint() *ap.Accesspoint {
return s.ap
}

func (s *Session) WebApi(method string, path string, query url.Values, header http.Header, body []byte) (*http.Response, error) {
return s.sp.WebApiRequest(method, path, query, header, body)
}
17 changes: 15 additions & 2 deletions spclient/spclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ func NewSpclient(addr librespot.GetAddressFunc, accessToken librespot.GetLogin5T
}, nil
}

func (c *Spclient) request(method string, path string, query url.Values, header http.Header, body []byte) (*http.Response, error) {
reqUrl := c.baseUrl.JoinPath(path)
func (c *Spclient) innerRequest(method string, reqUrl *url.URL, query url.Values, header http.Header, body []byte) (*http.Response, error) {
if query != nil {
reqUrl.RawQuery = query.Encode()
}
Expand Down Expand Up @@ -97,6 +96,20 @@ func (c *Spclient) request(method string, path string, query url.Values, header
return resp, nil
}

func (c *Spclient) WebApiRequest(method string, path string, query url.Values, header http.Header, body []byte) (*http.Response, error) {
reqPath, err := url.Parse("https://api.spotify.com/")
if err != nil {
panic("invalid api base url")
}
reqURL := reqPath.JoinPath(path)
return c.innerRequest(method, reqURL, query, header, body)
}

func (c *Spclient) request(method string, path string, query url.Values, header http.Header, body []byte) (*http.Response, error) {
reqUrl := c.baseUrl.JoinPath(path)
return c.innerRequest(method, reqUrl, query, header, body)
}

type putStateError struct {
ErrorType string `json:"error_type"`
Message string `json:"message"`
Expand Down

0 comments on commit 84b3c3f

Please sign in to comment.