diff --git a/internal/database/filter.go b/internal/database/filter.go index ce56a09..424d696 100644 --- a/internal/database/filter.go +++ b/internal/database/filter.go @@ -568,6 +568,198 @@ func (r *FilterRepo) Update(ctx context.Context, filter domain.Filter) (*domain. 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 { var err error diff --git a/internal/domain/filter.go b/internal/domain/filter.go index 4eb8d8f..9d24ef2 100644 --- a/internal/domain/filter.go +++ b/internal/domain/filter.go @@ -23,6 +23,7 @@ type FilterRepo interface { ListFilters(ctx context.Context) ([]Filter, error) Store(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 Delete(ctx context.Context, filterID int) error StoreIndexerConnection(ctx context.Context, filterID int, indexerID int) error @@ -116,6 +117,70 @@ type Filter struct { 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) { // reset rejections first to clean previous checks r.resetRejections() diff --git a/internal/filter/service.go b/internal/filter/service.go index 3a260bd..4fdc88f 100644 --- a/internal/filter/service.go +++ b/internal/filter/service.go @@ -27,6 +27,7 @@ type Service interface { ListFilters(ctx context.Context) ([]domain.Filter, error) Store(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) ToggleEnabled(ctx context.Context, filterID int, enabled bool) 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 } +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) { // find filter baseFilter, err := s.repo.FindByID(ctx, filterID) diff --git a/internal/http/filter.go b/internal/http/filter.go index 1334b4c..d3ef678 100644 --- a/internal/http/filter.go +++ b/internal/http/filter.go @@ -17,6 +17,7 @@ type filterService interface { Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error) Delete(ctx context.Context, filterID int) 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) 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.Post("/", h.store) r.Put("/{filterID}", h.update) + r.Patch("/{filterID}", h.updatePartial) r.Put("/{filterID}/enabled", h.toggleEnabled) 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) if err != nil { // + h.encoder.Error(w, err) + return } 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") ) - 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) if err != nil { @@ -77,7 +85,11 @@ func (h filterHandler) duplicate(w http.ResponseWriter, r *http.Request) { 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) 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 { // encode error + h.encoder.Error(w, err) return } filter, err := h.service.Store(ctx, data) if err != nil { // encode error + h.encoder.Error(w, err) return } - h.encoder.StatusResponse(ctx, w, filter, http.StatusCreated) + h.encoder.StatusCreatedData(w, filter) } 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 { // encode error + h.encoder.Error(w, err) return } filter, err := h.service.Update(ctx, data) if err != nil { // encode error + h.encoder.Error(w, err) return } 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) { var ( 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 { // encode error + h.encoder.Error(w, err) return } - err := h.service.ToggleEnabled(ctx, id, data.Enabled) - if err != nil { + if err := h.service.ToggleEnabled(ctx, id, data.Enabled); err != nil { // encode error + h.encoder.Error(w, err) return } - h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent) + h.encoder.NoContent(w) } 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") ) - 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 { // return err + h.encoder.Error(w, err) } h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent)