move scrobblers into their own package
This commit is contained in:
committed by
Senan Kelly
parent
b9998f7ee6
commit
4443d7d0f5
@@ -9,8 +9,8 @@ import (
|
|||||||
|
|
||||||
"go.senan.xyz/gonic/server/db"
|
"go.senan.xyz/gonic/server/db"
|
||||||
"go.senan.xyz/gonic/server/encode"
|
"go.senan.xyz/gonic/server/encode"
|
||||||
"go.senan.xyz/gonic/server/lastfm"
|
|
||||||
"go.senan.xyz/gonic/server/scanner"
|
"go.senan.xyz/gonic/server/scanner"
|
||||||
|
"go.senan.xyz/gonic/server/scrobble/lastfm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func firstExisting(or string, strings ...string) string {
|
func firstExisting(or string, strings ...string) string {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
||||||
"go.senan.xyz/gonic/server/jukebox"
|
"go.senan.xyz/gonic/server/jukebox"
|
||||||
"go.senan.xyz/gonic/server/lastfm"
|
"go.senan.xyz/gonic/server/scrobble"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CtxKey int
|
type CtxKey int
|
||||||
@@ -29,7 +29,7 @@ type Controller struct {
|
|||||||
CachePath string
|
CachePath string
|
||||||
CoverCachePath string
|
CoverCachePath string
|
||||||
Jukebox *jukebox.Jukebox
|
Jukebox *jukebox.Jukebox
|
||||||
Scrobblers []lastfm.Scrobbler
|
Scrobblers []scrobble.Scrobbler
|
||||||
}
|
}
|
||||||
|
|
||||||
type metaResponse struct {
|
type metaResponse struct {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
|
||||||
"go.senan.xyz/gonic/server/db"
|
"go.senan.xyz/gonic/server/db"
|
||||||
"go.senan.xyz/gonic/server/lastfm"
|
"go.senan.xyz/gonic/server/scrobble/lastfm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response {
|
func (c *Controller) ServeGetArtists(r *http.Request) *spec.Response {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
|
||||||
"go.senan.xyz/gonic/server/db"
|
"go.senan.xyz/gonic/server/db"
|
||||||
"go.senan.xyz/gonic/server/lastfm"
|
|
||||||
"go.senan.xyz/gonic/server/scanner"
|
"go.senan.xyz/gonic/server/scanner"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,20 +49,13 @@ func (c *Controller) ServeScrobble(r *http.Request) *spec.Response {
|
|||||||
Preload("Album").
|
Preload("Album").
|
||||||
Preload("Artist").
|
Preload("Artist").
|
||||||
First(track, id.Value)
|
First(track, id.Value)
|
||||||
// scrobble with above info
|
// clients will provide time in miliseconds, so use that or
|
||||||
opts := lastfm.ScrobbleOptions{
|
// instead convert UnixNano to miliseconds
|
||||||
Track: track,
|
optStampMili := params.GetOrInt("time", int(time.Now().UnixNano()/1e6))
|
||||||
// clients will provide time in miliseconds, so use that or
|
optSubmission := params.GetOrBool("submission", true)
|
||||||
// instead convert UnixNano to miliseconds
|
|
||||||
StampMili: params.GetOrInt("time", int(time.Now().UnixNano()/1e6)),
|
|
||||||
Submission: params.GetOrBool("submission", true),
|
|
||||||
}
|
|
||||||
scrobbleErrs := []error{}
|
scrobbleErrs := []error{}
|
||||||
for _, scrobbler := range c.Scrobblers {
|
for _, scrobbler := range c.Scrobblers {
|
||||||
if !scrobbler.Enabled(user) {
|
err = scrobbler.Scrobble(user, track, optStampMili, optSubmission)
|
||||||
continue
|
|
||||||
}
|
|
||||||
err = scrobbler.Scrobble(user, opts)
|
|
||||||
scrobbleErrs = append(scrobbleErrs, err)
|
scrobbleErrs = append(scrobbleErrs, err)
|
||||||
}
|
}
|
||||||
if len(scrobbleErrs) != 0 {
|
if len(scrobbleErrs) != 0 {
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
package lastfm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
|
|
||||||
"go.senan.xyz/gonic/server/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Scrobbler interface {
|
|
||||||
Scrobble(*db.User, ScrobbleOptions) error
|
|
||||||
Enabled(*db.User) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type LastFM struct {
|
|
||||||
XMLName xml.Name `xml:"lfm"`
|
|
||||||
Status string `xml:"status,attr"`
|
|
||||||
Session Session `xml:"session"`
|
|
||||||
Error Error `xml:"error"`
|
|
||||||
Artist Artist `xml:"artist"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Session struct {
|
|
||||||
Name string `xml:"name"`
|
|
||||||
Key string `xml:"key"`
|
|
||||||
Subscriber uint `xml:"subscriber"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Error struct {
|
|
||||||
Code uint `xml:"code,attr"`
|
|
||||||
Value string `xml:",chardata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Artist struct {
|
|
||||||
XMLName xml.Name `xml:"artist"`
|
|
||||||
Name string `xml:"name"`
|
|
||||||
MBID string `xml:"mbid"`
|
|
||||||
URL string `xml:"url"`
|
|
||||||
Image []struct {
|
|
||||||
Text string `xml:",chardata"`
|
|
||||||
Size string `xml:"size,attr"`
|
|
||||||
} `xml:"image"`
|
|
||||||
Streamable string `xml:"streamable"`
|
|
||||||
Stats struct {
|
|
||||||
Listeners string `xml:"listeners"`
|
|
||||||
Plays string `xml:"plays"`
|
|
||||||
} `xml:"stats"`
|
|
||||||
Similar struct {
|
|
||||||
Artists []Artist `xml:"artist"`
|
|
||||||
} `xml:"similar"`
|
|
||||||
Tags struct {
|
|
||||||
Tag []ArtistTag `xml:"tag"`
|
|
||||||
} `xml:"tags"`
|
|
||||||
Bio ArtistBio `xml:"bio"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ArtistTag struct {
|
|
||||||
Name string `xml:"name"`
|
|
||||||
URL string `xml:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ArtistBio struct {
|
|
||||||
Published string `xml:"published"`
|
|
||||||
Summary string `xml:"summary"`
|
|
||||||
Content string `xml:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListenBrainzAdditionalInfo struct {
|
|
||||||
TrackNumber int `json:"tracknumber"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListenBrainzTrackMetadata struct {
|
|
||||||
AdditionalInfo ListenBrainzAdditionalInfo `json:"additional_info"`
|
|
||||||
ArtistName string `json:"artist_name"`
|
|
||||||
TrackName string `json:"track_name"`
|
|
||||||
ReleaseName string `json:"release_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListenBrainzPayload struct {
|
|
||||||
ListenedAt int `json:"listened_at"`
|
|
||||||
TrackMetadata ListenBrainzTrackMetadata `json:"track_metadata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListenBrainzScrobble struct {
|
|
||||||
ListenType string `json:"listen_type"`
|
|
||||||
Payload []ListenBrainzPayload `json:"payload"`
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
package lastfm
|
package lastfm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -14,19 +12,69 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"go.senan.xyz/gonic/server/db"
|
"go.senan.xyz/gonic/server/db"
|
||||||
|
"go.senan.xyz/gonic/server/scrobble"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
lastfmBaseURL = "https://ws.audioscrobbler.com/2.0/"
|
baseURL = "https://ws.audioscrobbler.com/2.0/"
|
||||||
lbBaseURL = "https://api.listenbrainz.org"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrLastFM = errors.New("last.fm error")
|
ErrLastFM = errors.New("last.fm error")
|
||||||
ErrListenBrainz = errors.New("listenbrainz error")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: remove this package's dependency on models/db
|
type LastFM struct {
|
||||||
|
XMLName xml.Name `xml:"lfm"`
|
||||||
|
Status string `xml:"status,attr"`
|
||||||
|
Session Session `xml:"session"`
|
||||||
|
Error Error `xml:"error"`
|
||||||
|
Artist Artist `xml:"artist"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
Name string `xml:"name"`
|
||||||
|
Key string `xml:"key"`
|
||||||
|
Subscriber uint `xml:"subscriber"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Code uint `xml:"code,attr"`
|
||||||
|
Value string `xml:",chardata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Artist struct {
|
||||||
|
XMLName xml.Name `xml:"artist"`
|
||||||
|
Name string `xml:"name"`
|
||||||
|
MBID string `xml:"mbid"`
|
||||||
|
URL string `xml:"url"`
|
||||||
|
Image []struct {
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Size string `xml:"size,attr"`
|
||||||
|
} `xml:"image"`
|
||||||
|
Streamable string `xml:"streamable"`
|
||||||
|
Stats struct {
|
||||||
|
Listeners string `xml:"listeners"`
|
||||||
|
Plays string `xml:"plays"`
|
||||||
|
} `xml:"stats"`
|
||||||
|
Similar struct {
|
||||||
|
Artists []Artist `xml:"artist"`
|
||||||
|
} `xml:"similar"`
|
||||||
|
Tags struct {
|
||||||
|
Tag []ArtistTag `xml:"tag"`
|
||||||
|
} `xml:"tags"`
|
||||||
|
Bio ArtistBio `xml:"bio"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArtistTag struct {
|
||||||
|
Name string `xml:"name"`
|
||||||
|
URL string `xml:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArtistBio struct {
|
||||||
|
Published string `xml:"published"`
|
||||||
|
Summary string `xml:"summary"`
|
||||||
|
Content string `xml:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
func getParamSignature(params url.Values, secret string) string {
|
func getParamSignature(params url.Values, secret string) string {
|
||||||
// the parameters must be in order before hashing
|
// the parameters must be in order before hashing
|
||||||
@@ -46,7 +94,7 @@ func getParamSignature(params url.Values, secret string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeRequest(method string, params url.Values) (LastFM, error) {
|
func makeRequest(method string, params url.Values) (LastFM, error) {
|
||||||
req, _ := http.NewRequest(method, lastfmBaseURL, nil)
|
req, _ := http.NewRequest(method, baseURL, nil)
|
||||||
req.URL.RawQuery = params.Encode()
|
req.URL.RawQuery = params.Encode()
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -64,6 +112,18 @@ func makeRequest(method string, params url.Values) (LastFM, error) {
|
|||||||
return lastfm, nil
|
return lastfm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ArtistGetInfo(apiKey string, artist *db.Artist) (Artist, error) {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("method", "artist.getInfo")
|
||||||
|
params.Add("api_key", apiKey)
|
||||||
|
params.Add("artist", artist.Name)
|
||||||
|
resp, err := makeRequest("GET", params)
|
||||||
|
if err != nil {
|
||||||
|
return Artist{}, fmt.Errorf("making artist GET: %w", err)
|
||||||
|
}
|
||||||
|
return resp.Artist, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetSession(apiKey, secret, token string) (string, error) {
|
func GetSession(apiKey, secret, token string) (string, error) {
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add("method", "auth.getSession")
|
params.Add("method", "auth.getSession")
|
||||||
@@ -77,101 +137,39 @@ func GetSession(apiKey, secret, token string) (string, error) {
|
|||||||
return resp.Session.Key, nil
|
return resp.Session.Key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrobbleOptions struct {
|
type Scrobbler struct {
|
||||||
Track *db.Track
|
|
||||||
StampMili int
|
|
||||||
Submission bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type LastfmScrobbler struct { //nolint
|
|
||||||
DB *db.DB
|
DB *db.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lfm *LastfmScrobbler) Scrobble(user *db.User, opts ScrobbleOptions) error {
|
func (s *Scrobbler) Scrobble(user *db.User, track *db.Track, stampMili int, submission bool) error {
|
||||||
apiKey := lfm.DB.GetSetting("lastfm_api_key")
|
if user.LastFMSession == "" {
|
||||||
secret := lfm.DB.GetSetting("lastfm_secret")
|
return nil
|
||||||
|
}
|
||||||
|
apiKey := s.DB.GetSetting("lastfm_api_key")
|
||||||
|
secret := s.DB.GetSetting("lastfm_secret")
|
||||||
// fetch user to get lastfm session
|
// fetch user to get lastfm session
|
||||||
if user.LastFMSession == "" {
|
if user.LastFMSession == "" {
|
||||||
return fmt.Errorf("you don't have a last.fm session: %w", ErrLastFM)
|
return fmt.Errorf("you don't have a last.fm session: %w", ErrLastFM)
|
||||||
}
|
}
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
if opts.Submission {
|
if submission {
|
||||||
params.Add("method", "track.Scrobble")
|
params.Add("method", "track.Scrobble")
|
||||||
// last.fm wants the timestamp in seconds
|
// last.fm wants the timestamp in seconds
|
||||||
params.Add("timestamp", strconv.Itoa(opts.StampMili/1e3))
|
params.Add("timestamp", strconv.Itoa(stampMili/1e3))
|
||||||
} else {
|
} else {
|
||||||
params.Add("method", "track.updateNowPlaying")
|
params.Add("method", "track.updateNowPlaying")
|
||||||
}
|
}
|
||||||
params.Add("api_key", apiKey)
|
params.Add("api_key", apiKey)
|
||||||
params.Add("sk", user.LastFMSession)
|
params.Add("sk", user.LastFMSession)
|
||||||
params.Add("artist", opts.Track.TagTrackArtist)
|
params.Add("artist", track.TagTrackArtist)
|
||||||
params.Add("track", opts.Track.TagTitle)
|
params.Add("track", track.TagTitle)
|
||||||
params.Add("trackNumber", strconv.Itoa(opts.Track.TagTrackNumber))
|
params.Add("trackNumber", strconv.Itoa(track.TagTrackNumber))
|
||||||
params.Add("album", opts.Track.Album.TagTitle)
|
params.Add("album", track.Album.TagTitle)
|
||||||
params.Add("mbid", opts.Track.TagBrainzID)
|
params.Add("mbid", track.TagBrainzID)
|
||||||
params.Add("albumArtist", opts.Track.Artist.Name)
|
params.Add("albumArtist", track.Artist.Name)
|
||||||
params.Add("api_sig", getParamSignature(params, secret))
|
params.Add("api_sig", getParamSignature(params, secret))
|
||||||
_, err := makeRequest("POST", params)
|
_, err := makeRequest("POST", params)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lfm *LastfmScrobbler) Enabled(user *db.User) bool {
|
var _ scrobble.Scrobbler = (*Scrobbler)(nil)
|
||||||
return user.LastFMSession != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func ArtistGetInfo(apiKey string, artist *db.Artist) (Artist, error) {
|
|
||||||
params := url.Values{}
|
|
||||||
params.Add("method", "artist.getInfo")
|
|
||||||
params.Add("api_key", apiKey)
|
|
||||||
params.Add("artist", artist.Name)
|
|
||||||
resp, err := makeRequest("GET", params)
|
|
||||||
if err != nil {
|
|
||||||
return Artist{}, fmt.Errorf("making artist GET: %w", err)
|
|
||||||
}
|
|
||||||
return resp.Artist, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListenBrainzScrobbler struct {
|
|
||||||
DB *db.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lb *ListenBrainzScrobbler) Scrobble(user *db.User, opts ScrobbleOptions) error {
|
|
||||||
listenType := "single"
|
|
||||||
if !opts.Submission {
|
|
||||||
listenType = "playing_now"
|
|
||||||
}
|
|
||||||
scrobble := ListenBrainzScrobble{
|
|
||||||
ListenType: listenType,
|
|
||||||
Payload: []ListenBrainzPayload{{
|
|
||||||
ListenedAt: opts.StampMili / 1e3,
|
|
||||||
TrackMetadata: ListenBrainzTrackMetadata{
|
|
||||||
AdditionalInfo: ListenBrainzAdditionalInfo{
|
|
||||||
TrackNumber: opts.Track.TagTrackNumber,
|
|
||||||
},
|
|
||||||
ArtistName: opts.Track.TagTrackArtist,
|
|
||||||
TrackName: opts.Track.TagTitle,
|
|
||||||
ReleaseName: opts.Track.Album.TagTitle,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
payloadBuf := bytes.Buffer{}
|
|
||||||
if err := json.NewEncoder(&payloadBuf).Encode(scrobble); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req, _ := http.NewRequest("POST", lbBaseURL+"/1/submit-listens", &payloadBuf)
|
|
||||||
req.Header.Add("Authorization", "Token "+user.ListenBrainzSession)
|
|
||||||
res, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if res.StatusCode == http.StatusUnauthorized {
|
|
||||||
return fmt.Errorf("unathorized error scrobbling to listenbrainz %w",
|
|
||||||
ErrListenBrainz)
|
|
||||||
}
|
|
||||||
res.Body.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lb *ListenBrainzScrobbler) Enabled(user *db.User) bool {
|
|
||||||
return user.ListenBrainzSession != ""
|
|
||||||
}
|
|
||||||
90
server/scrobble/listenbrainz/listenbrainz.go
Normal file
90
server/scrobble/listenbrainz/listenbrainz.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package listenbrainz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.senan.xyz/gonic/server/db"
|
||||||
|
"go.senan.xyz/gonic/server/scrobble"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
baseURL = "https://api.listenbrainz.org"
|
||||||
|
submitPath = "/1/submit-listens"
|
||||||
|
|
||||||
|
listenTypeSingle = "single"
|
||||||
|
listenTypePlayingNow = "playing_now"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrListenBrainz = errors.New("listenbrainz error")
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdditionalInfo struct {
|
||||||
|
TrackNumber int `json:"tracknumber"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrackMetadata struct {
|
||||||
|
AdditionalInfo AdditionalInfo `json:"additional_info"`
|
||||||
|
ArtistName string `json:"artist_name"`
|
||||||
|
TrackName string `json:"track_name"`
|
||||||
|
ReleaseName string `json:"release_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Payload struct {
|
||||||
|
ListenedAt int `json:"listened_at"`
|
||||||
|
TrackMetadata TrackMetadata `json:"track_metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Scrobble struct {
|
||||||
|
ListenType string `json:"listen_type"`
|
||||||
|
Payload []Payload `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Scrobbler struct{}
|
||||||
|
|
||||||
|
func (s *Scrobbler) Scrobble(user *db.User, track *db.Track, stampMili int, submission bool) error {
|
||||||
|
if user.ListenBrainzSession == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
payload := Payload{
|
||||||
|
ListenedAt: stampMili / 1e3,
|
||||||
|
TrackMetadata: TrackMetadata{
|
||||||
|
AdditionalInfo: AdditionalInfo{
|
||||||
|
TrackNumber: track.TagTrackNumber,
|
||||||
|
},
|
||||||
|
ArtistName: track.TagTrackArtist,
|
||||||
|
TrackName: track.TagTitle,
|
||||||
|
ReleaseName: track.Album.TagTitle,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
scrobble := Scrobble{
|
||||||
|
ListenType: listenTypeSingle,
|
||||||
|
Payload: []Payload{payload},
|
||||||
|
}
|
||||||
|
if !submission {
|
||||||
|
scrobble.ListenType = listenTypePlayingNow
|
||||||
|
}
|
||||||
|
payloadBuf := bytes.Buffer{}
|
||||||
|
if err := json.NewEncoder(&payloadBuf).Encode(scrobble); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
submitURL := fmt.Sprintf("%s%s", baseURL, submitPath)
|
||||||
|
authHeader := fmt.Sprintf("Token %s", user.ListenBrainzSession)
|
||||||
|
req, _ := http.NewRequest("POST", submitURL, &payloadBuf)
|
||||||
|
req.Header.Add("Authorization", authHeader)
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("http post: %w", err)
|
||||||
|
}
|
||||||
|
if res.StatusCode == http.StatusUnauthorized {
|
||||||
|
return fmt.Errorf("unathorized error scrobbling to listenbrainz %w", ErrListenBrainz)
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ scrobble.Scrobbler = (*Scrobbler)(nil)
|
||||||
9
server/scrobble/scrobble.go
Normal file
9
server/scrobble/scrobble.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package scrobble
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.senan.xyz/gonic/server/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Scrobbler interface {
|
||||||
|
Scrobble(user *db.User, track *db.Track, stampMili int, submission bool) error
|
||||||
|
}
|
||||||
@@ -17,8 +17,10 @@ import (
|
|||||||
"go.senan.xyz/gonic/server/ctrlsubsonic"
|
"go.senan.xyz/gonic/server/ctrlsubsonic"
|
||||||
"go.senan.xyz/gonic/server/db"
|
"go.senan.xyz/gonic/server/db"
|
||||||
"go.senan.xyz/gonic/server/jukebox"
|
"go.senan.xyz/gonic/server/jukebox"
|
||||||
"go.senan.xyz/gonic/server/lastfm"
|
|
||||||
"go.senan.xyz/gonic/server/scanner"
|
"go.senan.xyz/gonic/server/scanner"
|
||||||
|
"go.senan.xyz/gonic/server/scrobble"
|
||||||
|
"go.senan.xyz/gonic/server/scrobble/lastfm"
|
||||||
|
"go.senan.xyz/gonic/server/scrobble/listenbrainz"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@@ -63,11 +65,9 @@ func New(opts Options) *Server {
|
|||||||
sessDB.SessionOpts.SameSite = http.SameSiteLaxMode
|
sessDB.SessionOpts.SameSite = http.SameSiteLaxMode
|
||||||
//
|
//
|
||||||
ctrlAdmin := ctrladmin.New(base, sessDB)
|
ctrlAdmin := ctrladmin.New(base, sessDB)
|
||||||
lastfmScrobbler := &lastfm.LastfmScrobbler{DB: opts.DB}
|
scrobblers := []scrobble.Scrobbler{
|
||||||
listenbrainzScrobbler := &lastfm.ListenBrainzScrobbler{DB: opts.DB}
|
&lastfm.Scrobbler{DB: opts.DB},
|
||||||
scrobblers := []lastfm.Scrobbler{
|
&listenbrainz.Scrobbler{},
|
||||||
lastfmScrobbler,
|
|
||||||
listenbrainzScrobbler,
|
|
||||||
}
|
}
|
||||||
ctrlSubsonic := &ctrlsubsonic.Controller{
|
ctrlSubsonic := &ctrlsubsonic.Controller{
|
||||||
Controller: base,
|
Controller: base,
|
||||||
|
|||||||
Reference in New Issue
Block a user