feat(filters): support partial update (#409)

This commit is contained in:
ze0s 2022-08-15 15:54:34 +02:00 committed by GitHub
parent fa20978d58
commit 3a334121f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 349 additions and 8 deletions

View file

@ -568,6 +568,198 @@ func (r *FilterRepo) Update(ctx context.Context, filter domain.Filter) (*domain.
return &filter, nil return &filter, nil
} }
func (r *FilterRepo) UpdatePartial(ctx context.Context, filter domain.FilterUpdate) error {
var err error
q := r.db.squirrel.Update("filter")
if filter.Name != nil {
q = q.Set("name", filter.Name)
}
if filter.Enabled != nil {
q = q.Set("enabled", filter.Enabled)
}
if filter.MinSize != nil {
q = q.Set("min_size", filter.MinSize)
}
if filter.MaxSize != nil {
q = q.Set("max_size", filter.MaxSize)
}
if filter.Delay != nil {
q = q.Set("delay", filter.Delay)
}
if filter.Priority != nil {
q = q.Set("priority", filter.Priority)
}
if filter.MaxDownloads != nil {
q = q.Set("max_downloads", filter.MaxDownloads)
}
if filter.MaxDownloadsUnit != nil {
q = q.Set("max_downloads_unit", filter.MaxDownloadsUnit)
}
if filter.UseRegex != nil {
q = q.Set("use_regex", filter.UseRegex)
}
if filter.MatchReleases != nil {
q = q.Set("match_releases", filter.MatchReleases)
}
if filter.ExceptReleases != nil {
q = q.Set("except_releases", filter.ExceptReleases)
}
if filter.MatchReleaseGroups != nil {
q = q.Set("match_release_groups", filter.MatchReleaseGroups)
}
if filter.ExceptReleaseGroups != nil {
q = q.Set("except_release_groups", filter.ExceptReleaseGroups)
}
if filter.Scene != nil {
q = q.Set("scene", filter.Scene)
}
if filter.Freeleech != nil {
q = q.Set("freeleech", filter.Freeleech)
}
if filter.FreeleechPercent != nil {
q = q.Set("freeleech_percent", filter.FreeleechPercent)
}
if filter.Shows != nil {
q = q.Set("shows", filter.Shows)
}
if filter.Seasons != nil {
q = q.Set("seasons", filter.Seasons)
}
if filter.Episodes != nil {
q = q.Set("episodes", filter.Episodes)
}
if filter.Resolutions != nil {
q = q.Set("resolutions", pq.Array(filter.Resolutions))
}
if filter.Codecs != nil {
q = q.Set("codecs", pq.Array(filter.Codecs))
}
if filter.Sources != nil {
q = q.Set("sources", pq.Array(filter.Sources))
}
if filter.Containers != nil {
q = q.Set("containers", pq.Array(filter.Containers))
}
if filter.MatchHDR != nil {
q = q.Set("match_hdr", pq.Array(filter.MatchHDR))
}
if filter.ExceptHDR != nil {
q = q.Set("except_hdr", pq.Array(filter.ExceptHDR))
}
if filter.MatchOther != nil {
q = q.Set("match_other", pq.Array(filter.MatchOther))
}
if filter.ExceptOther != nil {
q = q.Set("except_other", pq.Array(filter.ExceptOther))
}
if filter.Years != nil {
q = q.Set("years", filter.Years)
}
if filter.MatchCategories != nil {
q = q.Set("match_categories", filter.MatchCategories)
}
if filter.ExceptCategories != nil {
q = q.Set("except_categories", filter.ExceptCategories)
}
if filter.MatchUploaders != nil {
q = q.Set("match_uploaders", filter.MatchUploaders)
}
if filter.ExceptUploaders != nil {
q = q.Set("except_uploaders", filter.ExceptUploaders)
}
if filter.Tags != nil {
q = q.Set("tags", filter.Tags)
}
if filter.ExceptTags != nil {
q = q.Set("except_tags", filter.ExceptTags)
}
if filter.Artists != nil {
q = q.Set("artists", filter.Artists)
}
if filter.Albums != nil {
q = q.Set("albums", filter.Albums)
}
if filter.MatchReleaseTypes != nil {
q = q.Set("release_types_match", pq.Array(filter.MatchReleaseTypes))
}
if filter.Formats != nil {
q = q.Set("formats", pq.Array(filter.Formats))
}
if filter.Quality != nil {
q = q.Set("quality", pq.Array(filter.Quality))
}
if filter.Media != nil {
q = q.Set("media", pq.Array(filter.Media))
}
if filter.LogScore != nil {
q = q.Set("log_score", filter.LogScore)
}
if filter.Log != nil {
q = q.Set("has_log", filter.Log)
}
if filter.Cue != nil {
q = q.Set("has_cue", filter.Cue)
}
if filter.PerfectFlac != nil {
q = q.Set("perfect_flac", filter.PerfectFlac)
}
if filter.Origins != nil {
q = q.Set("origins", pq.Array(filter.Origins))
}
if filter.ExceptOrigins != nil {
q = q.Set("except_origins", pq.Array(filter.ExceptOrigins))
}
if filter.ExternalScriptEnabled != nil {
q = q.Set("external_script_enabled", filter.ExternalScriptEnabled)
}
if filter.ExternalScriptCmd != nil {
q = q.Set("external_script_cmd", filter.ExternalScriptCmd)
}
if filter.ExternalScriptArgs != nil {
q = q.Set("external_script_args", filter.ExternalScriptArgs)
}
if filter.ExternalScriptExpectStatus != nil {
q = q.Set("external_script_expect_status", filter.ExternalScriptExpectStatus)
}
if filter.ExternalWebhookEnabled != nil {
q = q.Set("external_webhook_enabled", filter.ExternalWebhookEnabled)
}
if filter.ExternalWebhookHost != nil {
q = q.Set("external_webhook_host", filter.ExternalWebhookHost)
}
if filter.ExternalWebhookData != nil {
q = q.Set("external_webhook_data", filter.ExternalWebhookData)
}
if filter.ExternalWebhookExpectStatus != nil {
q = q.Set("external_webhook_expect_status", filter.ExternalWebhookExpectStatus)
}
q = q.Where("id = ?", filter.ID)
query, args, err := q.ToSql()
if err != nil {
return errors.Wrap(err, "error building query")
}
result, err := r.db.handler.ExecContext(ctx, query, args...)
if err != nil {
return errors.Wrap(err, "error executing query")
}
count, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "error executing query")
}
if count == 0 {
return errors.New("no rows affected")
}
return nil
}
func (r *FilterRepo) ToggleEnabled(ctx context.Context, filterID int, enabled bool) error { func (r *FilterRepo) ToggleEnabled(ctx context.Context, filterID int, enabled bool) error {
var err error var err error

View file

@ -23,6 +23,7 @@ type FilterRepo interface {
ListFilters(ctx context.Context) ([]Filter, error) ListFilters(ctx context.Context) ([]Filter, error)
Store(ctx context.Context, filter Filter) (*Filter, error) Store(ctx context.Context, filter Filter) (*Filter, error)
Update(ctx context.Context, filter Filter) (*Filter, error) Update(ctx context.Context, filter Filter) (*Filter, error)
UpdatePartial(ctx context.Context, filter FilterUpdate) error
ToggleEnabled(ctx context.Context, filterID int, enabled bool) error ToggleEnabled(ctx context.Context, filterID int, enabled bool) error
Delete(ctx context.Context, filterID int) error Delete(ctx context.Context, filterID int) error
StoreIndexerConnection(ctx context.Context, filterID int, indexerID int) error StoreIndexerConnection(ctx context.Context, filterID int, indexerID int) error
@ -116,6 +117,70 @@ type Filter struct {
Downloads *FilterDownloads `json:"-"` Downloads *FilterDownloads `json:"-"`
} }
type FilterUpdate struct {
ID int `json:"id"`
Name *string `json:"name,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
MinSize *string `json:"min_size,omitempty"`
MaxSize *string `json:"max_size,omitempty"`
Delay *int `json:"delay,omitempty"`
Priority *int32 `json:"priority,omitempty"`
MaxDownloads *int `json:"max_downloads,omitempty"`
MaxDownloadsUnit *FilterMaxDownloadsUnit `json:"max_downloads_unit,omitempty"`
MatchReleases *string `json:"match_releases,omitempty"`
ExceptReleases *string `json:"except_releases,omitempty"`
UseRegex *bool `json:"use_regex,omitempty"`
MatchReleaseGroups *string `json:"match_release_groups,omitempty"`
ExceptReleaseGroups *string `json:"except_release_groups,omitempty"`
Scene *bool `json:"scene,omitempty"`
Origins *[]string `json:"origins,omitempty"`
ExceptOrigins *[]string `json:"except_origins,omitempty"`
Bonus *[]string `json:"bonus,omitempty"`
Freeleech *bool `json:"freeleech,omitempty"`
FreeleechPercent *string `json:"freeleech_percent,omitempty"`
Shows *string `json:"shows,omitempty"`
Seasons *string `json:"seasons,omitempty"`
Episodes *string `json:"episodes,omitempty"`
Resolutions *[]string `json:"resolutions,omitempty"` // SD, 480i, 480p, 576p, 720p, 810p, 1080i, 1080p.
Codecs *[]string `json:"codecs,omitempty"` // XviD, DivX, x264, h.264 (or h264), mpeg2 (or mpeg-2), VC-1 (or VC1), WMV, Remux, h.264 Remux (or h264 Remux), VC-1 Remux (or VC1 Remux).
Sources *[]string `json:"sources,omitempty"` // DSR, PDTV, HDTV, HR.PDTV, HR.HDTV, DVDRip, DVDScr, BDr, BD5, BD9, BDRip, BRRip, DVDR, MDVDR, HDDVD, HDDVDRip, BluRay, WEB-DL, TVRip, CAM, R5, TELESYNC, TS, TELECINE, TC. TELESYNC and TS are synonyms (you don't need both). Same for TELECINE and TC
Containers *[]string `json:"containers,omitempty"`
MatchHDR *[]string `json:"match_hdr,omitempty"`
ExceptHDR *[]string `json:"except_hdr,omitempty"`
MatchOther *[]string `json:"match_other,omitempty"`
ExceptOther *[]string `json:"except_other,omitempty"`
Years *string `json:"years,omitempty"`
Artists *string `json:"artists,omitempty"`
Albums *string `json:"albums,omitempty"`
MatchReleaseTypes *[]string `json:"match_release_types,omitempty"` // Album,Single,EP
ExceptReleaseTypes *string `json:"except_release_types,omitempty"`
Formats *[]string `json:"formats,omitempty"` // MP3, FLAC, Ogg, AAC, AC3, DTS
Quality *[]string `json:"quality,omitempty"` // 192, 320, APS (VBR), V2 (VBR), V1 (VBR), APX (VBR), V0 (VBR), q8.x (VBR), Lossless, 24bit Lossless, Other
Media *[]string `json:"media,omitempty"` // CD, DVD, Vinyl, Soundboard, SACD, DAT, Cassette, WEB, Other
PerfectFlac *bool `json:"perfect_flac,omitempty"`
Cue *bool `json:"cue,omitempty"`
Log *bool `json:"log,omitempty"`
LogScore *int `json:"log_score,omitempty"`
MatchCategories *string `json:"match_categories,omitempty"`
ExceptCategories *string `json:"except_categories,omitempty"`
MatchUploaders *string `json:"match_uploaders,omitempty"`
ExceptUploaders *string `json:"except_uploaders,omitempty"`
Tags *string `json:"tags,omitempty"`
ExceptTags *string `json:"except_tags,omitempty"`
TagsAny *string `json:"tags_any,omitempty"`
ExceptTagsAny *string `json:"except_tags_any,omitempty"`
ExternalScriptEnabled *bool `json:"external_script_enabled,omitempty"`
ExternalScriptCmd *string `json:"external_script_cmd,omitempty"`
ExternalScriptArgs *string `json:"external_script_args,omitempty"`
ExternalScriptExpectStatus *int `json:"external_script_expect_status,omitempty"`
ExternalWebhookEnabled *bool `json:"external_webhook_enabled,omitempty"`
ExternalWebhookHost *string `json:"external_webhook_host,omitempty"`
ExternalWebhookData *string `json:"external_webhook_data,omitempty"`
ExternalWebhookExpectStatus *int `json:"external_webhook_expect_status,omitempty"`
Actions []*Action `json:"actions,omitempty"`
Indexers []Indexer `json:"indexers,omitempty"`
}
func (f Filter) CheckFilter(r *Release) ([]string, bool) { func (f Filter) CheckFilter(r *Release) ([]string, bool) {
// reset rejections first to clean previous checks // reset rejections first to clean previous checks
r.resetRejections() r.resetRejections()

View file

@ -27,6 +27,7 @@ type Service interface {
ListFilters(ctx context.Context) ([]domain.Filter, error) ListFilters(ctx context.Context) ([]domain.Filter, error)
Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error) Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error)
Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error) Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error)
UpdatePartial(ctx context.Context, filter domain.FilterUpdate) error
Duplicate(ctx context.Context, filterID int) (*domain.Filter, error) Duplicate(ctx context.Context, filterID int) (*domain.Filter, error)
ToggleEnabled(ctx context.Context, filterID int, enabled bool) error ToggleEnabled(ctx context.Context, filterID int, enabled bool) error
Delete(ctx context.Context, filterID int) error Delete(ctx context.Context, filterID int) error
@ -153,6 +154,33 @@ func (s *service) Update(ctx context.Context, filter domain.Filter) (*domain.Fil
return f, nil return f, nil
} }
func (s *service) UpdatePartial(ctx context.Context, filter domain.FilterUpdate) error {
// update
if err := s.repo.UpdatePartial(ctx, filter); err != nil {
s.log.Error().Err(err).Msgf("could not update partial filter: %v", filter.ID)
return err
}
if filter.Indexers != nil {
// take care of connected indexers
if err := s.repo.StoreIndexerConnections(ctx, filter.ID, filter.Indexers); err != nil {
s.log.Error().Err(err).Msgf("could not store filter indexer connections: %v", filter.Name)
return err
}
}
if filter.Actions != nil {
// take care of filter actions
if _, err := s.actionRepo.StoreFilterActions(ctx, filter.Actions, int64(filter.ID)); err != nil {
s.log.Error().Err(err).Msgf("could not store filter actions: %v", filter.ID)
return err
}
}
return nil
}
func (s *service) Duplicate(ctx context.Context, filterID int) (*domain.Filter, error) { func (s *service) Duplicate(ctx context.Context, filterID int) (*domain.Filter, error) {
// find filter // find filter
baseFilter, err := s.repo.FindByID(ctx, filterID) baseFilter, err := s.repo.FindByID(ctx, filterID)

View file

@ -17,6 +17,7 @@ type filterService interface {
Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error) Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error)
Delete(ctx context.Context, filterID int) error Delete(ctx context.Context, filterID int) error
Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error) Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error)
UpdatePartial(ctx context.Context, filter domain.FilterUpdate) error
Duplicate(ctx context.Context, filterID int) (*domain.Filter, error) Duplicate(ctx context.Context, filterID int) (*domain.Filter, error)
ToggleEnabled(ctx context.Context, filterID int, enabled bool) error ToggleEnabled(ctx context.Context, filterID int, enabled bool) error
} }
@ -39,6 +40,7 @@ func (h filterHandler) Routes(r chi.Router) {
r.Get("/{filterID}/duplicate", h.duplicate) r.Get("/{filterID}/duplicate", h.duplicate)
r.Post("/", h.store) r.Post("/", h.store)
r.Put("/{filterID}", h.update) r.Put("/{filterID}", h.update)
r.Patch("/{filterID}", h.updatePartial)
r.Put("/{filterID}/enabled", h.toggleEnabled) r.Put("/{filterID}/enabled", h.toggleEnabled)
r.Delete("/{filterID}", h.delete) r.Delete("/{filterID}", h.delete)
} }
@ -49,6 +51,8 @@ func (h filterHandler) getFilters(w http.ResponseWriter, r *http.Request) {
trackers, err := h.service.ListFilters(ctx) trackers, err := h.service.ListFilters(ctx)
if err != nil { if err != nil {
// //
h.encoder.Error(w, err)
return
} }
h.encoder.StatusResponse(ctx, w, trackers, http.StatusOK) h.encoder.StatusResponse(ctx, w, trackers, http.StatusOK)
@ -60,7 +64,11 @@ func (h filterHandler) getByID(w http.ResponseWriter, r *http.Request) {
filterID = chi.URLParam(r, "filterID") filterID = chi.URLParam(r, "filterID")
) )
id, _ := strconv.Atoi(filterID) id, err := strconv.Atoi(filterID)
if err != nil {
h.encoder.Error(w, err)
return
}
filter, err := h.service.FindByID(ctx, id) filter, err := h.service.FindByID(ctx, id)
if err != nil { if err != nil {
@ -77,7 +85,11 @@ func (h filterHandler) duplicate(w http.ResponseWriter, r *http.Request) {
filterID = chi.URLParam(r, "filterID") filterID = chi.URLParam(r, "filterID")
) )
id, _ := strconv.Atoi(filterID) id, err := strconv.Atoi(filterID)
if err != nil {
h.encoder.Error(w, err)
return
}
filter, err := h.service.Duplicate(ctx, id) filter, err := h.service.Duplicate(ctx, id)
if err != nil { if err != nil {
@ -96,16 +108,18 @@ func (h filterHandler) store(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&data); err != nil { if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
// encode error // encode error
h.encoder.Error(w, err)
return return
} }
filter, err := h.service.Store(ctx, data) filter, err := h.service.Store(ctx, data)
if err != nil { if err != nil {
// encode error // encode error
h.encoder.Error(w, err)
return return
} }
h.encoder.StatusResponse(ctx, w, filter, http.StatusCreated) h.encoder.StatusCreatedData(w, filter)
} }
func (h filterHandler) update(w http.ResponseWriter, r *http.Request) { func (h filterHandler) update(w http.ResponseWriter, r *http.Request) {
@ -116,18 +130,50 @@ func (h filterHandler) update(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&data); err != nil { if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
// encode error // encode error
h.encoder.Error(w, err)
return return
} }
filter, err := h.service.Update(ctx, data) filter, err := h.service.Update(ctx, data)
if err != nil { if err != nil {
// encode error // encode error
h.encoder.Error(w, err)
return return
} }
h.encoder.StatusResponse(ctx, w, filter, http.StatusOK) h.encoder.StatusResponse(ctx, w, filter, http.StatusOK)
} }
func (h filterHandler) updatePartial(w http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
data domain.FilterUpdate
filterID = chi.URLParam(r, "filterID")
)
// set id from param and convert to int
id, err := strconv.Atoi(filterID)
if err != nil {
h.encoder.Error(w, err)
return
}
data.ID = id
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
// encode error
h.encoder.Error(w, err)
return
}
if err := h.service.UpdatePartial(ctx, data); err != nil {
// encode error
h.encoder.Error(w, err)
return
}
h.encoder.NoContent(w)
}
func (h filterHandler) toggleEnabled(w http.ResponseWriter, r *http.Request) { func (h filterHandler) toggleEnabled(w http.ResponseWriter, r *http.Request) {
var ( var (
ctx = r.Context() ctx = r.Context()
@ -137,20 +183,25 @@ func (h filterHandler) toggleEnabled(w http.ResponseWriter, r *http.Request) {
} }
) )
id, _ := strconv.Atoi(filterID) id, err := strconv.Atoi(filterID)
if err != nil {
h.encoder.Error(w, err)
return
}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil { if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
// encode error // encode error
h.encoder.Error(w, err)
return return
} }
err := h.service.ToggleEnabled(ctx, id, data.Enabled) if err := h.service.ToggleEnabled(ctx, id, data.Enabled); err != nil {
if err != nil {
// encode error // encode error
h.encoder.Error(w, err)
return return
} }
h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent) h.encoder.NoContent(w)
} }
func (h filterHandler) delete(w http.ResponseWriter, r *http.Request) { func (h filterHandler) delete(w http.ResponseWriter, r *http.Request) {
@ -159,10 +210,15 @@ func (h filterHandler) delete(w http.ResponseWriter, r *http.Request) {
filterID = chi.URLParam(r, "filterID") filterID = chi.URLParam(r, "filterID")
) )
id, _ := strconv.Atoi(filterID) id, err := strconv.Atoi(filterID)
if err != nil {
h.encoder.Error(w, err)
return
}
if err := h.service.Delete(ctx, id); err != nil { if err := h.service.Delete(ctx, id); err != nil {
// return err // return err
h.encoder.Error(w, err)
} }
h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent) h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent)