feat(filters): add external script and webhook checks

This commit is contained in:
ze0s 2022-07-23 15:19:28 +02:00
parent 16dd8c5419
commit d56693cd33
17 changed files with 635 additions and 200 deletions

View file

@ -135,7 +135,7 @@ func (s *service) delugeV1(client *domain.DownloadClient, action domain.Action,
}
// macros handle args and replace vars
m := NewMacro(release)
m := domain.NewMacro(release)
options, err := s.prepareDelugeOptions(action, m)
if err != nil {
@ -224,7 +224,7 @@ func (s *service) delugeV2(client *domain.DownloadClient, action domain.Action,
}
// macros handle args and replace vars
m := NewMacro(release)
m := domain.NewMacro(release)
// set options
options, err := s.prepareDelugeOptions(action, m)
@ -265,7 +265,7 @@ func (s *service) delugeV2(client *domain.DownloadClient, action domain.Action,
return nil, nil
}
func (s *service) prepareDelugeOptions(action domain.Action, m Macro) (delugeClient.Options, error) {
func (s *service) prepareDelugeOptions(action domain.Action, m domain.Macro) (delugeClient.Options, error) {
// set options
options := delugeClient.Options{}

View file

@ -56,7 +56,7 @@ func (s *service) execCmd(action domain.Action, release domain.Release) error {
func (s *service) parseExecArgs(release domain.Release, execArgs string) ([]string, error) {
// handle args and replace vars
m := NewMacro(release)
m := domain.NewMacro(release)
// parse and replace values in argument string before continuing
parsedArgs, err := m.Parse(execArgs)

View file

@ -76,7 +76,7 @@ func (s *service) qbittorrent(action domain.Action, release domain.Release) ([]s
}
// macros handle args and replace vars
m := NewMacro(release)
m := domain.NewMacro(release)
options, err := s.prepareQbitOptions(action, m)
if err != nil {
@ -100,7 +100,7 @@ func (s *service) qbittorrent(action domain.Action, release domain.Release) ([]s
return nil, nil
}
func (s *service) prepareQbitOptions(action domain.Action, m Macro) (map[string]string, error) {
func (s *service) prepareQbitOptions(action domain.Action, m domain.Macro) (map[string]string, error) {
options := map[string]string{}

View file

@ -136,7 +136,7 @@ func (s *service) watchFolder(action domain.Action, release domain.Release) erro
}
}
m := NewMacro(release)
m := domain.NewMacro(release)
// parse and replace values in argument string before continuing
watchFolderArgs, err := m.Parse(action.WatchFolder)
@ -187,7 +187,7 @@ func (s *service) webhook(action domain.Action, release domain.Release) error {
}
}
m := NewMacro(release)
m := domain.NewMacro(release)
// parse and replace values in argument string before continuing
dataArgs, err := m.Parse(action.WebhookData)

View file

@ -123,6 +123,14 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter
"tags",
"except_tags",
"origins",
"external_script_enabled",
"external_script_cmd",
"external_script_args",
"external_script_expect_status",
"external_webhook_enabled",
"external_webhook_host",
"external_webhook_data",
"external_webhook_expect_status",
"created_at",
"updated_at",
).
@ -140,11 +148,11 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter
}
var f domain.Filter
var minSize, maxSize, maxDownloadsUnit, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, freeleechPercent, shows, seasons, episodes, years, artists, albums, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags sql.NullString
var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac sql.NullBool
var delay, maxDownloads, logScore sql.NullInt32
var minSize, maxSize, maxDownloadsUnit, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, freeleechPercent, shows, seasons, episodes, years, artists, albums, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags, extScriptCmd, extScriptArgs, extWebhookHost, extWebhookData sql.NullString
var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac, extScriptEnabled, extWebhookEnabled sql.NullBool
var delay, maxDownloads, logScore, extWebhookStatus, extScriptStatus sql.NullInt32
if err := row.Scan(&f.ID, &f.Enabled, &f.Name, &minSize, &maxSize, &delay, &f.Priority, &maxDownloads, &maxDownloadsUnit, &matchReleases, &exceptReleases, &useRegex, &matchReleaseGroups, &exceptReleaseGroups, &scene, &freeleech, &freeleechPercent, &shows, &seasons, &episodes, pq.Array(&f.Resolutions), pq.Array(&f.Codecs), pq.Array(&f.Sources), pq.Array(&f.Containers), pq.Array(&f.MatchHDR), pq.Array(&f.ExceptHDR), pq.Array(&f.MatchOther), pq.Array(&f.ExceptOther), &years, &artists, &albums, pq.Array(&f.MatchReleaseTypes), pq.Array(&f.Formats), pq.Array(&f.Quality), pq.Array(&f.Media), &logScore, &hasLog, &hasCue, &perfectFlac, &matchCategories, &exceptCategories, &matchUploaders, &exceptUploaders, &tags, &exceptTags, pq.Array(&f.Origins), &f.CreatedAt, &f.UpdatedAt); err != nil {
if err := row.Scan(&f.ID, &f.Enabled, &f.Name, &minSize, &maxSize, &delay, &f.Priority, &maxDownloads, &maxDownloadsUnit, &matchReleases, &exceptReleases, &useRegex, &matchReleaseGroups, &exceptReleaseGroups, &scene, &freeleech, &freeleechPercent, &shows, &seasons, &episodes, pq.Array(&f.Resolutions), pq.Array(&f.Codecs), pq.Array(&f.Sources), pq.Array(&f.Containers), pq.Array(&f.MatchHDR), pq.Array(&f.ExceptHDR), pq.Array(&f.MatchOther), pq.Array(&f.ExceptOther), &years, &artists, &albums, pq.Array(&f.MatchReleaseTypes), pq.Array(&f.Formats), pq.Array(&f.Quality), pq.Array(&f.Media), &logScore, &hasLog, &hasCue, &perfectFlac, &matchCategories, &exceptCategories, &matchUploaders, &exceptUploaders, &tags, &exceptTags, pq.Array(&f.Origins), &extScriptEnabled, &extScriptCmd, &extScriptArgs, &extScriptStatus, &extWebhookEnabled, &extWebhookHost, &extWebhookData, &extWebhookStatus, &f.CreatedAt, &f.UpdatedAt); err != nil {
return nil, errors.Wrap(err, "error scanning row")
}
@ -178,6 +186,16 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter
f.Scene = scene.Bool
f.Freeleech = freeleech.Bool
f.ExternalScriptEnabled = extScriptEnabled.Bool
f.ExternalScriptCmd = extScriptCmd.String
f.ExternalScriptArgs = extScriptArgs.String
f.ExternalScriptExpectStatus = int(extScriptStatus.Int32)
f.ExternalWebhookEnabled = extWebhookEnabled.Bool
f.ExternalWebhookHost = extWebhookHost.String
f.ExternalWebhookData = extWebhookData.String
f.ExternalWebhookExpectStatus = int(extWebhookStatus.Int32)
return &f, nil
}
@ -255,6 +273,14 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, tx *Tx, indexe
"f.tags",
"f.except_tags",
"f.origins",
"f.external_script_enabled",
"f.external_script_cmd",
"f.external_script_args",
"f.external_script_expect_status",
"f.external_webhook_enabled",
"f.external_webhook_host",
"f.external_webhook_data",
"f.external_webhook_expect_status",
"f.created_at",
"f.updated_at",
).
@ -282,11 +308,11 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, tx *Tx, indexe
for rows.Next() {
var f domain.Filter
var minSize, maxSize, maxDownloadsUnit, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, freeleechPercent, shows, seasons, episodes, years, artists, albums, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags sql.NullString
var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac sql.NullBool
var delay, maxDownloads, logScore sql.NullInt32
var minSize, maxSize, maxDownloadsUnit, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, freeleechPercent, shows, seasons, episodes, years, artists, albums, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags, extScriptCmd, extScriptArgs, extWebhookHost, extWebhookData sql.NullString
var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac, extScriptEnabled, extWebhookEnabled sql.NullBool
var delay, maxDownloads, logScore, extWebhookStatus, extScriptStatus sql.NullInt32
if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &minSize, &maxSize, &delay, &f.Priority, &maxDownloads, &maxDownloadsUnit, &matchReleases, &exceptReleases, &useRegex, &matchReleaseGroups, &exceptReleaseGroups, &scene, &freeleech, &freeleechPercent, &shows, &seasons, &episodes, pq.Array(&f.Resolutions), pq.Array(&f.Codecs), pq.Array(&f.Sources), pq.Array(&f.Containers), pq.Array(&f.MatchHDR), pq.Array(&f.ExceptHDR), pq.Array(&f.MatchOther), pq.Array(&f.ExceptOther), &years, &artists, &albums, pq.Array(&f.MatchReleaseTypes), pq.Array(&f.Formats), pq.Array(&f.Quality), pq.Array(&f.Media), &logScore, &hasLog, &hasCue, &perfectFlac, &matchCategories, &exceptCategories, &matchUploaders, &exceptUploaders, &tags, &exceptTags, pq.Array(&f.Origins), &f.CreatedAt, &f.UpdatedAt); err != nil {
if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &minSize, &maxSize, &delay, &f.Priority, &maxDownloads, &maxDownloadsUnit, &matchReleases, &exceptReleases, &useRegex, &matchReleaseGroups, &exceptReleaseGroups, &scene, &freeleech, &freeleechPercent, &shows, &seasons, &episodes, pq.Array(&f.Resolutions), pq.Array(&f.Codecs), pq.Array(&f.Sources), pq.Array(&f.Containers), pq.Array(&f.MatchHDR), pq.Array(&f.ExceptHDR), pq.Array(&f.MatchOther), pq.Array(&f.ExceptOther), &years, &artists, &albums, pq.Array(&f.MatchReleaseTypes), pq.Array(&f.Formats), pq.Array(&f.Quality), pq.Array(&f.Media), &logScore, &hasLog, &hasCue, &perfectFlac, &matchCategories, &exceptCategories, &matchUploaders, &exceptUploaders, &tags, &exceptTags, pq.Array(&f.Origins), &extScriptEnabled, &extScriptCmd, &extScriptArgs, &extScriptStatus, &extWebhookEnabled, &extWebhookHost, &extWebhookData, &extWebhookStatus, &f.CreatedAt, &f.UpdatedAt); err != nil {
return nil, errors.Wrap(err, "error scanning row")
}
@ -320,6 +346,16 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, tx *Tx, indexe
f.Scene = scene.Bool
f.Freeleech = freeleech.Bool
f.ExternalScriptEnabled = extScriptEnabled.Bool
f.ExternalScriptCmd = extScriptCmd.String
f.ExternalScriptArgs = extScriptArgs.String
f.ExternalScriptExpectStatus = int(extScriptStatus.Int32)
f.ExternalWebhookEnabled = extWebhookEnabled.Bool
f.ExternalWebhookHost = extWebhookHost.String
f.ExternalWebhookData = extWebhookData.String
f.ExternalWebhookExpectStatus = int(extWebhookStatus.Int32)
filters = append(filters, f)
}
@ -375,6 +411,14 @@ func (r *FilterRepo) Store(ctx context.Context, filter domain.Filter) (*domain.F
"has_cue",
"perfect_flac",
"origins",
"external_script_enabled",
"external_script_cmd",
"external_script_args",
"external_script_expect_status",
"external_webhook_enabled",
"external_webhook_host",
"external_webhook_data",
"external_webhook_expect_status",
).
Values(
filter.Name,
@ -422,6 +466,14 @@ func (r *FilterRepo) Store(ctx context.Context, filter domain.Filter) (*domain.F
filter.Cue,
filter.PerfectFlac,
pq.Array(filter.Origins),
filter.ExternalScriptEnabled,
filter.ExternalScriptCmd,
filter.ExternalScriptArgs,
filter.ExternalScriptExpectStatus,
filter.ExternalWebhookEnabled,
filter.ExternalWebhookHost,
filter.ExternalWebhookData,
filter.ExternalWebhookExpectStatus,
).
Suffix("RETURNING id").RunWith(r.db.handler)
@ -488,6 +540,14 @@ func (r *FilterRepo) Update(ctx context.Context, filter domain.Filter) (*domain.
Set("has_cue", filter.Cue).
Set("perfect_flac", filter.PerfectFlac).
Set("origins", pq.Array(filter.Origins)).
Set("external_script_enabled", filter.ExternalScriptEnabled).
Set("external_script_cmd", filter.ExternalScriptCmd).
Set("external_script_args", filter.ExternalScriptArgs).
Set("external_script_expect_status", filter.ExternalScriptExpectStatus).
Set("external_webhook_enabled", filter.ExternalWebhookEnabled).
Set("external_webhook_host", filter.ExternalWebhookHost).
Set("external_webhook_data", filter.ExternalWebhookData).
Set("external_webhook_expect_status", filter.ExternalWebhookExpectStatus).
Set("updated_at", time.Now().Format(time.RFC3339)).
Where("id = ?", filter.ID)

View file

@ -60,55 +60,63 @@ CREATE TABLE irc_channel
CREATE TABLE filter
(
id SERIAL PRIMARY KEY,
enabled BOOLEAN,
name TEXT NOT NULL,
min_size TEXT,
max_size TEXT,
delay INTEGER,
priority INTEGER DEFAULT 0 NOT NULL,
max_downloads INTEGER DEFAULT 0,
max_downloads_unit TEXT,
match_releases TEXT,
except_releases TEXT,
use_regex BOOLEAN,
match_release_groups TEXT,
except_release_groups TEXT,
scene BOOLEAN,
freeleech BOOLEAN,
freeleech_percent TEXT,
shows TEXT,
seasons TEXT,
episodes TEXT,
resolutions TEXT [] DEFAULT '{}' NOT NULL,
codecs TEXT [] DEFAULT '{}' NOT NULL,
sources TEXT [] DEFAULT '{}' NOT NULL,
containers TEXT [] DEFAULT '{}' NOT NULL,
match_hdr TEXT [] DEFAULT '{}',
except_hdr TEXT [] DEFAULT '{}',
match_other TEXT [] DEFAULT '{}',
except_other TEXT [] DEFAULT '{}',
years TEXT,
artists TEXT,
albums TEXT,
release_types_match TEXT [] DEFAULT '{}',
release_types_ignore TEXT [] DEFAULT '{}',
formats TEXT [] DEFAULT '{}',
quality TEXT [] DEFAULT '{}',
media TEXT [] DEFAULT '{}',
log_score INTEGER,
has_log BOOLEAN,
has_cue BOOLEAN,
perfect_flac BOOLEAN,
match_categories TEXT,
except_categories TEXT,
match_uploaders TEXT,
except_uploaders TEXT,
tags TEXT,
except_tags TEXT,
origins TEXT [] DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
id SERIAL PRIMARY KEY,
enabled BOOLEAN,
name TEXT NOT NULL,
min_size TEXT,
max_size TEXT,
delay INTEGER,
priority INTEGER DEFAULT 0 NOT NULL,
max_downloads INTEGER DEFAULT 0,
max_downloads_unit TEXT,
match_releases TEXT,
except_releases TEXT,
use_regex BOOLEAN,
match_release_groups TEXT,
except_release_groups TEXT,
scene BOOLEAN,
freeleech BOOLEAN,
freeleech_percent TEXT,
shows TEXT,
seasons TEXT,
episodes TEXT,
resolutions TEXT [] DEFAULT '{}' NOT NULL,
codecs TEXT [] DEFAULT '{}' NOT NULL,
sources TEXT [] DEFAULT '{}' NOT NULL,
containers TEXT [] DEFAULT '{}' NOT NULL,
match_hdr TEXT [] DEFAULT '{}',
except_hdr TEXT [] DEFAULT '{}',
match_other TEXT [] DEFAULT '{}',
except_other TEXT [] DEFAULT '{}',
years TEXT,
artists TEXT,
albums TEXT,
release_types_match TEXT [] DEFAULT '{}',
release_types_ignore TEXT [] DEFAULT '{}',
formats TEXT [] DEFAULT '{}',
quality TEXT [] DEFAULT '{}',
media TEXT [] DEFAULT '{}',
log_score INTEGER,
has_log BOOLEAN,
has_cue BOOLEAN,
perfect_flac BOOLEAN,
match_categories TEXT,
except_categories TEXT,
match_uploaders TEXT,
except_uploaders TEXT,
tags TEXT,
except_tags TEXT,
origins TEXT [] DEFAULT '{}',
external_script_enabled BOOLEAN DEFAULT FALSE,
external_script_cmd TEXT,
external_script_args TEXT,
external_script_expect_status INTEGER,
external_webhook_enabled BOOLEAN DEFAULT FALSE,
external_webhook_host TEXT,
external_webhook_data TEXT,
external_webhook_expect_status INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE filter_indexer
@ -494,4 +502,29 @@ CREATE INDEX indexer_identifier_index
ALTER TABLE release_action_status
ADD COLUMN filter TEXT;
`,
`
ALTER TABLE filter
ADD COLUMN external_script_enabled BOOLEAN DEFAULT FALSE;
ALTER TABLE filter
ADD COLUMN external_script_cmd TEXT;
ALTER TABLE filter
ADD COLUMN external_script_args TEXT;
ALTER TABLE filter
ADD COLUMN external_script_expect_status INTEGER;
ALTER TABLE filter
ADD COLUMN external_webhook_enabled BOOLEAN DEFAULT FALSE;
ALTER TABLE filter
ADD COLUMN external_webhook_host TEXT;
ALTER TABLE filter
ADD COLUMN external_webhook_data TEXT;
ALTER TABLE filter
ADD COLUMN external_webhook_expect_status INTEGER;
`,
}

View file

@ -60,55 +60,63 @@ CREATE TABLE irc_channel
CREATE TABLE filter
(
id INTEGER PRIMARY KEY,
enabled BOOLEAN,
name TEXT NOT NULL,
min_size TEXT,
max_size TEXT,
delay INTEGER,
priority INTEGER DEFAULT 0 NOT NULL,
max_downloads INTEGER DEFAULT 0,
max_downloads_unit TEXT,
match_releases TEXT,
except_releases TEXT,
use_regex BOOLEAN,
match_release_groups TEXT,
except_release_groups TEXT,
scene BOOLEAN,
freeleech BOOLEAN,
freeleech_percent TEXT,
shows TEXT,
seasons TEXT,
episodes TEXT,
resolutions TEXT [] DEFAULT '{}' NOT NULL,
codecs TEXT [] DEFAULT '{}' NOT NULL,
sources TEXT [] DEFAULT '{}' NOT NULL,
containers TEXT [] DEFAULT '{}' NOT NULL,
match_hdr TEXT [] DEFAULT '{}',
except_hdr TEXT [] DEFAULT '{}',
match_other TEXT [] DEFAULT '{}',
except_other TEXT [] DEFAULT '{}',
years TEXT,
artists TEXT,
albums TEXT,
release_types_match TEXT [] DEFAULT '{}',
release_types_ignore TEXT [] DEFAULT '{}',
formats TEXT [] DEFAULT '{}',
quality TEXT [] DEFAULT '{}',
media TEXT [] DEFAULT '{}',
log_score INTEGER,
has_log BOOLEAN,
has_cue BOOLEAN,
perfect_flac BOOLEAN,
match_categories TEXT,
except_categories TEXT,
match_uploaders TEXT,
except_uploaders TEXT,
tags TEXT,
except_tags TEXT,
origins TEXT [] DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
id INTEGER PRIMARY KEY,
enabled BOOLEAN,
name TEXT NOT NULL,
min_size TEXT,
max_size TEXT,
delay INTEGER,
priority INTEGER DEFAULT 0 NOT NULL,
max_downloads INTEGER DEFAULT 0,
max_downloads_unit TEXT,
match_releases TEXT,
except_releases TEXT,
use_regex BOOLEAN,
match_release_groups TEXT,
except_release_groups TEXT,
scene BOOLEAN,
freeleech BOOLEAN,
freeleech_percent TEXT,
shows TEXT,
seasons TEXT,
episodes TEXT,
resolutions TEXT [] DEFAULT '{}' NOT NULL,
codecs TEXT [] DEFAULT '{}' NOT NULL,
sources TEXT [] DEFAULT '{}' NOT NULL,
containers TEXT [] DEFAULT '{}' NOT NULL,
match_hdr TEXT [] DEFAULT '{}',
except_hdr TEXT [] DEFAULT '{}',
match_other TEXT [] DEFAULT '{}',
except_other TEXT [] DEFAULT '{}',
years TEXT,
artists TEXT,
albums TEXT,
release_types_match TEXT [] DEFAULT '{}',
release_types_ignore TEXT [] DEFAULT '{}',
formats TEXT [] DEFAULT '{}',
quality TEXT [] DEFAULT '{}',
media TEXT [] DEFAULT '{}',
log_score INTEGER,
has_log BOOLEAN,
has_cue BOOLEAN,
perfect_flac BOOLEAN,
match_categories TEXT,
except_categories TEXT,
match_uploaders TEXT,
except_uploaders TEXT,
tags TEXT,
except_tags TEXT,
origins TEXT [] DEFAULT '{}',
external_script_enabled BOOLEAN DEFAULT FALSE,
external_script_cmd TEXT,
external_script_args TEXT,
external_script_expect_status INTEGER,
external_webhook_enabled BOOLEAN DEFAULT FALSE,
external_webhook_host TEXT,
external_webhook_data TEXT,
external_webhook_expect_status INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE filter_indexer
@ -814,4 +822,29 @@ CREATE INDEX indexer_identifier_index
ALTER TABLE release_action_status
ADD COLUMN filter TEXT;
`,
`
ALTER TABLE filter
ADD COLUMN external_script_enabled BOOLEAN DEFAULT FALSE;
ALTER TABLE filter
ADD COLUMN external_script_cmd TEXT;
ALTER TABLE filter
ADD COLUMN external_script_args TEXT;
ALTER TABLE filter
ADD COLUMN external_script_expect_status INTEGER;
ALTER TABLE filter
ADD COLUMN external_webhook_enabled BOOLEAN DEFAULT FALSE;
ALTER TABLE filter
ADD COLUMN external_webhook_host TEXT;
ALTER TABLE filter
ADD COLUMN external_webhook_data TEXT;
ALTER TABLE filter
ADD COLUMN external_webhook_expect_status INTEGER;
`,
}

View file

@ -49,61 +49,69 @@ const (
)
type Filter struct {
ID int `json:"id"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
MinSize string `json:"min_size"`
MaxSize string `json:"max_size"`
Delay int `json:"delay"`
Priority int32 `json:"priority"`
MaxDownloads int `json:"max_downloads"`
MaxDownloadsUnit FilterMaxDownloadsUnit `json:"max_downloads_unit"`
MatchReleases string `json:"match_releases"`
ExceptReleases string `json:"except_releases"`
UseRegex bool `json:"use_regex"`
MatchReleaseGroups string `json:"match_release_groups"`
ExceptReleaseGroups string `json:"except_release_groups"`
Scene bool `json:"scene"`
Origins []string `json:"origins"`
Bonus []string `json:"bonus"`
Freeleech bool `json:"freeleech"`
FreeleechPercent string `json:"freeleech_percent"`
Shows string `json:"shows"`
Seasons string `json:"seasons"`
Episodes string `json:"episodes"`
Resolutions []string `json:"resolutions"` // SD, 480i, 480p, 576p, 720p, 810p, 1080i, 1080p.
Codecs []string `json:"codecs"` // 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"` // 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"`
MatchHDR []string `json:"match_hdr"`
ExceptHDR []string `json:"except_hdr"`
MatchOther []string `json:"match_other"`
ExceptOther []string `json:"except_other"`
Years string `json:"years"`
Artists string `json:"artists"`
Albums string `json:"albums"`
MatchReleaseTypes []string `json:"match_release_types"` // Album,Single,EP
ExceptReleaseTypes string `json:"except_release_types"`
Formats []string `json:"formats"` // MP3, FLAC, Ogg, AAC, AC3, DTS
Quality []string `json:"quality"` // 192, 320, APS (VBR), V2 (VBR), V1 (VBR), APX (VBR), V0 (VBR), q8.x (VBR), Lossless, 24bit Lossless, Other
Media []string `json:"media"` // CD, DVD, Vinyl, Soundboard, SACD, DAT, Cassette, WEB, Other
PerfectFlac bool `json:"perfect_flac"`
Cue bool `json:"cue"`
Log bool `json:"log"`
LogScore int `json:"log_score"`
MatchCategories string `json:"match_categories"`
ExceptCategories string `json:"except_categories"`
MatchUploaders string `json:"match_uploaders"`
ExceptUploaders string `json:"except_uploaders"`
Tags string `json:"tags"`
ExceptTags string `json:"except_tags"`
TagsAny string `json:"tags_any"`
ExceptTagsAny string `json:"except_tags_any"`
Actions []*Action `json:"actions"`
Indexers []Indexer `json:"indexers"`
Downloads *FilterDownloads `json:"-"`
ID int `json:"id"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
MinSize string `json:"min_size"`
MaxSize string `json:"max_size"`
Delay int `json:"delay"`
Priority int32 `json:"priority"`
MaxDownloads int `json:"max_downloads"`
MaxDownloadsUnit FilterMaxDownloadsUnit `json:"max_downloads_unit"`
MatchReleases string `json:"match_releases"`
ExceptReleases string `json:"except_releases"`
UseRegex bool `json:"use_regex"`
MatchReleaseGroups string `json:"match_release_groups"`
ExceptReleaseGroups string `json:"except_release_groups"`
Scene bool `json:"scene"`
Origins []string `json:"origins"`
Bonus []string `json:"bonus"`
Freeleech bool `json:"freeleech"`
FreeleechPercent string `json:"freeleech_percent"`
Shows string `json:"shows"`
Seasons string `json:"seasons"`
Episodes string `json:"episodes"`
Resolutions []string `json:"resolutions"` // SD, 480i, 480p, 576p, 720p, 810p, 1080i, 1080p.
Codecs []string `json:"codecs"` // 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"` // 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"`
MatchHDR []string `json:"match_hdr"`
ExceptHDR []string `json:"except_hdr"`
MatchOther []string `json:"match_other"`
ExceptOther []string `json:"except_other"`
Years string `json:"years"`
Artists string `json:"artists"`
Albums string `json:"albums"`
MatchReleaseTypes []string `json:"match_release_types"` // Album,Single,EP
ExceptReleaseTypes string `json:"except_release_types"`
Formats []string `json:"formats"` // MP3, FLAC, Ogg, AAC, AC3, DTS
Quality []string `json:"quality"` // 192, 320, APS (VBR), V2 (VBR), V1 (VBR), APX (VBR), V0 (VBR), q8.x (VBR), Lossless, 24bit Lossless, Other
Media []string `json:"media"` // CD, DVD, Vinyl, Soundboard, SACD, DAT, Cassette, WEB, Other
PerfectFlac bool `json:"perfect_flac"`
Cue bool `json:"cue"`
Log bool `json:"log"`
LogScore int `json:"log_score"`
MatchCategories string `json:"match_categories"`
ExceptCategories string `json:"except_categories"`
MatchUploaders string `json:"match_uploaders"`
ExceptUploaders string `json:"except_uploaders"`
Tags string `json:"tags"`
ExceptTags string `json:"except_tags"`
TagsAny string `json:"tags_any"`
ExceptTagsAny string `json:"except_tags_any"`
ExternalScriptEnabled bool `json:"external_script_enabled"`
ExternalScriptCmd string `json:"external_script_cmd"`
ExternalScriptArgs string `json:"external_script_args"`
ExternalScriptExpectStatus int `json:"external_script_expect_status"`
ExternalWebhookEnabled bool `json:"external_webhook_enabled"`
ExternalWebhookHost string `json:"external_webhook_host"`
ExternalWebhookData string `json:"external_webhook_data"`
ExternalWebhookExpectStatus int `json:"external_webhook_expect_status"`
Actions []*Action `json:"actions"`
Indexers []Indexer `json:"indexers"`
Downloads *FilterDownloads `json:"-"`
}
func (f Filter) CheckFilter(r *Release) ([]string, bool) {

View file

@ -1,4 +1,4 @@
package action
package domain
import (
"bytes"
@ -6,7 +6,6 @@ import (
"text/template"
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/pkg/errors"
)
@ -32,7 +31,7 @@ type Macro struct {
CurrentSecond int
}
func NewMacro(release domain.Release) Macro {
func NewMacro(release Release) Macro {
currentTime := time.Now()
ma := Macro{

View file

@ -1,12 +1,10 @@
package action
package domain
import (
"fmt"
"testing"
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/stretchr/testify/assert"
)
@ -25,14 +23,14 @@ func TestMacros_Parse(t *testing.T) {
tests := []struct {
name string
fields fields
release domain.Release
release Release
args args
want string
wantErr bool
}{
{
name: "test_ok",
release: domain.Release{
release: Release{
TorrentName: "This movie 2021",
TorrentTmpFile: "/tmp/a-temporary-file.torrent",
Indexer: "mock1",
@ -43,7 +41,7 @@ func TestMacros_Parse(t *testing.T) {
},
{
name: "test_bad",
release: domain.Release{
release: Release{
TorrentName: "This movie 2021",
TorrentTmpFile: "/tmp/a-temporary-file.torrent",
Indexer: "mock1",
@ -54,7 +52,7 @@ func TestMacros_Parse(t *testing.T) {
},
{
name: "test_program_arg",
release: domain.Release{
release: Release{
TorrentName: "This movie 2021",
TorrentTmpFile: "/tmp/a-temporary-file.torrent",
Indexer: "mock1",
@ -65,7 +63,7 @@ func TestMacros_Parse(t *testing.T) {
},
{
name: "test_program_arg_bad",
release: domain.Release{
release: Release{
TorrentTmpFile: "/tmp/a-temporary-file.torrent",
Indexer: "mock1",
},
@ -75,7 +73,7 @@ func TestMacros_Parse(t *testing.T) {
},
{
name: "test_program_arg",
release: domain.Release{
release: Release{
TorrentName: "This movie 2021",
TorrentTmpFile: "/tmp/a-temporary-file.torrent",
Indexer: "mock1",
@ -86,7 +84,7 @@ func TestMacros_Parse(t *testing.T) {
},
{
name: "test_args_long",
release: domain.Release{
release: Release{
TorrentName: "This movie 2021",
TorrentURL: "https://some.site/download/fakeid",
Indexer: "mock1",
@ -97,7 +95,7 @@ func TestMacros_Parse(t *testing.T) {
},
{
name: "test_args_long_1",
release: domain.Release{
release: Release{
TorrentName: "This movie 2021",
TorrentURL: "https://some.site/download/fakeid",
Indexer: "mock1",
@ -108,7 +106,7 @@ func TestMacros_Parse(t *testing.T) {
},
{
name: "test_args_category",
release: domain.Release{
release: Release{
TorrentName: "This movie 2021",
TorrentURL: "https://some.site/download/fakeid",
Indexer: "mock1",
@ -119,7 +117,7 @@ func TestMacros_Parse(t *testing.T) {
},
{
name: "test_args_category_year",
release: domain.Release{
release: Release{
TorrentName: "This movie 2021",
TorrentURL: "https://some.site/download/fakeid",
Indexer: "mock1",
@ -130,7 +128,7 @@ func TestMacros_Parse(t *testing.T) {
},
{
name: "test_args_category_year",
release: domain.Release{
release: Release{
TorrentName: "This movie 2021",
TorrentURL: "https://some.site/download/fakeid",
Indexer: "mock1",
@ -143,7 +141,7 @@ func TestMacros_Parse(t *testing.T) {
},
{
name: "test_args_category_and_if",
release: domain.Release{
release: Release{
TorrentName: "This movie 2021",
TorrentURL: "https://some.site/download/fakeid",
Indexer: "mock1",
@ -156,7 +154,7 @@ func TestMacros_Parse(t *testing.T) {
},
{
name: "test_release_year_1",
release: domain.Release{
release: Release{
TorrentName: "This movie 2021",
TorrentURL: "https://some.site/download/fakeid",
Indexer: "mock1",

View file

@ -344,6 +344,10 @@ func (r *Release) addRejection(reason string) {
r.Rejections = append(r.Rejections, reason)
}
func (r *Release) AddRejectionF(format string, v ...interface{}) {
r.addRejectionF(format, v...)
}
func (r *Release) addRejectionF(format string, v ...interface{}) {
r.Rejections = append(r.Rejections, fmt.Sprintf(format, v...))
}

View file

@ -1,15 +1,22 @@
package filter
import (
"bytes"
"context"
"errors"
"crypto/tls"
"fmt"
"net/http"
"os/exec"
"strings"
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/internal/indexer"
"github.com/autobrr/autobrr/internal/logger"
"github.com/autobrr/autobrr/pkg/errors"
"github.com/dustin/go-humanize"
"github.com/mattn/go-shellwords"
"github.com/rs/zerolog"
)
@ -269,6 +276,37 @@ func (s *service) CheckFilter(f domain.Filter, release *domain.Release) (bool, e
}
}
// run external script
if f.ExternalScriptEnabled && f.ExternalScriptCmd != "" {
exitCode, err := s.execCmd(release, f.ExternalScriptCmd, f.ExternalScriptArgs)
if err != nil {
s.log.Error().Err(err).Msgf("filter.Service.CheckFilter: error executing external command for filter: %+v", f.Name)
return false, err
}
if exitCode != f.ExternalScriptExpectStatus {
s.log.Trace().Msgf("filter.Service.CheckFilter: external script unexpected exit code. got: %v want: %v", exitCode, f.ExternalScriptExpectStatus)
release.AddRejectionF("external script unexpected exit code. got: %v want: %v", exitCode, f.ExternalScriptExpectStatus)
return false, nil
}
}
// run external webhook
if f.ExternalWebhookEnabled && f.ExternalWebhookHost != "" && f.ExternalWebhookData != "" {
// run external scripts
statusCode, err := s.webhook(release, f.ExternalWebhookHost, f.ExternalWebhookData)
if err != nil {
s.log.Error().Err(err).Msgf("filter.Service.CheckFilter: error executing external webhook for filter: %v", f.Name)
return false, err
}
if statusCode != f.ExternalWebhookExpectStatus {
s.log.Trace().Msgf("filter.Service.CheckFilter: external webhook unexpected status code. got: %v want: %v", statusCode, f.ExternalWebhookExpectStatus)
release.AddRejectionF("external webhook unexpected status code. got: %v want: %v", statusCode, f.ExternalWebhookExpectStatus)
return false, nil
}
}
// found matching filter, lets find the filter actions and attach
actions, err := s.actionRepo.FindByFilterID(context.TODO(), f.ID)
if err != nil {
@ -279,7 +317,7 @@ func (s *service) CheckFilter(f domain.Filter, release *domain.Release) (bool, e
// if no actions, continue to next filter
if len(actions) == 0 {
s.log.Trace().Msgf("filter.Service.CheckFilter: no actions found for filter '%v', trying next one..", f.Name)
return false, err
return false, nil
}
release.Filter.Actions = actions
@ -371,3 +409,102 @@ func checkSizeFilter(minSize string, maxSize string, releaseSize uint64) (bool,
return true, nil
}
func (s *service) execCmd(release *domain.Release, cmd string, args string) (int, error) {
s.log.Debug().Msgf("filter exec release: %v", release.TorrentName)
if release.TorrentTmpFile == "" && strings.Contains(args, "TorrentPathName") {
if err := release.DownloadTorrentFile(); err != nil {
return 0, errors.Wrap(err, "error downloading torrent file for release: %v", release.TorrentName)
}
}
// check if program exists
cmd, err := exec.LookPath(cmd)
if err != nil {
return 0, errors.Wrap(err, "exec failed, could not find program: %v", cmd)
}
// handle args and replace vars
m := domain.NewMacro(*release)
// parse and replace values in argument string before continuing
parsedArgs, err := m.Parse(args)
if err != nil {
return 0, errors.Wrap(err, "could not parse macro")
}
// we need to split on space into a string slice, so we can spread the args into exec
p := shellwords.NewParser()
p.ParseBacktick = true
commandArgs, err := p.Parse(parsedArgs)
if err != nil {
return 0, errors.Wrap(err, "could not parse into shell-words")
}
start := time.Now()
// setup command and args
command := exec.Command(cmd, commandArgs...)
err = command.Run()
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
s.log.Debug().Msgf("filter script command exited with non zero code: %v", exitErr.ExitCode())
return exitErr.ExitCode(), nil
}
duration := time.Since(start)
s.log.Debug().Msgf("executed external script: (%v), args: (%v) for release: (%v) indexer: (%v) total time (%v)", cmd, args, release.TorrentName, release.Indexer, duration)
return 0, nil
}
func (s *service) webhook(release *domain.Release, url string, data string) (int, error) {
if release.TorrentTmpFile == "" && strings.Contains(data, "TorrentPathName") {
if err := release.DownloadTorrentFile(); err != nil {
return 0, errors.Wrap(err, "webhook: could not download torrent file for release: %v", release.TorrentName)
}
}
m := domain.NewMacro(*release)
// parse and replace values in argument string before continuing
dataArgs, err := m.Parse(data)
if err != nil {
return 0, errors.Wrap(err, "could not parse webhook data macro: %v", data)
}
t := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := http.Client{Transport: t, Timeout: 15 * time.Second}
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBufferString(dataArgs))
if err != nil {
return 0, errors.Wrap(err, "could not build request for webhook")
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "autobrr")
res, err := client.Do(req)
if err != nil {
return 0, errors.Wrap(err, "could not make request for webhook")
}
defer res.Body.Close()
if res.StatusCode > 299 {
return res.StatusCode, nil
}
s.log.Debug().Msgf("successfully ran external webhook filter to: (%v) payload: (%v)", url, dataArgs)
return res.StatusCode, nil
}

View file

@ -114,8 +114,6 @@ func (s *service) Process(release *domain.Release) {
release.FilterName = f.Name
release.FilterID = f.ID
// TODO filter limit checks
// test filter
match, err := s.filterSvc.CheckFilter(f, release)
if err != nil {

View file

@ -13,6 +13,7 @@ interface TextFieldProps {
columns?: COL_WIDTHS;
autoComplete?: string;
hidden?: boolean;
disabled?: boolean;
}
export const TextField = ({
@ -22,7 +23,8 @@ export const TextField = ({
placeholder,
columns,
autoComplete,
hidden
hidden,
disabled,
}: TextFieldProps) => (
<div
className={classNames(
@ -47,7 +49,12 @@ export const TextField = ({
type="text"
defaultValue={defaultValue}
autoComplete={autoComplete}
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "mt-2 block w-full dark:bg-gray-800 dark:text-gray-100 rounded-md")}
className={classNames(
meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700",
disabled ? "bg-gray-100 dark:bg-gray-700 cursor-not-allowed" : "dark:bg-gray-800",
"mt-2 block w-full dark:text-gray-100 rounded-md",
)}
disabled={disabled}
placeholder={placeholder}
/>
@ -60,6 +67,70 @@ export const TextField = ({
</div>
);
interface TextAreaProps {
name: string;
defaultValue?: string;
label?: string;
placeholder?: string;
columns?: COL_WIDTHS;
rows?: number;
autoComplete?: string;
hidden?: boolean;
disabled?: boolean;
}
export const TextArea = ({
name,
defaultValue,
label,
placeholder,
columns,
rows,
autoComplete,
hidden,
disabled,
}: TextAreaProps) => (
<div
className={classNames(
hidden ? "hidden" : "",
columns ? `col-span-${columns}` : "col-span-12"
)}
>
{label && (
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
{label}
</label>
)}
<Field name={name}>
{({
field,
meta
}: FieldProps) => (
<div>
<textarea
{...field}
id={name}
rows={rows}
defaultValue={defaultValue}
autoComplete={autoComplete}
className={classNames(
meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700",
disabled ? "bg-gray-100 dark:bg-gray-700 cursor-not-allowed" : "dark:bg-gray-800",
"mt-2 block w-full dark:text-gray-100 rounded-md",
)}
placeholder={placeholder}
disabled={disabled}
/>
{meta.touched && meta.error && (
<p className="error text-sm text-red-600 mt-1">* {meta.error}</p>
)}
</div>
)}
</Field>
</div>
);
interface PasswordFieldProps {
name: string;
label?: string;
@ -132,13 +203,15 @@ interface NumberFieldProps {
label?: string;
placeholder?: string;
step?: number;
disabled?: boolean;
}
export const NumberField = ({
name,
label,
placeholder,
step
step,
disabled,
}: NumberFieldProps) => (
<div className="col-span-12 sm:col-span-6">
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
@ -159,9 +232,11 @@ export const NumberField = ({
meta.touched && meta.error
? "focus:ring-red-500 focus:border-red-500 border-red-500"
: "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300",
"mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-100 rounded-md"
"mt-2 block w-full border border-gray-300 dark:border-gray-700 dark:text-gray-100 rounded-md",
disabled ? "bg-gray-100 dark:bg-gray-700 cursor-not-allowed" : "dark:bg-gray-800",
)}
placeholder={placeholder}
disabled={disabled}
/>
{meta.touched && meta.error && (
<div className="error">{meta.error}</div>

View file

@ -74,16 +74,18 @@ interface SwitchGroupProps {
label?: string;
description?: string;
className?: string;
heading?: boolean;
}
const SwitchGroup = ({
name,
label,
description
description,
heading,
}: SwitchGroupProps) => (
<HeadlessSwitch.Group as="ol" className="py-4 flex items-center justify-between">
{label && <div className="flex flex-col">
<HeadlessSwitch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-gray-100"
<HeadlessSwitch.Label as={heading ? "h2" : "p"} className={classNames("font-medium text-gray-900 dark:text-gray-100", heading ? "text-lg" : "text-sm")}
passive>
{label}
</HeadlessSwitch.Label>

View file

@ -9,7 +9,7 @@ import {
useParams
} from "react-router-dom";
import { toast } from "react-hot-toast";
import { Field, FieldArray, FieldProps, Form, Formik, FormikValues } from "formik";
import { Field, FieldArray, FieldProps, Form, Formik, FormikValues, useFormikContext } from "formik";
import { Dialog, Transition, Switch as SwitchBasic } from "@headlessui/react";
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/solid";
@ -50,6 +50,7 @@ import { AlertWarning } from "../../components/alerts";
import { DeleteModal } from "../../components/modals";
import { TitleSubtitle } from "../../components/headings";
import { EmptyListState } from "../../components/emptystates";
import { TextArea } from "../../components/inputs/input";
interface tabType {
name: string;
@ -61,6 +62,7 @@ const tabs: tabType[] = [
{ name: "Movies and TV", href: "movies-tv" },
{ name: "Music", href: "music" },
{ name: "Advanced", href: "advanced" },
{ name: "External", href: "external" },
{ name: "Actions", href: "actions" }
];
@ -280,7 +282,15 @@ export default function FilterDetails() {
albums: filter.albums,
origins: filter.origins || [],
indexers: filter.indexers || [],
actions: filter.actions || []
actions: filter.actions || [],
external_script_enabled: filter.external_script_enabled || false,
external_script_cmd: filter.external_script_cmd || "",
external_script_args: filter.external_script_args || "",
external_script_expect_status: filter.external_script_expect_status || 0,
external_webhook_enabled: filter.external_webhook_enabled || false,
external_webhook_host: filter.external_webhook_host || "",
external_webhook_data: filter.external_webhook_data ||"",
external_webhook_expect_status: filter.external_webhook_expect_status || 0,
} as Filter}
onSubmit={handleSubmit}
>
@ -291,6 +301,7 @@ export default function FilterDetails() {
<Route path="movies-tv" element={<MoviesTv />} />
<Route path="music" element={<Music />} />
<Route path="advanced" element={<Advanced />} />
<Route path="external" element={<External />} />
<Route path="actions" element={<FilterActions filter={filter} values={values} />}
/>
</Routes>
@ -527,6 +538,75 @@ function CollapsableSection({ title, subtitle, children }: CollapsableSectionPro
);
}
export function External() {
const { values } = useFormikContext<Filter>();
return (
<div>
<div className="mt-6">
<SwitchGroup name="external_script_enabled" heading={true} label="Script" description="Run external script and check status as part of filtering" />
<div className="mt-6 grid grid-cols-12 gap-6">
<TextField
name="external_script_cmd"
label="Command"
columns={6}
placeholder="Path to program eg. /bin/test"
disabled={!values.external_script_enabled}
/>
<TextField
name="external_script_args"
label="Arguments"
columns={6}
placeholder="Arguments eg. --test"
disabled={!values.external_script_enabled}
/>
<NumberField
name="external_script_expect_status"
label="Expected exit status"
placeholder="0"
disabled={!values.external_script_enabled}
/>
</div>
</div>
<div className="mt-6">
<div className="border-t dark:border-gray-700">
<SwitchGroup name="external_webhook_enabled" heading={true} label="Webhook" description="Run external webhook and check status as part of filtering" />
</div>
<div className="mt-6 grid grid-cols-12 gap-6">
<div className="grid col-span-6 gap-6">
<TextField
name="external_webhook_host"
label="Host"
columns={6}
placeholder="Host eg. http://localhost/webhook"
disabled={!values.external_webhook_enabled}
/>
<NumberField
name="external_webhook_expect_status"
label="Expected http status"
placeholder="200"
disabled={!values.external_webhook_enabled}
/>
</div>
<TextArea
name="external_webhook_data"
label="Data (json)"
columns={6}
rows={5}
placeholder={"{ \"key\": \"value\" }"}
disabled={!values.external_webhook_enabled}
/>
</div>
</div>
</div>
);
}
interface FilterActionsProps {
filter: Filter;
values: FormikValues;

View file

@ -52,6 +52,14 @@ interface Filter {
except_tags_any: string;
actions: Action[];
indexers: Indexer[];
external_script_enabled: boolean;
external_script_cmd: string;
external_script_args: string;
external_script_expect_status: number;
external_webhook_enabled: boolean;
external_webhook_host: string;
external_webhook_data: string;
external_webhook_expect_status: number;
}
interface Action {