Initial Ombi integration

When enabled, an account for the user is created on both Jellyfin and
Ombi. Account defaults can be stored similarly to jf.
This commit is contained in:
Harvey Tindall 2020-09-05 17:32:49 +01:00
parent 9850545f1b
commit ba67fa7536
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
11 changed files with 493 additions and 30 deletions

52
api.go
View File

@ -4,6 +4,7 @@ import (
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
@ -253,6 +254,18 @@ func (app *appContext) NewUser(gc *gin.Context) {
app.storage.emails[id] = req.Email
app.storage.storeEmails()
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
app.storage.loadOmbiTemplate()
if len(app.storage.ombi_template) != 0 {
errors, code, err := app.ombi.newUser(req.Username, req.Password, req.Email, app.storage.ombi_template)
if err != nil || code != 200 {
app.info.Printf("Failed to create Ombi user (%d): %s", code, err)
app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", "))
} else {
app.info.Println("Created Ombi user")
}
}
}
gc.JSON(200, validation)
}
@ -471,6 +484,30 @@ func (app *appContext) GetUsers(gc *gin.Context) {
gc.JSON(200, resp)
}
type ombiUser struct {
Name string `json:"name,omitempty"`
ID string `json:"id"`
}
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)
respond(500, "Couldn't get users", gc)
return
}
userlist := make([]ombiUser, len(users))
for i, data := range users {
userlist[i] = ombiUser{
Name: data["userName"].(string),
ID: data["id"].(string),
}
}
gc.JSON(200, map[string][]ombiUser{"users": userlist})
}
func (app *appContext) ModifyEmails(gc *gin.Context) {
var req map[string]string
gc.BindJSON(&req)
@ -492,6 +529,21 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
gc.JSON(200, map[string]bool{"success": true})
}
func (app *appContext) SetOmbiDefaults(gc *gin.Context) {
var req ombiUser
gc.BindJSON(&req)
template, code, err := app.ombi.templateByID(req.ID)
if err != nil || code != 200 || len(template) == 0 {
app.err.Printf("Couldn't get user from Ombi: %d %s", code, err)
respond(500, "Couldn't get user", gc)
return
}
app.storage.ombi_template = template
fmt.Println(app.storage.ombi_path)
app.storage.storeOmbiTemplate()
gc.JSON(200, map[string]bool{"success": true})
}
type defaultsReq struct {
Username string `json:"username"`
Homescreen bool `json:"homescreen"`

View File

@ -48,7 +48,7 @@ func (app *appContext) loadConfig() error {
// }
key.SetValue(key.MustString(filepath.Join(app.data_path, (key.Name() + ".json"))))
}
for _, key := range []string{"user_configuration", "user_displayprefs"} {
for _, key := range []string{"user_configuration", "user_displayprefs", "ombi_template"} {
// if app.config.Section("files").Key(key).MustString("") == "" {
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
// }

View File

@ -40,7 +40,7 @@
"required": true,
"requires_restart": true,
"type": "text",
"value": "jf-accounts",
"value": "jfa-go",
"description": "This and below settings will show on the Jellyfin dashboard when the program connects. You may as well leave them alone."
},
"version": {
@ -55,14 +55,14 @@
"required": true,
"requires_restart": true,
"type": "text",
"value": "jf-accounts"
"value": "jfa-go"
},
"device_id": {
"name": "Device ID",
"required": true,
"requires_restart": true,
"type": "text",
"value": "jf-accounts-{version}"
"value": "jfa-go-{version}"
}
},
"ui": {
@ -398,7 +398,7 @@
"depends_true": "enabled",
"type": "text",
"value": "http://accounts.jellyf.in:8056/invite",
"description": "Base URL for jf-accounts. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself."
"description": "Base URL for jfa-go. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself."
}
},
"notifications": {
@ -511,6 +511,38 @@
"value": "smtp password"
}
},
"ombi": {
"meta": {
"name": "Ombi Integration",
"description": "Connect to Ombi to automatically create a new user's account. You'll need to create an Ombi user template."
},
"enabled": {
"name": "Enabled",
"required": false,
"requires_restart": true,
"type": "bool",
"value": false,
"description": "Enable to create an Ombi account for new Jellyfin users"
},
"server": {
"name": "URL",
"required": false,
"requires_restart": true,
"type": "text",
"value": "localhost:5000",
"depends_true": "enabled",
"description": "Ombi server URL, including http(s)://."
},
"api_key": {
"name": "API Key",
"required": false,
"requires_restart": true,
"type": "text",
"value": "",
"depends_true": "enabled",
"description": "API Key. Get this from the first tab in Ombi settings."
}
},
"files": {
"meta": {
"name": "File Storage",
@ -532,6 +564,14 @@
"value": "",
"description": "Location of stored email addresses (json)."
},
"ombi_template": {
"name": "Ombi user template",
"required": false,
"requires_restart": false,
"type": "text",
"value": "",
"description": "Location of stored Ombi user template."
},
"user_template": {
"name": "User Template",
"required": false,

View File

@ -9,6 +9,7 @@
"notifications",
"mailgun",
"smtp",
"ombi",
"files"
],
"jellyfin": {
@ -62,7 +63,7 @@
"required": true,
"requires_restart": true,
"type": "text",
"value": "jf-accounts",
"value": "jfa-go",
"description": "This and below settings will show on the Jellyfin dashboard when the program connects. You may as well leave them alone."
},
"version": {
@ -77,14 +78,14 @@
"required": true,
"requires_restart": true,
"type": "text",
"value": "jf-accounts"
"value": "jfa-go"
},
"device_id": {
"name": "Device ID",
"required": true,
"requires_restart": true,
"type": "text",
"value": "jf-accounts-{version}"
"value": "jfa-go-{version}"
}
},
"ui": {
@ -466,7 +467,7 @@
"depends_true": "enabled",
"type": "text",
"value": "http://accounts.jellyf.in:8056/invite",
"description": "Base URL for jf-accounts. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself."
"description": "Base URL for jfa-go. This is necessary because using a reverse proxy means the program has no way of knowing the URL itself."
}
},
"notifications": {
@ -596,10 +597,48 @@
"value": "smtp password"
}
},
"ombi": {
"order": [
"enabled",
"server",
"api_key"
],
"meta": {
"name": "Ombi Integration",
"description": "Connect to Ombi to automatically create a new user's account. You'll need to create an Ombi user template."
},
"enabled": {
"name": "Enabled",
"required": false,
"requires_restart": true,
"type": "bool",
"value": false,
"description": "Enable to create an Ombi account for new Jellyfin users"
},
"server": {
"name": "URL",
"required": false,
"requires_restart": true,
"type": "text",
"value": "localhost:5000",
"depends_true": "enabled",
"description": "Ombi server URL."
},
"api_key": {
"name": "API Key",
"required": false,
"requires_restart": true,
"type": "text",
"value": "",
"depends_true": "enabled",
"description": "API Key. Get this from the first tab in Ombi settings."
}
},
"files": {
"order": [
"invites",
"emails",
"ombi_template",
"user_template",
"user_configuration",
"user_displayprefs",
@ -625,6 +664,14 @@
"value": "",
"description": "Location of stored email addresses (json)."
},
"ombi_template": {
"name": "Ombi user template",
"required": false,
"requires_restart": false,
"type": "text",
"value": "",
"description": "Location of stored Ombi user template."
},
"user_template": {
"name": "User Template",
"required": false,

View File

@ -627,9 +627,6 @@ document.getElementById('openDefaultsWizard').onclick = function() {
let users = req.response['users'];
let radioList = document.getElementById('defaultUserRadios');
radioList.textContent = '';
if (document.getElementById('setDefaultUser')) {
document.getElementById('setDefaultUser').remove();
}
let first = true;
for (user of users) {
let radio = document.createElement('div');
@ -639,14 +636,14 @@ document.getElementById('openDefaultsWizard').onclick = function() {
first = false;
} else {
checked = '';
};
}
radio.innerHTML =
`<label><input type="radio" name="defaultRadios" id="default_${user['name']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
radioList.appendChild(radio);
}
let button = document.getElementById('openDefaultsWizard');
button.disabled = false;
button.innerHTML = 'Set new account defaults';
button.innerHTML = 'New account defaults';
let submitButton = document.getElementById('storeDefaults');
submitButton.disabled = false;
submitButton.textContent = 'Submit';
@ -715,6 +712,107 @@ document.getElementById('storeDefaults').onclick = function () {
}
};
var ombiDefaultsModal = '';
if (ombiEnabled) {
ombiDefaultsModal = createModal('ombiDefaults');
document.getElementById('openOmbiDefaults').onclick = function() {
this.disabled = true;
this.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'Loading...';
let req = new XMLHttpRequest();
req.responseType = 'json';
req.open("GET", "/getOmbiUsers", true);
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status == 200) {
let users = req.response['users'];
let radioList = document.getElementById('ombiUserRadios');
radioList.textContent = '';
let first = true;
// name and id
for (user of users) {
let radio = document.createElement('div');
radio.classList.add('radio');
let checked = 'checked';
if (first) {
first = false;
} else {
checked = '';
}
radio.innerHTML =
`<label><input type="radio" name="ombiRadios" id="default_${user['id']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
radioList.appendChild(radio);
}
let button = document.getElementById('openOmbiDefaults');
button.disabled = false;
button.innerHTML = 'Ombi user defaults';
let submitButton = document.getElementById('storeOmbiDefaults');
submitButton.disabled = false;
submitButton.textContent = 'Submit';
if (submitButton.classList.contains('btn-success')) {
submitButton.classList.remove('btn-success');
submitButton.classList.add('btn-primary');
} else if (submitButton.classList.contains('btn-danger')) {
submitButton.classList.remove('btn-danger');
submitButton.classList.add('btn-primary');
}
settingsModal.hide();
ombiDefaultsModal.show();
}
}
};
req.send();
};
document.getElementById('storeOmbiDefaults').onclick = function() {
this.disabled = true;
this.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'Loading...';
let button = document.getElementById('storeOmbiDefaults');
let radios = document.getElementsByName('ombiRadios');
for (let radio of radios) {
if (radio.checked) {
let data = {
'id': radio.id.slice(8),
};
let req = new XMLHttpRequest();
req.open("POST", "/setOmbiDefaults", true);
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status == 200 || this.status == 204) {
button.textContent = "Success";
if (button.classList.contains('btn-danger')) {
button.classList.remove('btn-danger');
} else if (button.classList.contains('btn-primary')) {
button.classList.remove('btn-primary');
}
button.classList.add('btn-success');
button.disabled = false;
setTimeout(function() { ombiDefaultsModal.hide(); }, 1000);
} else {
button.textContent = "Failed";
button.classList.remove('btn-primary');
button.classList.add('btn-danger');
setTimeout(function() {
let button = document.getElementById('storeOmbiDefaults');
button.textContent = "Submit";
button.classList.remove('btn-danger');
button.classList.add('btn-primary');
button.disabled = false;
}, 1000);
}
}
};
req.send(JSON.stringify(data));
}
}
};
}
document.getElementById('openUsers').onclick = function () {
this.disabled = true;
this.innerHTML =

View File

@ -52,6 +52,8 @@
}
css.setAttribute('href', cssFile);
document.head.appendChild(css);
// store whether ombi is enabled, 1 or 0.
var ombiEnabled = {{ .ombiEnabled }}
</script>
{{ if not .bs5 }}
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
@ -177,6 +179,11 @@
<button type="button" class="list-group-item list-group-item-action" id="openDefaultsWizard">
New account defaults
</button>
{{ if .ombiEnabled }}
<button type="button" class="list-group-item list-group-item-action" id="openOmbiDefaults">
Ombi user defaults
</button>
{{ end }}
</ul>
<div class="list-group list-group-flush" id="settingsList">
</div>
@ -230,6 +237,28 @@
</div>
</div>
</div>
{{ if .ombiEnabled }}
<div class="modal fade" id="ombiDefaults" role="dialog" aria-labelledby="Ombi Users" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ombiTitle">Ombi user defaults</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Create an Ombi user and configure it to your liking, then choose it from below to store the settings and permissions as a template for all new users.</p>
<div id="ombiUserRadios"></div>
</div>
<div class="modal-footer" id="ombiFooter">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="storeOmbiDefaults">Submit</button>
</div>
</div>
</div>
</div>
{{ end }}
<div class="modal fade" id="restartModal" role="dialog" aria-labelledby="Restart Warning" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">

View File

@ -43,10 +43,10 @@ type Jellyfin struct {
noFail bool
}
func (jf *Jellyfin) timeoutHandler() {
func timeoutHandler(name, addr string, noFail bool) {
if r := recover(); r != nil {
out := fmt.Sprintf("Failed to authenticate with Jellyfin @ %s: Timed out", jf.server)
if jf.noFail {
out := fmt.Sprintf("Failed to authenticate with %s @ %s: Timed out", name, addr)
if noFail {
log.Printf(out)
} else {
log.Fatalf(out)
@ -78,7 +78,7 @@ func newJellyfin(server, client, version, device, deviceId string) (*Jellyfin, e
infoUrl := fmt.Sprintf("%s/System/Info/Public", server)
req, _ := http.NewRequest("GET", infoUrl, nil)
resp, err := jf.httpClient.Do(req)
defer jf.timeoutHandler()
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
if err == nil {
data, _ := ioutil.ReadAll(resp.Body)
json.Unmarshal(data, &jf.serverInfo)
@ -106,7 +106,7 @@ func (jf *Jellyfin) authenticate(username, password string) (map[string]interfac
// loginParams, _ := json.Marshal(jf.loginParams)
url := fmt.Sprintf("%s/Users/authenticatebyname", jf.server)
req, err := http.NewRequest("POST", url, buffer)
defer jf.timeoutHandler()
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
if err != nil {
return nil, 0, err
}
@ -148,7 +148,7 @@ func (jf *Jellyfin) _getReader(url string, params map[string]string) (io.Reader,
req.Header.Add(name, value)
}
resp, err := jf.httpClient.Do(req)
defer jf.timeoutHandler()
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
if err != nil || resp.StatusCode != 200 {
if resp.StatusCode == 401 && jf.authenticated {
jf.authenticated = false
@ -180,7 +180,7 @@ func (jf *Jellyfin) _post(url string, data map[string]interface{}, response bool
req.Header.Add(name, value)
}
resp, err := jf.httpClient.Do(req)
defer jf.timeoutHandler()
defer timeoutHandler("Jellyfin", jf.server, jf.noFail)
if err != nil || resp.StatusCode != 200 {
if resp.StatusCode == 401 && jf.authenticated {
jf.authenticated = false

30
main.go
View File

@ -45,6 +45,7 @@ type appContext struct {
invalidTokens []string
jf *Jellyfin
authJf *Jellyfin
ombi *Ombi
datePattern string
timePattern string
storage Storage
@ -225,17 +226,32 @@ func main() {
app.debug.Println("Loading storage")
app.storage.invite_path = filepath.Join(app.data_path, "invites.json")
// app.storage.invite_path = filepath.Join(app.data_path, "invites.json")
app.storage.invite_path = app.config.Section("files").Key("invites").String()
app.storage.loadInvites()
app.storage.emails_path = filepath.Join(app.data_path, "emails.json")
// app.storage.emails_path = filepath.Join(app.data_path, "emails.json")
app.storage.emails_path = app.config.Section("files").Key("emails").String()
app.storage.loadEmails()
app.storage.policy_path = filepath.Join(app.data_path, "user_template.json")
// app.storage.policy_path = filepath.Join(app.data_path, "user_template.json")
app.storage.policy_path = app.config.Section("files").Key("user_template").String()
app.storage.loadPolicy()
app.storage.configuration_path = filepath.Join(app.data_path, "user_configuration.json")
// app.storage.configuration_path = filepath.Join(app.data_path, "user_configuration.json")
app.storage.policy_path = app.config.Section("files").Key("user_configuration").String()
app.storage.loadConfiguration()
app.storage.displayprefs_path = filepath.Join(app.data_path, "user_displayprefs.json")
// app.storage.displayprefs_path = filepath.Join(app.data_path, "user_displayprefs.json")
app.storage.displayprefs_path = app.config.Section("files").Key("user_displayprefs").String()
app.storage.loadDisplayprefs()
if app.config.Section("ombi").Key("enabled").MustBool(false) {
app.storage.ombi_path = app.config.Section("files").Key("ombi_template").String()
app.storage.loadOmbiTemplate()
app.ombi = newOmbi(
app.config.Section("ombi").Key("server").String(),
app.config.Section("ombi").Key("api_key").String(),
true,
)
}
app.configBase_path = filepath.Join(app.local_path, "config-base.json")
config_base, _ := ioutil.ReadFile(app.configBase_path)
json.Unmarshal(config_base, &app.configBase)
@ -340,6 +356,10 @@ func main() {
api.POST("/setDefaults", app.SetDefaults)
api.GET("/getConfig", app.GetConfig)
api.POST("/modifyConfig", app.ModifyConfig)
if app.config.Section("ombi").Key("enabled").MustBool(false) {
api.GET("/getOmbiUsers", app.OmbiUsers)
api.POST("/setOmbiDefaults", app.SetOmbiDefaults)
}
app.info.Printf("Starting router @ %s", address)
} else {
router.GET("/", func(gc *gin.Context) {

167
ombi.go Normal file
View File

@ -0,0 +1,167 @@
package main
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
type Ombi struct {
server, key string
header map[string]string
httpClient *http.Client
noFail bool
}
func newOmbi(server, key string, noFail bool) *Ombi {
return &Ombi{
server: server,
key: key,
noFail: noFail,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
header: map[string]string{
"ApiKey": key,
},
}
}
func (ombi *Ombi) _getReader(url string, params map[string]string) (string, int, error) {
if ombi.key == "" {
return "", 401, fmt.Errorf("No API key provided")
}
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 ombi.header {
req.Header.Add(name, value)
}
resp, err := ombi.httpClient.Do(req)
defer timeoutHandler("Ombi", ombi.server, ombi.noFail)
if err != nil || resp.StatusCode != 200 {
if resp.StatusCode == 401 {
return "", 401, fmt.Errorf("Invalid API Key")
}
return "", 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
}
buf := new(strings.Builder)
_, err = io.Copy(buf, data)
if err != nil {
return "", 500, err
}
return buf.String(), resp.StatusCode, nil
}
func (ombi *Ombi) _post(url string, data map[string]interface{}, response bool) (string, int, error) {
responseText := ""
params, _ := json.Marshal(data)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(params))
req.Header.Add("Content-Type", "application/json")
for name, value := range ombi.header {
req.Header.Add(name, value)
}
resp, err := ombi.httpClient.Do(req)
defer timeoutHandler("Ombi", ombi.server, ombi.noFail)
if err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 201) {
if resp.StatusCode == 401 {
return "", 401, fmt.Errorf("Invalid API Key")
}
return responseText, resp.StatusCode, err
}
if response {
defer resp.Body.Close()
var out io.Reader
switch resp.Header.Get("Content-Encoding") {
case "gzip":
out, _ = gzip.NewReader(resp.Body)
default:
out = resp.Body
}
buf := new(strings.Builder)
_, err = io.Copy(buf, out)
if err != nil {
return "", 500, err
}
responseText = buf.String()
}
return responseText, resp.StatusCode, nil
}
func (ombi *Ombi) userByID(id string) (result map[string]interface{}, code int, err error) {
resp, code, err := ombi._getReader(fmt.Sprintf("%s/api/v1/Identity/User/%s", ombi.server, id), nil)
json.Unmarshal([]byte(resp), &result)
return
}
func (ombi *Ombi) getUsers() (result []map[string]interface{}, code int, err error) {
resp, code, err := ombi._getReader(fmt.Sprintf("%s/api/v1/Identity/Users", ombi.server), nil)
json.Unmarshal([]byte(resp), &result)
return
}
// Strip these from a user when saving as a template.
// We also need to strip userQualityProfiles{"id", "userId"}
var stripFromOmbi = []string{
"alias",
"emailAddress",
"hasLoggedIn",
"id",
"lastLoggedIn",
"password",
"userName",
}
func (ombi *Ombi) templateByID(id string) (result map[string]interface{}, code int, err error) {
result, code, err = ombi.userByID(id)
if err != nil || code != 200 {
return
}
for _, key := range stripFromOmbi {
if _, ok := result[key]; ok {
delete(result, key)
}
}
if qp, ok := result["userQualityProfiles"].(map[string]interface{}); ok {
delete(qp, "id")
delete(qp, "userId")
result["userQualityProfiles"] = qp
}
return
}
func (ombi *Ombi) newUser(username, password, email string, template map[string]interface{}) ([]string, int, error) {
url := fmt.Sprintf("%s/api/v1/Identity", ombi.server)
user := template
user["userName"] = username
user["password"] = password
user["emailAddress"] = email
resp, code, err := ombi._post(url, user, true)
var data map[string]interface{}
json.Unmarshal([]byte(resp), &data)
if err != nil || code != 200 {
var lst []string
if data["errors"] != nil {
lst = data["errors"].([]string)
}
return lst, code, err
}
return nil, code, err
}

View File

@ -7,10 +7,10 @@ import (
)
type Storage struct {
timePattern string
invite_path, emails_path, policy_path, configuration_path, displayprefs_path string
invites Invites
emails, policy, configuration, displayprefs map[string]interface{}
timePattern string
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path string
invites Invites
emails, policy, configuration, displayprefs, ombi_template map[string]interface{}
}
// timePattern: %Y-%m-%dT%H:%M:%S.%f
@ -67,6 +67,14 @@ func (st *Storage) storeDisplayprefs() error {
return storeJSON(st.displayprefs_path, st.displayprefs)
}
func (st *Storage) loadOmbiTemplate() error {
return loadJSON(st.ombi_path, &st.ombi_template)
}
func (st *Storage) storeOmbiTemplate() error {
return storeJSON(st.ombi_path, st.ombi_template)
}
func loadJSON(path string, obj interface{}) error {
var file []byte
var err error

View File

@ -10,6 +10,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
bs5 := app.config.Section("ui").Key("bs5").MustBool(false)
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
gc.HTML(http.StatusOK, "admin.html", gin.H{
"bs5": bs5,
"cssFile": app.cssFile,
@ -18,6 +19,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
"notifications": notificationsEnabled,
"version": VERSION,
"commit": COMMIT,
"ombiEnabled": ombiEnabled,
})
}