From 76fa1715754faf30848b24926dd998bb535d3238 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 19 Feb 2021 00:47:01 +0000 Subject: [PATCH] cleanup logs and use structs in jf/emby api Also means times are directly parsed when pulling data from jf/emby, which was *painful* to get working (something broke the whole program and it took me an hour to figure out it was this lol). Time parsing should be a lot stabler too. --- api.go | 170 ++++++++++++----------------------- auth.go | 10 +-- main.go | 2 +- mediabrowser/emby.go | 50 +++++------ mediabrowser/jfapi.go | 54 +++++------ mediabrowser/mediabrowser.go | 53 ++++++----- mediabrowser/models.go | 135 ++++++++++++++++++++++++++++ pwreset.go | 7 +- storage.go | 32 ++++--- 9 files changed, 303 insertions(+), 210 deletions(-) create mode 100644 mediabrowser/models.go diff --git a/api.go b/api.go index a6d7950..001cc17 100644 --- a/api.go +++ b/api.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "fmt" "os" "strconv" @@ -11,6 +10,7 @@ import ( "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" + "github.com/hrfee/jfa-go/mediabrowser" "github.com/knz/strtime" "github.com/lithammer/shortuuid/v3" "gopkg.in/ini.v1" @@ -128,11 +128,9 @@ func (app *appContext) checkInvites() { defer wait.Done() msg, err := app.email.constructExpiry(code, data, app) if err != nil { - app.err.Printf("%s: Failed to construct expiry notification", code) - app.debug.Printf("Error: %s", err) + app.err.Printf("%s: Failed to construct expiry notification: %s", code, err) } else if err := app.email.send(msg, addr); err != nil { - app.err.Printf("%s: Failed to send expiry notification", code) - app.debug.Printf("Error: %s", err) + app.err.Printf("%s: Failed to send expiry notification: %s", code, err) } else { app.info.Printf("Sent expiry notification to %s", addr) } @@ -167,11 +165,9 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool go func() { msg, err := app.email.constructExpiry(code, inv, app) if err != nil { - app.err.Printf("%s: Failed to construct expiry notification", code) - app.debug.Printf("Error: %s", err) + app.err.Printf("%s: Failed to construct expiry notification: %s", code, err) } else if err := app.email.send(msg, address); err != nil { - app.err.Printf("%s: Failed to send expiry notification", code) - app.debug.Printf("Error: %s", err) + app.err.Printf("%s: Failed to send expiry notification: %s", code, err) } else { app.info.Printf("Sent expiry notification to %s", address) } @@ -213,7 +209,7 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er if err != nil || code != 200 { return nil, code, err } - username := jfUser["Name"].(string) + username := jfUser.Name email := "" if e, ok := app.storage.emails[jfID]; ok { email = e.(string) @@ -252,7 +248,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) { var req newUserDTO gc.BindJSON(&req) existingUser, _, _ := app.jf.UserByName(req.Username, false) - if existingUser != nil { + if existingUser.Name != "" { msg := fmt.Sprintf("User already exists named %s", req.Username) app.info.Printf("%s New user failed: %s", req.Username, msg) respondUser(401, false, false, msg, gc) @@ -264,18 +260,14 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) { respondUser(401, false, false, "Unknown error", gc) return } - var id string - if user["Id"] != nil { - id = user["Id"].(string) - } - if len(app.storage.policy) != 0 { + id := user.ID + if app.storage.policy.BlockedTags != nil { status, err = app.jf.SetPolicy(id, app.storage.policy) if !(status == 200 || status == 204 || err == nil) { - app.err.Printf("%s: Failed to set user policy: Code %d", req.Username, status) - app.debug.Printf("%s: Error: %s", req.Username, err) + app.err.Printf("%s: Failed to set user policy (%d): %s", req.Username, status, err) } } - if len(app.storage.configuration) != 0 && len(app.storage.displayprefs) != 0 { + if app.storage.configuration.GroupedFolders != nil && len(app.storage.displayprefs) != 0 { status, err = app.jf.SetConfiguration(id, app.storage.configuration) if (status == 200 || status == 204) && err == nil { status, err = app.jf.SetDisplayPreferences(id, app.storage.displayprefs) @@ -323,7 +315,7 @@ type errorFunc func(gc *gin.Context) func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, success bool) { existingUser, _, _ := app.jf.UserByName(req.Username, false) - if existingUser != nil { + if existingUser.Name != "" { f = func(gc *gin.Context) { msg := fmt.Sprintf("User %s already exists", req.Username) app.info.Printf("%s: New user failed: %s", req.Code, msg) @@ -361,8 +353,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc respond(401, "confirmEmail", gc) msg, err := app.email.constructConfirmation(req.Code, req.Username, key, app) if err != nil { - app.err.Printf("%s: Failed to construct confirmation email", req.Code) - app.debug.Printf("%s: Error: %s", req.Code, err) + app.err.Printf("%s: Failed to construct confirmation email: %s", req.Code, err) } else if err := app.email.send(msg, req.Email); err != nil { app.err.Printf("%s: Failed to send user confirmation email: %s", req.Code, err) } else { @@ -391,11 +382,9 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc go func() { msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app) if err != nil { - app.err.Printf("%s: Failed to construct user creation notification", req.Code) - app.debug.Printf("%s: Error: %s", req.Code, err) + app.err.Printf("%s: Failed to construct user creation notification: %s", req.Code, err) } else if err := app.email.send(msg, address); err != nil { - app.err.Printf("%s: Failed to send user creation notification", req.Code) - app.debug.Printf("%s: Error: %s", req.Code, err) + app.err.Printf("%s: Failed to send user creation notification: %s", req.Code, err) } else { app.info.Printf("%s: Sent user creation notification to %s", req.Code, address) } @@ -403,33 +392,28 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc } } } - var id string - if user["Id"] != nil { - id = user["Id"].(string) - } + id := user.ID if invite.Profile != "" { app.debug.Printf("Applying settings from profile \"%s\"", invite.Profile) profile, ok := app.storage.profiles[invite.Profile] if !ok { profile = app.storage.profiles["Default"] } - if len(profile.Policy) != 0 { + if profile.Policy.BlockedTags != nil { app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile) status, err = app.jf.SetPolicy(id, profile.Policy) if !((status == 200 || status == 204) && err == nil) { - app.err.Printf("%s: Failed to set user policy: Code %d", req.Code, status) - app.debug.Printf("%s: Error: %s", req.Code, err) + app.err.Printf("%s: Failed to set user policy (%d): %s", req.Code, status, err) } } - if len(profile.Configuration) != 0 && len(profile.Displayprefs) != 0 { + if profile.Configuration.GroupedFolders != nil && len(profile.Displayprefs) != 0 { app.debug.Printf("Applying homescreen from profile \"%s\"", invite.Profile) status, err = app.jf.SetConfiguration(id, profile.Configuration) if (status == 200 || status == 204) && err == nil { status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs) } if !((status == 200 || status == 204) && err == nil) { - app.err.Printf("%s: Failed to set configuration template: Code %d", req.Code, status) - app.debug.Printf("%s: Error: %s", req.Code, err) + app.err.Printf("%s: Failed to set configuration template (%d): %s", req.Code, status, err) } } } @@ -534,17 +518,15 @@ func (app *appContext) Announce(gc *gin.Context) { } msg, err := app.email.constructAnnouncement(req.Subject, req.Message, app) if err != nil { - app.err.Println("Failed to construct announcement email") - app.debug.Printf("Error: %s", err) + app.err.Printf("Failed to construct announcement emails: %s", err) respondBool(500, false, gc) return } else if err := app.email.send(msg, addresses...); err != nil { - app.err.Println("Failed to send announcement email") - app.debug.Printf("Error: %s", err) + app.err.Printf("Failed to send announcement emails: %s", err) respondBool(500, false, gc) return } - app.info.Println("Sent announcement email") + app.info.Printf("Sent announcement email to %d users", len(addresses)) respondBool(200, true, gc) } @@ -569,7 +551,7 @@ func (app *appContext) DeleteUser(gc *gin.Context) { if id, ok := ombiUser["id"]; ok { status, err := app.ombi.DeleteUser(id.(string)) if err != nil || status != 200 { - app.err.Printf("Failed to delete ombi user: %d %s", status, err) + app.err.Printf("Failed to delete ombi user (%d): %s", status, err) errors[userID] = fmt.Sprintf("Ombi: %d %s, ", status, err) } } @@ -590,11 +572,9 @@ func (app *appContext) DeleteUser(gc *gin.Context) { go func(userID, reason, address string) { msg, err := app.email.constructDeleted(reason, app) if err != nil { - app.err.Printf("%s: Failed to construct account deletion email", userID) - app.debug.Printf("%s: Error: %s", userID, err) + app.err.Printf("%s: Failed to construct account deletion email: %s", userID, err) } else if err := app.email.send(msg, address); err != nil { - app.err.Printf("%s: Failed to send to %s", userID, address) - app.debug.Printf("%s: Error: %s", userID, err) + app.err.Printf("%s: Failed to send to %s: %s", userID, address, err) } else { app.info.Printf("%s: Sent deletion email to %s", userID, address) } @@ -657,12 +637,10 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { msg, err := app.email.constructInvite(inviteCode, invite, app) if err != nil { invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) - app.err.Printf("%s: Failed to construct invite email", inviteCode) - app.debug.Printf("%s: Error: %s", inviteCode, err) + app.err.Printf("%s: Failed to construct invite email: %s", inviteCode, err) } else if err := app.email.send(msg, req.Email); err != nil { invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) - app.err.Printf("%s: %s", inviteCode, invite.Email) - app.debug.Printf("%s: Error: %s", inviteCode, err) + app.err.Printf("%s: %s: %s", inviteCode, invite.Email, err) } else { app.info.Printf("%s: Sent invite email to %s", inviteCode, req.Email) } @@ -770,22 +748,20 @@ func (app *appContext) CreateProfile(gc *gin.Context) { gc.BindJSON(&req) user, status, err := app.jf.UserByID(req.ID, false) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get user from Jellyfin: Code %d", status) - app.debug.Printf("Error: %s", err) + app.err.Printf("Failed to get user from Jellyfin (%d): %s", status, err) respond(500, "Couldn't get user", gc) return } profile := Profile{ - FromUser: user["Name"].(string), - Policy: user["Policy"].(map[string]interface{}), + FromUser: user.Name, + Policy: user.Policy, } - app.debug.Printf("Creating profile from user \"%s\"", user["Name"].(string)) + app.debug.Printf("Creating profile from user \"%s\"", user.Name) if req.Homescreen { - profile.Configuration = user["Configuration"].(map[string]interface{}) + profile.Configuration = user.Configuration profile.Displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get DisplayPrefs: Code %d", status) - app.debug.Printf("Error: %s", err) + app.err.Printf("Failed to get DisplayPrefs (%d): %s", status, err) respond(500, "Couldn't get displayprefs", gc) return } @@ -981,33 +957,6 @@ func (app *appContext) DeleteInvite(gc *gin.Context) { respond(400, "Code doesn't exist", gc) } -type dateToParse struct { - Parsed time.Time `json:"parseme"` -} - -func parseDT(date string) time.Time { - // decent method - dt, err := time.Parse("2006-01-02T15:04:05.000000", date) - if err == nil { - return dt - } - // emby method - dt, err = time.Parse("2006-01-02T15:04:05.0000000+00:00", date) - if err == nil { - return dt - } - // magic method - // some stored dates from jellyfin have no timezone at the end, if not we assume UTC - if date[len(date)-1] != 'Z' { - date += "Z" - } - timeJSON := []byte("{ \"parseme\": \"" + date + "\" }") - var parsed dateToParse - // Magically turn it into a time.Time - json.Unmarshal(timeJSON, &parsed) - return parsed.Parsed -} - // @Summary Get a list of Jellyfin users. // @Produce json // @Success 200 {object} getUsersDTO @@ -1021,22 +970,21 @@ func (app *appContext) GetUsers(gc *gin.Context) { resp.UserList = []respUser{} users, status, err := app.jf.GetUsers(false) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get users from Jellyfin: Code %d", status) - app.debug.Printf("Error: %s", err) + app.err.Printf("Failed to get users from Jellyfin (%d): %s", status, err) respond(500, "Couldn't get users", gc) return } for _, jfUser := range users { - var user respUser + user := respUser{ + ID: jfUser.ID, + Name: jfUser.Name, + Admin: jfUser.Policy.IsAdministrator, + } user.LastActive = "n/a" - if jfUser["LastActivityDate"] != nil { - date := parseDT(jfUser["LastActivityDate"].(string)) - user.LastActive = app.formatDatetime(date) - } - user.ID = jfUser["Id"].(string) - user.Name = jfUser["Name"].(string) - user.Admin = jfUser["Policy"].(map[string]interface{})["IsAdministrator"].(bool) - if email, ok := app.storage.emails[jfUser["Id"].(string)]; ok { + if !jfUser.LastActivityDate.IsZero() { + user.LastActive = app.formatDatetime(jfUser.LastActivityDate.Time) + } + if email, ok := app.storage.emails[jfUser.ID]; ok { user.Email = email.(string) } @@ -1056,8 +1004,7 @@ func (app *appContext) OmbiUsers(gc *gin.Context) { app.debug.Println("Ombi users requested") users, status, err := app.ombi.GetUsers() if err != nil || status != 200 { - app.err.Printf("Failed to get users from Ombi: Code %d", status) - app.debug.Printf("Error: %s", err) + app.err.Printf("Failed to get users from Ombi (%d): %s", status, err) respond(500, "Couldn't get users", gc) return } @@ -1107,16 +1054,15 @@ func (app *appContext) ModifyEmails(gc *gin.Context) { app.debug.Println("Email modification requested") users, status, err := app.jf.GetUsers(false) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get users from Jellyfin: Code %d", status) - app.debug.Printf("Error: %s", err) + app.err.Printf("Failed to get users from Jellyfin (%d): %s", status, err) respond(500, "Couldn't get users", gc) return } ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) for _, jfUser := range users { - id := jfUser["Id"].(string) + id := jfUser.ID if address, ok := req[id]; ok { - app.storage.emails[jfUser["Id"].(string)] = address + app.storage.emails[id] = address if ombiEnabled { ombiUser, code, err := app.getOmbiUser(id) if code == 200 && err == nil { @@ -1147,16 +1093,18 @@ func (app *appContext) ApplySettings(gc *gin.Context) { var req userSettingsDTO gc.BindJSON(&req) applyingFrom := "profile" - var policy, configuration, displayprefs map[string]interface{} + var policy mediabrowser.Policy + var configuration mediabrowser.Configuration + var displayprefs map[string]interface{} if req.From == "profile" { app.storage.loadProfiles() - if _, ok := app.storage.profiles[req.Profile]; !ok || len(app.storage.profiles[req.Profile].Policy) == 0 { + if _, ok := app.storage.profiles[req.Profile]; !ok || app.storage.profiles[req.Profile].Policy.BlockedTags == nil { app.err.Printf("Couldn't find profile \"%s\" or profile was empty", req.Profile) respond(500, "Couldn't find profile", gc) return } if req.Homescreen { - if len(app.storage.profiles[req.Profile].Configuration) == 0 || len(app.storage.profiles[req.Profile].Displayprefs) == 0 { + if app.storage.profiles[req.Profile].Configuration.GroupedFolders == nil || len(app.storage.profiles[req.Profile].Displayprefs) == 0 { app.err.Printf("No homescreen saved in profile \"%s\"", req.Profile) respond(500, "No homescreen template available", gc) return @@ -1169,22 +1117,20 @@ func (app *appContext) ApplySettings(gc *gin.Context) { applyingFrom = "user" user, status, err := app.jf.UserByID(req.ID, false) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get user from Jellyfin: Code %d", status) - app.debug.Printf("Error: %s", err) + app.err.Printf("Failed to get user from Jellyfin (%d): %s", status, err) respond(500, "Couldn't get user", gc) return } - applyingFrom = "\"" + user["Name"].(string) + "\"" - policy = user["Policy"].(map[string]interface{}) + applyingFrom = "\"" + user.Name + "\"" + policy = user.Policy if req.Homescreen { displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get DisplayPrefs: Code %d", status) - app.debug.Printf("Error: %s", err) + app.err.Printf("Failed to get DisplayPrefs (%d): %s", status, err) respond(500, "Couldn't get displayprefs", gc) return } - configuration = user["Configuration"].(map[string]interface{}) + configuration = user.Configuration } } app.info.Printf("Applying settings to %d user(s) from %s", len(req.ApplyTo), applyingFrom) diff --git a/auth.go b/auth.go index fc927c4..1e2ff29 100644 --- a/auth.go +++ b/auth.go @@ -135,10 +135,7 @@ func (app *appContext) getTokenLogin(gc *gin.Context) { return } if !match { - var status int - var err error - var user map[string]interface{} - user, status, err = app.authJf.Authenticate(creds[0], creds[1]) + user, status, err := app.authJf.Authenticate(creds[0], creds[1]) if status != 200 || err != nil { if status == 401 || status == 400 { app.info.Println("Auth denied: Invalid username/password (Jellyfin)") @@ -149,9 +146,10 @@ func (app *appContext) getTokenLogin(gc *gin.Context) { respond(500, "Jellyfin error", gc) return } - jfID = user["Id"].(string) + jfID = user.ID if app.config.Section("ui").Key("admin_only").MustBool(true) { - if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) { + fmt.Printf("%+v\n", user.Policy) + if !user.Policy.IsAdministrator { app.debug.Printf("Auth denied: Users \"%s\" isn't admin", creds[0]) respond(401, "Unauthorized", gc) return diff --git a/main.go b/main.go index 8801a51..d80d05d 100644 --- a/main.go +++ b/main.go @@ -336,7 +336,7 @@ func start(asDaemon, firstCall bool) { app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String() app.storage.loadProfiles() - if !(len(app.storage.policy) == 0 && len(app.storage.configuration) == 0 && len(app.storage.displayprefs) == 0) { + if !(app.storage.policy.BlockedTags == nil && app.storage.configuration.GroupedFolders == nil && len(app.storage.displayprefs) == 0) { app.info.Println("Migrating user template files to new profile format") app.storage.migrateToProfile() for _, path := range [3]string{app.storage.policy_path, app.storage.configuration_path, app.storage.displayprefs_path} { diff --git a/mediabrowser/emby.go b/mediabrowser/emby.go index 1098a89..c95d210 100644 --- a/mediabrowser/emby.go +++ b/mediabrowser/emby.go @@ -20,8 +20,8 @@ func embyDeleteUser(emby *MediaBrowser, userID string) (int, error) { return resp.StatusCode, err } -func embyGetUsers(emby *MediaBrowser, public bool) ([]map[string]interface{}, int, error) { - var result []map[string]interface{} +func embyGetUsers(emby *MediaBrowser, public bool) ([]User, int, error) { + var result []User var data string var status int var err error @@ -39,42 +39,40 @@ func embyGetUsers(emby *MediaBrowser, public bool) ([]map[string]interface{}, in json.Unmarshal([]byte(data), &result) emby.userCache = result emby.CacheExpiry = time.Now().Add(time.Minute * time.Duration(emby.cacheLength)) - if id, ok := result[0]["Id"]; ok { - if id.(string)[8] == '-' { - emby.Hyphens = true - } + if result[0].ID[8] == '-' { + emby.Hyphens = true } return result, status, nil } return emby.userCache, 200, nil } -func embyUserByName(emby *MediaBrowser, username string, public bool) (map[string]interface{}, int, error) { - var match map[string]interface{} - find := func() (map[string]interface{}, int, error) { +func embyUserByName(emby *MediaBrowser, username string, public bool) (User, int, error) { + var match User + find := func() (User, int, error) { users, status, err := emby.GetUsers(public) if err != nil || status != 200 { - return nil, status, err + return User{}, status, err } for _, user := range users { - if user["Name"].(string) == username { + if user.Name == username { return user, status, err } } - return nil, status, err + return User{}, status, err } match, status, err := find() - if match == nil { + if match.Name == "" { emby.CacheExpiry = time.Now() match, status, err = find() } return match, status, err } -func embyUserByID(emby *MediaBrowser, userID string, public bool) (map[string]interface{}, int, error) { +func embyUserByID(emby *MediaBrowser, userID string, public bool) (User, int, error) { if emby.CacheExpiry.After(time.Now()) { for _, user := range emby.userCache { - if user["Id"].(string) == userID { + if user.ID == userID { return user, 200, nil } } @@ -82,23 +80,23 @@ func embyUserByID(emby *MediaBrowser, userID string, public bool) (map[string]in if public { users, status, err := emby.GetUsers(public) if err != nil || status != 200 { - return nil, status, err + return User{}, status, err } for _, user := range users { - if user["Id"].(string) == userID { + if user.ID == userID { return user, status, nil } } - return nil, status, err + return User{}, status, err } - var result map[string]interface{} + var result User var data string var status int var err error url := fmt.Sprintf("%s/users/%s", emby.Server, userID) data, status, err = emby.get(url, emby.loginParams) if err != nil || status != 200 { - return nil, status, err + return User{}, status, err } json.Unmarshal([]byte(data), &result) return result, status, nil @@ -109,19 +107,19 @@ func embyUserByID(emby *MediaBrowser, userID string, public bool) (map[string]in // Immediately disable it // Set password // Reeenable it -func embyNewUser(emby *MediaBrowser, username, password string) (map[string]interface{}, int, error) { +func embyNewUser(emby *MediaBrowser, username, password string) (User, int, error) { url := fmt.Sprintf("%s/Users/New", emby.Server) data := map[string]interface{}{ "Name": username, } response, status, err := emby.post(url, data, true) - var recv map[string]interface{} + var recv User json.Unmarshal([]byte(response), &recv) if err != nil || !(status == 200 || status == 204) { - return nil, status, err + return User{}, status, err } // Step 2: Set password - id := recv["Id"].(string) + id := recv.ID url = fmt.Sprintf("%s/Users/%s/Password", emby.Server, id) data = map[string]interface{}{ "Id": id, @@ -136,7 +134,7 @@ func embyNewUser(emby *MediaBrowser, username, password string) (map[string]inte return recv, status, nil } -func embySetPolicy(emby *MediaBrowser, userID string, policy map[string]interface{}) (int, error) { +func embySetPolicy(emby *MediaBrowser, userID string, policy Policy) (int, error) { url := fmt.Sprintf("%s/Users/%s/Policy", emby.Server, userID) _, status, err := emby.post(url, policy, false) if err != nil || status != 200 { @@ -145,7 +143,7 @@ func embySetPolicy(emby *MediaBrowser, userID string, policy map[string]interfac return status, nil } -func embySetConfiguration(emby *MediaBrowser, userID string, configuration map[string]interface{}) (int, error) { +func embySetConfiguration(emby *MediaBrowser, userID string, configuration Configuration) (int, error) { url := fmt.Sprintf("%s/Users/%s/Configuration", emby.Server, userID) _, status, err := emby.post(url, configuration, false) return status, err diff --git a/mediabrowser/jfapi.go b/mediabrowser/jfapi.go index 21d20f6..2c9d0e0 100644 --- a/mediabrowser/jfapi.go +++ b/mediabrowser/jfapi.go @@ -18,8 +18,8 @@ func jfDeleteUser(jf *MediaBrowser, userID string) (int, error) { return resp.StatusCode, err } -func jfGetUsers(jf *MediaBrowser, public bool) ([]map[string]interface{}, int, error) { - var result []map[string]interface{} +func jfGetUsers(jf *MediaBrowser, public bool) ([]User, int, error) { + var result []User var data string var status int var err error @@ -34,45 +34,47 @@ func jfGetUsers(jf *MediaBrowser, public bool) ([]map[string]interface{}, int, e if err != nil || status != 200 { return nil, status, err } - json.Unmarshal([]byte(data), &result) + err := json.Unmarshal([]byte(data), &result) + if err != nil { + fmt.Println(err) + return nil, status, err + } jf.userCache = result jf.CacheExpiry = time.Now().Add(time.Minute * time.Duration(jf.cacheLength)) - if id, ok := result[0]["Id"]; ok { - if id.(string)[8] == '-' { - jf.Hyphens = true - } + if result[0].ID[8] == '-' { + jf.Hyphens = true } return result, status, nil } return jf.userCache, 200, nil } -func jfUserByName(jf *MediaBrowser, username string, public bool) (map[string]interface{}, int, error) { - var match map[string]interface{} - find := func() (map[string]interface{}, int, error) { +func jfUserByName(jf *MediaBrowser, username string, public bool) (User, int, error) { + var match User + find := func() (User, int, error) { users, status, err := jf.GetUsers(public) if err != nil || status != 200 { - return nil, status, err + return User{}, status, err } for _, user := range users { - if user["Name"].(string) == username { + if user.Name == username { return user, status, err } } - return nil, status, err + return User{}, status, err } match, status, err := find() - if match == nil { + if match.Name == "" { jf.CacheExpiry = time.Now() match, status, err = find() } return match, status, err } -func jfUserByID(jf *MediaBrowser, userID string, public bool) (map[string]interface{}, int, error) { +func jfUserByID(jf *MediaBrowser, userID string, public bool) (User, int, error) { if jf.CacheExpiry.After(time.Now()) { for _, user := range jf.userCache { - if user["Id"].(string) == userID { + if user.ID == userID { return user, 200, nil } } @@ -80,29 +82,29 @@ func jfUserByID(jf *MediaBrowser, userID string, public bool) (map[string]interf if public { users, status, err := jf.GetUsers(public) if err != nil || status != 200 { - return nil, status, err + return User{}, status, err } for _, user := range users { - if user["Id"].(string) == userID { + if user.ID == userID { return user, status, nil } } - return nil, status, err + return User{}, status, err } - var result map[string]interface{} + var result User var data string var status int var err error url := fmt.Sprintf("%s/users/%s", jf.Server, userID) data, status, err = jf.get(url, jf.loginParams) if err != nil || status != 200 { - return nil, status, err + return User{}, status, err } json.Unmarshal([]byte(data), &result) return result, status, nil } -func jfNewUser(jf *MediaBrowser, username, password string) (map[string]interface{}, int, error) { +func jfNewUser(jf *MediaBrowser, username, password string) (User, int, error) { url := fmt.Sprintf("%s/Users/New", jf.Server) stringData := map[string]string{ "Name": username, @@ -113,15 +115,15 @@ func jfNewUser(jf *MediaBrowser, username, password string) (map[string]interfac data[key] = value } response, status, err := jf.post(url, data, true) - var recv map[string]interface{} + var recv User json.Unmarshal([]byte(response), &recv) if err != nil || !(status == 200 || status == 204) { - return nil, status, err + return User{}, status, err } return recv, status, nil } -func jfSetPolicy(jf *MediaBrowser, userID string, policy map[string]interface{}) (int, error) { +func jfSetPolicy(jf *MediaBrowser, userID string, policy Policy) (int, error) { url := fmt.Sprintf("%s/Users/%s/Policy", jf.Server, userID) _, status, err := jf.post(url, policy, false) if err != nil || status != 200 { @@ -130,7 +132,7 @@ func jfSetPolicy(jf *MediaBrowser, userID string, policy map[string]interface{}) return status, nil } -func jfSetConfiguration(jf *MediaBrowser, userID string, configuration map[string]interface{}) (int, error) { +func jfSetConfiguration(jf *MediaBrowser, userID string, configuration Configuration) (int, error) { url := fmt.Sprintf("%s/Users/%s/Configuration", jf.Server, userID) _, status, err := jf.post(url, configuration, false) return status, err diff --git a/mediabrowser/mediabrowser.go b/mediabrowser/mediabrowser.go index f3a14a9..08a3b4c 100644 --- a/mediabrowser/mediabrowser.go +++ b/mediabrowser/mediabrowser.go @@ -14,10 +14,12 @@ import ( "github.com/hrfee/jfa-go/common" ) -type serverType bool +type serverType int -var JellyfinServer serverType = false -var EmbyServer serverType = true +const ( + JellyfinServer serverType = iota + EmbyServer +) type serverInfo struct { LocalAddress string `json:"LocalAddress"` @@ -45,7 +47,7 @@ type MediaBrowser struct { userID string httpClient *http.Client loginParams map[string]string - userCache []map[string]interface{} + userCache []User CacheExpiry time.Time cacheLength int noFail bool @@ -131,7 +133,7 @@ func (mb *MediaBrowser) get(url string, params map[string]string) (string, int, return buf.String(), resp.StatusCode, nil } -func (mb *MediaBrowser) post(url string, data map[string]interface{}, response bool) (string, int, error) { +func (mb *MediaBrowser) post(url string, data interface{}, response bool) (string, int, error) { params, _ := json.Marshal(data) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(params)) for name, value := range mb.header { @@ -167,7 +169,7 @@ func (mb *MediaBrowser) post(url string, data map[string]interface{}, response b } // Authenticate attempts to authenticate using a username & password -func (mb *MediaBrowser) Authenticate(username, password string) (map[string]interface{}, int, error) { +func (mb *MediaBrowser) Authenticate(username, password string) (User, int, error) { mb.Username = username mb.password = password mb.loginParams = map[string]string{ @@ -180,35 +182,44 @@ func (mb *MediaBrowser) Authenticate(username, password string) (map[string]inte encoder.SetEscapeHTML(false) err := encoder.Encode(mb.loginParams) if err != nil { - return nil, 0, err + return User{}, 0, err } // loginParams, _ := json.Marshal(jf.loginParams) url := fmt.Sprintf("%s/Users/authenticatebyname", mb.Server) req, err := http.NewRequest("POST", url, buffer) defer mb.timeoutHandler() if err != nil { - return nil, 0, err + return User{}, 0, err } for name, value := range mb.header { req.Header.Add(name, value) } resp, err := mb.httpClient.Do(req) if err != nil || resp.StatusCode != 200 { - return nil, resp.StatusCode, err + return User{}, resp.StatusCode, err } defer resp.Body.Close() - var data io.Reader + var d io.Reader switch resp.Header.Get("Content-Encoding") { case "gzip": - data, _ = gzip.NewReader(resp.Body) + d, _ = gzip.NewReader(resp.Body) default: - data = resp.Body + d = resp.Body + } + data, err := io.ReadAll(d) + if err != nil { + return User{}, 0, err } var respData map[string]interface{} - json.NewDecoder(data).Decode(&respData) + json.Unmarshal(data, &respData) mb.AccessToken = respData["AccessToken"].(string) - user := respData["User"].(map[string]interface{}) - mb.userID = respData["User"].(map[string]interface{})["Id"].(string) + var user User + ju, err := json.Marshal(respData["User"]) + if err != nil { + return User{}, 0, err + } + json.Unmarshal(ju, &user) + mb.userID = user.ID mb.auth = fmt.Sprintf("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\", Token=\"%s\"", mb.client, mb.device, mb.deviceID, mb.version, mb.AccessToken) mb.header["X-Emby-Authorization"] = mb.auth mb.Authenticated = true @@ -224,7 +235,7 @@ func (mb *MediaBrowser) DeleteUser(userID string) (int, error) { } // GetUsers returns all (visible) users on the Emby instance. -func (mb *MediaBrowser) GetUsers(public bool) ([]map[string]interface{}, int, error) { +func (mb *MediaBrowser) GetUsers(public bool) ([]User, int, error) { if mb.serverType == JellyfinServer { return jfGetUsers(mb, public) } @@ -232,7 +243,7 @@ func (mb *MediaBrowser) GetUsers(public bool) ([]map[string]interface{}, int, er } // UserByName returns the user corresponding to the provided username. -func (mb *MediaBrowser) UserByName(username string, public bool) (map[string]interface{}, int, error) { +func (mb *MediaBrowser) UserByName(username string, public bool) (User, int, error) { if mb.serverType == JellyfinServer { return jfUserByName(mb, username, public) } @@ -240,7 +251,7 @@ func (mb *MediaBrowser) UserByName(username string, public bool) (map[string]int } // UserByID returns the user corresponding to the provided ID. -func (mb *MediaBrowser) UserByID(userID string, public bool) (map[string]interface{}, int, error) { +func (mb *MediaBrowser) UserByID(userID string, public bool) (User, int, error) { if mb.serverType == JellyfinServer { return jfUserByID(mb, userID, public) } @@ -248,7 +259,7 @@ func (mb *MediaBrowser) UserByID(userID string, public bool) (map[string]interfa } // NewUser creates a new user with the provided username and password. -func (mb *MediaBrowser) NewUser(username, password string) (map[string]interface{}, int, error) { +func (mb *MediaBrowser) NewUser(username, password string) (User, int, error) { if mb.serverType == JellyfinServer { return jfNewUser(mb, username, password) } @@ -256,7 +267,7 @@ func (mb *MediaBrowser) NewUser(username, password string) (map[string]interface } // SetPolicy sets the access policy for the user corresponding to the provided ID. -func (mb *MediaBrowser) SetPolicy(userID string, policy map[string]interface{}) (int, error) { +func (mb *MediaBrowser) SetPolicy(userID string, policy Policy) (int, error) { if mb.serverType == JellyfinServer { return jfSetPolicy(mb, userID, policy) } @@ -264,7 +275,7 @@ func (mb *MediaBrowser) SetPolicy(userID string, policy map[string]interface{}) } // SetConfiguration sets the configuration (part of homescreen layout) for the user corresponding to the provided ID. -func (mb *MediaBrowser) SetConfiguration(userID string, configuration map[string]interface{}) (int, error) { +func (mb *MediaBrowser) SetConfiguration(userID string, configuration Configuration) (int, error) { if mb.serverType == JellyfinServer { return jfSetConfiguration(mb, userID, configuration) } diff --git a/mediabrowser/models.go b/mediabrowser/models.go new file mode 100644 index 0000000..9ff5365 --- /dev/null +++ b/mediabrowser/models.go @@ -0,0 +1,135 @@ +package mediabrowser + +import ( + "encoding/json" + "fmt" + "strings" + "time" +) + +type magicParse struct { + Parsed time.Time `json:"parseme"` +} + +type Time struct { + time.Time +} + +func (t *Time) UnmarshalJSON(b []byte) (err error) { + str := strings.TrimSuffix(strings.TrimPrefix(string(b), "\""), "\"") + // Trim nanoseconds to always have 6 digits, so overall length is always the same. + if str[len(str)-1] == 'Z' { + str = str[:26] + "Z" + } else { + str = str[:26] + } + // decent method + t.Time, err = time.Parse("2006-01-02T15:04:05.000000Z", str) + if err == nil { + return + } + t.Time, err = time.Parse("2006-01-02T15:04:05.000000", str) + if err == nil { + return + } + // emby method + t.Time, err = time.Parse("2006-01-02T15:04:05.0000000+00:00", str) + if err == nil { + return + } + fmt.Println("THIRDERR", err) + // magic method + // some stored dates from jellyfin have no timezone at the end, if not we assume UTC + if str[len(str)-1] != 'Z' { + str += "Z" + } + timeJSON := []byte("{ \"parseme\": \"" + str + "\" }") + var parsed magicParse + // Magically turn it into a time.Time + err = json.Unmarshal(timeJSON, &parsed) + t.Time = parsed.Parsed + return +} + +type User struct { + Name string `json:"Name"` + ServerID string `json:"ServerId"` + ID string `json:"Id"` + HasPassword bool `json:"HasPassword"` + HasConfiguredPassword bool `json:"HasConfiguredPassword"` + HasConfiguredEasyPassword bool `json:"HasConfiguredEasyPassword"` + EnableAutoLogin bool `json:"EnableAutoLogin"` + LastLoginDate Time `json:"LastLoginDate"` + LastActivityDate Time `json:"LastActivityDate"` + Configuration Configuration `json:"Configuration"` + Policy Policy `json:"Policy"` +} + +type SessionInfo struct { + RemoteEndpoint string `json:"RemoteEndPoint"` + UserID string `json:"UserId"` +} + +type AuthenticationResult struct { + User User `json:"User"` + AccessToken string `json:"AccessToken"` + ServerID string `json:"ServerId"` + SessionInfo SessionInfo `json:"SessionInfo"` +} + +type Configuration struct { + PlayDefaultAudioTrack bool `json:"PlayDefaultAudioTrack"` + SubtitleLanguagePreference string `json:"SubtitleLanguagePreference"` + DisplayMissingEpisodes bool `json:"DisplayMissingEpisodes"` + GroupedFolders []interface{} `json:"GroupedFolders"` + SubtitleMode string `json:"SubtitleMode"` + DisplayCollectionsView bool `json:"DisplayCollectionsView"` + EnableLocalPassword bool `json:"EnableLocalPassword"` + OrderedViews []interface{} `json:"OrderedViews"` + LatestItemsExcludes []interface{} `json:"LatestItemsExcludes"` + MyMediaExcludes []interface{} `json:"MyMediaExcludes"` + HidePlayedInLatest bool `json:"HidePlayedInLatest"` + RememberAudioSelections bool `json:"RememberAudioSelections"` + RememberSubtitleSelections bool `json:"RememberSubtitleSelections"` + EnableNextEpisodeAutoPlay bool `json:"EnableNextEpisodeAutoPlay"` +} +type Policy struct { + IsAdministrator bool `json:"IsAdministrator"` + IsHidden bool `json:"IsHidden"` + IsDisabled bool `json:"IsDisabled"` + BlockedTags []interface{} `json:"BlockedTags"` + EnableUserPreferenceAccess bool `json:"EnableUserPreferenceAccess"` + AccessSchedules []interface{} `json:"AccessSchedules"` + BlockUnratedItems []interface{} `json:"BlockUnratedItems"` + EnableRemoteControlOfOtherUsers bool `json:"EnableRemoteControlOfOtherUsers"` + EnableSharedDeviceControl bool `json:"EnableSharedDeviceControl"` + EnableRemoteAccess bool `json:"EnableRemoteAccess"` + EnableLiveTvManagement bool `json:"EnableLiveTvManagement"` + EnableLiveTvAccess bool `json:"EnableLiveTvAccess"` + EnableMediaPlayback bool `json:"EnableMediaPlayback"` + EnableAudioPlaybackTranscoding bool `json:"EnableAudioPlaybackTranscoding"` + EnableVideoPlaybackTranscoding bool `json:"EnableVideoPlaybackTranscoding"` + EnablePlaybackRemuxing bool `json:"EnablePlaybackRemuxing"` + ForceRemoteSourceTranscoding bool `json:"ForceRemoteSourceTranscoding"` + EnableContentDeletion bool `json:"EnableContentDeletion"` + EnableContentDeletionFromFolders []interface{} `json:"EnableContentDeletionFromFolders"` + EnableContentDownloading bool `json:"EnableContentDownloading"` + EnableSyncTranscoding bool `json:"EnableSyncTranscoding"` + EnableMediaConversion bool `json:"EnableMediaConversion"` + EnabledDevices []interface{} `json:"EnabledDevices"` + EnableAllDevices bool `json:"EnableAllDevices"` + EnabledChannels []interface{} `json:"EnabledChannels"` + EnableAllChannels bool `json:"EnableAllChannels"` + EnabledFolders []string `json:"EnabledFolders"` + EnableAllFolders bool `json:"EnableAllFolders"` + InvalidLoginAttemptCount int `json:"InvalidLoginAttemptCount"` + LoginAttemptsBeforeLockout int `json:"LoginAttemptsBeforeLockout"` + MaxActiveSessions int `json:"MaxActiveSessions"` + EnablePublicSharing bool `json:"EnablePublicSharing"` + BlockedMediaFolders []interface{} `json:"BlockedMediaFolders"` + BlockedChannels []interface{} `json:"BlockedChannels"` + RemoteClientBitrateLimit int `json:"RemoteClientBitrateLimit"` + AuthenticationProviderID string `json:"AuthenticationProviderId"` + PasswordResetProviderID string `json:"PasswordResetProviderId"` + SyncPlayAccess string `json:"SyncPlayAccess"` +} diff --git a/pwreset.go b/pwreset.go index b623c97..61a0fac 100644 --- a/pwreset.go +++ b/pwreset.go @@ -70,13 +70,12 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) { } app.storage.loadEmails() var address string - uid := user["Id"] - if uid == nil { + uid := user.ID + if uid == "" { app.err.Printf("Couldn't get user ID for user \"%s\"", pwr.Username) - app.debug.Printf("user maplength: %d", len(user)) return } - addr, ok := app.storage.emails[user["Id"].(string)] + addr, ok := app.storage.emails[uid] if !ok || addr == nil { app.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username) return diff --git a/storage.go b/storage.go index 5100a81..a40786c 100644 --- a/storage.go +++ b/storage.go @@ -9,6 +9,8 @@ import ( "strconv" "strings" "time" + + "github.com/hrfee/jfa-go/mediabrowser" ) type Storage struct { @@ -17,20 +19,22 @@ type Storage struct { invites Invites profiles map[string]Profile defaultProfile string - emails, policy, configuration, displayprefs, ombi_template map[string]interface{} + emails, displayprefs, ombi_template map[string]interface{} + policy mediabrowser.Policy + configuration mediabrowser.Configuration lang Lang } // timePattern: %Y-%m-%dT%H:%M:%S.%f type Profile struct { - Admin bool `json:"admin,omitempty"` - LibraryAccess string `json:"libraries,omitempty"` - FromUser string `json:"fromUser,omitempty"` - Policy map[string]interface{} `json:"policy,omitempty"` - Configuration map[string]interface{} `json:"configuration,omitempty"` - Displayprefs map[string]interface{} `json:"displayprefs,omitempty"` - Default bool `json:"default,omitempty"` + Admin bool `json:"admin,omitempty"` + LibraryAccess string `json:"libraries,omitempty"` + FromUser string `json:"fromUser,omitempty"` + Policy mediabrowser.Policy `json:"policy,omitempty"` + Configuration mediabrowser.Configuration `json:"configuration,omitempty"` + Displayprefs map[string]interface{} `json:"displayprefs,omitempty"` + Default bool `json:"default,omitempty"` } type Invite struct { @@ -429,12 +433,12 @@ func (st *Storage) loadProfiles() error { st.defaultProfile = name } change := false - if profile.Policy["IsAdministrator"] != nil { - profile.Admin = profile.Policy["IsAdministrator"].(bool) + if profile.Policy.IsAdministrator != profile.Admin { change = true } - if profile.Policy["EnabledFolders"] != nil { - length := len(profile.Policy["EnabledFolders"].([]interface{})) + profile.Admin = profile.Policy.IsAdministrator + if profile.Policy.EnabledFolders != nil { + length := len(profile.Policy.EnabledFolders) if length == 0 { profile.LibraryAccess = "All" } else { @@ -517,7 +521,7 @@ func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[ } newEmails := map[string]interface{}{} for _, user := range jfUsers { - unHyphenated := user["Id"].(string) + unHyphenated := user.ID hyphenated := hyphenate(unHyphenated) email, ok := old[hyphenated] if ok { @@ -534,7 +538,7 @@ func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[st } newEmails := map[string]interface{}{} for _, user := range jfUsers { - unstripped := user["Id"].(string) + unstripped := user.ID stripped := strings.ReplaceAll(unstripped, "-", "") email, ok := old[stripped] if ok {