diff options
| author | Thibaut Horel <thibaut.horel@gmail.com> | 2016-11-19 17:35:07 -0500 |
|---|---|---|
| committer | Thibaut Horel <thibaut.horel@gmail.com> | 2016-11-19 17:35:07 -0500 |
| commit | c2c39cc2756230c1de29d8065b8b320f2f084045 (patch) | |
| tree | baf3569f25b0fabeb93144d636bf61a2deae6b4a | |
| download | lastfm-api-c2c39cc2756230c1de29d8065b8b320f2f084045.tar.gz | |
Initial commit
| -rw-r--r-- | TODO.txt | 5 | ||||
| -rw-r--r-- | data.go | 113 | ||||
| -rw-r--r-- | main.go | 161 | ||||
| -rw-r--r-- | migration.sql | 17 | ||||
| -rw-r--r-- | modern.go | 90 | ||||
| -rw-r--r-- | schema.sql | 25 | ||||
| -rw-r--r-- | utils.go | 18 |
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 @@ -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, + ) + } +} @@ -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[:]) +} |
