feat: add sonarr support

This commit is contained in:
Dawid Wysokiński 2022-07-14 07:12:37 +02:00
parent 456c7b2018
commit 31608426e4
Signed by: Kichiyaki
GPG Key ID: 1ECC5DE481BE5184
6 changed files with 192 additions and 1 deletions

6
go.mod
View File

@ -2,7 +2,11 @@ module gitea.dwysokinski.me/Kichiyaki/notificationarr
go 1.18
require github.com/stretchr/testify v1.8.0
require (
github.com/go-chi/chi/v5 v5.0.7
github.com/google/go-cmp v0.5.8
github.com/stretchr/testify v1.8.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect

4
go.sum
View File

@ -1,6 +1,10 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

52
internal/rest/rest.go Normal file
View File

@ -0,0 +1,52 @@
package rest
import (
"encoding/json"
"errors"
"net/http"
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
)
var (
errInternal = domain.NewError(
domain.WithCode(domain.ErrorCodeUnknown),
domain.WithMessage("internal server error"),
)
)
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
}
type ErrorResponse struct {
Error APIError `json:"error"`
}
func renderErrorResponse(w http.ResponseWriter, err error) {
var convError domain.Error
if !errors.As(err, &convError) {
convError = errInternal
}
renderJSON(w, errorCodeToHTTPStatus(convError.Code()), ErrorResponse{
Error: APIError{
Code: convError.Code().String(),
Message: convError.Message(),
},
})
}
func errorCodeToHTTPStatus(code domain.ErrorCode) int {
switch code {
case domain.ErrorCodeValidation:
return http.StatusBadRequest
case domain.ErrorCodeUnknown:
fallthrough
default:
return http.StatusInternalServerError
}
}
func renderJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(data)
}

View File

@ -0,0 +1,33 @@
package rest
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
)
func doRequest(mux chi.Router, method, target string, body io.Reader) *http.Response {
rr := httptest.NewRecorder()
req := httptest.NewRequest(method, target, body)
mux.ServeHTTP(rr, req)
return rr.Result()
}
func assertResponse(tb testing.TB, resp *http.Response, expectedStatus int, expected any, target any) {
tb.Helper()
assert.Equal(tb, expectedStatus, resp.StatusCode)
assert.NoError(tb, json.NewDecoder(resp.Body).Decode(target))
opts := cmp.Options{
cmpopts.IgnoreUnexported(time.Time{}),
}
assert.True(tb, cmp.Equal(expected, target, opts...), cmp.Diff(expected, target, opts...))
}

View File

@ -0,0 +1,91 @@
package rest
import (
"context"
"encoding/json"
"fmt"
"net/http"
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
"github.com/go-chi/chi/v5"
)
type SonarrService interface {
Process(ctx context.Context, payload domain.SonarrWebhookPayload) error
}
type WebhookHandler struct {
sonarrSvc SonarrService
}
func NewWebhookHandler(sonarrSvc SonarrService) *WebhookHandler {
return &WebhookHandler{
sonarrSvc: sonarrSvc,
}
}
func (h *WebhookHandler) Register(r chi.Router) {
r.Post("/webhook/sonarr", h.handleSonarrWebhook)
}
type SonarrSeries struct {
ID int64 `json:"id"`
Title string `json:"title"`
}
type SonarrEpisode struct {
ID int64 `json:"id"`
EpisodeNumber int16 `json:"episodeNumber"`
SeasonNumber int16 `json:"seasonNumber"`
Title string `json:"title"`
}
type SonarrWebhookRequest struct {
EventType string `json:"eventType"`
Series SonarrSeries `json:"series"`
Episodes []SonarrEpisode `json:"episodes"`
}
func (h *WebhookHandler) handleSonarrWebhook(w http.ResponseWriter, r *http.Request) {
var req SonarrWebhookRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
renderErrorResponse(w, domain.NewError(
domain.WithErr(err),
domain.WithCode(domain.ErrorCodeValidation),
domain.WithMessage("invalid request body"),
))
return
}
eventType, err := domain.NewSonarrEventType(req.EventType)
if err != nil {
renderErrorResponse(w, fmt.Errorf("domain.NewSonarrEventType: %w", err))
return
}
payload := domain.SonarrWebhookPayload{
EventType: eventType,
Series: domain.SonarrSeries{
ID: req.Series.ID,
Title: req.Series.Title,
},
Episodes: make([]domain.SonarrEpisode, 0, len(req.Episodes)),
}
for _, ep := range req.Episodes {
payload.Episodes = append(payload.Episodes, domain.SonarrEpisode{
ID: ep.ID,
EpisodeNumber: ep.EpisodeNumber,
SeasonNumber: ep.SeasonNumber,
Title: ep.Title,
})
}
if err := h.sonarrSvc.Process(r.Context(), payload); err != nil {
renderErrorResponse(w, fmt.Errorf("SonarrService.Process: %w", err))
return
}
w.WriteHeader(http.StatusOK)
}

View File

@ -0,0 +1,7 @@
package rest_test
import "testing"
func TestWebhookHandler_Sonarr(t *testing.T) {
t.Parallel()
}