mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
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:
parent
256fbb49ba
commit
a86258aaa7
10 changed files with 192 additions and 3 deletions
|
@ -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})
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:"-"`
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 || []
|
||||||
|
|
|
@ -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> = {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
4
web/src/types/Filter.d.ts
vendored
4
web/src/types/Filter.d.ts
vendored
|
@ -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[];
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue