feat(filters): show enabled and disabled actions in list view (#1304)

* feat(filters): reflect enabled actions

* dont store release unless enabled action found

* store the release after the delay

* add new parameter to FindByFilterID method
This commit is contained in:
soup 2023-12-17 21:18:26 +01:00 committed by GitHub
parent 95cd053db5
commit 6e12654f6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 59 additions and 45 deletions

1
.gitignore vendored
View file

@ -38,6 +38,7 @@ bin/
dist/ dist/
.run/ .run/
tmp/ tmp/
.golangci.yml
# Preserve files # Preserve files
!.gitkeep !.gitkeep

View file

@ -20,7 +20,7 @@ type Service interface {
Store(ctx context.Context, action domain.Action) (*domain.Action, error) Store(ctx context.Context, action domain.Action) (*domain.Action, error)
List(ctx context.Context) ([]domain.Action, error) List(ctx context.Context) ([]domain.Action, error)
Get(ctx context.Context, req *domain.GetActionRequest) (*domain.Action, error) Get(ctx context.Context, req *domain.GetActionRequest) (*domain.Action, error)
FindByFilterID(ctx context.Context, filterID int) ([]*domain.Action, error) FindByFilterID(ctx context.Context, filterID int, active *bool) ([]*domain.Action, error)
Delete(ctx context.Context, req *domain.DeleteActionRequest) error Delete(ctx context.Context, req *domain.DeleteActionRequest) error
DeleteByFilterID(ctx context.Context, filterID int) error DeleteByFilterID(ctx context.Context, filterID int) error
ToggleEnabled(actionID int) error ToggleEnabled(actionID int) error
@ -76,8 +76,8 @@ func (s *service) Get(ctx context.Context, req *domain.GetActionRequest) (*domai
return a, nil return a, nil
} }
func (s *service) FindByFilterID(ctx context.Context, filterID int) ([]*domain.Action, error) { func (s *service) FindByFilterID(ctx context.Context, filterID int, active *bool) ([]*domain.Action, error) {
return s.repo.FindByFilterID(ctx, filterID) return s.repo.FindByFilterID(ctx, filterID, active)
} }
func (s *service) Delete(ctx context.Context, req *domain.DeleteActionRequest) error { func (s *service) Delete(ctx context.Context, req *domain.DeleteActionRequest) error {

View file

@ -30,7 +30,7 @@ func NewActionRepo(log logger.Logger, db *DB, clientRepo domain.DownloadClientRe
} }
} }
func (r *ActionRepo) FindByFilterID(ctx context.Context, filterID int) ([]*domain.Action, error) { func (r *ActionRepo) FindByFilterID(ctx context.Context, filterID int, active *bool) ([]*domain.Action, error) {
tx, err := r.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) tx, err := r.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil { if err != nil {
return nil, err return nil, err
@ -38,7 +38,7 @@ func (r *ActionRepo) FindByFilterID(ctx context.Context, filterID int) ([]*domai
defer tx.Rollback() defer tx.Rollback()
actions, err := r.findByFilterID(ctx, tx, filterID) actions, err := r.findByFilterID(ctx, tx, filterID, active)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -59,7 +59,7 @@ func (r *ActionRepo) FindByFilterID(ctx context.Context, filterID int) ([]*domai
return actions, nil return actions, nil
} }
func (r *ActionRepo) findByFilterID(ctx context.Context, tx *Tx, filterID int) ([]*domain.Action, error) { func (r *ActionRepo) findByFilterID(ctx context.Context, tx *Tx, filterID int, active *bool) ([]*domain.Action, error) {
queryBuilder := r.db.squirrel. queryBuilder := r.db.squirrel.
Select( Select(
"id", "id",
@ -95,6 +95,10 @@ func (r *ActionRepo) findByFilterID(ctx context.Context, tx *Tx, filterID int) (
From("action"). From("action").
Where(sq.Eq{"filter_id": filterID}) Where(sq.Eq{"filter_id": filterID})
if active != nil {
queryBuilder = queryBuilder.Where(sq.Eq{"enabled": *active})
}
query, args, err := queryBuilder.ToSql() query, args, err := queryBuilder.ToSql()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error building query") return nil, errors.Wrap(err, "error building query")

View file

@ -214,7 +214,7 @@ func TestActionRepo_FindByFilterID(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
// Actual test for FindByFilterID // Actual test for FindByFilterID
actions, err := repo.FindByFilterID(context.Background(), createdFilters[0].ID) actions, err := repo.FindByFilterID(context.Background(), createdFilters[0].ID, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, actions) assert.NotNil(t, actions)
assert.Equal(t, 1, len(actions)) assert.Equal(t, 1, len(actions))
@ -235,7 +235,7 @@ func TestActionRepo_FindByFilterID(t *testing.T) {
assert.NotNil(t, createdFilters) assert.NotNil(t, createdFilters)
// Actual test for FindByFilterID // Actual test for FindByFilterID
actions, err := repo.FindByFilterID(context.Background(), createdFilters[0].ID) actions, err := repo.FindByFilterID(context.Background(), createdFilters[0].ID, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, len(actions)) assert.Equal(t, 0, len(actions))
@ -244,7 +244,7 @@ func TestActionRepo_FindByFilterID(t *testing.T) {
}) })
t.Run(fmt.Sprintf("FindByFilterID_Succeeds_With_Invalid_FilterID [%s]", dbType), func(t *testing.T) { t.Run(fmt.Sprintf("FindByFilterID_Succeeds_With_Invalid_FilterID [%s]", dbType), func(t *testing.T) {
actions, err := repo.FindByFilterID(context.Background(), 9999) // 9999 is an invalid filter ID actions, err := repo.FindByFilterID(context.Background(), 9999, nil) // 9999 is an invalid filter ID
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, actions) assert.NotNil(t, actions)
assert.Equal(t, 0, len(actions)) assert.Equal(t, 0, len(actions))
@ -254,7 +254,7 @@ func TestActionRepo_FindByFilterID(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
defer cancel() defer cancel()
actions, err := repo.FindByFilterID(ctx, 1) actions, err := repo.FindByFilterID(ctx, 1, nil)
assert.Error(t, err) assert.Error(t, err)
assert.Nil(t, actions) assert.Nil(t, actions)
}) })

View file

@ -57,6 +57,12 @@ func (r *FilterRepo) find(ctx context.Context, tx *Tx, params domain.FilterQuery
From("action a"). From("action a").
Where("a.filter_id = f.id") Where("a.filter_id = f.id")
actionEnabledCountQuery := r.db.squirrel.
Select("COUNT(*)").
From("action a").
Where("a.filter_id = f.id").
Where("a.enabled = '1'")
queryBuilder := r.db.squirrel. queryBuilder := r.db.squirrel.
Select( Select(
"f.id", "f.id",
@ -68,6 +74,7 @@ func (r *FilterRepo) find(ctx context.Context, tx *Tx, params domain.FilterQuery
). ).
Distinct(). Distinct().
Column(sq.Alias(actionCountQuery, "action_count")). Column(sq.Alias(actionCountQuery, "action_count")).
Column(sq.Alias(actionEnabledCountQuery, "actions_enabled_count")).
LeftJoin("filter_indexer fi ON f.id = fi.filter_id"). LeftJoin("filter_indexer fi ON f.id = fi.filter_id").
LeftJoin("indexer i ON i.id = fi.indexer_id"). LeftJoin("indexer i ON i.id = fi.indexer_id").
From("filter f") From("filter f")
@ -89,7 +96,6 @@ func (r *FilterRepo) find(ctx context.Context, tx *Tx, params domain.FilterQuery
for _, v := range params.Filters.Indexers { for _, v := range params.Filters.Indexers {
filter = append(filter, sq.Eq{"i.identifier": v}) filter = append(filter, sq.Eq{"i.identifier": v})
} }
queryBuilder = queryBuilder.Where(filter) queryBuilder = queryBuilder.Where(filter)
} }
@ -102,14 +108,13 @@ func (r *FilterRepo) find(ctx context.Context, tx *Tx, params domain.FilterQuery
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error executing query") return nil, errors.Wrap(err, "error executing query")
} }
defer rows.Close() defer rows.Close()
var filters []domain.Filter var filters []domain.Filter
for rows.Next() { for rows.Next() {
var f domain.Filter var f domain.Filter
if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &f.Priority, &f.CreatedAt, &f.UpdatedAt, &f.ActionsCount); err != nil { if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &f.Priority, &f.CreatedAt, &f.UpdatedAt, &f.ActionsCount, &f.ActionsEnabledCount); err != nil {
return nil, errors.Wrap(err, "error scanning row") return nil, errors.Wrap(err, "error scanning row")
} }

View file

@ -14,7 +14,7 @@ import (
type ActionRepo interface { type ActionRepo interface {
Store(ctx context.Context, action Action) (*Action, error) Store(ctx context.Context, action Action) (*Action, error)
StoreFilterActions(ctx context.Context, filterID int64, actions []*Action) ([]*Action, error) StoreFilterActions(ctx context.Context, filterID int64, actions []*Action) ([]*Action, error)
FindByFilterID(ctx context.Context, filterID int) ([]*Action, error) FindByFilterID(ctx context.Context, filterID int, active *bool) ([]*Action, error)
List(ctx context.Context) ([]Action, error) List(ctx context.Context) ([]Action, error)
Get(ctx context.Context, req *GetActionRequest) (*Action, error) Get(ctx context.Context, req *GetActionRequest) (*Action, error)
Delete(ctx context.Context, req *DeleteActionRequest) error Delete(ctx context.Context, req *DeleteActionRequest) error

View file

@ -133,6 +133,7 @@ type Filter struct {
ExceptDescription string `json:"except_description,omitempty"` ExceptDescription string `json:"except_description,omitempty"`
UseRegexDescription bool `json:"use_regex_description,omitempty"` UseRegexDescription bool `json:"use_regex_description,omitempty"`
ActionsCount int `json:"actions_count"` ActionsCount int `json:"actions_count"`
ActionsEnabledCount int `json:"actions_enabled_count"`
Actions []*Action `json:"actions,omitempty"` Actions []*Action `json:"actions,omitempty"`
External []FilterExternal `json:"external,omitempty"` External []FilterExternal `json:"external,omitempty"`
Indexers []Indexer `json:"indexers"` Indexers []Indexer `json:"indexers"`

View file

@ -118,7 +118,7 @@ func (s *service) FindByID(ctx context.Context, filterID int) (*domain.Filter, e
return nil, err return nil, err
} }
actions, err := s.actionRepo.FindByFilterID(ctx, filter.ID) actions, err := s.actionRepo.FindByFilterID(ctx, filter.ID, nil)
if err != nil { if err != nil {
s.log.Error().Err(err).Msgf("could not find filter actions for filter id: %v", filter.ID) s.log.Error().Err(err).Msgf("could not find filter actions for filter id: %v", filter.ID)
} }
@ -691,7 +691,7 @@ func (s *service) webhook(ctx context.Context, external domain.FilterExternal, r
} }
// add header to req // add header to req
req.Header.Add(http.CanonicalHeaderKey(h[0]), h[1]) req.Header.Add(h[0], h[1]) // go already canonicalizes the provided header key.
} }
} }

View file

@ -162,6 +162,27 @@ func (s *service) processFilters(ctx context.Context, filters []*domain.Filter,
l.Info().Msgf("Matched '%s' (%s) for %s", release.TorrentName, release.FilterName, release.Indexer) l.Info().Msgf("Matched '%s' (%s) for %s", release.TorrentName, release.FilterName, release.Indexer)
// found matching filter, lets find the filter actions and attach
active := true
actions, err := s.actionSvc.FindByFilterID(ctx, f.ID, &active)
if err != nil {
s.log.Error().Err(err).Msgf("release.Process: error finding actions for filter: %s", f.Name)
return err
}
// if no actions, continue to next filter
if len(actions) == 0 {
s.log.Warn().Msgf("release.Process: no active actions found for filter '%s', trying next one..", f.Name)
continue
}
// sleep for the delay period specified in the filter before running actions
delay := release.Filter.Delay
if delay > 0 {
l.Debug().Msgf("release.Process: delaying processing of '%s' (%s) for %s by %d seconds as specified in the filter", release.TorrentName, release.FilterName, release.Indexer, delay)
time.Sleep(time.Duration(delay) * time.Second)
}
// save release here to only save those with rejections from actions instead of all releases // save release here to only save those with rejections from actions instead of all releases
if release.ID == 0 { if release.ID == 0 {
release.FilterStatus = domain.ReleaseStatusFilterApproved release.FilterStatus = domain.ReleaseStatusFilterApproved
@ -172,26 +193,6 @@ func (s *service) processFilters(ctx context.Context, filters []*domain.Filter,
} }
} }
// found matching filter, lets find the filter actions and attach
actions, err := s.actionSvc.FindByFilterID(ctx, f.ID)
if err != nil {
s.log.Error().Err(err).Msgf("release.Process: error finding actions for filter: %s", f.Name)
return err
}
// if no actions, continue to next filter
if len(actions) == 0 {
s.log.Warn().Msgf("release.Process: no actions found for filter '%s', trying next one..", f.Name)
return nil
}
// sleep for the delay period specified in the filter before running actions
delay := release.Filter.Delay
if delay > 0 {
l.Debug().Msgf("release.Process: delaying processing of '%s' (%s) for %s by %d seconds as specified in the filter", release.TorrentName, release.FilterName, release.Indexer, delay)
time.Sleep(time.Duration(delay) * time.Second)
}
var rejections []string var rejections []string
// run actions (watchFolder, test, exec, qBittorrent, Deluge, arr etc.) // run actions (watchFolder, test, exec, qBittorrent, Deluge, arr etc.)

View file

@ -645,19 +645,20 @@ function FilterListItem({ filter, values, idx }: FilterListItemProps) {
to={`${filter.id.toString()}/actions`} to={`${filter.id.toString()}/actions`}
className="flex items-center cursor-pointer hover:text-black dark:hover:text-gray-300" className="flex items-center cursor-pointer hover:text-black dark:hover:text-gray-300"
> >
<span className={classNames(!filter.actions_count ? "text-red-500 hover:text-red-400 dark:hover:text-red-400" : "")}> <span className={filter.actions_count === 0 || filter.actions_enabled_count === 0 ? "text-red-500 hover:text-red-400 dark:hover:text-red-400" : ""}>
Actions: {filter.actions_count} Actions: {filter.actions_enabled_count}/{filter.actions_count}
</span> </span>
{!filter.actions_count && (
<span className="mr-2 ml-2 flex h-3 w-3 relative">
</span>
)}
</Link> </Link>
} }
> >
{!filter.actions_count ? ( {filter.actions_count === 0 ? (
<>{"You need to setup an action in the filter otherwise you will not get any snatches."}</> <>
{"No actions defined. Set up actions to enable snatching."}
</>
) : filter.actions_enabled_count === 0 ? (
<>
{"You need to enable at least one action in the filter otherwise you will not get any snatches."}
</>
) : null} ) : null}
</Tooltip> </Tooltip>
</span> </span>

View file

@ -68,6 +68,7 @@ interface Filter {
tags_match_logic: string; tags_match_logic: string;
except_tags_match_logic: string; except_tags_match_logic: string;
actions_count: number; actions_count: number;
actions_enabled_count: number;
actions: Action[]; actions: Action[];
indexers: Indexer[]; indexers: Indexer[];
external: ExternalFilter[]; external: ExternalFilter[];