Skip to content

Commit

Permalink
Moved apps handlers into their own package. (#9)
Browse files Browse the repository at this point in the history
* Rename an error
* make a new package with apps
* remove sync
* slide a few more pices around
  • Loading branch information
davidnewhall authored Dec 29, 2020
1 parent 6d13c98 commit 4c01de5
Show file tree
Hide file tree
Showing 14 changed files with 353 additions and 323 deletions.
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"os"
"time"

"github.com/Go-Lift-TV/discordnotifier-client/pkg/dnclient"
"github.com/Go-Lift-TV/discordnotifier-client/pkg/client"
)

func main() {
Expand All @@ -25,7 +25,7 @@ func run() error {
log.Print(err)
}

return dnclient.Start()
return client.Start()
}

func setTimeZone(tz string) (err error) {
Expand Down
185 changes: 185 additions & 0 deletions pkg/apps/apps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package apps

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"path"
"strconv"
"time"

"github.com/gorilla/mux"
"golift.io/starr/lidarr"
"golift.io/starr/radarr"
"golift.io/starr/readarr"
"golift.io/starr/sonarr"
)

/* This file contains:
*** The middleware procedure that stores the app interface in a request context.
*** Procedures to save and fetch an app interface into/from a request content.
*/

// Apps is the input configuration to relay requests to Starr apps.
type Apps struct {
APIKey string `json:"api_key" toml:"api_key" xml:"api_key" yaml:"api_key"`
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"`
Router *mux.Router `json:"-" toml:"-" xml:"-" yaml:"-"`
ErrorLog *log.Logger `json:"-" toml:"-" xml:"-" yaml:"-"`
}

// Responder converts all our data to a JSON response.

// App allows safely storing context values.
type App string

// Constant for each app to unique identify itself.
// These strings are also used as a suffix to the /api/ web path.
const (
Sonarr App = "sonarr"
Readarr App = "readarr"
Radarr App = "radarr"
Lidarr App = "lidarr"
)

// Errors sent to client web requests.
var (
ErrNoTMDB = fmt.Errorf("TMDB ID must not be empty")
ErrNoGRID = fmt.Errorf("GRID ID must not be empty")
ErrNoTVDB = fmt.Errorf("TVDB ID must not be empty")
ErrNoMBID = fmt.Errorf("MBID ID must not be empty")
ErrNoRadarr = fmt.Errorf("configured radarr ID not found")
ErrNoSonarr = fmt.Errorf("configured sonarr ID not found")
ErrNoLidarr = fmt.Errorf("configured lidarr ID not found")
ErrNoReadarr = fmt.Errorf("configured readarr ID not found")
ErrExists = fmt.Errorf("the requested item already exists")
)

// APIHandler is our custom handler function for APIs.
type APIHandler func(r *http.Request) (int, interface{})

