navidrome/core/agents/listenbrainz/client.go

178 lines
4.5 KiB
Go

package listenbrainz
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"github.com/navidrome/navidrome/log"
)
type listenBrainzError struct {
Code int
Message string
}
func (e *listenBrainzError) Error() string {
return fmt.Sprintf("ListenBrainz error(%d): %s", e.Code, e.Message)
}
type httpDoer interface {
Do(req *http.Request) (*http.Response, error)
}
func newClient(baseURL string, hc httpDoer) *client {
return &client{baseURL, hc}
}
type client struct {
baseURL string
hc httpDoer
}
type listenBrainzResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Error string `json:"error"`
Status string `json:"status"`
Valid bool `json:"valid"`
UserName string `json:"user_name"`
}
type listenBrainzRequest struct {
ApiKey string
Body listenBrainzRequestBody
}
type listenBrainzRequestBody struct {
ListenType listenType `json:"listen_type,omitempty"`
Payload []listenInfo `json:"payload,omitempty"`
}
type listenType string
const (
Single listenType = "single"
PlayingNow listenType = "playing_now"
)
type listenInfo struct {
ListenedAt int `json:"listened_at,omitempty"`
TrackMetadata trackMetadata `json:"track_metadata,omitempty"`
}
type trackMetadata struct {
ArtistName string `json:"artist_name,omitempty"`
TrackName string `json:"track_name,omitempty"`
ReleaseName string `json:"release_name,omitempty"`
AdditionalInfo additionalInfo `json:"additional_info,omitempty"`
}
type additionalInfo struct {
SubmissionClient string `json:"submission_client,omitempty"`
SubmissionClientVersion string `json:"submission_client_version,omitempty"`
TrackNumber int `json:"tracknumber,omitempty"`
RecordingMbzID string `json:"recording_mbid,omitempty"`
ArtistMbzIDs []string `json:"artist_mbids,omitempty"`
ReleaseMbID string `json:"release_mbid,omitempty"`
DurationMs int `json:"duration_ms,omitempty"`
}
func (c *client) validateToken(ctx context.Context, apiKey string) (*listenBrainzResponse, error) {
r := &listenBrainzRequest{
ApiKey: apiKey,
}
response, err := c.makeRequest(ctx, http.MethodGet, "validate-token", r)
if err != nil {
return nil, err
}
return response, nil
}
func (c *client) updateNowPlaying(ctx context.Context, apiKey string, li listenInfo) error {
r := &listenBrainzRequest{
ApiKey: apiKey,
Body: listenBrainzRequestBody{
ListenType: PlayingNow,
Payload: []listenInfo{li},
},
}
resp, err := c.makeRequest(ctx, http.MethodPost, "submit-listens", r)
if err != nil {
return err
}
if resp.Status != "ok" {
log.Warn(ctx, "ListenBrainz: NowPlaying was not accepted", "status", resp.Status)
}
return nil
}
func (c *client) scrobble(ctx context.Context, apiKey string, li listenInfo) error {
r := &listenBrainzRequest{
ApiKey: apiKey,
Body: listenBrainzRequestBody{
ListenType: Single,
Payload: []listenInfo{li},
},
}
resp, err := c.makeRequest(ctx, http.MethodPost, "submit-listens", r)
if err != nil {
return err
}
if resp.Status != "ok" {
log.Warn(ctx, "ListenBrainz: Scrobble was not accepted", "status", resp.Status)
}
return nil
}
func (c *client) path(endpoint string) (string, error) {
u, err := url.Parse(c.baseURL)
if err != nil {
return "", err
}
u.Path = path.Join(u.Path, endpoint)
return u.String(), nil
}
func (c *client) makeRequest(ctx context.Context, method string, endpoint string, r *listenBrainzRequest) (*listenBrainzResponse, error) {
b, _ := json.Marshal(r.Body)
uri, err := c.path(endpoint)
if err != nil {
return nil, err
}
req, _ := http.NewRequestWithContext(ctx, method, uri, bytes.NewBuffer(b))
req.Header.Add("Content-Type", "application/json; charset=UTF-8")
if r.ApiKey != "" {
req.Header.Add("Authorization", fmt.Sprintf("Token %s", r.ApiKey))
}
log.Trace(ctx, fmt.Sprintf("Sending ListenBrainz %s request", req.Method), "url", req.URL)
resp, err := c.hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
decoder := json.NewDecoder(resp.Body)
var response listenBrainzResponse
jsonErr := decoder.Decode(&response)
if resp.StatusCode != 200 && jsonErr != nil {
return nil, fmt.Errorf("ListenBrainz: HTTP Error, Status: (%d)", resp.StatusCode)
}
if jsonErr != nil {
return nil, jsonErr
}
if response.Code != 0 && response.Code != 200 {
return &response, &listenBrainzError{Code: response.Code, Message: response.Error}
}
return &response, nil
}