Skip to content

Custom Rules

A custom rule is a short Rego policy that reads a release and adds named score entries, or blocks it. Rules run after the quality profile has filtered and the persona has scored, so they express what the built-in scoring cannot: a tracker’s freeleech flag, a favorite anime group, a hard size ceiling.

Rules live in Settings → Rules and require the manageCatalogSettings permission.

A rule is a Rego module whose only output is score_entry, a map from a code to an integer delta.

package scryer.rules.user.prefer_web_dl
import rego.v1
score_entry["prefer_web_dl"] := 100 if {
scryer.normalize_source(input.release.source) == "WEB-DL"
}

When a release is evaluated, Scryer merges every entry from every applicable rule into the scoring log. The code is the label you will see there, so name it for what it means. The delta is added to the release score. A rule that matches nothing contributes nothing.

Three details of the contract matter:

  • The package is managed for you. Scryer rewrites the declaration to package scryer.rules.user.<id> on save. If you omit the package or the import rego.v1 line, both are added.
  • Deltas must be integers. A float is rejected at validation with a hint to use round() or ceil().
  • Only score_entry is read. Helper rules are fine, but any other output is ignored.

Every rule receives one input document with five sections.

SectionWhat it holdsAvailable
input.releaseThe parsed release: title, quality, source, codecs, audio, languages, flags, group, size, age, indexer extrasAlways
input.profileThe quality profile in force: tiers, allowlists, HDR and DV settings, required languagesAlways
input.contextThe title and library: media type, category, original language, tags, whether a file exists and its score, search mode, anime flagsAlways
input.builtin_scoreWhat the profile and persona already decided: total, blocked, and the codes that firedAlways
input.fileProbed media info from the downloaded file: streams, codecs, bit depth, HDR format, chaptersPost-download only, otherwise null
  • Identity: raw_title, release_group (may be null), year, parse_confidence, edition, streaming_service.
  • Format: quality such as "2160P", source, video_codec, audio, audio_codecs[], audio_channels, is_remux, is_bd_disk, is_10bit, detected_hdr, is_dolby_vision, is_hdr10plus, is_hlg, is_atmos.
  • Language: languages_audio[], languages_subtitles[], is_dual_audio, is_dubs_only, is_hardcoded_subs.
  • Hygiene: is_proper_upload, is_repack, is_ai_enhanced, is_password_protected, is_obfuscated, is_retagged, has_release_group.
  • Structure: is_season_pack, is_multi_episode, episode_release_type, anime_version.
  • Size and age: size_bytes, age_days, plus thumbs_up and thumbs_down where an indexer reports them.
  • guide_facts[]: codes the bundled TRaSH data attached to this release, such as trash.ai_enhanced, trash.lang.not_original, or trash.locale.french.group.tier1.
  • extra: whatever the indexer plugin supplied. Torrent indexers typically add seeders, leechers, and freeleech. Keys vary by indexer, so guard for absence.

media_type and category tell you which facet you are in. original_language and inferred_original_audio_language let language rules avoid hard-coding a language. tags[] carries title tags, which is how the TRaSH locale packs scope themselves. has_existing_file and existing_score let a rule behave differently for upgrades. is_anime and is_filler are set for anime episodes.

input.file is null at search time and populated after the download finishes and the file is probed. A rule that inspects it runs at import, where a block rejects the file instead of the release. Always guard with input.file != null so the same rule stays quiet during search.

score_entry["missing_japanese_audio"] := scryer.block_score() if {
input.file != null
not has_japanese_audio
}
has_japanese_audio if {
some lang in input.file.audio_languages
scryer.lang_matches(lang, "ja")
}

The editor’s Input Context Reference lists every field with its type. The same contract is exposed to plugins as JSON.

Standard OPA builtins are available except those the sandbox removes. Scryer adds five of its own.

BuiltinReturnsUse it for
scryer.block_score()The blocking sentinelAny rule that must reject a release
scryer.size_gib(bytes)Size in GiB as a numberSize thresholds without arithmetic
scryer.lang_matches(code, pattern)BooleanLanguage checks across ISO 639 aliases; ("jpn", "ja") is true
scryer.normalize_source(raw)Canonical source such as "WEB-DL"Comparing sources without caring how the release spelled them
scryer.normalize_codec(raw)Canonical codec such as "H.264"Comparing codecs; "h264", "x264", and "AVC" normalize alike

Prefer the normalizers over string comparison. Release naming is inconsistent, and the normalizers already know the variants.

Blocking is a specific delta, not a big negative number. scryer.block_score() returns the sentinel Scryer checks for when it decides whether a release is allowed at all.

score_entry["oversized"] := scryer.block_score() if {
scryer.size_gib(input.release.size_bytes) > 100
}

A hand-written -9500 is only a heavy penalty. At search time the release still ranks, and if it is the only candidate it still gets grabbed. Use the builtin every time.

Two other mechanisms look like blocking and are not:

  • Minimum score to grab on the quality profile vetoes releases whose total falls below a floor, with the code score_below_minimum. It is a grab-time threshold, not a rule.
  • Quality tier is compared before score. A release outside the profile’s tiers never reaches your rules.

A blocked release stays in the scoring log with its block code, so interactive search shows exactly which rule rejected it.

Rules run in a sandboxed evaluator with these limits:

  • No network or filesystem. http.send and similar builtins are unavailable.
  • Package isolation. A rule cannot import or read another rule’s package.
  • Read-only input. Rules cannot change the release, profile, or context.
  • Restricted output. Only score_entry is collected, and only integer deltas are accepted.
  • Error isolation. A rule that fails at runtime logs a user_rule_error entry with a delta of zero. Other rules keep running and the release is still scored.

Validate in the editor compiles the rule, runs it against a synthetic release, and checks the output shape. Save runs the same validation and refuses a rule that fails it. Validation cannot tell you whether the rule expresses what you meant, so follow it with an interactive search on a title the rule should affect and read the scoring log.

Three fields on the rule control where it applies:

  • Applied facets limits the rule to Movies, Series, or Anime. Leave it empty to apply everywhere. This is the only scoping mechanism. Rules are not attached to quality profiles, routing, or indexers.
  • Enabled turns the rule off without deleting it.
  • Priority only orders the list in Settings → Rules. It does not change evaluation, because every enabled rule contributes to the same score.

Rules that need finer targeting than facets should read input.context. Tags are the usual handle: a rule can require "4k-only" in input.context.tags and then only titles carrying that tag are affected.

Name codes for the scoring log. freeleech_bonus and poorly_seeded read well in a list of thirty entries. rule1 does not.

One concern per rule. A rule that rewards freeleech and penalizes low seeders is fine, because both are about the torrent. A rule that also blocks x264 at 4K is two rules glued together and harder to switch off.

Guard for absence. Optional fields such as release_group and everything under extra may be missing or null. Rego treats an undefined comparison as a non-match, which is usually what you want, but explicit null checks make the intent visible.

score_entry["well_seeded"] := 200 if {
input.release.extra.seeders >= 10
}
score_entry["poorly_seeded"] := -300 if {
input.release.extra.seeders != null
input.release.extra.seeders < 3
}

Normalize before comparing. Use scryer.normalize_source, scryer.normalize_codec, and scryer.lang_matches rather than string equality. Lowercase release groups.

Use sets for lists. Membership tests read better than chained comparisons and are easy to extend.

preferred_groups := {"subsplease", "erai-raws", "ember", "yameii"}
score_entry["preferred_anime_group"] := 400 if {
input.release.release_group != null
lower(input.release.release_group) in preferred_groups
}

Size deltas against the persona. Persona weights for a single attribute run from a few dozen to a few hundred points, and the TRaSH tier bonuses default to 120, 60, and 20. A delta of 100 nudges. A delta of 500 dominates everything except tier. Start small and check the scoring log.

Check what already fired. input.builtin_score.codes and input.release.guide_facts tell you what the profile, persona, and bundled TRaSH data already decided. If trash.ai_enhanced is present, the built-in scoring has handled upscales. Duplicating it double-counts.

Prefer facets, then context. Set applied facets for coarse scope. Read input.context.is_anime, category, or tags for anything finer. Do not encode the facet in the rule body when the field does it for you.

Block for constraints, score for preferences. If you would refuse the file when it arrives, block. If you would take it grudgingly, penalize.

