summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apiv1.go58
-rw-r--r--data.go104
-rw-r--r--main.go6
-rw-r--r--modern.go7
-rw-r--r--schema.sql48
-rw-r--r--static/style.css25
-rw-r--r--templates/base.tmpl15
-rw-r--r--templates/index.tmpl15
-rw-r--r--utils.go17
9 files changed, 187 insertions, 108 deletions
diff --git a/apiv1.go b/apiv1.go
index f36f022..892c06b 100644
--- a/apiv1.go
+++ b/apiv1.go
@@ -42,15 +42,25 @@ func (app *App) mainHandler(w http.ResponseWriter, r *http.Request) {
return
}
- client := r.FormValue("c")
- s := NewSession(user, client, protocol)
- app.PutSession(s)
+ s := &Session{
+ User: user,
+ Client: r.FormValue("c"),
+ ClientVersion: r.FormValue("v"),
+ Key: randomToken(16),
+ Protocol: protocol,
+ }
+ if err := app.PutSession(s); err != nil {
+ log.Println(err)
+ fmt.Fprintln(w, "FAILED Couldn't create session")
+ return
+ }
+
fmt.Fprintln(w, "OK")
- fmt.Fprintf(w, "%s\n", s.Key)
+ fmt.Fprintln(w, s.Key)
fmt.Fprintln(w, "http://post.audioscrobbler.com:80/np")
fmt.Fprintln(w, "http://post.audioscrobbler.com:80/scrobble")
} else {
- fmt.Fprintf(w, "<html>This is an endpoint, see <a href=\"http://www.last.fm/api/submissions\">here</a></html>")
+ fmt.Fprintf(w, "api")
}
}
@@ -160,27 +170,33 @@ func parseScrobbles(values url.Values, session *Session) ([]Scrobble, int) {
}
func (app *App) scrobbleHandler(w http.ResponseWriter, r *http.Request) {
- if session, err := app.GetSession(r.FormValue("s")); err != nil {
+ session, err := app.GetSession(r.FormValue("s"))
+ if err != nil {
+ log.Println(err)
fmt.Fprintln(w, "BADSESSION")
- } else {
- scrobbles, _ := parseScrobbles(r.Form, session)
+ return
+ }
+ scrobbles, _ := parseScrobbles(r.Form, session)
- for i, s := range scrobbles {
- t := app.TrackInfo(s.TrackName.Name, s.Artist.Name)
- s.Mbid = t.Mbid
- scrobbles[i] = s
- _, err = app.DB.Exec("INSERT INTO songs VALUES ($1, $2) "+
- "ON CONFLICT (mbid) DO UPDATE SET mbid=$1, image=$2",
- t.Mbid, t.GetImage("medium"))
- if err != nil {
- log.Println(err)
- }
+ for i, s := range scrobbles {
+ t := app.TrackInfo(s.TrackName.Name, s.Artist.Name)
+ s.MbidComp = t.Mbid
+ scrobbles[i] = s
+ _, err = app.DB.Exec("INSERT INTO songs VALUES ($1, $2) "+
+ "ON CONFLICT (mbid) DO UPDATE SET mbid=$1, image=$2",
+ t.Mbid, t.GetImage("medium"))
+ if err != nil {
+ log.Println(err)
}
+ }
- fmt.Printf("%v\n", scrobbles)
- app.PutScrobbles(scrobbles)
- fmt.Fprintln(w, "OK")
+ err = app.PutScrobbles(scrobbles)
+ if err != nil {
+ log.Println(err)
+ fmt.Fprintln(w, "Failed Database")
+ return
}
+ fmt.Fprintln(w, "OK")
}
func (app *App) nowPlayingHandler(w http.ResponseWriter, r *http.Request) {
diff --git a/data.go b/data.go
index 4e28750..318e43e 100644
--- a/data.go
+++ b/data.go
@@ -3,7 +3,6 @@ package main
import (
"database/sql"
"encoding/xml"
- "fmt"
"log"
"time"
@@ -20,25 +19,28 @@ type Scrobble struct {
Time time.Time `xml:"timestamp" json:"timestamp,string"`
Chosen bool `xml:"-" json:"-"`
Mbid string `xml:"-" json:"-"`
- Session string `xml:"-" json:"-"`
+ MbidComp string
+ Session string `xml:"-" json:"-"`
+ User string
Image 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 time.Time
- Subscriber int64 `json:"subscriber" xml:"subscriber"`
+ XMLName xml.Name `json:"-" xml:"session"`
+ User string `json:"name" xml:"name"`
+ Key string `json:"key" xml:"key"`
+ Client string
+ ClientVersion string
+ Protocol string
+ Created time.Time
+ Subscriber int64 `json:"subscriber" xml:"subscriber"`
}
type DataStore interface {
- PutSession(*Session)
+ PutSession(*Session) error
GetSession(key string) (*Session, error)
GetPassword(userName string) (string, error)
- PutScrobbles([]Scrobble)
+ PutScrobbles([]Scrobble) error
RecentScrobbles(lfmName string) []*Scrobble
Api
}
@@ -47,65 +49,67 @@ type SqlStore struct {
*sql.DB
}
-func NewSession(user string, client string, protocol string) *Session {
- return &Session{
- User: user,
- Key: randomToken(16),
- Client: client,
- Protocol: protocol,
- Created: time.Now(),
- }
-}
-
-func (store *SqlStore) PutSession(s *Session) {
- _, err := store.Exec("INSERT INTO scrobbling_sessions VALUES ($1, $2, $3, $4, $5)",
- s.User, s.Key, s.Client, s.Protocol, s.Created)
- if err != nil {
- log.Println(err)
- }
+func (store *SqlStore) PutSession(s *Session) error {
+ query := `
+ INSERT INTO scrobbling_sessions (lfm_name, session_key, client, client_version, protocol)
+ VALUES ($1, $2, $3, $4, $5)`
+ _, err := store.Exec(query, s.User, s.Key, s.Client, s.ClientVersion, s.Protocol)
+ return err
}
func (store *SqlStore) GetSession(key string) (*Session, error) {
+ query := `
+ SELECT lfm_name, session_key, client, client_version, protocol, created
+ FROM scrobbling_sessions WHERE session_key = $1`
+ row := store.QueryRow(query, key)
s := &Session{}
- row := store.QueryRow("SELECT * FROM scrobbling_sessions WHERE key = $1", key)
- err := row.Scan(&s.User, &s.Key, &s.Client, &s.Protocol, &s.Created)
+ err := row.Scan(&s.User, &s.Key, &s.Client, &s.ClientVersion, &s.Protocol,
+ &s.Created)
return s, err
}
func (store *SqlStore) GetPassword(name string) (string, error) {
var password string
- row := store.QueryRow("SELECT lfm_password FROM users WHERE lfm_name = $1", name)
+ row := store.QueryRow("SELECT lfm_password FROM users WHERE lfm_name = $1",
+ name)
err := row.Scan(&password)
return password, err
}
-func (store *SqlStore) PutScrobbles(scrobbles []Scrobble) {
+func (store *SqlStore) PutScrobbles(scrobbles []Scrobble) error {
+ tx, err := store.Begin()
+ if err != nil {
+ return err
+ }
+ query := `
+ INSERT INTO scrobbles
+ (artist, albumartist, trackname, album, tracknumber, duration, time,
+ chosen, mbid, mbid_computed, session_key, lfm_name)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`
+ st, err := tx.Prepare(query)
+ if err != nil {
+ return err
+ }
for _, s := range scrobbles {
- if _, err := store.Exec(
- "INSERT INTO scrobbles VALUES ($1, $2, $3, $4,$5, $6, $7, $8, $9, $10)",
- s.Artist,
- s.AlbumArtist,
- s.TrackName,
- s.Album,
- s.TrackNumber,
- s.Duration,
- s.Time,
- s.Chosen,
- s.Mbid,
- s.Session,
- ); err != nil {
- fmt.Printf("error : %v\n", err)
+ _, err = st.Exec(s.Artist, s.AlbumArtist, s.TrackName, s.Album,
+ s.TrackNumber, s.Duration, s.Time, s.Chosen, s.Mbid, s.MbidComp,
+ s.Session, s.User)
+ if err != nil {
+ tx.Rollback()
+ return err
}
}
+ return tx.Commit()
}
func (store *SqlStore) RecentScrobbles(lfmName string) []*Scrobble {
scrobbles := make([]*Scrobble, 0, 10)
- rows, err := store.Query("SELECT artist, album, trackname, time, image "+
- "FROM scrobbles JOIN scrobbling_sessions ON session = key "+
- "LEFT JOIN songs ON scrobbles.mbid = songs.mbid "+
- "WHERE lfm_name=$1 ORDER BY time DESC LIMIT 10",
- lfmName)
+ query := `
+ SELECT artist, album, trackname, time, image
+ FROM scrobbles
+ LEFT JOIN songs ON COALESCE(scrobbles.mbid, scrobbles.mbid_computed) = songs.mbid
+ WHERE lfm_name=$1 ORDER BY time DESC LIMIT 10`
+ rows, err := store.Query(query, lfmName)
if err != nil {
log.Println(err)
}
diff --git a/main.go b/main.go
index 4065dad..4f34f66 100644
--- a/main.go
+++ b/main.go
@@ -36,7 +36,10 @@ func New() *App {
blockKey, err := hex.DecodeString(config.HashKey)
s := securecookie.New(hashKey, blockKey)
app.CookieHandler = s
- templates := template.Must(template.ParseGlob("templates/*.tmpl"))
+ fmap := template.FuncMap{
+ "ago": ago,
+ }
+ templates := template.Must(template.New("").Funcs(fmap).ParseGlob("templates/*.tmpl"))
app.Template = templates
return app
}
@@ -51,6 +54,7 @@ func logg(fn http.HandlerFunc) http.HandlerFunc {
}
func main() {
+ log.SetFlags(log.LstdFlags | log.Lshortfile)
app := New()
go func() {
diff --git a/modern.go b/modern.go
index 6277796..79bebd3 100644
--- a/modern.go
+++ b/modern.go
@@ -83,7 +83,12 @@ func (store *SqlStore) AuthGetSession(r *http.Request) ApiResponse {
var response struct {
Session *Session `json:"session"`
}
- session := NewSession("thibauthorel", r.FormValue("api_key"), "2.0")
+ session := &Session{
+ User: "thibauthorel",
+ Client: r.FormValue("api_key"),
+ Protocol: "2.0",
+ Key: randomToken(16),
+ }
store.PutSession(session)
response.Session = session
return response
diff --git a/schema.sql b/schema.sql
index fb13dc7..475c46d 100644
--- a/schema.sql
+++ b/schema.sql
@@ -1,37 +1,41 @@
+CREATE TABLE IF NOT EXISTS users (
+ user_id SERIAL PRIMARY KEY,
+ type text,
+ op_id text,
+ name text,
+ email text,
+ lfm_name text UNIQUE,
+ lfm_password text,
+ created timestamptz DEFAULT current_timestamp
+);
+
CREATE TABLE IF NOT EXISTS scrobbling_sessions (
- lfm_name text,
- key text PRIMARY KEY,
+ session_key text PRIMARY KEY,
+ lfm_name text REFERENCES users(lfm_name),
client text,
+ client_version text,
protocol text,
created timestamptz DEFAULT current_timestamp
);
-
CREATE TABLE IF NOT EXISTS scrobbles (
- artist text,
+ artist text,
albumartist text,
- trackname text,
- album text,
+ trackname text,
+ album text,
tracknumber int,
- duration int,
- time timestamptz,
- chosen bool,
- mbid text,
- session text
-);
-
-CREATE TABLE IF NOT EXISTS users (
- user_id SERIAL PRIMARY KEY,
- type text,
- op_id text,
- name text,
- email text,
- lfm_name text,
- lfm_password text
+ duration int,
+ time timestamptz,
+ chosen bool,
+ mbid text,
+ mbid_computed text,
+ session_key text REFERENCES scrobbling_sessions,
+ lfm_name text REFERENCES users(lfm_name)
);
CREATE TABLE IF NOT EXISTS user_sessions (
id text PRIMARY KEY,
- user_id integer REFERENCES users
+ user_id integer REFERENCES users,
+ created timestamptz DEFAULT current_timestamp
);
CREATE TABLE IF NOT EXISTS songs (
diff --git a/static/style.css b/static/style.css
index fca47c8..e4730aa 100644
--- a/static/style.css
+++ b/static/style.css
@@ -1,3 +1,17 @@
+/* Rules for sizing the icon. */
+.material-icons.md-18 { font-size: 18px; }
+.material-icons.md-24 { font-size: 24px; }
+.material-icons.md-36 { font-size: 36px; }
+.material-icons.md-48 { font-size: 48px; }
+
+/* Rules for using icons as black on a light background. */
+.material-icons.md-dark { color: rgba(0, 0, 0, 0.54); }
+.material-icons.md-dark.md-inactive { color: rgba(0, 0, 0, 0.26); }
+
+/* Rules for using icons as white on a dark background. */
+.material-icons.md-light { color: rgba(255, 255, 255, 1); }
+.material-icons.md-light.md-inactive { color: rgba(255, 255, 255, 0.3); }
+
body {
font-family: 'Roboto';
font-size: 16px;
@@ -49,6 +63,7 @@ ul.scrobbles li {
ul.scrobbles li:hover {
background-color: #E8EAF6;
+ cursor: pointer;
}
ul.scrobbles li img {
@@ -70,6 +85,16 @@ ul.scrobbles li .album {
color: #4f4f4f;
}
+ul.scrobbles .like {
+ margin: 0;
+ padding: 0;
+ margin-left: auto;
+ padding-left: 4px;
+ box-sizing: border-box;
+ min-width: 28px;
+ text-align: right;
+}
+
#center {
height: 100%;
max-width: 1000px;
diff --git a/templates/base.tmpl b/templates/base.tmpl
index 2ee087a..07331e2 100644
--- a/templates/base.tmpl
+++ b/templates/base.tmpl
@@ -1,4 +1,4 @@
-{{define "header"}}
+{{define "header" -}}
<!DOCTYPE html>
<html lang="en">
<head>
@@ -6,17 +6,18 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ListenDiary</title>
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700" rel="stylesheet">
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="static/style.css">
</head>
<body>
<header>
- <div id="center">
- <h1><a href="/">ListenDiary</a></h1>
- <a href="/settings" class="user">{{ .Session.UserName }}</a>
- </div>
+ <div id="center">
+ <h1><a href="/">ListenDiary</a></h1>
+ <a href="/settings" class="user">{{ .Session.UserName }}</a>
+ </div>
</header>
-<div id="main">
-{{end}}
+ <div id="main">
+{{- end}}
{{define "footer"}}
</div>
diff --git a/templates/index.tmpl b/templates/index.tmpl
index 7ee1801..8468b31 100644
--- a/templates/index.tmpl
+++ b/templates/index.tmpl
@@ -1,15 +1,18 @@
{{template "header" . }}
<h2>Recent Listens</h2>
<ul class="scrobbles">
-{{range .Scrobbles}}
+{{- range .Scrobbles}}
<li>
- <img src="{{or .Image "https://lastfm-img2.akamaized.net/i/u/64s/4128a6eb29f94943c9d206c08e625904.png"}}"
- height="40"/>
+ <img src="{{or .Image "https://lastfm-img2.akamaized.net/i/u/64s/4128a6eb29f94943c9d206c08e625904.png"}}"/>
<div>
- {{.Artist}} — {{.TrackName}}<br/>
- <span class="album">in {{.Album}}</span>
+ {{.Artist}} — {{.TrackName}}<br/>
+ <span class="album">in {{.Album}}</span>
+ </div>
+ <div class="like">
+ <span class="album">{{ago .Time}}</span><br/>
+ <i class="material-icons">favorite_border</i>
</div>
</li>
-{{end}}
+{{- end}}
</ul>
{{template "footer" }}
diff --git a/utils.go b/utils.go
index 49fdc5b..2a47905 100644
--- a/utils.go
+++ b/utils.go
@@ -4,7 +4,9 @@ import (
"crypto/md5"
"crypto/rand"
"encoding/hex"
+ "fmt"
"net/http"
+ "time"
)
func randomToken(length int) string {
@@ -43,3 +45,18 @@ func (app *App) SetCookie(w http.ResponseWriter, name string, v interface{}, exp
}
http.SetCookie(w, cookie)
}
+
+func ago(t time.Time) string {
+ delta := time.Since(t)
+ if delta < time.Minute {
+ return fmt.Sprintf("%ds ago", int(delta/time.Second))
+ } else if delta < time.Hour {
+ return fmt.Sprintf("%dm ago", int(delta/time.Minute))
+ } else if delta < 24*time.Hour {
+ return fmt.Sprintf("%dh ago", int(delta/time.Hour))
+ } else if delta < 5*24*time.Hour {
+ return fmt.Sprintf("%dd ago", int(delta/(24*time.Hour)))
+ } else {
+ return t.Format("Jan 2")
+ }
+}