You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

274 lines
7.4 KiB

package main
import (
"bytes"
"context"
"crawshaw.io/sqlite/sqlitex"
"encoding/json"
"errors"
"flag"
"golang.org/x/crypto/acme/autocert"
"html/template"
"io"
"log"
"net/http"
"strconv"
"time"
)
var databasePath = flag.String("database", "./data.sqlite3", "database path")
var pageTemplate = `{{ define "base" }}
<!DOCTYPE html>
<html lang='en'>
<head>
<title>etascrobbler</title>
<link rel='stylesheet' type='text/css' href='//eta.st/assets/webfonts/stylesheet.css'>
<link rel='stylesheet' type='text/css' href='//eta.st/css/main.css'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
</head>
<header class='site-header'>
<div class='wrapper'>
<a class='site-title' href='/'>
<img src='//eta.st/assets/img/logo.svg' alt='η'>(eta)
</a>
<nav class='site-nav'>
<div class='trigger'>etascrobbler</div>
</nav>
</div>
</header>
<div class='page-content'>
<div class='wrapper'>
<h1>now playing</h1>
<p>
This shows what I was last listening to on any of my devices, and the song before that. This tiny webapp has observed <b>{{ .TotalSongs }}</b>
songs being played so far.
</p>
<p>This site is very beta. More information should be added shortly!</p>
{{if .SomethingPlaying}}
<div class="book-meta">
<p class="book-title">🎵 {{ .CurrentTrack.Artist }}{{ .CurrentTrack.Name }}
</div>
{{end}}
<p>
<i>Previously: </i>
{{ .PreviousTrack.Artist }}{{ .PreviousTrack.Name }}
</p>
<p class='detail'>
Artists (<strong>{{ .TotalArtists }}</strong>): {{range .Artists}}{{.}} &middot; {{end}}
</p>
</div>
</div>
</html>
{{ end }}
`
var tmpl *template.Template = template.Must(template.New("base").Parse(pageTemplate))
var dbpool *sqlitex.Pool
type Scrobble struct {
Artist string `json:"artist"`
Name string `json:"track"`
Ts time.Time
}
var currentScrobble *Scrobble
func getLastScrobble() (s *Scrobble, err error) {
conn := dbpool.Get(context.Background())
if conn == nil {
err = errors.New("no connection whoopsie")
return s, err
}
defer dbpool.Put(conn)
stmt := conn.Prep("SELECT artist, title FROM plays ORDER BY ts DESC LIMIT 1")
hasRow, err := stmt.Step()
if hasRow {
s = &Scrobble{
Artist: stmt.GetText("artist"),
Name: stmt.GetText("title"),
// FIXME(eta): lol don't actually set `ts` because who cares
}
stmt.Step()
return s, nil
} else {
return s, nil
}
}
func getAllArtists() (artists []string, err error) {
conn := dbpool.Get(context.Background())
if conn == nil {
err = errors.New("no connection whoopsie")
return nil, err
}
defer dbpool.Put(conn)
artists = make([]string, 0)
stmt := conn.Prep("SELECT DISTINCT artist FROM plays ORDER BY artist ASC")
for {
hasRow, err := stmt.Step()
if err != nil {
return nil, err
} else if hasRow {
artists = append(artists, stmt.GetText("artist"))
} else {
break
}
}
return artists, nil
}
func countAllScrobbles() (count int64, err error) {
conn := dbpool.Get(context.Background())
if conn == nil {
err = errors.New("no connection whoopsie")
return -1, err
}
defer dbpool.Put(conn)
stmt := conn.Prep("SELECT COUNT(*) AS c FROM plays")
hasRow, err := stmt.Step()
if hasRow {
count = stmt.GetInt64("c")
stmt.Step()
return count, nil
} else {
err = errors.New("wut")
return -1, nil
}
}
type ListenBrainzPayload struct {
ListenedAt interface{} `json:"listened_at"`
TrackMetadata struct {
ArtistName string `json:"artist_name"`
ReleaseName string `json:"release_name"`
TrackName string `json:"track_name"`
} `json:"track_metadata"`
}
func (payload *ListenBrainzPayload) fudgeListenedAt() int64 {
data, ok := payload.ListenedAt.(string)
if ok {
ret, _ := strconv.Atoi(data)
return int64(ret)
}
data2, ok := payload.ListenedAt.(int64)
if ok {
return data2
}
return 0
}
func insertTrack(track ListenBrainzPayload) (err error) {
conn := dbpool.Get(context.Background())
if conn == nil {
err = errors.New("no connection whoopsie")
return
}
defer dbpool.Put(conn)
stmt := conn.Prep("INSERT INTO plays (artist, title, ts) VALUES (?, ?, ?)")
stmt.BindText(1, track.TrackMetadata.ArtistName)
stmt.BindText(2, track.TrackMetadata.TrackName)
stmt.BindInt64(3, track.fudgeListenedAt())
_, err = stmt.Step()
return
}
type SubmitListens struct {
ListenType string `json:"listen_type"`
Payload []ListenBrainzPayload `json:"payload"`
}
func validateToken(w http.ResponseWriter, r *http.Request) {
log.Printf("validate token")
r.Header.Set("Content-Type", "application/json")
w.Write([]byte(`{"code": 200, "message": "wiggly donkers", "valid": true, "user": "eta"}`))
}
func submitListen(w http.ResponseWriter, r *http.Request) {
var payload SubmitListens
body, err := io.ReadAll(r.Body)
if err != nil {
panic(err)
}
log.Printf("submit listen: %v", string(body))
err = json.NewDecoder(bytes.NewReader(body)).Decode(&payload)
if err != nil {
panic(err)
}
log.Printf("parsed: %#v\n", payload)
if payload.ListenType == "playing_now" {
currentScrobble = &Scrobble{
Artist: payload.Payload[0].TrackMetadata.ArtistName,
Name: payload.Payload[0].TrackMetadata.TrackName,
Ts: time.Now(),
}
log.Printf("now playing: %v — %v", currentScrobble.Artist, currentScrobble.Name)
} else if payload.ListenType == "single" || payload.ListenType == "import" {
log.Printf("submission of type %v", payload.ListenType)
for _, item := range payload.Payload {
log.Printf("inserting: %v — %v", item.TrackMetadata.ArtistName, item.TrackMetadata.TrackName)
err := insertTrack(item)
if err != nil {
panic(err)
}
}
}
r.Header.Set("Content-Type", "application/json")
w.Write([]byte(`{"status": "ok"}`))
}
func nowPlayingJson(w http.ResponseWriter, r *http.Request) {
log.Printf("now playing")
if currentScrobble == nil {
http.NotFound(w, r)
return
}
if time.Since(currentScrobble.Ts) > 30*time.Minute {
http.NotFound(w, r)
return
}
r.Header.Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(*currentScrobble)
}
func rootPage(w http.ResponseWriter, r *http.Request) {
log.Printf("root page %v", r.URL.String())
pageData := make(map[string]interface{})
var err error
pageData["TotalSongs"], err = countAllScrobbles()
if err != nil {
log.Fatalf("failed to count scrobbles: %v", err)
}
artists, err := getAllArtists()
if err != nil {
log.Fatalf("failed to get artists: %v", err)
}
pageData["Artists"] = artists
pageData["TotalArtists"] = len(artists)
pageData["PreviousTrack"], err = getLastScrobble()
if err != nil {
log.Fatalf("failed to get last scrobble: %v", err)
}
pageData["SomethingPlaying"] = currentScrobble != nil
if currentScrobble != nil {
pageData["CurrentTrack"] = *currentScrobble
}
tmpl.ExecuteTemplate(w, "base", pageData)
}
func main() {
flag.Parse()
var err error
dbpool, err = sqlitex.Open(*databasePath, 0, 10)
if err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
mux.HandleFunc("/", rootPage)
mux.HandleFunc("/now-playing.json", nowPlayingJson)
mux.HandleFunc("/1/validate-token", validateToken)
mux.HandleFunc("/1/submit-listens", submitListen)
log.Fatal(http.Serve(autocert.NewListener("etascrobbler.i.eta.st"), mux))
}