🌐 Extensions
Extensions allow you to bring your own content sources into Shiru, such as a personal media server or any other legally owned media you host yourself.
Important
Shiru does not provide extensions, and the maintainers cannot provide you sources. Please do not ask.
Extensions are intended for accessing legally owned media only, such as content hosted on your own personal media server. Users are responsible for ensuring their use of any extension complies with all applicable laws.
🔎 Overview
What are extensions?
Extensions are JavaScript modules that connect Shiru to external content sources you control. Out of the box, Shiru plays files you already have locally. Extensions extend this by allowing Shiru to fetch content from sources like your own personal media server, making your library accessible from anywhere.
Does Shiru provide any extensions?
No. Shiru is a bring-your-own-content application and does not offer, recommend, or endorse any specific extensions or sources.
Is there a curated list of extensions?
No. Shiru is not directly associated with any extensions or sources. It is solely a client for managing and playing your own media.
Can I disable an extension without removing it?
Yes. Extensions can be toggled on or off individually from Settings > Extensions. Disabled extensions will not be queried, but are validated and remain added for easy re-enabling later.
⚙️ Adding & Managing Extensions
Navigate to Settings > Extensions > Sources to manage your extension sources.
A source entry points to an index.json manifest that describes one or more extensions. It can be added in three ways:
- Direct URL - provide a full URL to an
index.json, e.g.https://example.com/index.json - GitHub - use the format
gh:username/repoorgh:username/repo/pathif the manifest is in a subdirectory - npm - use the format
npm:package-name
Shiru will automatically import all extensions the source provides (when the source manifest is not set as an extension repository), but you can individually disable extensions you do not want.
Extension Repositories
A single source can provide multiple extensions via an index.json that contains an array of entries, each with its own main path pointing to an extension source. This is useful for adding a repository of extensions that adds an Available Sources dropdown under the sources tab. You can then easily add which sources you want from the repository. The index.json that defines a repository would be as follows:
[
{
"main": "gh:username/repo/extension-one"
},
{
"main": "gh:username/repo/extension-two"
}
]
Adding gh:username/repo as a source will automatically import all sources defined in that repository's index.json but will not download the extensions in each source; you must manually click to add the source extensions.
🔧 Developing Extensions
Extensions are written in JavaScript and run in an isolated Web Worker. Any data you fetch must be CORS enabled.
Full type definitions, the abstract base class, and the reference implementation are available here.
Extensions load in parallel, so results from multiple extensions appear as each one completes rather than waiting for all to finish. Results are cached for approximately two minutes, so re-opening the search modal will not trigger another full fetch.
Structure Overview
An extension source consists of three parts:
index.json- the manifest describing your extension(s)sources/your-extension.js- the extension logic, extendingAbstractSourceindex.d.ts- optional but recommended type definitions for IDE support
Testing Locally
You can specify a direct file path to your index.json instead of a URL during development. This lets you verify changes without hosting the extension remotely.
You can also load the official, fully functional reference extension via gh:RockinChaos/Shiru/extensions. It generates dummy results based on query parameters and is useful for understanding how extensions work end-to-end.
Hosting on the Web
Extensions must be served as ESM-compatible JavaScript modules. If your extension is hosted as a plain .js file or npm package, you can use esm.sh to serve it as an ESM module without any build step:
https://esm.sh/gh/username/repo/my-extension
Both gh: and npm: prefixes are resolved through esm.sh, which serves the module as ESM-compatible JavaScript automatically. If you are pointing to a raw URL, ensure your server responds with Content-Type: application/javascript and includes the appropriate CORS headers.
Manifest - index.json
Each entry in your index.json describes one extension. A minimal example:
[
{
"id": "my-extension",
"name": "My Extension",
"version": "0.0.1",
"main": "sources/my-extension",
"update": "gh:username/repo",
"type": "torrent",
"speed": "fast",
"accuracy": "high",
"regions": ["US", "GB"],
"description": "Connects to my personal media server.",
"icon": "iVBORw0KGgoAAAANS..."
}
]
The icon field accepts either a raw base64-encoded image string (without the data:image/...;base64, prefix) or a direct URL. Base64 is preferred as it avoids an extra network request and ensures the icon is always available, regardless of whether the source is reachable.
SourceConfig Fields
| Field | Type | Description |
|---|---|---|
id |
string |
Unique identifier for the extension. The more unique, the better |
name |
string |
Display name shown in the UI |
version |
string |
Semantic version, e.g. 0.0.1 |
main |
string |
Path to the extension module relative to the manifest. Supports gh:, npm:, or a direct URL |
update |
string |
Path to the manifest used for update checks. Supports gh: and npm: prefixes |
nsfw |
boolean? |
Set to true if the source may return NSFW results e.g. Hentai |
unregulated |
boolean? |
Set to true if the source allows anonymous or unregistered uploads, which increases security risk |
type |
'torrent'? |
Source type. Currently only torrent is supported |
speed |
'fast' | 'moderate' | 'slow'? |
Estimated average fetch speed across various user locations. Do not factor in your own location |
accuracy |
'high' | 'medium' | 'low'? |
Likelihood that results match the requested series. Use high only for guaranteed matches |
regions |
ServerLocations[]? |
ISO 3166-1 alpha-2 country codes representing the server node locations |
deprecated |
boolean? |
Set to true when an extension is no longer maintained, or its source has shut down |
description |
string? |
Short description shown in the UI. Markdown and basic HTML are supported |
icon |
string? |
Raw base64-encoded image (without prefix) or URL. Base64 is recommended |
RepositoryConfig Fields
If your index.json is acting as a repository, a list of pointers to other extension sources rather than defining extensions directly, each entry only needs a main field:
| Field | Type | Description |
|---|---|---|
main |
string |
Path to the extension source. Supports gh:username/repo/path, npm:package-name, or a direct URL |
Extension Class
Extensions extend AbstractSource and must implement four methods: single, batch, movie, and validate.
import AbstractSource from './abstract.js'
export default new class MySource extends AbstractSource {
url = 'https://your-source-url.com'
async single(options) {
// Return results for a single episode.
}
async batch(options) {
// Return results for a full batch or season.
}
async movie(options) {
// Return results for a movie.
// If your source cannot distinguish movies from episodes, return [] and rely on single() instead.
}
async validate() {
// Return true if the source is reachable.
return (await fetch(this.url))?.ok
}
}()
The validate() method supports failover logic; you can check multiple mirror URLs and update this.url to a working endpoint before queries begin:
async validate() {
const mirrors = ['https://primary.example.com', 'https://backup.example.com']
for (const mirror of mirrors) {
if ((await fetch(mirror))?.ok) {
this.url = mirror
return true
}
}
return false
}
Options Object
The options object is passed to single, batch, and movie as the first parameter.
| Field | Type | Description |
|---|---|---|
anilistId |
number |
AniList anime ID |
media |
object |
Full AniList media entry for the requested series |
mappingsA |
object? |
Anime-level cross-platform mapping data (AniDB, TVDB, IMDB, MVDB), not always present |
mappingsE |
object? |
Episode-level mapping data, not always present |
anidbAid |
number? |
AniDB anime ID, not always present |
anidbEid |
number? |
AniDB episode ID, not always present |
tvdbAid |
number? |
TVDB series ID, not always present |
tvdbEid |
number? |
TVDB episode ID, not always present |
imdbAid |
string? |
IMDB ID, not always present |
mvdbAid |
number? |
TheMovieDB ID, not always present |
titles |
string[] |
All known titles and alternative titles for the anime |
episode |
number? |
Episode number to search for, not present for movies |
episodeCount |
number? |
Total episode count, not always present |
resolution |
'2160' | '1080' | '720' | '540' | '480' | '' |
Preferred video resolution. An empty string means any |
exclusions |
string[] |
Keywords to exclude, such as unsupported codecs. Empty when an external player is enabled |
Note
Mapping fields such as
anidbAid,anidbEid,tvdbAid,tvdbEid,imdbAid, andmvdbAidsignificantly improve search accuracy when available. It is recommended to make use of them wherever possible.
Results Object
Each function must return a Promise<TorrentResult[]>. Each result should conform to the following:
| Field | Type | Description |
|---|---|---|
title |
string |
Content title. May represent multiple files in a batch |
link |
string |
Direct http:// link to a content file, or a magnet: URI |
id |
number? |
Optional source-specific ID |
seeders |
number |
Number of seeders |
leechers |
number |
Number of leechers |
downloads |
number |
Total download count |
accuracy |
'high' | 'medium' | 'low'? |
Confidence that this result matches the requested episode |
hash |
string |
Required. Info hash for the content |
size |
number |
File size in bytes |
date |
Date |
Upload date |
type |
'batch' | 'best' | 'alt'? |
Result classification. best and alt indicate relative quality ranking |
Extension Deprecation
Extensions support a deprecated flag in the manifest to mark them as no longer maintained or as having had their source shut down. Deprecated extensions are visually indicated in the UI so users know to look for alternatives.
Tips for Extension Developers
- Use anitomyscript to parse file names accurately, it is manually exposed to all extensions and can be accessed via
this.anitomyscriptwithout any import. This is especially useful for plain text searches where title matching is ambiguous - Always include both the original and any romanized or translated titles in your queries. Passing only a modified title can cause mismatches
- Make use of
anidbEid,tvdbEid, and the mapping objects wherever possible; they significantly improve accuracy for series with ambiguous or duplicate titles - The
validate()method supports mirror failover: check multiple URLs and assignthis.urlto a working one before queries begin - For the
movie()method, if your source cannot distinguish movies from episodes, return an empty array and rely onsingle()instead - Test locally using a direct file path to your
index.jsonbefore publishing - Markdown and basic HTML are supported in extension
descriptionfields - The reference implementation at
gh:RockinChaos/Shiru/extensionsis fully functional and generates dummy results. Add it to explore how all parts of an extension fit together

