1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-09-19 19:00:11 +00:00

jellyseerr: use in profiles, apply on user creation and modification

added in the same way as ombi profiles. Most code is copy-pasted and
adjusted from ombi (especially on web), so maybe this can be merged in
the future. Also, profile names are url-escaped like announcement
template names were not too long ago. API client has "LogRequestBodies"
option which just dumps the request body when enabled (useful for
recreating reqs in the jellyseerr swagger UI). User.Name() helper
returns a name from all three possible values in the struct.
This commit is contained in:
Harvey Tindall 2024-07-30 16:36:59 +01:00
parent 7b9cdf385a
commit a97bccc88f
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
19 changed files with 555 additions and 105 deletions

100
api-jellyseerr.go Normal file
View File

@ -0,0 +1,100 @@
package main
import (
"net/url"
"strconv"
"github.com/gin-gonic/gin"
)
// @Summary Get a list of Jellyseerr users.
// @Produce json
// @Success 200 {object} ombiUsersDTO
// @Failure 500 {object} stringResponse
// @Router /jellyseerr/users [get]
// @Security Bearer
// @tags Jellyseerr
func (app *appContext) JellyseerrUsers(gc *gin.Context) {
app.debug.Println("Jellyseerr users requested")
users, err := app.js.GetUsers()
if err != nil {
app.err.Printf("Failed to get users from Jellyseerr: %v", err)
respond(500, "Couldn't get users", gc)
return
}
app.debug.Printf("Jellyseerr users retrieved: %d", len(users))
userlist := make([]ombiUser, len(users))
i := 0
for _, u := range users {
userlist[i] = ombiUser{
Name: u.Name(),
ID: strconv.FormatInt(u.ID, 10),
}
i++
}
gc.JSON(200, ombiUsersDTO{Users: userlist})
}
// @Summary Store Jellyseerr user template in an existing profile.
// @Produce json
// @Param id path string true "Jellyseerr ID of user to source from"
// @Param profile path string true "Name of profile to store in"
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /profiles/jellyseerr/{profile}/{id} [post]
// @Security Bearer
// @tags Jellyseerr
func (app *appContext) SetJellyseerrProfile(gc *gin.Context) {
jellyseerrID, err := strconv.ParseInt(gc.Param("id"), 10, 64)
if err != nil {
respondBool(400, false, gc)
return
}
escapedProfileName := gc.Param("profile")
profileName, _ := url.QueryUnescape(escapedProfileName)
profile, ok := app.storage.GetProfileKey(profileName)
if !ok {
respondBool(400, false, gc)
return
}
u, err := app.js.UserByID(jellyseerrID)
if err != nil {
app.err.Printf("Couldn't get user from Jellyseerr: %v", err)
respond(500, "Couldn't get user", gc)
return
}
profile.Jellyseerr.User = u.UserTemplate
n, err := app.js.GetNotificationPreferencesByID(jellyseerrID)
if err != nil {
app.err.Printf("Couldn't get user's notification prefs from Jellyseerr: %v", err)
respond(500, "Couldn't get user notification prefs", gc)
return
}
profile.Jellyseerr.Notifications = n.NotificationsTemplate
profile.Jellyseerr.Enabled = true
app.storage.SetProfileKey(profileName, profile)
respondBool(204, true, gc)
}
// @Summary Remove jellyseerr user template from a profile.
// @Produce json
// @Param profile path string true "Name of profile to store in"
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /profiles/jellyseerr/{profile} [delete]
// @Security Bearer
// @tags Jellyseerr
func (app *appContext) DeleteJellyseerrProfile(gc *gin.Context) {
escapedProfileName := gc.Param("profile")
profileName, _ := url.QueryUnescape(escapedProfileName)
profile, ok := app.storage.GetProfileKey(profileName)
if !ok {
respondBool(400, false, gc)
return
}
profile.Jellyseerr.Enabled = false
app.storage.SetProfileKey(profileName, profile)
respondBool(204, true, gc)
}

