From a86258aaa723ed22b54169756009251c68ca2de3 Mon Sep 17 00:00:00 2001 From: luckyboy <0708yc@gmail.com> Date: Sat, 13 Jan 2024 00:08:18 +0800 Subject: [PATCH] feat(filters): implement min and max seeders/leechers filtering for Torznab feeds (#1342) * feat(filter):implement min and max seeders/leechers filtering * chore: go fmt and reorder fields --------- Co-authored-by: ze0s --- internal/database/filter.go | 40 +++++++++++++++++ internal/database/postgres_migrate.go | 18 +++++++- internal/database/sqlite_migrate.go | 18 +++++++- internal/domain/filter.go | 33 ++++++++++++++ internal/domain/release.go | 2 + internal/feed/torznab.go | 26 +++++++++++ web/src/screens/filters/Details.tsx | 4 ++ web/src/screens/filters/_const.ts | 6 ++- web/src/screens/filters/sections/Advanced.tsx | 44 +++++++++++++++++++ web/src/types/Filter.d.ts | 4 ++ 10 files changed, 192 insertions(+), 3 deletions(-) diff --git a/internal/database/filter.go b/internal/database/filter.go index d5f0d43..2aa4c9b 100644 --- a/internal/database/filter.go +++ b/internal/database/filter.go @@ -239,6 +239,10 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter "f.except_tags_match_logic", "f.origins", "f.except_origins", + "f.min_seeders", + "f.max_seeders", + "f.min_leechers", + "f.max_leechers", "f.created_at", "f.updated_at", "fe.id as external_id", @@ -349,6 +353,10 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter &exceptTagsMatchLogic, pq.Array(&f.Origins), pq.Array(&f.ExceptOrigins), + &f.MinSeeders, + &f.MaxSeeders, + &f.MinLeechers, + &f.MaxLeechers, &f.CreatedAt, &f.UpdatedAt, &extId, @@ -503,6 +511,10 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, indexer string "f.except_tags_match_logic", "f.origins", "f.except_origins", + "f.min_seeders", + "f.max_seeders", + "f.min_leechers", + "f.max_leechers", "f.created_at", "f.updated_at", "fe.id as external_id", @@ -617,6 +629,10 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, indexer string &exceptTagsMatchLogic, pq.Array(&f.Origins), pq.Array(&f.ExceptOrigins), + &f.MinSeeders, + &f.MaxSeeders, + &f.MinLeechers, + &f.MaxLeechers, &f.CreatedAt, &f.UpdatedAt, &extId, @@ -870,6 +886,10 @@ func (r *FilterRepo) Store(ctx context.Context, filter *domain.Filter) error { "perfect_flac", "origins", "except_origins", + "min_seeders", + "max_seeders", + "min_leechers", + "max_leechers", ). Values( filter.Name, @@ -929,6 +949,10 @@ func (r *FilterRepo) Store(ctx context.Context, filter *domain.Filter) error { filter.PerfectFlac, pq.Array(filter.Origins), pq.Array(filter.ExceptOrigins), + filter.MinSeeders, + filter.MaxSeeders, + filter.MinLeechers, + filter.MaxLeechers, ). Suffix("RETURNING id").RunWith(r.db.handler) @@ -1006,6 +1030,10 @@ func (r *FilterRepo) Update(ctx context.Context, filter *domain.Filter) error { Set("perfect_flac", filter.PerfectFlac). Set("origins", pq.Array(filter.Origins)). Set("except_origins", pq.Array(filter.ExceptOrigins)). + Set("min_seeders", filter.MinSeeders). + Set("max_seeders", filter.MaxSeeders). + Set("min_leechers", filter.MinLeechers). + Set("max_leechers", filter.MaxLeechers). Set("updated_at", time.Now().Format(time.RFC3339)). Where(sq.Eq{"id": filter.ID}) @@ -1237,6 +1265,18 @@ func (r *FilterRepo) UpdatePartial(ctx context.Context, filter domain.FilterUpda if filter.ExternalWebhookRetryDelaySeconds != nil { q = q.Set("external_webhook_retry_delay_seconds", filter.ExternalWebhookRetryDelaySeconds) } + if filter.MinSeeders != nil { + q = q.Set("min_seeders", filter.MinSeeders) + } + if filter.MaxSeeders != nil { + q = q.Set("max_seeders", filter.MaxSeeders) + } + if filter.MinLeechers != nil { + q = q.Set("min_leechers", filter.MinLeechers) + } + if filter.MaxLeechers != nil { + q = q.Set("max_leechers", filter.MaxLeechers) + } q = q.Where(sq.Eq{"id": filter.ID}) diff --git a/internal/database/postgres_migrate.go b/internal/database/postgres_migrate.go index 282f868..de29a0b 100644 --- a/internal/database/postgres_migrate.go +++ b/internal/database/postgres_migrate.go @@ -129,7 +129,11 @@ CREATE TABLE filter origins TEXT [] DEFAULT '{}', except_origins TEXT [] DEFAULT '{}', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + min_seeders INTEGER DEFAULT 0, + max_seeders INTEGER DEFAULT 0, + min_leechers INTEGER DEFAULT 0, + max_leechers INTEGER DEFAULT 0 ); CREATE TABLE filter_external @@ -842,5 +846,17 @@ ALTER TABLE filter_external `, `ALTER TABLE action ADD COLUMN external_client TEXT; +`,` +ALTER TABLE filter + ADD COLUMN min_seeders INTEGER DEFAULT 0; + +ALTER TABLE filter + ADD COLUMN max_seeders INTEGER DEFAULT 0; + +ALTER TABLE filter + ADD COLUMN min_leechers INTEGER DEFAULT 0; + +ALTER TABLE filter + ADD COLUMN max_leechers INTEGER DEFAULT 0; `, } diff --git a/internal/database/sqlite_migrate.go b/internal/database/sqlite_migrate.go index ebd18d7..f31e50f 100644 --- a/internal/database/sqlite_migrate.go +++ b/internal/database/sqlite_migrate.go @@ -129,7 +129,11 @@ CREATE TABLE filter origins TEXT [] DEFAULT '{}', except_origins TEXT [] DEFAULT '{}', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + min_seeders INTEGER DEFAULT 0, + max_seeders INTEGER DEFAULT 0, + min_leechers INTEGER DEFAULT 0, + max_leechers INTEGER DEFAULT 0 ); CREATE TABLE filter_external @@ -1483,5 +1487,17 @@ ALTER TABLE feed_dg_tmp `, `ALTER TABLE action ADD COLUMN external_client TEXT; +`,` +ALTER TABLE filter + ADD COLUMN min_seeders INTEGER DEFAULT 0; + +ALTER TABLE filter + ADD COLUMN max_seeders INTEGER DEFAULT 0; + +ALTER TABLE filter + ADD COLUMN min_leechers INTEGER DEFAULT 0; + +ALTER TABLE filter + ADD COLUMN max_leechers INTEGER DEFAULT 0; `, } diff --git a/internal/domain/filter.go b/internal/domain/filter.go index bfa2e03..069a636 100644 --- a/internal/domain/filter.go +++ b/internal/domain/filter.go @@ -133,6 +133,10 @@ type Filter struct { MatchDescription string `json:"match_description,omitempty"` ExceptDescription string `json:"except_description,omitempty"` UseRegexDescription bool `json:"use_regex_description,omitempty"` + MinSeeders int `json:"min_seeders,omitempty"` + MaxSeeders int `json:"max_seeders,omitempty"` + MinLeechers int `json:"min_leechers,omitempty"` + MaxLeechers int `json:"max_leechers,omitempty"` ActionsCount int `json:"actions_count"` ActionsEnabledCount int `json:"actions_enabled_count"` Actions []*Action `json:"actions,omitempty"` @@ -232,6 +236,10 @@ type FilterUpdate struct { ExceptTagsAny *string `json:"except_tags_any,omitempty"` TagsMatchLogic *string `json:"tags_match_logic,omitempty"` ExceptTagsMatchLogic *string `json:"except_tags_match_logic,omitempty"` + MinSeeders *int `json:"min_seeders,omitempty"` + MaxSeeders *int `json:"max_seeders,omitempty"` + MinLeechers *int `json:"min_leechers,omitempty"` + MaxLeechers *int `json:"max_leechers,omitempty"` ExternalScriptEnabled *bool `json:"external_script_enabled,omitempty"` ExternalScriptCmd *string `json:"external_script_cmd,omitempty"` ExternalScriptArgs *string `json:"external_script_args,omitempty"` @@ -507,6 +515,31 @@ func (f *Filter) CheckFilter(r *Release) ([]string, bool) { } } + // Min and Max Seeders/Leechers is only for Torznab feeds + if f.MinSeeders > 0 { + if f.MinSeeders > r.Seeders { + f.addRejectionF("min seeders not matcing. got: %d want %d", r.Seeders, f.MinSeeders) + } + } + + if f.MaxSeeders > 0 { + if f.MaxSeeders < r.Seeders { + f.addRejectionF("max seeders not matcing. got: %d want %d", r.Seeders, f.MaxSeeders) + } + } + + if f.MinLeechers > 0 { + if f.MinLeechers > r.Leechers { + f.addRejectionF("min leechers not matcing. got: %d want %d", r.Leechers, f.MinLeechers) + } + } + + if f.MaxLeechers > 0 { + if f.MaxLeechers < r.Leechers { + f.addRejectionF("max leechers not matcing. got: %d want %d", r.Leechers, f.MaxLeechers) + } + } + if len(f.Rejections) > 0 { return f.Rejections, false } diff --git a/internal/domain/release.go b/internal/domain/release.go index 75e96f7..12e478c 100644 --- a/internal/domain/release.go +++ b/internal/domain/release.go @@ -94,6 +94,8 @@ type Release struct { PreTime string `json:"pre_time"` Other []string `json:"-"` RawCookie string `json:"-"` + Seeders int `json:"-"` + Leechers int `json:"-"` AdditionalSizeCheckRequired bool `json:"-"` FilterID int `json:"-"` Filter *Filter `json:"-"` diff --git a/internal/feed/torznab.go b/internal/feed/torznab.go index cf7d4ec..f76afa7 100644 --- a/internal/feed/torznab.go +++ b/internal/feed/torznab.go @@ -114,6 +114,18 @@ func (j *TorznabJob) process(ctx context.Context) error { rls.ParseString(item.Title) + rls.Seeders, err = parseIntAttribute(item, "seeders") + if err != nil { + rls.Seeders = 0 + } + + var peers, err = parseIntAttribute(item, "peers") + + rls.Leechers = peers - rls.Seeders + if err != nil { + rls.Leechers = 0 + } + if j.Feed.Settings != nil && j.Feed.Settings.DownloadType == domain.FeedDownloadTypeMagnet { rls.MagnetURI = item.Link rls.DownloadURL = "" @@ -152,6 +164,20 @@ func (j *TorznabJob) process(ctx context.Context) error { return nil } +func parseIntAttribute(item torznab.FeedItem, attrName string) (int, error) { + for _, attr := range item.Attributes { + if attr.Name == attrName { + // Parse the value as decimal number + intValue, err := strconv.Atoi(attr.Value) + if err != nil { + return 0, err + } + return intValue, err + } + } + return 0, nil +} + // Parse the downloadvolumefactor attribute. The returned value is the percentage // of downloaded data that does NOT count towards a user's total download amount. func parseFreeleechTorznab(item torznab.FeedItem) (int, error) { diff --git a/web/src/screens/filters/Details.tsx b/web/src/screens/filters/Details.tsx index 1c3be95..5e1e7a8 100644 --- a/web/src/screens/filters/Details.tsx +++ b/web/src/screens/filters/Details.tsx @@ -437,6 +437,10 @@ export const FilterDetails = () => { albums: filter.albums, origins: filter.origins || [], except_origins: filter.except_origins || [], + min_seeders: filter.min_seeders, + max_seeders: filter.max_seeders, + min_leechers: filter.min_leechers, + max_leechers: filter.max_leechers, indexers: filter.indexers || [], actions: filter.actions || [], external: filter.external || [] diff --git a/web/src/screens/filters/_const.ts b/web/src/screens/filters/_const.ts index d6822a7..cb2853f 100644 --- a/web/src/screens/filters/_const.ts +++ b/web/src/screens/filters/_const.ts @@ -48,7 +48,11 @@ export const FILTER_FIELDS: Record = { "except_tags_any": "boolean", "formats": "[]string", "quality": "[]string", - "media": "[]string" + "media": "[]string", + "min_seeders": "number", + "max_seeders": "number", + "min_leechers": "number", + "max_leechers": "number", } as const; export const IRC_FIELDS: Record = { diff --git a/web/src/screens/filters/sections/Advanced.tsx b/web/src/screens/filters/sections/Advanced.tsx index 1cad2df..b357a36 100644 --- a/web/src/screens/filters/sections/Advanced.tsx +++ b/web/src/screens/filters/sections/Advanced.tsx @@ -397,6 +397,50 @@ const FeedSpecific = ({ values }: ValueConsumer) => ( } /> + +

Number of min seeders as specified by the respective unit. Only for Torznab

+ + + } + /> + +

Number of max seeders as specified by the respective unit. Only for Torznab

+ + + } + /> + +

Number of min leechers as specified by the respective unit. Only for Torznab

+ + + } + /> + +

Number of max leechers as specified by the respective unit. Only for Torznab

+ + + } + /> ); diff --git a/web/src/types/Filter.d.ts b/web/src/types/Filter.d.ts index 6e0a786..41f83e1 100644 --- a/web/src/types/Filter.d.ts +++ b/web/src/types/Filter.d.ts @@ -67,6 +67,10 @@ interface Filter { except_tags_any: string; tags_match_logic: string; except_tags_match_logic: string; + min_seeders: number; + max_seeders: number; + min_leechers: number; + max_leechers: number; actions_count: number; actions_enabled_count: number; actions: Action[];