// HandleAPIpath makes adding API paths a little cleaner.
// This grabs the app struct and saves it in a context before calling the handler.
func (a *Apps) HandleAPIpath(app App, api string, next APIHandler, method ...string) {
if len(method) == 0 {
method = []string{"GET"}
}

id := "{id:[0-9]+}"
if app == "" {
id = ""
}

// disccordnotifier uses 1-indexes.
a.Router.Handle(path.Join("/", a.URLBase, "api", string(app), id, api),
a.checkAPIKey(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now() // Capture the starr app request time in a response header.
switch id, _ := strconv.Atoi(mux.Vars(r)["id"]); {
default: // unknown app, just run the handler.
i, m := next(r)
a.Respond(w, i, m, start)
case app == Radarr && (id > len(a.Radarr) || id < 1):
a.Respond(w, http.StatusUnprocessableEntity, fmt.Errorf("%v: %w", id, ErrNoRadarr), start)
case app == Lidarr && (id > len(a.Lidarr) || id < 1):
a.Respond(w, http.StatusUnprocessableEntity, fmt.Errorf("%v: %w", id, ErrNoLidarr), start)
case app == Sonarr && (id > len(a.Sonarr) || id < 1):
a.Respond(w, http.StatusUnprocessableEntity, fmt.Errorf("%v: %w", id, ErrNoSonarr), start)
case app == Readarr && (id > len(a.Readarr) || id < 1):
a.Respond(w, http.StatusUnprocessableEntity, fmt.Errorf("%v: %w", id, ErrNoReadarr), start)

// These store the application configuration (starr) in a context then pass that into the next method.
// They retrieve the return code and output, then send a response (a.respond).
case app == Radarr:
i, m := next(r.WithContext(context.WithValue(r.Context(), Radarr, a.Radarr[id-1])))
a.Respond(w, i, m, start)
case app == Lidarr:
i, m := next(r.WithContext(context.WithValue(r.Context(), Lidarr, a.Lidarr[id-1])))
a.Respond(w, i, m, start)
case app == Sonarr:
i, m := next(r.WithContext(context.WithValue(r.Context(), Sonarr, a.Sonarr[id-1])))
a.Respond(w, i, m, start)
case app == Readarr:
i, m := next(r.WithContext(context.WithValue(r.Context(), Readarr, a.Readarr[id-1])))
a.Respond(w, i, m, start)
}
}))).Methods(method...)
}

// checkAPIKey drops a 403 if the API key doesn't match.
func (a *Apps) checkAPIKey(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-API-Key") != a.APIKey {
w.WriteHeader(http.StatusUnauthorized)
} else {
next.ServeHTTP(w, r)
}
})
}

// InitHandlers activates all our handlers.
func (a *Apps) InitHandlers() {
a.radarrHandlers()
a.readarrHandlers()
a.lidarrHandlers()
a.sonarrHandlers()
}

// Setup creates request interfaces and sets the timeout for each server.
func (a *Apps) Setup(timeout time.Duration) {
for i := range a.Radarr {
a.Radarr[i].fix(timeout)
}

for i := range a.Readarr {
a.Readarr[i].fix(timeout)
}

for i := range a.Sonarr {
a.Sonarr[i].fix(timeout)
}

for i := range a.Lidarr {
a.Lidarr[i].fix(timeout)
}
}

func (a *Apps) Respond(w http.ResponseWriter, stat int, msg interface{}, start time.Time) {
w.Header().Set("X-Request-Time", time.Since(start).Round(time.Microsecond).String())
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(stat)

statusTxt := strconv.Itoa(stat) + ": " + http.StatusText(stat)

if m, ok := msg.(error); ok {
a.ErrorLog.Printf("Status: %s, Message: %v", statusTxt, m)
msg = m.Error()
}

b, _ := json.Marshal(map[string]interface{}{"status": statusTxt, "message": msg})
_, _ = w.Write(b)
_, _ = w.Write([]byte("\n")) // curl likes new lines.
}

/* Every API call runs one of these methods to find the interface for the respective app. */

func getLidarr(r *http.Request) *lidarr.Lidarr {
return r.Context().Value(Lidarr).(*LidarrConfig).lidarr
}

func getRadarr(r *http.Request) *radarr.Radarr {
return r.Context().Value(Radarr).(*RadarrConfig).radarr
}

func getReadarr(r *http.Request) *readarr.Readarr {
return r.Context().Value(Readarr).(*ReadarrConfig).readarr
}

func getSonarr(r *http.Request) *sonarr.Sonarr {
return r.Context().Value(Sonarr).(*SonarrConfig).sonarr
}
39 changes: 27 additions & 12 deletions pkg/dnclient/lidarr.go → pkg/apps/lidarr.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package dnclient
package apps

import (
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/gorilla/mux"
"golift.io/starr"
"golift.io/starr/lidarr"
)

Expand All @@ -14,15 +16,28 @@ mbid - music brainz is the source for lidarr (todo)
*/