View File

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"net/url"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
@ -95,7 +96,8 @@ func (app *appContext) OmbiUsers(gc *gin.Context) {
func (app *appContext) SetOmbiProfile(gc *gin.Context) { func (app *appContext) SetOmbiProfile(gc *gin.Context) {
var req ombiUser var req ombiUser
gc.BindJSON(&req) gc.BindJSON(&req)
profileName := gc.Param("profile") escapedProfileName := gc.Param("profile")
profileName, _ := url.QueryUnescape(escapedProfileName)
profile, ok := app.storage.GetProfileKey(profileName) profile, ok := app.storage.GetProfileKey(profileName)
if !ok { if !ok {
respondBool(400, false, gc) respondBool(400, false, gc)
@ -122,7 +124,8 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
// @Security Bearer // @Security Bearer
// @tags Ombi // @tags Ombi
func (app *appContext) DeleteOmbiProfile(gc *gin.Context) { func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
profileName := gc.Param("profile") escapedProfileName := gc.Param("profile")
profileName, _ := url.QueryUnescape(escapedProfileName)
profile, ok := app.storage.GetProfileKey(profileName) profile, ok := app.storage.GetProfileKey(profileName)
if !ok { if !ok {
respondBool(400, false, gc) respondBool(400, false, gc)

View File

@ -27,6 +27,7 @@ func (app *appContext) GetProfiles(gc *gin.Context) {
LibraryAccess: p.LibraryAccess, LibraryAccess: p.LibraryAccess,
FromUser: p.FromUser, FromUser: p.FromUser,
Ombi: p.Ombi != nil, Ombi: p.Ombi != nil,
Jellyseerr: p.Jellyseerr.Enabled,
ReferralsEnabled: false, ReferralsEnabled: false,
} }
if referralsEnabled { if referralsEnabled {

View File

@ -4,11 +4,13 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
"github.com/hrfee/jfa-go/jellyseerr"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3" "github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4" "github.com/timshannon/badgerhold/v4"
@ -94,6 +96,29 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
app.info.Println("Created Ombi user") app.info.Println("Created Ombi user")
} }
} }
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
// Gets existing user (not possible) or imports the given user.
_, err := app.js.MustGetUser(id)
if err != nil {
app.err.Printf("Failed to create Jellyseerr user: %v", err)
} else {
app.info.Println("Created Jellyseerr user")
}
err = app.js.ApplyTemplateToUser(id, profile.Jellyseerr.User)
if err != nil {
app.err.Printf("Failed to apply Jellyseerr user template: %v\n", err)
}
err = app.js.ApplyNotificationsTemplateToUser(id, profile.Jellyseerr.Notifications)
if err != nil {
app.err.Printf("Failed to apply Jellyseerr notifications template: %v\n", err)
}
if emailEnabled {
err = app.js.ModifyUser(id, map[jellyseerr.UserField]any{jellyseerr.FieldEmail: req.Email})
if err != nil {
app.err.Printf("Failed to set Jellyseerr email address: %v\n", err)
}
}
}
if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" { if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" {
app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email) app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email)
msg, err := app.email.constructWelcome(req.Username, time.Time{}, app, false) msg, err := app.email.constructWelcome(req.Username, time.Time{}, app, false)
@ -338,6 +363,10 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
Addr: req.Email, Addr: req.Email,
Contact: (req.Email != ""), Contact: (req.Email != ""),
} }
// Only allow disabling of email contact if some other method is available.
if req.DiscordContact || req.TelegramContact || req.MatrixContact {
emailStore.Contact = req.EmailContact
}
if invite.UserLabel != "" { if invite.UserLabel != "" {
emailStore.Label = invite.UserLabel emailStore.Label = invite.UserLabel
@ -468,6 +497,51 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
app.debug.Printf("Skipping Ombi: Profile \"%s\" was empty", invite.Profile) app.debug.Printf("Skipping Ombi: Profile \"%s\" was empty", invite.Profile)
} }
} }
if invite.Profile != "" && app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
if profile.Jellyseerr.Enabled {
// Gets existing user (not possible) or imports the given user.
_, err := app.js.MustGetUser(id)
if err != nil {
app.err.Printf("Failed to create Jellyseerr user: %v", err)
} else {
app.info.Println("Created Jellyseerr user")
}
err = app.js.ApplyTemplateToUser(id, profile.Jellyseerr.User)
if err != nil {
app.err.Printf("Failed to apply Jellyseerr user template: %v\n", err)
}
err = app.js.ApplyNotificationsTemplateToUser(id, profile.Jellyseerr.Notifications)
if err != nil {
app.err.Printf("Failed to apply Jellyseerr notifications template: %v\n", err)
}
contactMethods := map[jellyseerr.NotificationsField]any{}
if emailEnabled {
err = app.js.ModifyUser(id, map[jellyseerr.UserField]any{jellyseerr.FieldEmail: req.Email})
if err != nil {
app.err.Printf("Failed to set Jellyseerr email address: %v\n", err)
} else {
contactMethods[jellyseerr.FieldEmailEnabled] = req.EmailContact
}
}
if discordVerified {
contactMethods[jellyseerr.FieldDiscord] = discordUser.ID
contactMethods[jellyseerr.FieldDiscordEnabled] = req.DiscordContact
}
if telegramVerified {
u, _ := app.storage.GetTelegramKey(user.ID)
contactMethods[jellyseerr.FieldTelegram] = strconv.FormatInt(u.ChatID, 10)
contactMethods[jellyseerr.FieldTelegramEnabled] = req.TelegramContact
}
if emailEnabled || discordVerified || telegramVerified {
err := app.js.ModifyNotifications(id, contactMethods)
if err != nil {
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
}
} else {
app.debug.Printf("Skipping Jellyseerr: Profile \"%s\" was empty", invite.Profile)
}
}
if matrixVerified { if matrixVerified {
matrixUser.Contact = req.MatrixContact matrixUser.Contact = req.MatrixContact
delete(app.matrix.tokens, req.MatrixPIN) delete(app.matrix.tokens, req.MatrixPIN)
@ -1265,6 +1339,8 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
var configuration mediabrowser.Configuration var configuration mediabrowser.Configuration
var displayprefs map[string]interface{} var displayprefs map[string]interface{}
var ombi map[string]interface{} var ombi map[string]interface{}
var jellyseerr JellyseerrTemplate
jellyseerr.Enabled = false
if req.From == "profile" { if req.From == "profile" {
// Check profile exists & isn't empty // Check profile exists & isn't empty
profile, ok := app.storage.GetProfileKey(req.Profile) profile, ok := app.storage.GetProfileKey(req.Profile)
@ -1288,6 +1364,11 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
ombi = profile.Ombi ombi = profile.Ombi
} }
} }
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
if profile.Jellyseerr.Enabled {
jellyseerr = profile.Jellyseerr
}
}
} else if req.From == "user" { } else if req.From == "user" {
applyingFrom = "user" applyingFrom = "user"
@ -1315,6 +1396,7 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
"policy": map[string]string{}, "policy": map[string]string{},
"homescreen": map[string]string{}, "homescreen": map[string]string{},
"ombi": map[string]string{}, "ombi": map[string]string{},
"jellyseerr": map[string]string{},
} }
/* Jellyfin doesn't seem to like too many of these requests sent in succession /* Jellyfin doesn't seem to like too many of these requests sent in succession
and can crash and mess up its database. Issue #160 says this occurs when more and can crash and mess up its database. Issue #160 says this occurs when more
@ -1367,6 +1449,26 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
errors["ombi"][id] = errorString errors["ombi"][id] = errorString
} }
} }
if jellyseerr.Enabled {
errorString := ""
// newUser := ombi
// newUser["id"] = user["id"]
// newUser["userName"] = user["userName"]
// newUser["alias"] = user["alias"]
// newUser["emailAddress"] = user["emailAddress"]
err := app.js.ApplyTemplateToUser(id, jellyseerr.User)
if err != nil {
errorString += fmt.Sprintf("ApplyUser: %v ", err)
}
err = app.js.ApplyNotificationsTemplateToUser(id, jellyseerr.Notifications)
if err != nil {
errorString += fmt.Sprintf("ApplyNotifications: %v ", err)
}
if errorString != "" {
errors["jellyseerr"][id] = errorString
}
}
if shouldDelay { if shouldDelay {
time.Sleep(250 * time.Millisecond) time.Sleep(250 * time.Millisecond)
} }

View File

@ -1580,6 +1580,41 @@
} }
} }
}, },
"jellyseerr": {
"order": [],
"meta": {
"name": "Jellyseerr Integration",
"description": "Connect to Jellyseerr to automatically trigger the import of users on account creation, and to automatically link contact methods (email, discord and telegram). A template must be added to a User Profile for accounts to be created."
},
"settings": {
"enabled": {
"name": "Enabled",
"required": false,
"requires_restart": true,
"type": "bool",
"value": false,
"description": "Enable the Jellyseerr integration."
},
"server": {
"name": "URL",
"required": false,
"requires_restart": true,
"type": "text",
"value": "localhost:5000",
"depends_true": "enabled",
"description": "Jellyseerr 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 Jellyseerr's settings."
}
}
},
"backups": { "backups": {
"order": [], "order": [],
"meta": { "meta": {

5
go.mod
View File

@ -16,6 +16,8 @@ replace github.com/hrfee/jfa-go/api => ./api
replace github.com/hrfee/jfa-go/easyproxy => ./easyproxy replace github.com/hrfee/jfa-go/easyproxy => ./easyproxy
replace github.com/hrfee/jfa-go/jellyseerr => ./jellyseerr
require ( require (
github.com/bwmarrin/discordgo v0.27.1 github.com/bwmarrin/discordgo v0.27.1
github.com/dgraph-io/badger/v3 v3.2103.5 github.com/dgraph-io/badger/v3 v3.2103.5
@ -29,7 +31,7 @@ require (
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a
github.com/hrfee/jfa-go/common v0.0.0-20230626224816-f72960635dc3 github.com/hrfee/jfa-go/common v0.0.0-20240728190513-dabef831d769
github.com/hrfee/jfa-go/docs v0.0.0-20230626224816-f72960635dc3 github.com/hrfee/jfa-go/docs v0.0.0-20230626224816-f72960635dc3
github.com/hrfee/jfa-go/easyproxy v0.0.0-00010101000000-000000000000 github.com/hrfee/jfa-go/easyproxy v0.0.0-00010101000000-000000000000
github.com/hrfee/jfa-go/linecache v0.0.0-20230626224816-f72960635dc3 github.com/hrfee/jfa-go/linecache v0.0.0-20230626224816-f72960635dc3
@ -88,6 +90,7 @@ require (
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect
github.com/hrfee/jfa-go/jellyseerr v0.0.0-00010101000000-000000000000 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.6 // indirect github.com/klauspost/compress v1.16.6 // indirect

View File

@ -10,6 +10,7 @@
window.discordEnabled = {{ .discordEnabled }}; window.discordEnabled = {{ .discordEnabled }};
window.matrixEnabled = {{ .matrixEnabled }}; window.matrixEnabled = {{ .matrixEnabled }};
window.ombiEnabled = {{ .ombiEnabled }}; window.ombiEnabled = {{ .ombiEnabled }};
window.jellyseerrEnabled = {{ .jellyseerrEnabled }};
window.usernameEnabled = {{ .username }}; window.usernameEnabled = {{ .username }};
window.langFile = JSON.parse({{ .language }}); window.langFile = JSON.parse({{ .language }});
window.linkResetEnabled = {{ .linkResetEnabled }}; window.linkResetEnabled = {{ .linkResetEnabled }};
@ -396,6 +397,19 @@
</label> </label>
</form> </form>
</div> </div>
<div id="modal-jellyseerr-profile" class="modal">
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-jellyseerr-defaults" href="">
<span class="heading">{{ .strings.jellyseerrProfile }} <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.jellyseerrUserDefaultsDescription }}</p>
<div class="select ~neutral @low mb-4">
<select></select>
</div>
<label>
<input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.submit }}</span>
</label>
</form>
</div>
<div id="modal-user-profiles" class="modal"> <div id="modal-user-profiles" class="modal">
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-2/3 content card"> <div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-2/3 content card">
<span class="heading">{{ .strings.userProfiles }} <span class="modal-close">&times;</span></span> <span class="heading">{{ .strings.userProfiles }} <span class="modal-close">&times;</span></span>
@ -409,6 +423,9 @@
{{ if .ombiEnabled }} {{ if .ombiEnabled }}
<th>Ombi</th> <th>Ombi</th>
{{ end }} {{ end }}
{{ if .jellyseerrEnabled }}
<th>Jellyseerr</th>
{{ end }}
{{ if .referralsEnabled }} {{ if .referralsEnabled }}
<th>{{ .strings.referrals }}</th> <th>{{ .strings.referrals }}</th>
{{ end }} {{ end }}

View File

@ -21,13 +21,14 @@ const (
// Jellyseerr represents a running Jellyseerr instance. // Jellyseerr represents a running Jellyseerr instance.
type Jellyseerr struct { type Jellyseerr struct {
server, key string server, key string
header map[string]string header map[string]string
httpClient *http.Client httpClient *http.Client
userCache map[string]User // Map of jellyfin IDs to users userCache map[string]User // Map of jellyfin IDs to users
cacheExpiry time.Time cacheExpiry time.Time
cacheLength time.Duration cacheLength time.Duration
timeoutHandler common.TimeoutHandler timeoutHandler common.TimeoutHandler
LogRequestBodies bool
} }
// NewJellyseerr returns an Ombi object. // NewJellyseerr returns an Ombi object.
@ -44,10 +45,11 @@ func NewJellyseerr(server, key string, timeoutHandler common.TimeoutHandler) *Je
header: map[string]string{ header: map[string]string{
"X-Api-Key": key, "X-Api-Key": key,
}, },
cacheLength: time.Duration(30) * time.Minute, cacheLength: time.Duration(30) * time.Minute,
cacheExpiry: time.Now(), cacheExpiry: time.Now(),
timeoutHandler: timeoutHandler, timeoutHandler: timeoutHandler,
userCache: map[string]User{}, userCache: map[string]User{},
LogRequestBodies: false,
} }
} }
@ -59,6 +61,9 @@ func (js *Jellyseerr) getJSON(url string, params map[string]string, queryParams
var req *http.Request var req *http.Request
if params != nil { if params != nil {
jsonParams, _ := json.Marshal(params) jsonParams, _ := json.Marshal(params)
if js.LogRequestBodies {
fmt.Printf("Jellyseerr API Client: Sending Data \"%s\"\n", string(jsonParams))
}
req, _ = http.NewRequest("GET", url+"?"+queryParams.Encode(), bytes.NewBuffer(jsonParams)) req, _ = http.NewRequest("GET", url+"?"+queryParams.Encode(), bytes.NewBuffer(jsonParams))
} else { } else {
req, _ = http.NewRequest("GET", url+"?"+queryParams.Encode(), nil) req, _ = http.NewRequest("GET", url+"?"+queryParams.Encode(), nil)
@ -94,6 +99,9 @@ func (js *Jellyseerr) getJSON(url string, params map[string]string, queryParams
func (js *Jellyseerr) send(mode string, url string, data any, response bool, headers map[string]string) (string, int, error) { func (js *Jellyseerr) send(mode string, url string, data any, response bool, headers map[string]string) (string, int, error) {
responseText := "" responseText := ""
params, _ := json.Marshal(data) params, _ := json.Marshal(data)
if js.LogRequestBodies {
fmt.Printf("Jellyseerr API Client: Sending Data \"%s\"\n", string(params))
}
req, _ := http.NewRequest(mode, url, bytes.NewBuffer(params)) req, _ := http.NewRequest(mode, url, bytes.NewBuffer(params))
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
for name, value := range js.header { for name, value := range js.header {
@ -291,7 +299,7 @@ func (js *Jellyseerr) ApplyTemplateToUser(jfID string, tmpl UserTemplate) error
return nil return nil
} }
func (js *Jellyseerr) ModifyUser(jfID string, conf map[UserField]string) error { func (js *Jellyseerr) ModifyUser(jfID string, conf map[UserField]any) error {
u, err := js.MustGetUser(jfID) u, err := js.MustGetUser(jfID)
if err != nil { if err != nil {
return err return err
@ -327,13 +335,16 @@ func (js *Jellyseerr) DeleteUser(jfID string) error {
} }
func (js *Jellyseerr) GetNotificationPreferences(jfID string) (Notifications, error) { func (js *Jellyseerr) GetNotificationPreferences(jfID string) (Notifications, error) {
var data Notifications
u, err := js.MustGetUser(jfID) u, err := js.MustGetUser(jfID)
if err != nil { if err != nil {
return data, err return Notifications{}, err
} }
return js.GetNotificationPreferencesByID(u.ID)
}
resp, status, err := js.getJSON(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), nil, url.Values{}) func (js *Jellyseerr) GetNotificationPreferencesByID(jellyseerrID int64) (Notifications, error) {
var data Notifications
resp, status, err := js.getJSON(fmt.Sprintf(js.server+"/user/%d/settings/notifications", jellyseerrID), nil, url.Values{})
if err != nil { if err != nil {
return data, err return data, err
} }
@ -360,7 +371,7 @@ func (js *Jellyseerr) ApplyNotificationsTemplateToUser(jfID string, tmpl Notific
return nil return nil
} }
func (js *Jellyseerr) ModifyNotifications(jfID string, conf map[NotificationsField]string) error { func (js *Jellyseerr) ModifyNotifications(jfID string, conf map[NotificationsField]any) error {
u, err := js.MustGetUser(jfID) u, err := js.MustGetUser(jfID)
if err != nil { if err != nil {
return err return err
@ -375,3 +386,21 @@ func (js *Jellyseerr) ModifyNotifications(jfID string, conf map[NotificationsFie
} }
return nil return nil
} }
func (js *Jellyseerr) GetUsers() (map[string]User, error) {
err := js.getUsers()
return js.userCache, err
}
func (js *Jellyseerr) UserByID(jellyseerrID int64) (User, error) {
resp, status, err := js.getJSON(js.server+fmt.Sprintf("/user/%d", jellyseerrID), nil, url.Values{})
var data User
if status != 200 {
return data, fmt.Errorf("failed (error %d)", status)
}
if err != nil {
return data, err
}
err = json.Unmarshal([]byte(resp), &data)
return data, err
}

View File

@ -11,61 +11,74 @@ const (
type User struct { type User struct {
UserTemplate // Note: You can set this with User.UserTemplate = value. UserTemplate // Note: You can set this with User.UserTemplate = value.
Warnings []any `json:"warnings"` UserType int64 `json:"userType,omitempty"`
ID int `json:"id"` Warnings []any `json:"warnings,omitempty"`
Email string `json:"email"` ID int64 `json:"id,omitempty"`
PlexUsername string `json:"plexUsername"` Email string `json:"email,omitempty"`
JellyfinUsername string `json:"jellyfinUsername"` PlexUsername string `json:"plexUsername,omitempty"`
Username string `json:"username"` JellyfinUsername string `json:"jellyfinUsername,omitempty"`
RecoveryLinkExpirationDate any `json:"recoveryLinkExpirationDate"` Username string `json:"username,omitempty"`
PlexID string `json:"plexId"` RecoveryLinkExpirationDate any `json:"recoveryLinkExpirationDate,omitempty"`
JellyfinUserID string `json:"jellyfinUserId"` PlexID string `json:"plexId,omitempty"`
JellyfinDeviceID string `json:"jellyfinDeviceId"` JellyfinUserID string `json:"jellyfinUserId,omitempty"`
JellyfinAuthToken string `json:"jellyfinAuthToken"` JellyfinDeviceID string `json:"jellyfinDeviceId,omitempty"`
PlexToken string `json:"plexToken"` JellyfinAuthToken string `json:"jellyfinAuthToken,omitempty"`
Avatar string `json:"avatar"` PlexToken string `json:"plexToken,omitempty"`
CreatedAt time.Time `json:"createdAt"` Avatar string `json:"avatar,omitempty"`
UpdatedAt time.Time `json:"updatedAt"` CreatedAt time.Time `json:"createdAt,omitempty"`
RequestCount int `json:"requestCount"` UpdatedAt time.Time `json:"updatedAt,omitempty"`
DisplayName string `json:"displayName"` RequestCount int64 `json:"requestCount,omitempty"`
DisplayName string `json:"displayName,omitempty"`
}
func (u User) Name() string {
var n string
if u.Username != "" {
n = u.Username
} else if u.JellyfinUsername != "" {
n = u.JellyfinUsername
}
if u.DisplayName != "" {
n += " (" + u.DisplayName + ")"
}
return n
} }
type UserTemplate struct { type UserTemplate struct {
Permissions Permissions `json:"permissions"` Permissions Permissions `json:"permissions,omitempty"`
UserType int `json:"userType"` MovieQuotaLimit any `json:"movieQuotaLimit,omitempty"`
MovieQuotaLimit any `json:"movieQuotaLimit"` MovieQuotaDays any `json:"movieQuotaDays,omitempty"`
MovieQuotaDays any `json:"movieQuotaDays"` TvQuotaLimit any `json:"tvQuotaLimit,omitempty"`
TvQuotaLimit any `json:"tvQuotaLimit"` TvQuotaDays any `json:"tvQuotaDays,omitempty"`
TvQuotaDays any `json:"tvQuotaDays"`
} }
type PageInfo struct { type PageInfo struct {
Pages int `json:"pages"` Pages int `json:"pages,omitempty"`
PageSize int `json:"pageSize"` PageSize int `json:"pageSize,omitempty"`
Results int `json:"results"` Results int `json:"results,omitempty"`
Page int `json:"page"` Page int `json:"page,omitempty"`
} }
type GetUsersDTO struct { type GetUsersDTO struct {
Page PageInfo `json:"pageInfo"` Page PageInfo `json:"pageInfo,omitempty"`
Results []User `json:"results"` Results []User `json:"results,omitempty"`
} }
type permissionsDTO struct { type permissionsDTO struct {
Permissions Permissions `json:"permissions"` Permissions Permissions `json:"permissions,omitempty"`
} }
type Permissions int type Permissions int
type NotificationTypes struct { type NotificationTypes struct {
Discord int `json:"discord"` Discord int64 `json:"discord,omitempty"`
Email int `json:"email"` Email int64 `json:"email,omitempty"`
Pushbullet int `json:"pushbullet"` Pushbullet int64 `json:"pushbullet,omitempty"`
Pushover int `json:"pushover"` Pushover int64 `json:"pushover,omitempty"`
Slack int `json:"slack"` Slack int64 `json:"slack,omitempty"`
Telegram int `json:"telegram"` Telegram int64 `json:"telegram,omitempty"`
Webhook int `json:"webhook"` Webhook int64 `json:"webhook,omitempty"`
Webpush int `json:"webpush"` Webpush int64 `json:"webpush,omitempty"`
} }
type NotificationsField string type NotificationsField string
@ -80,21 +93,21 @@ const (
type Notifications struct { type Notifications struct {
NotificationsTemplate NotificationsTemplate
PgpKey any `json:"pgpKey"` PgpKey any `json:"pgpKey,omitempty"`
DiscordID string `json:"discordId"` DiscordID string `json:"discordId,omitempty"`
PushbulletAccessToken any `json:"pushbulletAccessToken"` PushbulletAccessToken any `json:"pushbulletAccessToken,omitempty"`
PushoverApplicationToken any `json:"pushoverApplicationToken"` PushoverApplicationToken any `json:"pushoverApplicationToken,omitempty"`
PushoverUserKey any `json:"pushoverUserKey"` PushoverUserKey any `json:"pushoverUserKey,omitempty"`
TelegramChatID string `json:"telegramChatId"` TelegramChatID string `json:"telegramChatId,omitempty"`
} }
type NotificationsTemplate struct { type NotificationsTemplate struct {
EmailEnabled bool `json:"emailEnabled"` EmailEnabled bool `json:"emailEnabled,omitempty"`
DiscordEnabled bool `json:"discordEnabled"` DiscordEnabled bool `json:"discordEnabled,omitempty"`
DiscordEnabledTypes int `json:"discordEnabledTypes"` DiscordEnabledTypes int64 `json:"discordEnabledTypes,omitempty"`
PushoverSound any `json:"pushoverSound"` PushoverSound any `json:"pushoverSound,omitempty"`
TelegramEnabled bool `json:"telegramEnabled"` TelegramEnabled bool `json:"telegramEnabled,omitempty"`
TelegramSendSilently any `json:"telegramSendSilently"` TelegramSendSilently any `json:"telegramSendSilently,omitempty"`
WebPushEnabled bool `json:"webPushEnabled"` WebPushEnabled bool `json:"webPushEnabled,omitempty"`
NotifTypes NotificationTypes `json:"notificationTypes"` NotifTypes NotificationTypes `json:"notificationTypes,omitempty"`
} }

View File

@ -99,6 +99,8 @@
"settingsMaybeUnderAdvanced": "Tip: You might find what you're looking for by enabling Advanced Settings.", "settingsMaybeUnderAdvanced": "Tip: You might find what you're looking for by enabling Advanced Settings.",
"ombiProfile": "Ombi user profile", "ombiProfile": "Ombi user profile",
"ombiUserDefaultsDescription": "Create an Ombi user and configure it, then select it below. It's settings/permissions will be stored and applied to new Ombi users created by jfa-go when this profile is selected.", "ombiUserDefaultsDescription": "Create an Ombi user and configure it, then select it below. It's settings/permissions will be stored and applied to new Ombi users created by jfa-go when this profile is selected.",
"jellyseerrProfile": "Jellyseerr user profile",
"jellyseerrUserDefaultsDescription": "Create a Jellyseerr user and configure it, then select it below. It's settings/permissions will be stored and applied to new Jellyseerr users created by jfa-go when this profile is selected.",
"userProfiles": "User Profiles", "userProfiles": "User Profiles",
"userProfilesDescription": "Profiles are applied to users when they create an account. A profile includes library access rights and homescreen layout.", "userProfilesDescription": "Profiles are applied to users when they create an account. A profile includes library access rights and homescreen layout.",
"userProfilesIsDefault": "Default", "userProfilesIsDefault": "Default",
@ -208,6 +210,7 @@
"sentAnnouncement": "Announcement sent.", "sentAnnouncement": "Announcement sent.",
"savedAnnouncement": "Announcement saved.", "savedAnnouncement": "Announcement saved.",
"setOmbiProfile": "Stored ombi profile.", "setOmbiProfile": "Stored ombi profile.",
"savedProfile": "Stored profile changes.",
"updateApplied": "Update applied, please restart.", "updateApplied": "Update applied, please restart.",
"updateAppliedRefresh": "Update applied, please refresh.", "updateAppliedRefresh": "Update applied, please refresh.",
"telegramVerified": "Telegram account verified.", "telegramVerified": "Telegram account verified.",
@ -224,6 +227,7 @@
"errorDeleteProfile": "Failed to delete profile {n}", "errorDeleteProfile": "Failed to delete profile {n}",
"errorLoadProfiles": "Failed to load profiles.", "errorLoadProfiles": "Failed to load profiles.",
"errorCreateProfile": "Failed to create profile {n}", "errorCreateProfile": "Failed to create profile {n}",
"errorSavedProfile": "Failed to save profile {n}",
"errorSetDefaultProfile": "Failed to set default profile.", "errorSetDefaultProfile": "Failed to set default profile.",
"errorLoadUsers": "Failed to load users.", "errorLoadUsers": "Failed to load users.",
"errorLoadSettings": "Failed to load settings.", "errorLoadSettings": "Failed to load settings.",

13
main.go
View File

@ -25,6 +25,7 @@ import (
"github.com/hrfee/jfa-go/common" "github.com/hrfee/jfa-go/common"
_ "github.com/hrfee/jfa-go/docs" _ "github.com/hrfee/jfa-go/docs"
"github.com/hrfee/jfa-go/easyproxy" "github.com/hrfee/jfa-go/easyproxy"
"github.com/hrfee/jfa-go/jellyseerr"
"github.com/hrfee/jfa-go/logger" "github.com/hrfee/jfa-go/logger"
"github.com/hrfee/jfa-go/ombi" "github.com/hrfee/jfa-go/ombi"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
@ -101,6 +102,7 @@ type appContext struct {
jf *mediabrowser.MediaBrowser jf *mediabrowser.MediaBrowser
authJf *mediabrowser.MediaBrowser authJf *mediabrowser.MediaBrowser
ombi *ombi.Ombi ombi *ombi.Ombi
js *jellyseerr.Jellyseerr
datePattern string datePattern string
timePattern string timePattern string
storage Storage storage Storage
@ -359,6 +361,17 @@ func start(asDaemon, firstCall bool) {
} }
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
app.debug.Printf("Connecting to Jellyseerr")
jellyseerrServer := app.config.Section("jellyseerr").Key("server").String()
app.js = jellyseerr.NewJellyseerr(
jellyseerrServer,
app.config.Section("jellyseerr").Key("api_key").String(),
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
)
}
app.storage.db_path = filepath.Join(app.dataPath, "db") app.storage.db_path = filepath.Join(app.dataPath, "db")
app.loadPendingBackup() app.loadPendingBackup()
app.ConnectDB() app.ConnectDB()

View File

@ -16,6 +16,7 @@ type newUserDTO struct {
Username string `json:"username" example:"jeff" binding:"required"` // User's username Username string `json:"username" example:"jeff" binding:"required"` // User's username
Password string `json:"password" example:"guest" binding:"required"` // User's password Password string `json:"password" example:"guest" binding:"required"` // User's password
Email string `json:"email" example:"jeff@jellyf.in"` // User's email address Email string `json:"email" example:"jeff@jellyf.in"` // User's email address
EmailContact bool `json:"email_contact"` // Whether or not to use email for notifications/pwrs
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser) Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
TelegramPIN string `json:"telegram_pin" example:"A1-B2-3C"` // Telegram verification PIN (if used) TelegramPIN string `json:"telegram_pin" example:"A1-B2-3C"` // Telegram verification PIN (if used)
TelegramContact bool `json:"telegram_contact"` // Whether or not to use telegram for notifications/pwrs TelegramContact bool `json:"telegram_contact"` // Whether or not to use telegram for notifications/pwrs
@ -76,6 +77,7 @@ type profileDTO struct {
LibraryAccess string `json:"libraries" example:"all"` // Number of libraries profile has access to LibraryAccess string `json:"libraries" example:"all"` // Number of libraries profile has access to
FromUser string `json:"fromUser" example:"jeff"` // The user the profile is based on FromUser string `json:"fromUser" example:"jeff"` // The user the profile is based on
Ombi bool `json:"ombi"` // Whether or not Ombi settings are stored in this profile. Ombi bool `json:"ombi"` // Whether or not Ombi settings are stored in this profile.
Jellyseerr bool `json:"jellyseerr"` // Whether or not Jellyseerr settings are stored in this profile.
ReferralsEnabled bool `json:"referrals_enabled" example:"true"` // Whether or not the profile has referrals enabled, and has a template invite stored. ReferralsEnabled bool `json:"referrals_enabled" example:"true"` // Whether or not the profile has referrals enabled, and has a template invite stored.
} }

View File

@ -238,6 +238,11 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/users/discord/:username", app.DiscordGetUsers) api.GET(p+"/users/discord/:username", app.DiscordGetUsers)
api.POST(p+"/users/discord", app.DiscordConnect) api.POST(p+"/users/discord", app.DiscordConnect)
} }
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
api.GET(p+"/jellyseerr/users", app.JellyseerrUsers)
api.POST(p+"/profiles/jellyseerr/:profile/:id", app.SetJellyseerrProfile)
api.DELETE(p+"/profiles/jellyseerr/:profile", app.DeleteJellyseerrProfile)
}
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
api.GET(p+"/ombi/users", app.OmbiUsers) api.GET(p+"/ombi/users", app.OmbiUsers)
api.POST(p+"/profiles/ombi/:profile", app.SetOmbiProfile) api.POST(p+"/profiles/ombi/:profile", app.SetOmbiProfile)

View File

@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/hrfee/jfa-go/jellyseerr"
"github.com/hrfee/jfa-go/logger" "github.com/hrfee/jfa-go/logger"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
"github.com/timshannon/badgerhold/v4" "github.com/timshannon/badgerhold/v4"
@ -650,9 +651,16 @@ type Profile struct {
Displayprefs map[string]interface{} `json:"displayprefs,omitempty"` Displayprefs map[string]interface{} `json:"displayprefs,omitempty"`
Default bool `json:"default,omitempty"` Default bool `json:"default,omitempty"`
Ombi map[string]interface{} `json:"ombi,omitempty"` Ombi map[string]interface{} `json:"ombi,omitempty"`
Jellyseerr JellyseerrTemplate `json:"jellyseerr,omitempty"`
ReferralTemplateKey string ReferralTemplateKey string
} }
type JellyseerrTemplate struct {
Enabled bool `json:"enabled,omitempty"`
User jellyseerr.UserTemplate `json:"user,omitempty"`
Notifications jellyseerr.NotificationsTemplate `json:"notifications,omitempty"`
}
type Invite struct { type Invite struct {
Code string `badgerhold:"key"` Code string `badgerhold:"key"`
Created time.Time `json:"created"` Created time.Time `json:"created"`

View File

@ -49,6 +49,9 @@ window.availableProfiles = window.availableProfiles || [];
window.modals.ombiProfile = new Modal(document.getElementById('modal-ombi-profile')); window.modals.ombiProfile = new Modal(document.getElementById('modal-ombi-profile'));
document.getElementById('form-ombi-defaults').addEventListener('submit', window.modals.ombiProfile.close); document.getElementById('form-ombi-defaults').addEventListener('submit', window.modals.ombiProfile.close);
window.modals.jellyseerrProfile = new Modal(document.getElementById('modal-jellyseerr-profile'));
document.getElementById('form-jellyseerr-defaults').addEventListener('submit', window.modals.jellyseerrProfile.close);
window.modals.profiles = new Modal(document.getElementById("modal-user-profiles")); window.modals.profiles = new Modal(document.getElementById("modal-user-profiles"));

View File

@ -224,6 +224,7 @@ if (window.emailRequired) {
interface sendDTO { interface sendDTO {
code: string; code: string;
email: string; email: string;
email_contact?: boolean;
username: string; username: string;
password: string; password: string;
telegram_pin?: string; telegram_pin?: string;
@ -252,8 +253,9 @@ const create = (event: SubmitEvent) => {
code: window.code, code: window.code,
username: usernameField.value, username: usernameField.value,
email: emailField.value, email: emailField.value,
email_contact: true,
password: passwordField.value password: passwordField.value
}; }
if (telegramVerified) { if (telegramVerified) {
send.telegram_pin = window.telegramPIN; send.telegram_pin = window.telegramPIN;
const checkbox = document.getElementById("contact-via-telegram") as HTMLInputElement; const checkbox = document.getElementById("contact-via-telegram") as HTMLInputElement;
@ -275,6 +277,10 @@ const create = (event: SubmitEvent) => {
send.matrix_contact = true; send.matrix_contact = true;
} }
} }
if (matrixVerified || discordVerified || telegramVerified) {
const checkbox = document.getElementById("contact-via-email") as HTMLInputElement;
send.email_contact = checkbox.checked;
}
if (window.captcha) { if (window.captcha) {
if (window.reCAPTCHA) { if (window.reCAPTCHA) {
send.captcha_text = grecaptcha.getResponse(); send.captcha_text = grecaptcha.getResponse();

View File

@ -5,6 +5,7 @@ interface Profile {
libraries: string; libraries: string;
fromUser: string; fromUser: string;
ombi: boolean; ombi: boolean;
jellyseerr: boolean;
referrals_enabled: boolean; referrals_enabled: boolean;
} }
@ -14,9 +15,11 @@ class profile implements Profile {
private _adminChip: HTMLSpanElement; private _adminChip: HTMLSpanElement;
private _libraries: HTMLTableDataCellElement; private _libraries: HTMLTableDataCellElement;
private _ombiButton: HTMLSpanElement; private _ombiButton: HTMLSpanElement;
private _ombi: boolean;
private _jellyseerrButton: HTMLSpanElement;
private _jellyseerr: boolean;
private _fromUser: HTMLTableDataCellElement; private _fromUser: HTMLTableDataCellElement;
private _defaultRadio: HTMLInputElement; private _defaultRadio: HTMLInputElement;
private _ombi: boolean;
private _referralsButton: HTMLSpanElement; private _referralsButton: HTMLSpanElement;
private _referralsEnabled: boolean; private _referralsEnabled: boolean;
@ -51,6 +54,21 @@ class profile implements Profile {
this._ombiButton.classList.remove("~critical"); this._ombiButton.classList.remove("~critical");
} }
} }
get jellyseerr(): boolean { return this._jellyseerr; }
set jellyseerr(v: boolean) {
if (!window.jellyseerrEnabled) return;
this._jellyseerr = v;
if (v) {
this._jellyseerrButton.textContent = window.lang.strings("delete");
this._jellyseerrButton.classList.add("~critical");
this._jellyseerrButton.classList.remove("~neutral");
} else {
this._jellyseerrButton.textContent = window.lang.strings("add");
this._jellyseerrButton.classList.add("~neutral");
this._jellyseerrButton.classList.remove("~critical");
}
}
get fromUser(): string { return this._fromUser.textContent; } get fromUser(): string { return this._fromUser.textContent; }
set fromUser(v: string) { this._fromUser.textContent = v; } set fromUser(v: string) { this._fromUser.textContent = v; }
@ -82,6 +100,9 @@ class profile implements Profile {
if (window.ombiEnabled) innerHTML += ` if (window.ombiEnabled) innerHTML += `
<td><span class="button @low profile-ombi"></span></td> <td><span class="button @low profile-ombi"></span></td>
`; `;
if (window.jellyseerrEnabled) innerHTML += `
<td><span class="button @low profile-jellyseerr"></span></td>
`;
if (window.referralsEnabled) innerHTML += ` if (window.referralsEnabled) innerHTML += `
<td><span class="button @low profile-referrals"></span></td> <td><span class="button @low profile-referrals"></span></td>
`; `;
@ -96,6 +117,8 @@ class profile implements Profile {
this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement; this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement;
if (window.ombiEnabled) if (window.ombiEnabled)
this._ombiButton = this._row.querySelector("span.profile-ombi") as HTMLSpanElement; this._ombiButton = this._row.querySelector("span.profile-ombi") as HTMLSpanElement;
if (window.jellyseerrEnabled)
this._jellyseerrButton = this._row.querySelector("span.profile-jellyseerr") as HTMLSpanElement;
if (window.referralsEnabled) if (window.referralsEnabled)
this._referralsButton = this._row.querySelector("span.profile-referrals") as HTMLSpanElement; this._referralsButton = this._row.querySelector("span.profile-referrals") as HTMLSpanElement;
this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement; this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement;
@ -112,10 +135,12 @@ class profile implements Profile {
this.fromUser = p.fromUser; this.fromUser = p.fromUser;
this.libraries = p.libraries; this.libraries = p.libraries;
this.ombi = p.ombi; this.ombi = p.ombi;
this.jellyseerr = p.jellyseerr;
this.referrals_enabled = p.referrals_enabled; this.referrals_enabled = p.referrals_enabled;
} }
setOmbiFunc = (ombiFunc: (ombi: boolean) => void) => { this._ombiButton.onclick = () => ombiFunc(this._ombi); } setOmbiFunc = (ombiFunc: (ombi: boolean) => void) => { this._ombiButton.onclick = () => ombiFunc(this._ombi); }
setJellyseerrFunc = (jellyseerrFunc: (jellyseerr: boolean) => void) => { this._jellyseerrButton.onclick = () => jellyseerrFunc(this._jellyseerr); }
setReferralFunc = (referralFunc: (enabled: boolean) => void) => { this._referralsButton.onclick = () => referralFunc(this._referralsEnabled); } setReferralFunc = (referralFunc: (enabled: boolean) => void) => { this._referralsButton.onclick = () => referralFunc(this._referralsEnabled); }
remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); } remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); }
@ -144,6 +169,7 @@ export class ProfileEditor {
private _profiles: { [name: string]: profile } = {}; private _profiles: { [name: string]: profile } = {};
private _default: string; private _default: string;
private _ombiProfiles: ombiProfiles; private _ombiProfiles: ombiProfiles;
private _jellyseerrProfiles: jellyseerrProfiles;
private _createForm = document.getElementById("form-add-profile") as HTMLFormElement; private _createForm = document.getElementById("form-add-profile") as HTMLFormElement;
private _profileName = document.getElementById("add-profile-name") as HTMLInputElement; private _profileName = document.getElementById("add-profile-name") as HTMLInputElement;
@ -181,7 +207,7 @@ export class ProfileEditor {
this._profiles[name].update(name, resp.profiles[name]); this._profiles[name].update(name, resp.profiles[name]);
} else { } else {
this._profiles[name] = new profile(name, resp.profiles[name]); this._profiles[name] = new profile(name, resp.profiles[name]);
if (window.ombiEnabled) if (window.ombiEnabled) {
this._profiles[name].setOmbiFunc((ombi: boolean) => { this._profiles[name].setOmbiFunc((ombi: boolean) => {
if (ombi) { if (ombi) {
this._ombiProfiles.delete(name, (req: XMLHttpRequest) => { this._ombiProfiles.delete(name, (req: XMLHttpRequest) => {
@ -198,7 +224,26 @@ export class ProfileEditor {
this._ombiProfiles.load(name); this._ombiProfiles.load(name);
} }
}); });
if (window.referralsEnabled) }
if (window.jellyseerrEnabled) {
this._profiles[name].setJellyseerrFunc((jellyseerr: boolean) => {
if (jellyseerr) {
this._jellyseerrProfiles.delete(name, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 204) {
window.notifications.customError("errorDeleteJellyseerr", window.lang.notif("errorUnknown"));
return;
}
this._profiles[name].jellyseerr = false;
}
});
} else {
window.modals.profiles.close();
this._jellyseerrProfiles.load(name);
}
});
}
if (window.referralsEnabled) {
this._profiles[name].setReferralFunc((enabled: boolean) => { this._profiles[name].setReferralFunc((enabled: boolean) => {
if (enabled) { if (enabled) {
this.disableReferrals(name); this.disableReferrals(name);
@ -206,6 +251,7 @@ export class ProfileEditor {
this.enableReferrals(name); this.enableReferrals(name);
} }
}); });
}
this._table.appendChild(this._profiles[name].asElement()); this._table.appendChild(this._profiles[name].asElement());
} }
} }
@ -299,6 +345,8 @@ export class ProfileEditor {
if (window.ombiEnabled) if (window.ombiEnabled)
this._ombiProfiles = new ombiProfiles(); this._ombiProfiles = new ombiProfiles();
if (window.jellyseerrEnabled)
this._jellyseerrProfiles = new jellyseerrProfiles();
this._createButton.onclick = () => _get("/users", null, (req: XMLHttpRequest) => { this._createButton.onclick = () => _get("/users", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
@ -366,7 +414,7 @@ export class ombiProfiles {
let resp = {} as ombiUser; let resp = {} as ombiUser;
resp.id = this._select.value; resp.id = this._select.value;
resp.name = this._users[resp.id]; resp.name = this._users[resp.id];
_post("/profiles/ombi/" + this._currentProfile, resp, (req: XMLHttpRequest) => { _post("/profiles/ombi/" + encodeURIComponent(encodeURIComponent(this._currentProfile)), resp, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
toggleLoader(button); toggleLoader(button);
if (req.status == 200 || req.status == 204) { if (req.status == 200 || req.status == 204) {
@ -379,7 +427,7 @@ export class ombiProfiles {
}); });
} }
delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/ombi/" + profile, null, post); delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/ombi/" + encodeURIComponent(encodeURIComponent(profile)), null, post);
load = (profile: string) => { load = (profile: string) => {
this._currentProfile = profile; this._currentProfile = profile;
@ -401,3 +449,54 @@ export class ombiProfiles {
}); });
} }
} }
export class jellyseerrProfiles {
private _form: HTMLFormElement;
private _select: HTMLSelectElement;
private _users: { [id: string]: string } = {};
private _currentProfile: string;
constructor() {
this._form = document.getElementById("form-jellyseerr-defaults") as HTMLFormElement;
this._form.onsubmit = this.send;
this._select = this._form.querySelector("select") as HTMLSelectElement;
}
send = () => {
const button = this._form.querySelector("span.submit") as HTMLSpanElement;
toggleLoader(button);
let encodedProfile = encodeURIComponent(encodeURIComponent(this._currentProfile));
_post("/profiles/jellyseerr/" + encodedProfile + "/" + this._select.value, null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
toggleLoader(button);
if (req.status == 200 || req.status == 204) {
window.notifications.customSuccess("jellyseerrDefaults", window.lang.notif("savedProfile"));
} else {
window.notifications.customError("jellyseerrDefaults", window.lang.notif("errorSavedProfile"));
}
window.modals.jellyseerrProfile.close();
}
});
}
delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/jellyseerr/" + encodeURIComponent(encodeURIComponent(profile)), null, post);
load = (profile: string) => {
this._currentProfile = profile;
_get("/jellyseerr/users", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 200 && "users" in req.response) {
const users = req.response["users"] as ombiUser[];
let innerHTML = "";
for (let user of users) {
this._users[user.id] = user.name;
innerHTML += `<option value="${user.id}">${user.name}</option>`;
}
this._select.innerHTML = innerHTML;
window.modals.jellyseerrProfile.show();
} else {
window.notifications.customError("jellyseerrLoadError", window.lang.notif("errorLoadUsers"))
}
}
});
}
}

View File

@ -24,6 +24,7 @@ declare interface Window {
discordEnabled: boolean; discordEnabled: boolean;
matrixEnabled: boolean; matrixEnabled: boolean;
ombiEnabled: boolean; ombiEnabled: boolean;
jellyseerrEnabled: boolean;
usernameEnabled: boolean; usernameEnabled: boolean;
linkResetEnabled: boolean; linkResetEnabled: boolean;
token: string; token: string;
@ -101,6 +102,7 @@ declare interface Modals {
settingsRestart: Modal; settingsRestart: Modal;
settingsRefresh: Modal; settingsRefresh: Modal;
ombiProfile?: Modal; ombiProfile?: Modal;
jellyseerrProfile?: Modal;
profiles: Modal; profiles: Modal;
addProfile: Modal; addProfile: Modal;
announce: Modal; announce: Modal;

View File

@ -133,6 +133,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool() emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool() notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
jellyseerrEnabled := app.config.Section("jellyseerr").Key("enabled").MustBool(false)
jfAdminOnly := app.config.Section("ui").Key("admin_only").MustBool(true) jfAdminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
jfAllowAll := app.config.Section("ui").Key("allow_all").MustBool(false) jfAllowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
var license string var license string
@ -155,34 +156,35 @@ func (app *appContext) AdminPage(gc *gin.Context) {
} }
gcHTML(gc, http.StatusOK, "admin.html", gin.H{ gcHTML(gc, http.StatusOK, "admin.html", gin.H{
"urlBase": app.getURLBase(gc), "urlBase": app.getURLBase(gc),
"cssClass": app.cssClass, "cssClass": app.cssClass,
"cssVersion": cssVersion, "cssVersion": cssVersion,
"contactMessage": "", "contactMessage": "",
"emailEnabled": emailEnabled, "emailEnabled": emailEnabled,
"telegramEnabled": telegramEnabled, "telegramEnabled": telegramEnabled,
"discordEnabled": discordEnabled, "discordEnabled": discordEnabled,
"matrixEnabled": matrixEnabled, "matrixEnabled": matrixEnabled,
"ombiEnabled": ombiEnabled, "ombiEnabled": ombiEnabled,
"linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false), "jellyseerrEnabled": jellyseerrEnabled,
"notifications": notificationsEnabled, "linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false),
"version": version, "notifications": notificationsEnabled,
"commit": commit, "version": version,
"buildTime": buildTime, "commit": commit,
"builtBy": builtBy, "buildTime": buildTime,
"username": !app.config.Section("email").Key("no_username").MustBool(false), "builtBy": builtBy,
"strings": app.storage.lang.Admin[lang].Strings, "username": !app.config.Section("email").Key("no_username").MustBool(false),
"quantityStrings": app.storage.lang.Admin[lang].QuantityStrings, "strings": app.storage.lang.Admin[lang].Strings,
"language": app.storage.lang.Admin[lang].JSON, "quantityStrings": app.storage.lang.Admin[lang].QuantityStrings,
"langName": lang, "language": app.storage.lang.Admin[lang].JSON,
"license": license, "langName": lang,
"jellyfinLogin": app.jellyfinLogin, "license": license,
"jfAdminOnly": jfAdminOnly, "jellyfinLogin": app.jellyfinLogin,
"jfAllowAll": jfAllowAll, "jfAdminOnly": jfAdminOnly,
"userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false), "jfAllowAll": jfAllowAll,
"showUserPageLink": app.config.Section("user_page").Key("show_link").MustBool(true), "userPageEnabled": app.config.Section("user_page").Key("enabled").MustBool(false),
"referralsEnabled": app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false), "showUserPageLink": app.config.Section("user_page").Key("show_link").MustBool(true),
"loginAppearance": app.config.Section("ui").Key("login_appearance").MustString("clear"), "referralsEnabled": app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false),
"loginAppearance": app.config.Section("ui").Key("login_appearance").MustString("clear"),
}) })
} }
@ -192,6 +194,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool() emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool() notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
jellyseerrEnabled := app.config.Section("jellyseerr").Key("enabled").MustBool(false)
data := gin.H{ data := gin.H{
"urlBase": app.getURLBase(gc), "urlBase": app.getURLBase(gc),
"cssClass": app.cssClass, "cssClass": app.cssClass,
@ -203,6 +206,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
"discordEnabled": discordEnabled, "discordEnabled": discordEnabled,
"matrixEnabled": matrixEnabled, "matrixEnabled": matrixEnabled,
"ombiEnabled": ombiEnabled, "ombiEnabled": ombiEnabled,
"jellyseerrEnabled": jellyseerrEnabled,
"pwrEnabled": app.config.Section("password_resets").Key("enabled").MustBool(false), "pwrEnabled": app.config.Section("password_resets").Key("enabled").MustBool(false),
"linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false), "linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false),
"notifications": notificationsEnabled, "notifications": notificationsEnabled,
@ -278,6 +282,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
"strings": app.storage.lang.PasswordReset[lang].Strings, "strings": app.storage.lang.PasswordReset[lang].Strings,
"success": false, "success": false,
"ombiEnabled": app.config.Section("ombi").Key("enabled").MustBool(false), "ombiEnabled": app.config.Section("ombi").Key("enabled").MustBool(false),
"jellyseerrEnabled": app.config.Section("jellyseerr").Key("enabled").MustBool(false),
"customSuccessCard": false, "customSuccessCard": false,
} }
pwr, isInternal := app.internalPWRs[pin] pwr, isInternal := app.internalPWRs[pin]