From 3698c0b4367a5f3d5181cdedefec1a4a33f2d74f Mon Sep 17 00:00:00 2001 From: Daniel Mason Date: Thu, 6 Jan 2022 10:20:30 +1300 Subject: [PATCH] 0.1.2 - Add docker-compose file for local dev - Implemented top listeners for artist/album endpoints to match track - Add recent endpoint --- .env.development | 29 ++++++++++ .env.production | 29 ++++++++++ .gitignore | 4 +- .gitlab-ci.yml | 4 +- cmd/{go-scrobble => goscrobble}/main.go | 0 data/img/placeholder.jpg | Bin 0 -> 4553 bytes docker-compose.yml | 44 +++++++++++++++ docs/changelog.md | 5 ++ docs/removing_bad_data.md | 29 ++++++++++ internal/goscrobble/album.go | 55 +++++++++++++++++++ internal/goscrobble/artist.go | 56 +++++++++++++++++++ internal/goscrobble/ingress_spotify.go | 17 +++--- internal/goscrobble/scrobble.go | 47 ++++++++++++++++ internal/goscrobble/server.go | 70 +++++++++++++++++++++++- internal/goscrobble/track.go | 26 ++------- internal/goscrobble/user.go | 17 ++++++ migrations/0_create_db.sql | 1 + 17 files changed, 398 insertions(+), 35 deletions(-) create mode 100644 .env.development create mode 100644 .env.production rename cmd/{go-scrobble => goscrobble}/main.go (100%) create mode 100644 data/img/placeholder.jpg create mode 100644 docker-compose.yml create mode 100644 migrations/0_create_db.sql diff --git a/.env.development b/.env.development new file mode 100644 index 00000000..4a369c9b --- /dev/null +++ b/.env.development @@ -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" \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 00000000..d8f029ff --- /dev/null +++ b/.env.production @@ -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" diff --git a/.gitignore b/.gitignore index 88083e5e..86717f57 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,8 @@ *.so *.dylib .env -web/.env.production -web/.env.development -web/img/* +/data # Test binary, built with `go test -c` *.test diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 15939cef..e092f188 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,7 @@ stages: - build variables: - VERSION: 0.1.1 + VERSION: 0.1.2 build-go: image: golang:1.17 @@ -10,7 +10,7 @@ build-go: only: - master script: - - go build -o goscrobble cmd/go-scrobble/*.go + - go build -o goscrobble cmd/goscrobble/*.go artifacts: expire_in: 1 week paths: diff --git a/cmd/go-scrobble/main.go b/cmd/goscrobble/main.go similarity index 100% rename from cmd/go-scrobble/main.go rename to cmd/goscrobble/main.go diff --git a/data/img/placeholder.jpg b/data/img/placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ff8ff816f4528bbe0fbbd9a6a7503fb82f1a272e GIT binary patch literal 4553 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 literal 0 HcmV?d00001 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..47b1c261 --- /dev/null +++ b/docker-compose.yml @@ -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: \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 2f798924..b6129e35 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,8 @@ +# 0.1.2 +- Add docker-compose file for local dev +- Implemented top listeners for artist/album endpoints to match track +- Add recent endpoint + # 0.1.1 - Cached all config values - Updated spotify sdk package to v2 diff --git a/docs/removing_bad_data.md b/docs/removing_bad_data.md index b8a86409..6e1ebcc8 100644 --- a/docs/removing_bad_data.md +++ b/docs/removing_bad_data.md @@ -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 scrobbles FROM scrobbles LEFT JOIN tracks ON tracks.uuid = scrobbles.track WHERE tracks.uuid is null; 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('', 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('', true) + GROUP BY track, HOUR(created_at) + HAVING count(*) > 1 + ) x + ); + + + diff --git a/internal/goscrobble/album.go b/internal/goscrobble/album.go index ab93dfb9..a0340380 100644 --- a/internal/goscrobble/album.go +++ b/internal/goscrobble/album.go @@ -128,3 +128,58 @@ func getAlbumByUUID(uuid string) (Album, error) { return album, nil } + +// getTopUsersForAlbumUUID - Returns list of top users for a track +func getTopUsersForAlbumUUID(albumUUID string, limit int, page int) (TopUserResponse, error) { + response := TopUserResponse{} + var count int + + total, err := getDbCount( + "SELECT COUNT(*) FROM `scrobbles` "+ + "JOIN `track_album` ON `track_album`.`track` = `scrobbles`.`track` "+ + "WHERE `track_album`.`album` = UUID_TO_BIN(?, true);", albumUUID) + + 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` "+ + "JOIN `track_album` ON `track_album`.`track` = `scrobbles`.`track` "+ + "WHERE `track_album`.`album` = UUID_TO_BIN(?, true) "+ + "GROUP BY `scrobbles`.`user` "+ + "ORDER BY COUNT(*) DESC LIMIT ?", + albumUUID, 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 +} diff --git a/internal/goscrobble/artist.go b/internal/goscrobble/artist.go index 9b82feb4..9b33d9ba 100644 --- a/internal/goscrobble/artist.go +++ b/internal/goscrobble/artist.go @@ -164,3 +164,59 @@ func getTopArtists(userUuid string) (TopArtists, error) { return topArtist, nil } + +// getTopUsersForArtistUUID - Returns list of top users for a track +func getTopUsersForArtistUUID(artistUUID string, limit int, page int) (TopUserResponse, error) { + response := TopUserResponse{} + + var count int + + total, err := getDbCount( + "SELECT COUNT(*) FROM `scrobbles` "+ + "JOIN `track_artist` ON `track_artist`.`track` = `scrobbles`.`track` "+ + "WHERE `track_artist`.`artist` = UUID_TO_BIN(?, true);", artistUUID) + + 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` "+ + "JOIN `track_artist` ON `track_artist`.`track` = `scrobbles`.`track` "+ + "WHERE `track_artist`.`artist` = UUID_TO_BIN(?, true) "+ + "GROUP BY `scrobbles`.`user` "+ + "ORDER BY COUNT(*) DESC LIMIT ?", + artistUUID, 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 +} diff --git a/internal/goscrobble/ingress_spotify.go b/internal/goscrobble/ingress_spotify.go index 62936f8b..78f45887 100644 --- a/internal/goscrobble/ingress_spotify.go +++ b/internal/goscrobble/ingress_spotify.go @@ -227,17 +227,17 @@ func ParseSpotifyInput(ctx context.Context, userUUID string, data spotify.Recent } // 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 val, _ := getConfigValue("SPOTIFY_API_SECRET") if val == "" { - return nil + return } // TO BE REWORKED TO NOT USE A DAMN USER ARGHHH dbToken, err := user.getSpotifyTokens() if err != nil { - return nil + return } 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") if err != nil { log.Printf("Failed to fetch config: %+v", err) - return errors.New("Failed to fetch artists") + return } toUpdate := make(map[string]string) @@ -263,7 +263,7 @@ func (user *User) updateImageDataFromSpotify() error { if err != nil { log.Printf("Failed to fetch artists: %+v", err) rows.Close() - return errors.New("Failed to fetch artist") + return } res, err := client.Search(ctx, name, spotify.SearchTypeArtist) 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") if err != nil { log.Printf("Failed to fetch config: %+v", err) - return errors.New("Failed to fetch artists") + return } toUpdate = make(map[string]string) @@ -307,7 +307,7 @@ func (user *User) updateImageDataFromSpotify() error { if err != nil { log.Printf("Failed to fetch albums: %+v", err) rows.Close() - return errors.New("Failed to fetch album") + return } res, err := client.Search(ctx, name, spotify.SearchTypeAlbum) if len(res.Albums.Albums) > 0 { @@ -331,5 +331,6 @@ func (user *User) updateImageDataFromSpotify() error { _ = album.updateAlbum("img", "pending", tx) } tx.Commit() - return nil + + return } diff --git a/internal/goscrobble/scrobble.go b/internal/goscrobble/scrobble.go index 3ae0c844..f83858a8 100644 --- a/internal/goscrobble/scrobble.go +++ b/internal/goscrobble/scrobble.go @@ -35,6 +35,7 @@ type ScrobbleResponseItem struct { Album string `json:"album"` Track ScrobbleTrackItem `json:"track"` Source string `json:"source"` + User ScrobbleTrackItem `json:"user"` } type ScrobbleTrackItem struct { @@ -127,3 +128,49 @@ func checkIfScrobbleExists(userUuid string, timestamp time.Time, source string) return count != 0 } + +func getRecentScrobbles() (ScrobbleResponse, error) { + scrobbleReq := ScrobbleResponse{} + var count int + limit := 50 + + rows, err := db.Query( + "SELECT BIN_TO_UUID(`scrobbles`.`uuid`, true), `scrobbles`.`created_at`, BIN_TO_UUID(`artists`.`uuid`, true), `artists`.`name`, `albums`.`name`, BIN_TO_UUID(`tracks`.`uuid`, true), `tracks`.`name`, `scrobbles`.`source`, BIN_TO_UUID(`scrobbles`.`user`, true), `users`.`username` 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 "+ + "JOIN artists ON track_artist.artist = artists.uuid "+ + "JOIN albums ON track_album.album = albums.uuid "+ + "JOIN users ON scrobbles.user = users.uuid "+ + "GROUP BY scrobbles.uuid, albums.uuid "+ + "ORDER BY scrobbles.created_at DESC LIMIT ?", limit) + + if err != nil { + log.Printf("Failed to fetch scrobbles: %+v", err) + return scrobbleReq, errors.New("Failed to fetch scrobbles") + } + defer rows.Close() + + for rows.Next() { + item := ScrobbleResponseItem{} + err := rows.Scan(&item.UUID, &item.Timestamp, &item.Artist.UUID, &item.Artist.Name, &item.Album, &item.Track.UUID, &item.Track.Name, &item.Source, &item.User.UUID, &item.User.Name) + if err != nil { + log.Printf("Failed to fetch scrobbles: %+v", err) + return scrobbleReq, errors.New("Failed to fetch scrobbles") + } + count++ + scrobbleReq.Items = append(scrobbleReq.Items, item) + } + + err = rows.Err() + if err != nil { + log.Printf("Failed to fetch scrobbles: %+v", err) + return scrobbleReq, errors.New("Failed to fetch scrobbles") + } + + scrobbleReq.Meta.Count = count + scrobbleReq.Meta.Total = 50 + scrobbleReq.Meta.Page = 1 + + return scrobbleReq, nil +} diff --git a/internal/goscrobble/server.go b/internal/goscrobble/server.go index a7cfa5e4..9ebb0029 100644 --- a/internal/goscrobble/server.go +++ b/internal/goscrobble/server.go @@ -72,13 +72,17 @@ func HandleRequests(port string) { // No Auth v1.HandleFunc("/stats", limitMiddleware(handleStats, lightLimiter)).Methods("GET") + v1.HandleFunc("/recent", limitMiddleware(handleRecentScrobbles, lightLimiter)).Methods("GET") + v1.HandleFunc("/profile/{username}", limitMiddleware(getProfile, 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}/top", limitMiddleware(getTopUsersForArtist, 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}/top", limitMiddleware(getTopUsersForAlbum, lightLimiter)).Methods("GET") 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 @@ -417,6 +421,8 @@ func patchUser(w http.ResponseWriter, r *http.Request, claims CustomClaims, reqU } else if k == "token" { token := generateToken(32) userFull.updateUser("token", token, ip) + } else if k == "active" { + userFull.updateUser("active", "0", ip) } } @@ -685,6 +691,56 @@ func getTopUsersForTrack(w http.ResponseWriter, r *http.Request) { 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 func postSpotifyReponse(w http.ResponseWriter, r *http.Request) { err := connectSpotifyResponse(r) @@ -782,7 +838,7 @@ func getServerInfo(w http.ResponseWriter, r *http.Request) { } info := ServerInfo{ - Version: "0.1.1", + Version: "0.1.2", RegistrationEnabled: registrationEnabled, } @@ -790,3 +846,15 @@ func getServerInfo(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(js) } + +func handleRecentScrobbles(w http.ResponseWriter, r *http.Request) { + scrobbleList, err := getRecentScrobbles() + if err != nil { + throwOkError(w, err.Error()) + return + } + + json, _ := json.Marshal(&scrobbleList) + w.WriteHeader(http.StatusOK) + w.Write(json) +} diff --git a/internal/goscrobble/track.go b/internal/goscrobble/track.go index 88f0df05..3e942673 100644 --- a/internal/goscrobble/track.go +++ b/internal/goscrobble/track.go @@ -26,27 +26,11 @@ type TopTrack struct { Img string `json:"img"` Plays int `json:"plays"` } + type TopTracks struct { 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 func insertTrack(name string, legnth int, mbid string, spotifyId string, album string, artists []string, tx *sql.Tx) (Track, error) { track := Track{} @@ -313,12 +297,12 @@ func (track *Track) getAlbumsForTrack() error { } // getTopUsersForTrackUUID - Returns list of top users for a track -func getTopUsersForTrackUUID(trackUUID string, limit int, page int) (TopUserTrackResponse, error) { - response := TopUserTrackResponse{} +func getTopUsersForTrackUUID(trackUUID string, limit int, page int) (TopUserResponse, error) { + response := TopUserResponse{} var count int total, err := getDbCount( - "SELECT COUNT(*) FROM `scrobbles` WHERE `track` = UUID_TO_BIN(?, true) GROUP BY `track`, `user`", trackUUID) + "SELECT COUNT(*) FROM `scrobbles` WHERE `track` = UUID_TO_BIN(?, true)", trackUUID) if err != nil { log.Printf("Failed to fetch scrobble count: %+v", err) @@ -341,7 +325,7 @@ func getTopUsersForTrackUUID(trackUUID string, limit int, page int) (TopUserTrac defer rows.Close() for rows.Next() { - item := TopUserTrackResponseItem{} + item := TopUserResponseItem{} err := rows.Scan(&item.UserUUID, &item.UserName, &item.Count) if err != nil { log.Printf("Failed to fetch scrobbles: %+v", err) diff --git a/internal/goscrobble/user.go b/internal/goscrobble/user.go index 830e5d7b..94be1192 100644 --- a/internal/goscrobble/user.go +++ b/internal/goscrobble/user.go @@ -48,6 +48,23 @@ type UserResponse struct { 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 func createUser(req *RequestRequest, ip net.IP) error { // Check if user already exists.. diff --git a/migrations/0_create_db.sql b/migrations/0_create_db.sql new file mode 100644 index 00000000..cbcfb9ce --- /dev/null +++ b/migrations/0_create_db.sql @@ -0,0 +1 @@ +CREATE DATABASE IF NOT EXISTS goscrobble; \ No newline at end of file