feat: add sonarr support
This commit is contained in:
parent
456c7b2018
commit
31608426e4
6
go.mod
6
go.mod
|
@ -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
4
go.sum
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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...))
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package rest_test
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestWebhookHandler_Sonarr(t *testing.T) {
|
||||
t.Parallel()
|
||||
}
|
Reference in New Issue