178 lines
4.5 KiB
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
|
|
}
|