mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-11-10 04:10:10 +00:00
jellyseerr: add option to auto-import users
"import_existing" option in settings enables an every 5-minute daemon which loops through users and imports them to Jellyseerr and copies contact info, if necessary. Also sets new API client flag AutoImportUsers, which decides whether to automatically import non-existent users in it's various methods. also cleaned up the various daemons in the software, most now using the GenericDaemon struct and just providing a new constructor. broken page loop in jellyseerr client also fixed.
This commit is contained in:
parent
2a6937228c
commit
1fa340f096
@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -529,7 +528,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
|
|||||||
}
|
}
|
||||||
if telegramVerified {
|
if telegramVerified {
|
||||||
u, _ := app.storage.GetTelegramKey(user.ID)
|
u, _ := app.storage.GetTelegramKey(user.ID)
|
||||||
contactMethods[jellyseerr.FieldTelegram] = strconv.FormatInt(u.ChatID, 10)
|
contactMethods[jellyseerr.FieldTelegram] = u.ChatID
|
||||||
contactMethods[jellyseerr.FieldTelegramEnabled] = req.TelegramContact
|
contactMethods[jellyseerr.FieldTelegramEnabled] = req.TelegramContact
|
||||||
}
|
}
|
||||||
if emailEnabled || discordVerified || telegramVerified {
|
if emailEnabled || discordVerified || telegramVerified {
|
||||||
|
15
backups.go
15
backups.go
@ -161,20 +161,13 @@ func (app *appContext) loadPendingBackup() {
|
|||||||
LOADBAK = ""
|
LOADBAK = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBackupDaemon(app *appContext) *housekeepingDaemon {
|
func newBackupDaemon(app *appContext) *GenericDaemon {
|
||||||
interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
|
interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
|
||||||
daemon := housekeepingDaemon{
|
d := NewGenericDaemon(interval, app,
|
||||||
Stopped: false,
|
|
||||||
ShutdownChannel: make(chan string),
|
|
||||||
Interval: interval,
|
|
||||||
period: interval,
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
daemon.jobs = []func(app *appContext){
|
|
||||||
func(app *appContext) {
|
func(app *appContext) {
|
||||||
app.debug.Println("Backups: Creating backup")
|
app.debug.Println("Backups: Creating backup")
|
||||||
app.makeBackup()
|
app.makeBackup()
|
||||||
},
|
},
|
||||||
}
|
)
|
||||||
return &daemon
|
return d
|
||||||
}
|
}
|
||||||
|
@ -1620,6 +1620,15 @@
|
|||||||
"value": "",
|
"value": "",
|
||||||
"depends_true": "enabled",
|
"depends_true": "enabled",
|
||||||
"description": "API Key. Get this from the first tab in Jellyseerr's settings."
|
"description": "API Key. Get this from the first tab in Jellyseerr's settings."
|
||||||
|
},
|
||||||
|
"import_existing": {
|
||||||
|
"name": "Import existing users to Jellyseerr",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": true,
|
||||||
|
"type": "bool",
|
||||||
|
"value": false,
|
||||||
|
"depends_true": "enabled",
|
||||||
|
"description": "Existing users (and those created outside jfa-go) will have their contact info imported to Jellyseerr."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
69
daemon.go
69
daemon.go
@ -116,32 +116,16 @@ func (app *appContext) clearActivities() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
|
func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaemon {
|
||||||
|
d := NewGenericDaemon(interval, app,
|
||||||
type housekeepingDaemon struct {
|
|
||||||
Stopped bool
|
|
||||||
ShutdownChannel chan string
|
|
||||||
Interval time.Duration
|
|
||||||
period time.Duration
|
|
||||||
jobs []func(app *appContext)
|
|
||||||
app *appContext
|
|
||||||
}
|
|
||||||
|
|
||||||
func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemon {
|
|
||||||
daemon := housekeepingDaemon{
|
|
||||||
Stopped: false,
|
|
||||||
ShutdownChannel: make(chan string),
|
|
||||||
Interval: interval,
|
|
||||||
period: interval,
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
daemon.jobs = []func(app *appContext){
|
|
||||||
func(app *appContext) {
|
func(app *appContext) {
|
||||||
app.debug.Println("Housekeeping: Checking for expired invites")
|
app.debug.Println("Housekeeping: Checking for expired invites")
|
||||||
app.checkInvites()
|
app.checkInvites()
|
||||||
},
|
},
|
||||||
func(app *appContext) { app.clearActivities() },
|
func(app *appContext) { app.clearActivities() },
|
||||||
}
|
)
|
||||||
|
|
||||||
|
d.Name("Housekeeping daemon")
|
||||||
|
|
||||||
clearEmail := app.config.Section("email").Key("require_unique").MustBool(false)
|
clearEmail := app.config.Section("email").Key("require_unique").MustBool(false)
|
||||||
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false)
|
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false)
|
||||||
@ -150,53 +134,24 @@ func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemo
|
|||||||
clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
||||||
|
|
||||||
if clearEmail || clearDiscord || clearTelegram || clearMatrix {
|
if clearEmail || clearDiscord || clearTelegram || clearMatrix {
|
||||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.jf.CacheExpiry = time.Now() })
|
d.appendJobs(func(app *appContext) { app.jf.CacheExpiry = time.Now() })
|
||||||
}
|
}
|
||||||
|
|
||||||
if clearEmail {
|
if clearEmail {
|
||||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearEmails() })
|
d.appendJobs(func(app *appContext) { app.clearEmails() })
|
||||||
}
|
}
|
||||||
if clearDiscord {
|
if clearDiscord {
|
||||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearDiscord() })
|
d.appendJobs(func(app *appContext) { app.clearDiscord() })
|
||||||
}
|
}
|
||||||
if clearTelegram {
|
if clearTelegram {
|
||||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearTelegram() })
|
d.appendJobs(func(app *appContext) { app.clearTelegram() })
|
||||||
}
|
}
|
||||||
if clearMatrix {
|
if clearMatrix {
|
||||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearMatrix() })
|
d.appendJobs(func(app *appContext) { app.clearMatrix() })
|
||||||
}
|
}
|
||||||
if clearPWR {
|
if clearPWR {
|
||||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearPWRCaptchas() })
|
d.appendJobs(func(app *appContext) { app.clearPWRCaptchas() })
|
||||||
}
|
}
|
||||||
|
|
||||||
return &daemon
|
return d
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *housekeepingDaemon) run() {
|
|
||||||
rt.app.info.Println("Invite daemon started")
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-rt.ShutdownChannel:
|
|
||||||
rt.ShutdownChannel <- "Down"
|
|
||||||
return
|
|
||||||
case <-time.After(rt.period):
|
|
||||||
break
|
|
||||||
}
|
|
||||||
started := time.Now()
|
|
||||||
|
|
||||||
for _, job := range rt.jobs {
|
|
||||||
job(rt.app)
|
|
||||||
}
|
|
||||||
|
|
||||||
finished := time.Now()
|
|
||||||
duration := finished.Sub(started)
|
|
||||||
rt.period = rt.Interval - duration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *housekeepingDaemon) Shutdown() {
|
|
||||||
rt.Stopped = true
|
|
||||||
rt.ShutdownChannel <- "Down"
|
|
||||||
<-rt.ShutdownChannel
|
|
||||||
close(rt.ShutdownChannel)
|
|
||||||
}
|
}
|
||||||
|
65
genericdaemon.go
Normal file
65
genericdaemon.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
|
||||||
|
|
||||||
|
type GenericDaemon struct {
|
||||||
|
Stopped bool
|
||||||
|
ShutdownChannel chan string
|
||||||
|
Interval time.Duration
|
||||||
|
period time.Duration
|
||||||
|
jobs []func(app *appContext)
|
||||||
|
app *appContext
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GenericDaemon) appendJobs(jobs ...func(app *appContext)) {
|
||||||
|
d.jobs = append(d.jobs, jobs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGenericDaemon returns a daemon which can be given jobs that utilize appContext.
|
||||||
|
func NewGenericDaemon(interval time.Duration, app *appContext, jobs ...func(app *appContext)) *GenericDaemon {
|
||||||
|
d := GenericDaemon{
|
||||||
|
Stopped: false,
|
||||||
|
ShutdownChannel: make(chan string),
|
||||||
|
Interval: interval,
|
||||||
|
period: interval,
|
||||||
|
app: app,
|
||||||
|
name: "Generic Daemon",
|
||||||
|
}
|
||||||
|
d.jobs = jobs
|
||||||
|
return &d
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GenericDaemon) Name(name string) { d.name = name }
|
||||||
|
|
||||||
|
func (d *GenericDaemon) run() {
|
||||||
|
d.app.info.Printf("%s started", d.name)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-d.ShutdownChannel:
|
||||||
|
d.ShutdownChannel <- "Down"
|
||||||
|
return
|
||||||
|
case <-time.After(d.period):
|
||||||
|
break
|
||||||
|
}
|
||||||
|
started := time.Now()
|
||||||
|
|
||||||
|
for _, job := range d.jobs {
|
||||||
|
job(d.app)
|
||||||
|
}
|
||||||
|
|
||||||
|
finished := time.Now()
|
||||||
|
duration := finished.Sub(started)
|
||||||
|
d.period = d.Interval - duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GenericDaemon) Shutdown() {
|
||||||
|
d.Stopped = true
|
||||||
|
d.ShutdownChannel <- "Down"
|
||||||
|
<-d.ShutdownChannel
|
||||||
|
close(d.ShutdownChannel)
|
||||||
|
}
|
@ -30,6 +30,7 @@ type Jellyseerr struct {
|
|||||||
cacheLength time.Duration
|
cacheLength time.Duration
|
||||||
timeoutHandler common.TimeoutHandler
|
timeoutHandler common.TimeoutHandler
|
||||||
LogRequestBodies bool
|
LogRequestBodies bool
|
||||||
|
AutoImportUsers bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJellyseerr returns an Ombi object.
|
// NewJellyseerr returns an Ombi object.
|
||||||
@ -63,7 +64,7 @@ func (js *Jellyseerr) getJSON(url string, params map[string]string, queryParams
|
|||||||
if params != nil {
|
if params != nil {
|
||||||
jsonParams, _ := json.Marshal(params)
|
jsonParams, _ := json.Marshal(params)
|
||||||
if js.LogRequestBodies {
|
if js.LogRequestBodies {
|
||||||
fmt.Printf("Jellyseerr API Client: Sending Data \"%s\"\n", string(jsonParams))
|
fmt.Printf("Jellyseerr API Client: Sending Data \"%s\" to \"%s\"\n", string(jsonParams), url)
|
||||||
}
|
}
|
||||||
req, _ = http.NewRequest("GET", url+"?"+queryParams.Encode(), bytes.NewBuffer(jsonParams))
|
req, _ = http.NewRequest("GET", url+"?"+queryParams.Encode(), bytes.NewBuffer(jsonParams))
|
||||||
} else {
|
} else {
|
||||||
@ -101,7 +102,7 @@ func (js *Jellyseerr) send(mode string, url string, data any, response bool, hea
|
|||||||
responseText := ""
|
responseText := ""
|
||||||
params, _ := json.Marshal(data)
|
params, _ := json.Marshal(data)
|
||||||
if js.LogRequestBodies {
|
if js.LogRequestBodies {
|
||||||
fmt.Printf("Jellyseerr API Client: Sending Data \"%s\"\n", string(params))
|
fmt.Printf("Jellyseerr API Client: Sending Data \"%s\" to \"%s\"\n", string(params), url)
|
||||||
}
|
}
|
||||||
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")
|
||||||
@ -175,7 +176,7 @@ func (js *Jellyseerr) getUsers() error {
|
|||||||
pageCount := 1
|
pageCount := 1
|
||||||
pageIndex := 0
|
pageIndex := 0
|
||||||
for {
|
for {
|
||||||
res, err := js.getUserPage(0)
|
res, err := js.getUserPage(pageIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -197,8 +198,11 @@ func (js *Jellyseerr) getUsers() error {
|
|||||||
func (js *Jellyseerr) getUserPage(page int) (GetUsersDTO, error) {
|
func (js *Jellyseerr) getUserPage(page int) (GetUsersDTO, error) {
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add("take", "30")
|
params.Add("take", "30")
|
||||||
params.Add("skip", strconv.Itoa(page))
|
params.Add("skip", strconv.Itoa(page*30))
|
||||||
params.Add("sort", "created")
|
params.Add("sort", "created")
|
||||||
|
if js.LogRequestBodies {
|
||||||
|
fmt.Printf("Jellyseerr API Client: Sending with URL params \"%+v\"\n", params)
|
||||||
|
}
|
||||||
resp, status, err := js.getJSON(js.server+"/user", nil, params)
|
resp, status, err := js.getJSON(js.server+"/user", nil, params)
|
||||||
var data GetUsersDTO
|
var data GetUsersDTO
|
||||||
if status != 200 {
|
if status != 200 {
|
||||||
@ -211,25 +215,55 @@ func (js *Jellyseerr) getUserPage(page int) (GetUsersDTO, error) {
|
|||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustGetUser provides the same function as ImportFromJellyfin, but will always return the user,
|
|
||||||
// even if they already existed.
|
|
||||||
func (js *Jellyseerr) MustGetUser(jfID string) (User, error) {
|
func (js *Jellyseerr) MustGetUser(jfID string) (User, error) {
|
||||||
js.getUsers()
|
u, _, err := js.GetOrImportUser(jfID)
|
||||||
if u, ok := js.userCache[jfID]; ok {
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
users, err := js.ImportFromJellyfin(jfID)
|
|
||||||
var u User
|
|
||||||
if err != nil {
|
|
||||||
return u, err
|
return u, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetImportedUser provides the same function as ImportFromJellyfin, but will always return the user,
|
||||||
|
// even if they already existed. Also returns whether the user was imported or not,
|
||||||
|
func (js *Jellyseerr) GetOrImportUser(jfID string) (u User, imported bool, err error) {
|
||||||
|
imported = false
|
||||||
|
u, err = js.GetExistingUser(jfID)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var users []User
|
||||||
|
users, err = js.ImportFromJellyfin(jfID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if len(users) != 0 {
|
if len(users) != 0 {
|
||||||
return users[0], err
|
u = users[0]
|
||||||
|
err = nil
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if u, ok := js.userCache[jfID]; ok {
|
err = fmt.Errorf("user not found or imported")
|
||||||
return u, nil
|
return
|
||||||
}
|
}
|
||||||
return u, fmt.Errorf("user not found")
|
|
||||||
|
func (js *Jellyseerr) GetExistingUser(jfID string) (u User, err error) {
|
||||||
|
js.getUsers()
|
||||||
|
ok := false
|
||||||
|
err = nil
|
||||||
|
if u, ok = js.userCache[jfID]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
js.cacheExpiry = time.Now()
|
||||||
|
js.getUsers()
|
||||||
|
if u, ok = js.userCache[jfID]; ok {
|
||||||
|
err = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("user not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) getUser(jfID string) (User, error) {
|
||||||
|
if js.AutoImportUsers {
|
||||||
|
return js.MustGetUser(jfID)
|
||||||
|
}
|
||||||
|
return js.GetExistingUser(jfID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (js *Jellyseerr) Me() (User, error) {
|
func (js *Jellyseerr) Me() (User, error) {
|
||||||
@ -248,7 +282,7 @@ func (js *Jellyseerr) Me() (User, error) {
|
|||||||
|
|
||||||
func (js *Jellyseerr) GetPermissions(jfID string) (Permissions, error) {
|
func (js *Jellyseerr) GetPermissions(jfID string) (Permissions, error) {
|
||||||
data := permissionsDTO{Permissions: -1}
|
data := permissionsDTO{Permissions: -1}
|
||||||
u, err := js.MustGetUser(jfID)
|
u, err := js.getUser(jfID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data.Permissions, err
|
return data.Permissions, err
|
||||||
}
|
}
|
||||||
@ -265,7 +299,7 @@ func (js *Jellyseerr) GetPermissions(jfID string) (Permissions, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (js *Jellyseerr) SetPermissions(jfID string, perm Permissions) error {
|
func (js *Jellyseerr) SetPermissions(jfID string, perm Permissions) error {
|
||||||
u, err := js.MustGetUser(jfID)
|
u, err := js.getUser(jfID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -283,7 +317,7 @@ func (js *Jellyseerr) SetPermissions(jfID string, perm Permissions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (js *Jellyseerr) ApplyTemplateToUser(jfID string, tmpl UserTemplate) error {
|
func (js *Jellyseerr) ApplyTemplateToUser(jfID string, tmpl UserTemplate) error {
|
||||||
u, err := js.MustGetUser(jfID)
|
u, err := js.getUser(jfID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -304,7 +338,7 @@ func (js *Jellyseerr) ModifyUser(jfID string, conf map[UserField]any) error {
|
|||||||
if _, ok := conf[FieldEmail]; ok {
|
if _, ok := conf[FieldEmail]; ok {
|
||||||
return fmt.Errorf("email is read only, set with ModifyMainUserSettings instead")
|
return fmt.Errorf("email is read only, set with ModifyMainUserSettings instead")
|
||||||
}
|
}
|
||||||
u, err := js.MustGetUser(jfID)
|
u, err := js.getUser(jfID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -322,7 +356,7 @@ func (js *Jellyseerr) ModifyUser(jfID string, conf map[UserField]any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (js *Jellyseerr) DeleteUser(jfID string) error {
|
func (js *Jellyseerr) DeleteUser(jfID string) error {
|
||||||
u, err := js.MustGetUser(jfID)
|
u, err := js.getUser(jfID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -339,7 +373,7 @@ func (js *Jellyseerr) DeleteUser(jfID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (js *Jellyseerr) GetNotificationPreferences(jfID string) (Notifications, error) {
|
func (js *Jellyseerr) GetNotificationPreferences(jfID string) (Notifications, error) {
|
||||||
u, err := js.MustGetUser(jfID)
|
u, err := js.getUser(jfID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Notifications{}, err
|
return Notifications{}, err
|
||||||
}
|
}
|
||||||
@ -364,7 +398,7 @@ func (js *Jellyseerr) ApplyNotificationsTemplateToUser(jfID string, tmpl Notific
|
|||||||
/* if tmpl.NotifTypes.Empty() {
|
/* if tmpl.NotifTypes.Empty() {
|
||||||
tmpl.NotifTypes = nil
|
tmpl.NotifTypes = nil
|
||||||
}*/
|
}*/
|
||||||
u, err := js.MustGetUser(jfID)
|
u, err := js.getUser(jfID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -380,7 +414,7 @@ func (js *Jellyseerr) ApplyNotificationsTemplateToUser(jfID string, tmpl Notific
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (js *Jellyseerr) ModifyNotifications(jfID string, conf map[NotificationsField]any) error {
|
func (js *Jellyseerr) ModifyNotifications(jfID string, conf map[NotificationsField]any) error {
|
||||||
u, err := js.MustGetUser(jfID)
|
u, err := js.getUser(jfID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -414,12 +448,12 @@ func (js *Jellyseerr) UserByID(jellyseerrID int64) (User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (js *Jellyseerr) ModifyMainUserSettings(jfID string, conf MainUserSettings) error {
|
func (js *Jellyseerr) ModifyMainUserSettings(jfID string, conf MainUserSettings) error {
|
||||||
u, err := js.MustGetUser(jfID)
|
u, err := js.getUser(jfID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, status, err := js.put(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false)
|
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
81
jellyseerrdaemon.go
Normal file
81
jellyseerrdaemon.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hrfee/jfa-go/jellyseerr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
||||||
|
user, imported, err := app.js.GetOrImportUser(jfID)
|
||||||
|
if err != nil {
|
||||||
|
app.debug.Printf("Failed to get or trigger import for Jellyseerr (user \"%s\"): %v", jfID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if imported {
|
||||||
|
app.debug.Printf("Jellyseerr: Triggered import for Jellyfin user \"%s\" (ID %d)", jfID, user.ID)
|
||||||
|
}
|
||||||
|
notif, err := app.js.GetNotificationPreferencesByID(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
app.debug.Printf("Failed to get notification prefs for Jellyseerr (user \"%s\"): %v", jfID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contactMethods := map[jellyseerr.NotificationsField]any{}
|
||||||
|
email, ok := app.storage.GetEmailsKey(jfID)
|
||||||
|
if ok && email.Addr != "" && user.Email != email.Addr {
|
||||||
|
err = app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: email.Addr})
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("Failed to set Jellyseerr email address: %v\n", err)
|
||||||
|
} else {
|
||||||
|
contactMethods[jellyseerr.FieldEmailEnabled] = email.Contact
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if discordEnabled {
|
||||||
|
dcUser, ok := app.storage.GetDiscordKey(jfID)
|
||||||
|
if ok && dcUser.ID != "" && notif.DiscordID != dcUser.ID {
|
||||||
|
contactMethods[jellyseerr.FieldDiscord] = dcUser.ID
|
||||||
|
contactMethods[jellyseerr.FieldDiscordEnabled] = dcUser.Contact
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if telegramEnabled {
|
||||||
|
tgUser, ok := app.storage.GetTelegramKey(jfID)
|
||||||
|
chatID, _ := strconv.ParseInt(notif.TelegramChatID, 10, 64)
|
||||||
|
if ok && tgUser.ChatID != 0 && chatID != tgUser.ChatID {
|
||||||
|
u, _ := app.storage.GetTelegramKey(jfID)
|
||||||
|
contactMethods[jellyseerr.FieldTelegram] = u.ChatID
|
||||||
|
contactMethods[jellyseerr.FieldTelegramEnabled] = tgUser.Contact
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(contactMethods) != 0 {
|
||||||
|
err := app.js.ModifyNotifications(jfID, contactMethods)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) SynchronizeJellyseerrUsers() {
|
||||||
|
users, status, err := app.jf.GetUsers(false)
|
||||||
|
if err != nil || status != 200 {
|
||||||
|
app.err.Printf("Failed to get users (%d): %s", status, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// I'm sure Jellyseerr can handle it,
|
||||||
|
// but past issues with the Jellyfin db scare me from
|
||||||
|
// running these concurrently. W/e, its a bg task anyway.
|
||||||
|
for _, user := range users {
|
||||||
|
app.SynchronizeJellyseerrUser(user.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newJellyseerrDaemon(interval time.Duration, app *appContext) *GenericDaemon {
|
||||||
|
d := NewGenericDaemon(interval, app,
|
||||||
|
func(app *appContext) {
|
||||||
|
app.SynchronizeJellyseerrUsers()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
d.Name("Jellyseerr import daemon")
|
||||||
|
return d
|
||||||
|
}
|
15
main.go
15
main.go
@ -369,6 +369,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.config.Section("jellyseerr").Key("api_key").String(),
|
app.config.Section("jellyseerr").Key("api_key").String(),
|
||||||
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
|
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
|
||||||
)
|
)
|
||||||
|
app.js.AutoImportUsers = app.config.Section("jellyseerr").Key("import_existing").MustBool(false)
|
||||||
// app.js.LogRequestBodies = true
|
// app.js.LogRequestBodies = true
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -480,13 +481,21 @@ func start(asDaemon, firstCall bool) {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
invDaemon := newInviteDaemon(time.Duration(60*time.Second), app)
|
invDaemon := newHousekeepingDaemon(time.Duration(60*time.Second), app)
|
||||||
go invDaemon.run()
|
go invDaemon.run()
|
||||||
defer invDaemon.Shutdown()
|
defer invDaemon.Shutdown()
|
||||||
|
|
||||||
userDaemon := newUserDaemon(time.Duration(60*time.Second), app)
|
userDaemon := newUserDaemon(time.Duration(60*time.Second), app)
|
||||||
go userDaemon.run()
|
go userDaemon.run()
|
||||||
defer userDaemon.shutdown()
|
defer userDaemon.Shutdown()
|
||||||
|
|
||||||
|
var jellyseerrDaemon *GenericDaemon
|
||||||
|
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) && app.config.Section("jellyseerr").Key("import_existing").MustBool(false) {
|
||||||
|
jellyseerrDaemon = newJellyseerrDaemon(time.Duration(30*time.Second), app)
|
||||||
|
// jellyseerrDaemon = newJellyseerrDaemon(time.Duration(5*time.Minute), app)
|
||||||
|
go jellyseerrDaemon.run()
|
||||||
|
defer jellyseerrDaemon.Shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
if app.config.Section("password_resets").Key("enabled").MustBool(false) && serverType == mediabrowser.JellyfinServer {
|
if app.config.Section("password_resets").Key("enabled").MustBool(false) && serverType == mediabrowser.JellyfinServer {
|
||||||
go app.StartPWR()
|
go app.StartPWR()
|
||||||
@ -496,7 +505,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
go app.checkForUpdates()
|
go app.checkForUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
var backupDaemon *housekeepingDaemon
|
var backupDaemon *GenericDaemon
|
||||||
if app.config.Section("backups").Key("enabled").MustBool(false) {
|
if app.config.Section("backups").Key("enabled").MustBool(false) {
|
||||||
backupDaemon = newBackupDaemon(app)
|
backupDaemon = newBackupDaemon(app)
|
||||||
go backupDaemon.run()
|
go backupDaemon.run()
|
||||||
|
@ -7,47 +7,14 @@ import (
|
|||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type userDaemon struct {
|
func newUserDaemon(interval time.Duration, app *appContext) *GenericDaemon {
|
||||||
Stopped bool
|
d := NewGenericDaemon(interval, app,
|
||||||
ShutdownChannel chan string
|
func(app *appContext) {
|
||||||
Interval time.Duration
|
app.checkUsers()
|
||||||
period time.Duration
|
},
|
||||||
app *appContext
|
)
|
||||||
}
|
d.Name("User daemon")
|
||||||
|
return d
|
||||||
func newUserDaemon(interval time.Duration, app *appContext) *userDaemon {
|
|
||||||
return &userDaemon{
|
|
||||||
Stopped: false,
|
|
||||||
ShutdownChannel: make(chan string),
|
|
||||||
Interval: interval,
|
|
||||||
period: interval,
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *userDaemon) run() {
|
|
||||||
rt.app.info.Println("User daemon started")
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-rt.ShutdownChannel:
|
|
||||||
rt.ShutdownChannel <- "Down"
|
|
||||||
return
|
|
||||||
case <-time.After(rt.period):
|
|
||||||
break
|
|
||||||
}
|
|
||||||
started := time.Now()
|
|
||||||
rt.app.checkUsers()
|
|
||||||
finished := time.Now()
|
|
||||||
duration := finished.Sub(started)
|
|
||||||
rt.period = rt.Interval - duration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *userDaemon) shutdown() {
|
|
||||||
rt.Stopped = true
|
|
||||||
rt.ShutdownChannel <- "Down"
|
|
||||||
<-rt.ShutdownChannel
|
|
||||||
close(rt.ShutdownChannel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) checkUsers() {
|
func (app *appContext) checkUsers() {
|
||||||
|
Loading…
Reference in New Issue
Block a user