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 <ze0s@riseup.net>
This commit is contained in:
luckyboy 2024-01-13 00:08:18 +08:00 committed by GitHub
parent 256fbb49ba
commit a86258aaa7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 192 additions and 3 deletions

View file

@ -239,6 +239,10 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter
"f.except_tags_match_logic", "f.except_tags_match_logic",
"f.origins", "f.origins",
"f.except_origins", "f.except_origins",
"f.min_seeders",
"f.max_seeders",
"f.min_leechers",
"f.max_leechers",
"f.created_at", "f.created_at",
"f.updated_at", "f.updated_at",
"fe.id as external_id", "fe.id as external_id",
@ -349,6 +353,10 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter
&exceptTagsMatchLogic, &exceptTagsMatchLogic,
pq.Array(&f.Origins), pq.Array(&f.Origins),
pq.Array(&f.ExceptOrigins), pq.Array(&f.ExceptOrigins),
&f.MinSeeders,
&f.MaxSeeders,
&f.MinLeechers,
&f.MaxLeechers,
&f.CreatedAt, &f.CreatedAt,
&f.UpdatedAt, &f.UpdatedAt,
&extId, &extId,
@ -503,6 +511,10 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, indexer string
"f.except_tags_match_logic", "f.except_tags_match_logic",
"f.origins", "f.origins",
"f.except_origins", "f.except_origins",
"f.min_seeders",
"f.max_seeders",
"f.min_leechers",
"f.max_leechers",
"f.created_at", "f.created_at",
"f.updated_at", "f.updated_at",
"fe.id as external_id", "fe.id as external_id",
@ -617,6 +629,10 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, indexer string
&exceptTagsMatchLogic, &exceptTagsMatchLogic,
pq.Array(&f.Origins), pq.Array(&f.Origins),
pq.Array(&f.ExceptOrigins), pq.Array(&f.ExceptOrigins),
&f.MinSeeders,
&f.MaxSeeders,
&f.MinLeechers,
&f.MaxLeechers,
&f.CreatedAt, &f.CreatedAt,
&f.UpdatedAt, &f.UpdatedAt,
&extId, &extId,
@ -870,6 +886,10 @@ func (r *FilterRepo) Store(ctx context.Context, filter *domain.Filter) error {
"perfect_flac", "perfect_flac",
"origins", "origins",
"except_origins", "except_origins",
"min_seeders",
"max_seeders",
"min_leechers",
"max_leechers",
). ).
Values( Values(
filter.Name, filter.Name,
@ -929,6 +949,10 @@ func (r *FilterRepo) Store(ctx context.Context, filter *domain.Filter) error {
filter.PerfectFlac, filter.PerfectFlac,
pq.Array(filter.Origins), pq.Array(filter.Origins),
pq.Array(filter.ExceptOrigins), pq.Array(filter.ExceptOrigins),
filter.MinSeeders,
filter.MaxSeeders,
filter.MinLeechers,
filter.MaxLeechers,
). ).
Suffix("RETURNING id").RunWith(r.db.handler) 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("perfect_flac", filter.PerfectFlac).
Set("origins", pq.Array(filter.Origins)). Set("origins", pq.Array(filter.Origins)).
Set("except_origins", pq.Array(filter.ExceptOrigins)). 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)). Set("updated_at", time.Now().Format(time.RFC3339)).
Where(sq.Eq{"id": filter.ID}) Where(sq.Eq{"id": filter.ID})
@ -1237,6 +1265,18 @@ func (r *FilterRepo) UpdatePartial(ctx context.Context, filter domain.FilterUpda
if filter.ExternalWebhookRetryDelaySeconds != nil { if filter.ExternalWebhookRetryDelaySeconds != nil {
q = q.Set("external_webhook_retry_delay_seconds", filter.ExternalWebhookRetryDelaySeconds) 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}) q = q.Where(sq.Eq{"id": filter.ID})

View file

@ -129,7 +129,11 @@ CREATE TABLE filter
origins TEXT [] DEFAULT '{}', origins TEXT [] DEFAULT '{}',
except_origins TEXT [] DEFAULT '{}', except_origins TEXT [] DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 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 CREATE TABLE filter_external
@ -842,5 +846,17 @@ ALTER TABLE filter_external
`, `,
`ALTER TABLE action `ALTER TABLE action
ADD COLUMN external_client TEXT; 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;
`, `,
} }

View file

@ -129,7 +129,11 @@ CREATE TABLE filter
origins TEXT [] DEFAULT '{}', origins TEXT [] DEFAULT '{}',
except_origins TEXT [] DEFAULT '{}', except_origins TEXT [] DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 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 CREATE TABLE filter_external
@ -1483,5 +1487,17 @@ ALTER TABLE feed_dg_tmp
`, `,
`ALTER TABLE action `ALTER TABLE action
ADD COLUMN external_client TEXT; 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;
`, `,
} }

View file

