- Add duplicate cache checker for jellyfin/multiscrobbler
This commit is contained in:
Daniel Mason 2021-04-03 02:48:38 +13:00
parent ca7ef56326
commit bda7023949
Signed by: idanoo
GPG Key ID: 387387CDBC02F132
7 changed files with 98 additions and 23 deletions

View File

@ -3,7 +3,7 @@ stages:
- bundle - bundle
variables: variables:
VERSION: 0.0.13 VERSION: 0.0.14
build-go: build-go:
image: golang:1.16.2 image: golang:1.16.2

View File

@ -1,3 +1,6 @@
# 0.0.14
- Add duplicate cache checker for jellyfin/multiscrobbler
# 0.0.13 # 0.0.13
- Fix multiscrobbler support - Fix multiscrobbler support

View File

@ -9,30 +9,71 @@ import (
"time" "time"
) )
// ParseJellyfinInput - Transform API data into a common struct. Uses MusicBrainzID primarily type JellyfinRequest struct {
func ParseJellyfinInput(userUUID string, data map[string]interface{}, ip net.IP, tx *sql.Tx) error { Album string `json:"Album"`
// Debugging Artist string `json:"Artist"`
// fmt.Printf("%+v", data) 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") return errors.New("Media type not audio")
} }
// Safety Checks // Safety Checks
if data["Artist"] == nil { if jf.Artist == "" {
return errors.New("Missing artist data") return errors.New("Missing artist data")
} }
if data["Album"] == nil { if jf.Album == "" {
return errors.New("Missing album data") return errors.New("Missing album data")
} }
if data["Name"] == nil { if jf.Name == "" {
return errors.New("Missing track data") return errors.New("Missing track data")
} }
// Insert artist if not exist // 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 { if err != nil {
log.Printf("%+v", err) log.Printf("%+v", err)
return errors.New("Failed to map artist") 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 // Insert album if not exist
artists := []string{artist.Uuid} 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 { if err != nil {
log.Printf("%+v", err) log.Printf("%+v", err)
return errors.New("Failed to map album") return errors.New("Failed to map album")
} }
// Insert track if not exist // Insert track if not exist
length := timestampToSeconds(fmt.Sprintf("%s", data["RunTime"])) length := timestampToSeconds(jf.RunTime)
track, err := insertTrack(fmt.Sprintf("%s", data["Name"]), length, fmt.Sprintf("%s", data["Provider_musicbrainztrack"]), "", album.Uuid, artists, tx) track, err := insertTrack(jf.Name, length, jf.ProviderMusicbrainztrack, "", album.Uuid, artists, tx)
if err != nil { if err != nil {
log.Printf("%+v", err) log.Printf("%+v", err)
return errors.New("Failed to map track") 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") 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 return nil
} }

View File

@ -2,13 +2,15 @@ package goscrobble
import ( import (
"database/sql" "database/sql"
"encoding/json"
"errors" "errors"
"fmt"
"log" "log"
"net" "net"
"time" "time"
) )
type MultiScrobblerInput struct { type MultiScrobblerRequest struct {
Artists []string `json:"artists"` Artists []string `json:"artists"`
Album string `json:"album"` Album string `json:"album"`
Track string `json:"track"` Track string `json:"track"`
@ -17,8 +19,15 @@ type MultiScrobblerInput struct {
} }
// ParseMultiScrobblerInput - Transform API data // ParseMultiScrobblerInput - Transform API data
func ParseMultiScrobblerInput(userUUID string, data MultiScrobblerInput, ip net.IP, tx *sql.Tx) error { func ParseMultiScrobblerInput(userUUID string, data MultiScrobblerRequest, ip net.IP, tx *sql.Tx) error {
// Debugging // 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) artists := make([]string, 0)
albumartists := 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") 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 return nil
} }

View File

@ -73,3 +73,13 @@ func getRedisVal(key string) string {
return val 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
}

View File

@ -228,22 +228,21 @@ func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
switch ingressType { switch ingressType {
case "jellyfin": case "jellyfin":
bodyJson, err := decodeJson(r.Body) jfInput := JellyfinRequest{}
fmt.Println(err) err := json.NewDecoder(r.Body).Decode(&jfInput)
if err != nil { if err != nil {
throwInvalidJson(w) throwInvalidJson(w)
return return
} }
err = ParseJellyfinInput(userUuid, bodyJson, ip, tx) err = ParseJellyfinInput(userUuid, jfInput, ip, tx)
if err != nil { if err != nil {
fmt.Printf("Err? %+v", err)
tx.Rollback() tx.Rollback()
throwOkError(w, err.Error()) throwOkError(w, err.Error())
return return
} }
case "multiscrobbler": case "multiscrobbler":
msInput := MultiScrobblerInput{} msInput := MultiScrobblerRequest{}
err := json.NewDecoder(r.Body).Decode(&msInput) err := json.NewDecoder(r.Body).Decode(&msInput)
if err != nil { if err != nil {
fmt.Println(err) 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) { func fetchServerInfo(w http.ResponseWriter, r *http.Request) {
info := ServerInfo{ info := ServerInfo{
Version: "0.0.13", Version: "0.0.14",
} }
js, _ := json.Marshal(&info) js, _ := json.Marshal(&info)

View File

@ -1,6 +1,7 @@
package goscrobble package goscrobble
import ( import (
"crypto/md5"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -154,3 +155,8 @@ func isValidTimezone(tz string) bool {
_, err := time.LoadLocation(tz) _, err := time.LoadLocation(tz)
return err == nil return err == nil
} }
func getMd5(val string) string {
hash := md5.Sum([]byte(val))
return hex.EncodeToString(hash[:])
}