diff --git a/.env.example b/.env.example
index 64c5ad79..b75930d0 100644
--- a/.env.example
+++ b/.env.example
@@ -8,6 +8,8 @@ REDIS_DB=
REDIS_PREFIX="gs:"
REDIS_AUTH=""
+TIMEZONE=Etc/UTC
+
JWT_SECRET=
JWT_EXPIRY=86400
diff --git a/README.md b/README.md
index dfac77d7..ca2f2a76 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,8 @@ There are prebuilt binaries/packages available.
Copy .env.example to .env and set variables. You can use https://www.grc.com/passwords.htm to generate a JWT_SECRET.
## More documentation
+[Changelog](docs/changelog.md)
+
[Environment Variables](docs/config.md)
## Setup MySQL
diff --git a/docs/changelog.md b/docs/changelog.md
new file mode 100644
index 00000000..eb96f457
--- /dev/null
+++ b/docs/changelog.md
@@ -0,0 +1,3 @@
+# 0.0.1
+- Initial commit
+- Added basic registration/login flow
diff --git a/internal/goscrobble/album.go b/internal/goscrobble/album.go
new file mode 100644
index 00000000..de3c0cac
--- /dev/null
+++ b/internal/goscrobble/album.go
@@ -0,0 +1,85 @@
+package goscrobble
+
+import (
+ "database/sql"
+ "errors"
+ "log"
+)
+
+type Album struct {
+ Uuid string `json:"uuid"`
+ Name string `json:"name"`
+ Desc sql.NullString `json:"desc"`
+ Img sql.NullString `json:"img"`
+ MusicBrainzID sql.NullString `json:"mbid"`
+}
+
+// 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) {
+ album := 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")
+ }
+
+ album = fetchAlbum("mbid", mbid, tx)
+ }
+ } else {
+ album = fetchAlbum("name", name, tx)
+ if album.Uuid == "" {
+ err := insertNewAlbum(name, mbid, tx)
+ if err != nil {
+ log.Printf("Error inserting album via Name %s %+v", name, err)
+ return album, errors.New("Failed to insert album")
+ }
+
+ album = fetchAlbum("name", name, tx)
+ }
+ }
+
+ if album.Uuid == "" {
+ return album, errors.New("Unable to fetch album!")
+ }
+
+ return album, nil
+}
+
+func fetchAlbum(col string, val string, tx *sql.Tx) Album {
+ var album Album
+ err := tx.QueryRow(
+ "SELECT BIN_TO_UUID(`uuid`, true), `name`, `desc`, `img`, `mbid` FROM `albums` WHERE `"+col+"` = ?",
+ val).Scan(&album.Uuid, &album.Name, &album.Desc, &album.Img, &album.MusicBrainzID)
+
+ if err != nil {
+ if err != sql.ErrNoRows {
+ log.Printf("Error fetching albums: %+v", err)
+ }
+ }
+
+ 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)
+
+ return err
+}
+
+func (album *Album) linkAlbumToArtists(artists []string, tx *sql.Tx) error {
+ var err error
+ for _, artist := range artists {
+ _, err = tx.Exec("INSERT INTO `track_artist` (`track`, `artist`) "+
+ "VALUES (UUID_TO_BIN(?, true), UUID_TO_BIN(?, true)", album.Uuid, artist)
+ if err != nil {
+ return err
+ }
+ }
+
+ return err
+}
diff --git a/internal/goscrobble/artist.go b/internal/goscrobble/artist.go
new file mode 100644
index 00000000..6058311d
--- /dev/null
+++ b/internal/goscrobble/artist.go
@@ -0,0 +1,72 @@
+package goscrobble
+
+import (
+ "database/sql"
+ "errors"
+ "log"
+)
+
+type Artist struct {
+ Uuid string `json:"uuid"`
+ Name string `json:"name"`
+ Desc sql.NullString `json:"desc"`
+ Img sql.NullString `json:"img"`
+ MusicBrainzID sql.NullString `json:"mbid"`
+}
+
+// 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) {
+ artist := 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")
+ }
+
+ artist = fetchArtist("mbid", mbid, tx)
+ }
+ } else {
+ 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 artist.Uuid == "" {
+ return artist, errors.New("Unable to fetch artist!")
+ }
+
+ return artist, nil
+}
+
+func fetchArtist(col string, val string, tx *sql.Tx) Artist {
+ var artist Artist
+ err := tx.QueryRow(
+ "SELECT BIN_TO_UUID(`uuid`, true), `name`, `desc`, `img`, `mbid` FROM `artists` WHERE `"+col+"` = ?",
+ val).Scan(&artist.Uuid, &artist.Name, &artist.Desc, &artist.Img, &artist.MusicBrainzID)
+
+ if err != nil {
+ if err != sql.ErrNoRows {
+ log.Printf("Error fetching artists: %+v", err)
+ }
+ }
+
+ 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)
+
+ return err
+}
diff --git a/internal/goscrobble/db.go b/internal/goscrobble/db.go
index d3b7f9a0..210a169f 100644
--- a/internal/goscrobble/db.go
+++ b/internal/goscrobble/db.go
@@ -6,6 +6,7 @@ import (
"fmt"
"log"
"os"
+ "strings"
"time"
_ "github.com/go-sql-driver/mysql"
@@ -22,8 +23,13 @@ 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")
+ dbConn, err := sql.Open("mysql", dbUser+":"+dbPass+"@tcp("+dbHost+")/"+dbName+"?multiStatements=true"+dbTz)
if err != nil {
panic(err)
}
diff --git a/internal/goscrobble/jellyfin.go b/internal/goscrobble/jellyfin.go
index a660d11c..154beac7 100644
--- a/internal/goscrobble/jellyfin.go
+++ b/internal/goscrobble/jellyfin.go
@@ -1,6 +1,50 @@
package goscrobble
+import (
+ "database/sql"
+ "errors"
+ "fmt"
+ "log"
+ "net"
+)
+
// ParseJellyfinInput - Transform API data into a common struct
-func ParseJellyfinInput(test string) string {
- return test
+func ParseJellyfinInput(userUUID string, data map[string]interface{}, ip net.IP, tx *sql.Tx) error {
+ log.Printf("%+v : %+v", userUUID, data)
+
+ // Insert artist if not exist
+ 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")
+ }
+
+ // 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)
+ 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)
+ if err != nil {
+ log.Printf("%+v", err)
+ return errors.New("Failed to map track")
+ }
+
+ // Insert album if not exist
+ err = insertScrobble(userUUID, track.Uuid, 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
}
diff --git a/internal/goscrobble/scrobble.go b/internal/goscrobble/scrobble.go
new file mode 100644
index 00000000..48e63981
--- /dev/null
+++ b/internal/goscrobble/scrobble.go
@@ -0,0 +1,50 @@
+package goscrobble
+
+import (
+ "database/sql"
+ "errors"
+ "log"
+ "net"
+ "time"
+)
+
+type Scrobble struct {
+ Uuid string `json:"uuid"`
+ CreatedAt time.Time `json:"created_at"`
+ CreatedIp net.IP `json:"created_ip"`
+ User string `json:"user"`
+ Track string `json:"track"`
+}
+
+// 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)
+ if err != nil {
+ log.Printf("Error inserting scrobble %s %+v", user, err)
+ return errors.New("Failed to insert scrobble!")
+ }
+
+ return nil
+}
+
+func fetchScrobble(col string, val string, tx *sql.Tx) Scrobble {
+ var scrobble Scrobble
+ err := tx.QueryRow(
+ "SELECT BIN_TO_UUID(`uuid`, true), `created_at`, `created_ip`, `user`, `track` FROM `scrobbles` WHERE `"+col+"` = ?",
+ val).Scan(&scrobble.Uuid, &scrobble.CreatedAt, &scrobble.CreatedIp, &scrobble.User, &scrobble.Track)
+
+ if err != nil {
+ if err != sql.ErrNoRows {
+ log.Printf("Error fetching scrobbles: %+v", err)
+ }
+ }
+
+ return scrobble
+}
+
+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)
+
+ return err
+}
diff --git a/internal/goscrobble/server.go b/internal/goscrobble/server.go
index 8a844e38..2791baad 100644
--- a/internal/goscrobble/server.go
+++ b/internal/goscrobble/server.go
@@ -8,6 +8,7 @@ import (
"net/http"
"os"
"path/filepath"
+ "strings"
"github.com/gorilla/mux"
"github.com/rs/cors"
@@ -25,10 +26,10 @@ type jsonResponse struct {
}
// Limits to 1 req / 10 sec
-var heavyLimiter = NewIPRateLimiter(0.1, 1)
+var heavyLimiter = NewIPRateLimiter(0.25, 2)
// Limits to 5 req / sec
-var standardLimiter = NewIPRateLimiter(5, 5)
+var standardLimiter = NewIPRateLimiter(1, 5)
// List of Reverse proxies
var ReverseProxies []string
@@ -45,15 +46,16 @@ func HandleRequests(port string) {
v1 := r.PathPrefix("/api/v1").Subrouter()
// Static Token for /ingress
- v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(serveEndpoint))
+ v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(handleIngress))
// JWT Auth
- v1.HandleFunc("/profile/{id}", jwtMiddleware(serveEndpoint))
+ // v1.HandleFunc("/profile/{id}", jwtMiddleware(handleIngress))
// No Auth
v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST")
v1.HandleFunc("/login", limitMiddleware(handleLogin, standardLimiter)).Methods("POST")
- v1.HandleFunc("/logout", serveEndpoint).Methods("POST")
+ // For now just trash JWT in frontend until we have full state management "Good enough"
+ // v1.HandleFunc("/logout", handleIngress).Methods("POST")
// This just prevents it serving frontend stuff over /api
r.PathPrefix("/api")
@@ -126,9 +128,21 @@ func generateJsonError(m string) []byte {
// tokenMiddleware - Validates token to a user
func tokenMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
- throwUnauthorized(w, "Invalid API Token")
- return
- // next(res, req)
+ fullToken := r.Header.Get("Authorization")
+ authToken := strings.Replace(fullToken, "Bearer ", "", 1)
+ if authToken == "" {
+ throwUnauthorized(w, "A token is required")
+ }
+
+ userUuid, err := getUserForToken(authToken)
+ if err != nil {
+ throwUnauthorized(w, err.Error())
+ return
+ }
+
+ // Lets tack this on the request for now..
+ r.Header.Set("UserUUID", userUuid)
+ next(w, r)
}
}
@@ -136,8 +150,7 @@ func tokenMiddleware(next http.HandlerFunc) http.HandlerFunc {
func jwtMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
throwUnauthorized(w, "Invalid JWT Token")
- return
- // next(res, req)
+ next(w, r)
}
}
@@ -175,7 +188,7 @@ func handleRegister(w http.ResponseWriter, r *http.Request) {
return
}
- msg := generateJsonMessage("User created succesfully")
+ msg := generateJsonMessage("User created succesfully. You may now login")
w.WriteHeader(http.StatusCreated)
w.Write(msg)
}
@@ -202,14 +215,35 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
}
// serveEndpoint - API stuffs
-func serveEndpoint(w http.ResponseWriter, r *http.Request) {
- _, err := decodeJson(r.Body)
+func handleIngress(w http.ResponseWriter, r *http.Request) {
+ bodyJson, err := decodeJson(r.Body)
if err != nil {
// If we can't decode. Lets tell them nicely.
http.Error(w, "{\"error\":\"Invalid JSON\"}", http.StatusBadRequest)
return
}
+ ingressType := strings.Replace(r.URL.Path, "/api/v1/ingress/", "", 1)
+ log.Println(ingressType)
+ switch ingressType {
+ case "jellyfin":
+ tx, _ := db.Begin()
+ ip := getUserIp(r)
+ err := ParseJellyfinInput(r.Header.Get("UserUUID"), bodyJson, ip, tx)
+ if err != nil {
+ log.Printf("Error inserting track: %+v", err)
+ tx.Rollback()
+ throwBadReq(w, err.Error())
+ return
+ }
+ err = tx.Commit()
+ if err != nil {
+ throwBadReq(w, err.Error())
+ return
+ }
+ throwOkMessage(w, "{}")
+ return
+ }
// Lets trick 'em for now ;) ;)
fmt.Fprintf(w, "{}")
}
diff --git a/internal/goscrobble/tokens.go b/internal/goscrobble/tokens.go
new file mode 100644
index 00000000..8505500d
--- /dev/null
+++ b/internal/goscrobble/tokens.go
@@ -0,0 +1,25 @@
+package goscrobble
+
+import (
+ "errors"
+ "math/rand"
+)
+
+const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+func generateToken(n int) string {
+ b := make([]byte, n)
+ for i := range b {
+ b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
+ }
+ return string(b)
+}
+
+func getUserForToken(token string) (string, error) {
+ var uuid string
+ err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true) FROM `users` WHERE `token` = ? AND `active` = 1", token).Scan(&uuid)
+ if err != nil {
+ return "", errors.New("Invalid Token")
+ }
+ return uuid, nil
+}
diff --git a/internal/goscrobble/track.go b/internal/goscrobble/track.go
new file mode 100644
index 00000000..a5aa3340
--- /dev/null
+++ b/internal/goscrobble/track.go
@@ -0,0 +1,112 @@
+package goscrobble
+
+import (
+ "database/sql"
+ "errors"
+ "log"
+)
+
+type Track struct {
+ Uuid string `json:"uuid"`
+ Name string `json:"name"`
+ Desc sql.NullString `json:"desc"`
+ Img sql.NullString `json:"img"`
+ MusicBrainzID sql.NullString `json:"mbid"`
+}
+
+// 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) {
+ track := 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")
+ }
+
+ track = fetchTrack("mbid", mbid, tx)
+ err = track.linkTrack(album, artists, tx)
+ if err != nil {
+ return track, err
+ }
+ }
+ } else {
+ track = fetchTrack("name", name, tx)
+ if track.Uuid == "" {
+ err := insertNewTrack(name, mbid, tx)
+ if err != nil {
+ log.Printf("Error inserting track via Name %s %+v", name, err)
+ return track, errors.New("Failed to insert track")
+ }
+
+ track = fetchTrack("name", name, tx)
+ err = track.linkTrack(album, artists, tx)
+ if err != nil {
+ return track, err
+ }
+ }
+ }
+
+ if track.Uuid == "" {
+ return track, errors.New("Unable to fetch track!")
+ }
+
+ return track, nil
+}
+
+func fetchTrack(col string, val string, tx *sql.Tx) Track {
+ var track Track
+ err := tx.QueryRow(
+ "SELECT BIN_TO_UUID(`uuid`, true), `name`, `desc`, `img`, `mbid` FROM `tracks` WHERE `"+col+"` = ?",
+ val).Scan(&track.Uuid, &track.Name, &track.Desc, &track.Img, &track.MusicBrainzID)
+
+ if err != nil {
+ if err != sql.ErrNoRows {
+ log.Printf("Error fetching tracks: %+v", err)
+ }
+ }
+
+ 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)
+
+ return err
+}
+
+func (track *Track) linkTrack(album string, artists []string, tx *sql.Tx) error {
+ err := track.linkTrackToAlbum(album, tx)
+ if err != nil {
+ return err
+ }
+ err = track.linkTrackToArtists(artists, tx)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (track Track) linkTrackToAlbum(albumUuid string, tx *sql.Tx) error {
+ _, err := tx.Exec("INSERT INTO `track_album` (`track`, `album`) "+
+ "VALUES (UUID_TO_BIN(?, true), UUID_TO_BIN(?, true))", track.Uuid, albumUuid)
+
+ return err
+}
+
+func (track Track) linkTrackToArtists(artists []string, tx *sql.Tx) error {
+ var err error
+ for _, artist := range artists {
+ _, err = tx.Exec("INSERT INTO `track_artist` (`track`, `artist`) "+
+ "VALUES (UUID_TO_BIN(?, true),UUID_TO_BIN(?, true))", track.Uuid, artist)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/internal/goscrobble/user.go b/internal/goscrobble/user.go
index a604f926..dd5f0340 100644
--- a/internal/goscrobble/user.go
+++ b/internal/goscrobble/user.go
@@ -153,13 +153,14 @@ func generateJwt(user User) (string, error) {
// insertUser - Does the dirtywork!
func insertUser(username string, email string, password []byte, ip net.IP) error {
- _, err := db.Exec("INSERT INTO users (uuid, created_at, created_ip, modified_at, modified_ip, username, email, password) "+
- "VALUES (UUID_TO_BIN(UUID(), true),NOW(),?,NOW(),?,?,?,?)", ip, ip, username, email, password)
+ token := generateToken(32)
+ _, err := db.Exec("INSERT INTO users (uuid, created_at, created_ip, modified_at, modified_ip, username, email, password, token) "+
+ "VALUES (UUID_TO_BIN(UUID(), true),NOW(),?,NOW(),?,?,?,?,?)", ip, ip, username, email, password, token)
return err
}
-func updateUser(uuid string, field string, value string, ip string) error {
+func updateUser(uuid string, field string, value string, ip net.IP) error {
_, err := db.Exec("UPDATE users SET ? = ?, modified_at = NOW(), modified_ip = ? WHERE uuid = ?", field, value, uuid, ip)
return err
diff --git a/internal/goscrobble/utils.go b/internal/goscrobble/utils.go
index a5dc3d39..0078368f 100644
--- a/internal/goscrobble/utils.go
+++ b/internal/goscrobble/utils.go
@@ -4,6 +4,7 @@ import (
"encoding/hex"
"encoding/json"
"io"
+ "log"
"math/big"
"net"
"net/http"
@@ -44,11 +45,14 @@ func getUserIp(r *http.Request) net.IP {
var ip net.IP
host, _, _ := net.SplitHostPort(r.RemoteAddr)
if contains(ReverseProxies, host) {
- host = r.Header.Get("X-FOWARDED-FOR")
+ forwardedFor := r.Header.Get("X-FOWARDED-FOR")
+ if forwardedFor != "" {
+ host = forwardedFor
+ }
}
ip = net.ParseIP(host)
-
+ log.Printf("%+v", ip)
return ip
}
diff --git a/migrations/3_tracks.up.sql b/migrations/3_tracks.up.sql
index 98a954cf..8219705e 100644
--- a/migrations/3_tracks.up.sql
+++ b/migrations/3_tracks.up.sql
@@ -9,23 +9,23 @@ CREATE TABLE IF NOT EXISTS `links` (
CREATE TABLE IF NOT EXISTS `artists` (
`uuid` BINARY(16) PRIMARY KEY,
- `name` BINARY(16) NOT NULL,
+ `name` VARCHAR(255) NOT NULL,
`desc` TEXT,
- `img` VARCHAR(255) DEFAULT ''
+ `img` VARCHAR(255) DEFAULT NULL
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS `albums` (
`uuid` BINARY(16) PRIMARY KEY,
- `name` BINARY(16) NOT NULL,
+ `name` VARCHAR(255) NOT NULL,
`desc` TEXT,
- `img` VARCHAR(255) DEFAULT ''
+ `img` VARCHAR(255) DEFAULT NULL
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS `tracks` (
`uuid` BINARY(16) PRIMARY KEY,
- `name` BINARY(16) NOT NULL,
+ `name` VARCHAR(255) NOT NULL,
`desc` TEXT,
- `img` VARCHAR(255) DEFAULT ''
+ `img` VARCHAR(255) DEFAULT NULL
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS `scrobbles` (
@@ -40,14 +40,6 @@ CREATE TABLE IF NOT EXISTS `scrobbles` (
FOREIGN KEY (user) REFERENCES users(uuid)
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
-CREATE TABLE IF NOT EXISTS `track_artist` (
- `track` BINARY(16) NOT NULL,
- `artist` BINARY(16) NOT NULL,
- PRIMARY KEY (`track`, `artist`),
- FOREIGN KEY (track) REFERENCES tracks(uuid),
- FOREIGN KEY (artist) REFERENCES artists(uuid)
-) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
-
CREATE TABLE IF NOT EXISTS `album_artist` (
`album` BINARY(16) NOT NULL,
`artist` BINARY(16) NOT NULL,
@@ -56,4 +48,20 @@ CREATE TABLE IF NOT EXISTS `album_artist` (
FOREIGN KEY (artist) REFERENCES artists(uuid)
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
+CREATE TABLE IF NOT EXISTS `track_album` (
+ `track` BINARY(16) NOT NULL,
+ `album` BINARY(16) NOT NULL,
+ PRIMARY KEY (`track`, `album`),
+ FOREIGN KEY (track) REFERENCES tracks(uuid),
+ FOREIGN KEY (album) REFERENCES albums(uuid)
+) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
+
+CREATE TABLE IF NOT EXISTS `track_artist` (
+ `track` BINARY(16) NOT NULL,
+ `artist` BINARY(16) NOT NULL,
+ PRIMARY KEY (`track`, `artist`),
+ FOREIGN KEY (track) REFERENCES tracks(uuid),
+ FOREIGN KEY (artist) REFERENCES artists(uuid)
+) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
+
COMMIT;
diff --git a/migrations/4_mbid.down.sql b/migrations/4_mbid.down.sql
new file mode 100644
index 00000000..0322c21e
--- /dev/null
+++ b/migrations/4_mbid.down.sql
@@ -0,0 +1,7 @@
+START TRANSACTION;
+
+ALTER TABLE albums DROP COLUMN `mbid`;
+ALTER TABLE artists DROP COLUMN `mbid`;
+ALTER TABLE tracks DROP COLUMN `mbid`;
+
+COMMIT;
diff --git a/migrations/5_apitoken.down.sql b/migrations/5_apitoken.down.sql
new file mode 100644
index 00000000..3b9ffb0d
--- /dev/null
+++ b/migrations/5_apitoken.down.sql
@@ -0,0 +1 @@
+ALTER TABLE `users` DROP COLUMN `token`;
\ No newline at end of file
diff --git a/migrations/5_apitoken.up.sql b/migrations/5_apitoken.up.sql
new file mode 100644
index 00000000..e11faf55
--- /dev/null
+++ b/migrations/5_apitoken.up.sql
@@ -0,0 +1 @@
+ALTER TABLE `users` ADD COLUMN `token` VARCHAR(32) NOT NULL;
\ No newline at end of file
diff --git a/test/jellyfin_test.go b/test/jellyfin_test.go
deleted file mode 100644
index d9933987..00000000
--- a/test/jellyfin_test.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package goscrobble
-
-import (
- "testing"
-
- "git.m2.nz/go-scrobble/internal/goscrobble"
-)
-
-func TestParseJellyfinInput(t *testing.T) {
- got := goscrobble.ParseJellyfinInput("TestString!")
- if got != "TestString!" {
- t.Errorf("ParseJellyfinInput returned: %s", got)
- }
-}
diff --git a/web/package-lock.json b/web/package-lock.json
index 664fceb3..b6987644 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -21,6 +21,7 @@
"react-redux": "^7.2.3",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
+ "react-spinners": "^0.10.6",
"react-toast": "^1.0.1",
"react-toast-notifications": "^2.4.3",
"reactstrap": "^8.9.0",
@@ -16679,6 +16680,18 @@
"semver": "bin/semver"
}
},
+ "node_modules/react-spinners": {
+ "version": "0.10.6",
+ "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.10.6.tgz",
+ "integrity": "sha512-UPLcaMFhFnLWtS1zVDDT14ssW08gnZ44cjPN6GL27dEGboiCvRSthOilZXRDWESp449GB2iI6gjEbxzaMtA+dg==",
+ "dependencies": {
+ "@emotion/core": "^10.0.35"
+ },
+ "peerDependencies": {
+ "react": "^16.0.0 || ^17.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0"
+ }
+ },
"node_modules/react-toast": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/react-toast/-/react-toast-1.0.1.tgz",
@@ -35210,6 +35223,14 @@
}
}
},
+ "react-spinners": {
+ "version": "0.10.6",
+ "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.10.6.tgz",
+ "integrity": "sha512-UPLcaMFhFnLWtS1zVDDT14ssW08gnZ44cjPN6GL27dEGboiCvRSthOilZXRDWESp449GB2iI6gjEbxzaMtA+dg==",
+ "requires": {
+ "@emotion/core": "^10.0.35"
+ }
+ },
"react-toast": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/react-toast/-/react-toast-1.0.1.tgz",
diff --git a/web/package.json b/web/package.json
index e6c794f8..920e3084 100644
--- a/web/package.json
+++ b/web/package.json
@@ -16,6 +16,7 @@
"react-redux": "^7.2.3",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
+ "react-spinners": "^0.10.6",
"react-toast": "^1.0.1",
"react-toast-notifications": "^2.4.3",
"reactstrap": "^8.9.0",
diff --git a/web/src/App.js b/web/src/App.js
index 30b0c7a2..661ad89a 100644
--- a/web/src/App.js
+++ b/web/src/App.js
@@ -1,12 +1,13 @@
import './App.css';
import Home from './Components/Pages/Home';
import About from './Components/Pages/About';
+import Help from './Components/Pages/Help';
import Login from './Components/Pages/Login';
import Settings from './Components/Pages/Settings';
import Register from './Components/Pages/Register';
import Navigation from './Components/Navigation';
-import { Route, Switch } from 'react-router-dom';
+import { Route, Switch, withRouter } from 'react-router-dom';
import { connect } from "react-redux";
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
@@ -32,6 +33,7 @@ const App = () => {
+ Jellyfin Configuration
+