From 0ecacc60646dc411c1c39b1de4640bb648939b8d Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 28 Nov 2025 17:26:27 +0000 Subject: [PATCH] tasks: add /tasks/jellyseerr, document now live in tasks.go and have actual API documentation. /tasks/jellyseerr triggers account import. --- common/config.go | 16 +++++++++------- config/config-base.yaml | 4 ++++ jellyseerr-d.go | 1 - main.go | 36 ++++++++++++++++++----------------- router.go | 7 +++++-- tasks.go | 42 +++++++++++++++++++++++++++++++++++++++++ ts/modules/settings.ts | 15 +++++++++++---- 7 files changed, 90 insertions(+), 31 deletions(-) create mode 100644 tasks.go diff --git a/common/config.go b/common/config.go index 1a8eba5..4e5de56 100644 --- a/common/config.go +++ b/common/config.go @@ -1,13 +1,14 @@ package common type SectionMeta struct { - Name string `json:"name" yaml:"name" example:"My Section"` // friendly name of the section - Description string `json:"description" yaml:"description"` - Advanced bool `json:"advanced,omitempty" yaml:"advanced,omitempty"` - Disabled bool `json:"disabled,omitempty" yaml:"disabled,omitempty"` - DependsTrue string `json:"depends_true,omitempty" yaml:"depends_true,omitempty"` - DependsFalse string `json:"depends_false,omitempty" yaml:"depends_false,omitempty"` - WikiLink string `json:"wiki_link,omitempty" yaml:"wiki_link,omitempty"` + Name string `json:"name" yaml:"name" example:"My Section"` // friendly name of the section + Description string `json:"description" yaml:"description"` + Advanced bool `json:"advanced,omitempty" yaml:"advanced,omitempty"` + Disabled bool `json:"disabled,omitempty" yaml:"disabled,omitempty"` + DependsTrue string `json:"depends_true,omitempty" yaml:"depends_true,omitempty"` + DependsFalse string `json:"depends_false,omitempty" yaml:"depends_false,omitempty"` + WikiLink string `json:"wiki_link,omitempty" yaml:"wiki_link,omitempty"` + Aliases []string `json:"aliases,omitempty" yaml:"aliases,omitempty"` } type Option [2]string @@ -40,6 +41,7 @@ type Setting struct { Style string `json:"style,omitempty" yaml:"style,omitempty"` Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` WikiLink string `json:"wiki_link,omitempty" yaml:"wiki_link,omitempty"` + Aliases []string `json:"aliases,omitempty" yaml:"aliases,omitempty"` } type Section struct { diff --git a/config/config-base.yaml b/config/config-base.yaml index 237166f..0c96547 100644 --- a/config/config-base.yaml +++ b/config/config-base.yaml @@ -1368,6 +1368,10 @@ sections: and telegram). A template must be added to a User Profile for accounts to be created. wiki_link: https://wiki.jfa-go.com/docs/external-services/jellyseerr/ + aliases: + - Jellyseerr + - Overseerr + - Seerr settings: - setting: enabled name: Enabled diff --git a/jellyseerr-d.go b/jellyseerr-d.go index b82aaf3..78d4ff6 100644 --- a/jellyseerr-d.go +++ b/jellyseerr-d.go @@ -32,7 +32,6 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) { if strings.Contains(err.Error(), "INVALID_EMAIL") { app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, jfID, err.Error()+"\""+email.Addr+"\"") } else { - app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, jfID, err) } } else { diff --git a/main.go b/main.go index 1ae58d6..c71b6ce 100644 --- a/main.go +++ b/main.go @@ -112,19 +112,19 @@ type appContext struct { adminUsers []User invalidTokens []string // Keeping jf name because I can't think of a better one - jf *mediabrowser.MediaBrowser - authJf *mediabrowser.MediaBrowser - ombi *OmbiWrapper - js *JellyseerrWrapper - thirdPartyServices []ThirdPartyService - storage *Storage - validator Validator - email *Emailer - telegram *TelegramDaemon - discord *DiscordDaemon - matrix *MatrixDaemon - housekeepingDaemon, userDaemon *GenericDaemon - contactMethods []ContactMethodLinker + jf *mediabrowser.MediaBrowser + authJf *mediabrowser.MediaBrowser + ombi *OmbiWrapper + js *JellyseerrWrapper + thirdPartyServices []ThirdPartyService + storage *Storage + validator Validator + email *Emailer + telegram *TelegramDaemon + discord *DiscordDaemon + matrix *MatrixDaemon + housekeepingDaemon, userDaemon, jellyseerrDaemon *GenericDaemon + contactMethods []ContactMethodLinker LoggerSet host string port int @@ -516,12 +516,11 @@ func start(asDaemon, firstCall bool) { go app.userDaemon.run() defer app.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(10*time.Minute), app) - go jellyseerrDaemon.run() - defer jellyseerrDaemon.Shutdown() + app.jellyseerrDaemon = newJellyseerrDaemon(time.Duration(10*time.Minute), app) + go app.jellyseerrDaemon.run() + defer app.jellyseerrDaemon.Shutdown() } if app.config.Section("password_resets").Key("enabled").MustBool(false) && serverType == mediabrowser.JellyfinServer { @@ -762,6 +761,9 @@ func flagPassed(name string) (found bool) { // @tag.name Statistics // @tag.description Routes that expose useful info/stats. +// @tag.name Tasks +// @tag.description Manual triggers for background tasks. + func printVersion() { tray := "" if TRAY { diff --git a/router.go b/router.go index 077dee3..8779e16 100644 --- a/router.go +++ b/router.go @@ -244,8 +244,11 @@ func (app *appContext) loadRoutes(router *gin.Engine) { api.POST(p+"/config", app.ModifyConfig) api.POST(p+"/restart", app.restart) api.GET(p+"/logs", app.GetLog) - api.POST(p+"/tasks/housekeeping", func(gc *gin.Context) { app.housekeepingDaemon.Trigger(); gc.Status(http.StatusNoContent) }) - api.POST(p+"/tasks/users", func(gc *gin.Context) { app.userDaemon.Trigger(); gc.Status(http.StatusNoContent) }) + api.POST(p+"/tasks/housekeeping", app.TaskHousekeeping) + api.POST(p+"/tasks/users", app.TaskUserCleanup) + if app.config.Section("jellyseerr").Key("enabled").MustBool(false) { + api.POST(p+"/tasks/jellyseerr", app.TaskJellyseerrImport) + } api.POST(p+"/backups", app.CreateBackup) api.GET(p+"/backups/:fname", app.GetBackup) api.GET(p+"/backups", app.GetBackups) diff --git a/tasks.go b/tasks.go new file mode 100644 index 0000000..4a823c8 --- /dev/null +++ b/tasks.go @@ -0,0 +1,42 @@ +// Routes for triggering background tasks manually. +package main + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// @Summary Triggers general housekeeping tasks: Clearing expired invites, activities, unused contact details, captchas, etc. +// @Success 204 +// @Router /tasks/housekeeping [post] +// @Security Bearer +// @tags Tasks +func (app *appContext) TaskHousekeeping(gc *gin.Context) { + app.housekeepingDaemon.Trigger() + gc.Status(http.StatusNoContent) +} + +// @Summary Triggers check for account expiry. +// @Success 204 +// @Router /tasks/users [post] +// @Security Bearer +// @tags Tasks +func (app *appContext) TaskUserCleanup(gc *gin.Context) { + app.userDaemon.Trigger() + gc.Status(http.StatusNoContent) +} + +// @Summary Triggers sync of user details with Jellyseerr. Not usually needed after one run, details are synced on change anyway. +// @Success 204 +// @Router /tasks/jellyseerr [post] +// @Security Bearer +// @tags Tasks +func (app *appContext) TaskJellyseerrImport(gc *gin.Context) { + if app.jellyseerrDaemon != nil { + app.jellyseerrDaemon.Trigger() + } else { + app.SynchronizeJellyseerrUsers() + } + gc.Status(http.StatusNoContent) +} diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts index 42be952..9ece496 100644 --- a/ts/modules/settings.ts +++ b/ts/modules/settings.ts @@ -50,6 +50,7 @@ interface Meta { depends_true?: string; depends_false?: string; wiki_link?: string; + aliases?: string[]; } interface Setting { @@ -65,6 +66,7 @@ interface Setting { depends_false?: string; wiki_link?: string; deprecated?: boolean; + aliases?: string[]; asElement: () => HTMLElement; update: (s: Setting) => void; @@ -170,6 +172,8 @@ class DOMSetting { this._registerDependencies(); } + get aliases(): string[] { return this._s.aliases; } + protected _registerDependencies() { // Doesn't re-register dependencies, but that isn't important in this application if (!(this._s.depends_true || this._s.depends_false)) return; @@ -1422,6 +1426,7 @@ export class settingsList { section.section.toLowerCase().includes(query) || section.meta.name.toLowerCase().includes(query) || section.meta.description.toLowerCase().includes(query); + if (section.meta.aliases) section.meta.aliases.forEach((term: string) => matchedSection ||= term.toLowerCase().includes(query)); matchedSection &&= ((section.meta.advanced && this._advanced) || !(section.meta.advanced)); if (matchedSection) { @@ -1447,10 +1452,12 @@ export class settingsList { // element.classList.remove("-mx-2", "my-2", "p-2", "aside", "~neutral", "@low"); element.classList.add("opacity-50", "pointer-events-none"); element.setAttribute("aria-disabled", "true"); - if (setting.setting.toLowerCase().includes(query) || - setting.name.toLowerCase().includes(query) || - setting.description.toLowerCase().includes(query) || - String(setting.value).toLowerCase().includes(query)) { + let matchedSetting = setting.setting.toLowerCase().includes(query) || + setting.name.toLowerCase().includes(query) || + setting.description.toLowerCase().includes(query) || + String(setting.value).toLowerCase().includes(query); + if (setting.aliases) setting.aliases.forEach((term: string) => matchedSetting ||= term.toLowerCase().includes(query)); + if (matchedSetting) { if ((section.meta.advanced && this._advanced) || !(section.meta.advanced)) { show(); firstVisibleSection = firstVisibleSection || section.section;