feat(filters): add match logic for tags and except tags (#810)

* feat(filters): add fields for tag and except tag matching logic

* refactor(filters): rearrange and simplify logic for containsAllMatch

---------

Co-authored-by: Gustavo Machado <me@gstv.dev>
This commit is contained in:
Gustavo Machado 2023-04-10 15:11:44 +01:00 committed by GitHub
parent d48e94ff33
commit ef75b67b25
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 364 additions and 76 deletions

View file

@ -221,6 +221,8 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter
"except_language", "except_language",
"tags", "tags",
"except_tags", "except_tags",
"tags_match_logic",
"except_tags_match_logic",
"origins", "origins",
"except_origins", "except_origins",
"external_script_enabled", "external_script_enabled",
@ -248,11 +250,11 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter
} }
var f domain.Filter var f domain.Filter
var minSize, maxSize, maxDownloadsUnit, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, matchReleaseTags, exceptReleaseTags, freeleechPercent, shows, seasons, episodes, years, artists, albums, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags, extScriptCmd, extScriptArgs, extWebhookHost, extWebhookData sql.NullString var minSize, maxSize, maxDownloadsUnit, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, matchReleaseTags, exceptReleaseTags, freeleechPercent, shows, seasons, episodes, years, artists, albums, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags, tagsMatchLogic, exceptTagsMatchLogic, extScriptCmd, extScriptArgs, extWebhookHost, extWebhookData sql.NullString
var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac, extScriptEnabled, extWebhookEnabled sql.NullBool var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac, extScriptEnabled, extWebhookEnabled sql.NullBool
var delay, maxDownloads, logScore, extWebhookStatus, extScriptStatus sql.NullInt32 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, &matchReleaseTags, &exceptReleaseTags, &f.UseRegexReleaseTags, &scene, &freeleech, &freeleechPercent, &f.SmartEpisode, &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, pq.Array(&f.MatchLanguage), pq.Array(&f.ExceptLanguage), &tags, &exceptTags, pq.Array(&f.Origins), pq.Array(&f.ExceptOrigins), &extScriptEnabled, &extScriptCmd, &extScriptArgs, &extScriptStatus, &extWebhookEnabled, &extWebhookHost, &extWebhookData, &extWebhookStatus, &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, &matchReleaseTags, &exceptReleaseTags, &f.UseRegexReleaseTags, &scene, &freeleech, &freeleechPercent, &f.SmartEpisode, &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, pq.Array(&f.MatchLanguage), pq.Array(&f.ExceptLanguage), &tags, &exceptTags, &tagsMatchLogic, &exceptTagsMatchLogic, pq.Array(&f.Origins), pq.Array(&f.ExceptOrigins), &extScriptEnabled, &extScriptCmd, &extScriptArgs, &extScriptStatus, &extWebhookEnabled, &extWebhookHost, &extWebhookData, &extWebhookStatus, &f.CreatedAt, &f.UpdatedAt); err != nil {
return nil, errors.Wrap(err, "error scanning row") return nil, errors.Wrap(err, "error scanning row")
} }
@ -284,6 +286,8 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter
f.ExceptUploaders = exceptUploaders.String f.ExceptUploaders = exceptUploaders.String
f.Tags = tags.String f.Tags = tags.String
f.ExceptTags = exceptTags.String f.ExceptTags = exceptTags.String
f.TagsMatchLogic = tagsMatchLogic.String
f.ExceptTagsMatchLogic = exceptTagsMatchLogic.String
f.UseRegex = useRegex.Bool f.UseRegex = useRegex.Bool
f.Scene = scene.Bool f.Scene = scene.Bool
f.Freeleech = freeleech.Bool f.Freeleech = freeleech.Bool
@ -384,6 +388,8 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, tx *Tx, indexe
"f.except_language", "f.except_language",
"f.tags", "f.tags",
"f.except_tags", "f.except_tags",
"f.tags_match_logic",
"f.except_tags_match_logic",
"f.origins", "f.origins",
"f.except_origins", "f.except_origins",
"f.external_script_enabled", "f.external_script_enabled",
@ -421,11 +427,11 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, tx *Tx, indexe
for rows.Next() { for rows.Next() {
var f domain.Filter var f domain.Filter
var minSize, maxSize, maxDownloadsUnit, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, matchReleaseTags, exceptReleaseTags, freeleechPercent, shows, seasons, episodes, years, artists, albums, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags, extScriptCmd, extScriptArgs, extWebhookHost, extWebhookData sql.NullString var minSize, maxSize, maxDownloadsUnit, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, matchReleaseTags, exceptReleaseTags, freeleechPercent, shows, seasons, episodes, years, artists, albums, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags, tagsMatchLogic, exceptTagsMatchLogic, extScriptCmd, extScriptArgs, extWebhookHost, extWebhookData sql.NullString
var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac, extScriptEnabled, extWebhookEnabled sql.NullBool var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac, extScriptEnabled, extWebhookEnabled sql.NullBool
var delay, maxDownloads, logScore, extWebhookStatus, extScriptStatus sql.NullInt32 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, &matchReleaseTags, &exceptReleaseTags, &f.UseRegexReleaseTags, &scene, &freeleech, &freeleechPercent, &f.SmartEpisode, &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, pq.Array(&f.MatchLanguage), pq.Array(&f.ExceptLanguage), &tags, &exceptTags, pq.Array(&f.Origins), pq.Array(&f.ExceptOrigins), &extScriptEnabled, &extScriptCmd, &extScriptArgs, &extScriptStatus, &extWebhookEnabled, &extWebhookHost, &extWebhookData, &extWebhookStatus, &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, &matchReleaseTags, &exceptReleaseTags, &f.UseRegexReleaseTags, &scene, &freeleech, &freeleechPercent, &f.SmartEpisode, &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, pq.Array(&f.MatchLanguage), pq.Array(&f.ExceptLanguage), &tags, &exceptTags, &tagsMatchLogic, &exceptTagsMatchLogic, pq.Array(&f.Origins), pq.Array(&f.ExceptOrigins), &extScriptEnabled, &extScriptCmd, &extScriptArgs, &extScriptStatus, &extWebhookEnabled, &extWebhookHost, &extWebhookData, &extWebhookStatus, &f.CreatedAt, &f.UpdatedAt); err != nil {
return nil, errors.Wrap(err, "error scanning row") return nil, errors.Wrap(err, "error scanning row")
} }
@ -457,6 +463,8 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, tx *Tx, indexe
f.ExceptUploaders = exceptUploaders.String f.ExceptUploaders = exceptUploaders.String
f.Tags = tags.String f.Tags = tags.String
f.ExceptTags = exceptTags.String f.ExceptTags = exceptTags.String
f.TagsMatchLogic = tagsMatchLogic.String
f.ExceptTagsMatchLogic = exceptTagsMatchLogic.String
f.UseRegex = useRegex.Bool f.UseRegex = useRegex.Bool
f.Scene = scene.Bool f.Scene = scene.Bool
f.Freeleech = freeleech.Bool f.Freeleech = freeleech.Bool
@ -521,6 +529,8 @@ func (r *FilterRepo) Store(ctx context.Context, filter domain.Filter) (*domain.F
"except_language", "except_language",
"tags", "tags",
"except_tags", "except_tags",
"tags_match_logic",
"except_tags_match_logic",
"artists", "artists",
"albums", "albums",
"release_types_match", "release_types_match",
@ -583,6 +593,8 @@ func (r *FilterRepo) Store(ctx context.Context, filter domain.Filter) (*domain.F
pq.Array(filter.ExceptLanguage), pq.Array(filter.ExceptLanguage),
filter.Tags, filter.Tags,
filter.ExceptTags, filter.ExceptTags,
filter.TagsMatchLogic,
filter.ExceptTagsMatchLogic,
filter.Artists, filter.Artists,
filter.Albums, filter.Albums,
pq.Array(filter.MatchReleaseTypes), pq.Array(filter.MatchReleaseTypes),
@ -664,6 +676,8 @@ func (r *FilterRepo) Update(ctx context.Context, filter domain.Filter) (*domain.
Set("except_language", pq.Array(filter.ExceptLanguage)). Set("except_language", pq.Array(filter.ExceptLanguage)).
Set("tags", filter.Tags). Set("tags", filter.Tags).
Set("except_tags", filter.ExceptTags). Set("except_tags", filter.ExceptTags).
Set("tags_match_logic", filter.TagsMatchLogic).
Set("except_tags_match_logic", filter.ExceptTagsMatchLogic).
Set("artists", filter.Artists). Set("artists", filter.Artists).
Set("albums", filter.Albums). Set("albums", filter.Albums).
Set("release_types_match", pq.Array(filter.MatchReleaseTypes)). Set("release_types_match", pq.Array(filter.MatchReleaseTypes)).
@ -825,6 +839,12 @@ func (r *FilterRepo) UpdatePartial(ctx context.Context, filter domain.FilterUpda
if filter.ExceptTags != nil { if filter.ExceptTags != nil {
q = q.Set("except_tags", filter.ExceptTags) q = q.Set("except_tags", filter.ExceptTags)
} }
if filter.TagsMatchLogic != nil {
q = q.Set("tags_match_logic", filter.TagsMatchLogic)
}
if filter.ExceptTagsMatchLogic != nil {
q = q.Set("except_tags_match_logic", filter.ExceptTagsMatchLogic)
}
if filter.Artists != nil { if filter.Artists != nil {
q = q.Set("artists", filter.Artists) q = q.Set("artists", filter.Artists)
} }

View file

@ -115,6 +115,8 @@ CREATE TABLE filter
except_language TEXT [] DEFAULT '{}', except_language TEXT [] DEFAULT '{}',
tags TEXT, tags TEXT,
except_tags TEXT, except_tags TEXT,
tags_match_logic TEXT,
except_tags_match_logic TEXT,
origins TEXT [] DEFAULT '{}', origins TEXT [] DEFAULT '{}',
except_origins TEXT [] DEFAULT '{}', except_origins TEXT [] DEFAULT '{}',
external_script_enabled BOOLEAN DEFAULT FALSE, external_script_enabled BOOLEAN DEFAULT FALSE,
@ -655,4 +657,18 @@ ADD COLUMN info_url TEXT;
ALTER TABLE "release" ALTER TABLE "release"
ADD COLUMN download_url TEXT; ADD COLUMN download_url TEXT;
`, `,
`ALTER TABLE filter
ADD COLUMN tags_match_logic TEXT;
ALTER TABLE filter
ADD COLUMN except_tags_match_logic TEXT;
UPDATE filter
SET tags_match_logic = 'ANY'
WHERE tags IS NOT NULL;
UPDATE filter
SET except_tags_match_logic = 'ANY'
WHERE except_tags IS NOT NULL;
`,
} }

View file

@ -115,6 +115,8 @@ CREATE TABLE filter
except_language TEXT [] DEFAULT '{}', except_language TEXT [] DEFAULT '{}',
tags TEXT, tags TEXT,
except_tags TEXT, except_tags TEXT,
tags_match_logic TEXT,
except_tags_match_logic TEXT,
origins TEXT [] DEFAULT '{}', origins TEXT [] DEFAULT '{}',
except_origins TEXT [] DEFAULT '{}', except_origins TEXT [] DEFAULT '{}',
external_script_enabled BOOLEAN DEFAULT FALSE, external_script_enabled BOOLEAN DEFAULT FALSE,
@ -1048,4 +1050,18 @@ ADD COLUMN info_url TEXT;
ALTER TABLE "release" ALTER TABLE "release"
ADD COLUMN download_url TEXT; ADD COLUMN download_url TEXT;
`, `,
`ALTER TABLE filter
ADD COLUMN tags_match_logic TEXT;
ALTER TABLE filter
ADD COLUMN except_tags_match_logic TEXT;
UPDATE filter
SET tags_match_logic = 'ANY'
WHERE tags IS NOT NULL;
UPDATE filter
SET except_tags_match_logic = 'ANY'
WHERE except_tags IS NOT NULL;
`,
} }

