Reviewed-on: #5
This commit is contained in:
parent
048e7601e5
commit
79cc49d98e
|
@ -5,6 +5,7 @@ set -o errexit -eo pipefail
|
||||||
cd ./internal/tools
|
cd ./internal/tools
|
||||||
|
|
||||||
go install \
|
go install \
|
||||||
github.com/golangci/golangci-lint/cmd/golangci-lint
|
github.com/golangci/golangci-lint/cmd/golangci-lint \
|
||||||
|
github.com/maxbrunsfeld/counterfeiter/v6
|
||||||
|
|
||||||
cd ../..
|
cd ../..
|
||||||
|
|
9
go.mod
9
go.mod
|
@ -2,10 +2,17 @@ module gitea.dwysokinski.me/Kichiyaki/notificationarr
|
||||||
|
|
||||||
go 1.18
|
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/google/uuid v1.3.0
|
||||||
|
github.com/stretchr/testify v1.8.0
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/kr/pretty v0.1.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
14
go.sum
14
go.sum
|
@ -1,6 +1,17 @@
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
@ -8,8 +19,9 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -6,10 +6,16 @@ type ErrorCode uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ErrorCodeUnknown ErrorCode = iota
|
ErrorCodeUnknown ErrorCode = iota
|
||||||
|
ErrorCodeValidation
|
||||||
|
ErrorCodeIO // External I/O error such as network failure.
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e ErrorCode) String() string {
|
func (e ErrorCode) String() string {
|
||||||
switch e {
|
switch e {
|
||||||
|
case ErrorCodeValidation:
|
||||||
|
return "validation-error"
|
||||||
|
case ErrorCodeIO:
|
||||||
|
return "io-error"
|
||||||
case ErrorCodeUnknown:
|
case ErrorCodeUnknown:
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -13,6 +13,8 @@ func TestErrorCode_String(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
assert.Equal(t, domain.ErrorCodeUnknown.String(), "internal-server-error")
|
assert.Equal(t, domain.ErrorCodeUnknown.String(), "internal-server-error")
|
||||||
|
assert.Equal(t, domain.ErrorCodeValidation.String(), "validation-error")
|
||||||
|
assert.Equal(t, domain.ErrorCodeIO.String(), "io-error")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestError(t *testing.T) {
|
func TestError(t *testing.T) {
|
||||||
|
@ -25,7 +27,7 @@ func TestError(t *testing.T) {
|
||||||
expectedMsg string
|
expectedMsg string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "OK: WithCode, WithMessage",
|
name: "OK: WithCode(domain.ErrorCodeUnknown), WithMessage",
|
||||||
options: []domain.ErrorOption{
|
options: []domain.ErrorOption{
|
||||||
domain.WithCode(domain.ErrorCodeUnknown),
|
domain.WithCode(domain.ErrorCodeUnknown),
|
||||||
domain.WithMessage("err err"),
|
domain.WithMessage("err err"),
|
||||||
|
@ -35,7 +37,27 @@ func TestError(t *testing.T) {
|
||||||
expectedMsg: "err err",
|
expectedMsg: "err err",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "OK: WithCode, WithMessagef, WithErr",
|
name: "OK: WithCode(domain.ErrorCodeValidation), WithMessage",
|
||||||
|
options: []domain.ErrorOption{
|
||||||
|
domain.WithCode(domain.ErrorCodeValidation),
|
||||||
|
domain.WithMessage("err err"),
|
||||||
|
},
|
||||||
|
expectedCode: domain.ErrorCodeValidation,
|
||||||
|
expectedErr: nil,
|
||||||
|
expectedMsg: "err err",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OK: WithCode(domain.ErrorCodeIO), WithMessage",
|
||||||
|
options: []domain.ErrorOption{
|
||||||
|
domain.WithCode(domain.ErrorCodeIO),
|
||||||
|
domain.WithMessage("err err"),
|
||||||
|
},
|
||||||
|
expectedCode: domain.ErrorCodeIO,
|
||||||
|
expectedErr: nil,
|
||||||
|
expectedMsg: "err err",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OK: WithCode(domain.ErrorCodeUnknown), WithMessagef, WithErr",
|
||||||
options: []domain.ErrorOption{
|
options: []domain.ErrorOption{
|
||||||
domain.WithCode(domain.ErrorCodeUnknown),
|
domain.WithCode(domain.ErrorCodeUnknown),
|
||||||
domain.WithMessagef("xxx %d", 25),
|
domain.WithMessagef("xxx %d", 25),
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
type SonarrEventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SonarrEventTypeDownload SonarrEventType = "Download"
|
||||||
|
SonarrEventTypeTest SonarrEventType = "Test"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnsupportedSonarrEventType = NewError(WithCode(ErrorCodeValidation), WithMessage("unsupported event type"))
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSonarrEventType(s string) (SonarrEventType, error) {
|
||||||
|
conv := SonarrEventType(s)
|
||||||
|
switch conv {
|
||||||
|
case SonarrEventTypeDownload,
|
||||||
|
SonarrEventTypeTest:
|
||||||
|
return conv, nil
|
||||||
|
default:
|
||||||
|
return "", ErrUnsupportedSonarrEventType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SonarrEventType) String() string {
|
||||||
|
return string(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SonarrSeries struct {
|
||||||
|
ID int64
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SonarrEpisode struct {
|
||||||
|
ID int64
|
||||||
|
EpisodeNumber int16
|
||||||
|
SeasonNumber int16
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SonarrWebhookPayload struct {
|
||||||
|
EventType SonarrEventType
|
||||||
|
Series SonarrSeries
|
||||||
|
Episodes []SonarrEpisode
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewSonarrEventType(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
events := []domain.SonarrEventType{domain.SonarrEventTypeDownload}
|
||||||
|
|
||||||
|
for _, ev := range events {
|
||||||
|
res, err := domain.NewSonarrEventType(ev.String())
|
||||||
|
assert.Equal(t, ev, res)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ERR: invalid event type", func(t *testing.T) {
|
||||||
|
events := []string{"test1", "test2", "aaaa", "bbb"}
|
||||||
|
|
||||||
|
for _, ev := range events {
|
||||||
|
res, err := domain.NewSonarrEventType(ev)
|
||||||
|
assert.Zero(t, res)
|
||||||
|
assert.ErrorIs(t, err, domain.ErrUnsupportedSonarrEventType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
// Code generated by counterfeiter. DO NOT EDIT.
|
||||||
|
package mock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FakeSonarrService struct {
|
||||||
|
ProcessStub func(context.Context, domain.SonarrWebhookPayload) error
|
||||||
|
processMutex sync.RWMutex
|
||||||
|
processArgsForCall []struct {
|
||||||
|
arg1 context.Context
|
||||||
|
arg2 domain.SonarrWebhookPayload
|
||||||
|
}
|
||||||
|
processReturns struct {
|
||||||
|
result1 error
|
||||||
|
}
|
||||||
|
processReturnsOnCall map[int]struct {
|
||||||
|
result1 error
|
||||||
|
}
|
||||||
|
invocations map[string][][]interface{}
|
||||||
|
invocationsMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeSonarrService) Process(arg1 context.Context, arg2 domain.SonarrWebhookPayload) error {
|
||||||
|
fake.processMutex.Lock()
|
||||||
|
ret, specificReturn := fake.processReturnsOnCall[len(fake.processArgsForCall)]
|
||||||
|
fake.processArgsForCall = append(fake.processArgsForCall, struct {
|
||||||
|
arg1 context.Context
|
||||||
|
arg2 domain.SonarrWebhookPayload
|
||||||
|
}{arg1, arg2})
|
||||||
|
stub := fake.ProcessStub
|
||||||
|
fakeReturns := fake.processReturns
|
||||||
|
fake.recordInvocation("Process", []interface{}{arg1, arg2})
|
||||||
|
fake.processMutex.Unlock()
|
||||||
|
if stub != nil {
|
||||||
|
return stub(arg1, arg2)
|
||||||
|
}
|
||||||
|
if specificReturn {
|
||||||
|
return ret.result1
|
||||||
|
}
|
||||||
|
return fakeReturns.result1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeSonarrService) ProcessCallCount() int {
|
||||||
|
fake.processMutex.RLock()
|
||||||
|
defer fake.processMutex.RUnlock()
|
||||||
|
return len(fake.processArgsForCall)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeSonarrService) ProcessCalls(stub func(context.Context, domain.SonarrWebhookPayload) error) {
|
||||||
|
fake.processMutex.Lock()
|
||||||
|
defer fake.processMutex.Unlock()
|
||||||
|
fake.ProcessStub = stub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeSonarrService) ProcessArgsForCall(i int) (context.Context, domain.SonarrWebhookPayload) {
|
||||||
|
fake.processMutex.RLock()
|
||||||
|
defer fake.processMutex.RUnlock()
|
||||||
|
argsForCall := fake.processArgsForCall[i]
|
||||||
|
return argsForCall.arg1, argsForCall.arg2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeSonarrService) ProcessReturns(result1 error) {
|
||||||
|
fake.processMutex.Lock()
|
||||||
|
defer fake.processMutex.Unlock()
|
||||||
|
fake.ProcessStub = nil
|
||||||
|
fake.processReturns = struct {
|
||||||
|
result1 error
|
||||||
|
}{result1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeSonarrService) ProcessReturnsOnCall(i int, result1 error) {
|
||||||
|
fake.processMutex.Lock()
|
||||||
|
defer fake.processMutex.Unlock()
|
||||||
|
fake.ProcessStub = nil
|
||||||
|
if fake.processReturnsOnCall == nil {
|
||||||
|
fake.processReturnsOnCall = make(map[int]struct {
|
||||||
|
result1 error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fake.processReturnsOnCall[i] = struct {
|
||||||
|
result1 error
|
||||||
|
}{result1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeSonarrService) Invocations() map[string][][]interface{} {
|
||||||
|
fake.invocationsMutex.RLock()
|
||||||
|
defer fake.invocationsMutex.RUnlock()
|
||||||
|
fake.processMutex.RLock()
|
||||||
|
defer fake.processMutex.RUnlock()
|
||||||
|
copiedInvocations := map[string][][]interface{}{}
|
||||||
|
for key, value := range fake.invocations {
|
||||||
|
copiedInvocations[key] = value
|
||||||
|
}
|
||||||
|
return copiedInvocations
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakeSonarrService) recordInvocation(key string, args []interface{}) {
|
||||||
|
fake.invocationsMutex.Lock()
|
||||||
|
defer fake.invocationsMutex.Unlock()
|
||||||
|
if fake.invocations == nil {
|
||||||
|
fake.invocations = map[string][][]interface{}{}
|
||||||
|
}
|
||||||
|
if fake.invocations[key] == nil {
|
||||||
|
fake.invocations[key] = [][]interface{}{}
|
||||||
|
}
|
||||||
|
fake.invocations[key] = append(fake.invocations[key], args)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ rest.SonarrService = new(FakeSonarrService)
|
|
@ -0,0 +1,56 @@
|
||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate counterfeiter -generate
|
||||||
|
|
||||||
|
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.ErrorCodeIO:
|
||||||
|
return http.StatusServiceUnavailable
|
||||||
|
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_test
|
||||||
|
|
||||||
|
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 assertJSONResponse(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,92 @@
|
||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
//counterfeiter:generate -o internal/mock/sonarr_service.gen.go . SonarrService
|
||||||
|
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.StatusNoContent)
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
package rest_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/rest"
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/rest/internal/mock"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWebhookHandler_Sonarr(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func(svc *mock.FakeSonarrService)
|
||||||
|
body io.Reader
|
||||||
|
expectedStatus int
|
||||||
|
target any
|
||||||
|
expectedResponse any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "OK: event type=Download",
|
||||||
|
setup: func(svc *mock.FakeSonarrService) {
|
||||||
|
svc.ProcessReturns(nil)
|
||||||
|
},
|
||||||
|
body: func() *bytes.Buffer {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = json.NewEncoder(&buf).Encode(rest.SonarrWebhookRequest{
|
||||||
|
EventType: "Download",
|
||||||
|
Series: rest.SonarrSeries{
|
||||||
|
ID: 1,
|
||||||
|
Title: "Series 1",
|
||||||
|
},
|
||||||
|
Episodes: []rest.SonarrEpisode{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
EpisodeNumber: 1,
|
||||||
|
SeasonNumber: 1,
|
||||||
|
Title: "Ep 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return &buf
|
||||||
|
}(),
|
||||||
|
expectedStatus: http.StatusNoContent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: only JSON is accepted",
|
||||||
|
setup: func(svc *mock.FakeSonarrService) {},
|
||||||
|
body: func() *bytes.Buffer {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = gob.NewEncoder(&buf).Encode(rest.SonarrWebhookRequest{
|
||||||
|
EventType: "Download",
|
||||||
|
Series: rest.SonarrSeries{
|
||||||
|
ID: 1,
|
||||||
|
Title: "Series 1",
|
||||||
|
},
|
||||||
|
Episodes: []rest.SonarrEpisode{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
EpisodeNumber: 1,
|
||||||
|
SeasonNumber: 1,
|
||||||
|
Title: "Ep 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return &buf
|
||||||
|
}(),
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
target: &rest.ErrorResponse{},
|
||||||
|
expectedResponse: &rest.ErrorResponse{
|
||||||
|
Error: rest.APIError{
|
||||||
|
Code: domain.ErrorCodeValidation.String(),
|
||||||
|
Message: "invalid request body",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: unsupported event type",
|
||||||
|
setup: func(svc *mock.FakeSonarrService) {},
|
||||||
|
body: func() *bytes.Buffer {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = json.NewEncoder(&buf).Encode(rest.SonarrWebhookRequest{
|
||||||
|
EventType: "xxxx",
|
||||||
|
})
|
||||||
|
return &buf
|
||||||
|
}(),
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
target: &rest.ErrorResponse{},
|
||||||
|
expectedResponse: &rest.ErrorResponse{
|
||||||
|
Error: rest.APIError{
|
||||||
|
Code: domain.ErrUnsupportedSonarrEventType.Code().String(),
|
||||||
|
Message: domain.ErrUnsupportedSonarrEventType.Message(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
svc := mock.FakeSonarrService{}
|
||||||
|
if tt.setup != nil {
|
||||||
|
tt.setup(&svc)
|
||||||
|
}
|
||||||
|
router := chi.NewRouter()
|
||||||
|
rest.NewWebhookHandler(&svc).Register(router)
|
||||||
|
|
||||||
|
resp := doRequest(router, http.MethodPost, "/webhook/sonarr", tt.body)
|
||||||
|
defer assert.NoError(t, resp.Body.Close())
|
||||||
|
if tt.target != nil && tt.expectedResponse != nil {
|
||||||
|
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, tt.expectedStatus, resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
// Code generated by counterfeiter. DO NOT EDIT.
|
||||||
|
package mock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FakePublisher struct {
|
||||||
|
PublishStub func(context.Context, string, string) error
|
||||||
|
publishMutex sync.RWMutex
|
||||||
|
publishArgsForCall []struct {
|
||||||
|
arg1 context.Context
|
||||||
|
arg2 string
|
||||||
|
arg3 string
|
||||||
|
}
|
||||||
|
publishReturns struct {
|
||||||
|
result1 error
|
||||||
|
}
|
||||||
|
publishReturnsOnCall map[int]struct {
|
||||||
|
result1 error
|
||||||
|
}
|
||||||
|
invocations map[string][][]interface{}
|
||||||
|
invocationsMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakePublisher) Publish(arg1 context.Context, arg2 string, arg3 string) error {
|
||||||
|
fake.publishMutex.Lock()
|
||||||
|
ret, specificReturn := fake.publishReturnsOnCall[len(fake.publishArgsForCall)]
|
||||||
|
fake.publishArgsForCall = append(fake.publishArgsForCall, struct {
|
||||||
|
arg1 context.Context
|
||||||
|
arg2 string
|
||||||
|
arg3 string
|
||||||
|
}{arg1, arg2, arg3})
|
||||||
|
stub := fake.PublishStub
|
||||||
|
fakeReturns := fake.publishReturns
|
||||||
|
fake.recordInvocation("Publish", []interface{}{arg1, arg2, arg3})
|
||||||
|
fake.publishMutex.Unlock()
|
||||||
|
if stub != nil {
|
||||||
|
return stub(arg1, arg2, arg3)
|
||||||
|
}
|
||||||
|
if specificReturn {
|
||||||
|
return ret.result1
|
||||||
|
}
|
||||||
|
return fakeReturns.result1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakePublisher) PublishCallCount() int {
|
||||||
|
fake.publishMutex.RLock()
|
||||||
|
defer fake.publishMutex.RUnlock()
|
||||||
|
return len(fake.publishArgsForCall)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakePublisher) PublishCalls(stub func(context.Context, string, string) error) {
|
||||||
|
fake.publishMutex.Lock()
|
||||||
|
defer fake.publishMutex.Unlock()
|
||||||
|
fake.PublishStub = stub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakePublisher) PublishArgsForCall(i int) (context.Context, string, string) {
|
||||||
|
fake.publishMutex.RLock()
|
||||||
|
defer fake.publishMutex.RUnlock()
|
||||||
|
argsForCall := fake.publishArgsForCall[i]
|
||||||
|
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakePublisher) PublishReturns(result1 error) {
|
||||||
|
fake.publishMutex.Lock()
|
||||||
|
defer fake.publishMutex.Unlock()
|
||||||
|
fake.PublishStub = nil
|
||||||
|
fake.publishReturns = struct {
|
||||||
|
result1 error
|
||||||
|
}{result1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakePublisher) PublishReturnsOnCall(i int, result1 error) {
|
||||||
|
fake.publishMutex.Lock()
|
||||||
|
defer fake.publishMutex.Unlock()
|
||||||
|
fake.PublishStub = nil
|
||||||
|
if fake.publishReturnsOnCall == nil {
|
||||||
|
fake.publishReturnsOnCall = make(map[int]struct {
|
||||||
|
result1 error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fake.publishReturnsOnCall[i] = struct {
|
||||||
|
result1 error
|
||||||
|
}{result1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakePublisher) Invocations() map[string][][]interface{} {
|
||||||
|
fake.invocationsMutex.RLock()
|
||||||
|
defer fake.invocationsMutex.RUnlock()
|
||||||
|
fake.publishMutex.RLock()
|
||||||
|
defer fake.publishMutex.RUnlock()
|
||||||
|
copiedInvocations := map[string][][]interface{}{}
|
||||||
|
for key, value := range fake.invocations {
|
||||||
|
copiedInvocations[key] = value
|
||||||
|
}
|
||||||
|
return copiedInvocations
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fake *FakePublisher) recordInvocation(key string, args []interface{}) {
|
||||||
|
fake.invocationsMutex.Lock()
|
||||||
|
defer fake.invocationsMutex.Unlock()
|
||||||
|
if fake.invocations == nil {
|
||||||
|
fake.invocations = map[string][][]interface{}{}
|
||||||
|
}
|
||||||
|
if fake.invocations[key] == nil {
|
||||||
|
fake.invocations[key] = [][]interface{}{}
|
||||||
|
}
|
||||||
|
fake.invocations[key] = append(fake.invocations[key], args)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ service.Publisher = new(FakePublisher)
|
|
@ -0,0 +1,61 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Ntfy struct {
|
||||||
|
client *http.Client
|
||||||
|
url string
|
||||||
|
topic string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNtfy(client *http.Client, url string, topic string, username string, password string) *Ntfy {
|
||||||
|
return &Ntfy{
|
||||||
|
client: client,
|
||||||
|
url: url,
|
||||||
|
topic: topic,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Ntfy) Publish(ctx context.Context, title, message string) error {
|
||||||
|
// initialize request
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, n.url+"/"+n.topic, strings.NewReader(message))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("http.NewRequestWithContext: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set required headers
|
||||||
|
req.Header.Set("Title", title)
|
||||||
|
if n.username != "" && n.password != "" {
|
||||||
|
req.SetBasicAuth(n.username, n.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// send request
|
||||||
|
resp, err := n.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("client.Do: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
_, _ = io.Copy(io.Discard, resp.Body) // discard body, as it is not needed
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return domain.NewError(
|
||||||
|
domain.WithCode(domain.ErrorCodeIO),
|
||||||
|
domain.WithMessagef("ntfy returned unexpected status code: %d", resp.StatusCode),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
package service_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/service"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNtfy_Publish(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
topic string
|
||||||
|
title string
|
||||||
|
message string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "without authorization",
|
||||||
|
topic: "topicc",
|
||||||
|
title: "title",
|
||||||
|
message: "msg",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with authorization",
|
||||||
|
topic: "topicc",
|
||||||
|
title: "title",
|
||||||
|
message: "msg",
|
||||||
|
username: "uname",
|
||||||
|
password: "pwd",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path[1:] != tt.topic {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Header.Get("Title") != tt.title {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, _ = w.Write([]byte("invalid title"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if uname, pwd, _ := r.BasicAuth(); uname != tt.username || pwd != tt.password {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, _ = w.Write([]byte("invalid username or password"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
err := service.NewNtfy(srv.Client(), srv.URL, tt.topic, tt.username, tt.password).
|
||||||
|
Publish(context.Background(), tt.title, tt.message)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ERR: status code != 200", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
err := service.NewNtfy(srv.Client(), srv.URL, "topic", "", "").
|
||||||
|
Publish(context.Background(), "title", "msg")
|
||||||
|
assert.ErrorIs(t, err, domain.NewError(
|
||||||
|
domain.WithCode(domain.ErrorCodeIO),
|
||||||
|
domain.WithMessagef("ntfy returned unexpected status code: %d", http.StatusNotImplemented),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
//go:generate counterfeiter -generate
|
||||||
|
|
||||||
|
//counterfeiter:generate -o internal/mock/publisher.gen.go . Publisher
|
||||||
|
type Publisher interface {
|
||||||
|
Publish(ctx context.Context, title, message string) error
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Sonarr struct {
|
||||||
|
publisher Publisher
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSonaar(publisher Publisher) *Sonarr {
|
||||||
|
return &Sonarr{
|
||||||
|
publisher: publisher,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sonarr) Process(ctx context.Context, payload domain.SonarrWebhookPayload) error {
|
||||||
|
for _, ep := range payload.Episodes {
|
||||||
|
title, err := s.buildTitle(payload.EventType, payload.Series, ep)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("buildTitle: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := s.buildMessage(payload.EventType, payload.Series, ep)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("buildMessage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.publisher.Publish(ctx, title, msg); err != nil {
|
||||||
|
return fmt.Errorf("Publisher.Publish: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sonarr) buildTitle(evType domain.SonarrEventType, series domain.SonarrSeries, _ domain.SonarrEpisode) (string, error) {
|
||||||
|
switch evType {
|
||||||
|
case domain.SonarrEventTypeDownload,
|
||||||
|
domain.SonarrEventTypeTest:
|
||||||
|
return series.Title + " - New episode (Sonarr)", nil
|
||||||
|
default:
|
||||||
|
return "", domain.ErrUnsupportedSonarrEventType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sonarr) buildMessage(evType domain.SonarrEventType, _ domain.SonarrSeries, ep domain.SonarrEpisode) (string, error) {
|
||||||
|
switch evType {
|
||||||
|
case domain.SonarrEventTypeDownload,
|
||||||
|
domain.SonarrEventTypeTest:
|
||||||
|
return fmt.Sprintf("S%d.E%d %s", ep.SeasonNumber, ep.EpisodeNumber, ep.Title), nil
|
||||||
|
default:
|
||||||
|
return "", domain.ErrUnsupportedSonarrEventType
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
package service_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/domain"
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/service"
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/service/internal/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSonarr_Process(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, evType := range [...]domain.SonarrEventType{
|
||||||
|
domain.SonarrEventTypeDownload,
|
||||||
|
domain.SonarrEventTypeTest,
|
||||||
|
} {
|
||||||
|
t.Run("event type="+evType.String(), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
publisher := &mock.FakePublisher{}
|
||||||
|
publisher.PublishReturns(nil)
|
||||||
|
|
||||||
|
payload := generateSonarrWebhookPayload(evType)
|
||||||
|
|
||||||
|
err := service.NewSonaar(publisher).Process(context.Background(), payload)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
require.Equal(t, len(payload.Episodes), publisher.PublishCallCount())
|
||||||
|
for i, ep := range payload.Episodes {
|
||||||
|
_, title, msg := publisher.PublishArgsForCall(i)
|
||||||
|
assert.Equal(t, payload.Series.Title+" - New episode (Sonarr)", title)
|
||||||
|
assert.Equal(t, fmt.Sprintf("S%d.E%d %s", ep.SeasonNumber, ep.EpisodeNumber, ep.Title), msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateSonarrWebhookPayload(ev domain.SonarrEventType) domain.SonarrWebhookPayload {
|
||||||
|
s := rand.NewSource(time.Now().UnixNano())
|
||||||
|
r := rand.New(s)
|
||||||
|
|
||||||
|
numEps := r.Int31n(19) + 1
|
||||||
|
eps := make([]domain.SonarrEpisode, 0, numEps)
|
||||||
|
for i := int32(0); i < numEps; i++ {
|
||||||
|
eps = append(eps, domain.SonarrEpisode{
|
||||||
|
ID: r.Int63(),
|
||||||
|
EpisodeNumber: int16(r.Int31n(100)),
|
||||||
|
SeasonNumber: int16(r.Int31n(100)),
|
||||||
|
Title: uuid.NewString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.SonarrWebhookPayload{
|
||||||
|
EventType: ev,
|
||||||
|
Series: domain.SonarrSeries{
|
||||||
|
ID: r.Int63(),
|
||||||
|
Title: uuid.NewString(),
|
||||||
|
},
|
||||||
|
Episodes: eps,
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,10 @@ module gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/tools
|
||||||
|
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require github.com/golangci/golangci-lint v1.46.2
|
require (
|
||||||
|
github.com/golangci/golangci-lint v1.46.2
|
||||||
|
github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
4d63.com/gochecknoglobals v0.1.0 // indirect
|
4d63.com/gochecknoglobals v0.1.0 // indirect
|
||||||
|
|
|
@ -533,6 +533,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
|
||||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0 h1:rBhB9Rls+yb8kA4x5a/cWxOufWfXt24E+kq4YlbGj3g=
|
||||||
|
github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0/go.mod h1:fJ0UAZc1fx3xZhU4eSHQDJ1ApFmTVhp5VTpV9tm2ogg=
|
||||||
github.com/mbilski/exhaustivestruct v1.2.0 h1:wCBmUnSYufAHO6J4AVWY6ff+oxWxsVFrwgOdMUQePUo=
|
github.com/mbilski/exhaustivestruct v1.2.0 h1:wCBmUnSYufAHO6J4AVWY6ff+oxWxsVFrwgOdMUQePUo=
|
||||||
github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc=
|
github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc=
|
||||||
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg=
|
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg=
|
||||||
|
@ -573,7 +575,6 @@ github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81
|
||||||
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
|
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
|
||||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA=
|
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA=
|
||||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8=
|
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/nishanths/exhaustive v0.7.11 h1:xV/WU3Vdwh5BUH4N06JNUznb6d5zhRPOnlgCrpNYNKA=
|
github.com/nishanths/exhaustive v0.7.11 h1:xV/WU3Vdwh5BUH4N06JNUznb6d5zhRPOnlgCrpNYNKA=
|
||||||
github.com/nishanths/exhaustive v0.7.11/go.mod h1:gX+MP7DWMKJmNa1HfMozK+u04hQd3na9i0hyqf3/dOI=
|
github.com/nishanths/exhaustive v0.7.11/go.mod h1:gX+MP7DWMKJmNa1HfMozK+u04hQd3na9i0hyqf3/dOI=
|
||||||
|
@ -689,6 +690,7 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
|
||||||
github.com/sagikazarmark/crypt v0.5.0/go.mod h1:l+nzl7KWh51rpzp2h7t4MZWyiEWdhNpOAnclKvg+mdA=
|
github.com/sagikazarmark/crypt v0.5.0/go.mod h1:l+nzl7KWh51rpzp2h7t4MZWyiEWdhNpOAnclKvg+mdA=
|
||||||
github.com/sanposhiho/wastedassign/v2 v2.0.6 h1:+6/hQIHKNJAUixEj6EmOngGIisyeI+T3335lYTyxRoA=
|
github.com/sanposhiho/wastedassign/v2 v2.0.6 h1:+6/hQIHKNJAUixEj6EmOngGIisyeI+T3335lYTyxRoA=
|
||||||
github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI=
|
github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI=
|
||||||
|
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
github.com/securego/gosec/v2 v2.11.0 h1:+PDkpzR41OI2jrw1q6AdXZCbsNGNGT7pQjal0H0cArI=
|
github.com/securego/gosec/v2 v2.11.0 h1:+PDkpzR41OI2jrw1q6AdXZCbsNGNGT7pQjal0H0cArI=
|
||||||
github.com/securego/gosec/v2 v2.11.0/go.mod h1:SX8bptShuG8reGC0XS09+a4H2BoWSJi+fscA+Pulbpo=
|
github.com/securego/gosec/v2 v2.11.0/go.mod h1:SX8bptShuG8reGC0XS09+a4H2BoWSJi+fscA+Pulbpo=
|
||||||
|
@ -1365,8 +1367,8 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||||
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
|
|
@ -5,4 +5,5 @@ package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||||
|
_ "github.com/maxbrunsfeld/counterfeiter/v6"
|
||||||
)
|
)
|
||||||
|
|
108
main.go
108
main.go
|
@ -2,32 +2,40 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/rest"
|
||||||
|
"gitea.dwysokinski.me/Kichiyaki/notificationarr/internal/service"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultPort = "9234"
|
defaultPort = "9234"
|
||||||
readTimeout = 2 * time.Second
|
readTimeout = 2 * time.Second
|
||||||
readHeaderTimeout = 2 * time.Second
|
readHeaderTimeout = 2 * time.Second
|
||||||
writeTimeout = 2 * time.Second
|
writeTimeout = 2 * time.Second
|
||||||
idleTimeout = 2 * time.Second
|
idleTimeout = 2 * time.Second
|
||||||
serverShutdownTimeout = 10 * time.Second
|
serverShutdownTimeout = 10 * time.Second
|
||||||
|
ntfyDefaultUrl = "https://ntfy.sh"
|
||||||
|
publisherDefaultTimeout = 5 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
srv := newServer()
|
srv, err := newServer()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("newServer:", err)
|
||||||
|
}
|
||||||
|
|
||||||
go func(srv *http.Server) {
|
go startServer(srv)
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
log.Fatalln("srv.ListenAndServe:", err)
|
|
||||||
}
|
|
||||||
}(srv)
|
|
||||||
|
|
||||||
log.Println("Server is listening on the port", defaultPort)
|
log.Println("Server is listening on the port", defaultPort)
|
||||||
|
|
||||||
|
@ -35,23 +43,83 @@ func main() {
|
||||||
|
|
||||||
ctxShutdown, cancelShutdown := context.WithTimeout(context.Background(), serverShutdownTimeout)
|
ctxShutdown, cancelShutdown := context.WithTimeout(context.Background(), serverShutdownTimeout)
|
||||||
defer cancelShutdown()
|
defer cancelShutdown()
|
||||||
err := srv.Shutdown(ctxShutdown)
|
err = srv.Shutdown(ctxShutdown)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("srv.Shutdown:", err)
|
log.Println("srv.Shutdown:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServer() *http.Server {
|
func newServer() (*http.Server, error) {
|
||||||
|
router, err := newRouter()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("newRouter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return &http.Server{
|
return &http.Server{
|
||||||
Addr: ":" + defaultPort,
|
Addr: ":" + defaultPort,
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
Handler: router,
|
||||||
_, _ = io.Copy(os.Stdout, r.Body)
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}),
|
|
||||||
ReadTimeout: readTimeout,
|
ReadTimeout: readTimeout,
|
||||||
ReadHeaderTimeout: readHeaderTimeout,
|
ReadHeaderTimeout: readHeaderTimeout,
|
||||||
WriteTimeout: writeTimeout,
|
WriteTimeout: writeTimeout,
|
||||||
IdleTimeout: idleTimeout,
|
IdleTimeout: idleTimeout,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRouter() (*chi.Mux, error) {
|
||||||
|
sonarrSvc, err := newSonarrService()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("newSonarrService: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Use(
|
||||||
|
middleware.RealIP,
|
||||||
|
middleware.RequestLogger(&middleware.DefaultLogFormatter{
|
||||||
|
NoColor: true,
|
||||||
|
Logger: log.Default(),
|
||||||
|
}),
|
||||||
|
middleware.Recoverer,
|
||||||
|
middleware.Heartbeat("/health"),
|
||||||
|
)
|
||||||
|
rest.NewWebhookHandler(sonarrSvc).Register(r)
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSonarrService() (*service.Sonarr, error) {
|
||||||
|
url := os.Getenv("SONARR_NTFY_URL")
|
||||||
|
if url == "" {
|
||||||
|
url = ntfyDefaultUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
topic := os.Getenv("SONARR_NTFY_TOPIC")
|
||||||
|
if topic == "" {
|
||||||
|
return nil, errors.New(`env "SONARR_NTFY_TOPIC" is required`)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := os.Getenv("SONARR_NTFY_REQ_TIMEOUT")
|
||||||
|
parsedTimeout := publisherDefaultTimeout
|
||||||
|
var err error
|
||||||
|
if timeout != "" {
|
||||||
|
parsedTimeout, err = time.ParseDuration(timeout)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(`os.Getenv("SONARR_NTFY_REQ_TIMEOUT"): %w`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return service.NewSonaar(
|
||||||
|
service.NewNtfy(
|
||||||
|
&http.Client{Timeout: parsedTimeout},
|
||||||
|
url,
|
||||||
|
topic,
|
||||||
|
os.Getenv("SONARR_NTFY_USERNAME"),
|
||||||
|
os.Getenv("SONARR_NTFY_PASSWORD"),
|
||||||
|
),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startServer(srv *http.Server) {
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalln("srv.ListenAndServe:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in New Issue