AnisurgeAnisurge / extensions

Create an Extension — .asx Pack Author Guide

Complete authoring guide for Anisurge .asx extension packs — declarative JSON pipeline authoring, step types, variable templates, conditional branching, validation, and publishing workflow.

Create an Extension — Author Guide

This guide walks through creating a new .asx extension pack for the Anisurge app. Packs are declarative JSON pipelines — there is no TypeScript, JavaScript, Kotlin, or any compiled runtime in the pack file. The Anisurge app's AsxPipelineRunner executes the steps natively.

Prerequisites

  • Anisurge build ≥ 140 (for testing installations)
  • GitHub account (for submitting packs to the official catalog)
  • Browser developer tools or curl ability to inspect target website HTML, JSON APIs, and network requests
  • Node.js 20+ (for running validation scripts)
  • Understanding of JSON, regular expressions, and HTTP request/response flow

Step 1: Clone the Repository & Copy Boilerplate

git clone https://github.com/Anisurge/extensions.git
cd extensions
cp -r boilerplate/my-extension extensions/your-source-name
mv extensions/your-source-name/my-extension.asx extensions/your-source-name/your-source-name.asx

The boilerplate provides a minimal valid .asx structure:

{
  "schemaVersion": 1,
  "id": "my-extension",
  "name": "My Extension",
  "version": "0.1.0",
  "versionCode": 1,
  "pipeline": {
    "resolve": []
  }
}

Step 2: Pack Metadata Fields

FieldTypeRequiredDescriptionConstraints
schemaVersionintegerYesSchema version; must be 1Must equal AsxOfficial.SCHEMA_VERSION
idstringYesUnique identifierRegex [a-z0-9-]+, must match folder name
namestringYesHuman-readable display nameUsed in UI lists
versionstringYesDisplay version stringe.g. 1.2.0, shown in catalog
versionCodeintegerYesMonotonic version for updatesMust increase on changes
languagestringNoLanguage codeDefault all
nsfwbooleanNoNSFW content flagDefault false, shows warning label
descriptionstringNoShort description of the sourceDisplayed in catalog cards
authorstringNoCreator name or handleShown in pack metadata
homepagestringNoProject homepage URL
iconUrlstringNo48×48 PNG icon URLDisplayed in install dialog and catalog
minAppVersionCodeintegerNoMinimum app build requiredPrevents install on incompatible versions
capabilitiesobjectNoFeature flagsSee below
defaults.headersobjectNoDefault HTTP headersMerged into every http.get step
pipeline.resolvearrayYesPipeline step listAt least one step, must end with map.videos

Capabilities Object

{
  "capabilities": {
    "subDub": true,
    "resolveBy": ["anilistId", "malId"]
  }
}
  • subDub — Set true if the source provides separate sub and dub streams. The app shows a language toggle.
  • resolveBy — Specifies which identifiers the pipeline can resolve from. Default is ["anilistId"].

Step 3: Runtime Context Variables

The app injects these context variables before the pipeline runs. Use them in step URLs and templates with the {{variable}} syntax:

VariableTypeDescriptionExample
{{anilistId}}integerAniList media ID21
{{malId}}integerMyAnimeList ID (0 if unknown)35247
{{episode}}integerEpisode number to resolve12
{{lang}}stringLanguage preferencesub or dub
{{title}}stringBest-effort anime titleOne Piece

Intermediate Variables

Pipeline steps can save results into named variables using the saveAs field. These can then be referenced in subsequent steps:

{{pageHtml}}         — Raw HTML from an http.get step
{{sources}}          — Parsed JSON object from json.parse
{{sources.sources.file}} — Nested value from a json.get path
{{streamUrl}}        — Extracted stream URL string

Step 4: Pipeline Steps Reference

http.get — Fetch Remote Content

Fetches a URL and saves the response body text to a variable. Supports custom headers merged from defaults.headers.

