Merge branch 'dev-0.1.2' into 'master'

0.1.2

See merge request goscrobble/goscrobble-api!4
This commit is contained in:
Daniel Mason 2022-01-05 22:28:15 +00:00
commit 2c717cb407
16 changed files with 332 additions and 34 deletions

29
.env.development Normal file
View File

@ -0,0 +1,29 @@
MYSQL_HOST=mysql
MYSQL_USER=root
MYSQL_PASS=supersecretdatabasepassword1
MYSQL_DB=goscrobble
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_DB=4
REDIS_PREFIX="gs:"
REDIS_AUTH=""
JWT_SECRET=abcdefg
JWT_EXPIRY=604800
REFRESH_EXPIRY=604800
REVERSE_PROXIES=
PORT=42069
SENDGRID_API_KEY=
MAIL_FROM_ADDRESS=
MAIL_FROM_NAME=
DEV_MODE=true
GOSCROBBLE_DOMAIN="http://127.0.0.1"
DATA_DIRECTORY="/app/data"
FRONTEND_DIRECTORY="/app"
API_DOCS_DIRECTORY="/app/docs/api/build"

29
.env.production Normal file
View File

@ -0,0 +1,29 @@
MYSQL_HOST=
MYSQL_USER=
MYSQL_PASS=
MYSQL_DB=
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_DB=
REDIS_PREFIX="gs:"
REDIS_AUTH=""
JWT_SECRET=
JWT_EXPIRY=1800
REFRESH_EXPIRY=604800
REVERSE_PROXIES=127.0.0.1
PORT=42069
SENDGRID_API_KEY=
MAIL_FROM_ADDRESS=
MAIL_FROM_NAME=
DEV_MODE=false
GOSCROBBLE_DOMAIN=""
DATA_DIRECTORY="/var/www/goscrobble-data"
FRONTEND_DIRECTORY="/var/www/goscrobble-web"
API_DOCS_DIRECTORY="/var/www/goscrobble-api/docs/api/build"

4
.gitignore vendored
View File