View file

@ -115,6 +115,8 @@ type Filter struct {
ExceptTags string `json:"except_tags,omitempty"` ExceptTags string `json:"except_tags,omitempty"`
TagsAny string `json:"tags_any,omitempty"` TagsAny string `json:"tags_any,omitempty"`
ExceptTagsAny string `json:"except_tags_any,omitempty"` ExceptTagsAny string `json:"except_tags_any,omitempty"`
TagsMatchLogic string `json:"tags_match_logic,omitempty"`
ExceptTagsMatchLogic string `json:"except_tags_match_logic,omitempty"`
MatchReleaseTags string `json:"match_release_tags,omitempty"` MatchReleaseTags string `json:"match_release_tags,omitempty"`
ExceptReleaseTags string `json:"except_release_tags,omitempty"` ExceptReleaseTags string `json:"except_release_tags,omitempty"`
UseRegexReleaseTags bool `json:"use_regex_release_tags,omitempty"` UseRegexReleaseTags bool `json:"use_regex_release_tags,omitempty"`
@ -190,6 +192,8 @@ type FilterUpdate struct {
ExceptTags *string `json:"except_tags,omitempty"` ExceptTags *string `json:"except_tags,omitempty"`
TagsAny *string `json:"tags_any,omitempty"` TagsAny *string `json:"tags_any,omitempty"`
ExceptTagsAny *string `json:"except_tags_any,omitempty"` ExceptTagsAny *string `json:"except_tags_any,omitempty"`
TagsMatchLogic *string `json:"tags_match_logic,omitempty"`
ExceptTagsMatchLogic *string `json:"except_tags_match_logic,omitempty"`
ExternalScriptEnabled *bool `json:"external_script_enabled,omitempty"` ExternalScriptEnabled *bool `json:"external_script_enabled,omitempty"`
ExternalScriptCmd *string `json:"external_script_cmd,omitempty"` ExternalScriptCmd *string `json:"external_script_cmd,omitempty"`
ExternalScriptArgs *string `json:"external_script_args,omitempty"` ExternalScriptArgs *string `json:"external_script_args,omitempty"`
@ -379,12 +383,20 @@ func (f Filter) CheckFilter(r *Release) ([]string, bool) {
r.addRejectionF("size not matching. got: %v want min: %v max: %v", r.Size, f.MinSize, f.MaxSize) r.addRejectionF("size not matching. got: %v want min: %v max: %v", r.Size, f.MinSize, f.MaxSize)
} }
if f.Tags != "" && !containsAny(r.Tags, f.Tags) { if f.Tags != "" {
r.addRejectionF("tags not matching. got: %v want: %v", r.Tags, f.Tags) if f.TagsMatchLogic == "ANY" && !containsAny(r.Tags, f.Tags) {
r.addRejectionF("tags not matching. got: %v want: %v", r.Tags, f.Tags)
} else if f.TagsMatchLogic == "ALL" && !containsAll(r.Tags, f.Tags) {
r.addRejectionF("tags not matching. got: %v want(all): %v", r.Tags, f.Tags)
}
} }
if f.ExceptTags != "" && containsAny(r.Tags, f.ExceptTags) { if f.ExceptTags != "" {
r.addRejectionF("tags unwanted. got: %v want: %v", r.Tags, f.ExceptTags) if f.ExceptTagsMatchLogic == "ANY" && containsAny(r.Tags, f.ExceptTags) {
r.addRejectionF("tags unwanted. got: %v want: %v", r.Tags, f.ExceptTags)
} else if f.ExceptTagsMatchLogic == "ALL" && containsAll(r.Tags, f.ExceptTags) {
r.addRejectionF("tags unwanted. got: %v want(all): %v", r.Tags, f.ExceptTags)
}
} }
if len(f.Artists) > 0 && !contains(r.Artists, f.Artists) { if len(f.Artists) > 0 && !contains(r.Artists, f.Artists) {
@ -620,6 +632,10 @@ func containsAny(tags []string, filter string) bool {
return containsMatch(tags, strings.Split(filter, ",")) return containsMatch(tags, strings.Split(filter, ","))
} }
func containsAll(tags []string, filter string) bool {
return containsAllMatch(tags, strings.Split(filter, ","))
}
func containsAnyOther(filter string, tags ...string) bool { func containsAnyOther(filter string, tags ...string) bool {
return containsMatch(tags, strings.Split(filter, ",")) return containsMatch(tags, strings.Split(filter, ","))
} }
@ -686,6 +702,39 @@ func containsMatch(tags []string, filters []string) bool {
return false return false
} }
func containsAllMatch(tags []string, filters []string) bool {
for _, filter := range filters {
if filter == "" {
continue
}
filter = strings.ToLower(filter)
filter = strings.Trim(filter, " ")
found := false
for _, tag := range tags {
if tag == "" {
continue
}
tag = strings.ToLower(tag)
if tag == filter {
found = true
break
} else if strings.ContainsAny(filter, "?|*") {
if wildcard.Match(filter, tag) {
found = true
break
}
}
}
if !found {
return false
}
}
return true
}
func containsMatchBasic(tags []string, filters []string) bool { func containsMatchBasic(tags []string, filters []string) bool {
for _, tag := range tags { for _, tag := range tags {
if tag == "" { if tag == "" {

View file

@ -520,7 +520,7 @@ func TestFilter_CheckFilter(t *testing.T) {
want: true, want: true,
}, },
{ {
name: "match_tags", name: "match_tags_any",
fields: &Release{ fields: &Release{
TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2", TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2",
Category: "TV", Category: "TV",
@ -535,12 +535,34 @@ func TestFilter_CheckFilter(t *testing.T) {
ExceptUploaders: "Anonymous", ExceptUploaders: "Anonymous",
Shows: "Good show", Shows: "Good show",
Tags: "tv", Tags: "tv",
TagsMatchLogic: "ANY",
}, },
}, },
want: true, want: true,
}, },
{ {
name: "match_tags_bad", name: "match_tags_all",
fields: &Release{
TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2",
Category: "TV",
Uploader: "Uploader1",
Tags: []string{"tv", "foreign"},
},
args: args{
filter: Filter{
Enabled: true,
MatchCategories: "*tv*",
MatchUploaders: "Uploader1,Uploader2",
ExceptUploaders: "Anonymous",
Shows: "Good show",
Tags: "tv,foreign",
TagsMatchLogic: "ALL",
},
},
want: true,
},
{
name: "match_tags_any_bad",
fields: &Release{ fields: &Release{
TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2", TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2",
Category: "TV", Category: "TV",
@ -555,13 +577,36 @@ func TestFilter_CheckFilter(t *testing.T) {
ExceptUploaders: "Anonymous", ExceptUploaders: "Anonymous",
Shows: "Good show", Shows: "Good show",
Tags: "tv", Tags: "tv",
TagsMatchLogic: "ANY",
}, },
rejections: []string{"tags not matching. got: [foreign] want: tv"}, rejections: []string{"tags not matching. got: [foreign] want: tv"},
}, },
want: false, want: false,
}, },
{ {
name: "match_except_tags", name: "match_tags_all_bad",
fields: &Release{
TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2",
Category: "TV",
Uploader: "Uploader1",
Tags: []string{"foreign"},
},
args: args{
filter: Filter{
Enabled: true,
MatchCategories: "*tv*",
MatchUploaders: "Uploader1,Uploader2",
ExceptUploaders: "Anonymous",
Shows: "Good show",
Tags: "tv,foreign",
TagsMatchLogic: "ALL",
},
rejections: []string{"tags not matching. got: [foreign] want(all): tv,foreign"},
},
want: false,
},
{
name: "match_except_tags_any",
fields: &Release{ fields: &Release{
TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2", TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2",
Category: "TV", Category: "TV",
@ -576,12 +621,13 @@ func TestFilter_CheckFilter(t *testing.T) {
ExceptUploaders: "Anonymous", ExceptUploaders: "Anonymous",
Shows: "Good show", Shows: "Good show",
ExceptTags: "tv", ExceptTags: "tv",
TagsMatchLogic: "ANY",
}, },
}, },
want: true, want: true,
}, },
{ {
name: "match_except_tags_2", name: "match_except_tags_all",
fields: &Release{ fields: &Release{
TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2", TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2",
Category: "TV", Category: "TV",
@ -595,12 +641,56 @@ func TestFilter_CheckFilter(t *testing.T) {
MatchUploaders: "Uploader1,Uploader2", MatchUploaders: "Uploader1,Uploader2",
ExceptUploaders: "Anonymous", ExceptUploaders: "Anonymous",
Shows: "Good show", Shows: "Good show",
ExceptTags: "foreign", ExceptTags: "tv,foreign",
TagsMatchLogic: "ALL",
},
},
want: true,
},
{
name: "match_except_tags_any_2",
fields: &Release{
TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2",
Category: "TV",
Uploader: "Uploader1",
Tags: []string{"foreign"},
},
args: args{
filter: Filter{
Enabled: true,
MatchCategories: "*tv*",
MatchUploaders: "Uploader1,Uploader2",
ExceptUploaders: "Anonymous",
Shows: "Good show",
ExceptTags: "foreign",
ExceptTagsMatchLogic: "ANY",
}, },
rejections: []string{"tags unwanted. got: [foreign] want: foreign"}, rejections: []string{"tags unwanted. got: [foreign] want: foreign"},
}, },
want: false, want: false,
}, },
{
name: "match_except_tags_all_2",
fields: &Release{
TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2",
Category: "TV",
Uploader: "Uploader1",
Tags: []string{"tv", "foreign"},
},
args: args{
filter: Filter{
Enabled: true,
MatchCategories: "*tv*",
MatchUploaders: "Uploader1,Uploader2",
ExceptUploaders: "Anonymous",
Shows: "Good show",
ExceptTags: "foreign,tv",
ExceptTagsMatchLogic: "ALL",
},
rejections: []string{"tags unwanted. got: [tv foreign] want(all): foreign,tv"},
},
want: false,
},
{ {
name: "match_group_1", name: "match_group_1",
fields: &Release{ fields: &Release{
@ -1717,64 +1807,64 @@ func TestFilter_CheckFilter1(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
f := Filter{ f := Filter{
ID: tt.fields.ID, ID: tt.fields.ID,
Name: tt.fields.Name, Name: tt.fields.Name,
Enabled: tt.fields.Enabled, Enabled: tt.fields.Enabled,
CreatedAt: tt.fields.CreatedAt, CreatedAt: tt.fields.CreatedAt,
UpdatedAt: tt.fields.UpdatedAt, UpdatedAt: tt.fields.UpdatedAt,
MinSize: tt.fields.MinSize, MinSize: tt.fields.MinSize,
MaxSize: tt.fields.MaxSize, MaxSize: tt.fields.MaxSize,
Delay: tt.fields.Delay, Delay: tt.fields.Delay,
Priority: tt.fields.Priority, Priority: tt.fields.Priority,
MaxDownloads: tt.fields.MaxDownloads, MaxDownloads: tt.fields.MaxDownloads,
MaxDownloadsUnit: tt.fields.MaxDownloadsUnit, MaxDownloadsUnit: tt.fields.MaxDownloadsUnit,
MatchReleases: tt.fields.MatchReleases, MatchReleases: tt.fields.MatchReleases,
ExceptReleases: tt.fields.ExceptReleases, ExceptReleases: tt.fields.ExceptReleases,
UseRegex: tt.fields.UseRegex, UseRegex: tt.fields.UseRegex,
MatchReleaseGroups: tt.fields.MatchReleaseGroups, MatchReleaseGroups: tt.fields.MatchReleaseGroups,
ExceptReleaseGroups: tt.fields.ExceptReleaseGroups, ExceptReleaseGroups: tt.fields.ExceptReleaseGroups,
MatchReleaseTags: tt.fields.MatchReleaseTags, MatchReleaseTags: tt.fields.MatchReleaseTags,
ExceptReleaseTags: tt.fields.ExceptReleaseTags, ExceptReleaseTags: tt.fields.ExceptReleaseTags,
UseRegexReleaseTags: tt.fields.UseRegexReleaseTags, UseRegexReleaseTags: tt.fields.UseRegexReleaseTags,
Scene: tt.fields.Scene, Scene: tt.fields.Scene,
Origins: tt.fields.Origins, Origins: tt.fields.Origins,
ExceptOrigins: tt.fields.ExceptOrigins, ExceptOrigins: tt.fields.ExceptOrigins,
Freeleech: tt.fields.Freeleech, Freeleech: tt.fields.Freeleech,
FreeleechPercent: tt.fields.FreeleechPercent, FreeleechPercent: tt.fields.FreeleechPercent,
Shows: tt.fields.Shows, Shows: tt.fields.Shows,
Seasons: tt.fields.Seasons, Seasons: tt.fields.Seasons,
Episodes: tt.fields.Episodes, Episodes: tt.fields.Episodes,
Resolutions: tt.fields.Resolutions, Resolutions: tt.fields.Resolutions,
Codecs: tt.fields.Codecs, Codecs: tt.fields.Codecs,
Sources: tt.fields.Sources, Sources: tt.fields.Sources,
Containers: tt.fields.Containers, Containers: tt.fields.Containers,
MatchHDR: tt.fields.MatchHDR, MatchHDR: tt.fields.MatchHDR,
ExceptHDR: tt.fields.ExceptHDR, ExceptHDR: tt.fields.ExceptHDR,
Years: tt.fields.Years, Years: tt.fields.Years,
Artists: tt.fields.Artists, Artists: tt.fields.Artists,
Albums: tt.fields.Albums, Albums: tt.fields.Albums,
MatchReleaseTypes: tt.fields.MatchReleaseTypes, MatchReleaseTypes: tt.fields.MatchReleaseTypes,
ExceptReleaseTypes: tt.fields.ExceptReleaseTypes, ExceptReleaseTypes: tt.fields.ExceptReleaseTypes,
Formats: tt.fields.Formats, Formats: tt.fields.Formats,
Quality: tt.fields.Quality, Quality: tt.fields.Quality,
Media: tt.fields.Media, Media: tt.fields.Media,
PerfectFlac: tt.fields.PerfectFlac, PerfectFlac: tt.fields.PerfectFlac,
Cue: tt.fields.Cue, Cue: tt.fields.Cue,
Log: tt.fields.Log, Log: tt.fields.Log,
LogScore: tt.fields.LogScore, LogScore: tt.fields.LogScore,
MatchOther: tt.fields.MatchOther, MatchOther: tt.fields.MatchOther,
ExceptOther: tt.fields.ExceptOther, ExceptOther: tt.fields.ExceptOther,
MatchCategories: tt.fields.MatchCategories, MatchCategories: tt.fields.MatchCategories,
ExceptCategories: tt.fields.ExceptCategories, ExceptCategories: tt.fields.ExceptCategories,
MatchUploaders: tt.fields.MatchUploaders, MatchUploaders: tt.fields.MatchUploaders,
ExceptUploaders: tt.fields.ExceptUploaders, ExceptUploaders: tt.fields.ExceptUploaders,
Tags: tt.fields.Tags, Tags: tt.fields.Tags,
ExceptTags: tt.fields.ExceptTags, ExceptTags: tt.fields.ExceptTags,
TagsAny: tt.fields.TagsAny, TagsMatchLogic: tt.fields.TagsMatchLogic,
ExceptTagsAny: tt.fields.ExceptTagsAny, ExceptTagsMatchLogic: tt.fields.ExceptTagsMatchLogic,
Actions: tt.fields.Actions, Actions: tt.fields.Actions,
Indexers: tt.fields.Indexers, Indexers: tt.fields.Indexers,
Downloads: tt.fields.Downloads, Downloads: tt.fields.Downloads,
} }
tt.args.r.ParseString(tt.args.r.TorrentName) tt.args.r.ParseString(tt.args.r.TorrentName)
rejections, match := f.CheckFilter(tt.args.r) rejections, match := f.CheckFilter(tt.args.r)
@ -1784,6 +1874,54 @@ func TestFilter_CheckFilter1(t *testing.T) {
} }
} }
func Test_containsMatch(t *testing.T) {
type args struct {
tags []string
filters []string
}
tests := []struct {
name string
args args
want bool
}{
{name: "test_1", args: args{tags: []string{"HDR", "DV"}, filters: []string{"DV"}}, want: true},
{name: "test_2", args: args{tags: []string{"HDR", "DV"}, filters: []string{"HD*", "D*"}}, want: true},
{name: "test_3", args: args{tags: []string{"HDR"}, filters: []string{"DV"}}, want: false},
{name: "test_4", args: args{tags: []string{"HDR"}, filters: []string{"TEST*"}}, want: false},
{name: "test_5", args: args{tags: []string{""}, filters: []string{"test,"}}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, containsMatch(tt.args.tags, tt.args.filters), "containsMatch(%v, %v)", tt.args.tags, tt.args.filters)
})
}
}
func Test_containsAllMatch(t *testing.T) {
type args struct {
tags []string
filters []string
}
tests := []struct {
name string
args args
want bool
}{
{name: "test_1", args: args{tags: []string{"HDR", "DV"}, filters: []string{"DV"}}, want: true},
{name: "test_2", args: args{tags: []string{"HDR", "DV"}, filters: []string{"DV", "DoVI"}}, want: false},
{name: "test_3", args: args{tags: []string{"HDR", "DV", "DoVI"}, filters: []string{"DV", "DoVI"}}, want: true},
{name: "test_4", args: args{tags: []string{"HDR", "DV"}, filters: []string{"HD*", "D*"}}, want: true},
{name: "test_5", args: args{tags: []string{"HDR", "DV"}, filters: []string{"HD*", "TEST*"}}, want: false},
{name: "test_6", args: args{tags: []string{"HDR"}, filters: []string{"DV"}}, want: false},
{name: "test_7", args: args{tags: []string{""}, filters: []string{"test,"}}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, containsAllMatch(tt.args.tags, tt.args.filters), "containsAllMatch(%v, %v)", tt.args.tags, tt.args.filters)
})
}
}
func Test_contains(t *testing.T) { func Test_contains(t *testing.T) {
type args struct { type args struct {
tag string tag string
@ -1854,6 +1992,31 @@ func Test_containsAny(t *testing.T) {
} }
} }
func Test_containsAll(t *testing.T) {
type args struct {
tags []string
filter string
}
tests := []struct {
name string
args args
want bool
}{
{name: "test_1", args: args{tags: []string{"HDR", "DV"}, filter: "DV"}, want: true},
{name: "test_2", args: args{tags: []string{"HDR", "DV"}, filter: "HDR,DV"}, want: true},
{name: "test_2", args: args{tags: []string{"HDR", "DV"}, filter: "HD*,D*"}, want: true},
{name: "test_3", args: args{tags: []string{"HDR", "DoVI"}, filter: "HDR,DV"}, want: false},
{name: "test_4", args: args{tags: []string{"HDR", "DV", "HDR10+"}, filter: "HDR,DV"}, want: true},
{name: "test_5", args: args{tags: []string{"HDR"}, filter: "DV"}, want: false},
{name: "test_6", args: args{tags: []string{""}, filter: "test,"}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, containsAll(tt.args.tags, tt.args.filter), "containsAll(%v, %v)", tt.args.tags, tt.args.filter)
})
}
}
func Test_sliceContainsSlice(t *testing.T) { func Test_sliceContainsSlice(t *testing.T) {
type args struct { type args struct {
tags []string tags []string

View file

@ -251,6 +251,7 @@ export interface SelectFieldProps {
label: string; label: string;
optionDefaultText: string; optionDefaultText: string;
options: SelectFieldOption[]; options: SelectFieldOption[];
columns?: COL_WIDTHS;
tooltip?: JSX.Element; tooltip?: JSX.Element;
} }
@ -259,10 +260,15 @@ export const Select = ({
label, label,
tooltip, tooltip,
optionDefaultText, optionDefaultText,
options options,
columns
}: SelectFieldProps) => { }: SelectFieldProps) => {
return ( return (
<div className="col-span-6"> <div
className={classNames(
columns ? `col-span-${columns}` : "col-span-6"
)}
>
<Field name={name} type="select"> <Field name={name} type="select">
{({ {({
field, field,

View file

@ -500,3 +500,14 @@ export const FeedDownloadTypeOptions: OptionBasicTyped<FeedDownloadType>[] = [
value: "TORRENT" value: "TORRENT"
} }
]; ];
export const tagsMatchLogicOptions: OptionBasic[] = [
{
label: "any",
value: "ANY"
},
{
label: "all",
value: "ALL"
}
];

View file

@ -18,7 +18,8 @@ import {
RELEASE_TYPE_MUSIC_OPTIONS, RELEASE_TYPE_MUSIC_OPTIONS,
RESOLUTION_OPTIONS, RESOLUTION_OPTIONS,
SOURCES_MUSIC_OPTIONS, SOURCES_MUSIC_OPTIONS,
SOURCES_OPTIONS SOURCES_OPTIONS,
tagsMatchLogicOptions
} from "../../domain/constants"; } from "../../domain/constants";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
@ -265,6 +266,8 @@ export default function FilterDetails() {
except_categories: filter.except_categories, except_categories: filter.except_categories,
tags: filter.tags, tags: filter.tags,
except_tags: filter.except_tags, except_tags: filter.except_tags,
tags_match_logic: filter.tags_match_logic,
except_tags_match_logic: filter.except_tags_match_logic,
match_uploaders: filter.match_uploaders, match_uploaders: filter.match_uploaders,
except_uploaders: filter.except_uploaders, except_uploaders: filter.except_uploaders,
match_language: filter.match_language || [], match_language: filter.match_language || [],
@ -509,8 +512,10 @@ export function Advanced({ values }: AdvancedProps) {
<TextField name="match_categories" label="Match categories" columns={6} placeholder="eg. *category*,category1" tooltip={<div><p>Comma separated list of categories to match.</p><a href='https://autobrr.com/filters/categories' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/filters/categories</a></div>} /> <TextField name="match_categories" label="Match categories" columns={6} placeholder="eg. *category*,category1" tooltip={<div><p>Comma separated list of categories to match.</p><a href='https://autobrr.com/filters/categories' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/filters/categories</a></div>} />
<TextField name="except_categories" label="Except categories" columns={6} placeholder="eg. *category*" tooltip={<div><p>Comma separated list of categories to ignore (takes priority over Match releases).</p><a href='https://autobrr.com/filters/categories' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/filters/categories</a></div>} /> <TextField name="except_categories" label="Except categories" columns={6} placeholder="eg. *category*" tooltip={<div><p>Comma separated list of categories to ignore (takes priority over Match releases).</p><a href='https://autobrr.com/filters/categories' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/filters/categories</a></div>} />
<TextField name="tags" label="Match tags" columns={6} placeholder="eg. tag1,tag2" tooltip={<div><p>Comma separated list of tags to match.</p><a href='https://autobrr.com/filters#advanced' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/filters#advanced</a></div>} /> <TextField name="tags" label="Match tags" columns={4} placeholder="eg. tag1,tag2" tooltip={<div><p>Comma separated list of tags to match.</p><a href='https://autobrr.com/filters#advanced' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/filters#advanced</a></div>} />
<TextField name="except_tags" label="Except tags" columns={6} placeholder="eg. tag1,tag2" tooltip={<div><p>Comma separated list of tags to ignore (takes priority over Match releases).</p><a href='https://autobrr.com/filters#advanced' className='text-blue-400 visited:text-blue-400' target='_blank'>hhttps://autobrr.com/filters#advanced</a></div>} /> <Select name="tags_match_logic" label="Tags logic" columns={2} options={tagsMatchLogicOptions} optionDefaultText="any" tooltip={<div><p>Logic used to match filter tags.</p><a href='https://autobrr.com/filters#advanced' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/filters#advanced</a></div>} />
<TextField name="except_tags" label="Except tags" columns={4} placeholder="eg. tag1,tag2" tooltip={<div><p>Comma separated list of tags to ignore (takes priority over Match releases).</p><a href='https://autobrr.com/filters#advanced' className='text-blue-400 visited:text-blue-400' target='_blank'>hhttps://autobrr.com/filters#advanced</a></div>} />
<Select name="except_tags_match_logic" label="Except tags logic" columns={2} options={tagsMatchLogicOptions} optionDefaultText="any" tooltip={<div><p>Logic used to match except tags.</p><a href='https://autobrr.com/filters#advanced' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/filters#advanced</a></div>} />
</CollapsableSection> </CollapsableSection>
<CollapsableSection defaultOpen={true} title="Uploaders" subtitle="Match or ignore uploaders."> <CollapsableSection defaultOpen={true} title="Uploaders" subtitle="Match or ignore uploaders.">

View file

@ -57,6 +57,8 @@ interface Filter {
except_tags: string; except_tags: string;
tags_any: string; tags_any: string;
except_tags_any: string; except_tags_any: string;
tags_match_logic: string;
except_tags_match_logic: string;
actions_count: number; actions_count: number;
actions: Action[]; actions: Action[];
indexers: Indexer[]; indexers: Indexer[];