{
  "step": "http.get",
  "url": "https://example.com/api/{{anilistId}}/{{episode}}",
  "headers": {
    "Accept": "text/html",
    "Referer": "https://example.com/"
  },
  "saveAs": "pageHtml",
  "optional": false
}
FieldTypeRequiredDescription
urlstringYesTarget URL with optional {{variable}} templates
headersobjectNoPer-request headers, merged on top of defaults.headers
saveAsstringYesVariable name to store the response body
optionalbooleanNoIf true, non-2xx responses don't fail the pipeline

extract.regex — Regex Extraction

Extracts a substring using a JavaScript-compatible regular expression pattern and capture group.

{
  "step": "extract.regex",
  "from": "pageHtml",
  "pattern": "data-id=\"(\\d+)\"",
  "group": 1,
  "saveAs": "dataId",
  "onlyIfMissing": "dataId"
}
FieldTypeRequiredDescription
fromstringYesSource variable containing the text
patternstringYesRegex pattern (escape backslashes for JSON)
groupintegerNoCapture group index (0 = full match, 1 = first group)
saveAsstringYesVariable to store the matched text
onlyIfMissingstringNoOnly run if this variable is empty/absent (deduplication)

json.parse — Parse JSON Text

Parses a JSON-formatted string variable into a structured object for subsequent navigation.

{
  "step": "json.parse",
  "from": "sourcesBody",
  "saveAs": "sources"
}
FieldTypeRequiredDescription
fromstringYesSource variable containing JSON text
saveAsstringYesVariable to store the parsed JSON object

json.get — Navigate JSON Structure

Retrieves a value from a parsed JSON object by dot-separated path.

{
  "step": "json.get",
  "from": "sources",
  "path": "sources.0.file",
  "saveAs": "streamUrl"
}
FieldTypeRequiredDescription
fromstringYesVariable containing a parsed JSON object
pathstringYesDot-separated path (array indices supported as 0, 1, etc.)
saveAsstringYesVariable to store the retrieved value

map.videos — Produce Stream Output

The final step in every pipeline. Maps resolved variables into video stream objects for the mpv player.

{
  "step": "map.videos",
  "file": "{{sources.sources.0.file}}",
  "quality": "{{sources.sources.0.label}}",
  "headers": {
    "Referer": "https://example.com/",
    "User-Agent": "Mozilla/5.0"
  },
  "subtitles": {
    "from": "sources.tracks",
    "urlKey": "file",
    "labelKey": "label"
  },
  "intro": "sources.intro",
  "outro": "sources.outro"
}
FieldTypeRequiredDescription
filestringYesFinal video stream URL (HLS .m3u8 or direct MP4)
qualitystringNoDisplay label for quality (e.g. Auto, 1080p, 720p)
headersobjectNoHeaders required for playback (Referer, User-Agent, etc.)
subtitlesobjectNoSubtitle track configuration
introstringNoVariable path for intro end timestamp (seconds)
outrostringNoVariable path for outro start timestamp (seconds)

Subtitles Object

FieldTypeDescription
fromstringSource variable containing subtitle tracks array
urlKeystringKey in each track object for the subtitle file URL
labelKeystringKey in each track object for the display label (e.g. English, Spanish)

try — Conditional Fallback Branches

Allows ordered fallback strategies. Each branch is tried in sequence; the first branch that completes without error wins. Branches can have optional when conditions.

{
  "step": "try",
  "branches": [
    {
      "name": "resolve via AniList",
      "when": { "op": "gt", "variable": "malId", "value": 0 },
      "steps": [
        { "step": "http.get", "url": "https://api.example.com/anilist/{{anilistId}}/{{episode}}", "saveAs": "pageHtml" },
        { "step": "extract.regex", "from": "pageHtml", "pattern": "src=\"(https:[^\"]+\\.m3u8)\"", "group": 1, "saveAs": "streamUrl" }
      ]
    },
    {
      "name": "fallback to MAL",
      "steps": [
        { "step": "http.get", "url": "https://api.example.com/mal/{{malId}}/{{episode}}", "saveAs": "pageHtml" },
        { "step": "extract.regex", "from": "pageHtml", "pattern": "src=\"(https:[^\"]+\\.m3u8)\"", "group": 1, "saveAs": "streamUrl" }
      ]
    }
  ]
}

