all repos — breadsite @ e2a4208da37b8e81e7033f183b6a381a28665134

Unnamed repository; edit this file 'description' to name the repository.

add a listening now endpoint
confusedbread confuseddbread@gmail.com
Wed, 02 Jul 2025 18:52:03 +0200
commit

e2a4208da37b8e81e7033f183b6a381a28665134

parent

fe16a21226d3887f723ae52947d44c7ae2484cad

5 files changed, 206 insertions(+), 1 deletions(-)

jump to
M .gitignore.gitignore

@@ -1,2 +1,3 @@

*.lock -target+target +.*
A backend/go.mod

@@ -0,0 +1,3 @@

+module main + +go 1.24.4
A backend/listenBrainz.go

@@ -0,0 +1,135 @@

+package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" +) + +type PlayingNow struct { + Payload struct { + Count int `json:"count"` + Listens []struct { + PlayingNow bool `json:"playing_now"` + TrackMetadata struct { + AdditionalInfo struct { + ArtistMbids []any `json:"artist_mbids"` + DurationMs int `json:"duration_ms"` + SubmissionClient string `json:"submission_client"` + SubmissionClientVersion string `json:"submission_client_version"` + Tracknumber int `json:"tracknumber"` + } `json:"additional_info"` + ArtistName string `json:"artist_name"` + ReleaseName string `json:"release_name"` + TrackName string `json:"track_name"` + } `json:"track_metadata"` + } `json:"listens"` + PlayingNow bool `json:"playing_now"` + UserID string `json:"user_id"` + } `json:"payload"` +} + +type Metadata struct { + ArtistCreditName string `json:"artist_credit_name"` + ArtistMbids []string `json:"artist_mbids"` + RecordingMbid string `json:"recording_mbid"` + RecordingName string `json:"recording_name"` + ReleaseMbid string `json:"release_mbid"` + ReleaseName string `json:"release_name"` +} + +type CoverArt struct { + Images []struct { + Types []string `json:"types"` + Front bool `json:"front"` + Back bool `json:"back"` + Edit int `json:"edit"` + Image string `json:"image"` + Comment string `json:"comment"` + Approved bool `json:"approved"` + Thumbnails struct { + // TODO: add interface here to allow for different size variations + Num250 string `json:"250"` + // Num500 string `json:"500"` + // Num1200 string `json:"1200"` + // Large string `json:"large"` + // Small string `json:"small"` + } `json:"thumbnails"` + ID int64 `json:"id"` + } `json:"images"` + Release string `json:"release"` +} + +func getPlayingNow(userName string) (PlayingNow, error) { + url := fmt.Sprintf("https://api.listenbrainz.org/1/user/%s/playing-now", url.QueryEscape(userName)) + res, err := http.Get(url) + if err != nil { + return PlayingNow{}, err + } + + body, err := io.ReadAll(res.Body) + if err != nil { + log.Printf("body: %s\nerror: %s\n",url, err) + return PlayingNow{}, err + } + + var playingNow PlayingNow + if err := json.Unmarshal(body, &playingNow); err != nil { + log.Printf("url: %s\nbody: %s\nerror: %s\n",url, body, err) + return PlayingNow{}, err + } + + return playingNow, nil +} + +func getMetadata(artistName string, recordingName string, releaseName string) (Metadata, error) { + url := fmt.Sprintf( + "https://api.listenbrainz.org/1/metadata/lookup/?artist_name=%s&recording_name=%s&release_name=%s&metadata=false&inc=release_mbid", + url.QueryEscape(artistName), + url.QueryEscape(recordingName), + url.QueryEscape(releaseName), + ) + res, err := http.Get(url) + if err != nil { + return Metadata{}, err + } + + body, err := io.ReadAll(res.Body) + if err != nil { + log.Printf("body: %s\nerror: %s\n",url, err) + return Metadata{}, err + } + + var metadata Metadata + if err := json.Unmarshal(body, &metadata); err != nil { + log.Printf("url: %s\nbody: %s\nerror: %s\n",url, body, err) + return Metadata{}, err + } + + return metadata, nil +} + +func getCover(releaseMbid string) (CoverArt, error) { + url := fmt.Sprintf("https://coverartarchive.org/release/%s", url.QueryEscape(releaseMbid)) + res, err := http.Get(url) + if err != nil { + return CoverArt{}, err + } + + body, err := io.ReadAll(res.Body) + if err != nil { + log.Printf("body: %s\nerror: %s\n",url, err) + return CoverArt{}, err + } + + var coverArt CoverArt + if err := json.Unmarshal(body, &coverArt); err != nil { + log.Printf("url: %s\nbody: %s\nerror: %s\n",url, body, err) + return CoverArt{}, err + } + + return coverArt, nil +}
A backend/main.go

@@ -0,0 +1,48 @@

+package main + +import ( + "fmt" + "log" + "net/http" + "reflect" +) + +type PlayingInfo struct { + artistName string + recordingName string + releaseName string + recordingUrl string + coverUrl string +} + +func main() { + playingInfo := PlayingInfo{} + + playingNow, err := getPlayingNow("confuseddbread") + if err != nil { + panic(err) + } + trackMetadata := playingNow.Payload.Listens[0].TrackMetadata + playingInfo.artistName = trackMetadata.ArtistName + playingInfo.recordingName = trackMetadata.TrackName + playingInfo.releaseName = trackMetadata.ReleaseName + + metadata, err := getMetadata(trackMetadata.ArtistName, trackMetadata.TrackName, trackMetadata.ReleaseName) + if err != nil { + log.Fatalf("error: %s\n", err) + } + playingInfo.recordingUrl = fmt.Sprintf("https://musicbrainz.org/recording/%s", metadata.RecordingMbid) + + coverArt, err := getCover(metadata.ReleaseMbid) + playingInfo.coverUrl = coverArt.Images[0].Image + + v := reflect.ValueOf(playingInfo) + k := v.Type() + for i := range v.NumField() { + log.Printf("%s: %s\n", k.Field(i).Name, v.Field(i)) + } + + http.HandleFunc("/", playingInfo.handler) + log.Println("http://localhost:8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +}
A backend/net.go

@@ -0,0 +1,18 @@

+package main + +import ( + "fmt" + "net/http" +) + +func (playingInfo *PlayingInfo) handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf( + w, + "<p>%s</p><p>%s</p><p>%s</p><a href=\"%s\"><img src=\"%s\"></a>", + playingInfo.artistName, + playingInfo.recordingName, + playingInfo.releaseName, + playingInfo.recordingUrl, + playingInfo.coverUrl, + ) +}