diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 083c8a2d..4d68b9a0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ stages: - bundle variables: - VERSION: 0.0.13 + VERSION: 0.0.14 build-go: image: golang:1.16.2 diff --git a/docs/changelog.md b/docs/changelog.md index b8f55822..a2b9a183 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,6 @@ +# 0.0.14 +- Add duplicate cache checker for jellyfin/multiscrobbler + # 0.0.13 - Fix multiscrobbler support diff --git a/internal/goscrobble/ingress_jellyfin.go b/internal/goscrobble/ingress_jellyfin.go index 50711080..3b4fe2af 100644 --- a/internal/goscrobble/ingress_jellyfin.go +++ b/internal/goscrobble/ingress_jellyfin.go @@ -9,30 +9,71 @@ import ( "time" ) -// 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) +type JellyfinRequest struct { + Album string `json:"Album"` + Artist string `json:"Artist"` + ClientName string `json:"ClientName"` + DeviceID string `json:"DeviceId"` + DeviceName string `json:"DeviceName"` + IsAutomated bool `json:"IsAutomated"` + IsPaused bool `json:"IsPaused"` + ItemID string `json:"ItemId"` + ItemType string `json:"ItemType"` + MediaSourceID string `json:"MediaSourceId"` + Name string `json:"Name"` + NotificationType string `json:"NotificationType"` + Overview string `json:"Overview"` + PlaybackPosition string `json:"PlaybackPosition"` + PlaybackPositionTicks int64 `json:"PlaybackPositionTicks"` + ProviderMusicbrainzalbum string `json:"Provider_musicbrainzalbum"` + ProviderMusicbrainzalbumartist string `json:"Provider_musicbrainzalbumartist"` + ProviderMusicbrainzartist string `json:"Provider_musicbrainzartist"` + ProviderMusicbrainzreleasegroup string `json:"Provider_musicbrainzreleasegroup"` + ProviderMusicbrainztrack string `json:"Provider_musicbrainztrack"` + RunTime string `json:"RunTime"` + RunTimeTicks int64 `json:"RunTimeTicks"` + ServerID string `json:"ServerId"` + ServerName string `json:"ServerName"` + ServerURL string `json:"ServerUrl"` + ServerVersion string `json:"ServerVersion"` + Timestamp string `json:"Timestamp"` + UserID string `json:"UserId"` + Username string `json:"Username"` + UtcTimestamp string `json:"UtcTimestamp"` + Year int64 `json:"Year"` +} - if data["ItemType"] != "Audio" { +// ParseJellyfinInput - Transform API data into a common struct. Uses MusicBrainzID primarily +func ParseJellyfinInput(userUUID string, jf JellyfinRequest, ip net.IP, tx *sql.Tx) error { + // Debugging + // fmt.Printf("%+v", jf) + + // Prevents scrobbling same song twice! + cacheKey := jf.UserID + ":" + jf.Name + ":" + jf.Artist + ":" + jf.Album + ":" + jf.ServerID + redisKey := getMd5(cacheKey + userUUID) + if getRedisKeyExists(redisKey) { + return nil + } + + if jf.ItemType != "Audio" { return errors.New("Media type not audio") } // Safety Checks - if data["Artist"] == nil { + if jf.Artist == "" { return errors.New("Missing artist data") } - if data["Album"] == nil { + if jf.Album == "" { return errors.New("Missing album data") } - if data["Name"] == nil { + if jf.Name == "" { return errors.New("Missing track data") } // Insert artist if not exist - artist, err := insertArtist(fmt.Sprintf("%s", data["Artist"]), fmt.Sprintf("%s", data["Provider_musicbrainzartist"]), "", tx) + artist, err := insertArtist(jf.Artist, jf.ProviderMusicbrainzartist, "", tx) if err != nil { log.Printf("%+v", err) return errors.New("Failed to map artist") @@ -40,15 +81,15 @@ 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(jf.Album, jf.ProviderMusicbrainzalbum, "", artists, tx) if err != nil { log.Printf("%+v", err) return errors.New("Failed to map album") } // 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) + length := timestampToSeconds(jf.RunTime) + track, err := insertTrack(jf.Name, length, jf.ProviderMusicbrainztrack, "", album.Uuid, artists, tx) if err != nil { log.Printf("%+v", err) return errors.New("Failed to map track") @@ -63,6 +104,9 @@ func ParseJellyfinInput(userUUID string, data map[string]interface{}, ip net.IP, return errors.New("Failed to map track") } - // Insert track if not exist + // Add cache key! + ttl := time.Duration(timestampToSeconds(jf.RunTime)) * time.Second + setRedisValTtl(redisKey, "1", ttl) + return nil } diff --git a/internal/goscrobble/ingress_multiscrobbler.go b/internal/goscrobble/ingress_multiscrobbler.go index 84fb1c40..408a7661 100644 --- a/internal/goscrobble/ingress_multiscrobbler.go +++ b/internal/goscrobble/ingress_multiscrobbler.go @@ -2,13 +2,15 @@ package goscrobble import ( "database/sql" + "encoding/json" "errors" + "fmt" "log" "net" "time" ) -type MultiScrobblerInput struct { +type MultiScrobblerRequest struct { Artists []string `json:"artists"` Album string `json:"album"` Track string `json:"track"` @@ -17,8 +19,15 @@ type MultiScrobblerInput struct { } // ParseMultiScrobblerInput - Transform API data -func ParseMultiScrobblerInput(userUUID string, data MultiScrobblerInput, ip net.IP, tx *sql.Tx) error { - // Debugging +func ParseMultiScrobblerInput(userUUID string, data MultiScrobblerRequest, ip net.IP, tx *sql.Tx) error { + // Cache key + json, _ := json.Marshal(data) + redisKey := getMd5(string(json) + userUUID) + if getRedisKeyExists(redisKey) { + fmt.Printf("Prevented duplicate entry!") + return nil + } + artists := make([]string, 0) albumartists := make([]string, 0) @@ -54,5 +63,9 @@ func ParseMultiScrobblerInput(userUUID string, data MultiScrobblerInput, ip net. return errors.New("Failed to map track") } + // Add cache key for the duration of the song *2 since we're caching the start time too + ttl := time.Duration(data.Duration*2) * time.Second + setRedisValTtl(redisKey, "1", ttl) + return nil } diff --git a/internal/goscrobble/redis.go b/internal/goscrobble/redis.go index a85a4454..23a8ada8 100644 --- a/internal/goscrobble/redis.go +++ b/internal/goscrobble/redis.go @@ -73,3 +73,13 @@ func getRedisVal(key string) string { return val } + +func getRedisKeyExists(key string) bool { + val, err := redisDb.Exists(ctx, redisPrefix+key).Result() + if err != nil { + log.Printf("Failed to fetch redis key (%+v) Error: %+v", key, err) + return false + } + + return val == 1 +} diff --git a/internal/goscrobble/server.go b/internal/goscrobble/server.go index 1fc777b9..cf146388 100644 --- a/internal/goscrobble/server.go +++ b/internal/goscrobble/server.go @@ -228,22 +228,21 @@ func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) { switch ingressType { case "jellyfin": - bodyJson, err := decodeJson(r.Body) - fmt.Println(err) + jfInput := JellyfinRequest{} + err := json.NewDecoder(r.Body).Decode(&jfInput) if err != nil { throwInvalidJson(w) return } - err = ParseJellyfinInput(userUuid, bodyJson, ip, tx) + err = ParseJellyfinInput(userUuid, jfInput, ip, tx) if err != nil { - fmt.Printf("Err? %+v", err) tx.Rollback() throwOkError(w, err.Error()) return } case "multiscrobbler": - msInput := MultiScrobblerInput{} + msInput := MultiScrobblerRequest{} err := json.NewDecoder(r.Body).Decode(&msInput) if err != nil { fmt.Println(err) @@ -448,7 +447,7 @@ func deleteSpotifyLink(w http.ResponseWriter, r *http.Request, u string, v strin func fetchServerInfo(w http.ResponseWriter, r *http.Request) { info := ServerInfo{ - Version: "0.0.13", + Version: "0.0.14", } js, _ := json.Marshal(&info) diff --git a/internal/goscrobble/utils.go b/internal/goscrobble/utils.go index 666d5b91..54e050ac 100644 --- a/internal/goscrobble/utils.go +++ b/internal/goscrobble/utils.go @@ -1,6 +1,7 @@ package goscrobble import ( + "crypto/md5" "encoding/hex" "encoding/json" "fmt" @@ -154,3 +155,8 @@ func isValidTimezone(tz string) bool { _, err := time.LoadLocation(tz) return err == nil } + +func getMd5(val string) string { + hash := md5.Sum([]byte(val)) + return hex.EncodeToString(hash[:]) +}