When Conditions

FieldTypeDescription
opstringOperator: gt (greater than), eq (equals), exists (truthy check)
variablestringVariable name to check (e.g., malId)
valuenumber/stringComparison value (required for gt and eq)

Step 5: Complete Example — Anokoto Pipeline

{
  "schemaVersion": 1,
  "id": "anokoto",
  "name": "Anokoto",
  "version": "1.0.1",
  "versionCode": 2,
  "language": "all",
  "nsfw": false,
  "description": "MegaPlay HLS + softsubs from AniList or MAL",
  "author": "Anisurge",
  "capabilities": { "subDub": false, "resolveBy": ["anilistId", "malId"] },
  "pipeline": {
    "resolve": [
      { "step": "http.get", "url": "https://api.anokoto.example/{{anilistId}}/{{episode}}", "saveAs": "pageHtml" },
      { "step": "extract.regex", "from": "pageHtml", "pattern": "data-sources='([^']+)'", "group": 1, "saveAs": "sourcesJson" },
      { "step": "json.parse", "from": "sourcesJson", "saveAs": "sources" },
      { "step": "map.videos", "file": "{{sources.file}}", "quality": "Auto", "headers": { "Referer": "https://example.com/" }, "subtitles": { "from": "sources.tracks", "urlKey": "file", "labelKey": "label" } }
    ]
  }
}

Step 6: Validate Your Pack

Run the official validation script to check for common issues:

node scripts/validate.mjs

The validator checks:

  • Valid JSON syntax
  • schemaVersion equals 1
  • id matches [a-z0-9-]+ pattern
  • versionCode is a positive integer
  • Pipeline contains at least one step ending with map.videos
  • Step fields match expected types
  • No unknown fields

For index-level validation (after adding to index.json):

node scripts/generate-index.mjs   # rebuilds index.json from extensions/
node scripts/validate.mjs         # validates both packs and index

Step 7: Test Installation

  1. Host your .asx over HTTPS (raw GitHub URLs work well for testing)
  2. Open the install deep link:
anisurgex://extensions/install?type=source&engine=ANISURGE&url=<your_asx_url>&name=<pack_name>&version=<version>
  1. Confirm installation in the dialog
  2. Play a test title — pick a known anime with a confirmed AniList ID
  3. Check output — verify the video loads, audio plays, subtitles appear if provided

Step 8: Submit to Official Catalog

  1. Create your pack folder under extensions/<id>/ with:

    • <id>.asx — the pack file
    • icon.png — 48×48 icon (optional but recommended)
    • README.md — usage notes (optional)
  2. Add to index.json — run node scripts/generate-index.mjs

  3. Open a pull request on github.com/Anisurge/extensions

PR Checklist

  • One extension per PR
  • versionCode bumped on behavior changes
  • index.json regenerated and committed
  • No secrets, API keys, or private tokens in the pack
  • nsfw: true set when appropriate
  • .asx passes validate.mjs
  • Tested with a real episode in the app

Best Practices

  1. Prefer try branches for resilience — try AniList first, fall back to MAL
  2. Keep HTTP requests minimal — each http.get adds latency to playback startup
  3. Use onlyIfMissing on regex steps to avoid redundant extractions
  4. Set optional: true on non-critical HTTP requests to avoid pipeline failure
  5. Include provider headers (Referer, User-Agent) that match browser behavior
  6. Version codes are monotonic — never decrease versionCode
  7. Test both sub and dub if capabilities.subDub is set
  8. Handle rate limiting — some providers block rapid sequential requests