// lidarrHandlers is called once on startup to register the web API paths.
func (c *Client) lidarrHandlers() {
c.handleAPIpath(Lidarr, "/add", c.lidarrAddAlbum, "POST")
c.handleAPIpath(Lidarr, "/check/{albumid:[-a-z0-9]+}", c.lidarrCheckAlbum, "GET")
c.handleAPIpath(Lidarr, "/qualityProfiles", c.lidarrProfiles, "GET")
c.handleAPIpath(Lidarr, "/qualityDefinitions", c.lidarrQualityDefs, "GET")
c.handleAPIpath(Lidarr, "/rootFolder", c.lidarrRootFolders, "GET")
func (a *Apps) lidarrHandlers() {
a.HandleAPIpath(Lidarr, "/add", lidarrAddAlbum, "POST")
a.HandleAPIpath(Lidarr, "/check/{albumid:[-a-z0-9]+}", lidarrCheckAlbum, "GET")
a.HandleAPIpath(Lidarr, "/qualityProfiles", lidarrProfiles, "GET")
a.HandleAPIpath(Lidarr, "/qualityDefinitions", lidarrQualityDefs, "GET")
a.HandleAPIpath(Lidarr, "/rootFolder", lidarrRootFolders, "GET")
}

func (c *Client) lidarrRootFolders(r *http.Request) (int, interface{}) {
// LidarrConfig represents the input data for a Lidarr server.
type LidarrConfig struct {
*starr.Config
lidarr *lidarr.Lidarr
}

func (r *LidarrConfig) fix(timeout time.Duration) {
r.lidarr = lidarr.New(r.Config)
if r.Timeout.Duration == 0 {
r.Timeout.Duration = timeout
}
}

func lidarrRootFolders(r *http.Request) (int, interface{}) {
// Get folder list from Lidarr.
folders, err := getLidarr(r).GetRootFolders()
if err != nil {
Expand All @@ -38,7 +53,7 @@ func (c *Client) lidarrRootFolders(r *http.Request) (int, interface{}) {
return http.StatusOK, p
}

func (c *Client) lidarrProfiles(r *http.Request) (int, interface{}) {
func lidarrProfiles(r *http.Request) (int, interface{}) {
// Get the profiles from lidarr.
profiles, err := getLidarr(r).GetQualityProfiles()
if err != nil {
Expand All @@ -54,7 +69,7 @@ func (c *Client) lidarrProfiles(r *http.Request) (int, interface{}) {
return http.StatusOK, p
}

func (c *Client) lidarrQualityDefs(r *http.Request) (int, interface{}) {
func lidarrQualityDefs(r *http.Request) (int, interface{}) {
// Get the profiles from lidarr.
definitions, err := getLidarr(r).GetQualityDefinition()
if err != nil {
Expand All @@ -70,7 +85,7 @@ func (c *Client) lidarrQualityDefs(r *http.Request) (int, interface{}) {
return http.StatusOK, p
}

func (c *Client) lidarrCheckAlbum(r *http.Request) (int, interface{}) {
func lidarrCheckAlbum(r *http.Request) (int, interface{}) {
// Check for existing movie.
if m, err := getLidarr(r).GetAlbum(mux.Vars(r)["albumid"]); err != nil {
return http.StatusServiceUnavailable, fmt.Errorf("checking album: %w", err)
Expand All @@ -81,7 +96,7 @@ func (c *Client) lidarrCheckAlbum(r *http.Request) (int, interface{}) {
return http.StatusOK, http.StatusText(http.StatusNotFound)
}

func (c *Client) lidarrAddAlbum(r *http.Request) (int, interface{}) {
func lidarrAddAlbum(r *http.Request) (int, interface{}) {
var payload lidarr.AddAlbumInput
// Extract payload and check for TMDB ID.
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
Expand Down
39 changes: 27 additions & 12 deletions pkg/dnclient/radarr.go → pkg/apps/radarr.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
package dnclient
package apps

import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"

"github.com/gorilla/mux"
"golift.io/starr"
"golift.io/starr/radarr"
)

// radarrHandlers is called once on startup to register the web API paths.
func (c *Client) radarrHandlers() {
c.handleAPIpath(Radarr, "/add", c.radarrAddMovie, "POST")
c.handleAPIpath(Radarr, "/search/{query}", c.radarrSearchMovie, "GET")
c.handleAPIpath(Radarr, "/check/{tmdbid:[0-9]+}", c.radarrCheckMovie, "GET")
c.handleAPIpath(Radarr, "/qualityProfiles", c.radarrProfiles, "GET")
c.handleAPIpath(Radarr, "/rootFolder", c.radarrRootFolders, "GET")
func (a *Apps) radarrHandlers() {
a.HandleAPIpath(Radarr, "/add", radarrAddMovie, "POST")
a.HandleAPIpath(Radarr, "/search/{query}", radarrSearchMovie, "GET")
a.HandleAPIpath(Radarr, "/check/{tmdbid:[0-9]+}", radarrCheckMovie, "GET")
a.HandleAPIpath(Radarr, "/qualityProfiles", radarrProfiles, "GET")
a.HandleAPIpath(Radarr, "/rootFolder", radarrRootFolders, "GET")
}

func (c *Client) radarrRootFolders(r *http.Request) (int, interface{}) {
// RadarrConfig represents the input data for a Radarr server.
type RadarrConfig struct {
*starr.Config
radarr *radarr.Radarr `json:"-" toml:"-" xml:"-" yaml:"-"`
}

func (r *RadarrConfig) fix(timeout time.Duration) {
r.radarr = radarr.New(r.Config)
if r.Timeout.Duration == 0 {
r.Timeout.Duration = timeout
}
}

func radarrRootFolders(r *http.Request) (int, interface{}) {
// Get folder list from Radarr.
folders, err := getRadarr(r).GetRootFolders()
if err != nil {
Expand All @@ -36,7 +51,7 @@ func (c *Client) radarrRootFolders(r *http.Request) (int, interface{}) {
return http.StatusOK, p
}

func (c *Client) radarrProfiles(r *http.Request) (int, interface{}) {
func radarrProfiles(r *http.Request) (int, interface{}) {
// Get the profiles from radarr.
profiles, err := getRadarr(r).GetQualityProfiles()
if err != nil {
Expand All @@ -52,7 +67,7 @@ func (c *Client) radarrProfiles(r *http.Request) (int, interface{}) {
return http.StatusOK, p
}

func (c *Client) radarrCheckMovie(r *http.Request) (int, interface{}) {
func radarrCheckMovie(r *http.Request) (int, interface{}) {
tmdbID, _ := strconv.ParseInt(mux.Vars(r)["tmdbid"], 10, 64)
// Check for existing movie.
if m, err := getRadarr(r).GetMovie(tmdbID); err != nil {
Expand All @@ -64,7 +79,7 @@ func (c *Client) radarrCheckMovie(r *http.Request) (int, interface{}) {
return http.StatusOK, http.StatusText(http.StatusNotFound)
}

func (c *Client) radarrSearchMovie(r *http.Request) (int, interface{}) {
func radarrSearchMovie(r *http.Request) (int, interface{}) {
// Get all movies
movies, err := getRadarr(r).GetMovie(0)
if err != nil {
Expand Down Expand Up @@ -108,7 +123,7 @@ func movieSearch(query string, titles []string, alts []*radarr.AlternativeTitle)
return false
}

func (c *Client) radarrAddMovie(r *http.Request) (int, interface{}) {
func radarrAddMovie(r *http.Request) (int, interface{}) {
payload := &radarr.AddMovieInput{}
// Extract payload and check for TMDB ID.
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
Expand Down
Loading

0 comments on commit 4c01de5

Please sign in to comment.