diff --git a/internal/action/deluge.go b/internal/action/deluge.go index d19e75a..d3fc569 100644 --- a/internal/action/deluge.go +++ b/internal/action/deluge.go @@ -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{} diff --git a/internal/action/exec.go b/internal/action/exec.go index 58c1894..28d3f2a 100644 --- a/internal/action/exec.go +++ b/internal/action/exec.go @@ -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) diff --git a/internal/action/qbittorrent.go b/internal/action/qbittorrent.go index 6b1f304..60787e4 100644 --- a/internal/action/qbittorrent.go +++ b/internal/action/qbittorrent.go @@ -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{} diff --git a/internal/action/run.go b/internal/action/run.go index 9e43a54..3a26d4b 100644 --- a/internal/action/run.go +++ b/internal/action/run.go @@ -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) diff --git a/internal/database/filter.go b/internal/database/filter.go index d77b336..20a12bd 100644 --- a/internal/database/filter.go +++ b/internal/database/filter.go @@ -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) diff --git a/internal/database/postgres_migrate.go b/internal/database/postgres_migrate.go index 817e0d3..cf686bd 100644 --- a/internal/database/postgres_migrate.go +++ b/internal/database/postgres_migrate.go @@ -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; + `, } diff --git a/internal/database/sqlite_migrate.go b/internal/database/sqlite_migrate.go index 732f526..782b935 100644 --- a/internal/database/sqlite_migrate.go +++ b/internal/database/sqlite_migrate.go @@ -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; + `, } diff --git a/internal/domain/filter.go b/internal/domain/filter.go index c70718f..9ab1b76 100644 --- a/internal/domain/filter.go +++ b/internal/domain/filter.go @@ -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) { diff --git a/internal/action/macros.go b/internal/domain/macros.go similarity index 94% rename from internal/action/macros.go rename to internal/domain/macros.go index 3de5df6..f0da4cd 100644 --- a/internal/action/macros.go +++ b/internal/domain/macros.go @@ -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{ diff --git a/internal/action/macros_test.go b/internal/domain/macros_test.go similarity index 91% rename from internal/action/macros_test.go rename to internal/domain/macros_test.go index 038a5a6..adf2cce 100644 --- a/internal/action/macros_test.go +++ b/internal/domain/macros_test.go @@ -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", diff --git a/internal/domain/release.go b/internal/domain/release.go index a84147a..972c3ae 100644 --- a/internal/domain/release.go +++ b/internal/domain/release.go @@ -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...)) } diff --git a/internal/filter/service.go b/internal/filter/service.go index 370984d..79e8a2a 100644 --- a/internal/filter/service.go +++ b/internal/filter/service.go @@ -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 +} diff --git a/internal/release/service.go b/internal/release/service.go index 0251027..f33714e 100644 --- a/internal/release/service.go +++ b/internal/release/service.go @@ -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 { diff --git a/web/src/components/inputs/input.tsx b/web/src/components/inputs/input.tsx index f6484b8..c521b9a 100644 --- a/web/src/components/inputs/input.tsx +++ b/web/src/components/inputs/input.tsx @@ -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) => (
@@ -60,6 +67,70 @@ export const TextField = ({
); +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) => ( +