From 6164aea9bad8e750dad329e110e128d50b417977 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Tue, 18 Jan 2022 18:33:25 -0800 Subject: [PATCH 01/10] Add missing contexts --- pkg/client/start.go | 27 +++++++---- pkg/notifiarr/handlers.go | 99 ++++++++++++++++++++------------------- pkg/plex/info.go | 5 +- 3 files changed, 69 insertions(+), 62 deletions(-) diff --git a/pkg/client/start.go b/pkg/client/start.go index d5a5651ef..a2bb42b6c 100644 --- a/pkg/client/start.go +++ b/pkg/client/start.go @@ -228,16 +228,7 @@ func (c *Client) loadSiteAppsConfig(clientInfo *notifiarr.ClientInfo) { //nolint // configureServices is called on startup and on reload, so be careful what goes in here. func (c *Client) configureServices(source notifiarr.EventType) *notifiarr.ClientInfo { clientInfo := c.loadSiteConfig(source) - - if c.Config.Plex.Configured() { - if info, err := c.Config.Plex.GetInfo(); err != nil { - c.Config.Plex.Name = "" - c.Errorf("=> Getting Plex Media Server info (check url and token): %v", err) - } else { - c.Config.Plex.Name = info.FriendlyName - } - } - + c.configureServicesPlex() c.website.Sighup = c.sighup c.Config.Snapshot.Validate() c.PrintStartupInfo() @@ -253,6 +244,22 @@ func (c *Client) configureServices(source notifiarr.EventType) *notifiarr.Client return clientInfo } +func (c *Client) configureServicesPlex() { + if !c.Config.Plex.Configured() { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), c.Config.Plex.Timeout.Duration) + defer cancel() + + if info, err := c.Config.Plex.GetInfo(ctx); err != nil { + c.Config.Plex.Name = "" + c.Errorf("=> Getting Plex Media Server info (check url and token): %v", err) + } else { + c.Config.Plex.Name = info.FriendlyName + } +} + // Exit stops the web server and logs our exit messages. Start() calls this. func (c *Client) Exit() error { c.StartWebServer() diff --git a/pkg/notifiarr/handlers.go b/pkg/notifiarr/handlers.go index 17cdf0ae7..53178a128 100644 --- a/pkg/notifiarr/handlers.go +++ b/pkg/notifiarr/handlers.go @@ -1,6 +1,7 @@ package notifiarr import ( + "context" "encoding/json" "fmt" "net/http" @@ -157,7 +158,7 @@ type conTest struct { // VersionHandler returns application run and build time data and application statuses: /api/version. func (c *Config) VersionHandler(r *http.Request) (int, interface{}) { output := c.Info() - output["appsStatus"] = c.appStatsForVersion() + output["appsStatus"] = c.appStatsForVersion(r.Context()) if host, err := c.GetHostInfoUID(); err != nil { output["hostError"] = err.Error() @@ -169,7 +170,7 @@ func (c *Config) VersionHandler(r *http.Request) (int, interface{}) { } // appStatsForVersion loops each app and gets the version info. -func (c *Config) appStatsForVersion() map[string]interface{} { +func (c *Config) appStatsForVersion(ctx context.Context) map[string]interface{} { var ( lid = make([]*conTest, len(c.Apps.Lidarr)) prl = make([]*conTest, len(c.Apps.Prowlarr)) @@ -180,12 +181,12 @@ func (c *Config) appStatsForVersion() map[string]interface{} { wg sync.WaitGroup ) - getPlexVersion(&wg, c.Plex, plx) - getLidarrVersion(&wg, c.Apps.Lidarr, lid) - getProwlarrVersion(&wg, c.Apps.Prowlarr, prl) - getRadarrVersion(&wg, c.Apps.Radarr, rad) - getReadarrVersion(&wg, c.Apps.Readarr, read) - getSonarrVersion(&wg, c.Apps.Sonarr, son) + getPlexVersion(ctx, &wg, c.Plex, &plx) + getLidarrVersion(ctx, &wg, c.Apps.Lidarr, lid) + getProwlarrVersion(ctx, &wg, c.Apps.Prowlarr, prl) + getRadarrVersion(ctx, &wg, c.Apps.Radarr, rad) + getReadarrVersion(ctx, &wg, c.Apps.Readarr, read) + getSonarrVersion(ctx, &wg, c.Apps.Sonarr, son) wg.Wait() return map[string]interface{}{ @@ -198,100 +199,102 @@ func (c *Config) appStatsForVersion() map[string]interface{} { } } -func getLidarrVersion(wait *sync.WaitGroup, lidarrs []*apps.LidarrConfig, lid []*conTest) { +func getLidarrVersion(ctx context.Context, wait *sync.WaitGroup, lidarrs []*apps.LidarrConfig, lid []*conTest) { for idx, app := range lidarrs { wait.Add(1) go func(idx int, app *apps.LidarrConfig) { defer wait.Done() - stat, err := app.GetSystemStatus() + stat, err := app.GetSystemStatusContext(ctx) lid[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} }(idx, app) } } -func getProwlarrVersion(wait *sync.WaitGroup, prowlarrs []*apps.ProwlarrConfig, prl []*conTest) { +func getProwlarrVersion(ctx context.Context, wait *sync.WaitGroup, prowlarrs []*apps.ProwlarrConfig, prl []*conTest) { for idx, app := range prowlarrs { wait.Add(1) go func(idx int, app *apps.ProwlarrConfig) { defer wait.Done() - stat, err := app.GetSystemStatus() + stat, err := app.GetSystemStatusContext(ctx) prl[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} }(idx, app) } } -func getRadarrVersion(wait *sync.WaitGroup, radarrs []*apps.RadarrConfig, rad []*conTest) { +func getRadarrVersion(ctx context.Context, wait *sync.WaitGroup, radarrs []*apps.RadarrConfig, rad []*conTest) { for idx, app := range radarrs { wait.Add(1) go func(idx int, app *apps.RadarrConfig) { defer wait.Done() - stat, err := app.GetSystemStatus() + stat, err := app.GetSystemStatusContext(ctx) rad[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} }(idx, app) } } -func getReadarrVersion(wait *sync.WaitGroup, readarrs []*apps.ReadarrConfig, read []*conTest) { +func getReadarrVersion(ctx context.Context, wait *sync.WaitGroup, readarrs []*apps.ReadarrConfig, read []*conTest) { for idx, app := range readarrs { wait.Add(1) go func(idx int, app *apps.ReadarrConfig) { defer wait.Done() - stat, err := app.GetSystemStatus() + stat, err := app.GetSystemStatusContext(ctx) read[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} }(idx, app) } } -func getSonarrVersion(wait *sync.WaitGroup, sonarrs []*apps.SonarrConfig, son []*conTest) { +func getSonarrVersion(ctx context.Context, wait *sync.WaitGroup, sonarrs []*apps.SonarrConfig, son []*conTest) { for idx, app := range sonarrs { wait.Add(1) go func(idx int, app *apps.SonarrConfig) { defer wait.Done() - stat, err := app.GetSystemStatus() + stat, err := app.GetSystemStatusContext(ctx) son[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} }(idx, app) } } -func getPlexVersion(wait *sync.WaitGroup, plexServer *plex.Server, plx []*conTest) { - if plexServer.Configured() { - wait.Add(1) - - go func() { - defer wait.Done() - - stat, err := plexServer.GetInfo() - if stat == nil { - stat = &plex.PMSInfo{} - } - - plx = []*conTest{{ - Instance: 1, - Up: err == nil, - Status: map[string]interface{}{ - "friendlyName": stat.FriendlyName, - "version": stat.Version, - "updatedAt": stat.UpdatedAt, - "platform": stat.Platform, - "platformVersion": stat.PlatformVersion, - "size": stat.Size, - "myPlexSigninState": stat.MyPlexSigninState, - "myPlexSubscription": stat.MyPlexSubscription, - "pushNotifications": stat.PushNotifications, - "streamingBrainVersion": stat.StreamingBrainVersion, - "streamingBrainABRVersion": stat.StreamingBrainABRVersion, - }, - }} - }() +func getPlexVersion(ctx context.Context, wait *sync.WaitGroup, plexServer *plex.Server, plx *[]*conTest) { + if !plexServer.Configured() { + return } + + wait.Add(1) + + go func() { + defer wait.Done() + + stat, err := plexServer.GetInfo(ctx) + if stat == nil { + stat = &plex.PMSInfo{} + } + + *plx = []*conTest{{ + Instance: 1, + Up: err == nil, + Status: map[string]interface{}{ + "friendlyName": stat.FriendlyName, + "version": stat.Version, + "updatedAt": stat.UpdatedAt, + "platform": stat.Platform, + "platformVersion": stat.PlatformVersion, + "size": stat.Size, + "myPlexSigninState": stat.MyPlexSigninState, + "myPlexSubscription": stat.MyPlexSubscription, + "pushNotifications": stat.PushNotifications, + "streamingBrainVersion": stat.StreamingBrainVersion, + "streamingBrainABRVersion": stat.StreamingBrainABRVersion, + }, + }} + }() } diff --git a/pkg/plex/info.go b/pkg/plex/info.go index 241ef27d3..db1f18c5b 100644 --- a/pkg/plex/info.go +++ b/pkg/plex/info.go @@ -7,10 +7,7 @@ import ( "fmt" ) -func (s *Server) GetInfo() (*PMSInfo, error) { - ctx, cancel := context.WithTimeout(context.Background(), s.Timeout.Duration) - defer cancel() - +func (s *Server) GetInfo(ctx context.Context) (*PMSInfo, error) { data, err := s.getPlexURL(ctx, s.URL, nil) if err != nil { return nil, err From a20f026ea3520fce69eb92372842a6e483d0b19e Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Tue, 18 Jan 2022 18:33:59 -0800 Subject: [PATCH 02/10] fix ordering for dashboard --- go.mod | 2 +- go.sum | 4 +++ pkg/notifiarr/dashboard.go | 54 ++++++++++++++++++++++---------------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index f9784185a..1a4b9d6b0 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( golift.io/deluge v0.9.4-0.20220103091211-1842b313e264 golift.io/qbit v0.0.0-20211121074815-1558e8969b98 golift.io/rotatorr v0.0.0-20210307012029-65b11a8ea8f9 - golift.io/starr v0.13.0 + golift.io/starr v0.13.1-0.20220117233154-f0fdc3b60b5c golift.io/version v0.0.2 golift.io/xtractr v0.0.11 ) diff --git a/go.sum b/go.sum index 53fb619fe..b7fe28214 100644 --- a/go.sum +++ b/go.sum @@ -674,6 +674,10 @@ golift.io/rotatorr v0.0.0-20210307012029-65b11a8ea8f9 h1:j/WeLF6Ew1lc/m8/bh5qleZ golift.io/rotatorr v0.0.0-20210307012029-65b11a8ea8f9/go.mod h1:EZevRvIGRh8jDMwuYL0/tlPns0KynquPZzb0SerIC1s= golift.io/starr v0.13.0 h1:LoihBAH3DQ0ikPNHTVg47tUU+475mzbr1ahMcY5gdno= golift.io/starr v0.13.0/go.mod h1:IZIzdT5/NBdhM08xAEO5R1INgGN+Nyp4vCwvgHrbKVs= +golift.io/starr v0.13.1-0.20220117212508-f1d3ae11b103 h1:Nr5oYDKYTIcUcAi019ujfWVHgJz5yMq5v04wld/UNOo= +golift.io/starr v0.13.1-0.20220117212508-f1d3ae11b103/go.mod h1:IZIzdT5/NBdhM08xAEO5R1INgGN+Nyp4vCwvgHrbKVs= +golift.io/starr v0.13.1-0.20220117233154-f0fdc3b60b5c h1:7qtem+mFuWd/m3ZS2NuzFwBfqvmij9ecjFYZ+SwGGU0= +golift.io/starr v0.13.1-0.20220117233154-f0fdc3b60b5c/go.mod h1:IZIzdT5/NBdhM08xAEO5R1INgGN+Nyp4vCwvgHrbKVs= golift.io/version v0.0.2 h1:i0gXRuSDHKs4O0sVDUg4+vNIuOxYoXhaxspftu2FRTE= golift.io/version v0.0.2/go.mod h1:76aHNz8/Pm7CbuxIsDi97jABL5Zui3f2uZxDm4vB6hU= golift.io/xtractr v0.0.11 h1:6SVjqX7aCT7Zl4y1rVunBnzwAqK56m90IMywbgeh3r8= diff --git a/pkg/notifiarr/dashboard.go b/pkg/notifiarr/dashboard.go index 70719faf7..43a9ab7cb 100644 --- a/pkg/notifiarr/dashboard.go +++ b/pkg/notifiarr/dashboard.go @@ -9,7 +9,11 @@ import ( "github.com/Notifiarr/notifiarr/pkg/apps" "golift.io/cnfg" + "golift.io/starr" + "golift.io/starr/lidarr" "golift.io/starr/radarr" + "golift.io/starr/readarr" + "golift.io/starr/sonarr" ) /* This file sends state of affairs to notifiarr.com */ @@ -418,7 +422,13 @@ func (c *Config) getLidarrState(instance int, app *apps.LidarrConfig) (*State, e // getLidarrHistory is not done. func (c *Config) getLidarrHistory(app *apps.LidarrConfig) ([]*Sortable, error) { - history, err := app.GetHistory(showLatest*40, 100) //nolint:gomnd + history, err := app.GetHistoryPage(&starr.Req{ + Page: 1, + PageSize: showLatest + 20, //nolint:gomnd // grab extra in case some are tracks and not albums. + SortDir: starr.SortDescend, + SortKey: "date", + Filter: lidarr.FilterTrackFileImported, + }) if err != nil { return nil, fmt.Errorf("getting history: %w", err) } @@ -426,15 +436,11 @@ func (c *Config) getLidarrHistory(app *apps.LidarrConfig) ([]*Sortable, error) { table := []*Sortable{} albumIDs := make(map[int64]*struct{}) -FORLOOP: for _, rec := range history.Records { - switch { - case len(table) >= showLatest: - break FORLOOP - case rec.EventType != "trackFileImported": - continue - case albumIDs[rec.AlbumID] != nil: - continue + if len(table) >= showLatest { + break + } else if albumIDs[rec.AlbumID] != nil { + continue // we already have this album } albumIDs[rec.AlbumID] = &struct{}{} @@ -616,26 +622,26 @@ func (c *Config) getReadarrState(instance int, app *apps.ReadarrConfig) (*State, // getReadarrHistory is not done. func (c *Config) getReadarrHistory(app *apps.ReadarrConfig) ([]*Sortable, error) { - history, err := app.GetHistory(showLatest*20, 100) //nolint:gomnd + history, err := app.GetHistoryPage(&starr.Req{ + Page: 1, + PageSize: showLatest, + SortDir: starr.SortDescend, + SortKey: "date", + Filter: readarr.FilterBookFileImported, + }) if err != nil { return nil, fmt.Errorf("getting history: %w", err) } table := []*Sortable{} - for _, rec := range history.Records { - if len(table) >= showLatest { - break - } else if rec.EventType != "bookFileImported" { - continue - } - + for idx := 0; idx < len(history.Records) && len(table) < showLatest; idx++ { // An error here gets swallowed. - if book, err := app.GetBookByID(rec.BookID); err == nil { + if book, err := app.GetBookByID(history.Records[idx].BookID); err == nil { table = append(table, &Sortable{ Name: book.Title, Sub: book.Author.AuthorName, - Date: rec.Date, + Date: history.Records[idx].Date, }) } } @@ -691,7 +697,13 @@ func (c *Config) getSonarrState(instance int, app *apps.SonarrConfig) (*State, e } func (c *Config) getSonarrHistory(app *apps.SonarrConfig) ([]*Sortable, error) { - history, err := app.GetHistory(showLatest*20, 100) //nolint:gomnd + history, err := app.GetHistoryPage(&starr.Req{ + Page: 1, + PageSize: showLatest + 5, //nolint:gomnd // grab extra in case there's an error. + SortDir: starr.SortDescend, + SortKey: "date", + Filter: sonarr.FilterDownloadFolderImported, + }) if err != nil { return nil, fmt.Errorf("getting history: %w", err) } @@ -701,8 +713,6 @@ func (c *Config) getSonarrHistory(app *apps.SonarrConfig) ([]*Sortable, error) { for _, rec := range history.Records { if len(table) >= showLatest { break - } else if rec.EventType != "downloadFolderImported" { - continue } series, err := app.GetSeriesByID(rec.SeriesID) From c835323398b098737a60e21052001764f797da1f Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Tue, 18 Jan 2022 19:38:17 -0800 Subject: [PATCH 03/10] move version handler, rename plex handler --- pkg/client/handlers.go | 2 +- pkg/client/handlers_version.go | 162 ++++++++++++++++++ .../{handlers.go => handlers_plex.go} | 153 ----------------- 3 files changed, 163 insertions(+), 154 deletions(-) create mode 100644 pkg/client/handlers_version.go rename pkg/notifiarr/{handlers.go => handlers_plex.go} (57%) diff --git a/pkg/client/handlers.go b/pkg/client/handlers.go index 435583cb2..99f93c9f1 100644 --- a/pkg/client/handlers.go +++ b/pkg/client/handlers.go @@ -18,7 +18,7 @@ import ( // internalHandlers initializes "special" internal API paths. func (c *Client) internalHandlers() { - c.Config.HandleAPIpath("", "version", c.website.VersionHandler, "GET", "HEAD") + c.Config.HandleAPIpath("", "version", c.versionHandler, "GET", "HEAD") c.Config.HandleAPIpath("", "trigger/{trigger:[0-9a-z-]+}", c.handleTrigger, "GET") c.Config.HandleAPIpath("", "trigger/{trigger:[0-9a-z-]+}/{content}", c.handleTrigger, "GET") diff --git a/pkg/client/handlers_version.go b/pkg/client/handlers_version.go new file mode 100644 index 000000000..d6fe8b3ee --- /dev/null +++ b/pkg/client/handlers_version.go @@ -0,0 +1,162 @@ +package client + +import ( + "context" + "net/http" + "sync" + + "github.com/Notifiarr/notifiarr/pkg/apps" + "github.com/Notifiarr/notifiarr/pkg/plex" +) + +/* The version handler gets the version from a bunch of apps and returns them. */ + +type conTest struct { + Instance int `json:"instance"` + Up bool `json:"up"` + Status interface{} `json:"systemStatus,omitempty"` +} + +// versionHandler returns application run and build time data and application statuses: /api/version. +func (c *Client) versionHandler(r *http.Request) (int, interface{}) { + output := c.website.Info() + output["appsStatus"] = c.appStatsForVersion(r.Context()) + + if host, err := c.website.GetHostInfoUID(); err != nil { + output["hostError"] = err.Error() + } else { + output["host"] = host + } + + return http.StatusOK, output +} + +// appStatsForVersion loops each app and gets the version info. +func (c *Client) appStatsForVersion(ctx context.Context) map[string]interface{} { + var ( + lid = make([]*conTest, len(c.Config.Apps.Lidarr)) + prl = make([]*conTest, len(c.Config.Apps.Prowlarr)) + rad = make([]*conTest, len(c.Config.Apps.Radarr)) + read = make([]*conTest, len(c.Config.Apps.Readarr)) + son = make([]*conTest, len(c.Config.Apps.Sonarr)) + plx = []*conTest{} + wg sync.WaitGroup + ) + + getPlexVersion(ctx, &wg, c.Config.Plex, &plx) + getLidarrVersion(ctx, &wg, c.Config.Apps.Lidarr, lid) + getProwlarrVersion(ctx, &wg, c.Config.Apps.Prowlarr, prl) + getRadarrVersion(ctx, &wg, c.Config.Apps.Radarr, rad) + getReadarrVersion(ctx, &wg, c.Config.Apps.Readarr, read) + getSonarrVersion(ctx, &wg, c.Config.Apps.Sonarr, son) + wg.Wait() + + return map[string]interface{}{ + "lidarr": lid, + "radarr": rad, + "readarr": read, + "sonarr": son, + "prowlarr": prl, + "plex": plx, + } +} + +func getLidarrVersion(ctx context.Context, wait *sync.WaitGroup, lidarrs []*apps.LidarrConfig, lid []*conTest) { + for idx, app := range lidarrs { + wait.Add(1) + + go func(idx int, app *apps.LidarrConfig) { + defer wait.Done() + + stat, err := app.GetSystemStatusContext(ctx) + lid[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} + }(idx, app) + } +} + +func getProwlarrVersion(ctx context.Context, wait *sync.WaitGroup, prowlarrs []*apps.ProwlarrConfig, prl []*conTest) { + for idx, app := range prowlarrs { + wait.Add(1) + + go func(idx int, app *apps.ProwlarrConfig) { + defer wait.Done() + + stat, err := app.GetSystemStatusContext(ctx) + prl[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} + }(idx, app) + } +} + +func getRadarrVersion(ctx context.Context, wait *sync.WaitGroup, radarrs []*apps.RadarrConfig, rad []*conTest) { + for idx, app := range radarrs { + wait.Add(1) + + go func(idx int, app *apps.RadarrConfig) { + defer wait.Done() + + stat, err := app.GetSystemStatusContext(ctx) + rad[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} + }(idx, app) + } +} + +func getReadarrVersion(ctx context.Context, wait *sync.WaitGroup, readarrs []*apps.ReadarrConfig, read []*conTest) { + for idx, app := range readarrs { + wait.Add(1) + + go func(idx int, app *apps.ReadarrConfig) { + defer wait.Done() + + stat, err := app.GetSystemStatusContext(ctx) + read[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} + }(idx, app) + } +} + +func getSonarrVersion(ctx context.Context, wait *sync.WaitGroup, sonarrs []*apps.SonarrConfig, son []*conTest) { + for idx, app := range sonarrs { + wait.Add(1) + + go func(idx int, app *apps.SonarrConfig) { + defer wait.Done() + + stat, err := app.GetSystemStatusContext(ctx) + son[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} + }(idx, app) + } +} + +func getPlexVersion(ctx context.Context, wait *sync.WaitGroup, plexServer *plex.Server, plx *[]*conTest) { + if !plexServer.Configured() { + return + } + + wait.Add(1) + + go func() { + defer wait.Done() + + stat, err := plexServer.GetInfo(ctx) + if stat == nil { + stat = &plex.PMSInfo{} + } + + *plx = []*conTest{{ + Instance: 1, + Up: err == nil, + Status: map[string]interface{}{ + "friendlyName": stat.FriendlyName, + "version": stat.Version, + "updatedAt": stat.UpdatedAt, + "platform": stat.Platform, + "platformVersion": stat.PlatformVersion, + "size": stat.Size, + "myPlexSigninState": stat.MyPlexSigninState, + "myPlexSubscription": stat.MyPlexSubscription, + "pushNotifications": stat.PushNotifications, + "streamingBrainVersion": stat.StreamingBrainVersion, + "streamingBrainABRVersion": stat.StreamingBrainABRVersion, + }, + }} + }() +} diff --git a/pkg/notifiarr/handlers.go b/pkg/notifiarr/handlers_plex.go similarity index 57% rename from pkg/notifiarr/handlers.go rename to pkg/notifiarr/handlers_plex.go index 53178a128..fb527bae1 100644 --- a/pkg/notifiarr/handlers.go +++ b/pkg/notifiarr/handlers_plex.go @@ -1,15 +1,12 @@ package notifiarr import ( - "context" "encoding/json" "fmt" "net/http" "strings" - "sync" "time" - "github.com/Notifiarr/notifiarr/pkg/apps" "github.com/Notifiarr/notifiarr/pkg/mnd" "github.com/Notifiarr/notifiarr/pkg/plex" ) @@ -148,153 +145,3 @@ func (c *Config) sendPlexWebhook(hook *plexIncomingWebhook) { c.Printf("Plex => Notifiarr: %s '%s' => %s. %s", hook.Account.Title, hook.Event, hook.Metadata.Title, resp) } - -type conTest struct { - Instance int `json:"instance"` - Up bool `json:"up"` - Status interface{} `json:"systemStatus,omitempty"` -} - -// VersionHandler returns application run and build time data and application statuses: /api/version. -func (c *Config) VersionHandler(r *http.Request) (int, interface{}) { - output := c.Info() - output["appsStatus"] = c.appStatsForVersion(r.Context()) - - if host, err := c.GetHostInfoUID(); err != nil { - output["hostError"] = err.Error() - } else { - output["host"] = host - } - - return http.StatusOK, output -} - -// appStatsForVersion loops each app and gets the version info. -func (c *Config) appStatsForVersion(ctx context.Context) map[string]interface{} { - var ( - lid = make([]*conTest, len(c.Apps.Lidarr)) - prl = make([]*conTest, len(c.Apps.Prowlarr)) - rad = make([]*conTest, len(c.Apps.Radarr)) - read = make([]*conTest, len(c.Apps.Readarr)) - son = make([]*conTest, len(c.Apps.Sonarr)) - plx = []*conTest{} - wg sync.WaitGroup - ) - - getPlexVersion(ctx, &wg, c.Plex, &plx) - getLidarrVersion(ctx, &wg, c.Apps.Lidarr, lid) - getProwlarrVersion(ctx, &wg, c.Apps.Prowlarr, prl) - getRadarrVersion(ctx, &wg, c.Apps.Radarr, rad) - getReadarrVersion(ctx, &wg, c.Apps.Readarr, read) - getSonarrVersion(ctx, &wg, c.Apps.Sonarr, son) - wg.Wait() - - return map[string]interface{}{ - "lidarr": lid, - "radarr": rad, - "readarr": read, - "sonarr": son, - "prowlarr": prl, - "plex": plx, - } -} - -func getLidarrVersion(ctx context.Context, wait *sync.WaitGroup, lidarrs []*apps.LidarrConfig, lid []*conTest) { - for idx, app := range lidarrs { - wait.Add(1) - - go func(idx int, app *apps.LidarrConfig) { - defer wait.Done() - - stat, err := app.GetSystemStatusContext(ctx) - lid[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} - }(idx, app) - } -} - -func getProwlarrVersion(ctx context.Context, wait *sync.WaitGroup, prowlarrs []*apps.ProwlarrConfig, prl []*conTest) { - for idx, app := range prowlarrs { - wait.Add(1) - - go func(idx int, app *apps.ProwlarrConfig) { - defer wait.Done() - - stat, err := app.GetSystemStatusContext(ctx) - prl[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} - }(idx, app) - } -} - -func getRadarrVersion(ctx context.Context, wait *sync.WaitGroup, radarrs []*apps.RadarrConfig, rad []*conTest) { - for idx, app := range radarrs { - wait.Add(1) - - go func(idx int, app *apps.RadarrConfig) { - defer wait.Done() - - stat, err := app.GetSystemStatusContext(ctx) - rad[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} - }(idx, app) - } -} - -func getReadarrVersion(ctx context.Context, wait *sync.WaitGroup, readarrs []*apps.ReadarrConfig, read []*conTest) { - for idx, app := range readarrs { - wait.Add(1) - - go func(idx int, app *apps.ReadarrConfig) { - defer wait.Done() - - stat, err := app.GetSystemStatusContext(ctx) - read[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} - }(idx, app) - } -} - -func getSonarrVersion(ctx context.Context, wait *sync.WaitGroup, sonarrs []*apps.SonarrConfig, son []*conTest) { - for idx, app := range sonarrs { - wait.Add(1) - - go func(idx int, app *apps.SonarrConfig) { - defer wait.Done() - - stat, err := app.GetSystemStatusContext(ctx) - son[idx] = &conTest{Instance: idx + 1, Up: err == nil, Status: stat} - }(idx, app) - } -} - -func getPlexVersion(ctx context.Context, wait *sync.WaitGroup, plexServer *plex.Server, plx *[]*conTest) { - if !plexServer.Configured() { - return - } - - wait.Add(1) - - go func() { - defer wait.Done() - - stat, err := plexServer.GetInfo(ctx) - if stat == nil { - stat = &plex.PMSInfo{} - } - - *plx = []*conTest{{ - Instance: 1, - Up: err == nil, - Status: map[string]interface{}{ - "friendlyName": stat.FriendlyName, - "version": stat.Version, - "updatedAt": stat.UpdatedAt, - "platform": stat.Platform, - "platformVersion": stat.PlatformVersion, - "size": stat.Size, - "myPlexSigninState": stat.MyPlexSigninState, - "myPlexSubscription": stat.MyPlexSubscription, - "pushNotifications": stat.PushNotifications, - "streamingBrainVersion": stat.StreamingBrainVersion, - "streamingBrainABRVersion": stat.StreamingBrainABRVersion, - }, - }} - }() -} From a6ded1d232eb74159f78a755c4b575b4b067c866 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Tue, 18 Jan 2022 20:19:38 -0800 Subject: [PATCH 04/10] export IntList --- pkg/client/handlers.go | 4 ++-- pkg/client/webserver.go | 2 +- pkg/notifiarr/cfsync.go | 7 ++++--- pkg/notifiarr/clientinfo.go | 6 ++++-- pkg/notifiarr/gaps.go | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pkg/client/handlers.go b/pkg/client/handlers.go index 99f93c9f1..5c88af3d0 100644 --- a/pkg/client/handlers.go +++ b/pkg/client/handlers.go @@ -16,8 +16,8 @@ import ( "golift.io/starr" ) -// internalHandlers initializes "special" internal API paths. -func (c *Client) internalHandlers() { +// httpHandlers initializes internaland other API routes. +func (c *Client) httpHandlers() { c.Config.HandleAPIpath("", "version", c.versionHandler, "GET", "HEAD") c.Config.HandleAPIpath("", "trigger/{trigger:[0-9a-z-]+}", c.handleTrigger, "GET") c.Config.HandleAPIpath("", "trigger/{trigger:[0-9a-z-]+}/{content}", c.handleTrigger, "GET") diff --git a/pkg/client/webserver.go b/pkg/client/webserver.go index f01211f52..7c1664e5b 100644 --- a/pkg/client/webserver.go +++ b/pkg/client/webserver.go @@ -35,7 +35,7 @@ func (c *Client) StartWebServer() { // Initialize all the application API paths. c.Config.Apps.InitHandlers() - c.internalHandlers() + c.httpHandlers() // Run the server. go c.runWebServer() } diff --git a/pkg/notifiarr/cfsync.go b/pkg/notifiarr/cfsync.go index 1da0ae0ea..f6da047ba 100644 --- a/pkg/notifiarr/cfsync.go +++ b/pkg/notifiarr/cfsync.go @@ -15,9 +15,9 @@ import ( type syncConfig struct { Interval cnfg.Duration `json:"interval"` // how often to fire in minutes. Radarr int64 `json:"radarr"` // items in sync - RadarrInstances intList `json:"radarrInstances"` // which instance IDs we sync + RadarrInstances IntList `json:"radarrInstances"` // which instance IDs we sync Sonarr int64 `json:"sonarr"` // items in sync - SonarrInstances intList `json:"sonarrInstances"` // which instance IDs we sync + SonarrInstances IntList `json:"sonarrInstances"` // which instance IDs we sync } // cfMapIDpayload is used to post-back ID changes for profiles and formats. @@ -46,7 +46,8 @@ type RadarrCustomFormatPayload struct { Name string `json:"name"` CustomFormats []*radarr.CustomFormat `json:"customFormats,omitempty"` QualityProfiles []*radarr.QualityProfile `json:"qualityProfiles,omitempty"` - NewMaps *cfMapIDpayload `json:"newMaps,omitempty"` + // Purposely not exported so as to not use it externally. + NewMaps *cfMapIDpayload `json:"newMaps,omitempty"` } func (t *Triggers) SyncCF(event EventType) { diff --git a/pkg/notifiarr/clientinfo.go b/pkg/notifiarr/clientinfo.go index f00f0ba99..c6d8495f6 100644 --- a/pkg/notifiarr/clientinfo.go +++ b/pkg/notifiarr/clientinfo.go @@ -55,9 +55,11 @@ type ServiceCheck struct { Interval cnfg.Duration `json:"interval"` } -type intList []int +// IntList has a method to abstract lookups. +type IntList []int -func (l intList) Has(instance int) bool { +// Has returns true if the list has an instance ID. +func (l IntList) Has(instance int) bool { for _, i := range l { if instance == i { return true diff --git a/pkg/notifiarr/gaps.go b/pkg/notifiarr/gaps.go index 209eb881a..6dd132ead 100644 --- a/pkg/notifiarr/gaps.go +++ b/pkg/notifiarr/gaps.go @@ -12,7 +12,7 @@ import ( // gapsConfig is the configuration returned from the notifiarr website. type gapsConfig struct { - Instances intList `json:"instances"` + Instances IntList `json:"instances"` Interval cnfg.Duration `json:"interval"` } From 11214b1314b5b7f5fa03d50610e82afaa70f9bd6 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Tue, 18 Jan 2022 21:32:34 -0800 Subject: [PATCH 05/10] fix missing things and a bug in cfsync loop --- pkg/client/handlers.go | 2 +- pkg/notifiarr/cfsync.go | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/client/handlers.go b/pkg/client/handlers.go index 5c88af3d0..716ecd8ab 100644 --- a/pkg/client/handlers.go +++ b/pkg/client/handlers.go @@ -16,7 +16,7 @@ import ( "golift.io/starr" ) -// httpHandlers initializes internaland other API routes. +// httpHandlers initializes internal and other API routes. func (c *Client) httpHandlers() { c.Config.HandleAPIpath("", "version", c.versionHandler, "GET", "HEAD") c.Config.HandleAPIpath("", "trigger/{trigger:[0-9a-z-]+}", c.handleTrigger, "GET") diff --git a/pkg/notifiarr/cfsync.go b/pkg/notifiarr/cfsync.go index f6da047ba..b7fd4f19f 100644 --- a/pkg/notifiarr/cfsync.go +++ b/pkg/notifiarr/cfsync.go @@ -46,6 +46,7 @@ type RadarrCustomFormatPayload struct { Name string `json:"name"` CustomFormats []*radarr.CustomFormat `json:"customFormats,omitempty"` QualityProfiles []*radarr.QualityProfile `json:"qualityProfiles,omitempty"` + Error string `json:"error"` // Purposely not exported so as to not use it externally. NewMaps *cfMapIDpayload `json:"newMaps,omitempty"` } @@ -80,7 +81,7 @@ func (c *Config) syncRadarr(event EventType) { if err := c.syncRadarrCF(instance, app); err != nil { c.Errorf("[%s requested] Radarr Custom Formats sync request for '%d:%s' failed: %v", event, instance, app.URL, err) - return + continue } c.Printf("[%s requested] Synced Custom Formats from Notifiarr for Radarr: %d:%s", event, instance, app.URL) @@ -200,7 +201,9 @@ type SonarrCustomFormatPayload struct { Name string `json:"name"` ReleaseProfiles []*sonarr.ReleaseProfile `json:"releaseProfiles,omitempty"` QualityProfiles []*sonarr.QualityProfile `json:"qualityProfiles,omitempty"` - NewMaps *cfMapIDpayload `json:"newMaps,omitempty"` + Error string `json:"error"` + // Purposely not exported so as to not use it externally. + NewMaps *cfMapIDpayload `json:"newMaps,omitempty"` } // syncSonarr triggers a custom format sync for Sonarr. @@ -223,7 +226,7 @@ func (c *Config) syncSonarr(event EventType) { if err := c.syncSonarrRP(instance, app); err != nil { c.Errorf("[%s requested] Sonarr Release Profiles sync for '%d:%s' failed: %v", event, instance, app.URL, err) - return + continue } c.Printf("[%s requested] Synced Sonarr Release Profiles from Notifiarr: %d:%s", event, instance, app.URL) From e4eb614cca8b473c79dd74fe80fb8b35cecfb216 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Wed, 19 Jan 2022 00:09:07 -0800 Subject: [PATCH 06/10] update exported names to be shorter --- pkg/notifiarr/cfsync.go | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/pkg/notifiarr/cfsync.go b/pkg/notifiarr/cfsync.go index b7fd4f19f..103c3b2f4 100644 --- a/pkg/notifiarr/cfsync.go +++ b/pkg/notifiarr/cfsync.go @@ -11,6 +11,11 @@ import ( "golift.io/starr/sonarr" ) +/* CF Sync means Custom Format Sync. This is a premium feature that allows syncing + TRaSH's custom Radarr formats and Sonarr Release Profiles. + The code in this file deals with sending data and getting updates at an interval. +*/ + // syncConfig is the configuration returned from the notifiarr website. type syncConfig struct { Interval cnfg.Duration `json:"interval"` // how often to fire in minutes. @@ -39,9 +44,10 @@ const success = "success" /*//*****/ // Radarr /*//*****/// -// RadarrCustomFormatPayload is the payload sent and received +// RadarrTrashPayload is the payload sent and received // to/from notifarr.com when updating custom formats for Radarr. -type RadarrCustomFormatPayload struct { +// This is used in other places, like the trash API handler in the 'client' module. +type RadarrTrashPayload struct { Instance int `json:"instance"` Name string `json:"name"` CustomFormats []*radarr.CustomFormat `json:"customFormats,omitempty"` @@ -91,7 +97,7 @@ func (c *Config) syncRadarr(event EventType) { func (c *Config) syncRadarrCF(instance int, app *apps.RadarrConfig) error { var ( err error - payload = RadarrCustomFormatPayload{Instance: instance, Name: app.Name, NewMaps: c.radarrCF[instance]} + payload = RadarrTrashPayload{Instance: instance, Name: app.Name, NewMaps: c.radarrCF[instance]} ) payload.QualityProfiles, err = app.GetQualityProfiles() @@ -123,7 +129,7 @@ func (c *Config) syncRadarrCF(instance int, app *apps.RadarrConfig) error { } func (c *Config) updateRadarrCF(instance int, app *apps.RadarrConfig, data []byte) error { - reply := &RadarrCustomFormatPayload{} + reply := &RadarrTrashPayload{} if err := json.Unmarshal(data, &reply); err != nil { return fmt.Errorf("bad json response: %w", err) } @@ -178,7 +184,7 @@ func (c *Config) postbackRadarrCF(instance int, maps *cfMapIDpayload) error { return nil } - _, err := c.SendData(CFSyncRoute.Path("", "app=radarr", "updateIDs=true"), &RadarrCustomFormatPayload{ + _, err := c.SendData(CFSyncRoute.Path("", "app=radarr", "updateIDs=true"), &RadarrTrashPayload{ Instance: instance, NewMaps: maps, }, false) @@ -194,9 +200,9 @@ func (c *Config) postbackRadarrCF(instance int, maps *cfMapIDpayload) error { /*//*****/ // Sonarr /*//*****/// -// SonarrCustomFormatPayload is the payload sent and received +// SonarrTrashPayload is the payload sent and received // to/from notifarr.com when updating custom formats for Sonarr. -type SonarrCustomFormatPayload struct { +type SonarrTrashPayload struct { Instance int `json:"instance"` Name string `json:"name"` ReleaseProfiles []*sonarr.ReleaseProfile `json:"releaseProfiles,omitempty"` @@ -236,7 +242,7 @@ func (c *Config) syncSonarr(event EventType) { func (c *Config) syncSonarrRP(instance int, app *apps.SonarrConfig) error { var ( err error - payload = SonarrCustomFormatPayload{Instance: instance, Name: app.Name, NewMaps: c.sonarrRP[instance]} + payload = SonarrTrashPayload{Instance: instance, Name: app.Name, NewMaps: c.sonarrRP[instance]} ) payload.QualityProfiles, err = app.GetQualityProfiles() @@ -268,7 +274,7 @@ func (c *Config) syncSonarrRP(instance int, app *apps.SonarrConfig) error { } func (c *Config) updateSonarrRP(instance int, app *apps.SonarrConfig, data []byte) error { - reply := &SonarrCustomFormatPayload{} + reply := &SonarrTrashPayload{} if err := json.Unmarshal(data, &reply); err != nil { return fmt.Errorf("bad json response: %w", err) } @@ -323,7 +329,7 @@ func (c *Config) postbackSonarrRP(instance int, maps *cfMapIDpayload) error { return nil } - _, err := c.SendData(CFSyncRoute.Path("", "app=sonarr", "updateIDs=true"), &SonarrCustomFormatPayload{ + _, err := c.SendData(CFSyncRoute.Path("", "app=sonarr", "updateIDs=true"), &SonarrTrashPayload{ Instance: instance, NewMaps: maps, }, false) From eaeebd34393a60d77e091fde74df40ceb5c3cf47 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Wed, 19 Jan 2022 01:07:55 -0800 Subject: [PATCH 07/10] allow extra api keys --- README.md | 1 + pkg/apps/apps.go | 39 +++++++++++++++++++++++--------------- pkg/configfile/template.go | 4 +++- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9c85ab6da..89329aa7c 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ Recommend not messing with these unless instructed to do so. |Config Name|Variable Name|Default / Note| |---|---|---| +|extra_keys|`DN_EXTRA_KEYS_0`|`[]` (empty list) / Add keys to allow API requests from places besides notifiarr.com| |mode|`DN_MODE`|`production` / Change application mode: `development` or `production`| |debug|`DN_DEBUG`|`false` / Adds payloads and other stuff to the log output; very verbose/noisy| |debug_log|`DN_DEBUG_LOG`|`""` / Set a file system path to write debug logs to a dedicated file| diff --git a/pkg/apps/apps.go b/pkg/apps/apps.go index 19080e9d1..4ee9345e9 100644 --- a/pkg/apps/apps.go +++ b/pkg/apps/apps.go @@ -29,20 +29,22 @@ import ( // Apps is the input configuration to relay requests to Starr apps. type Apps struct { - APIKey string `json:"apiKey" toml:"api_key" xml:"api_key" yaml:"apiKey"` - URLBase string `json:"urlbase" toml:"urlbase" xml:"urlbase" yaml:"urlbase"` - Sonarr []*SonarrConfig `json:"sonarr,omitempty" toml:"sonarr" xml:"sonarr" yaml:"sonarr,omitempty"` - Radarr []*RadarrConfig `json:"radarr,omitempty" toml:"radarr" xml:"radarr" yaml:"radarr,omitempty"` - Lidarr []*LidarrConfig `json:"lidarr,omitempty" toml:"lidarr" xml:"lidarr" yaml:"lidarr,omitempty"` - Readarr []*ReadarrConfig `json:"readarr,omitempty" toml:"readarr" xml:"readarr" yaml:"readarr,omitempty"` - Prowlarr []*ProwlarrConfig `json:"prowlarr,omitempty" toml:"prowlarr" xml:"prowlarr" yaml:"prowlarr,omitempty"` - Deluge []*DelugeConfig `json:"deluge,omitempty" toml:"deluge" xml:"deluge" yaml:"deluge,omitempty"` - Qbit []*QbitConfig `json:"qbit,omitempty" toml:"qbit" xml:"qbit" yaml:"qbit,omitempty"` - SabNZB []*SabNZBConfig `json:"sabnzbd,omitempty" toml:"sabnzbd" xml:"sabnzbd" yaml:"sabnzbd,omitempty"` - Tautulli *TautulliConfig `json:"tautulli,omitempty" toml:"tautulli" xml:"tautulli" yaml:"tautulli,omitempty"` - Router *mux.Router `json:"-" toml:"-" xml:"-" yaml:"-"` - ErrorLog *log.Logger `json:"-" toml:"-" xml:"-" yaml:"-"` - DebugLog *log.Logger `json:"-" toml:"-" xml:"-" yaml:"-"` + APIKey string `json:"apiKey" toml:"api_key" xml:"api_key" yaml:"apiKey"` + ExKeys []string `json:"extraKeys" toml:"extra_keys" xml:"extra_keys" yaml:"extraKeys"` + URLBase string `json:"urlbase" toml:"urlbase" xml:"urlbase" yaml:"urlbase"` + Sonarr []*SonarrConfig `json:"sonarr,omitempty" toml:"sonarr" xml:"sonarr" yaml:"sonarr,omitempty"` + Radarr []*RadarrConfig `json:"radarr,omitempty" toml:"radarr" xml:"radarr" yaml:"radarr,omitempty"` + Lidarr []*LidarrConfig `json:"lidarr,omitempty" toml:"lidarr" xml:"lidarr" yaml:"lidarr,omitempty"` + Readarr []*ReadarrConfig `json:"readarr,omitempty" toml:"readarr" xml:"readarr" yaml:"readarr,omitempty"` + Prowlarr []*ProwlarrConfig `json:"prowlarr,omitempty" toml:"prowlarr" xml:"prowlarr" yaml:"prowlarr,omitempty"` + Deluge []*DelugeConfig `json:"deluge,omitempty" toml:"deluge" xml:"deluge" yaml:"deluge,omitempty"` + Qbit []*QbitConfig `json:"qbit,omitempty" toml:"qbit" xml:"qbit" yaml:"qbit,omitempty"` + SabNZB []*SabNZBConfig `json:"sabnzbd,omitempty" toml:"sabnzbd" xml:"sabnzbd" yaml:"sabnzbd,omitempty"` + Tautulli *TautulliConfig `json:"tautulli,omitempty" toml:"tautulli" xml:"tautulli" yaml:"tautulli,omitempty"` + Router *mux.Router `json:"-" toml:"-" xml:"-" yaml:"-"` + ErrorLog *log.Logger `json:"-" toml:"-" xml:"-" yaml:"-"` + DebugLog *log.Logger `json:"-" toml:"-" xml:"-" yaml:"-"` + keys map[string]struct{} // for fast key lookup. } // Errors sent to client web requests. @@ -147,7 +149,7 @@ func (a *Apps) handleAPI(app starr.App, api APIHandler) http.HandlerFunc { //nol // CheckAPIKey drops a 403 if the API key doesn't match, otherwise run next handler. func (a *Apps) CheckAPIKey(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { //nolint:varnamelen - if r.Header.Get("X-API-Key") != a.APIKey { + if _, ok := a.keys[r.Header.Get("X-API-Key")]; !ok { w.WriteHeader(http.StatusUnauthorized) return } @@ -158,6 +160,13 @@ func (a *Apps) CheckAPIKey(next http.Handler) http.Handler { // InitHandlers activates all our handlers. This is part of the web server init. func (a *Apps) InitHandlers() { + a.keys = make(map[string]struct{}) + for _, key := range append(a.ExKeys, a.APIKey) { + if len(key) > 3 { //nolint:gomnd + a.keys[key] = struct{}{} + } + } + a.lidarrHandlers() a.prowlarrHandlers() a.radarrHandlers() diff --git a/pkg/configfile/template.go b/pkg/configfile/template.go index f066fe75c..0c1bec883 100644 --- a/pkg/configfile/template.go +++ b/pkg/configfile/template.go @@ -34,7 +34,9 @@ const tmpl = `############################################### ############################################### # This API key must be copied from your notifiarr.com account. -{{if .APIKey}}api_key = "{{.APIKey}}"{{else}}api_key = "api-key-from-notifiarr.com"{{end}} +{{if .APIKey}}api_key = "{{.APIKey}}"{{else}}api_key = "api-key-from-notifiarr.com"{{end}}{{if .ExKeys}} + +extra_keys = [{{range $s := .ExKeys}}"{{$s}}",{{end}}]{{end}} ## The ip:port to listen on for incoming HTTP requests. 0.0.0.0 means all/any IP and is recommended! ## You may use "127.0.0.1:5454" to listen only on localhost; good if using a local proxy. From b4b614f8368dc87134b05d578c6bf37eabb55157 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Fri, 21 Jan 2022 22:11:31 -0800 Subject: [PATCH 08/10] Make trash sync continue and send full report --- pkg/notifiarr/cfsync.go | 75 +++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/pkg/notifiarr/cfsync.go b/pkg/notifiarr/cfsync.go index 103c3b2f4..d70f05f46 100644 --- a/pkg/notifiarr/cfsync.go +++ b/pkg/notifiarr/cfsync.go @@ -35,8 +35,9 @@ type cfMapIDpayload struct { // idMap is used a mapping list from old ID to new ID. Part of cfMapIDpayload. type idMap struct { - OldID int64 `json:"oldId"` - NewID int64 `json:"newId"` + Name string `json:"name"` + OldID int64 `json:"oldId"` + NewID int64 `json:"newId"` } // success is a ssuccessful status message from notifiarr.com. @@ -140,39 +141,47 @@ func (c *Config) updateRadarrCF(instance int, app *apps.RadarrConfig, data []byt maps := &cfMapIDpayload{QP: []idMap{}, CF: []idMap{}, Instance: instance} for idx, profile := range reply.CustomFormats { - profileID := profile.ID - if _, err := app.UpdateCustomFormat(profile, profileID); err != nil { + newID, existingID := profile.ID, profile.ID + + if _, err := app.UpdateCustomFormat(profile, existingID); err != nil { profile.ID = 0 c.Debugf("Error Updating custom format [%d/%d] (attempting to ADD %d): %v", - idx, len(reply.CustomFormats), profileID, err) + idx+1, len(reply.CustomFormats), existingID, err) - newID, err2 := app.AddCustomFormat(profile) + newAdd, err2 := app.AddCustomFormat(profile) if err2 != nil { - return fmt.Errorf("[%d/%d] updating custom format: %d: (update) %v, (add) %w", - idx, len(reply.CustomFormats), profileID, err, err2) + c.Errorf("Ensuring custom format [%d/%d] %d: (update) %v, (add) %v", + idx+1, len(reply.CustomFormats), existingID, err, err2) + continue } - maps.CF = append(maps.CF, idMap{int64(profileID), int64(newID.ID)}) + newID = newAdd.ID } + + maps.CF = append(maps.CF, idMap{profile.Name, int64(existingID), int64(newID)}) } for idx, profile := range reply.QualityProfiles { + newID, existingID := profile.ID, profile.ID + if err := app.UpdateQualityProfile(profile); err != nil { - profileID := profile.ID profile.ID = 0 c.Debugf("Error Updating quality profile [%d/%d] (attempting to ADD %d): %v", - idx, len(reply.QualityProfiles), profileID, err) + idx+1, len(reply.QualityProfiles), existingID, err) - newID, err2 := app.AddQualityProfile(profile) + newAddID, err2 := app.AddQualityProfile(profile) if err2 != nil { - return fmt.Errorf("[%d/%d] updating quality profile: %d: (update) %v, (add) %w", - idx, len(reply.QualityProfiles), profileID, err, err2) + c.Errorf("Ensuring quality profile [%d/%d] %d: (update) %v, (add) %v", + idx+1, len(reply.QualityProfiles), existingID, err, err2) + continue } - maps.QP = append(maps.QP, idMap{profileID, newID}) + newID = newAddID } + + maps.QP = append(maps.QP, idMap{profile.Name, existingID, newID}) } return c.postbackRadarrCF(instance, maps) @@ -187,7 +196,7 @@ func (c *Config) postbackRadarrCF(instance int, maps *cfMapIDpayload) error { _, err := c.SendData(CFSyncRoute.Path("", "app=radarr", "updateIDs=true"), &RadarrTrashPayload{ Instance: instance, NewMaps: maps, - }, false) + }, true) if err != nil { c.radarrCF[instance] = maps return fmt.Errorf("updating custom format ID map: %w", err) @@ -285,39 +294,47 @@ func (c *Config) updateSonarrRP(instance int, app *apps.SonarrConfig, data []byt maps := &cfMapIDpayload{RP: []idMap{}, QP: []idMap{}, Instance: instance} for idx, profile := range reply.ReleaseProfiles { + newID, existingID := profile.ID, profile.ID + if err := app.UpdateReleaseProfile(profile); err != nil { - profileID := profile.ID profile.ID = 0 c.Debugf("Error Updating release profile [%d/%d] (attempting to ADD %d): %v", - idx, len(reply.ReleaseProfiles), profileID, err) + idx+1, len(reply.ReleaseProfiles), existingID, err) - newID, err2 := app.AddReleaseProfile(profile) + newAddID, err2 := app.AddReleaseProfile(profile) if err2 != nil { - return fmt.Errorf("[%d/%d] updating release profiles: %d: (update) %v, (add) %w", - idx, len(reply.ReleaseProfiles), profileID, err, err2) + c.Errorf("Ensuring release profile [%d/%d] %d: (update) %v, (add) %v", + idx+1, len(reply.ReleaseProfiles), existingID, err, err2) + continue } - maps.RP = append(maps.RP, idMap{profileID, newID}) + newID = newAddID } + + maps.RP = append(maps.RP, idMap{profile.Name, existingID, newID}) } for idx, profile := range reply.QualityProfiles { + newID, existingID := profile.ID, profile.ID + if err := app.UpdateQualityProfile(profile); err != nil { - profileID := profile.ID profile.ID = 0 c.Debugf("Error Updating quality format [%d/%d] (attempting to ADD %d): %v", - idx, len(reply.QualityProfiles), profileID, err) + idx+1, len(reply.QualityProfiles), existingID, err) - newID, err2 := app.AddQualityProfile(profile) + newAddID, err2 := app.AddQualityProfile(profile) if err2 != nil { - return fmt.Errorf("[%d/%d] updating quality profile: %d: (update) %v, (add) %w", - idx, len(reply.QualityProfiles), profileID, err, err2) + c.Errorf("Ensuring quality format [%d/%d] %d: (update) %v, (add) %v", + idx+1, len(reply.QualityProfiles), existingID, err, err2) + continue } - maps.QP = append(maps.QP, idMap{profileID, newID}) + newID = newAddID } + + maps.QP = append(maps.QP, idMap{profile.Name, existingID, newID}) } return c.postbackSonarrRP(instance, maps) @@ -332,7 +349,7 @@ func (c *Config) postbackSonarrRP(instance int, maps *cfMapIDpayload) error { _, err := c.SendData(CFSyncRoute.Path("", "app=sonarr", "updateIDs=true"), &SonarrTrashPayload{ Instance: instance, NewMaps: maps, - }, false) + }, true) if err != nil { c.sonarrRP[instance] = maps return fmt.Errorf("updating quality release ID map: %w", err) From c3de0ebd90c378792618a8325f387204164978f3 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Fri, 21 Jan 2022 22:11:59 -0800 Subject: [PATCH 09/10] Add new trash aggregate handler for website use --- pkg/client/handlers.go | 2 + pkg/client/handlers_trash.go | 117 +++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 pkg/client/handlers_trash.go diff --git a/pkg/client/handlers.go b/pkg/client/handlers.go index 716ecd8ab..ee2838f52 100644 --- a/pkg/client/handlers.go +++ b/pkg/client/handlers.go @@ -21,6 +21,8 @@ func (c *Client) httpHandlers() { c.Config.HandleAPIpath("", "version", c.versionHandler, "GET", "HEAD") c.Config.HandleAPIpath("", "trigger/{trigger:[0-9a-z-]+}", c.handleTrigger, "GET") c.Config.HandleAPIpath("", "trigger/{trigger:[0-9a-z-]+}/{content}", c.handleTrigger, "GET") + // Aggregate handlers. Non-app specific. + c.Config.HandleAPIpath("", "/trash/{app}", c.aggregateTrash, "POST") if c.Config.Plex.Configured() { c.Config.HandleAPIpath(starr.Plex, "sessions", c.Config.Plex.HandleSessions, "GET") diff --git a/pkg/client/handlers_trash.go b/pkg/client/handlers_trash.go new file mode 100644 index 000000000..4a3088361 --- /dev/null +++ b/pkg/client/handlers_trash.go @@ -0,0 +1,117 @@ +//nolint:dupl +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "sync" + + "github.com/Notifiarr/notifiarr/pkg/apps" + "github.com/Notifiarr/notifiarr/pkg/notifiarr" + "github.com/gorilla/mux" +) + +/* The site relies on release and quality profiles data from Radarr and Sonarr. + * If someone has several instances, it causes slow page loads times. + * So we made this file to aggregate responses from each of the app types. + */ + +func (c *Client) aggregateTrash(req *http.Request) (int, interface{}) { + var wait sync.WaitGroup + defer wait.Wait() + + var input struct { + Radarr struct { // used for "all" + Instances notifiarr.IntList `json:"instances"` + } `json:"radarr"` + Sonarr struct { // used for "all" + Instances notifiarr.IntList `json:"instances"` + } `json:"sonarr"` + Instances notifiarr.IntList `json:"instances"` + } + // Extract POST payload. + err := json.NewDecoder(req.Body).Decode(&input) + + switch app := mux.Vars(req)["app"]; { + default: + return http.StatusBadRequest, fmt.Errorf("%w: %s", apps.ErrInvalidApp, app) + case err != nil: + return http.StatusBadRequest, fmt.Errorf("decoding POST payload: (app: %s) %w", app, err) + case app == "sonarr": + return http.StatusOK, c.aggregateTrashSonarr(req.Context(), &wait, input.Instances) + case app == "radarr": + return http.StatusOK, c.aggregateTrashRadarr(req.Context(), &wait, input.Instances) + case app == "all": + return http.StatusOK, map[string]interface{}{ + "radarr": c.aggregateTrashRadarr(req.Context(), &wait, input.Radarr.Instances), + "sonarr": c.aggregateTrashSonarr(req.Context(), &wait, input.Sonarr.Instances), + } + } +} + +func (c *Client) aggregateTrashSonarr(ctx context.Context, wait *sync.WaitGroup, + instances notifiarr.IntList) []*notifiarr.SonarrTrashPayload { + output := []*notifiarr.SonarrTrashPayload{} + // Create our known+requested instances, so we can write slice values in go routines. + for i, app := range c.Config.Apps.Sonarr { + if instance := i + 1; instances.Has(instance) { + output = append(output, ¬ifiarr.SonarrTrashPayload{Instance: instance, Name: app.Name}) + } + } + + var err error + // Grab data for each requested instance in parallel/go routine. + for idx := range output { + wait.Add(1) + + go func(idx, instance int) { + defer wait.Done() + // Add the profiles, and/or error into our data structure/output data. + app := c.Config.Apps.Sonarr[instance-1] + if output[idx].QualityProfiles, err = app.GetQualityProfilesContext(ctx); err != nil { + output[idx].Error = fmt.Sprintf("getting quality profiles: %v", err) + c.Errorf("Handling Sonarr API request (%d): %s", instance, output[idx].Error) + } else if output[idx].ReleaseProfiles, err = app.GetReleaseProfilesContext(ctx); err != nil { + output[idx].Error = fmt.Sprintf("getting release profiles: %v", err) + c.Errorf("Handling Sonarr API request (%d): %s", instance, output[idx].Error) + } + }(idx, output[idx].Instance) + } + + return output +} + +// This is basically a duplicate of the above code. +func (c *Client) aggregateTrashRadarr(ctx context.Context, wait *sync.WaitGroup, + instances notifiarr.IntList) []*notifiarr.RadarrTrashPayload { + output := []*notifiarr.RadarrTrashPayload{} + // Create our known+requested instances, so we can write slice values in go routines. + for i, app := range c.Config.Apps.Radarr { + if instance := i + 1; instances.Has(instance) { + output = append(output, ¬ifiarr.RadarrTrashPayload{Instance: instance, Name: app.Name}) + } + } + + var err error + // Grab data for each requested instance in parallel/go routine. + for idx := range output { + wait.Add(1) + + go func(idx, instance int) { + defer wait.Done() + // Add the profiles, and/or error into our data structure/output data. + app := c.Config.Apps.Radarr[instance-1] + if output[idx].QualityProfiles, err = app.GetQualityProfilesContext(ctx); err != nil { + output[idx].Error = fmt.Sprintf("getting quality profiles: %v", err) + c.Errorf("Handling Radarr API request (%d): %s", instance, output[idx].Error) + } else if output[idx].CustomFormats, err = app.GetCustomFormatsContext(ctx); err != nil { + output[idx].Error = fmt.Sprintf("getting custom formats: %v", err) + c.Errorf("Handling Radarr API request (%d): %s", instance, output[idx].Error) + } + }(idx, output[idx].Instance) + } + + return output +} From 45e5f7657e7fd41632846aa2819ebee4379f70fd Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Fri, 21 Jan 2022 23:37:35 -0800 Subject: [PATCH 10/10] fix file mode for extra log files --- pkg/logs/logs.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/logs/logs.go b/pkg/logs/logs.go index cffb50016..0bceb097a 100644 --- a/pkg/logs/logs.go +++ b/pkg/logs/logs.go @@ -81,6 +81,7 @@ func New() *Logger { // SetupLogging splits log writers into a file and/or stdout. func (l *Logger) SetupLogging(config *LogConfig) { + fileMode = config.FileMode.Mode() l.logs = config l.setDefaultLogPaths() l.setLogPaths()