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 {
coverModTime time.Time // 1st 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 {
@@ -133,18 +133,18 @@ func handleFile(fullPath string, info *godirwalk.Dirent) error {
return fmt.Errorf("when reading tags: %v", err)
}
trackNumber, totalTracks := tags.Track()
discNumber, TotalDiscs := tags.Disc()
discNumber, totalDiscs := tags.Disc()
track.Path = fullPath
track.Title = tags.Title()
track.Artist = tags.Artist()
track.DiscNumber = uint(discNumber)
track.TotalDiscs = uint(TotalDiscs)
track.TotalTracks = uint(totalTracks)
track.TrackNumber = uint(trackNumber)
track.Year = uint(tags.Year())
track.DiscNumber = discNumber
track.TotalDiscs = totalDiscs
track.TotalTracks = totalTracks
track.TrackNumber = trackNumber
track.Year = tags.Year()
track.Suffix = extension
track.ContentType = mime
track.Size = uint(stat.Size())
track.Size = int(stat.Size())
// set album artist {
albumArtist := db.AlbumArtist{
Name: tags.AlbumArtist(),
@@ -188,6 +188,7 @@ func main() {
&db.Cover{},
&db.User{},
&db.Setting{},
&db.Play{},
)
// 🤫🤫🤫
orm.Exec(`

View File

@@ -46,6 +46,8 @@ func setSubsonicRoutes(mux *http.ServeMux) {
mux.HandleFunc("/rest/stream.view", withWare(cont.Stream))
mux.HandleFunc("/rest/download", 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.view", withWare(cont.GetCoverArt))
mux.HandleFunc("/rest/getArtists", withWare(cont.GetArtists))

View File

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

View File

@@ -1,48 +1,53 @@
package db
import "time"
// Album represents the albums table
type Album struct {
Base
IDBase
CrudBase
AlbumArtist AlbumArtist
AlbumArtistID uint
AlbumArtistID int
Title string `gorm:"not null;index"`
Tracks []Track
}
// AlbumArtist represents the AlbumArtists table
type AlbumArtist struct {
Base
IDBase
CrudBase
Albums []Album
Name string `gorm:"not null;unique_index"`
}
// Track represents the tracks table
type Track struct {
Base
IDBase
CrudBase
Album Album
AlbumID uint
AlbumID int
AlbumArtist AlbumArtist
AlbumArtistID uint
AlbumArtistID int
Artist string
Bitrate uint
Bitrate int
Codec string
DiscNumber uint
Duration uint
DiscNumber int
Duration int
Title string
TotalDiscs uint
TotalTracks uint
TrackNumber uint
Year uint
TotalDiscs int
TotalTracks int
TrackNumber int
Year int
Suffix string
ContentType string
Size uint
Size int
Path string `gorm:"not null;unique_index"`
}
// Cover represents the covers table
type Cover struct {
CrudBase
AlbumID uint `gorm:"primary_key;auto_increment:false"`
AlbumID int `gorm:"primary_key;auto_increment:false"`
Album Album
Image []byte
Path string `gorm:"not null;unique_index"`
@@ -50,7 +55,8 @@ type Cover struct {
// User represents the users table
type User struct {
Base
IDBase
CrudBase
Name string `gorm:"not null;unique_index"`
Password string
LastFMSession string
@@ -63,3 +69,13 @@ type Setting struct {
Key string `gorm:"primary_key;auto_increment:false"`
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
SelectedUser *db.User
AllUsers []*db.User
ArtistCount uint
AlbumCount uint
TrackCount uint
ArtistCount int
AlbumCount int
TrackCount int
CurrentLastFMAPIKey string
CurrentLastFMAPISecret string
RequestRoot string
@@ -96,6 +96,14 @@ func getStrParam(r *http.Request, key string) string {
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) {
strVal := r.URL.Query().Get(key)
if strVal == "" {
@@ -156,7 +164,7 @@ func respond(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(
code, message,
))

View File

@@ -4,10 +4,12 @@ import (
"fmt"
"net/http"
"os"
"time"
"unicode"
"github.com/jinzhu/gorm"
"github.com/sentriz/gonic/db"
"github.com/sentriz/gonic/lastfm"
"github.com/sentriz/gonic/subsonic"
"github.com/mozillazg/go-unidecode"
@@ -210,6 +212,51 @@ func (c *Controller) GetLicence(w http.ResponseWriter, r *http.Request) {
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) {
respondError(w, r, 0, "unknown route")
}

View File

@@ -7,7 +7,11 @@ import (
"fmt"
"net/http"
"net/url"
"sort"
"strconv"
"time"
"github.com/sentriz/gonic/db"
)
var (
@@ -18,38 +22,72 @@ var (
)
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 := ""
for k, v := range params {
for _, k := range paramKeys {
toHash += k
toHash += v[0]
toHash += params[k][0]
}
toHash += secret
hash := md5.Sum([]byte(toHash))
return hex.EncodeToString(hash[:])
}
func GetSession(apiKey, secret, token string) (string, error) {
params := url.Values{}
// 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)
func makeRequest(method string, params url.Values) (*LastFM, error) {
req, _ := http.NewRequest(method, baseURL, nil)
req.URL.RawQuery = params.Encode()
resp, err := client.Do(req)
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()
decoder := xml.NewDecoder(resp.Body)
var lastfm LastFM
err = decoder.Decode(&lastfm)
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 {
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"
type Album struct {
ID uint `xml:"id,attr" json:"id"`
ID int `xml:"id,attr" json:"id"`
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"`
TrackCount uint `xml:"songCount,attr" json:"songCount"`
Duration uint `xml:"duration,attr" json:"duration"`
CoverID uint `xml:"coverArt,attr" json:"coverArt"`
TrackCount int `xml:"songCount,attr" json:"songCount"`
Duration int `xml:"duration,attr" json:"duration"`
CoverID int `xml:"coverArt,attr" json:"coverArt"`
Created time.Time `xml:"created,attr" json:"created"`
Tracks []*Track `xml:"song" json:"song,omitempty"`
}
@@ -19,38 +19,38 @@ type RandomTracks struct {
}
type Track struct {
ID uint `xml:"id,attr" json:"id"`
Parent uint `xml:"parent,attr" json:"parent"`
ID int `xml:"id,attr" json:"id"`
Parent int `xml:"parent,attr" json:"parent"`
Title string `xml:"title,attr" json:"title"`
Album string `xml:"album,attr" json:"album"`
Artist string `xml:"artist,attr" json:"artist"`
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"`
Duration uint `xml:"duration,attr" json:"duration"`
Duration int `xml:"duration,attr" json:"duration"`
Genre string `xml:"genre,attr" json:"genre"`
BitRate uint `xml:"bitRate,attr" json:"bitRate"`
Size uint `xml:"size,attr" json:"size"`
BitRate int `xml:"bitRate,attr" json:"bitRate"`
Size int `xml:"size,attr" json:"size"`
Suffix string `xml:"suffix,attr" json:"suffix"`
ContentType string `xml:"contentType,attr" json:"contentType"`
IsVideo bool `xml:"isVideo,attr" json:"isVideo"`
Path string `xml:"path,attr" json:"path"`
AlbumID uint `xml:"albumId,attr" json:"albumId"`
ArtistID uint `xml:"artistId,attr" json:"artistId"`
TrackNo uint `xml:"track,attr" json:"track"`
AlbumID int `xml:"albumId,attr" json:"albumId"`
ArtistID int `xml:"artistId,attr" json:"artistId"`
TrackNo int `xml:"track,attr" json:"track"`
Type string `xml:"type,attr" json:"type"`
}
type Artist struct {
ID uint `xml:"id,attr" json:"id"`
ID int `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"`
CoverID uint `xml:"coverArt,attr" json:"coverArt,omitempty"`
AlbumCount uint `xml:"albumCount,attr" json:"albumCount,omitempty"`
CoverID int `xml:"coverArt,attr" json:"coverArt,omitempty"`
AlbumCount int `xml:"albumCount,attr" json:"albumCount,omitempty"`
Albums []*Album `xml:"album,omitempty" json:"album,omitempty"`
}
type Indexes struct {
LastModified uint `xml:"lastModified,attr" json:"lastModified"`
LastModified int `xml:"lastModified,attr" json:"lastModified"`
Index []*Index `xml:"index" json:"index"`
}
@@ -60,36 +60,36 @@ type Index struct {
}
type Directory struct {
ID uint `xml:"id,attr" json:"id"`
Parent uint `xml:"parent,attr" json:"parent"`
ID int `xml:"id,attr" json:"id"`
Parent int `xml:"parent,attr" json:"parent"`
Name string `xml:"name,attr" json:"name"`
Starred string `xml:"starred,attr,omitempty" json:"starred,omitempty"`
Children []Child `xml:"child" json:"child"`
}
type Child struct {
ID uint `xml:"id,attr" json:"id,omitempty"`
Parent uint `xml:"parent,attr" json:"parent,omitempty"`
ID int `xml:"id,attr" json:"id,omitempty"`
Parent int `xml:"parent,attr" json:"parent,omitempty"`
Title string `xml:"title,attr" json:"title,omitempty"`
IsDir bool `xml:"isDir,attr" json:"isDir,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"`
ArtistID uint `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
Track uint `xml:"track,attr,omitempty" json:"track,omitempty"`
Year uint `xml:"year,attr,omitempty" json:"year,omitempty"`
ArtistID int `xml:"artistId,attr,omitempty" json:"artistId,omitempty"`
Track int `xml:"track,attr,omitempty" json:"track,omitempty"`
Year int `xml:"year,attr,omitempty" json:"year,omitempty"`
Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"`
CoverID uint `xml:"coverArt,attr" json:"coverArt,omitempty"`
Size uint `xml:"size,attr,omitempty" json:"size,omitempty"`
CoverID int `xml:"coverArt,attr" json:"coverArt,omitempty"`
Size int `xml:"size,attr,omitempty" json:"size,omitempty"`
ContentType string `xml:"contentType,attr,omitempty" json:"contentType,omitempty"`
Suffix string `xml:"suffix,attr,omitempty" json:"suffix,omitempty"`
Duration uint `xml:"duration,attr,omitempty" json:"duration"`
BitRate uint `xml:"bitRate,attr,omitempty" json:"bitrate,omitempty"`
Duration int `xml:"duration,attr,omitempty" json:"duration"`
BitRate int `xml:"bitRate,attr,omitempty" json:"bitrate,omitempty"`
Path string `xml:"path,attr,omitempty" json:"path,omitempty"`
}
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"`
}

View File

@@ -32,7 +32,7 @@ type Response 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"`
}
@@ -44,7 +44,7 @@ func NewResponse() *Response {
}
}
func NewError(code uint64, message string) *Response {
func NewError(code int, message string) *Response {
return &Response{
Status: "failed",
XMLNS: xmlns,

View File

@@ -6,6 +6,8 @@
<title>{{ template "title" }}</title>
<link rel="stylesheet" href="/admin/static/stylesheets/awsm.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">
</head>
<body>

View File

@@ -38,7 +38,7 @@
</div>
<div class="right">
{{ 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 }}
<a href="/admin/create_user" class="button">create new</a>
</div>