summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--TODO.txt5
-rw-r--r--data.go113
-rw-r--r--main.go161
-rw-r--r--migration.sql17
-rw-r--r--modern.go90
-rw-r--r--schema.sql25
-rw-r--r--utils.go18
7 files changed, 429 insertions, 0 deletions
diff --git a/TODO.txt b/TODO.txt
new file mode 100644
index 0000000..46339e9
--- /dev/null
+++ b/TODO.txt
@@ -0,0 +1,5 @@
+Old Api
+=======
+
+* now playing
+* persist sessions to disk
diff --git a/data.go b/data.go
new file mode 100644
index 0000000..7cdccca
--- /dev/null
+++ b/data.go
@@ -0,0 +1,113 @@
+package main
+
+import (
+ "database/sql"
+ "encoding/xml"
+ "fmt"
+ "time"
+
+ _ "github.com/mattn/go-sqlite3"
+)
+
+type Scrobble struct {
+ Artist string
+ AlbumArtist string
+ TrackName string
+ Album string
+ TrackNumber int
+ Duration int
+ Time int
+ Chosen bool
+ Mbid string
+ Session string
+}
+
+type Session struct {
+ XMLName xml.Name `json:"-" xml:"session"`
+ User string `json:"name" xml:"name"`
+ Key string `json:"key" xml:"key"`
+ Client string
+ Protocol string
+ Created int64
+ Subscriber int64 `json:"subscriber" xml:"subscriber"`
+}
+
+type DataStore interface {
+ PutSession(*Session)
+ GetSession(key string) (*Session, bool)
+ GetPassword(userName string) (string, bool)
+ PutScrobbles([]Scrobble)
+ Api
+}
+
+type SqlStore struct {
+ *sql.DB
+}
+
+func NewSqlStore() *SqlStore {
+ db, err := sql.Open("sqlite3", "./test.db")
+ if err != nil {
+ fmt.Println(err)
+ }
+ err = db.Ping()
+ if err != nil {
+ fmt.Println(err)
+ }
+ return &SqlStore{db}
+}
+
+func NewSession(user string, client string, protocol string) *Session {
+ return &Session{
+ User: user,
+ Key: randomToken(16),
+ Client: client,
+ Protocol: protocol,
+ Created: time.Now().Unix(),
+ }
+}
+
+func (store *SqlStore) PutSession(s *Session) {
+ store.Exec("INSERT INTO sessions VALUES (?, ?, ?, ?, ?)",
+ s.User, s.Key, s.Client, s.Protocol, s.Created)
+}
+
+func (store *SqlStore) GetSession(key string) (*Session, bool) {
+ s := &Session{}
+ row := store.QueryRow("SELECT * FROM sessions WHERE key = ?", key)
+ err := row.Scan(&s.User, &s.Key, &s.Client, &s.Protocol, &s.Created)
+ if err == sql.ErrNoRows {
+ return s, false
+ } else if err != nil {
+ fmt.Println(err)
+ }
+ return s, true
+}
+
+func (store *SqlStore) GetPassword(name string) (string, bool) {
+ var password string
+ row := store.QueryRow("SELECT password FROM users WHERE name = ?", name)
+ err := row.Scan(&password)
+ if err == sql.ErrNoRows {
+ return password, false
+ } else if err != nil {
+ fmt.Println(err)
+ }
+ return password, true
+}
+
+func (store *SqlStore) PutScrobbles(scrobbles []Scrobble) {
+ for _, s := range scrobbles {
+ store.Exec("INSERT INTO scrobbles VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ s.Artist,
+ s.AlbumArtist,
+ s.TrackName,
+ s.Album,
+ s.TrackNumber,
+ s.Duration,
+ s.Time,
+ s.Chosen,
+ s.Mbid,
+ s.Session,
+ )
+ }
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..786263d
--- /dev/null
+++ b/main.go
@@ -0,0 +1,161 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "sort"
+ "strconv"
+ "time"
+)
+
+func mainHandler(ds DataStore, w http.ResponseWriter, r *http.Request) {
+ r.ParseForm()
+ if _, ok := r.Form["hs"]; ok {
+
+ protocol := r.FormValue("p")
+ if protocol != "1.2.1" && protocol != "1.2" {
+ fmt.Fprintf(w, "FAILED Protocol mismatch\n")
+ return
+ }
+
+ timestamp := r.FormValue("t")
+ ts, err := strconv.ParseInt(timestamp, 10, 0)
+ if err != nil {
+ fmt.Fprintf(w, "FAILED Invalid timestamp\n")
+ return
+ }
+
+ delta := time.Now().Unix() - ts
+ if delta > 30 || delta < -30 {
+ fmt.Fprintf(w, "BADTIME\n")
+ return
+ }
+
+ user := r.FormValue("u")
+ auth := r.FormValue("a")
+ password, ok := ds.GetPassword(user)
+
+ if (md5hex(password+timestamp) != auth) || !ok {
+ fmt.Fprintf(w, "BADAUTH\n")
+ return
+ }
+
+ client := r.FormValue("c")
+ s := NewSession(user, client, protocol)
+ ds.PutSession(s)
+ fmt.Fprint(w, "OK\n")
+ fmt.Fprintf(w, "%s\n", s.Key)
+ fmt.Fprint(w, "http://post.audioscrobbler.com:80/np\n")
+ fmt.Fprint(w, "http://post.audioscrobbler.com:80/scrobble\n")
+ } else {
+ fmt.Fprintf(w, "<html>This is an endpoint, see <a href=\"http://www.last.fm/api/submissions\">here</a></html>")
+ }
+}
+
+func parseValues(values url.Values) map[int]url.Values {
+ parts := make(map[int]url.Values)
+ var letter string
+ var idx int
+ for key, value := range values {
+ _, err := fmt.Sscanf(key, "%1s[%d]", &letter, &idx)
+ if err != nil {
+ continue
+ }
+ if _, ok := parts[idx]; !ok {
+ parts[idx] = make(url.Values)
+ }
+ parts[idx][letter] = value
+ }
+ return parts
+}
+
+func parsePart(values url.Values) (Scrobble, error) {
+ var scrobble Scrobble
+ scrobble.TrackName = values.Get("t")
+ scrobble.Artist = values.Get("a")
+ time, err := strconv.Atoi(values.Get("i"))
+ if err != nil {
+ return scrobble, errors.New("Could not parse timestamp")
+ }
+ scrobble.Time = time
+ scrobble.Album = values.Get("b")
+ scrobble.Mbid = values.Get("m")
+ tn, err := strconv.Atoi(values.Get("n"))
+ if err != nil {
+ return scrobble, errors.New("Could not parse track number")
+ }
+ scrobble.TrackNumber = tn
+ duration, err := strconv.Atoi(values.Get("l"))
+ if err != nil {
+ return scrobble, errors.New("Could not parse duration")
+ }
+ scrobble.Duration = duration
+ chosen := values.Get("o")
+ if chosen == "P" || chosen == "" {
+ scrobble.Chosen = true
+ }
+ return scrobble, nil
+}
+
+func parseScrobbles(values url.Values, session *Session) []Scrobble {
+ scrobbles := make([]Scrobble, 0, 1)
+ parts := parseValues(values)
+ keys := make([]int, len(parts))
+ i := 0
+ for key := range parts {
+ keys[i] = key
+ i++
+ }
+ sort.Ints(keys)
+ for _, key := range keys {
+ scrobble, err := parsePart(parts[key])
+ if err != nil {
+ continue
+ }
+ scrobble.Session = session.Key
+ scrobbles = append(scrobbles, scrobble)
+ }
+
+ return scrobbles
+}
+
+func scrobbleHandler(ds DataStore, w http.ResponseWriter, r *http.Request) {
+ if session, ok := ds.GetSession(r.FormValue("s")); ok {
+ scrobbles := parseScrobbles(r.Form, session)
+ ds.PutScrobbles(scrobbles)
+ fmt.Fprintf(w, "OK\n")
+ } else {
+ fmt.Fprintf(w, "BADSESSION\n")
+ }
+}
+
+func nowPlayingHandler(ds DataStore, w http.ResponseWriter, r *http.Request) {
+ if _, ok := ds.GetSession(r.FormValue("s")); ok {
+ fmt.Fprintf(w, "OK\n")
+ } else {
+ fmt.Fprintf(w, "BADSESSION\n")
+ }
+}
+
+type handler func(DataStore, http.ResponseWriter, *http.Request)
+
+func wrap(ds DataStore, fn handler) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ d, _ := httputil.DumpRequest(r, true)
+ fmt.Printf("--------------------------\n%s\n", d)
+ r.ParseForm()
+ fn(ds, w, r)
+ }
+}
+
+func main() {
+ store := NewSqlStore()
+ http.HandleFunc("/", wrap(store, handler(mainHandler)))
+ http.HandleFunc("/np", wrap(store, nowPlayingHandler))
+ http.HandleFunc("/scrobble", wrap(store, scrobbleHandler))
+ http.HandleFunc("/2.0/", wrap(store, ApiHandler))
+ http.ListenAndServe(":3001", nil)
+}
diff --git a/migration.sql b/migration.sql
new file mode 100644
index 0000000..cd95209
--- /dev/null
+++ b/migration.sql
@@ -0,0 +1,17 @@
+BEGIN;
+
+ CREATE TABLE new_sessions (
+ user string,
+ key string PRIMARY KEY,
+ client string,
+ protocol string,
+ created int
+ );
+
+ INSERT INTO new_sessions SELECT * FROM sessions;
+
+ DROP TABLE sessions;
+
+ ALTER TABLE new_sessions RENAME TO sessions;
+
+END;
diff --git a/modern.go b/modern.go
new file mode 100644
index 0000000..4a95f32
--- /dev/null
+++ b/modern.go
@@ -0,0 +1,90 @@
+package main
+
+import (
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+ "net/http"
+)
+
+type Api interface {
+ AuthGetToken(*http.Request) ApiResponse
+ AuthGetSession(*http.Request) ApiResponse
+ AuthGetMobileSession(*http.Request) ApiResponse
+}
+
+type ApiResponse interface {
+}
+
+type Name struct {
+ XMLName xml.Name
+}
+
+type Token struct {
+ Name
+ string
+}
+
+type Scrobbles struct {
+ XMLName xml.Name `xml:"scrobbles"`
+ Scrobbles []Scrobble
+}
+
+type Scrobble struct {
+ Track Track `xml:"track"`
+}
+
+type Track struct {
+ Corrected int `xml:"corrected,attr"`
+ Name string `xml:",chardata"`
+}
+
+func (n Name) getName() string {
+ return n.XMLName.Local
+}
+
+func (store *SqlStore) AuthGetToken(r *http.Request) ApiResponse {
+ token := randomToken(16)
+ response := struct {
+ Token string `xml:"token" json:"token"`
+ }{Token: token}
+ return response
+}
+
+func (store *SqlStore) AuthGetMobileSession(r *http.Request) ApiResponse {
+ return struct{}{}
+}
+
+func (store *SqlStore) AuthGetSession(r *http.Request) ApiResponse {
+ var response struct {
+ Session *Session `json:"session"`
+ }
+ session := NewSession("thibauthorel", r.FormValue("api_key"), "2.0")
+ store.PutSession(session)
+ response.Session = session
+ return response
+}
+
+func (store *SqlStore) TrackScrobble(r *http.Request) ApiResponse {
+ return struct{}{}
+}
+
+func ApiHandler(ds DataStore, w http.ResponseWriter, r *http.Request) {
+ method := r.FormValue("method")
+ var response ApiResponse
+ if method == "auth.getToken" {
+ response = ds.AuthGetToken(r)
+ } else if method == "auth.getSession" {
+ response = ds.AuthGetSession(r)
+ }
+
+ var text []byte
+ switch r.FormValue("format") {
+ case "json":
+ text, _ = json.Marshal(response)
+ default:
+ text, _ = xml.Marshal(response)
+ }
+ fmt.Printf("%s\n", text)
+ w.Write(text)
+}
diff --git a/schema.sql b/schema.sql
new file mode 100644
index 0000000..d087f58
--- /dev/null
+++ b/schema.sql
@@ -0,0 +1,25 @@
+CREATE TABLE users (
+ name string PRIMARY KEY,
+ password string
+);
+
+CREATE TABLE sessions (
+ user string,
+ token string PRIMARY KEY,
+ client string,
+ protocol string,
+ created int
+);
+
+CREATE TABLE scrobbles (
+ artist string,
+ albumartist string,
+ trackname string,
+ album string,
+ tracknumber int,
+ duration int,
+ time int,
+ chosen bool,
+ mbid string,
+ session string
+);
diff --git a/utils.go b/utils.go
new file mode 100644
index 0000000..cb29662
--- /dev/null
+++ b/utils.go
@@ -0,0 +1,18 @@
+package main
+
+import (
+ "crypto/md5"
+ "crypto/rand"
+ "encoding/hex"
+)
+
+func randomToken(length int) string {
+ b := make([]byte, length)
+ rand.Read(b)
+ return hex.EncodeToString(b)
+}
+
+func md5hex(s string) string {
+ hash := md5.Sum([]byte(s))
+ return hex.EncodeToString(hash[:])
+}