From ef75b67b25201e2b07c4fb98682b988f4bef7d94 Mon Sep 17 00:00:00 2001 From: Gustavo Machado Date: Mon, 10 Apr 2023 15:11:44 +0100 Subject: [PATCH] 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 --- internal/database/filter.go | 28 ++- internal/database/postgres_migrate.go | 16 ++ internal/database/sqlite_migrate.go | 16 ++ internal/domain/filter.go | 57 ++++- internal/domain/filter_test.go | 289 ++++++++++++++++++++------ web/src/components/inputs/select.tsx | 10 +- web/src/domain/constants.ts | 11 + web/src/screens/filters/details.tsx | 11 +- web/src/types/Filter.d.ts | 2 + 9 files changed, 364 insertions(+), 76 deletions(-) diff --git a/internal/database/filter.go b/internal/database/filter.go index 6e17173..0ed7099 100644 --- a/internal/database/filter.go +++ b/internal/database/filter.go @@ -221,6 +221,8 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter "except_language", "tags", "except_tags", + "tags_match_logic", + "except_tags_match_logic", "origins", "except_origins", "external_script_enabled", @@ -248,11 +250,11 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*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 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") } @@ -284,6 +286,8 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter f.ExceptUploaders = exceptUploaders.String f.Tags = tags.String f.ExceptTags = exceptTags.String + f.TagsMatchLogic = tagsMatchLogic.String + f.ExceptTagsMatchLogic = exceptTagsMatchLogic.String f.UseRegex = useRegex.Bool f.Scene = scene.Bool f.Freeleech = freeleech.Bool @@ -384,6 +388,8 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, tx *Tx, indexe "f.except_language", "f.tags", "f.except_tags", + "f.tags_match_logic", + "f.except_tags_match_logic", "f.origins", "f.except_origins", "f.external_script_enabled", @@ -421,11 +427,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, 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 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") } @@ -457,6 +463,8 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, tx *Tx, indexe f.ExceptUploaders = exceptUploaders.String f.Tags = tags.String f.ExceptTags = exceptTags.String + f.TagsMatchLogic = tagsMatchLogic.String + f.ExceptTagsMatchLogic = exceptTagsMatchLogic.String f.UseRegex = useRegex.Bool f.Scene = scene.Bool f.Freeleech = freeleech.Bool @@ -521,6 +529,8 @@ func (r *FilterRepo) Store(ctx context.Context, filter domain.Filter) (*domain.F "except_language", "tags", "except_tags", + "tags_match_logic", + "except_tags_match_logic", "artists", "albums", "release_types_match", @@ -583,6 +593,8 @@ func (r *FilterRepo) Store(ctx context.Context, filter domain.Filter) (*domain.F pq.Array(filter.ExceptLanguage), filter.Tags, filter.ExceptTags, + filter.TagsMatchLogic, + filter.ExceptTagsMatchLogic, filter.Artists, filter.Albums, 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("tags", filter.Tags). Set("except_tags", filter.ExceptTags). + Set("tags_match_logic", filter.TagsMatchLogic). + Set("except_tags_match_logic", filter.ExceptTagsMatchLogic). Set("artists", filter.Artists). Set("albums", filter.Albums). 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 { 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 { q = q.Set("artists", filter.Artists) } diff --git a/internal/database/postgres_migrate.go b/internal/database/postgres_migrate.go index 43e0773..cd8815c 100644 --- a/internal/database/postgres_migrate.go +++ b/internal/database/postgres_migrate.go @@ -115,6 +115,8 @@ CREATE TABLE filter except_language TEXT [] DEFAULT '{}', tags TEXT, except_tags TEXT, + tags_match_logic TEXT, + except_tags_match_logic TEXT, origins TEXT [] DEFAULT '{}', except_origins TEXT [] DEFAULT '{}', external_script_enabled BOOLEAN DEFAULT FALSE, @@ -655,4 +657,18 @@ ADD COLUMN info_url TEXT; ALTER TABLE "release" 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; + `, } diff --git a/internal/database/sqlite_migrate.go b/internal/database/sqlite_migrate.go index 9c12694..b937daf 100644 --- a/internal/database/sqlite_migrate.go +++ b/internal/database/sqlite_migrate.go @@ -115,6 +115,8 @@ CREATE TABLE filter except_language TEXT [] DEFAULT '{}', tags TEXT, except_tags TEXT, + tags_match_logic TEXT, + except_tags_match_logic TEXT, origins TEXT [] DEFAULT '{}', except_origins TEXT [] DEFAULT '{}', external_script_enabled BOOLEAN DEFAULT FALSE, @@ -1048,4 +1050,18 @@ ADD COLUMN info_url TEXT; ALTER TABLE "release" 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; + `, } diff --git a/internal/domain/filter.go b/internal/domain/filter.go index 67bf7b7..7f9d496 100644 --- a/internal/domain/filter.go +++ b/internal/domain/filter.go @@ -115,6 +115,8 @@ type Filter struct { ExceptTags string `json:"except_tags,omitempty"` TagsAny string `json:"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"` ExceptReleaseTags string `json:"except_release_tags,omitempty"` UseRegexReleaseTags bool `json:"use_regex_release_tags,omitempty"` @@ -190,6 +192,8 @@ type FilterUpdate struct { ExceptTags *string `json:"except_tags,omitempty"` TagsAny *string `json:"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"` ExternalScriptCmd *string `json:"external_script_cmd,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) } - if f.Tags != "" && !containsAny(r.Tags, f.Tags) { - r.addRejectionF("tags not matching. got: %v want: %v", r.Tags, f.Tags) + if 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) { - r.addRejectionF("tags unwanted. got: %v want: %v", r.Tags, f.ExceptTags) + if 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) { @@ -620,6 +632,10 @@ func containsAny(tags []string, filter string) bool { 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 { return containsMatch(tags, strings.Split(filter, ",")) } @@ -686,6 +702,39 @@ func containsMatch(tags []string, filters []string) bool { 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 { for _, tag := range tags { if tag == "" { diff --git a/internal/domain/filter_test.go b/internal/domain/filter_test.go index 4b7706f..0316802 100644 --- a/internal/domain/filter_test.go +++ b/internal/domain/filter_test.go @@ -520,7 +520,7 @@ func TestFilter_CheckFilter(t *testing.T) { want: true, }, { - name: "match_tags", + name: "match_tags_any", fields: &Release{ TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2", Category: "TV", @@ -535,12 +535,34 @@ func TestFilter_CheckFilter(t *testing.T) { ExceptUploaders: "Anonymous", Shows: "Good show", Tags: "tv", + TagsMatchLogic: "ANY", }, }, 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{ TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2", Category: "TV", @@ -555,13 +577,36 @@ func TestFilter_CheckFilter(t *testing.T) { ExceptUploaders: "Anonymous", Shows: "Good show", Tags: "tv", + TagsMatchLogic: "ANY", }, rejections: []string{"tags not matching. got: [foreign] want: tv"}, }, 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{ TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2", Category: "TV", @@ -576,12 +621,13 @@ func TestFilter_CheckFilter(t *testing.T) { ExceptUploaders: "Anonymous", Shows: "Good show", ExceptTags: "tv", + TagsMatchLogic: "ANY", }, }, want: true, }, { - name: "match_except_tags_2", + name: "match_except_tags_all", fields: &Release{ TorrentName: "Good show S02 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-GROUP2", Category: "TV", @@ -595,12 +641,56 @@ func TestFilter_CheckFilter(t *testing.T) { MatchUploaders: "Uploader1,Uploader2", ExceptUploaders: "Anonymous", 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"}, }, 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", fields: &Release{ @@ -1717,64 +1807,64 @@ func TestFilter_CheckFilter1(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { f := Filter{ - ID: tt.fields.ID, - Name: tt.fields.Name, - Enabled: tt.fields.Enabled, - CreatedAt: tt.fields.CreatedAt, - UpdatedAt: tt.fields.UpdatedAt, - MinSize: tt.fields.MinSize, - MaxSize: tt.fields.MaxSize, - Delay: tt.fields.Delay, - Priority: tt.fields.Priority, - MaxDownloads: tt.fields.MaxDownloads, - MaxDownloadsUnit: tt.fields.MaxDownloadsUnit, - MatchReleases: tt.fields.MatchReleases, - ExceptReleases: tt.fields.ExceptReleases, - UseRegex: tt.fields.UseRegex, - MatchReleaseGroups: tt.fields.MatchReleaseGroups, - ExceptReleaseGroups: tt.fields.ExceptReleaseGroups, - MatchReleaseTags: tt.fields.MatchReleaseTags, - ExceptReleaseTags: tt.fields.ExceptReleaseTags, - UseRegexReleaseTags: tt.fields.UseRegexReleaseTags, - Scene: tt.fields.Scene, - Origins: tt.fields.Origins, - ExceptOrigins: tt.fields.ExceptOrigins, - Freeleech: tt.fields.Freeleech, - FreeleechPercent: tt.fields.FreeleechPercent, - Shows: tt.fields.Shows, - Seasons: tt.fields.Seasons, - Episodes: tt.fields.Episodes, - Resolutions: tt.fields.Resolutions, - Codecs: tt.fields.Codecs, - Sources: tt.fields.Sources, - Containers: tt.fields.Containers, - MatchHDR: tt.fields.MatchHDR, - ExceptHDR: tt.fields.ExceptHDR, - Years: tt.fields.Years, - Artists: tt.fields.Artists, - Albums: tt.fields.Albums, - MatchReleaseTypes: tt.fields.MatchReleaseTypes, - ExceptReleaseTypes: tt.fields.ExceptReleaseTypes, - Formats: tt.fields.Formats, - Quality: tt.fields.Quality, - Media: tt.fields.Media, - PerfectFlac: tt.fields.PerfectFlac, - Cue: tt.fields.Cue, - Log: tt.fields.Log, - LogScore: tt.fields.LogScore, - MatchOther: tt.fields.MatchOther, - ExceptOther: tt.fields.ExceptOther, - MatchCategories: tt.fields.MatchCategories, - ExceptCategories: tt.fields.ExceptCategories, - MatchUploaders: tt.fields.MatchUploaders, - ExceptUploaders: tt.fields.ExceptUploaders, - Tags: tt.fields.Tags, - ExceptTags: tt.fields.ExceptTags, - TagsAny: tt.fields.TagsAny, - ExceptTagsAny: tt.fields.ExceptTagsAny, - Actions: tt.fields.Actions, - Indexers: tt.fields.Indexers, - Downloads: tt.fields.Downloads, + ID: tt.fields.ID, + Name: tt.fields.Name, + Enabled: tt.fields.Enabled, + CreatedAt: tt.fields.CreatedAt, + UpdatedAt: tt.fields.UpdatedAt, + MinSize: tt.fields.MinSize, + MaxSize: tt.fields.MaxSize, + Delay: tt.fields.Delay, + Priority: tt.fields.Priority, + MaxDownloads: tt.fields.MaxDownloads, + MaxDownloadsUnit: tt.fields.MaxDownloadsUnit, + MatchReleases: tt.fields.MatchReleases, + ExceptReleases: tt.fields.ExceptReleases, + UseRegex: tt.fields.UseRegex, + MatchReleaseGroups: tt.fields.MatchReleaseGroups, + ExceptReleaseGroups: tt.fields.ExceptReleaseGroups, + MatchReleaseTags: tt.fields.MatchReleaseTags, + ExceptReleaseTags: tt.fields.ExceptReleaseTags, + UseRegexReleaseTags: tt.fields.UseRegexReleaseTags, + Scene: tt.fields.Scene, + Origins: tt.fields.Origins, + ExceptOrigins: tt.fields.ExceptOrigins, + Freeleech: tt.fields.Freeleech, + FreeleechPercent: tt.fields.FreeleechPercent, + Shows: tt.fields.Shows, + Seasons: tt.fields.Seasons, + Episodes: tt.fields.Episodes, + Resolutions: tt.fields.Resolutions, + Codecs: tt.fields.Codecs, + Sources: tt.fields.Sources, + Containers: tt.fields.Containers, + MatchHDR: tt.fields.MatchHDR, + ExceptHDR: tt.fields.ExceptHDR, + Years: tt.fields.Years, + Artists: tt.fields.Artists, + Albums: tt.fields.Albums, + MatchReleaseTypes: tt.fields.MatchReleaseTypes, + ExceptReleaseTypes: tt.fields.ExceptReleaseTypes, + Formats: tt.fields.Formats, + Quality: tt.fields.Quality, + Media: tt.fields.Media, + PerfectFlac: tt.fields.PerfectFlac, + Cue: tt.fields.Cue, + Log: tt.fields.Log, + LogScore: tt.fields.LogScore, + MatchOther: tt.fields.MatchOther, + ExceptOther: tt.fields.ExceptOther, + MatchCategories: tt.fields.MatchCategories, + ExceptCategories: tt.fields.ExceptCategories, + MatchUploaders: tt.fields.MatchUploaders, + ExceptUploaders: tt.fields.ExceptUploaders, + Tags: tt.fields.Tags, + ExceptTags: tt.fields.ExceptTags, + TagsMatchLogic: tt.fields.TagsMatchLogic, + ExceptTagsMatchLogic: tt.fields.ExceptTagsMatchLogic, + Actions: tt.fields.Actions, + Indexers: tt.fields.Indexers, + Downloads: tt.fields.Downloads, } tt.args.r.ParseString(tt.args.r.TorrentName) 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) { type args struct { 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) { type args struct { tags []string diff --git a/web/src/components/inputs/select.tsx b/web/src/components/inputs/select.tsx index 33f24b3..6fe8dfa 100644 --- a/web/src/components/inputs/select.tsx +++ b/web/src/components/inputs/select.tsx @@ -251,6 +251,7 @@ export interface SelectFieldProps { label: string; optionDefaultText: string; options: SelectFieldOption[]; + columns?: COL_WIDTHS; tooltip?: JSX.Element; } @@ -259,10 +260,15 @@ export const Select = ({ label, tooltip, optionDefaultText, - options + options, + columns }: SelectFieldProps) => { return ( -
+
{({ field, diff --git a/web/src/domain/constants.ts b/web/src/domain/constants.ts index 7e95397..5269416 100644 --- a/web/src/domain/constants.ts +++ b/web/src/domain/constants.ts @@ -500,3 +500,14 @@ export const FeedDownloadTypeOptions: OptionBasicTyped[] = [ value: "TORRENT" } ]; + +export const tagsMatchLogicOptions: OptionBasic[] = [ + { + label: "any", + value: "ANY" + }, + { + label: "all", + value: "ALL" + } +]; \ No newline at end of file diff --git a/web/src/screens/filters/details.tsx b/web/src/screens/filters/details.tsx index 384aafa..e06daef 100644 --- a/web/src/screens/filters/details.tsx +++ b/web/src/screens/filters/details.tsx @@ -18,7 +18,8 @@ import { RELEASE_TYPE_MUSIC_OPTIONS, RESOLUTION_OPTIONS, SOURCES_MUSIC_OPTIONS, - SOURCES_OPTIONS + SOURCES_OPTIONS, + tagsMatchLogicOptions } from "../../domain/constants"; import { queryClient } from "../../App"; import { APIClient } from "../../api/APIClient"; @@ -265,6 +266,8 @@ export default function FilterDetails() { except_categories: filter.except_categories, tags: filter.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, except_uploaders: filter.except_uploaders, match_language: filter.match_language || [], @@ -509,8 +512,10 @@ export function Advanced({ values }: AdvancedProps) {

Comma separated list of categories to match.

https://autobrr.com/filters/categories
} />

Comma separated list of categories to ignore (takes priority over Match releases).

https://autobrr.com/filters/categories
} /> -

Comma separated list of tags to match.

https://autobrr.com/filters#advanced} /> -

Comma separated list of tags to ignore (takes priority over Match releases).

hhttps://autobrr.com/filters#advanced} /> +

Comma separated list of tags to match.

https://autobrr.com/filters#advanced} /> +

Logic used to match except tags.

https://autobrr.com/filters#advanced} /> diff --git a/web/src/types/Filter.d.ts b/web/src/types/Filter.d.ts index 5559d3b..ca74aa3 100644 --- a/web/src/types/Filter.d.ts +++ b/web/src/types/Filter.d.ts @@ -57,6 +57,8 @@ interface Filter { except_tags: string; tags_any: string; except_tags_any: string; + tags_match_logic: string; + except_tags_match_logic: string; actions_count: number; actions: Action[]; indexers: Indexer[];