@ -133,6 +133,10 @@ type Filter struct {
MatchDescription string `json:"match_description,omitempty"` MatchDescription string `json:"match_description,omitempty"`
ExceptDescription string `json:"except_description,omitempty"` ExceptDescription string `json:"except_description,omitempty"`
UseRegexDescription bool `json:"use_regex_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"` ActionsCount int `json:"actions_count"`
ActionsEnabledCount int `json:"actions_enabled_count"` ActionsEnabledCount int `json:"actions_enabled_count"`
Actions []*Action `json:"actions,omitempty"` Actions []*Action `json:"actions,omitempty"`
@ -232,6 +236,10 @@ type FilterUpdate struct {
ExceptTagsAny *string `json:"except_tags_any,omitempty"` ExceptTagsAny *string `json:"except_tags_any,omitempty"`
TagsMatchLogic *string `json:"tags_match_logic,omitempty"` TagsMatchLogic *string `json:"tags_match_logic,omitempty"`
ExceptTagsMatchLogic *string `json:"except_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"` 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"`
@ -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 { if len(f.Rejections) > 0 {
return f.Rejections, false return f.Rejections, false
} }

View file

@ -94,6 +94,8 @@ type Release struct {
PreTime string `json:"pre_time"` PreTime string `json:"pre_time"`
Other []string `json:"-"` Other []string `json:"-"`
RawCookie string `json:"-"` RawCookie string `json:"-"`
Seeders int `json:"-"`
Leechers int `json:"-"`
AdditionalSizeCheckRequired bool `json:"-"` AdditionalSizeCheckRequired bool `json:"-"`
FilterID int `json:"-"` FilterID int `json:"-"`
Filter *Filter `json:"-"` Filter *Filter `json:"-"`

View file

@ -114,6 +114,18 @@ func (j *TorznabJob) process(ctx context.Context) error {
rls.ParseString(item.Title) 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 { if j.Feed.Settings != nil && j.Feed.Settings.DownloadType == domain.FeedDownloadTypeMagnet {
rls.MagnetURI = item.Link rls.MagnetURI = item.Link
rls.DownloadURL = "" rls.DownloadURL = ""
@ -152,6 +164,20 @@ func (j *TorznabJob) process(ctx context.Context) error {
return nil 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 // Parse the downloadvolumefactor attribute. The returned value is the percentage
// of downloaded data that does NOT count towards a user's total download amount. // of downloaded data that does NOT count towards a user's total download amount.
func parseFreeleechTorznab(item torznab.FeedItem) (int, error) { func parseFreeleechTorznab(item torznab.FeedItem) (int, error) {

View file

@ -437,6 +437,10 @@ export const FilterDetails = () => {
albums: filter.albums, albums: filter.albums,
origins: filter.origins || [], origins: filter.origins || [],
except_origins: filter.except_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 || [], indexers: filter.indexers || [],
actions: filter.actions || [], actions: filter.actions || [],
external: filter.external || [] external: filter.external || []

View file

@ -48,7 +48,11 @@ export const FILTER_FIELDS: Record<string, string> = {
"except_tags_any": "boolean", "except_tags_any": "boolean",
"formats": "[]string", "formats": "[]string",
"quality": "[]string", "quality": "[]string",
"media": "[]string" "media": "[]string",
"min_seeders": "number",
"max_seeders": "number",
"min_leechers": "number",
"max_leechers": "number",
} as const; } as const;
export const IRC_FIELDS: Record<string, string> = { export const IRC_FIELDS: Record<string, string> = {

View file

@ -397,6 +397,50 @@ const FeedSpecific = ({ values }: ValueConsumer) => (
</div> </div>
} }
/> />
<Input.NumberField
name="min_seeders"
label="Min Seeders"
placeholder="Takes any number (0 is infinite)"
tooltip={
<div>
<p>Number of min seeders as specified by the respective unit. Only for Torznab</p>
<DocsLink href="https://autobrr.com/filters#rules" />
</div>
}
/>
<Input.NumberField
name="max_seeders"
label="Max Seeders"
placeholder="Takes any number (0 is infinite)"
tooltip={
<div>
<p>Number of max seeders as specified by the respective unit. Only for Torznab</p>
<DocsLink href="https://autobrr.com/filters#rules" />
</div>
}
/>
<Input.NumberField
name="min_leechers"
label="Min Leechers"
placeholder="Takes any number (0 is infinite)"
tooltip={
<div>
<p>Number of min leechers as specified by the respective unit. Only for Torznab</p>
<DocsLink href="https://autobrr.com/filters#rules" />
</div>
}
/>
<Input.NumberField
name="max_leechers"
label="Max Leechers"
placeholder="Takes any number (0 is infinite)"
tooltip={
<div>
<p>Number of max leechers as specified by the respective unit. Only for Torznab</p>
<DocsLink href="https://autobrr.com/filters#rules" />
</div>
}
/>
</CollapsibleSection> </CollapsibleSection>
); );

View file

@ -67,6 +67,10 @@ interface Filter {
except_tags_any: string; except_tags_any: string;
tags_match_logic: string; tags_match_logic: string;
except_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_count: number;
actions_enabled_count: number; actions_enabled_count: number;
actions: Action[]; actions: Action[];