178 lines
4.4 KiB
Go
178 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var lbToken string
|
|
var plexUsername string
|
|
var plexLibsStr string
|
|
var listen = ":9102"
|
|
|
|
func main() {
|
|
|
|
lbToken = os.Getenv("PB_LISTENBRAINZ_USER_TOKEN")
|
|
plexUsername = os.Getenv("PB_PLEX_USERNAME")
|
|
plexLibsStr = os.Getenv("PB_PLEX_LIBRARIES")
|
|
debug, _ := strconv.ParseBool(os.Getenv("PB_DEBUG"))
|
|
|
|
if lbToken == "" {
|
|
slog.Error("you must set PB_LISTENBRAINZ_USER_TOKEN - see https://listenbrainz.org/settings/")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if plexUsername == "" {
|
|
slog.Error("you must set PB_PLEX_USERNAME to the user who's listens will be recorded")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if plexLibsStr == "" {
|
|
slog.Error("you must set PB_PLEX_LIBRARIES to a comma separated list of plex libraries which will be scrobbled")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if debug {
|
|
slog.SetLogLoggerLevel(slog.LevelDebug)
|
|
}
|
|
|
|
plexLibs := strings.Split(plexLibsStr, ",")
|
|
|
|
http.HandleFunc("POST /plex", func(w http.ResponseWriter, r *http.Request) {
|
|
err := r.ParseMultipartForm(1 * 1024 * 1024)
|
|
if err != nil {
|
|
slog.Error("error parsing multipart form", "error", err.Error())
|
|
return
|
|
}
|
|
|
|
pl := r.FormValue("payload")
|
|
event := webhookEvent{}
|
|
err = json.Unmarshal([]byte(pl), &event)
|
|
|
|
if err != nil {
|
|
slog.Error("error parsing JSON", "error", err.Error())
|
|
w.WriteHeader(400)
|
|
w.Write([]byte("could not parse your JSON"))
|
|
return
|
|
}
|
|
|
|
slog.Debug("received event", "event", event)
|
|
// slog.Printf("account name: %s", event.Account.Title)
|
|
// slog.Printf("title: %s", event.Metadata.Title)
|
|
// slog.Printf("album: %s", event.Metadata.ParentTitle)
|
|
// slog.Printf("artist: %s", event.Metadata.GrandparentTitle)
|
|
// slog.Printf("library: %s", event.Metadata.LibrarySectionTitle)
|
|
|
|
// 2025/10/19 09:21:31 event: media.play
|
|
// 2025/10/19 09:21:31 account name: username
|
|
// 2025/10/19 09:21:31 title: Face to Face (Cosmo Vitelli remix)
|
|
// 2025/10/19 09:21:31 album: Daft Club
|
|
// 2025/10/19 09:21:31 artist: Daft Punk
|
|
// 2025/10/19 09:21:31 library: Music
|
|
if event.Account.Title != plexUsername {
|
|
slog.Error("not scrobbling for this user", "user", event.Account.Title)
|
|
w.WriteHeader(200)
|
|
w.Write([]byte("ok"))
|
|
return
|
|
}
|
|
|
|
if event.Event == "media.scrobble" {
|
|
found := false
|
|
for i := range plexLibs {
|
|
if event.Metadata.LibrarySectionTitle == plexLibs[i] {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
slog.Debug("not scrobbling for this library", "library", event.Metadata.LibrarySectionTitle)
|
|
slog.Debug("does not match a configured library", "libraries", plexLibsStr)
|
|
w.WriteHeader(200)
|
|
w.Write([]byte("ok"))
|
|
return
|
|
}
|
|
|
|
err = lbSubmit(event.Metadata.Title, event.Metadata.GrandparentTitle, event.Metadata.ParentTitle)
|
|
if err != nil {
|
|
slog.Error("error submitting to listenbrainz", "error", err.Error())
|
|
w.WriteHeader(400)
|
|
w.Write([]byte("could not submit to listenbrainz"))
|
|
return
|
|
} else {
|
|
slog.Info("scrobbled play", "grandparent", event.Metadata.GrandparentTitle, "title", event.Metadata.Title)
|
|
w.WriteHeader(200)
|
|
w.Write([]byte("ok"))
|
|
return
|
|
}
|
|
} else {
|
|
slog.Debug("non scrobble event", "event", event.Event)
|
|
w.WriteHeader(200)
|
|
w.Write([]byte("ok"))
|
|
return
|
|
}
|
|
|
|
})
|
|
slog.Info("starting web server", "listen", listen)
|
|
slog.Error(http.ListenAndServe(listen, nil).Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
func lbSubmit(title, artist, album string) error {
|
|
c := http.Client{}
|
|
|
|
c.Timeout = time.Second * 10
|
|
url := "https://api.listenbrainz.org/1/submit-listens"
|
|
|
|
b := bytes.Buffer{}
|
|
pl := lbListen{
|
|
ListenType: "single",
|
|
Payload: []lbListenPayload{{
|
|
ListenedAt: time.Now().Unix(),
|
|
TrackMetadata: lbTrackMetadata{
|
|
ArtistName: artist,
|
|
TrackName: title,
|
|
ReleaseName: album,
|
|
},
|
|
}},
|
|
}
|
|
|
|
err := json.NewEncoder(&b).Encode(pl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(b.Bytes()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", fmt.Sprintf("Token %s", lbToken))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
res, err := c.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode != 200 {
|
|
errB := bytes.Buffer{}
|
|
io.Copy(&errB, res.Body)
|
|
return fmt.Errorf("got non-200 response: %d - body: %s", res.StatusCode, errB.String())
|
|
}
|
|
|
|
// happy path
|
|
resB := bytes.Buffer{}
|
|
io.Copy(&resB, res.Body)
|
|
slog.Debug("listenbrainz OK", "response", resB.String())
|
|
|
|
return nil
|
|
}
|