mirror of
https://github.com/idanoo/GoScrobble
synced 2025-07-01 13:42:20 +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
33 changed files with 795 additions and 248 deletions
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue