add scrobblingt

This commit is contained in:
sentriz
2019-04-18 20:05:24 +01:00
parent 83374706d2
commit 1094f2da21
12 changed files with 191 additions and 83 deletions

View File

@@ -48,7 +48,7 @@ var (
type lastAlbum struct { type lastAlbum struct {
coverModTime time.Time // 1st needed for cover insertion coverModTime time.Time // 1st needed for cover insertion
coverPath string // 2rd needed for cover insertion coverPath string // 2rd needed for cover insertion
id uint // 3nd needed for cover insertion id int // 3nd needed for cover insertion
} }
func (l *lastAlbum) isEmpty() bool { func (l *lastAlbum) isEmpty() bool {
@@ -133,18 +133,18 @@ func handleFile(fullPath string, info *godirwalk.Dirent) error {
return fmt.Errorf("when reading tags: %v", err) return fmt.Errorf("when reading tags: %v", err)
} }
trackNumber, totalTracks := tags.Track() trackNumber, totalTracks := tags.Track()
discNumber, TotalDiscs := tags.Disc() discNumber, totalDiscs := tags.Disc()
track.Path = fullPath track.Path = fullPath
track.Title = tags.Title() track.Title = tags.Title()
track.Artist = tags.Artist() track.Artist = tags.Artist()
track.DiscNumber = uint(discNumber) track.DiscNumber = discNumber
track.TotalDiscs = uint(TotalDiscs) track.TotalDiscs = totalDiscs
track.TotalTracks = uint(totalTracks) track.TotalTracks = totalTracks
track.TrackNumber = uint(trackNumber) track.TrackNumber = trackNumber
track.Year = uint(tags.Year()) track.Year = tags.Year()
track.Suffix = extension track.Suffix = extension
track.ContentType = mime track.ContentType = mime
track.Size = uint(stat.Size()) track.Size = int(stat.Size())
// set album artist { // set album artist {
albumArtist := db.AlbumArtist{ albumArtist := db.AlbumArtist{
Name: tags.AlbumArtist(), Name: tags.AlbumArtist(),
@@ -188,6 +188,7 @@ func main() {
&db.Cover{}, &db.Cover{},
&db.User{}, &db.User{},
&db.Setting{}, &db.Setting{},
&db.Play{},
) )
// 🤫🤫🤫 // 🤫🤫🤫
orm.Exec(` orm.Exec(`

View File

@@ -46,6 +46,8 @@ func setSubsonicRoutes(mux *http.ServeMux) {
mux.HandleFunc("/rest/stream.view", withWare(cont.Stream)) mux.HandleFunc("/rest/stream.view", withWare(cont.Stream))
mux.HandleFunc("/rest/download", withWare(cont.Stream)) mux.HandleFunc("/rest/download", withWare(cont.Stream))
mux.HandleFunc("/rest/download.view", withWare(cont.Stream)) mux.HandleFunc("/rest/download.view", withWare(cont.Stream))
mux.HandleFunc("/rest/scrobble", withWare(cont.Scrobble))
mux.HandleFunc("/rest/scrobble.view", withWare(cont.Scrobble))
mux.HandleFunc("/rest/getCoverArt", withWare(cont.GetCoverArt)) mux.HandleFunc("/rest/getCoverArt", withWare(cont.GetCoverArt))
mux.HandleFunc("/rest/getCoverArt.view", withWare(cont.GetCoverArt)) mux.HandleFunc("/rest/getCoverArt.view", withWare(cont.GetCoverArt))
mux.HandleFunc("/rest/getArtists", withWare(cont.GetArtists)) mux.HandleFunc("/rest/getArtists", withWare(cont.GetArtists))

View File

@@ -11,11 +11,5 @@ type CrudBase struct {
} }
type IDBase struct { type IDBase struct {
ID uint `gorm:"primary_key"` ID int `gorm:"primary_key"`
}
// Base is the base model with an auto incrementing primary key
type Base struct {
IDBase
CrudBase
} }

View File

@@ -1,48 +1,53 @@
package db package db
import "time"
// Album represents the albums table // Album represents the albums table
type Album struct { type Album struct {
Base IDBase
CrudBase
AlbumArtist AlbumArtist AlbumArtist AlbumArtist
AlbumArtistID uint AlbumArtistID int
Title string `gorm:"not null;index"` Title string `gorm:"not null;index"`
Tracks []Track Tracks []Track
} }
// AlbumArtist represents the AlbumArtists table // AlbumArtist represents the AlbumArtists table
type AlbumArtist struct { type AlbumArtist struct {
Base IDBase
CrudBase
Albums []Album Albums []Album
Name string `gorm:"not null;unique_index"` Name string `gorm:"not null;unique_index"`
} }
// Track represents the tracks table // Track represents the tracks table
type Track struct { type Track struct {
Base IDBase
CrudBase
Album Album Album Album
AlbumID uint AlbumID int
AlbumArtist AlbumArtist AlbumArtist AlbumArtist
AlbumArtistID uint AlbumArtistID int
Artist string Artist string
Bitrate uint Bitrate int
Codec string Codec string
DiscNumber uint DiscNumber int
Duration uint Duration int
Title string Title string
TotalDiscs uint TotalDiscs int
TotalTracks uint TotalTracks int
TrackNumber uint TrackNumber int
Year uint Year int
Suffix string Suffix string
ContentType string ContentType string
Size uint Size int
Path string `gorm:"not null;unique_index"` Path string `gorm:"not null;unique_index"`
} }
// Cover represents the covers table // Cover represents the covers table
type Cover struct { type Cover struct {
CrudBase CrudBase
AlbumID uint `gorm:"primary_key;auto_increment:false"` AlbumID int `gorm:"primary_key;auto_increment:false"`
Album Album Album Album
Image []byte Image []byte
Path string `gorm:"not null;unique_index"` Path string `gorm:"not null;unique_index"`
@@ -50,7 +55,8 @@ type Cover struct {
// User represents the users table // User represents the users table
type User struct { type User struct {
Base IDBase
CrudBase
Name string `gorm:"not null;unique_index"` Name string `gorm:"not null;unique_index"`
Password string Password string
LastFMSession string LastFMSession string
@@ -63,3 +69,13 @@ type Setting struct {
Key string `gorm:"primary_key;auto_increment:false"` Key string `gorm:"primary_key;auto_increment:false"`
Value string Value string
} }
// Play represents the settings table
type Play struct {
IDBase
User User
UserID int
Track Track
TrackID int
Time time.Time
}

View File

@@ -84,9 +84,9 @@ type templateData struct {
User *db.User User *db.User
SelectedUser *db.User SelectedUser *db.User
AllUsers []*db.User AllUsers []*db.User
ArtistCount uint ArtistCount int
AlbumCount uint AlbumCount int
TrackCount uint TrackCount int
CurrentLastFMAPIKey string CurrentLastFMAPIKey string
CurrentLastFMAPISecret string CurrentLastFMAPISecret string
RequestRoot string RequestRoot string
@@ -96,6 +96,14 @@ func getStrParam(r *http.Request, key string) string {
return r.URL.Query().Get(key) return r.URL.Query().Get(key)
} }
func getStrParamOr(r *http.Request, key, or string) string {
val := getStrParam(r, key)
if val == "" {
return or
}
return val
}
func getIntParam(r *http.Request, key string) (int, error) { func getIntParam(r *http.Request, key string) (int, error) {
strVal := r.URL.Query().Get(key) strVal := r.URL.Query().Get(key)
if strVal == "" { if strVal == "" {
@@ -156,7 +164,7 @@ func respond(w http.ResponseWriter, r *http.Request,
} }
func respondError(w http.ResponseWriter, r *http.Request, func respondError(w http.ResponseWriter, r *http.Request,
code uint64, message string) { code int, message string) {
respondRaw(w, r, http.StatusBadRequest, subsonic.NewError( respondRaw(w, r, http.StatusBadRequest, subsonic.NewError(
code, message, code, message,
)) ))

View File

@@ -4,10 +4,12 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"time"
"unicode" "unicode"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/sentriz/gonic/db" "github.com/sentriz/gonic/db"
"github.com/sentriz/gonic/lastfm"
"github.com/sentriz/gonic/subsonic" "github.com/sentriz/gonic/subsonic"
"github.com/mozillazg/go-unidecode" "github.com/mozillazg/go-unidecode"
@@ -210,6 +212,51 @@ func (c *Controller) GetLicence(w http.ResponseWriter, r *http.Request) {
respond(w, r, sub) respond(w, r, sub)
} }
func (c *Controller) Scrobble(w http.ResponseWriter, r *http.Request) {
id, err := getIntParam(r, "id")
if err != nil {
respondError(w, r, 10, "please provide an `id` parameter")
return
}
// fetch user to get lastfm session
username := getStrParam(r, "u")
user := c.GetUserFromName(username)
if user == nil {
respondError(w, r, 10, "could not find a user with that name")
return
}
if user.LastFMSession == "" {
respondError(w, r, 0, fmt.Sprintf("no last.fm session for this user: %v", err))
return
}
// fetch track for getting info to send to last.fm function
var track db.Track
c.DB.
Preload("Album").
Preload("AlbumArtist").
First(&track, id)
// get time from args or use now
time := getIntParamOr(r, "time", int(time.Now().Unix()))
// get submission, where the default is true. we will
// check if it's false later
submission := getStrParamOr(r, "submission", "true")
// scrobble with above info
err = lastfm.Scrobble(
c.GetSetting("lastfm_api_key"),
c.GetSetting("lastfm_secret"),
user.LastFMSession,
&track,
time,
submission != "false",
)
if err != nil {
respondError(w, r, 0, fmt.Sprintf("error when submitting: %v", err))
return
}
sub := subsonic.NewResponse()
respond(w, r, sub)
}
func (c *Controller) NotFound(w http.ResponseWriter, r *http.Request) { func (c *Controller) NotFound(w http.ResponseWriter, r *http.Request) {
respondError(w, r, 0, "unknown route") respondError(w, r, 0, "unknown route")
} }

View File

@@ -7,7 +7,11 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"sort"
"strconv"
"time" "time"
"github.com/sentriz/gonic/db"
) )
var ( var (
@@ -18,38 +22,72 @@ var (
) )
func getParamSignature(params url.Values, secret string) string { func getParamSignature(params url.Values, secret string) string {
// the parameters must be in order before hashing
paramKeys := make([]string, 0)
for k, _ := range params {
paramKeys = append(paramKeys, k)
}
sort.Strings(paramKeys)
toHash := "" toHash := ""
for k, v := range params { for _, k := range paramKeys {
toHash += k toHash += k
toHash += v[0] toHash += params[k][0]
} }
toHash += secret toHash += secret
hash := md5.Sum([]byte(toHash)) hash := md5.Sum([]byte(toHash))
return hex.EncodeToString(hash[:]) return hex.EncodeToString(hash[:])
} }
func GetSession(apiKey, secret, token string) (string, error) { func makeRequest(method string, params url.Values) (*LastFM, error) {
params := url.Values{} req, _ := http.NewRequest(method, baseURL, nil)
// the first 3 parameters here must be in alphabetical order
params.Add("api_key", apiKey)
params.Add("method", "auth.getSession")
params.Add("token", token)
params.Add("api_sig", getParamSignature(params, secret))
req, _ := http.NewRequest("GET", baseURL, nil)
req.URL.RawQuery = params.Encode() req.URL.RawQuery = params.Encode()
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return "", fmt.Errorf("error when making request to last.fm: %v", err) return nil, fmt.Errorf("get: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
decoder := xml.NewDecoder(resp.Body) decoder := xml.NewDecoder(resp.Body)
var lastfm LastFM var lastfm LastFM
err = decoder.Decode(&lastfm) err = decoder.Decode(&lastfm)
if err != nil { if err != nil {
return "", fmt.Errorf("error when decoding last.fm response: %v", err) return nil, fmt.Errorf("decoding: %v", err)
} }
if lastfm.Error != nil { if lastfm.Error != nil {
return "", fmt.Errorf("error when parsing last.fm response: %v", lastfm.Error.Value) return nil, fmt.Errorf("parsing: %v", lastfm.Error.Value)
} }
return lastfm.Session.Key, nil return &lastfm, nil
}
func GetSession(apiKey, secret, token string) (string, error) {
params := url.Values{}
params.Add("method", "auth.getSession")
params.Add("api_key", apiKey)
params.Add("token", token)
params.Add("api_sig", getParamSignature(params, secret))
resp, err := makeRequest("GET", params)
if err != nil {
return "", fmt.Errorf("error when getting session step '%v'", err)
}
return resp.Session.Key, nil
}
func Scrobble(apiKey, secret, session string,
track *db.Track, stamp int, submission bool) error {
params := url.Values{}
if submission {
params.Add("method", "track.Scrobble")
params.Add("timestamp", strconv.Itoa(stamp))
} else {
params.Add("method", "track.updateNowPlaying")
}
params.Add("api_key", apiKey)
params.Add("sk", session)
params.Add("artist", track.Artist)
params.Add("track", track.Title)
params.Add("album", track.Album.Title)
params.Add("albumArtist", track.AlbumArtist.Name)
params.Add("trackNumber", strconv.Itoa(track.TrackNumber))
params.Add("api_sig", getParamSignature(params, secret))
_, err := makeRequest("POST", params)
return err
} }

BIN
static/images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -3,13 +3,13 @@ package subsonic
import "time" import "time"
type Album struct { type Album struct {
ID uint `xml:"id,attr" json:"id"` ID int `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"` Name string `xml:"name,attr" json:"name"`
ArtistID uint `xml:"artistId,attr" json:"artistId"` ArtistID int `xml:"artistId,attr" json:"artistId"`
Artist string `xml:"artist,attr" json:"artist"` Artist string `xml:"artist,attr" json:"artist"`
TrackCount uint `xml:"songCount,attr" json:"songCount"` TrackCount int `xml:"songCount,attr" json:"songCount"`
Duration uint `xml:"duration,attr" json:"duration"` Duration int `xml:"duration,attr" json:"duration"`
CoverID uint `xml:"coverArt,attr" json:"coverArt"` CoverID int `xml:"coverArt,attr" json:"coverArt"`
Created time.Time `xml:"created,attr" json:"created"` Created time.Time `xml:"created,attr" json:"created"`
Tracks []*Track `xml:"song" json:"song,omitempty"` Tracks []*Track `xml:"song" json:"song,omitempty"`
} }
@@ -19,38 +19,38 @@ type RandomTracks struct {
} }
type Track struct { type Track struct {
ID uint `xml:"id,attr" json:"id"` ID int `xml:"id,attr" json:"id"`
Parent uint `xml:"parent,attr" json:"parent"` Parent int `xml:"parent,attr" json:"parent"`
Title string `xml:"title,attr" json:"title"` Title string `xml:"title,attr" json:"title"`
Album string `xml:"album,attr" json:"album"` Album string `xml:"album,attr" json:"album"`
Artist string `xml:"artist,attr" json:"artist"` Artist string `xml:"artist,attr" json:"artist"`
IsDir bool `xml:"isDir,attr" json:"isDir"` IsDir bool `xml:"isDir,attr" json:"isDir"`
CoverID uint `xml:"coverArt,attr" json:"coverArt"` CoverID int `xml:"coverArt,attr" json:"coverArt"`
Created time.Time `xml:"created,attr" json:"created"` Created time.Time `xml:"created,attr" json:"created"`
Duration uint `xml:"duration,attr" json:"duration"` Duration int `xml:"duration,attr" json:"duration"`
Genre string `xml:"genre,attr" json:"genre"` Genre string `xml:"genre,attr" json:"genre"`
BitRate uint `xml:"bitRate,attr" json:"bitRate"` BitRate int `xml:"bitRate,attr" json:"bitRate"`
Size uint `xml:"size,attr" json:"size"` Size int `xml:"size,attr" json:"size"`
Suffix string `xml:"suffix,attr" json:"suffix"` Suffix string `xml:"suffix,attr" json:"suffix"`
ContentType string `xml:"contentType,attr" json:"contentType"` ContentType string `xml:"contentType,attr" json:"contentType"`
IsVideo bool `xml:"isVideo,attr" json:"isVideo"` IsVideo bool `xml:"isVideo,attr" json:"isVideo"`
Path string `xml:"path,attr" json:"path"` Path string `xml:"path,attr" json:"path"`
AlbumID uint `xml:"albumId,attr" json:"albumId"` AlbumID int `xml:"albumId,attr" json:"albumId"`
ArtistID uint `xml:"artistId,attr" json:"artistId"` ArtistID int `xml:"artistId,attr" json:"artistId"`
TrackNo uint `xml:"track,attr" json:"track"` TrackNo int `xml:"track,attr" json:"track"`
Type string `xml:"type,attr" json:"type"` Type string `xml:"type,attr" json:"type"`
} }
type Artist struct { type Artist struct {
ID uint `xml:"id,attr" json:"id"` ID int `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"` Name string `xml:"name,attr" json:"name"`
CoverID uint `xml:"coverArt,attr" json:"coverArt,omitempty"` CoverID int `xml:"coverArt,attr" json:"coverArt,omitempty"`
AlbumCount uint `xml:"albumCount,attr" json:"albumCount,omitempty"` AlbumCount int `xml:"albumCount,attr" json:"albumCount,omitempty"`
Albums []*Album `xml:"album,omitempty" json:"album,omitempty"` Albums []*Album `xml:"album,omitempty" json:"album,omitempty"`
} }
type Indexes struct { type Indexes struct {
LastModified uint `xml:"lastModified,attr" json:"lastModified"` LastModified int `xml:"lastModified,attr" json:"lastModified"`
Index []*Index `xml:"index" json:"index"` Index []*Index `xml:"index" json:"index"`
} }
@@ -60,36 +60,36 @@ type Index struct {
} }
type Directory struct { type Directory struct {
ID uint `xml:"id,attr" json:"id"` ID int `xml:"id,attr" json:"id"`
Parent uint `xml:"parent,attr" json:"parent"` Parent int `xml:"parent,attr" json:"parent"`
Name string `xml:"name,attr" json:"name"` Name string `xml:"name,attr" json:"name"`
Starred string `xml:"starred,attr,omitempty" json:"starred,omitempty"` Starred string `xml:"starred,attr,omitempty" json:"starred,omitempty"`
Children []Child `xml:"child" json:"child"` Children []Child `xml:"child" json:"child"`
} }
type Child struct { type Child struct {
ID uint `xml:"id,attr" json:"id,omitempty"` ID int `xml:"id,attr" json:"id,omitempty"`
Parent uint `xml:"parent,attr" json:"parent,omitempty"` Parent int `xml:"parent,attr" json:"parent,omitempty"`
Title string `xml:"title,attr" json:"title,omitempty"` Title string `xml:"title,attr" json:"title,omitempty"`
IsDir bool `xml:"isDir,attr" json:"isDir,omitempty"` IsDir bool `xml:"isDir,attr" json:"isDir,omitempty"`
Album string `xml:"album,attr,omitempty" json:"album,omitempty"` Album string `xml:"album,attr,omitempty" json:"album,omitempty"`
AlbumID uint `xml:"albumId,attr,omitempty" json:"albumId,omitempty"` AlbumID int `xml:"albumId,attr,omitempty" json:"albumId,omitempty"`
Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"` Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"`
ArtistID uint `xml:"artistId,attr,omitempty" json:"artistId,omitempty"` ArtistID int `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
Track uint `xml:"track,attr,omitempty" json:"track,omitempty"` Track int `xml:"track,attr,omitempty" json:"track,omitempty"`
Year uint `xml:"year,attr,omitempty" json:"year,omitempty"` Year int `xml:"year,attr,omitempty" json:"year,omitempty"`
Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"` Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
CoverID uint `xml:"coverArt,attr" json:"coverArt,omitempty"` CoverID int `xml:"coverArt,attr" json:"coverArt,omitempty"`
Size uint `xml:"size,attr,omitempty" json:"size,omitempty"` Size int `xml:"size,attr,omitempty" json:"size,omitempty"`
ContentType string `xml:"contentType,attr,omitempty" json:"contentType,omitempty"` ContentType string `xml:"contentType,attr,omitempty" json:"contentType,omitempty"`
Suffix string `xml:"suffix,attr,omitempty" json:"suffix,omitempty"` Suffix string `xml:"suffix,attr,omitempty" json:"suffix,omitempty"`
Duration uint `xml:"duration,attr,omitempty" json:"duration"` Duration int `xml:"duration,attr,omitempty" json:"duration"`
BitRate uint `xml:"bitRate,attr,omitempty" json:"bitrate,omitempty"` BitRate int `xml:"bitRate,attr,omitempty" json:"bitrate,omitempty"`
Path string `xml:"path,attr,omitempty" json:"path,omitempty"` Path string `xml:"path,attr,omitempty" json:"path,omitempty"`
} }
type MusicFolder struct { type MusicFolder struct {
ID uint `xml:"id,attr" json:"id,omitempty"` ID int `xml:"id,attr" json:"id,omitempty"`
Name string `xml:"name,attr" json:"name,omitempty"` Name string `xml:"name,attr" json:"name,omitempty"`
} }

View File

@@ -32,7 +32,7 @@ type Response struct {
} }
type Error struct { type Error struct {
Code uint64 `xml:"code,attr" json:"code"` Code int `xml:"code,attr" json:"code"`
Message string `xml:"message,attr" json:"message"` Message string `xml:"message,attr" json:"message"`
} }
@@ -44,7 +44,7 @@ func NewResponse() *Response {
} }
} }
func NewError(code uint64, message string) *Response { func NewError(code int, message string) *Response {
return &Response{ return &Response{
Status: "failed", Status: "failed",
XMLNS: xmlns, XMLNS: xmlns,

View File

@@ -6,6 +6,8 @@
<title>{{ template "title" }}</title> <title>{{ template "title" }}</title>
<link rel="stylesheet" href="/admin/static/stylesheets/awsm.css"> <link rel="stylesheet" href="/admin/static/stylesheets/awsm.css">
<link rel="stylesheet" href="/admin/static/stylesheets/main.css"> <link rel="stylesheet" href="/admin/static/stylesheets/main.css">
<link rel="shortcut icon" href="/admin/static/images/favicon.ico" type="image/x-icon">
<link rel="icon" href="/admin/static/images/favicon.ico" type="image/x-icon">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head> </head>
<body> <body>

View File

@@ -38,7 +38,7 @@
</div> </div>
<div class="right"> <div class="right">
{{ range $user := .AllUsers }} {{ range $user := .AllUsers }}
{{ $user.Name }} <span class="light">created</span> <u>{{ $user.CreatedAt.Format "Jan 02, 2006" }}</u> <a href="/admin/change_password?user={{ $user.Name }}">change password</a><br/> <i>{{ $user.Name }}</i> <span class="light">created</span> {{ $user.CreatedAt.Format "Jan 02, 2006" }} <a href="/admin/change_password?user={{ $user.Name }}">change password</a><br/>
{{ end }} {{ end }}
<a href="/admin/create_user" class="button">create new</a> <a href="/admin/create_user" class="button">create new</a>
</div> </div>