mirror of
https://github.com/idanoo/GoScrobble.git
synced 2024-11-24 17:35:16 +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_PREFIX="gs:"
|
||||||
REDIS_AUTH=""
|
REDIS_AUTH=""
|
||||||
|
|
||||||
TIMEZONE=Etc/UTC
|
|
||||||
|
|
||||||
JWT_SECRET=
|
JWT_SECRET=
|
||||||
JWT_EXPIRY=86400
|
JWT_EXPIRY=86400
|
||||||
|
|
||||||
|
@ -12,6 +12,11 @@ import (
|
|||||||
"gitlab.com/idanoo/go-scrobble/internal/goscrobble"
|
"gitlab.com/idanoo/go-scrobble/internal/goscrobble"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Set UTC for errything
|
||||||
|
os.Setenv("TZ", "Etc/UTC")
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Println("Starting goscrobble")
|
fmt.Println("Starting goscrobble")
|
||||||
err := godotenv.Load()
|
err := godotenv.Load()
|
||||||
@ -51,8 +56,9 @@ func main() {
|
|||||||
goscrobble.InitRedis()
|
goscrobble.InitRedis()
|
||||||
defer goscrobble.CloseRedisConn()
|
defer goscrobble.CloseRedisConn()
|
||||||
|
|
||||||
// Clear old reset tokens regularly
|
// Start background workers
|
||||||
// go goscrobble.ClearTokenTimer()
|
go goscrobble.StartBackgroundWorkers()
|
||||||
|
defer goscrobble.EndBackgroundWorkers()
|
||||||
|
|
||||||
// Boot up API webserver \o/
|
// Boot up API webserver \o/
|
||||||
goscrobble.HandleRequests(port)
|
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
|
# 0.0.10
|
||||||
- Fixed looking up invalid profiles
|
- Fixed looking up invalid profiles
|
||||||
- Added valid error handling to bad request && rate limiting
|
- 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
|
## FRONTEND VARS
|
||||||
These are stored in `web/.env.production` and `web/.env.development`
|
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_PREFIX="gs:" // Redis key prefix
|
||||||
REDIS_AUTH="" // Redis password
|
REDIS_AUTH="" // Redis password
|
||||||
|
|
||||||
TIMEZONE= // Unix Timezone. Used for MySQL connection
|
|
||||||
|
|
||||||
JWT_SECRET= // 32+ Char JWT secret
|
JWT_SECRET= // 32+ Char JWT secret
|
||||||
JWT_EXPIRY=86400 // JWT expiry
|
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/rest v2.6.3+incompatible // indirect
|
||||||
github.com/sendgrid/sendgrid-go v3.8.0+incompatible
|
github.com/sendgrid/sendgrid-go v3.8.0+incompatible
|
||||||
github.com/sirupsen/logrus v1.7.0 // indirect
|
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/crypto v0.0.0-20200622213623-75b288015ac9
|
||||||
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
||||||
google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3 // indirect
|
google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3 // indirect
|
||||||
google.golang.org/grpc v1.33.1 // 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.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/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 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA=
|
||||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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/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 h1:Lenfy7QHRXPZVsw/12CWpxX6d/JkrX8wrx2vO8G80Ng=
|
||||||
go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg=
|
go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg=
|
||||||
go.opentelemetry.io/otel/metric v0.19.0 h1:dtZ1Ju44gkJkYvo+3qGqVXmf88tc+a42edOywypengg=
|
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-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-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-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-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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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 h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U=
|
||||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
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-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-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-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-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/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=
|
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-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-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-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=
|
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.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/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-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
@ -3,6 +3,7 @@ package goscrobble
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -12,41 +13,47 @@ type Album struct {
|
|||||||
Desc sql.NullString `json:"desc"`
|
Desc sql.NullString `json:"desc"`
|
||||||
Img sql.NullString `json:"img"`
|
Img sql.NullString `json:"img"`
|
||||||
MusicBrainzID sql.NullString `json:"mbid"`
|
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
|
// 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{}
|
album := Album{}
|
||||||
|
|
||||||
|
// Try locate our album
|
||||||
if mbid != "" {
|
if mbid != "" {
|
||||||
album = fetchAlbum("mbid", mbid, tx)
|
album = fetchAlbum("mbid", mbid, tx)
|
||||||
|
} else if spotifyId != "" {
|
||||||
|
album = fetchAlbum("spotify_id", spotifyId, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it didn't match above, lookup by name
|
||||||
if album.Uuid == "" {
|
if album.Uuid == "" {
|
||||||
err := insertNewAlbum(name, mbid, tx)
|
album = fetchAlbum("name", name, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can't find it. Lets add it!
|
||||||
|
if album.Uuid == "" {
|
||||||
|
err := insertNewAlbum(name, mbid, spotifyId, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error inserting album via MBID %s %+v", name, err)
|
|
||||||
return album, errors.New("Failed to insert album")
|
return album, errors.New("Failed to insert album")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch the recently inserted album to get the UUID
|
||||||
|
if mbid != "" {
|
||||||
album = fetchAlbum("mbid", mbid, tx)
|
album = fetchAlbum("mbid", mbid, tx)
|
||||||
err = album.linkAlbumToArtists(artists, tx)
|
} else if spotifyId != "" {
|
||||||
if err != nil {
|
album = fetchAlbum("spotify_id", spotifyId, tx)
|
||||||
return album, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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 album.Uuid == "" {
|
||||||
album = fetchAlbum("name", name, tx)
|
album = fetchAlbum("name", name, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try linkem up
|
||||||
err = album.linkAlbumToArtists(artists, tx)
|
err = album.linkAlbumToArtists(artists, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return album, err
|
return album, errors.New("Unable to link albums!")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,9 +79,9 @@ func fetchAlbum(col string, val string, tx *sql.Tx) Album {
|
|||||||
return album
|
return album
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertNewAlbum(name string, mbid string, tx *sql.Tx) error {
|
func insertNewAlbum(name string, mbid string, spotifyId string, tx *sql.Tx) error {
|
||||||
_, err := tx.Exec("INSERT INTO `albums` (`uuid`, `name`, `mbid`) "+
|
_, err := tx.Exec("INSERT INTO `albums` (`uuid`, `name`, `mbid`, `spotify_id`) "+
|
||||||
"VALUES (UUID_TO_BIN(UUID(), true),?,?)", name, mbid)
|
"VALUES (UUID_TO_BIN(UUID(), true),?,?,?)", name, mbid, spotifyId)
|
||||||
|
|
||||||
return err
|
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`) "+
|
_, err = tx.Exec("INSERT INTO `album_artist` (`album`, `artist`) "+
|
||||||
"VALUES (UUID_TO_BIN(?, true), UUID_TO_BIN(?, true))", album.Uuid, artist)
|
"VALUES (UUID_TO_BIN(?, true), UUID_TO_BIN(?, true))", album.Uuid, artist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
return err
|
return 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,35 +12,44 @@ type Artist struct {
|
|||||||
Desc sql.NullString `json:"desc"`
|
Desc sql.NullString `json:"desc"`
|
||||||
Img sql.NullString `json:"img"`
|
Img sql.NullString `json:"img"`
|
||||||
MusicBrainzID sql.NullString `json:"mbid"`
|
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
|
// 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{}
|
artist := Artist{}
|
||||||
|
|
||||||
|
// Try locate our artist
|
||||||
if mbid != "" {
|
if mbid != "" {
|
||||||
artist = fetchArtist("mbid", mbid, tx)
|
artist = fetchArtist("mbid", mbid, tx)
|
||||||
if artist.Uuid == "" {
|
} else if spotifyId != "" {
|
||||||
err := insertNewArtist(name, mbid, tx)
|
artist = fetchArtist("spotify_id", spotifyId, tx)
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error inserting artist via MBID %s %+v", name, err)
|
|
||||||
return artist, errors.New("Failed to insert artist")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If it didn't match above, lookup by name
|
||||||
|
if artist.Uuid == "" {
|
||||||
|
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)
|
artist = fetchArtist("mbid", mbid, tx)
|
||||||
}
|
} else if spotifyId != "" {
|
||||||
} else {
|
artist = fetchArtist("spotify_id", spotifyId, tx)
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if artist.Uuid == "" {
|
||||||
artist = fetchArtist("name", name, tx)
|
artist = fetchArtist("name", name, tx)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if artist.Uuid == "" {
|
if artist.Uuid == "" {
|
||||||
return artist, errors.New("Unable to fetch artist!")
|
return artist, errors.New("Unable to fetch artist!")
|
||||||
@ -64,9 +73,15 @@ func fetchArtist(col string, val string, tx *sql.Tx) Artist {
|
|||||||
return artist
|
return artist
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertNewArtist(name string, mbid string, tx *sql.Tx) error {
|
func insertNewArtist(name string, mbid string, spotifyId string, tx *sql.Tx) error {
|
||||||
_, err := tx.Exec("INSERT INTO `artists` (`uuid`, `name`, `mbid`) "+
|
_, err := tx.Exec("INSERT INTO `artists` (`uuid`, `name`, `mbid`, `spotify_id`) "+
|
||||||
"VALUES (UUID_TO_BIN(UUID(), true),?,?)", name, mbid)
|
"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
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package goscrobble
|
package goscrobble
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@ -55,3 +56,17 @@ func updateConfigValue(key string, value string) error {
|
|||||||
|
|
||||||
return nil
|
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"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
@ -23,13 +22,8 @@ func InitDb() {
|
|||||||
dbUser := os.Getenv("MYSQL_USER")
|
dbUser := os.Getenv("MYSQL_USER")
|
||||||
dbPass := os.Getenv("MYSQL_PASS")
|
dbPass := os.Getenv("MYSQL_PASS")
|
||||||
dbName := os.Getenv("MYSQL_DB")
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"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 {
|
func ParseJellyfinInput(userUUID string, data map[string]interface{}, ip net.IP, tx *sql.Tx) error {
|
||||||
// Debugging
|
// Debugging
|
||||||
fmt.Printf("%+v", data)
|
// fmt.Printf("%+v", data)
|
||||||
|
|
||||||
if data["ItemType"] != "Audio" {
|
if data["ItemType"] != "Audio" {
|
||||||
return errors.New("Media type not 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
|
// 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 {
|
if err != nil {
|
||||||
log.Printf("%+v", err)
|
log.Printf("%+v", err)
|
||||||
return errors.New("Failed to map artist")
|
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
|
// Insert album if not exist
|
||||||
artists := []string{artist.Uuid}
|
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 {
|
if err != nil {
|
||||||
log.Printf("%+v", err)
|
log.Printf("%+v", err)
|
||||||
return errors.New("Failed to map album")
|
return errors.New("Failed to map album")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert album if not exist
|
// Insert track if not exist
|
||||||
track, err := insertTrack(fmt.Sprintf("%s", data["Name"]), fmt.Sprintf("%s", data["Provider_musicbrainztrack"]), album.Uuid, artists, tx)
|
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 {
|
if err != nil {
|
||||||
log.Printf("%+v", err)
|
log.Printf("%+v", err)
|
||||||
return errors.New("Failed to map track")
|
return errors.New("Failed to map track")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert album if not exist
|
// Insert scrobble if not exist
|
||||||
err = insertScrobble(userUUID, track.Uuid, "jellyfin", ip, tx)
|
timestamp := time.Now()
|
||||||
|
fmt.Println(timestamp)
|
||||||
|
err = insertScrobble(userUUID, track.Uuid, "jellyfin", timestamp, ip, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("%+v", err)
|
log.Printf("%+v", err)
|
||||||
return errors.New("Failed to map track")
|
return errors.New("Failed to map track")
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = album
|
|
||||||
_ = artist
|
|
||||||
_ = track
|
|
||||||
|
|
||||||
// Insert track if not exist
|
// Insert track if not exist
|
||||||
return nil
|
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
|
||||||
|
}
|
@ -3,7 +3,7 @@ package goscrobble
|
|||||||
type ProfileResponse struct {
|
type ProfileResponse struct {
|
||||||
UUID string `json:"uuid"`
|
UUID string `json:"uuid"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Scrobbles []ScrobbleRequestItem `json:"scrobbles"`
|
Scrobbles []ScrobbleResponseItem `json:"scrobbles"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getProfile(user User) (ProfileResponse, error) {
|
func getProfile(user User) (ProfileResponse, error) {
|
||||||
|
@ -3,6 +3,7 @@ package goscrobble
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
@ -16,28 +17,29 @@ type Scrobble struct {
|
|||||||
Track string `json:"track"`
|
Track string `json:"track"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrobbleRequest struct {
|
type ScrobbleResponse struct {
|
||||||
Meta ScrobbleRequestMeta `json:"meta"`
|
Meta ScrobbleResponseMeta `json:"meta"`
|
||||||
Items []ScrobbleRequestItem `json:"items"`
|
Items []ScrobbleResponseItem `json:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrobbleRequestMeta struct {
|
type ScrobbleResponseMeta struct {
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrobbleRequestItem struct {
|
type ScrobbleResponseItem struct {
|
||||||
UUID string `json:"uuid"`
|
UUID string `json:"uuid"`
|
||||||
Timestamp time.Time `json:"time"`
|
Timestamp time.Time `json:"time"`
|
||||||
Artist string `json:"artist"`
|
Artist string `json:"artist"`
|
||||||
Album string `json:"album"`
|
Album string `json:"album"`
|
||||||
Track string `json:"track"`
|
Track string `json:"track"`
|
||||||
|
Source string `json:"source"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// insertScrobble - This will return if it exists or create it based on MBID > Name
|
// 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 {
|
func insertScrobble(user string, track string, source string, timestamp time.Time, ip net.IP, tx *sql.Tx) error {
|
||||||
err := insertNewScrobble(user, track, source, ip, tx)
|
err := insertNewScrobble(user, track, source, timestamp, ip, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error inserting scrobble %s %+v", user, err)
|
log.Printf("Error inserting scrobble %s %+v", user, err)
|
||||||
return errors.New("Failed to insert scrobble!")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleRequest, error) {
|
func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleResponse, error) {
|
||||||
scrobbleReq := ScrobbleRequest{}
|
scrobbleReq := ScrobbleResponse{}
|
||||||
var count int
|
var count int
|
||||||
|
|
||||||
// Yeah this isn't great. But for now.. it works! Cache later
|
// 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 artists ON track_artist.artist = artists.uuid "+
|
||||||
"JOIN albums ON track_album.album = albums.uuid "+
|
"JOIN albums ON track_album.album = albums.uuid "+
|
||||||
"JOIN users ON scrobbles.user = users.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)
|
userUuid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -68,7 +71,7 @@ func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleReques
|
|||||||
}
|
}
|
||||||
|
|
||||||
rows, err := db.Query(
|
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 tracks ON scrobbles.track = tracks.uuid "+
|
||||||
"JOIN track_artist ON track_artist.track = tracks.uuid "+
|
"JOIN track_artist ON track_artist.track = tracks.uuid "+
|
||||||
"JOIN track_album ON track_album.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()
|
defer rows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
item := ScrobbleRequestItem{}
|
item := ScrobbleResponseItem{}
|
||||||
err := rows.Scan(&item.UUID, &item.Timestamp, &item.Artist, &item.Album, &item.Track)
|
err := rows.Scan(&item.UUID, &item.Timestamp, &item.Artist, &item.Album, &item.Track, &item.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to fetch scrobbles: %+v", err)
|
log.Printf("Failed to fetch scrobbles: %+v", err)
|
||||||
return scrobbleReq, errors.New("Failed to fetch scrobbles")
|
return scrobbleReq, errors.New("Failed to fetch scrobbles")
|
||||||
@ -108,9 +111,21 @@ func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleReques
|
|||||||
return scrobbleReq, nil
|
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`) "+
|
_, 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
|
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"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@ -32,20 +33,22 @@ func HandleRequests(port string) {
|
|||||||
v1 := r.PathPrefix("/api/v1").Subrouter()
|
v1 := r.PathPrefix("/api/v1").Subrouter()
|
||||||
|
|
||||||
// Static Token for /ingress
|
// Static Token for /ingress
|
||||||
v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(handleIngress)).Methods("POST")
|
v1.HandleFunc("/ingress/jellyfin", limitMiddleware(tokenMiddleware(handleIngress), lightLimiter)).Methods("POST")
|
||||||
v1.HandleFunc("/ingress/multiscrobbler", tokenMiddleware(handleIngress)).Methods("POST")
|
v1.HandleFunc("/ingress/multiscrobbler", limitMiddleware(tokenMiddleware(handleIngress), lightLimiter)).Methods("POST")
|
||||||
|
|
||||||
// JWT Auth - PWN PROFILE ONLY.
|
// JWT Auth - Own profile only (Uses uuid in JWT)
|
||||||
v1.HandleFunc("/user", jwtMiddleware(fetchUser)).Methods("GET")
|
v1.HandleFunc("/user", limitMiddleware(jwtMiddleware(fetchUser), lightLimiter)).Methods("GET")
|
||||||
// v1.HandleFunc("/user", jwtMiddleware(fetchScrobbleResponse)).Methods("PATCH")
|
// 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")
|
v1.HandleFunc("/user/{uuid}/scrobbles", jwtMiddleware(fetchScrobbleResponse)).Methods("GET")
|
||||||
|
|
||||||
// Config auth
|
// Config auth
|
||||||
v1.HandleFunc("/config", adminMiddleware(fetchConfig)).Methods("GET")
|
v1.HandleFunc("/config", limitMiddleware(adminMiddleware(fetchConfig), standardLimiter)).Methods("GET")
|
||||||
v1.HandleFunc("/config", adminMiddleware(postConfig)).Methods("POST")
|
v1.HandleFunc("/config", limitMiddleware(adminMiddleware(postConfig), standardLimiter)).Methods("POST")
|
||||||
|
|
||||||
// No Auth
|
// 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("/profile/{username}", limitMiddleware(fetchProfile, lightLimiter)).Methods("GET")
|
||||||
|
|
||||||
v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST")
|
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("/sendreset", limitMiddleware(handleSendReset, heavyLimiter)).Methods("POST")
|
||||||
v1.HandleFunc("/resetpassword", limitMiddleware(handleResetPassword, 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
|
// This just prevents it serving frontend stuff over /api
|
||||||
r.PathPrefix("/api")
|
r.PathPrefix("/api")
|
||||||
|
|
||||||
@ -70,6 +76,8 @@ func HandleRequests(port string) {
|
|||||||
|
|
||||||
// Serve it up!
|
// Serve it up!
|
||||||
fmt.Printf("Goscrobble listening on port %s", port)
|
fmt.Printf("Goscrobble listening on port %s", port)
|
||||||
|
fmt.Println("")
|
||||||
|
|
||||||
log.Fatal(http.ListenAndServe(":"+port, handler))
|
log.Fatal(http.ListenAndServe(":"+port, handler))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,6 +233,7 @@ func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
|
|||||||
case "jellyfin":
|
case "jellyfin":
|
||||||
err := ParseJellyfinInput(userUuid, bodyJson, ip, tx)
|
err := ParseJellyfinInput(userUuid, bodyJson, ip, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Printf("Err? %+v", err)
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
throwOkError(w, err.Error())
|
throwOkError(w, err.Error())
|
||||||
return
|
return
|
||||||
@ -269,6 +278,13 @@ func fetchUser(w http.ResponseWriter, r *http.Request, jwtUser string, reqUser s
|
|||||||
throwOkError(w, "Failed to fetch user information")
|
throwOkError(w, "Failed to fetch user information")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
oauth, err := getOauthToken(user.UUID, "spotify")
|
||||||
|
if err == nil {
|
||||||
|
user.SpotifyUsername = oauth.Username
|
||||||
|
}
|
||||||
|
|
||||||
json, _ := json.Marshal(&user)
|
json, _ := json.Marshal(&user)
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
@ -351,3 +367,44 @@ func fetchProfile(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write(json)
|
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"
|
"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() {
|
go func() {
|
||||||
for now := range time.Tick(time.Second) {
|
for {
|
||||||
fmt.Println(now)
|
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 {
|
type Track struct {
|
||||||
Uuid string `json:"uuid"`
|
Uuid string `json:"uuid"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Length int `json:"length"`
|
||||||
Desc sql.NullString `json:"desc"`
|
Desc sql.NullString `json:"desc"`
|
||||||
Img sql.NullString `json:"img"`
|
Img sql.NullString `json:"img"`
|
||||||
MusicBrainzID sql.NullString `json:"mbid"`
|
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
|
// 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{}
|
track := Track{}
|
||||||
|
|
||||||
|
// Try locate our track
|
||||||
if mbid != "" {
|
if mbid != "" {
|
||||||
track = fetchTrack("mbid", mbid, tx)
|
track = fetchTrack("mbid", mbid, tx)
|
||||||
|
} else if spotifyId != "" {
|
||||||
|
track = fetchTrack("spotify_id", spotifyId, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it didn't match above, lookup by name
|
||||||
if track.Uuid == "" {
|
if track.Uuid == "" {
|
||||||
err := insertNewTrack(name, mbid, tx)
|
track = fetchTrack("name", name, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can't find it. Lets add it!
|
||||||
|
if track.Uuid == "" {
|
||||||
|
err := insertNewTrack(name, legnth, mbid, spotifyId, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error inserting track via MBID %s %+v", name, err)
|
|
||||||
return track, errors.New("Failed to insert track")
|
return track, errors.New("Failed to insert track")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch the recently inserted track to get the UUID
|
||||||
|
if mbid != "" {
|
||||||
track = fetchTrack("mbid", mbid, tx)
|
track = fetchTrack("mbid", mbid, tx)
|
||||||
err = track.linkTrack(album, artists, tx)
|
} else if spotifyId != "" {
|
||||||
if err != nil {
|
track = fetchTrack("spotify_id", spotifyId, tx)
|
||||||
return track, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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 track.Uuid == "" {
|
||||||
track = fetchTrack("name", name, tx)
|
track = fetchTrack("name", name, tx)
|
||||||
|
}
|
||||||
|
|
||||||
err = track.linkTrack(album, artists, tx)
|
err = track.linkTrack(album, artists, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return track, err
|
return track, errors.New("Unable to link tracks!")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,9 +78,9 @@ func fetchTrack(col string, val string, tx *sql.Tx) Track {
|
|||||||
return track
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertNewTrack(name string, mbid string, tx *sql.Tx) error {
|
func insertNewTrack(name string, length int, mbid string, spotifyId string, tx *sql.Tx) error {
|
||||||
_, err := tx.Exec("INSERT INTO `tracks` (`uuid`, `name`, `mbid`) "+
|
_, err := tx.Exec("INSERT INTO `tracks` (`uuid`, `name`, `length`, `mbid`, `spotify_id`) "+
|
||||||
"VALUES (UUID_TO_BIN(UUID(), true),?,?)", name, mbid)
|
"VALUES (UUID_TO_BIN(UUID(), true),?,?,?,?)", name, length, mbid, spotifyId)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -110,3 +116,9 @@ func (track Track) linkTrackToArtists(artists []string, tx *sql.Tx) error {
|
|||||||
|
|
||||||
return nil
|
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,6 +28,7 @@ type User struct {
|
|||||||
Verified bool `json:"verified"`
|
Verified bool `json:"verified"`
|
||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
Admin bool `json:"admin"`
|
Admin bool `json:"admin"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserResponse struct {
|
type UserResponse struct {
|
||||||
@ -39,6 +40,8 @@ type UserResponse struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Verified bool `json:"verified"`
|
Verified bool `json:"verified"`
|
||||||
|
SpotifyUsername string `json:"spotify_username"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterRequest - Incoming JSON
|
// RegisterRequest - Incoming JSON
|
||||||
@ -208,8 +211,8 @@ func userAlreadyExists(req *RegisterRequest) bool {
|
|||||||
|
|
||||||
func getUser(uuid string) (User, error) {
|
func getUser(uuid string) (User, error) {
|
||||||
var user User
|
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",
|
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)
|
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 {
|
if err == sql.ErrNoRows {
|
||||||
return user, errors.New("Invalid JWT Token")
|
return user, errors.New("Invalid JWT Token")
|
||||||
@ -220,8 +223,8 @@ func getUser(uuid string) (User, error) {
|
|||||||
|
|
||||||
func getUserByUsername(username string) (User, error) {
|
func getUserByUsername(username string) (User, error) {
|
||||||
var user User
|
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",
|
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)
|
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 {
|
if err == sql.ErrNoRows {
|
||||||
return user, errors.New("Invalid Username")
|
return user, errors.New("Invalid Username")
|
||||||
@ -232,8 +235,8 @@ func getUserByUsername(username string) (User, error) {
|
|||||||
|
|
||||||
func getUserByEmail(email string) (User, error) {
|
func getUserByEmail(email string) (User, error) {
|
||||||
var user User
|
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",
|
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)
|
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 {
|
if err == sql.ErrNoRows {
|
||||||
return user, errors.New("Invalid Email")
|
return user, errors.New("Invalid Email")
|
||||||
@ -244,11 +247,9 @@ func getUserByEmail(email string) (User, error) {
|
|||||||
|
|
||||||
func getUserByResetToken(token string) (User, error) {
|
func getUserByResetToken(token string) (User, error) {
|
||||||
var user User
|
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",
|
"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)
|
token).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin, &user.Timezone)
|
||||||
|
|
||||||
fmt.Println(err)
|
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return user, errors.New("Invalid Token")
|
return user, errors.New("Invalid Token")
|
||||||
@ -256,11 +257,12 @@ func getUserByResetToken(token string) (User, error) {
|
|||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) sendResetEmail(ip net.IP) error {
|
func (user *User) sendResetEmail(ip net.IP) error {
|
||||||
token := generateToken(16)
|
token := generateToken(16)
|
||||||
|
|
||||||
// 24 hours
|
// 1 hour validation
|
||||||
exp := time.Now().AddDate(0, 0, 1)
|
exp := time.Now().Add(time.Hour * time.Duration(1))
|
||||||
err := user.saveResetToken(token, exp)
|
err := user.saveResetToken(token, exp)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -268,7 +270,9 @@ func (user *User) sendResetEmail(ip net.IP) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content := fmt.Sprintf(
|
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)
|
ip, user.Username, os.Getenv("GOSCROBBLE_DOMAIN"), token)
|
||||||
|
|
||||||
return sendEmail(user.Username, user.Email, "GoScrobble - Password Reset", content)
|
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
|
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
|
return ipHex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calcPageOffsetString - Used to SQL paging
|
||||||
func calcPageOffsetString(page int, offset int) string {
|
func calcPageOffsetString(page int, offset int) string {
|
||||||
return fmt.Sprintf("%d", page*offset)
|
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,
|
`active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
`admin` TINYINT(1) NOT NULL DEFAULT 0,
|
`admin` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
`private` 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 `usernameLookup` (`username`, `active`),
|
||||||
KEY `emailLookup` (`email`, `active`)
|
KEY `emailLookup` (`email`, `active`)
|
||||||
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
) 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'));
|
const user = JSON.parse(localStorage.getItem('user'));
|
||||||
|
|
||||||
if (user && user.jwt) {
|
if (user && user.jwt) {
|
||||||
return { Authorization: 'Bearer ' + user.jwt };
|
return { Authorization: 'Bearer ' + user.jwt, changeOrigin: true };
|
||||||
} else {
|
} else {
|
||||||
return {};
|
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) => {
|
export const PostLogin = (formValues) => {
|
||||||
return axios.post(process.env.REACT_APP_API_URL + "login", formValues)
|
return axios.post(process.env.REACT_APP_API_URL + "login", formValues)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -34,7 +60,7 @@ export const PostLogin = (formValues) => {
|
|||||||
}
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (error.response === 401) {
|
if (error.response === 401) {
|
||||||
return {};
|
toast.error('Unauthorized')
|
||||||
} else if (error.response === 429) {
|
} else if (error.response === 429) {
|
||||||
toast.error('Rate limited. Please try again shortly')
|
toast.error('Rate limited. Please try again shortly')
|
||||||
} else {
|
} else {
|
||||||
@ -57,13 +83,7 @@ export const PostRegister = (formValues) => {
|
|||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (error.response === 401) {
|
handleErrorResp(error)
|
||||||
return {};
|
|
||||||
} else if (error.response === 429) {
|
|
||||||
toast.error('Rate limited. Please try again shortly')
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to connect');
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -81,13 +101,7 @@ export const PostResetPassword = (formValues) => {
|
|||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (error.response === 401) {
|
handleErrorResp(error)
|
||||||
return {};
|
|
||||||
} else if (error.response === 429) {
|
|
||||||
toast.error('Rate limited. Please try again shortly')
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to connect');
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -97,14 +111,7 @@ export const sendPasswordReset = (values) => {
|
|||||||
(data) => {
|
(data) => {
|
||||||
return data.data;
|
return data.data;
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (error.response === 401) {
|
return handleErrorResp(error)
|
||||||
return {};
|
|
||||||
} else if (error.response === 429) {
|
|
||||||
toast.error('Rate limited. Please try again shortly')
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to connect');
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -113,14 +120,7 @@ export const getStats = () => {
|
|||||||
(data) => {
|
(data) => {
|
||||||
return data.data;
|
return data.data;
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (error.response === 401) {
|
return handleErrorResp(error)
|
||||||
return {};
|
|
||||||
} else if (error.response === 429) {
|
|
||||||
toast.error('Rate limited. Please try again shortly')
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to connect');
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -129,14 +129,7 @@ export const getRecentScrobbles = (id) => {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data.data;
|
return data.data;
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (error.response === 401) {
|
return handleErrorResp(error)
|
||||||
return {};
|
|
||||||
} else if (error.response === 429) {
|
|
||||||
toast.error('Rate limited. Please try again shortly')
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to connect');
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -145,14 +138,7 @@ export const getConfigs = () => {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data.data;
|
return data.data;
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (error.response === 401) {
|
return handleErrorResp(error)
|
||||||
return {};
|
|
||||||
} else if (error.response === 429) {
|
|
||||||
toast.error('Rate limited. Please try again shortly')
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to connect');
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -171,14 +157,7 @@ export const postConfigs = (values, toggle) => {
|
|||||||
toast.error(data.data.error);
|
toast.error(data.data.error);
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (error.response === 401) {
|
return handleErrorResp(error)
|
||||||
return {};
|
|
||||||
} else if (error.response === 429) {
|
|
||||||
toast.error('Rate limited. Please try again shortly')
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to connect');
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -187,14 +166,7 @@ export const getProfile = (userName) => {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data.data;
|
return data.data;
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (error.response === 401) {
|
return handleErrorResp(error)
|
||||||
return {};
|
|
||||||
} else if (error.response === 429) {
|
|
||||||
toast.error('Rate limited. Please try again shortly')
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to connect');
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -203,34 +175,56 @@ export const getUser = () => {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data.data;
|
return data.data;
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (error.response === 401) {
|
return handleErrorResp(error)
|
||||||
return {};
|
|
||||||
} else if (error.response === 429) {
|
|
||||||
toast.error('Rate limited. Please try again shortly')
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to connect');
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateResetPassword = (tokenStr) => {
|
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) => {
|
.then((data) => {
|
||||||
if (data.error) {
|
|
||||||
toast.error(data.error);
|
|
||||||
return {valid: false}
|
|
||||||
}
|
|
||||||
return data.data;
|
return data.data;
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (error.response === 401) {
|
return handleErrorResp(error)
|
||||||
return {};
|
|
||||||
} else if (error.response === 429) {
|
|
||||||
toast.error('Rate limited. Please try again shortly')
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to connect');
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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>Track</td>
|
||||||
<td>Artist</td>
|
<td>Artist</td>
|
||||||
<td>Album</td>
|
<td>Album</td>
|
||||||
|
<td>Source</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -21,6 +22,7 @@ const ScrobbleTable = (props) => {
|
|||||||
<td>{element.track}</td>
|
<td>{element.track}</td>
|
||||||
<td>{element.artist}</td>
|
<td>{element.artist}</td>
|
||||||
<td>{element.album}</td>
|
<td>{element.album}</td>
|
||||||
|
<td>{element.source}</td>
|
||||||
</tr>;
|
</tr>;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,8 @@ const Admin = () => {
|
|||||||
if (data.configs) {
|
if (data.configs) {
|
||||||
setConfigs(data.configs);
|
setConfigs(data.configs);
|
||||||
setToggle(data.configs.REGISTRATION_ENABLED === "1")
|
setToggle(data.configs.REGISTRATION_ENABLED === "1")
|
||||||
}
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -31,6 +31,14 @@ const Admin = () => {
|
|||||||
setToggle(!toggle);
|
setToggle(!toggle);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
history.push("/login")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user && !user.admin) {
|
||||||
|
history.push("/Dashboard")
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="pageWrapper">
|
<div className="pageWrapper">
|
||||||
@ -39,9 +47,7 @@ const Admin = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user || !user.admin) {
|
|
||||||
history.push("/login")
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pageWrapper">
|
<div className="pageWrapper">
|
||||||
|
@ -8,7 +8,7 @@ import ScrobbleTable from "../Components/ScrobbleTable";
|
|||||||
import AuthContext from '../Contexts/AuthContext';
|
import AuthContext from '../Contexts/AuthContext';
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
// const history = useHistory();
|
const history = useHistory();
|
||||||
let { user } = useContext(AuthContext);
|
let { user } = useContext(AuthContext);
|
||||||
let [loading, setLoading] = useState(true);
|
let [loading, setLoading] = useState(true);
|
||||||
let [dashboardData, setDashboardData] = useState({});
|
let [dashboardData, setDashboardData] = useState({});
|
||||||
@ -24,6 +24,10 @@ const Dashboard = () => {
|
|||||||
})
|
})
|
||||||
}, [user])
|
}, [user])
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
history.push("/login")
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="pageWrapper">
|
<div className="pageWrapper">
|
||||||
@ -37,11 +41,13 @@ const Dashboard = () => {
|
|||||||
<h1>
|
<h1>
|
||||||
{user.username}'s Dashboard!
|
{user.username}'s Dashboard!
|
||||||
</h1>
|
</h1>
|
||||||
|
<div className="dashboardBody">
|
||||||
{loading
|
{loading
|
||||||
? <ScaleLoader color="#6AD7E5" size={60} />
|
? <ScaleLoader color="#6AD7E5" size={60} />
|
||||||
: <ScrobbleTable data={dashboardData.items} />
|
: <ScrobbleTable data={dashboardData.items} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ const Login = () => {
|
|||||||
className="loginButton"
|
className="loginButton"
|
||||||
onClick={redirectReset}
|
onClick={redirectReset}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>{loading ? <ScaleLoader color="#FFF" size={35} /> : "Reset Password"}</Button>
|
>Reset Password</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,8 @@ import { useHistory } from "react-router";
|
|||||||
import AuthContext from '../Contexts/AuthContext';
|
import AuthContext from '../Contexts/AuthContext';
|
||||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||||
import { getUser } from '../Api/index'
|
import { getUser } from '../Api/index'
|
||||||
|
import { Button } from 'reactstrap';
|
||||||
|
import { spotifyConnectionRequest, spotifyDisonnectionRequest } from '../Api/index'
|
||||||
|
|
||||||
const User = () => {
|
const User = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@ -12,6 +14,7 @@ const User = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [userdata, setUserdata] = useState({});
|
const [userdata, setUserdata] = useState({});
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return
|
return
|
||||||
@ -24,6 +27,10 @@ const User = () => {
|
|||||||
})
|
})
|
||||||
}, [user])
|
}, [user])
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
history.push("/login")
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="pageWrapper">
|
<div className="pageWrapper">
|
||||||
@ -32,10 +39,6 @@ const User = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
history.push("/login")
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pageWrapper">
|
<div className="pageWrapper">
|
||||||
<h1>
|
<h1>
|
||||||
@ -44,7 +47,25 @@ const User = () => {
|
|||||||
<p className="userBody">
|
<p className="userBody">
|
||||||
Created At: {userdata.created_at}<br/>
|
Created At: {userdata.created_at}<br/>
|
||||||
Email: {userdata.email}<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user