mirror of
https://github.com/idanoo/GoScrobble.git
synced 2024-12-25 16:19:00 +00:00
0.0.11
- Fix redirects to /login for auth required pages - Add handling for 401/429 + No connection responses in API calls - Add background workers for Go (clear out password resets) - Fixed timezone issues
This commit is contained in:
parent
fd615102a8
commit
9866dea36b
@ -9,8 +9,6 @@ REDIS_DB=
|
||||
REDIS_PREFIX="gs:"
|
||||
REDIS_AUTH=""
|
||||
|
||||
TIMEZONE=Etc/UTC
|
||||
|
||||
JWT_SECRET=
|
||||
JWT_EXPIRY=86400
|
||||
|
||||
|
@ -12,6 +12,11 @@ import (
|
||||
"gitlab.com/idanoo/go-scrobble/internal/goscrobble"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Set UTC for errything
|
||||
os.Setenv("TZ", "Etc/UTC")
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Println("Starting goscrobble")
|
||||
err := godotenv.Load()
|
||||
@ -51,8 +56,9 @@ func main() {
|
||||
goscrobble.InitRedis()
|
||||
defer goscrobble.CloseRedisConn()
|
||||
|
||||
// Clear old reset tokens regularly
|
||||
// go goscrobble.ClearTokenTimer()
|
||||
// Start background workers
|
||||
go goscrobble.StartBackgroundWorkers()
|
||||
defer goscrobble.EndBackgroundWorkers()
|
||||
|
||||
// Boot up API webserver \o/
|
||||
goscrobble.HandleRequests(port)
|
||||
|
@ -1,3 +1,10 @@
|
||||
# 0.0.11
|
||||
- Fix redirects to /login for auth required pages
|
||||
- Add handling for 401/429 + No connection responses in API calls
|
||||
- Add background workers for Go (clear out password resets)
|
||||
- Add spotify scrobbling!!!11111!!!!!
|
||||
- Fixed timezone issues
|
||||
|
||||
# 0.0.10
|
||||
- Fixed looking up invalid profiles
|
||||
- Added valid error handling to bad request && rate limiting
|
||||
|
@ -1,3 +1,6 @@
|
||||
## Timezones
|
||||
GoScrobble runs as UTC and connects to MySQL as UTC. All timezone handling is done in the frontend.
|
||||
|
||||
## FRONTEND VARS
|
||||
These are stored in `web/.env.production` and `web/.env.development`
|
||||
|
||||
@ -17,8 +20,6 @@ These are stored in `web/.env.production` and `web/.env.development`
|
||||
REDIS_PREFIX="gs:" // Redis key prefix
|
||||
REDIS_AUTH="" // Redis password
|
||||
|
||||
TIMEZONE= // Unix Timezone. Used for MySQL connection
|
||||
|
||||
JWT_SECRET= // 32+ Char JWT secret
|
||||
JWT_EXPIRY=86400 // JWT expiry
|
||||
|
||||
|
2
go.mod
2
go.mod
@ -24,7 +24,9 @@ require (
|
||||
github.com/sendgrid/rest v2.6.3+incompatible // indirect
|
||||
github.com/sendgrid/sendgrid-go v3.8.0+incompatible
|
||||
github.com/sirupsen/logrus v1.7.0 // indirect
|
||||
github.com/zmb3/spotify v1.1.1
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
||||
google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3 // indirect
|
||||
google.golang.org/grpc v1.33.1 // indirect
|
||||
|
9
go.sum
9
go.sum
@ -1,4 +1,5 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
@ -104,6 +105,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/zmb3/spotify v1.1.1 h1:3v2IxmzMrvl/1mieDINOI4JAXCJYWP/NQ4o2A1Ni35k=
|
||||
github.com/zmb3/spotify v1.1.1/go.mod h1:GD7AAEMUJVYc2Z7p2a2S0E3/5f/KxM/vOnErNr4j+Tw=
|
||||
go.opentelemetry.io/otel v0.19.0 h1:Lenfy7QHRXPZVsw/12CWpxX6d/JkrX8wrx2vO8G80Ng=
|
||||
go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg=
|
||||
go.opentelemetry.io/otel/metric v0.19.0 h1:dtZ1Ju44gkJkYvo+3qGqVXmf88tc+a42edOywypengg=
|
||||
@ -124,6 +127,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@ -133,8 +137,11 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -167,8 +174,10 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
|
@ -3,6 +3,7 @@ package goscrobble
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
@ -12,41 +13,47 @@ type Album struct {
|
||||
Desc sql.NullString `json:"desc"`
|
||||
Img sql.NullString `json:"img"`
|
||||
MusicBrainzID sql.NullString `json:"mbid"`
|
||||
SpotifyID sql.NullString `json:"spotify_id"`
|
||||
}
|
||||
|
||||
// insertAlbum - This will return if it exists or create it based on MBID > Name
|
||||
func insertAlbum(name string, mbid string, artists []string, tx *sql.Tx) (Album, error) {
|
||||
func insertAlbum(name string, mbid string, spotifyId string, artists []string, tx *sql.Tx) (Album, error) {
|
||||
album := Album{}
|
||||
|
||||
// Try locate our album
|
||||
if mbid != "" {
|
||||
album = fetchAlbum("mbid", mbid, tx)
|
||||
if album.Uuid == "" {
|
||||
err := insertNewAlbum(name, mbid, tx)
|
||||
if err != nil {
|
||||
log.Printf("Error inserting album via MBID %s %+v", name, err)
|
||||
return album, errors.New("Failed to insert album")
|
||||
}
|
||||
} else if spotifyId != "" {
|
||||
album = fetchAlbum("spotify_id", spotifyId, tx)
|
||||
}
|
||||
|
||||
album = fetchAlbum("mbid", mbid, tx)
|
||||
err = album.linkAlbumToArtists(artists, tx)
|
||||
if err != nil {
|
||||
return album, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If it didn't match above, lookup by name
|
||||
if album.Uuid == "" {
|
||||
album = fetchAlbum("name", name, tx)
|
||||
if album.Uuid == "" {
|
||||
err := insertNewAlbum(name, mbid, tx)
|
||||
if err != nil {
|
||||
log.Printf("Error inserting album via Name %s %+v", name, err)
|
||||
return album, errors.New("Failed to insert album")
|
||||
}
|
||||
}
|
||||
|
||||
// If we can't find it. Lets add it!
|
||||
if album.Uuid == "" {
|
||||
err := insertNewAlbum(name, mbid, spotifyId, tx)
|
||||
if err != nil {
|
||||
return album, errors.New("Failed to insert album")
|
||||
}
|
||||
|
||||
// Fetch the recently inserted album to get the UUID
|
||||
if mbid != "" {
|
||||
album = fetchAlbum("mbid", mbid, tx)
|
||||
} else if spotifyId != "" {
|
||||
album = fetchAlbum("spotify_id", spotifyId, tx)
|
||||
}
|
||||
|
||||
if album.Uuid == "" {
|
||||
album = fetchAlbum("name", name, tx)
|
||||
err = album.linkAlbumToArtists(artists, tx)
|
||||
if err != nil {
|
||||
return album, err
|
||||
}
|
||||
}
|
||||
|
||||
// Try linkem up
|
||||
err = album.linkAlbumToArtists(artists, tx)
|
||||
if err != nil {
|
||||
return album, errors.New("Unable to link albums!")
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,9 +79,9 @@ func fetchAlbum(col string, val string, tx *sql.Tx) Album {
|
||||
return album
|
||||
}
|
||||
|
||||
func insertNewAlbum(name string, mbid string, tx *sql.Tx) error {
|
||||
_, err := tx.Exec("INSERT INTO `albums` (`uuid`, `name`, `mbid`) "+
|
||||
"VALUES (UUID_TO_BIN(UUID(), true),?,?)", name, mbid)
|
||||
func insertNewAlbum(name string, mbid string, spotifyId string, tx *sql.Tx) error {
|
||||
_, err := tx.Exec("INSERT INTO `albums` (`uuid`, `name`, `mbid`, `spotify_id`) "+
|
||||
"VALUES (UUID_TO_BIN(UUID(), true),?,?,?)", name, mbid, spotifyId)
|
||||
|
||||
return err
|
||||
}
|
||||
@ -85,9 +92,16 @@ func (album *Album) linkAlbumToArtists(artists []string, tx *sql.Tx) error {
|
||||
_, err = tx.Exec("INSERT INTO `album_artist` (`album`, `artist`) "+
|
||||
"VALUES (UUID_TO_BIN(?, true), UUID_TO_BIN(?, true))", album.Uuid, artist)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func updateAlbum(uuid string, col string, val string, tx *sql.Tx) error {
|
||||
_, err := tx.Exec("UPDATE `albums` SET `"+col+"` = ? WHERE `uuid` = UUID_TO_BIN(?,true)", val, uuid)
|
||||
|
||||
return err
|
||||
}
|
||||
|
@ -12,36 +12,45 @@ type Artist struct {
|
||||
Desc sql.NullString `json:"desc"`
|
||||
Img sql.NullString `json:"img"`
|
||||
MusicBrainzID sql.NullString `json:"mbid"`
|
||||
SpotifyID sql.NullString `json:"spotify_id"`
|
||||
}
|
||||
|
||||
// insertArtist - This will return if it exists or create it based on MBID > Name
|
||||
func insertArtist(name string, mbid string, tx *sql.Tx) (Artist, error) {
|
||||
func insertArtist(name string, mbid string, spotifyId string, tx *sql.Tx) (Artist, error) {
|
||||
artist := Artist{}
|
||||
|
||||
// Try locate our artist
|
||||
if mbid != "" {
|
||||
artist = fetchArtist("mbid", mbid, tx)
|
||||
if artist.Uuid == "" {
|
||||
err := insertNewArtist(name, mbid, tx)
|
||||
if err != nil {
|
||||
log.Printf("Error inserting artist via MBID %s %+v", name, err)
|
||||
return artist, errors.New("Failed to insert artist")
|
||||
}
|
||||
} else if spotifyId != "" {
|
||||
artist = fetchArtist("spotify_id", spotifyId, tx)
|
||||
}
|
||||
|
||||
artist = fetchArtist("mbid", mbid, tx)
|
||||
}
|
||||
} else {
|
||||
// If it didn't match above, lookup by name
|
||||
if artist.Uuid == "" {
|
||||
artist = fetchArtist("name", name, tx)
|
||||
if artist.Uuid == "" {
|
||||
err := insertNewArtist(name, mbid, tx)
|
||||
if err != nil {
|
||||
log.Printf("Error inserting artist via Name %s %+v", name, err)
|
||||
return artist, errors.New("Failed to insert artist")
|
||||
}
|
||||
}
|
||||
|
||||
artist = fetchArtist("name", name, tx)
|
||||
// If we can't find it. Lets add it!
|
||||
if artist.Uuid == "" {
|
||||
err := insertNewArtist(name, mbid, spotifyId, tx)
|
||||
if err != nil {
|
||||
log.Printf("Error inserting artist: %+v", err)
|
||||
return artist, errors.New("Failed to insert artist")
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the recently inserted artist to get the UUID
|
||||
if mbid != "" {
|
||||
artist = fetchArtist("mbid", mbid, tx)
|
||||
} else if spotifyId != "" {
|
||||
artist = fetchArtist("spotify_id", spotifyId, tx)
|
||||
}
|
||||
|
||||
if artist.Uuid == "" {
|
||||
artist = fetchArtist("name", name, tx)
|
||||
}
|
||||
|
||||
if artist.Uuid == "" {
|
||||
return artist, errors.New("Unable to fetch artist!")
|
||||
}
|
||||
@ -64,9 +73,15 @@ func fetchArtist(col string, val string, tx *sql.Tx) Artist {
|
||||
return artist
|
||||
}
|
||||
|
||||
func insertNewArtist(name string, mbid string, tx *sql.Tx) error {
|
||||
_, err := tx.Exec("INSERT INTO `artists` (`uuid`, `name`, `mbid`) "+
|
||||
"VALUES (UUID_TO_BIN(UUID(), true),?,?)", name, mbid)
|
||||
func insertNewArtist(name string, mbid string, spotifyId string, tx *sql.Tx) error {
|
||||
_, err := tx.Exec("INSERT INTO `artists` (`uuid`, `name`, `mbid`, `spotify_id`) "+
|
||||
"VALUES (UUID_TO_BIN(UUID(), true),?,?,?)", name, mbid, spotifyId)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func updateArtist(uuid string, col string, val string, tx *sql.Tx) error {
|
||||
_, err := tx.Exec("UPDATE `artists` SET `"+col+"` = ? WHERE `uuid` = UUID_TO_BIN(?,true)", val, uuid)
|
||||
|
||||
return err
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package goscrobble
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@ -55,3 +56,17 @@ func updateConfigValue(key string, value string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getConfigValue(key string) (string, error) {
|
||||
var value string
|
||||
|
||||
err := db.QueryRow("SELECT `value` FROM `config` "+
|
||||
"WHERE `key` = ?",
|
||||
key).Scan(&value)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return value, errors.New("Config key doesn't exist")
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
@ -23,13 +22,8 @@ func InitDb() {
|
||||
dbUser := os.Getenv("MYSQL_USER")
|
||||
dbPass := os.Getenv("MYSQL_PASS")
|
||||
dbName := os.Getenv("MYSQL_DB")
|
||||
timeZone := os.Getenv("TIMEZONE")
|
||||
dbTz := ""
|
||||
if timeZone != "" {
|
||||
dbTz = "&loc=" + strings.Replace(timeZone, "/", fmt.Sprintf("%%2F"), 1)
|
||||
}
|
||||
|
||||
dbConn, err := sql.Open("mysql", dbUser+":"+dbPass+"@tcp("+dbHost+")/"+dbName+"?multiStatements=true&parseTime=true"+dbTz)
|
||||
dbConn, err := sql.Open("mysql", dbUser+":"+dbPass+"@tcp("+dbHost+")/"+dbName+"?multiStatements=true&parseTime=true&loc=Etc%2FUTC")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -6,12 +6,13 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ParseJellyfinInput - Transform API data into a common struct
|
||||
// ParseJellyfinInput - Transform API data into a common struct. Uses MusicBrainzID primarily
|
||||
func ParseJellyfinInput(userUUID string, data map[string]interface{}, ip net.IP, tx *sql.Tx) error {
|
||||
// Debugging
|
||||
fmt.Printf("%+v", data)
|
||||
// fmt.Printf("%+v", data)
|
||||
|
||||
if data["ItemType"] != "Audio" {
|
||||
return errors.New("Media type not audio")
|
||||
@ -31,7 +32,7 @@ func ParseJellyfinInput(userUUID string, data map[string]interface{}, ip net.IP,
|
||||
}
|
||||
|
||||
// Insert artist if not exist
|
||||
artist, err := insertArtist(fmt.Sprintf("%s", data["Artist"]), fmt.Sprintf("%s", data["Provider_musicbrainzartist"]), tx)
|
||||
artist, err := insertArtist(fmt.Sprintf("%s", data["Artist"]), fmt.Sprintf("%s", data["Provider_musicbrainzartist"]), "", tx)
|
||||
if err != nil {
|
||||
log.Printf("%+v", err)
|
||||
return errors.New("Failed to map artist")
|
||||
@ -39,30 +40,29 @@ func ParseJellyfinInput(userUUID string, data map[string]interface{}, ip net.IP,
|
||||
|
||||
// Insert album if not exist
|
||||
artists := []string{artist.Uuid}
|
||||
album, err := insertAlbum(fmt.Sprintf("%s", data["Album"]), fmt.Sprintf("%s", data["Provider_musicbrainzalbum"]), artists, tx)
|
||||
album, err := insertAlbum(fmt.Sprintf("%s", data["Album"]), fmt.Sprintf("%s", data["Provider_musicbrainzalbum"]), "", artists, tx)
|
||||
if err != nil {
|
||||
log.Printf("%+v", err)
|
||||
return errors.New("Failed to map album")
|
||||
}
|
||||
|
||||
// Insert album if not exist
|
||||
track, err := insertTrack(fmt.Sprintf("%s", data["Name"]), fmt.Sprintf("%s", data["Provider_musicbrainztrack"]), album.Uuid, artists, tx)
|
||||
// Insert track if not exist
|
||||
length := timestampToSeconds(fmt.Sprintf("%s", data["RunTime"]))
|
||||
track, err := insertTrack(fmt.Sprintf("%s", data["Name"]), length, fmt.Sprintf("%s", data["Provider_musicbrainztrack"]), "", album.Uuid, artists, tx)
|
||||
if err != nil {
|
||||
log.Printf("%+v", err)
|
||||
return errors.New("Failed to map track")
|
||||
}
|
||||
|
||||
// Insert album if not exist
|
||||
err = insertScrobble(userUUID, track.Uuid, "jellyfin", ip, tx)
|
||||
// Insert scrobble if not exist
|
||||
timestamp := time.Now()
|
||||
fmt.Println(timestamp)
|
||||
err = insertScrobble(userUUID, track.Uuid, "jellyfin", timestamp, ip, tx)
|
||||
if err != nil {
|
||||
log.Printf("%+v", err)
|
||||
return errors.New("Failed to map track")
|
||||
}
|
||||
|
||||
_ = album
|
||||
_ = artist
|
||||
_ = track
|
||||
|
||||
// Insert track if not exist
|
||||
return nil
|
||||
}
|
||||
|
202
internal/goscrobble/ingress_spotify.go
Normal file
202
internal/goscrobble/ingress_spotify.go
Normal file
@ -0,0 +1,202 @@
|
||||
package goscrobble
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/zmb3/spotify"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// updateSpotifyData - Pull data for all users
|
||||
func updateSpotifyData() {
|
||||
// Lets ignore if not configured
|
||||
val, _ := getConfigValue("SPOTIFY_APP_SECRET")
|
||||
if val == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Get all active users with a spotify token
|
||||
users, err := getAllSpotifyUsers()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to fetch spotify users")
|
||||
return
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
user.updateSpotifyPlaydata()
|
||||
}
|
||||
}
|
||||
|
||||
func getSpotifyAuthHandler() spotify.Authenticator {
|
||||
appId, _ := getConfigValue("SPOTIFY_APP_ID")
|
||||
appSecret, _ := getConfigValue("SPOTIFY_APP_SECRET")
|
||||
|
||||
redirectUrl := os.Getenv("GOSCROBBLE_DOMAIN") + "/api/v1/link/spotify"
|
||||
if redirectUrl == "http://localhost:3000/api/v1/link/spotify" {
|
||||
// Handle backend on a different port
|
||||
redirectUrl = "http://localhost:42069/api/v1/link/spotify"
|
||||
}
|
||||
|
||||
auth := spotify.NewAuthenticator(redirectUrl,
|
||||
spotify.ScopeUserReadRecentlyPlayed, spotify.ScopeUserReadCurrentlyPlaying)
|
||||
|
||||
auth.SetAuthInfo(appId, appSecret)
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
func connectSpotifyResponse(r *http.Request) error {
|
||||
urlParams := r.URL.Query()
|
||||
userUuid := urlParams["state"][0]
|
||||
|
||||
// TODO: Add validation user exists here
|
||||
|
||||
auth := getSpotifyAuthHandler()
|
||||
token, err := auth.Token(userUuid, r)
|
||||
if err != nil {
|
||||
fmt.Printf("%+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get displayName
|
||||
client := auth.NewClient(token)
|
||||
client.AutoRetry = true
|
||||
spotifyUser, err := client.CurrentUser()
|
||||
|
||||
// Lets pull in last 30 minutes
|
||||
time := time.Now().UTC().Add(-(time.Duration(30) * time.Minute))
|
||||
err = insertOauthToken(userUuid, "spotify", token.AccessToken, token.RefreshToken, token.Expiry, spotifyUser.DisplayName, time)
|
||||
if err != nil {
|
||||
fmt.Printf("%+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (user *User) updateSpotifyPlaydata() {
|
||||
dbToken, err := user.getSpotifyTokens()
|
||||
if err != nil {
|
||||
fmt.Printf("No spotify token for user: %+v %+v", user.Username, err)
|
||||
return
|
||||
}
|
||||
|
||||
token := new(oauth2.Token)
|
||||
token.AccessToken = dbToken.AccessToken
|
||||
token.RefreshToken = dbToken.RefreshToken
|
||||
token.Expiry = dbToken.Expiry
|
||||
token.TokenType = "Bearer"
|
||||
|
||||
auth := getSpotifyAuthHandler()
|
||||
client := auth.NewClient(token)
|
||||
client.AutoRetry = true
|
||||
|
||||
// Only fetch tracks since last sync
|
||||
opts := spotify.RecentlyPlayedOptions{
|
||||
AfterEpochMs: dbToken.LastSynced.UnixNano() / int64(time.Millisecond),
|
||||
}
|
||||
|
||||
// We want the next sync timestamp from before we call
|
||||
// so we don't end up with a few seconds gap
|
||||
currTime := time.Now()
|
||||
items, err := client.PlayerRecentlyPlayedOpt(&opts)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Printf("Unable to get recently played tracks for user: %+v", user.Username)
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range items {
|
||||
if !checkIfSpotifyAlreadyScrobbled(user.UUID, v) {
|
||||
tx, _ := db.Begin()
|
||||
err := ParseSpotifyInput(user.UUID, v, client, tx)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to insert Spotify scrobble: %+v", err)
|
||||
tx.Rollback()
|
||||
break
|
||||
}
|
||||
tx.Commit()
|
||||
fmt.Printf("Updated spotify track: %+v", v.Track.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if token has changed.. if so, save it to db
|
||||
currToken, err := client.Token()
|
||||
err = insertOauthToken(user.UUID, "spotify", currToken.AccessToken, currToken.RefreshToken, currToken.Expiry, dbToken.Username, currTime)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to update spotify token in database")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func checkIfSpotifyAlreadyScrobbled(userUuid string, data spotify.RecentlyPlayedItem) bool {
|
||||
return checkIfScrobbleExists(userUuid, data.PlayedAt, "spotify")
|
||||
}
|
||||
|
||||
// ParseSpotifyInput - Transform API data
|
||||
func ParseSpotifyInput(userUUID string, data spotify.RecentlyPlayedItem, client spotify.Client, tx *sql.Tx) error {
|
||||
artists := make([]string, 0)
|
||||
albumartists := make([]string, 0)
|
||||
|
||||
// Insert track artists
|
||||
for _, artist := range data.Track.Artists {
|
||||
artist, err := insertArtist(artist.Name, "", artist.ID.String(), tx)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("%+v", err)
|
||||
return errors.New("Failed to map artist: " + artist.Name)
|
||||
}
|
||||
artists = append(artists, artist.Uuid)
|
||||
}
|
||||
|
||||
// Get full track data (album / track info)
|
||||
fulltrack, err := client.GetTrack(data.Track.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to get full track info from spotify: %+v", data.Track.Name)
|
||||
return errors.New("Failed to get full track info from spotify: " + data.Track.Name)
|
||||
}
|
||||
|
||||
// Insert album artists
|
||||
for _, artist := range fulltrack.Album.Artists {
|
||||
albumartist, err := insertArtist(artist.Name, "", artist.ID.String(), tx)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("%+v", err)
|
||||
return errors.New("Failed to map album: " + artist.Name)
|
||||
}
|
||||
albumartists = append(albumartists, albumartist.Uuid)
|
||||
}
|
||||
|
||||
// Insert album if not exist
|
||||
album, err := insertAlbum(fulltrack.Album.Name, "", fulltrack.Album.ID.String(), albumartists, tx)
|
||||
if err != nil {
|
||||
log.Printf("%+v", err)
|
||||
return errors.New("Failed to map album")
|
||||
}
|
||||
|
||||
// Insert track if not exist
|
||||
length := int(fulltrack.Duration / 60)
|
||||
track, err := insertTrack(fulltrack.Name, length, "", fulltrack.ID.String(), album.Uuid, artists, tx)
|
||||
if err != nil {
|
||||
log.Printf("%+v", err)
|
||||
return errors.New("Failed to map track")
|
||||
}
|
||||
|
||||
// Insert scrobble if not exist
|
||||
ip := net.ParseIP("0.0.0.0")
|
||||
err = insertScrobble(userUUID, track.Uuid, "spotify", data.PlayedAt, ip, tx)
|
||||
if err != nil {
|
||||
log.Printf("%+v", err)
|
||||
return errors.New("Failed to map track")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
44
internal/goscrobble/oauth_tokens.go
Normal file
44
internal/goscrobble/oauth_tokens.go
Normal file
@ -0,0 +1,44 @@
|
||||
package goscrobble
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type OauthToken struct {
|
||||
UserUUID string `json:"user"`
|
||||
Service string `json:"service"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Expiry time.Time `json:"expiry"`
|
||||
Username string `json:"username"`
|
||||
LastSynced time.Time `json:"last_synced"`
|
||||
}
|
||||
|
||||
func getOauthToken(userUuid string, service string) (OauthToken, error) {
|
||||
var oauth OauthToken
|
||||
|
||||
err := db.QueryRow("SELECT BIN_TO_UUID(`user`, true), `service`, `access_token`, `refresh_token`, `expiry`, `username`, `last_synced` FROM `oauth_tokens` "+
|
||||
"WHERE `user` = UUID_TO_BIN(?, true) AND `service` = ?",
|
||||
userUuid, service).Scan(&oauth.UserUUID, &oauth.Service, &oauth.AccessToken, &oauth.RefreshToken, &oauth.Expiry, &oauth.Username, &oauth.LastSynced)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return oauth, errors.New("No token for user")
|
||||
}
|
||||
|
||||
return oauth, nil
|
||||
}
|
||||
|
||||
func insertOauthToken(userUuid string, service string, token string, refresh string, expiry time.Time, username string, lastSynced time.Time) error {
|
||||
_, err := db.Exec("REPLACE INTO `oauth_tokens` (`user`, `service`, `access_token`, `refresh_token`, `expiry`, `username`, `last_synced`) "+
|
||||
"VALUES (UUID_TO_BIN(?, true),?,?,?,?,?,?)", userUuid, service, token, refresh, expiry, username, lastSynced)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func removeOauthToken(userUuid string, service string) error {
|
||||
_, err := db.Exec("DELETE FROM `oauth_tokens` WHERE `user` = UUID_TO_BIN(?, true) AND `service` = ?", userUuid, service)
|
||||
|
||||
return err
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package goscrobble
|
||||
|
||||
type ProfileResponse struct {
|
||||
UUID string `json:"uuid"`
|
||||
Username string `json:"username"`
|
||||
Scrobbles []ScrobbleRequestItem `json:"scrobbles"`
|
||||
UUID string `json:"uuid"`
|
||||
Username string `json:"username"`
|
||||
Scrobbles []ScrobbleResponseItem `json:"scrobbles"`
|
||||
}
|
||||
|
||||
func getProfile(user User) (ProfileResponse, error) {
|
||||
|
@ -3,6 +3,7 @@ package goscrobble
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
@ -16,28 +17,29 @@ type Scrobble struct {
|
||||
Track string `json:"track"`
|
||||
}
|
||||
|
||||
type ScrobbleRequest struct {
|
||||
Meta ScrobbleRequestMeta `json:"meta"`
|
||||
Items []ScrobbleRequestItem `json:"items"`
|
||||
type ScrobbleResponse struct {
|
||||
Meta ScrobbleResponseMeta `json:"meta"`
|
||||
Items []ScrobbleResponseItem `json:"items"`
|
||||
}
|
||||
|
||||
type ScrobbleRequestMeta struct {
|
||||
type ScrobbleResponseMeta struct {
|
||||
Count int `json:"count"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
}
|
||||
|
||||
type ScrobbleRequestItem struct {
|
||||
type ScrobbleResponseItem struct {
|
||||
UUID string `json:"uuid"`
|
||||
Timestamp time.Time `json:"time"`
|
||||
Artist string `json:"artist"`
|
||||
Album string `json:"album"`
|
||||
Track string `json:"track"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// insertScrobble - This will return if it exists or create it based on MBID > Name
|
||||
func insertScrobble(user string, track string, source string, ip net.IP, tx *sql.Tx) error {
|
||||
err := insertNewScrobble(user, track, source, ip, tx)
|
||||
func insertScrobble(user string, track string, source string, timestamp time.Time, ip net.IP, tx *sql.Tx) error {
|
||||
err := insertNewScrobble(user, track, source, timestamp, ip, tx)
|
||||
if err != nil {
|
||||
log.Printf("Error inserting scrobble %s %+v", user, err)
|
||||
return errors.New("Failed to insert scrobble!")
|
||||
@ -46,8 +48,8 @@ func insertScrobble(user string, track string, source string, ip net.IP, tx *sql
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleRequest, error) {
|
||||
scrobbleReq := ScrobbleRequest{}
|
||||
func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleResponse, error) {
|
||||
scrobbleReq := ScrobbleResponse{}
|
||||
var count int
|
||||
|
||||
// Yeah this isn't great. But for now.. it works! Cache later
|
||||
@ -59,7 +61,8 @@ func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleReques
|
||||
"JOIN artists ON track_artist.artist = artists.uuid "+
|
||||
"JOIN albums ON track_album.album = albums.uuid "+
|
||||
"JOIN users ON scrobbles.user = users.uuid "+
|
||||
"WHERE user = UUID_TO_BIN(?, true)",
|
||||
"WHERE user = UUID_TO_BIN(?, true) "+
|
||||
"GROUP BY scrobbles.uuid",
|
||||
userUuid)
|
||||
|
||||
if err != nil {
|
||||
@ -68,7 +71,7 @@ func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleReques
|
||||
}
|
||||
|
||||
rows, err := db.Query(
|
||||
"SELECT BIN_TO_UUID(`scrobbles`.`uuid`, true), `scrobbles`.`created_at`, `artists`.`name`, `albums`.`name`,`tracks`.`name` FROM `scrobbles` "+
|
||||
"SELECT BIN_TO_UUID(`scrobbles`.`uuid`, true), `scrobbles`.`created_at`, `artists`.`name`, `albums`.`name`,`tracks`.`name`, `scrobbles`.`source` FROM `scrobbles` "+
|
||||
"JOIN tracks ON scrobbles.track = tracks.uuid "+
|
||||
"JOIN track_artist ON track_artist.track = tracks.uuid "+
|
||||
"JOIN track_album ON track_album.track = tracks.uuid "+
|
||||
@ -85,8 +88,8 @@ func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleReques
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
item := ScrobbleRequestItem{}
|
||||
err := rows.Scan(&item.UUID, &item.Timestamp, &item.Artist, &item.Album, &item.Track)
|
||||
item := ScrobbleResponseItem{}
|
||||
err := rows.Scan(&item.UUID, &item.Timestamp, &item.Artist, &item.Album, &item.Track, &item.Source)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch scrobbles: %+v", err)
|
||||
return scrobbleReq, errors.New("Failed to fetch scrobbles")
|
||||
@ -108,9 +111,21 @@ func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleReques
|
||||
return scrobbleReq, nil
|
||||
}
|
||||
|
||||
func insertNewScrobble(user string, track string, source string, ip net.IP, tx *sql.Tx) error {
|
||||
func insertNewScrobble(user string, track string, source string, timestamp time.Time, ip net.IP, tx *sql.Tx) error {
|
||||
_, err := tx.Exec("INSERT INTO `scrobbles` (`uuid`, `created_at`, `created_ip`, `user`, `track`, `source`) "+
|
||||
"VALUES (UUID_TO_BIN(UUID(), true), NOW(), ?, UUID_TO_BIN(?, true),UUID_TO_BIN(?, true), ?)", ip, user, track, source)
|
||||
"VALUES (UUID_TO_BIN(UUID(), true), ?, ?, UUID_TO_BIN(?, true), UUID_TO_BIN(?, true), ?)", timestamp, ip, user, track, source)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func checkIfScrobbleExists(userUuid string, timestamp time.Time, source string) bool {
|
||||
count, err := getDbCount("SELECT COUNT(*) FROM `scrobbles` WHERE `user` = UUID_TO_BIN(?, true) AND `created_at` = ? AND `source` = ?",
|
||||
userUuid, timestamp, source)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching scrobble exists count: %+v", err)
|
||||
return true
|
||||
}
|
||||
|
||||
return count != 0
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@ -32,20 +33,22 @@ func HandleRequests(port string) {
|
||||
v1 := r.PathPrefix("/api/v1").Subrouter()
|
||||
|
||||
// Static Token for /ingress
|
||||
v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(handleIngress)).Methods("POST")
|
||||
v1.HandleFunc("/ingress/multiscrobbler", tokenMiddleware(handleIngress)).Methods("POST")
|
||||
v1.HandleFunc("/ingress/jellyfin", limitMiddleware(tokenMiddleware(handleIngress), lightLimiter)).Methods("POST")
|
||||
v1.HandleFunc("/ingress/multiscrobbler", limitMiddleware(tokenMiddleware(handleIngress), lightLimiter)).Methods("POST")
|
||||
|
||||
// JWT Auth - PWN PROFILE ONLY.
|
||||
v1.HandleFunc("/user", jwtMiddleware(fetchUser)).Methods("GET")
|
||||
// JWT Auth - Own profile only (Uses uuid in JWT)
|
||||
v1.HandleFunc("/user", limitMiddleware(jwtMiddleware(fetchUser), lightLimiter)).Methods("GET")
|
||||
// v1.HandleFunc("/user", jwtMiddleware(fetchScrobbleResponse)).Methods("PATCH")
|
||||
v1.HandleFunc("/user/spotify", limitMiddleware(jwtMiddleware(getSpotifyClientID), lightLimiter)).Methods("GET")
|
||||
v1.HandleFunc("/user/spotify", limitMiddleware(jwtMiddleware(deleteSpotifyLink), lightLimiter)).Methods("DELETE")
|
||||
v1.HandleFunc("/user/{uuid}/scrobbles", jwtMiddleware(fetchScrobbleResponse)).Methods("GET")
|
||||
|
||||
// Config auth
|
||||
v1.HandleFunc("/config", adminMiddleware(fetchConfig)).Methods("GET")
|
||||
v1.HandleFunc("/config", adminMiddleware(postConfig)).Methods("POST")
|
||||
v1.HandleFunc("/config", limitMiddleware(adminMiddleware(fetchConfig), standardLimiter)).Methods("GET")
|
||||
v1.HandleFunc("/config", limitMiddleware(adminMiddleware(postConfig), standardLimiter)).Methods("POST")
|
||||
|
||||
// No Auth
|
||||
v1.HandleFunc("/stats", handleStats).Methods("GET")
|
||||
v1.HandleFunc("/stats", limitMiddleware(handleStats, lightLimiter)).Methods("GET")
|
||||
v1.HandleFunc("/profile/{username}", limitMiddleware(fetchProfile, lightLimiter)).Methods("GET")
|
||||
|
||||
v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST")
|
||||
@ -53,6 +56,9 @@ func HandleRequests(port string) {
|
||||
v1.HandleFunc("/sendreset", limitMiddleware(handleSendReset, heavyLimiter)).Methods("POST")
|
||||
v1.HandleFunc("/resetpassword", limitMiddleware(handleResetPassword, heavyLimiter)).Methods("POST")
|
||||
|
||||
// Redirect from Spotify Oauth
|
||||
v1.HandleFunc("/link/spotify", limitMiddleware(postSpotifyReponse, lightLimiter))
|
||||
|
||||
// This just prevents it serving frontend stuff over /api
|
||||
r.PathPrefix("/api")
|
||||
|
||||
@ -70,6 +76,8 @@ func HandleRequests(port string) {
|
||||
|
||||
// Serve it up!
|
||||
fmt.Printf("Goscrobble listening on port %s", port)
|
||||
fmt.Println("")
|
||||
|
||||
log.Fatal(http.ListenAndServe(":"+port, handler))
|
||||
}
|
||||
|
||||
@ -225,6 +233,7 @@ func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
|
||||
case "jellyfin":
|
||||
err := ParseJellyfinInput(userUuid, bodyJson, ip, tx)
|
||||
if err != nil {
|
||||
fmt.Printf("Err? %+v", err)
|
||||
tx.Rollback()
|
||||
throwOkError(w, err.Error())
|
||||
return
|
||||
@ -269,6 +278,13 @@ func fetchUser(w http.ResponseWriter, r *http.Request, jwtUser string, reqUser s
|
||||
throwOkError(w, "Failed to fetch user information")
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
oauth, err := getOauthToken(user.UUID, "spotify")
|
||||
if err == nil {
|
||||
user.SpotifyUsername = oauth.Username
|
||||
}
|
||||
|
||||
json, _ := json.Marshal(&user)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
@ -351,3 +367,44 @@ func fetchProfile(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
// postSpotifyResponse - Oauth Response from Spotify
|
||||
func postSpotifyReponse(w http.ResponseWriter, r *http.Request) {
|
||||
err := connectSpotifyResponse(r)
|
||||
|
||||
if err != nil {
|
||||
throwOkError(w, "Failed to connect to spotify")
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, os.Getenv("GOSCROBBLE_DOMAIN")+"/user", 302)
|
||||
}
|
||||
|
||||
// getSpotifyClientID - Returns public spotify APP ID
|
||||
func getSpotifyClientID(w http.ResponseWriter, r *http.Request, u string, v string) {
|
||||
key, err := getConfigValue("SPOTIFY_APP_ID")
|
||||
if err != nil {
|
||||
throwOkError(w, "Failed to get Spotify ID")
|
||||
return
|
||||
|
||||
}
|
||||
response := LoginResponse{
|
||||
Token: key,
|
||||
}
|
||||
|
||||
resp, _ := json.Marshal(&response)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(resp)
|
||||
}
|
||||
|
||||
// deleteSpotifyLink - Unlinks spotify account
|
||||
func deleteSpotifyLink(w http.ResponseWriter, r *http.Request, u string, v string) {
|
||||
err := removeOauthToken(u, "spotify")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
throwOkError(w, "Failed to unlink spotify account")
|
||||
return
|
||||
}
|
||||
|
||||
throwOkMessage(w, "Spotify account successfully unlinked")
|
||||
}
|
||||
|
@ -5,10 +5,32 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func ClearTokenTimer() {
|
||||
var endTicker chan bool
|
||||
|
||||
func StartBackgroundWorkers() {
|
||||
endTicker := make(chan bool)
|
||||
|
||||
hourTicker := time.NewTicker(time.Hour)
|
||||
minuteTicker := time.NewTicker(time.Duration(1) * time.Minute)
|
||||
|
||||
go func() {
|
||||
for now := range time.Tick(time.Second) {
|
||||
fmt.Println(now)
|
||||
for {
|
||||
select {
|
||||
case <-endTicker:
|
||||
fmt.Println("Stopping background workers")
|
||||
return
|
||||
case <-hourTicker.C:
|
||||
// Clear old password reset tokens
|
||||
clearOldResetTokens()
|
||||
|
||||
case <-minuteTicker.C:
|
||||
// Update playdata from spotify
|
||||
updateSpotifyData()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func EndBackgroundWorkers() {
|
||||
endTicker <- true
|
||||
}
|
||||
|
@ -9,44 +9,50 @@ import (
|
||||
type Track struct {
|
||||
Uuid string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Length int `json:"length"`
|
||||
Desc sql.NullString `json:"desc"`
|
||||
Img sql.NullString `json:"img"`
|
||||
MusicBrainzID sql.NullString `json:"mbid"`
|
||||
SpotifyID sql.NullString `json:"spotify_id"`
|
||||
}
|
||||
|
||||
// insertTrack - This will return if it exists or create it based on MBID > Name
|
||||
func insertTrack(name string, mbid string, album string, artists []string, tx *sql.Tx) (Track, error) {
|
||||
func insertTrack(name string, legnth int, mbid string, spotifyId string, album string, artists []string, tx *sql.Tx) (Track, error) {
|
||||
track := Track{}
|
||||
|
||||
// Try locate our track
|
||||
if mbid != "" {
|
||||
track = fetchTrack("mbid", mbid, tx)
|
||||
if track.Uuid == "" {
|
||||
err := insertNewTrack(name, mbid, tx)
|
||||
if err != nil {
|
||||
log.Printf("Error inserting track via MBID %s %+v", name, err)
|
||||
return track, errors.New("Failed to insert track")
|
||||
}
|
||||
} else if spotifyId != "" {
|
||||
track = fetchTrack("spotify_id", spotifyId, tx)
|
||||
}
|
||||
|
||||
track = fetchTrack("mbid", mbid, tx)
|
||||
err = track.linkTrack(album, artists, tx)
|
||||
if err != nil {
|
||||
return track, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If it didn't match above, lookup by name
|
||||
if track.Uuid == "" {
|
||||
track = fetchTrack("name", name, tx)
|
||||
if track.Uuid == "" {
|
||||
err := insertNewTrack(name, mbid, tx)
|
||||
if err != nil {
|
||||
log.Printf("Error inserting track via Name %s %+v", name, err)
|
||||
return track, errors.New("Failed to insert track")
|
||||
}
|
||||
}
|
||||
|
||||
// If we can't find it. Lets add it!
|
||||
if track.Uuid == "" {
|
||||
err := insertNewTrack(name, legnth, mbid, spotifyId, tx)
|
||||
if err != nil {
|
||||
return track, errors.New("Failed to insert track")
|
||||
}
|
||||
|
||||
// Fetch the recently inserted track to get the UUID
|
||||
if mbid != "" {
|
||||
track = fetchTrack("mbid", mbid, tx)
|
||||
} else if spotifyId != "" {
|
||||
track = fetchTrack("spotify_id", spotifyId, tx)
|
||||
}
|
||||
|
||||
if track.Uuid == "" {
|
||||
track = fetchTrack("name", name, tx)
|
||||
err = track.linkTrack(album, artists, tx)
|
||||
if err != nil {
|
||||
return track, err
|
||||
}
|
||||
}
|
||||
|
||||
err = track.linkTrack(album, artists, tx)
|
||||
if err != nil {
|
||||
return track, errors.New("Unable to link tracks!")
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,9 +78,9 @@ func fetchTrack(col string, val string, tx *sql.Tx) Track {
|
||||
return track
|
||||
}
|
||||
|
||||
func insertNewTrack(name string, mbid string, tx *sql.Tx) error {
|
||||
_, err := tx.Exec("INSERT INTO `tracks` (`uuid`, `name`, `mbid`) "+
|
||||
"VALUES (UUID_TO_BIN(UUID(), true),?,?)", name, mbid)
|
||||
func insertNewTrack(name string, length int, mbid string, spotifyId string, tx *sql.Tx) error {
|
||||
_, err := tx.Exec("INSERT INTO `tracks` (`uuid`, `name`, `length`, `mbid`, `spotify_id`) "+
|
||||
"VALUES (UUID_TO_BIN(UUID(), true),?,?,?,?)", name, length, mbid, spotifyId)
|
||||
|
||||
return err
|
||||
}
|
||||
@ -110,3 +116,9 @@ func (track Track) linkTrackToArtists(artists []string, tx *sql.Tx) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateTrack(uuid string, col string, val string, tx *sql.Tx) error {
|
||||
_, err := tx.Exec("UPDATE `tracks` SET `"+col+"` = ? WHERE `uuid` = UUID_TO_BIN(?,true)", val, uuid)
|
||||
|
||||
return err
|
||||
}
|
||||
|
@ -28,17 +28,20 @@ type User struct {
|
||||
Verified bool `json:"verified"`
|
||||
Active bool `json:"active"`
|
||||
Admin bool `json:"admin"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
|
||||
type UserResponse struct {
|
||||
UUID string `json:"uuid"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
CreatedIp net.IP `json:"created_ip"`
|
||||
ModifiedAt time.Time `json:"modified_at"`
|
||||
ModifiedIP net.IP `jsos:"modified_ip"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Verified bool `json:"verified"`
|
||||
UUID string `json:"uuid"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
CreatedIp net.IP `json:"created_ip"`
|
||||
ModifiedAt time.Time `json:"modified_at"`
|
||||
ModifiedIP net.IP `jsos:"modified_ip"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Verified bool `json:"verified"`
|
||||
SpotifyUsername string `json:"spotify_username"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
|
||||
// RegisterRequest - Incoming JSON
|
||||
@ -208,8 +211,8 @@ func userAlreadyExists(req *RegisterRequest) bool {
|
||||
|
||||
func getUser(uuid string) (User, error) {
|
||||
var user User
|
||||
err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin` FROM `users` WHERE `uuid` = UUID_TO_BIN(?, true) AND `active` = 1",
|
||||
uuid).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin)
|
||||
err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin`, `timezone` FROM `users` WHERE `uuid` = UUID_TO_BIN(?, true) AND `active` = 1",
|
||||
uuid).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin, &user.Timezone)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return user, errors.New("Invalid JWT Token")
|
||||
@ -220,8 +223,8 @@ func getUser(uuid string) (User, error) {
|
||||
|
||||
func getUserByUsername(username string) (User, error) {
|
||||
var user User
|
||||
err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin` FROM `users` WHERE `username` = ? AND `active` = 1",
|
||||
username).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin)
|
||||
err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin`, `timezone` FROM `users` WHERE `username` = ? AND `active` = 1",
|
||||
username).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin, &user.Timezone)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return user, errors.New("Invalid Username")
|
||||
@ -232,8 +235,8 @@ func getUserByUsername(username string) (User, error) {
|
||||
|
||||
func getUserByEmail(email string) (User, error) {
|
||||
var user User
|
||||
err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin` FROM `users` WHERE `email` = ? AND `active` = 1",
|
||||
email).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin)
|
||||
err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin`, `timezone` FROM `users` WHERE `email` = ? AND `active` = 1",
|
||||
email).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin, &user.Timezone)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return user, errors.New("Invalid Email")
|
||||
@ -244,11 +247,9 @@ func getUserByEmail(email string) (User, error) {
|
||||
|
||||
func getUserByResetToken(token string) (User, error) {
|
||||
var user User
|
||||
err := db.QueryRow("SELECT BIN_TO_UUID(`users`.`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin` FROM `users` "+
|
||||
err := db.QueryRow("SELECT BIN_TO_UUID(`users`.`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin`, timezone FROM `users` "+
|
||||
"JOIN `resettoken` ON `resettoken`.`user` = `users`.`uuid` WHERE `resettoken`.`token` = ? AND `active` = 1",
|
||||
token).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin)
|
||||
|
||||
fmt.Println(err)
|
||||
token).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin, &user.Timezone)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return user, errors.New("Invalid Token")
|
||||
@ -256,11 +257,12 @@ func getUserByResetToken(token string) (User, error) {
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (user *User) sendResetEmail(ip net.IP) error {
|
||||
token := generateToken(16)
|
||||
|
||||
// 24 hours
|
||||
exp := time.Now().AddDate(0, 0, 1)
|
||||
// 1 hour validation
|
||||
exp := time.Now().Add(time.Hour * time.Duration(1))
|
||||
err := user.saveResetToken(token, exp)
|
||||
|
||||
if err != nil {
|
||||
@ -268,7 +270,9 @@ func (user *User) sendResetEmail(ip net.IP) error {
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(
|
||||
"Someone at %s has request a password reset for %s. Click the following link to reset your password: %s/reset/%s",
|
||||
"Someone at %s has request a password reset for %s.\n"+
|
||||
"Click the following link to reset your password: %s/reset/%s\n\n"+
|
||||
"This is link is valid for 1 hour",
|
||||
ip, user.Username, os.Getenv("GOSCROBBLE_DOMAIN"), token)
|
||||
|
||||
return sendEmail(user.Username, user.Email, "GoScrobble - Password Reset", content)
|
||||
@ -316,3 +320,39 @@ func (user *User) updatePassword(newPassword string, ip net.IP) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (user *User) getSpotifyTokens() (OauthToken, error) {
|
||||
return getOauthToken(user.UUID, "spotify")
|
||||
}
|
||||
|
||||
func getAllSpotifyUsers() ([]User, error) {
|
||||
users := make([]User, 0)
|
||||
rows, err := db.Query("SELECT BIN_TO_UUID(`users`.`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `users`.`username`, `email`, `password`, `verified`, `admin`, `timezone` FROM `users` " +
|
||||
"JOIN `oauth_tokens` ON `oauth_tokens`.`user` = `users`.`uuid` AND `oauth_tokens`.`service` = 'spotify' WHERE `users`.`active` = 1")
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch spotify users: %+v", err)
|
||||
return users, errors.New("Failed to fetch configs")
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var user User
|
||||
err := rows.Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin, &user.Timezone)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch spotify user: %+v", err)
|
||||
return users, errors.New("Failed to fetch users")
|
||||
}
|
||||
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch spotify users: %+v", err)
|
||||
return users, errors.New("Failed to fetch users")
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
@ -111,6 +111,36 @@ func Inet6_Aton(ip net.IP) string {
|
||||
return ipHex
|
||||
}
|
||||
|
||||
// calcPageOffsetString - Used to SQL paging
|
||||
func calcPageOffsetString(page int, offset int) string {
|
||||
return fmt.Sprintf("%d", page*offset)
|
||||
}
|
||||
|
||||
// timestampToSeconds - Converts HH:MM:SS to (int)seconds
|
||||
func timestampToSeconds(timestamp string) int {
|
||||
var h, m, s int
|
||||
n, err := fmt.Sscanf(timestamp, "%d:%d:%d", &h, &m, &s)
|
||||
if err != nil || n != 3 {
|
||||
return 0
|
||||
}
|
||||
return h*3600 + m*60 + s
|
||||
}
|
||||
|
||||
func filterSlice(s []string) []string {
|
||||
m := make(map[string]bool)
|
||||
for _, item := range s {
|
||||
if item != "" {
|
||||
if _, ok := m[strings.TrimSpace(item)]; !ok {
|
||||
m[strings.TrimSpace(item)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result []string
|
||||
for item, _ := range m {
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
fmt.Printf("RESTULS: %+v", result)
|
||||
return result
|
||||
}
|
||||
|
1
migrations/10_tracklength.down.sql
Normal file
1
migrations/10_tracklength.down.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE `tracks` DROP COLUMN `length`;
|
1
migrations/10_tracklength.up.sql
Normal file
1
migrations/10_tracklength.up.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE `tracks` ADD COLUMN `length` INT NOT NULL DEFAULT 0;
|
@ -11,6 +11,7 @@ CREATE TABLE IF NOT EXISTS `users` (
|
||||
`active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`admin` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`private` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`timezone` VARCHAR(100) NOT NULL DEFAULT 'Etc/UTC',
|
||||
KEY `usernameLookup` (`username`, `active`),
|
||||
KEY `emailLookup` (`email`, `active`)
|
||||
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
1
migrations/8_oauth.down.sql
Normal file
1
migrations/8_oauth.down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS `oauth_tokens`;
|
10
migrations/8_oauth.up.sql
Normal file
10
migrations/8_oauth.up.sql
Normal file
@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS `oauth_tokens` (
|
||||
`user` BINARY(16),
|
||||
`service` VARCHAR(64) NOT NULL,
|
||||
`access_token` VARCHAR(255) NULL DEFAULT '',
|
||||
`refresh_token` VARCHAR(255) NULL DEFAULT '',
|
||||
`expiry` DATETIME NOT NULL,
|
||||
`username` VARCHAR(100) NULL DEFAULT '',
|
||||
`last_synced` DATETIME NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY `userService` (`user`, `service`)
|
||||
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
8
migrations/9_spotify.down.sql
Normal file
8
migrations/9_spotify.down.sql
Normal file
@ -0,0 +1,8 @@
|
||||
START TRANSACTION;
|
||||
|
||||
ALTER TABLE `tracks` DROP COLUMN `spotify_id`;
|
||||
ALTER TABLE `users` DROP COLUMN `spotify_id`;
|
||||
ALTER TABLE `albums` DROP COLUMN `spotify_id`;
|
||||
ALTER TABLE `artists` DROP COLUMN `spotify_id`;
|
||||
|
||||
COMMIT;
|
13
migrations/9_spotify.up.sql
Normal file
13
migrations/9_spotify.up.sql
Normal file
@ -0,0 +1,13 @@
|
||||
START TRANSACTION;
|
||||
|
||||
ALTER TABLE `users` ADD COLUMN `spotify_id` VARCHAR(255) DEFAULT '';
|
||||
ALTER TABLE `albums` ADD COLUMN `spotify_id` VARCHAR(255) DEFAULT '';
|
||||
ALTER TABLE `artists` ADD COLUMN `spotify_id` VARCHAR(255) DEFAULT '';
|
||||
ALTER TABLE `tracks` ADD COLUMN `spotify_id` VARCHAR(255) DEFAULT '';
|
||||
|
||||
ALTER TABLE `users` ADD INDEX `spotifyLookup` (`spotify_id`);
|
||||
ALTER TABLE `albums` ADD INDEX `spotifyLookup` (`spotify_id`);
|
||||
ALTER TABLE `artists` ADD INDEX `spotifyLookup` (`spotify_id`);
|
||||
ALTER TABLE `tracks` ADD INDEX `spotifyLookup` (`spotify_id`);
|
||||
|
||||
COMMIT;
|
@ -7,12 +7,38 @@ function getHeaders() {
|
||||
const user = JSON.parse(localStorage.getItem('user'));
|
||||
|
||||
if (user && user.jwt) {
|
||||
return { Authorization: 'Bearer ' + user.jwt };
|
||||
return { Authorization: 'Bearer ' + user.jwt, changeOrigin: true };
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function getUserUuid() {
|
||||
// Todo: move this to use Context values instead.
|
||||
const user = JSON.parse(localStorage.getItem('user'));
|
||||
|
||||
if (user && user.uuid) {
|
||||
return user.uuid
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function handleErrorResp(error) {
|
||||
if (error.response) {
|
||||
if (error.response.status === 401) {
|
||||
toast.error('Unauthorized')
|
||||
} else if (error.response.status === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
toast.error('An unknown error has occurred');
|
||||
}
|
||||
} else {
|
||||
toast.error('Failed to connect to API');
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export const PostLogin = (formValues) => {
|
||||
return axios.post(process.env.REACT_APP_API_URL + "login", formValues)
|
||||
.then((response) => {
|
||||
@ -34,7 +60,7 @@ export const PostLogin = (formValues) => {
|
||||
}
|
||||
}).catch((error) => {
|
||||
if (error.response === 401) {
|
||||
return {};
|
||||
toast.error('Unauthorized')
|
||||
} else if (error.response === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
@ -57,14 +83,8 @@ export const PostRegister = (formValues) => {
|
||||
return Promise.reject();
|
||||
}
|
||||
}).catch((error) => {
|
||||
if (error.response === 401) {
|
||||
return {};
|
||||
} else if (error.response === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
toast.error('Failed to connect');
|
||||
}
|
||||
return Promise.resolve();
|
||||
handleErrorResp(error)
|
||||
return Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
@ -81,15 +101,9 @@ export const PostResetPassword = (formValues) => {
|
||||
return Promise.reject();
|
||||
}
|
||||
}).catch((error) => {
|
||||
if (error.response === 401) {
|
||||
return {};
|
||||
} else if (error.response === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
toast.error('Failed to connect');
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
handleErrorResp(error)
|
||||
return Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
export const sendPasswordReset = (values) => {
|
||||
@ -97,14 +111,7 @@ export const sendPasswordReset = (values) => {
|
||||
(data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
if (error.response === 401) {
|
||||
return {};
|
||||
} else if (error.response === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
toast.error('Failed to connect');
|
||||
}
|
||||
return {};
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
@ -113,14 +120,7 @@ export const getStats = () => {
|
||||
(data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
if (error.response === 401) {
|
||||
return {};
|
||||
} else if (error.response === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
toast.error('Failed to connect');
|
||||
}
|
||||
return {};
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
@ -129,14 +129,7 @@ export const getRecentScrobbles = (id) => {
|
||||
.then((data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
if (error.response === 401) {
|
||||
return {};
|
||||
} else if (error.response === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
toast.error('Failed to connect');
|
||||
}
|
||||
return {};
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
@ -145,14 +138,7 @@ export const getConfigs = () => {
|
||||
.then((data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
if (error.response === 401) {
|
||||
return {};
|
||||
} else if (error.response === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
toast.error('Failed to connect');
|
||||
}
|
||||
return {};
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
@ -171,15 +157,8 @@ export const postConfigs = (values, toggle) => {
|
||||
toast.error(data.data.error);
|
||||
}
|
||||
}).catch((error) => {
|
||||
if (error.response === 401) {
|
||||
return {};
|
||||
} else if (error.response === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
toast.error('Failed to connect');
|
||||
}
|
||||
return {};
|
||||
});
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const getProfile = (userName) => {
|
||||
@ -187,14 +166,7 @@ export const getProfile = (userName) => {
|
||||
.then((data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
if (error.response === 401) {
|
||||
return {};
|
||||
} else if (error.response === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
toast.error('Failed to connect');
|
||||
}
|
||||
return {};
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
@ -203,34 +175,56 @@ export const getUser = () => {
|
||||
.then((data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
if (error.response === 401) {
|
||||
return {};
|
||||
} else if (error.response === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
toast.error('Failed to connect');
|
||||
}
|
||||
return {};
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const validateResetPassword = (tokenStr) => {
|
||||
return axios.post(process.env.REACT_APP_API_URL + "resetpassword", { token: tokenStr })
|
||||
return axios.get(process.env.REACT_APP_API_URL + "user/", { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
toast.error(data.error);
|
||||
return {valid: false}
|
||||
}
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
if (error.response === 401) {
|
||||
return {};
|
||||
} else if (error.response === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
toast.error('Failed to connect');
|
||||
}
|
||||
return {};
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const getSpotifyClientId = () => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "user/spotify", { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
return data.data
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
}
|
||||
|
||||
export const spotifyConnectionRequest = () => {
|
||||
return getSpotifyClientId().then((resp) => {
|
||||
var scopes = 'user-read-recently-played user-read-currently-playing';
|
||||
|
||||
// Local, lets forward it to API
|
||||
let redirectUri = window.location.origin.toString()+ "/api/v1/link/spotify";
|
||||
|
||||
// Stupid dev
|
||||
if (window.location.origin.toString() === "http://localhost:3000") {
|
||||
redirectUri = "http://localhost:42069/api/v1/link/spotify"
|
||||
}
|
||||
|
||||
window.location = 'https://accounts.spotify.com/authorize' +
|
||||
'?response_type=code' +
|
||||
'&client_id=' + resp.token +
|
||||
'&scope=' + encodeURIComponent(scopes) +
|
||||
'&redirect_uri=' + encodeURIComponent(redirectUri) +
|
||||
'&state=' + getUserUuid();
|
||||
})
|
||||
};
|
||||
|
||||
export const spotifyDisonnectionRequest = () => {
|
||||
return axios.delete(process.env.REACT_APP_API_URL + "user/spotify", { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
toast.success(data.data.message);
|
||||
return true
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ const ScrobbleTable = (props) => {
|
||||
<td>Track</td>
|
||||
<td>Artist</td>
|
||||
<td>Album</td>
|
||||
<td>Source</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -21,6 +22,7 @@ const ScrobbleTable = (props) => {
|
||||
<td>{element.track}</td>
|
||||
<td>{element.artist}</td>
|
||||
<td>{element.album}</td>
|
||||
<td>{element.source}</td>
|
||||
</tr>;
|
||||
})
|
||||
}
|
||||
|
@ -22,8 +22,8 @@ const Admin = () => {
|
||||
if (data.configs) {
|
||||
setConfigs(data.configs);
|
||||
setToggle(data.configs.REGISTRATION_ENABLED === "1")
|
||||
setLoading(false);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
}, [])
|
||||
|
||||
@ -31,6 +31,14 @@ const Admin = () => {
|
||||
setToggle(!toggle);
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
history.push("/login")
|
||||
}
|
||||
|
||||
if (user && !user.admin) {
|
||||
history.push("/Dashboard")
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
@ -39,9 +47,7 @@ const Admin = () => {
|
||||
)
|
||||
}
|
||||
|
||||
if (!user || !user.admin) {
|
||||
history.push("/login")
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
|
@ -8,7 +8,7 @@ import ScrobbleTable from "../Components/ScrobbleTable";
|
||||
import AuthContext from '../Contexts/AuthContext';
|
||||
|
||||
const Dashboard = () => {
|
||||
// const history = useHistory();
|
||||
const history = useHistory();
|
||||
let { user } = useContext(AuthContext);
|
||||
let [loading, setLoading] = useState(true);
|
||||
let [dashboardData, setDashboardData] = useState({});
|
||||
@ -24,6 +24,10 @@ const Dashboard = () => {
|
||||
})
|
||||
}, [user])
|
||||
|
||||
if (!user) {
|
||||
history.push("/login")
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
@ -37,10 +41,12 @@ const Dashboard = () => {
|
||||
<h1>
|
||||
{user.username}'s Dashboard!
|
||||
</h1>
|
||||
<div className="dashboardBody">
|
||||
{loading
|
||||
? <ScaleLoader color="#6AD7E5" size={60} />
|
||||
: <ScrobbleTable data={dashboardData.items} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ const Login = () => {
|
||||
className="loginButton"
|
||||
onClick={redirectReset}
|
||||
disabled={loading}
|
||||
>{loading ? <ScaleLoader color="#FFF" size={35} /> : "Reset Password"}</Button>
|
||||
>Reset Password</Button>
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
|
@ -5,6 +5,8 @@ import { useHistory } from "react-router";
|
||||
import AuthContext from '../Contexts/AuthContext';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { getUser } from '../Api/index'
|
||||
import { Button } from 'reactstrap';
|
||||
import { spotifyConnectionRequest, spotifyDisonnectionRequest } from '../Api/index'
|
||||
|
||||
const User = () => {
|
||||
const history = useHistory();
|
||||
@ -12,6 +14,7 @@ const User = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userdata, setUserdata] = useState({});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
return
|
||||
@ -24,6 +27,10 @@ const User = () => {
|
||||
})
|
||||
}, [user])
|
||||
|
||||
if (!user) {
|
||||
history.push("/login")
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
@ -32,10 +39,6 @@ const User = () => {
|
||||
)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
history.push("/login")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
@ -44,7 +47,25 @@ const User = () => {
|
||||
<p className="userBody">
|
||||
Created At: {userdata.created_at}<br/>
|
||||
Email: {userdata.email}<br/>
|
||||
Verified: {userdata.verified ? '✓' : '✖'}
|
||||
Verified: {userdata.verified ? '✓' : '✖'}<br/>
|
||||
{userdata.spotify_username
|
||||
? <div>Spotify Account: {userdata.spotify_username}<br/><br/>
|
||||
<Button
|
||||
color="secondary"
|
||||
type="button"
|
||||
className="loginButton"
|
||||
onClick={spotifyDisonnectionRequest}
|
||||
>Disconnect Spotify</Button></div>
|
||||
: <div>
|
||||
<br/>
|
||||
<Button
|
||||
color="primary"
|
||||
type="button"
|
||||
className="loginButton"
|
||||
onClick={spotifyConnectionRequest}
|
||||
>Connect To Spotify</Button>
|
||||
</div>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user