mirror of
https://github.com/idanoo/GoScrobble
synced 2025-07-01 13:42:20 +00:00
0.0.9
- Fix mobile menu auto collapse on select - Add /u/ route for public user profiles (Added private flag to db - to implement later) - Add /user route for your own profile / edit profile - Added handling for if API is offline/incorrect - Add index.html loading spinner while react bundle downloads - Change HashRouter to BrowserRouter - Added sources column to scrobbles
This commit is contained in:
parent
af02bd99cc
commit
e570314ac2
27 changed files with 435 additions and 89 deletions
4
internal/goscrobble/external_lastfm.go
Normal file
4
internal/goscrobble/external_lastfm.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
package goscrobble
|
||||
|
||||
func getImageLastFM(src string) {
|
||||
}
|
|
@ -50,7 +50,7 @@ func ParseJellyfinInput(userUUID string, data map[string]interface{}, ip net.IP,
|
|||
}
|
||||
|
||||
// Insert album if not exist
|
||||
err = insertScrobble(userUUID, track.Uuid, ip, tx)
|
||||
err = insertScrobble(userUUID, track.Uuid, "jellyfin", ip, tx)
|
||||
if err != nil {
|
||||
log.Printf("%+v", err)
|
||||
return errors.New("Failed to map track")
|
21
internal/goscrobble/profile.go
Normal file
21
internal/goscrobble/profile.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package goscrobble
|
||||
|
||||
type ProfileResponse struct {
|
||||
UUID string `json:"uuid"`
|
||||
Username string `json:"username"`
|
||||
Scrobbles []ScrobbleRequestItem `json:"scrobbles"`
|
||||
}
|
||||
|
||||
func getProfile(user User) (ProfileResponse, error) {
|
||||
resp := ProfileResponse{
|
||||
UUID: user.UUID,
|
||||
Username: user.Username,
|
||||
}
|
||||
scrobbleReq, err := fetchScrobblesForUser(user.UUID, 10, 1)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
resp.Scrobbles = scrobbleReq.Items
|
||||
return resp, nil
|
||||
}
|
|
@ -36,8 +36,8 @@ type ScrobbleRequestItem struct {
|
|||
}
|
||||
|
||||
// insertScrobble - This will return if it exists or create it based on MBID > Name
|
||||
func insertScrobble(user string, track string, ip net.IP, tx *sql.Tx) error {
|
||||
err := insertNewScrobble(user, track, ip, tx)
|
||||
func insertScrobble(user string, track string, source string, ip net.IP, tx *sql.Tx) error {
|
||||
err := insertNewScrobble(user, track, source, ip, tx)
|
||||
if err != nil {
|
||||
log.Printf("Error inserting scrobble %s %+v", user, err)
|
||||
return errors.New("Failed to insert scrobble!")
|
||||
|
@ -46,7 +46,7 @@ func insertScrobble(user string, track string, ip net.IP, tx *sql.Tx) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func fetchScrobblesForUser(userUuid string, page int) (ScrobbleRequest, error) {
|
||||
func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleRequest, error) {
|
||||
scrobbleReq := ScrobbleRequest{}
|
||||
var count int
|
||||
|
||||
|
@ -76,8 +76,8 @@ func fetchScrobblesForUser(userUuid string, page int) (ScrobbleRequest, error) {
|
|||
"JOIN albums ON track_album.album = albums.uuid "+
|
||||
"JOIN users ON scrobbles.user = users.uuid "+
|
||||
"WHERE user = UUID_TO_BIN(?, true) "+
|
||||
"ORDER BY scrobbles.created_at DESC LIMIT 500",
|
||||
userUuid)
|
||||
"ORDER BY scrobbles.created_at DESC LIMIT ?",
|
||||
userUuid, limit)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch scrobbles: %+v", err)
|
||||
return scrobbleReq, errors.New("Failed to fetch scrobbles")
|
||||
|
@ -108,9 +108,9 @@ func fetchScrobblesForUser(userUuid string, page int) (ScrobbleRequest, error) {
|
|||
return scrobbleReq, nil
|
||||
}
|
||||
|
||||
func insertNewScrobble(user string, track string, ip net.IP, tx *sql.Tx) error {
|
||||
_, err := tx.Exec("INSERT INTO `scrobbles` (`uuid`, `created_at`, `created_ip`, `user`, `track`) "+
|
||||
"VALUES (UUID_TO_BIN(UUID(), true), NOW(), ?, UUID_TO_BIN(?, true),UUID_TO_BIN(?, true))", ip, user, track)
|
||||
func insertNewScrobble(user string, track string, source string, 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)
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -25,11 +25,14 @@ type jsonResponse struct {
|
|||
Msg string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// Limits to 1 req / 10 sec
|
||||
// Limits to 1 req / 4 sec
|
||||
var heavyLimiter = NewIPRateLimiter(0.25, 2)
|
||||
|
||||
// Limits to 5 req / sec
|
||||
var standardLimiter = NewIPRateLimiter(1, 5)
|
||||
var standardLimiter = NewIPRateLimiter(5, 5)
|
||||
|
||||
// Limits to 10 req / sec
|
||||
var lightLimiter = NewIPRateLimiter(10, 10)
|
||||
|
||||
// List of Reverse proxies
|
||||
var ReverseProxies []string
|
||||
|
@ -48,17 +51,21 @@ func HandleRequests(port string) {
|
|||
// Static Token for /ingress
|
||||
v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(handleIngress)).Methods("POST")
|
||||
|
||||
// JWT Auth
|
||||
v1.HandleFunc("/user/{id}/scrobbles", jwtMiddleware(fetchScrobbleResponse)).Methods("GET")
|
||||
// JWT Auth - PWN PROFILE ONLY.
|
||||
v1.HandleFunc("/user", jwtMiddleware(fetchUser)).Methods("GET")
|
||||
// v1.HandleFunc("/user", jwtMiddleware(fetchScrobbleResponse)).Methods("PATCH")
|
||||
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")
|
||||
|
||||
// No Auth
|
||||
v1.HandleFunc("/stats", handleStats).Methods("GET")
|
||||
v1.HandleFunc("/profile/{username}", limitMiddleware(fetchProfile, lightLimiter)).Methods("GET")
|
||||
|
||||
v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST")
|
||||
v1.HandleFunc("/login", limitMiddleware(handleLogin, standardLimiter)).Methods("POST")
|
||||
v1.HandleFunc("/stats", handleStats).Methods("GET")
|
||||
|
||||
// This just prevents it serving frontend stuff over /api
|
||||
r.PathPrefix("/api")
|
||||
|
@ -80,7 +87,7 @@ func HandleRequests(port string) {
|
|||
log.Fatal(http.ListenAndServe(":"+port, handler))
|
||||
}
|
||||
|
||||
// MIDDLEWARE
|
||||
// MIDDLEWARE RESPONSES
|
||||
// throwUnauthorized - Throws a 403
|
||||
func throwUnauthorized(w http.ResponseWriter, m string) {
|
||||
jr := jsonResponse{
|
||||
|
@ -149,6 +156,7 @@ func generateJsonError(m string) []byte {
|
|||
return js
|
||||
}
|
||||
|
||||
// MIDDLEWARE ACTIONS
|
||||
// tokenMiddleware - Validates token to a user
|
||||
func tokenMiddleware(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -182,15 +190,11 @@ func jwtMiddleware(next func(http.ResponseWriter, *http.Request, string, string)
|
|||
|
||||
var reqUuid string
|
||||
for k, v := range mux.Vars(r) {
|
||||
if k == "id" {
|
||||
if k == "uuid" {
|
||||
reqUuid = v
|
||||
}
|
||||
}
|
||||
|
||||
if reqUuid == "" {
|
||||
throwBadReq(w, "Invalid Request")
|
||||
}
|
||||
|
||||
next(w, r, claims.Subject, reqUuid)
|
||||
}
|
||||
}
|
||||
|
@ -311,13 +315,13 @@ func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
|
|||
if err != nil {
|
||||
// log.Printf("Error inserting track: %+v", err)
|
||||
tx.Rollback()
|
||||
throwBadReq(w, err.Error())
|
||||
throwOkError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
throwBadReq(w, err.Error())
|
||||
throwOkError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -328,11 +332,35 @@ func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
|
|||
throwBadReq(w, "Unknown ingress type")
|
||||
}
|
||||
|
||||
// fetchUser - Return personal userprofile
|
||||
func fetchUser(w http.ResponseWriter, r *http.Request, jwtUser string, reqUser string) {
|
||||
// We don't this var most of the time
|
||||
userFull, err := getUser(jwtUser)
|
||||
if err != nil {
|
||||
throwOkError(w, "Failed to fetch user information")
|
||||
return
|
||||
}
|
||||
|
||||
jsonFull, _ := json.Marshal(&userFull)
|
||||
|
||||
// Lets strip out vars we don't want to send.
|
||||
user := UserResponse{}
|
||||
err = json.Unmarshal(jsonFull, &user)
|
||||
if err != nil {
|
||||
throwOkError(w, "Failed to fetch user information")
|
||||
return
|
||||
}
|
||||
json, _ := json.Marshal(&user)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
// fetchScrobbles - Return an array of scrobbles
|
||||
func fetchScrobbleResponse(w http.ResponseWriter, r *http.Request, jwtUser string, reqUser string) {
|
||||
resp, err := fetchScrobblesForUser(reqUser, 1)
|
||||
resp, err := fetchScrobblesForUser(reqUser, 100, 1)
|
||||
if err != nil {
|
||||
throwBadReq(w, "Failed to fetch scrobbles")
|
||||
throwOkError(w, "Failed to fetch scrobbles")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -374,6 +402,37 @@ func postConfig(w http.ResponseWriter, r *http.Request, jwtUser string) {
|
|||
throwOkMessage(w, "Config updated successfully")
|
||||
}
|
||||
|
||||
// fetchProfile - Returns public user profile data
|
||||
func fetchProfile(w http.ResponseWriter, r *http.Request) {
|
||||
var username string
|
||||
for k, v := range mux.Vars(r) {
|
||||
if k == "username" {
|
||||
username = v
|
||||
}
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
throwOkError(w, "Invalid Username")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := getUserByUsername(username)
|
||||
if err != nil {
|
||||
throwOkError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := getProfile(user)
|
||||
if err != nil {
|
||||
throwOkError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
json, _ := json.Marshal(&resp)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(json)
|
||||
}
|
||||
|
||||
// FRONTEND HANDLING
|
||||
|
||||
// ServerHTTP - Frontend server
|
||||
|
|
|
@ -3,11 +3,14 @@ package goscrobble
|
|||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
// generateToken - Generates a unique token for user input
|
||||
func generateToken(n int) string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
|
||||
|
|
|
@ -29,6 +29,17 @@ type User struct {
|
|||
Admin bool `json:"admin"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
// RegisterRequest - Incoming JSON
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username"`
|
||||
|
@ -205,3 +216,15 @@ func getUser(uuid string) (User, error) {
|
|||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return user, errors.New("Invalid Username")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue