diff --git a/api.go b/api.go index 9c8d27e..afe4e0b 100644 --- a/api.go +++ b/api.go @@ -887,10 +887,8 @@ func (app *appContext) GetUsers(gc *gin.Context) { var user respUser user.LastActive = "n/a" if jfUser["LastActivityDate"] != nil { - fmt.Println(jfUser["LastActivityDate"].(string)) date := parseDT(jfUser["LastActivityDate"].(string)) user.LastActive = app.formatDatetime(date) - // fmt.Printf("%s: %s, %s, %+v\n", jfUser["Name"].(string), jfUser["LastActivityDate"].(string), user.LastActive, date) } user.ID = jfUser["Id"].(string) user.Name = jfUser["Name"].(string) @@ -1124,6 +1122,18 @@ func (app *appContext) GetConfig(gc *gin.Context) { s.Options = app.lang.langOptions s.Value = app.lang.langOptions[app.lang.chosenIndex] resp.Sections["ui"].Settings["language"] = s + + t := resp.Sections["jellyfin"].Settings["type"] + opts := make([]string, len(serverTypes)) + i := 0 + for _, v := range serverTypes { + opts[i] = v + i++ + } + t.Options = opts + t.Value = serverTypes[app.config.Section("jellyfin").Key("type").MustString("jellyfin")] + resp.Sections["jellyfin"].Settings["type"] = t + gc.JSON(200, resp) } @@ -1153,6 +1163,13 @@ func (app *appContext) ModifyConfig(gc *gin.Context) { break } } + } else if section == "jellyfin" && setting == "type" { + for k, v := range serverTypes { + if v == value.(string) { + tempConfig.Section("jellyfin").Key("type").SetValue(k) + break + } + } } else { tempConfig.Section(section).Key(setting).SetValue(value.(string)) } diff --git a/config/config-base.json b/config/config-base.json index 9d5c920..ed6953f 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -54,6 +54,18 @@ "type": "number", "value": 30, "description": "Timeout of user cache in minutes. Set to 0 to disable." + }, + "type": { + "name": "Server type", + "required": false, + "requires_restart": true, + "type": "select", + "options": [ + "jellyfin", + "emby" + ], + "value": "jellyfin", + "description": "Media server type." } } }, diff --git a/go.sum b/go.sum index c20e0ce..f7141d4 100644 --- a/go.sum +++ b/go.sum @@ -139,6 +139,8 @@ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hrfee/jfa-go/jfapi v0.0.0-20210109010027-4aae65518089 h1:WRk+JAywI8V4u+PBQpdvXBX73yCZxgnLwyIiX7xL+Xc= +github.com/hrfee/jfa-go/jfapi v0.0.0-20210109010027-4aae65518089/go.mod h1:Al1Rd1JGtpS+3KnK8t7+J0CZVDbT86QJrXHR6kZijds= github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e h1:OGunVjqY7y4U4laftpEHv+mvZBlr7UGimJXKEGQtg48= github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY= github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible h1:CL0ooBNfbNyJTJATno+m0h+zM5bW6v7fKlboKUGP/dI= diff --git a/main.go b/main.go index 4851c3b..085730f 100644 --- a/main.go +++ b/main.go @@ -26,8 +26,7 @@ import ( "github.com/gin-gonic/gin" "github.com/hrfee/jfa-go/common" _ "github.com/hrfee/jfa-go/docs" - "github.com/hrfee/jfa-go/emby" - "github.com/hrfee/jfa-go/jfapi" + "github.com/hrfee/jfa-go/mediabrowser" "github.com/hrfee/jfa-go/ombi" "github.com/lithammer/shortuuid/v3" "github.com/logrusorgru/aurora/v3" @@ -36,6 +35,11 @@ import ( "gopkg.in/ini.v1" ) +var serverTypes = map[string]string{ + "jellyfin": "Jellyfin", + "emby": "Emby (experimental)", +} + // User is used for auth purposes. type User struct { UserID string `json:"id"` @@ -57,8 +61,8 @@ type appContext struct { users []User invalidTokens []string // Keeping jf name because I can't think of a better one - jf common.MediaBrowserStruct - authJf common.MediaBrowserStruct + jf *mediabrowser.MediaBrowser + authJf *mediabrowser.MediaBrowser ombi *ombi.Ombi datePattern string timePattern string @@ -441,30 +445,28 @@ func start(asDaemon, firstCall bool) { server := app.config.Section("jellyfin").Key("server").String() cacheTimeout := int(app.config.Section("jellyfin").Key("cache_timeout").MustUint(30)) - mediaBrowser := app.config.Section("jellyfin").Key("type").String() - if mediaBrowser == "emby" { + stringServerType := app.config.Section("jellyfin").Key("type").String() + serverType := mediabrowser.JellyfinServer + timeoutHandler := common.NewTimeoutHandler("Jellyfin", server, true) + if stringServerType == "emby" { + serverType = mediabrowser.EmbyServer + timeoutHandler = common.NewTimeoutHandler("Emby", server, true) app.info.Println("Using Emby server type") - app.jf, _ = emby.NewEmby( - server, - app.config.Section("jellyfin").Key("client").String(), - app.config.Section("jellyfin").Key("version").String(), - app.config.Section("jellyfin").Key("device").String(), - app.config.Section("jellyfin").Key("device_id").String(), - common.NewTimeoutHandler("Emby", server, true), - cacheTimeout, - ) + fmt.Println(aurora.Yellow("WARNING: Emby compatibility is experimental, things may not work.")) } else { app.info.Println("Using Jellyfin server type") - app.jf, _ = jfapi.NewJellyfin( - server, - app.config.Section("jellyfin").Key("client").String(), - app.config.Section("jellyfin").Key("version").String(), - app.config.Section("jellyfin").Key("device").String(), - app.config.Section("jellyfin").Key("device_id").String(), - common.NewTimeoutHandler("Jellyfin", server, true), - cacheTimeout, - ) } + + app.jf, _ = mediabrowser.NewServer( + serverType, + server, + app.config.Section("jellyfin").Key("client").String(), + app.config.Section("jellyfin").Key("version").String(), + app.config.Section("jellyfin").Key("device").String(), + app.config.Section("jellyfin").Key("device_id").String(), + timeoutHandler, + cacheTimeout, + ) var status int _, status, err = app.jf.Authenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String()) if status != 200 || err != nil { @@ -483,7 +485,7 @@ func start(asDaemon, firstCall bool) { } return n } - if checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") { + if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") { // Get users to check if server uses hyphenated userIDs app.jf.GetUsers(false) @@ -524,7 +526,7 @@ func start(asDaemon, firstCall bool) { } } } - app.authJf, _ = jfapi.NewJellyfin(server, "jfa-go", app.version, "auth", "auth", common.NewTimeoutHandler("Jellyfin", server, true), cacheTimeout) + app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout) app.loadStrftime() diff --git a/mediabrowser/emby.go b/mediabrowser/emby.go index af36f53..1098a89 100644 --- a/mediabrowser/emby.go +++ b/mediabrowser/emby.go @@ -3,181 +3,13 @@ package mediabrowser // Almost identical to jfapi, with the most notable change being the password workaround. import ( - "bytes" - "compress/gzip" "encoding/json" "fmt" - "io" - "io/ioutil" "net/http" - "strings" "time" - - "github.com/hrfee/jfa-go/common" ) -// NewEmby returns a new Emby object. -func NewEmby(server, client, version, device, deviceID string, timeoutHandler common.TimeoutHandler, cacheTimeout int) (*MediaBrowserStruct, error) { - emby := &Emby{} - emby.Server = server - emby.client = client - emby.version = version - emby.device = device - emby.deviceID = deviceID - emby.useragent = fmt.Sprintf("%s/%s", client, version) - emby.timeoutHandler = timeoutHandler - emby.auth = fmt.Sprintf("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\"", client, device, deviceID, version) - emby.header = map[string]string{ - "Accept": "application/json", - "Content-type": "application/json; charset=UTF-8", - "X-Application": emby.useragent, - "Accept-Charset": "UTF-8,*", - "Accept-Encoding": "gzip", - "User-Agent": emby.useragent, - "X-Emby-Authorization": emby.auth, - } - emby.httpClient = &http.Client{ - Timeout: 10 * time.Second, - } - infoURL := fmt.Sprintf("%s/System/Info/Public", server) - req, _ := http.NewRequest("GET", infoURL, nil) - resp, err := emby.httpClient.Do(req) - defer emby.timeoutHandler() - if err == nil { - data, _ := ioutil.ReadAll(resp.Body) - json.Unmarshal(data, &emby.ServerInfo) - } - emby.cacheLength = cacheTimeout - emby.CacheExpiry = time.Now() - return emby, nil -} - -// Authenticate attempts to authenticate using a username & password -func (emby *MediaBrowserStruct) Authenticate(username, password string) (map[string]interface{}, int, error) { - emby.Username = username - emby.password = password - emby.loginParams = map[string]string{ - "Username": username, - "Pw": password, - "Password": password, - } - buffer := &bytes.Buffer{} - encoder := json.NewEncoder(buffer) - encoder.SetEscapeHTML(false) - err := encoder.Encode(emby.loginParams) - if err != nil { - return nil, 0, err - } - // loginParams, _ := json.Marshal(jf.loginParams) - url := fmt.Sprintf("%s/Users/authenticatebyname", emby.Server) - req, err := http.NewRequest("POST", url, buffer) - defer emby.timeoutHandler() - if err != nil { - return nil, 0, err - } - for name, value := range emby.header { - req.Header.Add(name, value) - } - resp, err := emby.httpClient.Do(req) - if err != nil || resp.StatusCode != 200 { - return nil, resp.StatusCode, err - } - defer resp.Body.Close() - var data io.Reader - switch resp.Header.Get("Content-Encoding") { - case "gzip": - data, _ = gzip.NewReader(resp.Body) - default: - data = resp.Body - } - var respData map[string]interface{} - json.NewDecoder(data).Decode(&respData) - emby.AccessToken = respData["AccessToken"].(string) - user := respData["User"].(map[string]interface{}) - emby.userID = respData["User"].(map[string]interface{})["Id"].(string) - emby.auth = fmt.Sprintf("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\", Token=\"%s\"", emby.client, emby.device, emby.deviceID, emby.version, emby.AccessToken) - emby.header["X-Emby-Authorization"] = emby.auth - emby.Authenticated = true - return user, resp.StatusCode, nil -} - -func (emby *MediaBrowserStruct) get(url string, params map[string]string) (string, int, error) { - var req *http.Request - if params != nil { - jsonParams, _ := json.Marshal(params) - req, _ = http.NewRequest("GET", url, bytes.NewBuffer(jsonParams)) - } else { - req, _ = http.NewRequest("GET", url, nil) - } - for name, value := range emby.header { - req.Header.Add(name, value) - } - resp, err := emby.httpClient.Do(req) - defer emby.timeoutHandler() - if err != nil || resp.StatusCode != 200 { - if resp.StatusCode == 401 && emby.Authenticated { - emby.Authenticated = false - _, _, authErr := emby.Authenticate(emby.Username, emby.password) - if authErr == nil { - v1, v2, v3 := emby.get(url, params) - return v1, v2, v3 - } - } - return "", resp.StatusCode, err - } - defer resp.Body.Close() - var data io.Reader - encoding := resp.Header.Get("Content-Encoding") - switch encoding { - case "gzip": - data, _ = gzip.NewReader(resp.Body) - default: - data = resp.Body - } - buf := new(strings.Builder) - io.Copy(buf, data) - //var respData map[string]interface{} - //json.NewDecoder(data).Decode(&respData) - return buf.String(), resp.StatusCode, nil -} - -func (emby *MediaBrowserStruct) post(url string, data map[string]interface{}, response bool) (string, int, error) { - params, _ := json.Marshal(data) - req, _ := http.NewRequest("POST", url, bytes.NewBuffer(params)) - for name, value := range emby.header { - req.Header.Add(name, value) - } - resp, err := emby.httpClient.Do(req) - defer emby.timeoutHandler() - if err != nil || resp.StatusCode != 200 { - if resp.StatusCode == 401 && emby.Authenticated { - emby.Authenticated = false - _, _, authErr := emby.Authenticate(emby.Username, emby.password) - if authErr == nil { - v1, v2, v3 := emby.post(url, data, response) - return v1, v2, v3 - } - } - return "", resp.StatusCode, err - } - if response { - defer resp.Body.Close() - var outData io.Reader - switch resp.Header.Get("Content-Encoding") { - case "gzip": - outData, _ = gzip.NewReader(resp.Body) - default: - outData = resp.Body - } - buf := new(strings.Builder) - io.Copy(buf, outData) - return buf.String(), resp.StatusCode, nil - } - return "", resp.StatusCode, nil -} - -// DeleteUser deletes the user corresponding to the provided ID. -func (emby *MediaBrowserStruct) DeleteUser(userID string) (int, error) { +func embyDeleteUser(emby *MediaBrowser, userID string) (int, error) { url := fmt.Sprintf("%s/Users/%s", emby.Server, userID) req, _ := http.NewRequest("DELETE", url, nil) for name, value := range emby.header { @@ -188,8 +20,7 @@ func (emby *MediaBrowserStruct) DeleteUser(userID string) (int, error) { return resp.StatusCode, err } -// GetUsers returns all (visible) users on the Emby instance. -func (emby *MediaBrowserStruct) GetUsers(public bool) ([]map[string]interface{}, int, error) { +func embyGetUsers(emby *MediaBrowser, public bool) ([]map[string]interface{}, int, error) { var result []map[string]interface{} var data string var status int @@ -218,8 +49,7 @@ func (emby *MediaBrowserStruct) GetUsers(public bool) ([]map[string]interface{}, return emby.userCache, 200, nil } -// UserByName returns the user corresponding to the provided username. -func (emby *MediaBrowserStruct) UserByName(username string, public bool) (map[string]interface{}, int, error) { +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) { users, status, err := emby.GetUsers(public) @@ -241,8 +71,7 @@ func (emby *MediaBrowserStruct) UserByName(username string, public bool) (map[st return match, status, err } -// UserByID returns the user corresponding to the provided ID. -func (emby *MediaBrowserStruct) UserByID(userID string, public bool) (map[string]interface{}, int, error) { +func embyUserByID(emby *MediaBrowser, userID string, public bool) (map[string]interface{}, int, error) { if emby.CacheExpiry.After(time.Now()) { for _, user := range emby.userCache { if user["Id"].(string) == userID { @@ -275,13 +104,12 @@ func (emby *MediaBrowserStruct) UserByID(userID string, public bool) (map[string return result, status, nil } -// NewUser creates a new user with the provided username and password. // Since emby doesn't allow one to specify a password on user creation, we: // Create the account // Immediately disable it // Set password // Reeenable it -func (emby *MediaBrowserStruct) NewUser(username, password string) (map[string]interface{}, int, error) { +func embyNewUser(emby *MediaBrowser, username, password string) (map[string]interface{}, int, error) { url := fmt.Sprintf("%s/Users/New", emby.Server) data := map[string]interface{}{ "Name": username, @@ -294,7 +122,7 @@ func (emby *MediaBrowserStruct) NewUser(username, password string) (map[string]i } // Step 2: Set password id := recv["Id"].(string) - url = fmt.Sprintf("/Users/%s/Password", id) + url = fmt.Sprintf("%s/Users/%s/Password", emby.Server, id) data = map[string]interface{}{ "Id": id, "CurrentPw": "", @@ -303,13 +131,12 @@ func (emby *MediaBrowserStruct) NewUser(username, password string) (map[string]i _, status, err = emby.post(url, data, false) // Step 3: If setting password errored, try to delete the account if err != nil || !(status == 200 || status == 204) { - status, err = emby.DeleteUser(id) + _, err = emby.DeleteUser(id) } return recv, status, nil } -// SetPolicy sets the access policy for the user corresponding to the provided ID. -func (emby *MediaBrowserStruct) SetPolicy(userID string, policy map[string]interface{}) (int, error) { +func embySetPolicy(emby *MediaBrowser, userID string, policy map[string]interface{}) (int, error) { url := fmt.Sprintf("%s/Users/%s/Policy", emby.Server, userID) _, status, err := emby.post(url, policy, false) if err != nil || status != 200 { @@ -318,15 +145,13 @@ func (emby *MediaBrowserStruct) SetPolicy(userID string, policy map[string]inter return status, nil } -// SetConfiguration sets the configuration (part of homescreen layout) for the user corresponding to the provided ID. -func (emby *MediaBrowserStruct) SetConfiguration(userID string, configuration map[string]interface{}) (int, error) { +func embySetConfiguration(emby *MediaBrowser, userID string, configuration map[string]interface{}) (int, error) { url := fmt.Sprintf("%s/Users/%s/Configuration", emby.Server, userID) _, status, err := emby.post(url, configuration, false) return status, err } -// GetDisplayPreferences gets the displayPreferences (part of homescreen layout) for the user corresponding to the provided ID. -func (emby *MediaBrowserStruct) GetDisplayPreferences(userID string) (map[string]interface{}, int, error) { +func embyGetDisplayPreferences(emby *MediaBrowser, userID string) (map[string]interface{}, int, error) { url := fmt.Sprintf("%s/DisplayPreferences/usersettings?userId=%s&client=emby", emby.Server, userID) data, status, err := emby.get(url, nil) if err != nil || !(status == 204 || status == 200) { @@ -340,8 +165,7 @@ func (emby *MediaBrowserStruct) GetDisplayPreferences(userID string) (map[string return displayprefs, status, nil } -// SetDisplayPreferences sets the displayPreferences (part of homescreen layout) for the user corresponding to the provided ID. -func (emby *MediaBrowserStruct) SetDisplayPreferences(userID string, displayprefs map[string]interface{}) (int, error) { +func embySetDisplayPreferences(emby *MediaBrowser, userID string, displayprefs map[string]interface{}) (int, error) { url := fmt.Sprintf("%s/DisplayPreferences/usersettings?userId=%s&client=emby", emby.Server, userID) _, status, err := emby.post(url, displayprefs, false) if err != nil || !(status == 204 || status == 200) { diff --git a/mediabrowser/jfapi.go b/mediabrowser/jfapi.go index ea9c531..21d20f6 100644 --- a/mediabrowser/jfapi.go +++ b/mediabrowser/jfapi.go @@ -1,181 +1,13 @@ package mediabrowser import ( - "bytes" - "compress/gzip" "encoding/json" "fmt" - "io" - "io/ioutil" "net/http" - "strings" "time" - - "github.com/hrfee/jfa-go/common" ) -// NewJellyfin returns a new Jellyfin object. -func NewJellyfin(server, client, version, device, deviceID string, timeoutHandler common.TimeoutHandler, cacheTimeout int) (*MediaBrowserStruct, error) { - jf := &MediaBrowserStruct{} - jf.Server = server - jf.client = client - jf.version = version - jf.device = device - jf.deviceID = deviceID - jf.useragent = fmt.Sprintf("%s/%s", client, version) - jf.timeoutHandler = timeoutHandler - jf.auth = fmt.Sprintf("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\"", client, device, deviceID, version) - jf.header = map[string]string{ - "Accept": "application/json", - "Content-type": "application/json; charset=UTF-8", - "X-Application": jf.useragent, - "Accept-Charset": "UTF-8,*", - "Accept-Encoding": "gzip", - "User-Agent": jf.useragent, - "X-Emby-Authorization": jf.auth, - } - jf.httpClient = &http.Client{ - Timeout: 10 * time.Second, - } - infoURL := fmt.Sprintf("%s/System/Info/Public", server) - req, _ := http.NewRequest("GET", infoURL, nil) - resp, err := jf.httpClient.Do(req) - defer jf.timeoutHandler() - if err == nil { - data, _ := ioutil.ReadAll(resp.Body) - json.Unmarshal(data, &jf.ServerInfo) - } - jf.cacheLength = cacheTimeout - jf.CacheExpiry = time.Now() - return jf, nil -} - -// Authenticate attempts to authenticate using a username & password -func (jf *MediaBrowserStruct) Authenticate(username, password string) (map[string]interface{}, int, error) { - jf.Username = username - jf.password = password - jf.loginParams = map[string]string{ - "Username": username, - "Pw": password, - "Password": password, - } - buffer := &bytes.Buffer{} - encoder := json.NewEncoder(buffer) - encoder.SetEscapeHTML(false) - err := encoder.Encode(jf.loginParams) - if err != nil { - return nil, 0, err - } - // loginParams, _ := json.Marshal(jf.loginParams) - url := fmt.Sprintf("%s/Users/authenticatebyname", jf.Server) - req, err := http.NewRequest("POST", url, buffer) - defer jf.timeoutHandler() - if err != nil { - return nil, 0, err - } - for name, value := range jf.header { - req.Header.Add(name, value) - } - resp, err := jf.httpClient.Do(req) - if err != nil || resp.StatusCode != 200 { - return nil, resp.StatusCode, err - } - defer resp.Body.Close() - var data io.Reader - switch resp.Header.Get("Content-Encoding") { - case "gzip": - data, _ = gzip.NewReader(resp.Body) - default: - data = resp.Body - } - var respData map[string]interface{} - json.NewDecoder(data).Decode(&respData) - jf.AccessToken = respData["AccessToken"].(string) - user := respData["User"].(map[string]interface{}) - jf.userID = respData["User"].(map[string]interface{})["Id"].(string) - jf.auth = fmt.Sprintf("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\", Token=\"%s\"", jf.client, jf.device, jf.deviceID, jf.version, jf.AccessToken) - jf.header["X-Emby-Authorization"] = jf.auth - jf.Authenticated = true - return user, resp.StatusCode, nil -} - -func (jf *MediaBrowserStruct) get(url string, params map[string]string) (string, int, error) { - var req *http.Request - if params != nil { - jsonParams, _ := json.Marshal(params) - req, _ = http.NewRequest("GET", url, bytes.NewBuffer(jsonParams)) - } else { - req, _ = http.NewRequest("GET", url, nil) - } - for name, value := range jf.header { - req.Header.Add(name, value) - } - resp, err := jf.httpClient.Do(req) - defer jf.timeoutHandler() - if err != nil || resp.StatusCode != 200 { - if resp.StatusCode == 401 && jf.Authenticated { - jf.Authenticated = false - _, _, authErr := jf.Authenticate(jf.Username, jf.password) - if authErr == nil { - v1, v2, v3 := jf.get(url, params) - return v1, v2, v3 - } - } - return "", resp.StatusCode, err - } - defer resp.Body.Close() - var data io.Reader - encoding := resp.Header.Get("Content-Encoding") - switch encoding { - case "gzip": - data, _ = gzip.NewReader(resp.Body) - default: - data = resp.Body - } - buf := new(strings.Builder) - io.Copy(buf, data) - //var respData map[string]interface{} - //json.NewDecoder(data).Decode(&respData) - return buf.String(), resp.StatusCode, nil -} - -func (jf *MediaBrowserStruct) post(url string, data map[string]interface{}, response bool) (string, int, error) { - params, _ := json.Marshal(data) - req, _ := http.NewRequest("POST", url, bytes.NewBuffer(params)) - for name, value := range jf.header { - req.Header.Add(name, value) - } - resp, err := jf.httpClient.Do(req) - defer jf.timeoutHandler() - if err != nil || resp.StatusCode != 200 { - if resp.StatusCode == 401 && jf.Authenticated { - jf.Authenticated = false - _, _, authErr := jf.Authenticate(jf.Username, jf.password) - if authErr == nil { - v1, v2, v3 := jf.post(url, data, response) - return v1, v2, v3 - } - } - return "", resp.StatusCode, err - } - if response { - defer resp.Body.Close() - var outData io.Reader - switch resp.Header.Get("Content-Encoding") { - case "gzip": - outData, _ = gzip.NewReader(resp.Body) - default: - outData = resp.Body - } - buf := new(strings.Builder) - io.Copy(buf, outData) - return buf.String(), resp.StatusCode, nil - } - return "", resp.StatusCode, nil -} - -// DeleteUser deletes the user corresponding to the provided ID. -func (jf *MediaBrowserStruct) DeleteUser(userID string) (int, error) { +func jfDeleteUser(jf *MediaBrowser, userID string) (int, error) { url := fmt.Sprintf("%s/Users/%s", jf.Server, userID) req, _ := http.NewRequest("DELETE", url, nil) for name, value := range jf.header { @@ -186,8 +18,7 @@ func (jf *MediaBrowserStruct) DeleteUser(userID string) (int, error) { return resp.StatusCode, err } -// GetUsers returns all (visible) users on the Jellyfin instance. -func (jf *MediaBrowserStruct) GetUsers(public bool) ([]map[string]interface{}, int, error) { +func jfGetUsers(jf *MediaBrowser, public bool) ([]map[string]interface{}, int, error) { var result []map[string]interface{} var data string var status int @@ -216,8 +47,7 @@ func (jf *MediaBrowserStruct) GetUsers(public bool) ([]map[string]interface{}, i return jf.userCache, 200, nil } -// UserByName returns the user corresponding to the provided username. -func (jf *MediaBrowserStruct) UserByName(username string, public bool) (map[string]interface{}, int, error) { +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) { users, status, err := jf.GetUsers(public) @@ -239,8 +69,7 @@ func (jf *MediaBrowserStruct) UserByName(username string, public bool) (map[stri return match, status, err } -// UserByID returns the user corresponding to the provided ID. -func (jf *MediaBrowserStruct) UserByID(userID string, public bool) (map[string]interface{}, int, error) { +func jfUserByID(jf *MediaBrowser, userID string, public bool) (map[string]interface{}, int, error) { if jf.CacheExpiry.After(time.Now()) { for _, user := range jf.userCache { if user["Id"].(string) == userID { @@ -273,8 +102,7 @@ func (jf *MediaBrowserStruct) UserByID(userID string, public bool) (map[string]i return result, status, nil } -// NewUser creates a new user with the provided username and password. -func (jf *MediaBrowserStruct) NewUser(username, password string) (map[string]interface{}, int, error) { +func jfNewUser(jf *MediaBrowser, username, password string) (map[string]interface{}, int, error) { url := fmt.Sprintf("%s/Users/New", jf.Server) stringData := map[string]string{ "Name": username, @@ -293,8 +121,7 @@ func (jf *MediaBrowserStruct) NewUser(username, password string) (map[string]int return recv, status, nil } -// SetPolicy sets the access policy for the user corresponding to the provided ID. -func (jf *MediaBrowserStruct) SetPolicy(userID string, policy map[string]interface{}) (int, error) { +func jfSetPolicy(jf *MediaBrowser, userID string, policy map[string]interface{}) (int, error) { url := fmt.Sprintf("%s/Users/%s/Policy", jf.Server, userID) _, status, err := jf.post(url, policy, false) if err != nil || status != 200 { @@ -303,15 +130,13 @@ func (jf *MediaBrowserStruct) SetPolicy(userID string, policy map[string]interfa return status, nil } -// SetConfiguration sets the configuration (part of homescreen layout) for the user corresponding to the provided ID. -func (jf *MediaBrowserStruct) SetConfiguration(userID string, configuration map[string]interface{}) (int, error) { +func jfSetConfiguration(jf *MediaBrowser, userID string, configuration map[string]interface{}) (int, error) { url := fmt.Sprintf("%s/Users/%s/Configuration", jf.Server, userID) _, status, err := jf.post(url, configuration, false) return status, err } -// GetDisplayPreferences gets the displayPreferences (part of homescreen layout) for the user corresponding to the provided ID. -func (jf *MediaBrowserStruct) GetDisplayPreferences(userID string) (map[string]interface{}, int, error) { +func jfGetDisplayPreferences(jf *MediaBrowser, userID string) (map[string]interface{}, int, error) { url := fmt.Sprintf("%s/DisplayPreferences/usersettings?userId=%s&client=emby", jf.Server, userID) data, status, err := jf.get(url, nil) if err != nil || !(status == 204 || status == 200) { @@ -325,8 +150,7 @@ func (jf *MediaBrowserStruct) GetDisplayPreferences(userID string) (map[string]i return displayprefs, status, nil } -// SetDisplayPreferences sets the displayPreferences (part of homescreen layout) for the user corresponding to the provided ID. -func (jf *MediaBrowserStruct) SetDisplayPreferences(userID string, displayprefs map[string]interface{}) (int, error) { +func jfSetDisplayPreferences(jf *MediaBrowser, userID string, displayprefs map[string]interface{}) (int, error) { url := fmt.Sprintf("%s/DisplayPreferences/usersettings?userId=%s&client=emby", jf.Server, userID) _, status, err := jf.post(url, displayprefs, false) if err != nil || !(status == 204 || status == 200) { diff --git a/mediabrowser/mediabrowser.go b/mediabrowser/mediabrowser.go index d7fad47..f3a14a9 100644 --- a/mediabrowser/mediabrowser.go +++ b/mediabrowser/mediabrowser.go @@ -1,10 +1,24 @@ package mediabrowser import ( + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "io/ioutil" "net/http" + "strings" "time" + + "github.com/hrfee/jfa-go/common" ) +type serverType bool + +var JellyfinServer serverType = false +var EmbyServer serverType = true + type serverInfo struct { LocalAddress string `json:"LocalAddress"` Name string `json:"ServerName"` @@ -13,7 +27,8 @@ type serverInfo struct { ID string `json:"Id"` } -type MediaBrowserStruct struct { +// MediaBrowser is an api instance of Jellyfin/Emby. +type MediaBrowser struct { Server string client string version string @@ -35,19 +50,239 @@ type MediaBrowserStruct struct { cacheLength int noFail bool Hyphens bool - timeoutHandler TimeoutHandler + serverType serverType + timeoutHandler common.TimeoutHandler } -// MediaBrowser is an api instance of Jellyfin/Emby. -type MediaBrowser interface { - Authenticate(username, password string) (map[string]interface{}, int, error) - DeleteUser(userID string) (int, error) - GetUsers(public bool) ([]map[string]interface{}, int, error) - UserByName(username string, public bool) (map[string]interface{}, int, error) - UserByID(userID string, public bool) (map[string]interface{}, int, error) - NewUser(username, password string) (map[string]interface{}, int, error) - SetPolicy(userID string, policy map[string]interface{}) (int, error) - SetConfiguration(userID string, configuration map[string]interface{}) (int, error) - GetDisplayPreferences(userID string) (map[string]interface{}, int, error) - SetDisplayPreferences(userID string, displayprefs map[string]interface{}) (int, error) +// NewServer returns a new Jellyfin object. +func NewServer(st serverType, server, client, version, device, deviceID string, timeoutHandler common.TimeoutHandler, cacheTimeout int) (*MediaBrowser, error) { + mb := &MediaBrowser{} + mb.serverType = st + mb.Server = server + mb.client = client + mb.version = version + mb.device = device + mb.deviceID = deviceID + mb.useragent = fmt.Sprintf("%s/%s", client, version) + mb.timeoutHandler = timeoutHandler + mb.auth = fmt.Sprintf("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\"", client, device, deviceID, version) + mb.header = map[string]string{ + "Accept": "application/json", + "Content-type": "application/json; charset=UTF-8", + "X-Application": mb.useragent, + "Accept-Charset": "UTF-8,*", + "Accept-Encoding": "gzip", + "User-Agent": mb.useragent, + "X-Emby-Authorization": mb.auth, + } + mb.httpClient = &http.Client{ + Timeout: 10 * time.Second, + } + infoURL := fmt.Sprintf("%s/System/Info/Public", server) + req, _ := http.NewRequest("GET", infoURL, nil) + resp, err := mb.httpClient.Do(req) + defer mb.timeoutHandler() + if err == nil { + data, _ := ioutil.ReadAll(resp.Body) + json.Unmarshal(data, &mb.ServerInfo) + } + mb.cacheLength = cacheTimeout + mb.CacheExpiry = time.Now() + return mb, nil +} + +func (mb *MediaBrowser) get(url string, params map[string]string) (string, int, error) { + var req *http.Request + if params != nil { + jsonParams, _ := json.Marshal(params) + req, _ = http.NewRequest("GET", url, bytes.NewBuffer(jsonParams)) + } else { + req, _ = http.NewRequest("GET", url, nil) + } + for name, value := range mb.header { + req.Header.Add(name, value) + } + resp, err := mb.httpClient.Do(req) + defer mb.timeoutHandler() + if err != nil || resp.StatusCode != 200 { + if resp.StatusCode == 401 && mb.Authenticated { + mb.Authenticated = false + _, _, authErr := mb.Authenticate(mb.Username, mb.password) + if authErr == nil { + v1, v2, v3 := mb.get(url, params) + return v1, v2, v3 + } + } + return "", resp.StatusCode, err + } + defer resp.Body.Close() + var data io.Reader + encoding := resp.Header.Get("Content-Encoding") + switch encoding { + case "gzip": + data, _ = gzip.NewReader(resp.Body) + default: + data = resp.Body + } + buf := new(strings.Builder) + io.Copy(buf, data) + //var respData map[string]interface{} + //json.NewDecoder(data).Decode(&respData) + return buf.String(), resp.StatusCode, nil +} + +func (mb *MediaBrowser) post(url string, data map[string]interface{}, response bool) (string, int, error) { + params, _ := json.Marshal(data) + req, _ := http.NewRequest("POST", url, bytes.NewBuffer(params)) + for name, value := range mb.header { + req.Header.Add(name, value) + } + resp, err := mb.httpClient.Do(req) + defer mb.timeoutHandler() + if err != nil || resp.StatusCode != 200 { + if resp.StatusCode == 401 && mb.Authenticated { + mb.Authenticated = false + _, _, authErr := mb.Authenticate(mb.Username, mb.password) + if authErr == nil { + v1, v2, v3 := mb.post(url, data, response) + return v1, v2, v3 + } + } + return "", resp.StatusCode, err + } + if response { + defer resp.Body.Close() + var outData io.Reader + switch resp.Header.Get("Content-Encoding") { + case "gzip": + outData, _ = gzip.NewReader(resp.Body) + default: + outData = resp.Body + } + buf := new(strings.Builder) + io.Copy(buf, outData) + return buf.String(), resp.StatusCode, nil + } + return "", resp.StatusCode, nil +} + +// Authenticate attempts to authenticate using a username & password +func (mb *MediaBrowser) Authenticate(username, password string) (map[string]interface{}, int, error) { + mb.Username = username + mb.password = password + mb.loginParams = map[string]string{ + "Username": username, + "Pw": password, + "Password": password, + } + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) + err := encoder.Encode(mb.loginParams) + if err != nil { + return nil, 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 + } + 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 + } + defer resp.Body.Close() + var data io.Reader + switch resp.Header.Get("Content-Encoding") { + case "gzip": + data, _ = gzip.NewReader(resp.Body) + default: + data = resp.Body + } + var respData map[string]interface{} + json.NewDecoder(data).Decode(&respData) + mb.AccessToken = respData["AccessToken"].(string) + user := respData["User"].(map[string]interface{}) + mb.userID = respData["User"].(map[string]interface{})["Id"].(string) + 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 + return user, resp.StatusCode, nil +} + +// DeleteUser deletes the user corresponding to the provided ID. +func (mb *MediaBrowser) DeleteUser(userID string) (int, error) { + if mb.serverType == JellyfinServer { + return jfDeleteUser(mb, userID) + } + return embyDeleteUser(mb, userID) +} + +// GetUsers returns all (visible) users on the Emby instance. +func (mb *MediaBrowser) GetUsers(public bool) ([]map[string]interface{}, int, error) { + if mb.serverType == JellyfinServer { + return jfGetUsers(mb, public) + } + return embyGetUsers(mb, public) +} + +// UserByName returns the user corresponding to the provided username. +func (mb *MediaBrowser) UserByName(username string, public bool) (map[string]interface{}, int, error) { + if mb.serverType == JellyfinServer { + return jfUserByName(mb, username, public) + } + return embyUserByName(mb, username, public) +} + +// UserByID returns the user corresponding to the provided ID. +func (mb *MediaBrowser) UserByID(userID string, public bool) (map[string]interface{}, int, error) { + if mb.serverType == JellyfinServer { + return jfUserByID(mb, userID, public) + } + return embyUserByID(mb, userID, public) +} + +// NewUser creates a new user with the provided username and password. +func (mb *MediaBrowser) NewUser(username, password string) (map[string]interface{}, int, error) { + if mb.serverType == JellyfinServer { + return jfNewUser(mb, username, password) + } + return embyNewUser(mb, username, password) +} + +// 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) { + if mb.serverType == JellyfinServer { + return jfSetPolicy(mb, userID, policy) + } + return embySetPolicy(mb, userID, policy) +} + +// 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) { + if mb.serverType == JellyfinServer { + return jfSetConfiguration(mb, userID, configuration) + } + return embySetConfiguration(mb, userID, configuration) +} + +// GetDisplayPreferences gets the displayPreferences (part of homescreen layout) for the user corresponding to the provided ID. +func (mb *MediaBrowser) GetDisplayPreferences(userID string) (map[string]interface{}, int, error) { + if mb.serverType == JellyfinServer { + return jfGetDisplayPreferences(mb, userID) + } + return embyGetDisplayPreferences(mb, userID) +} + +// SetDisplayPreferences sets the displayPreferences (part of homescreen layout) for the user corresponding to the provided ID. +func (mb *MediaBrowser) SetDisplayPreferences(userID string, displayprefs map[string]interface{}) (int, error) { + if mb.serverType == JellyfinServer { + return jfSetDisplayPreferences(mb, userID, displayprefs) + } + return embySetDisplayPreferences(mb, userID, displayprefs) } diff --git a/setup.go b/setup.go index b6f4502..4bb1ab3 100644 --- a/setup.go +++ b/setup.go @@ -3,7 +3,7 @@ package main import ( "github.com/gin-gonic/gin" "github.com/hrfee/jfa-go/common" - "github.com/hrfee/jfa-go/jfapi" + "github.com/hrfee/jfa-go/mediabrowser" ) type testReq struct { @@ -15,7 +15,7 @@ type testReq struct { func (app *appContext) TestJF(gc *gin.Context) { var req testReq gc.BindJSON(&req) - tempjf, _ := jfapi.NewJellyfin(req.Host, "jfa-go-setup", app.version, "auth", "auth", common.NewTimeoutHandler("authJF", req.Host, true), 30) + tempjf, _ := mediabrowser.NewServer(mediabrowser.JellyfinServer, req.Host, "jfa-go-setup", app.version, "auth", "auth", common.NewTimeoutHandler("authJF", req.Host, true), 30) _, status, err := tempjf.Authenticate(req.Username, req.Password) if !(status == 200 || status == 204) || err != nil { app.info.Printf("Auth failed with code %d (%s)", status, err)