@ -6,10 +6,8 @@
*.so *.so
*.dylib *.dylib
.env .env
web/.env.production
web/.env.development
web/img/* /data
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test

View File

@ -2,7 +2,7 @@ stages:
- build - build
variables: variables:
VERSION: 0.1.1 VERSION: 0.1.2
build-go: build-go:
image: golang:1.17 image: golang:1.17
@ -10,7 +10,7 @@ build-go:
only: only:
- master - master
script: script:
- go build -o goscrobble cmd/go-scrobble/*.go - go build -o goscrobble cmd/goscrobble/*.go
artifacts: artifacts:
expire_in: 1 week expire_in: 1 week
paths: paths:

BIN
data/img/placeholder.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

44
docker-compose.yml Normal file
View File

@ -0,0 +1,44 @@
version: "3.9"
services:
frontend:
image: node:16
volumes:
- ../goscrobble-web:/app
restart: always
ports:
- "127.0.0.1:3000:3000"
environment:
- REACT_APP_API_URL=http://127.0.0.1:42069
command: bash -c "cd /app && npm install && yarn start"
backend:
image: golang:1.17
volumes:
- ./:/app
ports:
- "127.0.0.1:42069:42069"
restart: always
command: bash -c "sleep 5 && cd /app && go mod tidy && go run cmd/goscrobble/*.go"
mysql:
image: mysql:8.0.27
command: --default-authentication-plugin=mysql_native_password --init-file /app/migrations/0_create_db.sql --sql_mode=
restart: always
cap_add:
- SYS_NICE
volumes:
- database-data:/var/lib/mysql
- ./migrations/0_create_db.sql:/app/migrations/0_create_db.sql
ports:
- "127.0.0.1:3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=supersecretdatabasepassword1
redis:
image: redis:6.2
ports:
- "127.0.0.1:6379:6379"
volumes:
database-data:

View File

@ -1,3 +1,7 @@
# 0.1.2
- Add docker-compose file for local dev
- Implemented top listeners for artist/album endpoints to match track
# 0.1.1 # 0.1.1
- Cached all config values - Cached all config values
- Updated spotify sdk package to v2 - Updated spotify sdk package to v2

View File

@ -11,3 +11,32 @@ This is by no means recommended.. But during testing I somehow scrobbled movies.
DELETE tracks FROM tracks LEFT JOIN track_artist ON track_artist.track = tracks.uuid WHERE track_artist.track IS NULL; DELETE tracks FROM tracks LEFT JOIN track_artist ON track_artist.track = tracks.uuid WHERE track_artist.track IS NULL;
DELETE scrobbles FROM scrobbles LEFT JOIN tracks ON tracks.uuid = scrobbles.track WHERE tracks.uuid is null; DELETE scrobbles FROM scrobbles LEFT JOIN tracks ON tracks.uuid = scrobbles.track WHERE tracks.uuid is null;
SET FOREIGN_KEY_CHECKS=1; SET FOREIGN_KEY_CHECKS=1;
Removing duplicates (based on same song played in same hour)
-- backup stuff first
DROP TABLE BACKUP_scrobbles;
CREATE TABLE BACKUP_scrobbles (primary key (uuid)) as select * from scrobbles;
SELECT BIN_TO_UUID(`user`, true), scrobbles.*, count(*) FROM scrobbles
-- WHERE `user`= UUID_TO_BIN('<userUUID>', true)
GROUP BY track, HOUR(created_at)
HAVING count(*) > 1
ORDER BY COUNT(*) DESC;
-- will only delete one set of dupes at a time, run until 0 updated rows
DELETE scrobbles
FROM scrobbles
WHERE uuid IN (
SELECT uuid FROM (
SELECT `uuid` FROM scrobbles
WHERE `user`= UUID_TO_BIN('<userUUID>', true)
GROUP BY track, HOUR(created_at)
HAVING count(*) > 1
) x
);

View File

@ -128,3 +128,56 @@ func getAlbumByUUID(uuid string) (Album, error) {
return album, nil return album, nil
} }
// getTopUsersForAlbumUUID - Returns list of top users for a track
func getTopUsersForAlbumUUID(trackUUID string, limit int, page int) (TopUserResponse, error) {
response := TopUserResponse{}
// TODO: Implement this
// var count int
// total, err := getDbCount(
// "SELECT COUNT(*) FROM `scrobbles` WHERE `album` = UUID_TO_BIN(?, true) GROUP BY `album`, `user`", trackUUID)
// if err != nil {
// log.Printf("Failed to fetch scrobble count: %+v", err)
// return response, errors.New("Failed to fetch combined scrobbles")
// }
// rows, err := db.Query(
// "SELECT BIN_TO_UUID(`scrobbles`.`user`, true), `users`.`username`, COUNT(*) "+
// "FROM `scrobbles` "+
// "JOIN `users` ON `scrobbles`.`user` = `users`.`uuid` "+
// "WHERE `track` = UUID_TO_BIN(?, true) "+
// "GROUP BY `scrobbles`.`user` "+
// "ORDER BY COUNT(*) DESC LIMIT ?",
// trackUUID, limit)
// if err != nil {
// log.Printf("Failed to fetch scrobbles: %+v", err)
// return response, errors.New("Failed to fetch combined scrobbles")
// }
// defer rows.Close()
// for rows.Next() {
// item := TopUserResponseItem{}
// err := rows.Scan(&item.UserUUID, &item.UserName, &item.Count)
// if err != nil {
// log.Printf("Failed to fetch scrobbles: %+v", err)
// return response, errors.New("Failed to fetch combined scrobbles")
// }
// count++
// response.Items = append(response.Items, item)
// }
// err = rows.Err()
// if err != nil {
// log.Printf("Failed to fetch scrobbles: %+v", err)
// return response, errors.New("Failed to fetch scrobbles")
// }
// response.Meta.Count = count
// response.Meta.Total = total
// response.Meta.Page = page
return response, nil
}

View File

@ -164,3 +164,58 @@ func getTopArtists(userUuid string) (TopArtists, error) {
return topArtist, nil return topArtist, nil
} }
// getTopUsersForArtistUUID - Returns list of top users for a track
func getTopUsersForArtistUUID(trackUUID string, limit int, page int) (TopUserResponse, error) {
response := TopUserResponse{}
// TODO: Implement
// var count int
// total, err := getDbCount(
// "SELECT COUNT(*) FROM `scrobbles` WHERE `track` = UUID_TO_BIN(?, true) GROUP BY `track`, `user`", trackUUID)
// if err != nil {
// log.Printf("Failed to fetch scrobble count: %+v", err)
// return response, errors.New("Failed to fetch combined scrobbles")
// }
// rows, err := db.Query(
// "SELECT BIN_TO_UUID(`scrobbles`.`user`, true), `users`.`username`, COUNT(*) "+
// "FROM `scrobbles` "+
// "JOIN `users` ON `scrobbles`.`user` = `users`.`uuid` "+
// "WHERE `track` = UUID_TO_BIN(?, true) "+
// "GROUP BY `scrobbles`.`user` "+
// "ORDER BY COUNT(*) DESC LIMIT ?",
// trackUUID, limit)
// if err != nil {
// log.Printf("Failed to fetch scrobbles: %+v", err)
// return response, errors.New("Failed to fetch combined scrobbles")
// }
// defer rows.Close()
// for rows.Next() {
// item := TopUserResponseItem{}
// err := rows.Scan(&item.UserUUID, &item.UserName, &item.Count)
// if err != nil {
// log.Printf("Failed to fetch scrobbles: %+v", err)
// return response, errors.New("Failed to fetch combined scrobbles")
// }
// count++
// response.Items = append(response.Items, item)
// }
// err = rows.Err()
// if err != nil {
// log.Printf("Failed to fetch scrobbles: %+v", err)
// return response, errors.New("Failed to fetch scrobbles")
// }
// response.Meta.Count = count
// response.Meta.Total = total
// response.Meta.Page = page
return response, nil
}

View File

@ -227,17 +227,17 @@ func ParseSpotifyInput(ctx context.Context, userUUID string, data spotify.Recent
} }
// updateImageDataFromSpotify update artist/album images from spotify ;D // updateImageDataFromSpotify update artist/album images from spotify ;D
func (user *User) updateImageDataFromSpotify() error { func (user *User) updateImageDataFromSpotify() {
// Check that data is set before we attempt to pull // Check that data is set before we attempt to pull
val, _ := getConfigValue("SPOTIFY_API_SECRET") val, _ := getConfigValue("SPOTIFY_API_SECRET")
if val == "" { if val == "" {
return nil return
} }
// TO BE REWORKED TO NOT USE A DAMN USER ARGHHH // TO BE REWORKED TO NOT USE A DAMN USER ARGHHH
dbToken, err := user.getSpotifyTokens() dbToken, err := user.getSpotifyTokens()
if err != nil { if err != nil {
return nil return
} }
token := new(oauth2.Token) token := new(oauth2.Token)
@ -252,7 +252,7 @@ func (user *User) updateImageDataFromSpotify() error {
rows, err := db.Query("SELECT BIN_TO_UUID(`uuid`, true), `name` FROM `artists` WHERE IFNULL(`img`,'') NOT IN ('pending', 'complete') LIMIT 100") rows, err := db.Query("SELECT BIN_TO_UUID(`uuid`, true), `name` FROM `artists` WHERE IFNULL(`img`,'') NOT IN ('pending', 'complete') LIMIT 100")
if err != nil { if err != nil {
log.Printf("Failed to fetch config: %+v", err) log.Printf("Failed to fetch config: %+v", err)
return errors.New("Failed to fetch artists") return
} }
toUpdate := make(map[string]string) toUpdate := make(map[string]string)
@ -263,7 +263,7 @@ func (user *User) updateImageDataFromSpotify() error {
if err != nil { if err != nil {
log.Printf("Failed to fetch artists: %+v", err) log.Printf("Failed to fetch artists: %+v", err)
rows.Close() rows.Close()
return errors.New("Failed to fetch artist") return
} }
res, err := client.Search(ctx, name, spotify.SearchTypeArtist) res, err := client.Search(ctx, name, spotify.SearchTypeArtist)
if len(res.Artists.Artists) > 0 { if len(res.Artists.Artists) > 0 {
@ -296,7 +296,7 @@ func (user *User) updateImageDataFromSpotify() error {
rows, err = db.Query("SELECT BIN_TO_UUID(`uuid`, true), `name` FROM `albums` WHERE IFNULL(`img`,'') NOT IN ('pending', 'complete') LIMIT 100") rows, err = db.Query("SELECT BIN_TO_UUID(`uuid`, true), `name` FROM `albums` WHERE IFNULL(`img`,'') NOT IN ('pending', 'complete') LIMIT 100")
if err != nil { if err != nil {
log.Printf("Failed to fetch config: %+v", err) log.Printf("Failed to fetch config: %+v", err)
return errors.New("Failed to fetch artists") return
} }
toUpdate = make(map[string]string) toUpdate = make(map[string]string)
@ -307,7 +307,7 @@ func (user *User) updateImageDataFromSpotify() error {
if err != nil { if err != nil {
log.Printf("Failed to fetch albums: %+v", err) log.Printf("Failed to fetch albums: %+v", err)
rows.Close() rows.Close()
return errors.New("Failed to fetch album") return
} }
res, err := client.Search(ctx, name, spotify.SearchTypeAlbum) res, err := client.Search(ctx, name, spotify.SearchTypeAlbum)
if len(res.Albums.Albums) > 0 { if len(res.Albums.Albums) > 0 {
@ -331,5 +331,6 @@ func (user *User) updateImageDataFromSpotify() error {
_ = album.updateAlbum("img", "pending", tx) _ = album.updateAlbum("img", "pending", tx)
} }
tx.Commit() tx.Commit()
return nil
return
} }

View File

@ -76,9 +76,11 @@ func HandleRequests(port string) {
v1.HandleFunc("/artists/top/{uuid}", limitMiddleware(getArtists, lightLimiter)).Methods("GET") v1.HandleFunc("/artists/top/{uuid}", limitMiddleware(getArtists, lightLimiter)).Methods("GET")
v1.HandleFunc("/artists/{uuid}", limitMiddleware(getArtist, lightLimiter)).Methods("GET") v1.HandleFunc("/artists/{uuid}", limitMiddleware(getArtist, lightLimiter)).Methods("GET")
v1.HandleFunc("/artists/{uuid}/top", limitMiddleware(getTopUsersForArtist, lightLimiter)).Methods("GET")
v1.HandleFunc("/albums/top/{uuid}", limitMiddleware(getArtists, lightLimiter)).Methods("GET") v1.HandleFunc("/albums/top/{uuid}", limitMiddleware(getArtists, lightLimiter)).Methods("GET")
v1.HandleFunc("/albums/{uuid}", limitMiddleware(getAlbum, lightLimiter)).Methods("GET") v1.HandleFunc("/albums/{uuid}", limitMiddleware(getAlbum, lightLimiter)).Methods("GET")
v1.HandleFunc("/albums/{uuid}/top", limitMiddleware(getTopUsersForAlbum, lightLimiter)).Methods("GET")
v1.HandleFunc("/tracks/top/{uuid}", limitMiddleware(getTracks, lightLimiter)).Methods("GET") // User UUID - Top Tracks v1.HandleFunc("/tracks/top/{uuid}", limitMiddleware(getTracks, lightLimiter)).Methods("GET") // User UUID - Top Tracks
v1.HandleFunc("/tracks/{uuid}", limitMiddleware(getTrack, lightLimiter)).Methods("GET") // Track UUID v1.HandleFunc("/tracks/{uuid}", limitMiddleware(getTrack, lightLimiter)).Methods("GET") // Track UUID
@ -417,6 +419,8 @@ func patchUser(w http.ResponseWriter, r *http.Request, claims CustomClaims, reqU
} else if k == "token" { } else if k == "token" {
token := generateToken(32) token := generateToken(32)
userFull.updateUser("token", token, ip) userFull.updateUser("token", token, ip)
} else if k == "active" {
userFull.updateUser("active", "0", ip)
} }
} }
@ -685,6 +689,56 @@ func getTopUsersForTrack(w http.ResponseWriter, r *http.Request) {
w.Write(json) w.Write(json)
} }
// getTopUsersForAlbum - I suck at naming. Returns top users that have scrobbled this track.
func getTopUsersForAlbum(w http.ResponseWriter, r *http.Request) {
var uuid string
for k, v := range mux.Vars(r) {
if k == "uuid" {
uuid = v
}
}
if uuid == "" {
throwOkError(w, "Invalid UUID")
return
}
userList, err := getTopUsersForAlbumUUID(uuid, 10, 1)
if err != nil {
throwOkError(w, err.Error())
return
}
json, _ := json.Marshal(&userList)
w.WriteHeader(http.StatusOK)
w.Write(json)
}
// getTopUsersForArtist - I suck at naming. Returns top users that have scrobbled this track.
func getTopUsersForArtist(w http.ResponseWriter, r *http.Request) {
var uuid string
for k, v := range mux.Vars(r) {
if k == "uuid" {
uuid = v
}
}
if uuid == "" {
throwOkError(w, "Invalid UUID")
return
}
userList, err := getTopUsersForArtistUUID(uuid, 10, 1)
if err != nil {
throwOkError(w, err.Error())
return
}
json, _ := json.Marshal(&userList)
w.WriteHeader(http.StatusOK)
w.Write(json)
}
// postSpotifyResponse - Oauth Response from Spotify // postSpotifyResponse - Oauth Response from Spotify
func postSpotifyReponse(w http.ResponseWriter, r *http.Request) { func postSpotifyReponse(w http.ResponseWriter, r *http.Request) {
err := connectSpotifyResponse(r) err := connectSpotifyResponse(r)
@ -782,7 +836,7 @@ func getServerInfo(w http.ResponseWriter, r *http.Request) {
} }
info := ServerInfo{ info := ServerInfo{
Version: "0.1.1", Version: "0.1.2",
RegistrationEnabled: registrationEnabled, RegistrationEnabled: registrationEnabled,
} }

View File

@ -26,27 +26,11 @@ type TopTrack struct {
Img string `json:"img"` Img string `json:"img"`
Plays int `json:"plays"` Plays int `json:"plays"`
} }
type TopTracks struct { type TopTracks struct {
Tracks map[int]TopTrack `json:"tracks"` Tracks map[int]TopTrack `json:"tracks"`
} }
type TopUserTrackResponse struct {
Meta TopUserTrackResponseMeta `json:"meta"`
Items []TopUserTrackResponseItem `json:"items"`
}
type TopUserTrackResponseMeta struct {
Count int `json:"count"`
Total int `json:"total"`
Page int `json:"page"`
}
type TopUserTrackResponseItem struct {
UserUUID string `json:"user_uuid"`
Count int `json:"count"`
UserName string `json:"user_name"`
}
// 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, legnth int, mbid string, spotifyId 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{}
@ -313,8 +297,8 @@ func (track *Track) getAlbumsForTrack() error {
} }
// getTopUsersForTrackUUID - Returns list of top users for a track // getTopUsersForTrackUUID - Returns list of top users for a track
func getTopUsersForTrackUUID(trackUUID string, limit int, page int) (TopUserTrackResponse, error) { func getTopUsersForTrackUUID(trackUUID string, limit int, page int) (TopUserResponse, error) {
response := TopUserTrackResponse{} response := TopUserResponse{}
var count int var count int
total, err := getDbCount( total, err := getDbCount(
@ -341,7 +325,7 @@ func getTopUsersForTrackUUID(trackUUID string, limit int, page int) (TopUserTrac
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
item := TopUserTrackResponseItem{} item := TopUserResponseItem{}
err := rows.Scan(&item.UserUUID, &item.UserName, &item.Count) err := rows.Scan(&item.UserUUID, &item.UserName, &item.Count)
if err != nil { if err != nil {
log.Printf("Failed to fetch scrobbles: %+v", err) log.Printf("Failed to fetch scrobbles: %+v", err)

View File

@ -48,6 +48,23 @@ type UserResponse struct {
NavidromeURL string `json:"navidrome_server"` NavidromeURL string `json:"navidrome_server"`
} }
type TopUserResponse struct {
Meta TopUserResponseMeta `json:"meta"`
Items []TopUserResponseItem `json:"items"`
}
type TopUserResponseMeta struct {
Count int `json:"count"`
Total int `json:"total"`
Page int `json:"page"`
}
type TopUserResponseItem struct {
UserUUID string `json:"user_uuid"`
Count int `json:"count"`
UserName string `json:"user_name"`
}
// createUser - Called from API // createUser - Called from API
func createUser(req *RequestRequest, ip net.IP) error { func createUser(req *RequestRequest, ip net.IP) error {
// Check if user already exists.. // Check if user already exists..

View File

@ -0,0 +1 @@
CREATE DATABASE IF NOT EXISTS goscrobble;