The Rule Library panel in Settings → Rules holds pre-built rules grouped by category. Select one and press Apply template to load its name, description, and source into the editor. Edit it there, rename it, validate, and save. The template stays available for the next rule.

CategoryTemplates
TorrentFreeleech bonus, halfleech bonus, well-seeded bonus
QualityPrefer WEB-DL, prefer x265, penalize x264 at 4K
SizeBlock oversized, prefer compact, block tiny releases
AudioRequire Japanese audio, require English audio, prefer multi-audio, prefer Atmos
AnimeAnime group preference, block mini encodes
BlockingBlock old releases, block password protected, require release group, block obfuscated or retagged, block low-quality groups, block hardcoded subs

Most rules are a template with the numbers changed. The anime group preference template is a set of groups and a bonus. Swap the groups. The block oversized template is a GiB threshold. Change the number. Treat the library as the starting point even when the rule you want is not there, because the closest template already has the right shape and the right null guards.

Managed rules, described below, cannot be edited in place. Copy as custom rule creates an editable draft named “Copy of …” with the same source. Fork one when you want its logic with different numbers.

A rule pack is a JSON file of rule templates published through the plugin registry. Packs appear as a tab in the Rule Library alongside the built-in categories. Applying a pack template behaves exactly like applying a built-in one: the source is copied into your editor and saved as your own rule. Nothing links back to the pack, and a pack update never changes a rule you already saved.

Two packs ship in the official registry today:

  • Anime scoring pack: prefer SubsPlease, prefer Erai-raws, block mini encodes, prefer batch releases, require Japanese audio after download.
  • Torrent optimization pack: freeleech bonus, halfleech bonus, well-seeded bonus, block no seeders, internal release bonus.

If the tab says no packs are available, refresh the plugin registry from Settings → Plugins and check again.

A pack file declares schema_version, an id, name, description, author, version, and a list of rules. Each rule carries an id, title, description, category, optional appliedFacets, and regoSource. The registry’s manifest maps the pack id to its download location and the minimum Scryer version it needs. Anyone can publish a pack by following that shape; see Plugins for the registry.

Plugins can also contribute scoring policies directly. Those appear in the scoring log with a System source and are managed from the plugin, not from Settings → Rules.

The TRaSH Guides Locale Packs panel lists score-only packs generated from TRaSH Guides data for language-specific setups. They cover the release group tiers and language rules the guides define for a locale, which the default persona does not carry.

PackCovers
TRaSH Guides French (MULTi VF)French dubs alongside the original audio
TRaSH Guides French (MULTi VO)Original audio with French available
TRaSH Guides French (VOSTFR)Original audio with French subtitles
TRaSH Guides German LocaleGerman and German anime score sets
TRaSH Guides Asian LocaleAsian-locale release groups

Each pack is a generated Rego rule. It reads the guide_facts codes the parser attached to the release and adds the guide’s score for each: tier 1, tier 2, and tier 3 groups earn a bonus, low-quality groups and scene releases take a penalty, and the language entries compare languages_audio against the title’s inferred original language. Where the guides define a score it is used. Where they do not, Scryer’s own default is marked in the source with a scryer-native score comment.

Every entry in a locale pack is gated by a tag filter. When you enable a pack you can set a managed tag filter. With no filter the pack applies to every title in its facets. With a filter such as locale:asian, only titles carrying that tag are scored by the pack. This is how one Scryer instance can run a French pack for one family member’s library and leave everything else alone.

  • Only one French pack at a time. The three French packs carry contradictory score sets. Enabling a second one fails with a message naming the pack that is already on.
  • Managed rules are read-only. Their source is regenerated whenever the bundled TRaSH data is updated, so edits would be lost. The editor shows them but does not let you change them.
  • Fork to customize. Copy as custom rule gives you an editable copy with the current generated source. The copy is yours and will not be regenerated.
  • They are score-only. A locale pack never blocks. Combine it with your own blocking rules or with the profile’s required audio languages when a language is mandatory.

Run an interactive search and open the Scoring log on any result. Each line shows a code, its delta, and its source: Builtin for the profile and persona, the rule’s name for a custom rule, and System for managed and plugin rules. The total is the release score.

Read the log before changing anything. Most surprises turn out to be a persona weight or a tier comparison rather than the rule you suspected.