Skip to content

Commit

Permalink
Merge pull request #167 from Notifiarr/dn2_dashboard
Browse files Browse the repository at this point in the history
Fixes Aplenty
  • Loading branch information
davidnewhall authored Jan 22, 2022
2 parents d75da75 + 45e5f76 commit 023c8ec
Show file tree
Hide file tree
Showing 17 changed files with 446 additions and 255 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
39 changes: 24 additions & 15 deletions pkg/apps/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand All @@ -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()
Expand Down
8 changes: 5 additions & 3 deletions pkg/client/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import (
"golift.io/starr"
)

// internalHandlers initializes "special" internal API paths.
func (c *Client) internalHandlers() {
c.Config.HandleAPIpath("", "version", c.website.VersionHandler, "GET", "HEAD")
// 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")
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")
Expand Down
117 changes: 117 additions & 0 deletions pkg/client/handlers_trash.go
Original file line number Diff line number Diff line change
@@ -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, &notifiarr.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, &notifiarr.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
}
162 changes: 162 additions & 0 deletions pkg/client/handlers_version.go
Original file line number Diff line number Diff line change
@@ -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,
},
}}
}()
}
Loading

0 comments on commit 023c8ec

Please sign in to comment.