Streamiva Plugins
A Streamiva plugin is a small HTTP server you run yourself. Streamiva calls it over HTTPS to ask for things it doesn't natively have — a curated or personalized row of titles, an alternate viewing source for a movie or episode, subtitle tracks, or the releases a title can be played from. Streamiva never runs your code; it only ever makes outbound HTTPS requests to endpoints you publish, and treats every response as untrusted input.
This document describes the contract as actually implemented. If you notice this file disagreeing with real server behavior, the server is the source of truth — please open an issue.
Getting listed
- Stand up your own HTTPS server that serves a manifest (see below) and the endpoints for whichever capabilities you're implementing.
- Add
{"manifest_url": "https://your-server/manifest.json"}toplugin-registry/registry.jsonin the Streamiva repo and open a PR. - Once merged, a project owner runs
python -m app.registry_sync(or the equivalent scheduled job), which fetches your manifest live, validates it, and upserts it into the Store's plugin listing. This re-runs periodically, so editing your live manifest (adding a capability, changing yourbehavior_hints, rotating an endpoint URL) is picked up automatically — no new PR needed unlessmanifest_urlitself changes. - A malformed, unreachable, or non-HTTPS manifest is skipped, not fatal — your existing listing (from the last successful fetch) stays live until the next sync succeeds.
A user installs your plugin from the Store on their own account. Each install gets its own opaque
request_id, generated by Streamiva, sent on every call your plugin receives — use it to
distinguish installs if you need to (e.g. for your own rate limiting), but it never reveals
anything about the underlying Streamiva account.
Manifest
Serve this as JSON from manifest_url (GET, no auth):
{
"id": "yourname.subtitle-vault",
"name": "Subtitle Vault",
"version": "1.0.0",
"description": "Finds Hebrew, English and Russian subtitle tracks on TheSubtitleDB and Wizdom.",
"developer": "yourname",
"manifest_url": "https://your-server/manifest.json",
"capabilities": [
{ "type": "subtitle", "media_types": ["movie", "tv_episode"] }
],
"endpoints": {
"row": null,
"viewing_source": null,
"subtitle": "https://your-server/subtitle",
"stream": null
},
"config_schema": [],
"behavior_hints": {
"cache_max_age_seconds": 3600
}
}
| Field | Type | Notes |
|---|---|---|
id |
string | Globally unique. Convention: developer.plugin-name. |
name |
string | Shown in the Store and in Settings. |
version |
string | Free-form (e.g. semver). Not currently diffed by Streamiva — informational. |
description |
string | Shown in the Store. |
developer |
string | Shown in the Store. |
manifest_url |
string | Must match the URL Streamiva fetched this document from. A mismatch falls under "malformed" in the Getting listed skip rule — Streamiva does not raise an error, it just leaves your last-known-good listing (or no listing at all, on a first sync) in place. |
capabilities |
array | One entry per capability you implement — see below. A plugin may declare more than one. |
endpoints |
object | {row, viewing_source, subtitle, stream}, each an HTTPS URL or null. Only endpoints matching a declared capability are ever called. |
config_schema |
array | Optional per-install settings fields a user fills in from Settings. [] if you have none. |
behavior_hints.cache_max_age_seconds |
int | null | How long Streamiva may cache your responses before calling you again. null means 15 minutes, and any value above 7 days is cut to 7 days, so a plugin can't make Streamiva serve indefinitely stale data. No minimum is enforced today. A value below 15 minutes is used as declared, but don't rely on that: declare 900 or more, because a low value only means more calls to your server. The cap is silent. Your own manifest response, the Store listing and GET /plugins/{id} all echo back the raw value you declared, not the one in effect. |
Capability entries
{ "type": "row", "id": "trending-horror", "personalized": false, "label": "Trending Horror" }
{ "type": "viewing_source", "media_types": ["movie", "tv_episode"] }
{ "type": "subtitle", "media_types": ["movie", "tv_episode"] }
{ "type": "stream", "media_types": ["movie", "tv_episode"] }
row:idis your own stable identifier for this row (used in the request body and as part of the cache key — see Row).labelis the row's display title.personalized: truemeans the row differs per viewer profile (see below); omit or setfalsefor a curated row every viewer sees the same way.viewing_source/subtitle/stream:media_typeslists which of"movie"/"tv_episode"you support. There is no whole-series media type — a TV show is only ever addressed episode by episode.
config_schema entries
{ "key": "region", "type": "select", "label": "Region", "options": ["US", "UK", "CA"] }
type is one of "string", "number", "boolean", "select" ("select" requires options).
A user's saved values are stored per-install and validated against this schema on save, but are
not currently forwarded on any capability call. If your endpoint needs configuration (an API
key, a region), your server needs its own copy of it (e.g. its own .env) — don't rely on
Streamiva sending config back to you. This applies just as much to Subtitle plugins as to Row or
Viewing Source — a region or preferred-language selector is a natural thing to want to declare
in config_schema for a subtitle plugin, and it still won't be forwarded; your server needs its
own default or its own copy of that setting.
Trust boundary baseline
Every outbound call Streamiva makes to your server — fetching your manifest or calling a capability endpoint — is bounded the same way:
- HTTPS only. A
manifest_urlor endpoint URL that isn'thttps://is never called. - 3-second hard timeout. No retries on Streamiva's side; a slow response is treated exactly like no response.
- 256 KB response size cap, enforced via
Content-Lengthwhen present and by counting bytes read otherwise. - Strict schema validation, reject-and-drop. Your response body must be valid JSON matching the shape below for the endpoint you're implementing. Anything else — a timeout, a non-2xx status, an oversized body, invalid JSON, or JSON that fails schema validation — is treated identically: as "no result." Streamiva never surfaces your failure as an error to the end user, and never retries within the same request.
- List-valued responses are the exception to whole-response reject-and-drop: each entry in a
subtitle response's
tracks, or a stream response'scandidates, is validated on its own, so one malformed entry doesn't discard the rest of an otherwise valid response.
Because failures are silent by design, test your endpoint directly (curl) rather than relying on
Streamiva's UI to tell you why nothing showed up.
Capabilities
Every capability call is a POST to the endpoint you declared, with a JSON body and a JSON
response. Every request body includes request_id (the opaque, per-install string described
above; personalized rows use a different opaque token — see below).
Row
Endpoint: endpoints.row. Called from the home/movies/TV pages.
Request:
{
"request_id": "b7e2b6b1b1c94e9c9c6a6a6a6a6a6a6a",
"row_id": "trending-horror"
}
row_id echoes the capability's own id field, letting one endpoint serve multiple declared
rows. If the capability is personalized: true, the request also carries the viewer's interaction
history, reusing the same fields as the account interactions model exactly:
{
"request_id": "5e2c...opaque-per-profile-token",
"row_id": "for-you",
"interactions": [
{ "tmdb_id": 550, "media_type": "movie", "watchlisted": true, "watched": false, "rating": "like" }
]
}
For a personalized row, request_id is itself the opaque per-(install, profile) token — generated
once by Streamiva and reused on every subsequent call for that profile, so you can tell repeat
visits from the same profile apart without ever learning Streamiva's real account or profile id.
rating is one of "like", "dislike", "neutral", or null.
Response:
{
"items": [
{ "tmdb_id": 550, "media_type": "movie" },
{ "tmdb_id": 1399, "media_type": "tv" }
]
}
Streamiva resolves each tmdb_id/media_type against its own Catalog and silently drops any it
can't resolve — you can only ever surface titles Streamiva already has.
Caching: a curated row's cache key is the bare row_id, shared across every account and
profile that has your plugin installed. A personalized row's cache key is
{row_id}:{profile_token}, scoped to one profile. Either way, TTL is your behavior_hints
value (15 minutes when unset, at most 7 days).
Viewing Source
Endpoint: endpoints.viewing_source. Called when rendering a title or episode's watch
providers.
Request, movie:
{
"request_id": "b7e2...",
"media_type": "movie",
"tmdb_id": 550,
"imdb_id": "tt0137523",
"title": "Fight Club",
"release_year": 1999
}
Request, TV episode:
{
"request_id": "b7e2...",
"media_type": "tv_episode",
"tmdb_id": 1399,
"imdb_id": "tt0944947",
"title": "Game of Thrones",
"release_year": 2011,
"season_number": 1,
"episode_number": 1,
"episode_title": "Winter Is Coming",
"air_date": "2011-04-17"
}
imdb_id may be null if Streamiva's Catalog doesn't have one for this title.
title and episode_title are in English, whatever language the viewer is browsing in. Your
answer is cached by tmdb_id alone and served to every viewer, so the request never depends on
who asked. A title the Catalog has no English name for comes in Russian, else Hebrew, else as
"".
Response, a match:
{ "match": true, "url": "https://example.com/watch/fight-club", "label": "Watch on Example" }
Response, no match:
{ "match": false }
A match appends one extra watch-provider entry (tagged as third-party in the UI, linking to your
url) after Streamiva's own native providers. match: false — or a timeout, or a malformed
response — contributes nothing; it's never shown as an error.
Caching: cache key is the bare tmdb_id for a movie, or {tmdb_id}:{season_number}:{episode_number}
for an episode. TTL is your behavior_hints value (15 minutes when unset, at most 7 days).
Subtitle
Endpoint: endpoints.subtitle. Called when a signed-in viewer opens the player on a title
or an episode, so the subtitle menu can list your tracks next to the ones embedded in the file.
Uses the exact same request body as Viewing Source (same movie/episode identifying fields above,
same request_id), plus two optional fields describing the release being played, when known:
release_name: the name of the release the viewer picked (e.g."Fight.Club.1999.1080p.BluRay.x264-CtrlHD"), so you can look for a subtitle cut for that very file.infohash: the release's BitTorrent infohash, lowercase hex, when the release is a torrent.
Response:
{
"tracks": [
{ "language": "en", "format": "srt", "url": "https://example.com/subs/en.srt", "label": "English",
"release_name": "Fight.Club.1999.1080p.BluRay.x264-CtrlHD", "hearing_impaired": false },
{ "language": "he", "format": "srt", "url": "https://example.com/subs/he.srt" }
]
}
language: a language code/name string, shown to the user (e.g."en").format: one of"srt","vtt","ass".url: where the track file itself lives. Streamiva's players load it as an in-player track, so the file must be fetchable from the viewer's browser: the response must carryAccess-Control-Allow-Origin: *and a content type withcharset=utf-8(application/x-subrip; charset=utf-8for SRT), and the bytes must be UTF-8. A file host you do not control rarely does either, so serve the files yourself, as the reference Subtitle Vault does.label: optional display label; falls back tolanguagein the UI if omitted.release_name: optional, the release this track was cut for. Streamiva uses it to rank a track that fits the release being played first.hearing_impaired: optional,truefor an SDH track. Streamiva lists those after plain ones.download_count: optional, how often the source has seen the track downloaded. Among tracks of one language that fit the playing release equally, the most downloaded is listed first.
An empty tracks: [], a timeout, or a malformed top-level response contributes nothing. As with
stream candidates, and unlike the other capabilities, each entry in tracks is validated on
its own — one bad track is dropped,
the rest of the list still renders.
Caching: same cache-key formula as Viewing Source (bare tmdb_id for a movie,
{tmdb_id}:{season_number}:{episode_number} for an episode), same TTL rule.
Stream
Endpoint: endpoints.stream. Called only when a signed-in viewer asks where a movie or
episode can be played from. It is never called while a page renders. A stream plugin says
which releases of a title exist and where to get them. Streamiva then plays the one the viewer
picks, in its own player. Your plugin carries metadata only and is never in the media path.
A worked example, a small plugin you can run, is in TUTORIAL-STREAM.md.
A manifest for a stream-only plugin:
{
"id": "yourname.open-films",
"name": "Open Films",
"version": "1.0.0",
"description": "Plays openly licensed films hosted by the Internet Archive.",
"developer": "yourname",
"manifest_url": "https://your-server/manifest.json",
"capabilities": [
{ "type": "stream", "media_types": ["movie", "tv_episode"] }
],
"endpoints": {
"row": null,
"viewing_source": null,
"subtitle": null,
"stream": "https://your-server/stream"
},
"config_schema": [],
"behavior_hints": {
"cache_max_age_seconds": 900
}
}
Request
The request body is the same as for Viewing Source, and it is posted to your
stream endpoint. Points that matter more here:
An episode request carries the series' ids. For a
tv_episode,tmdb_id,imdb_id,titleandrelease_yearbelong to the series.release_yearis the year the series began.season_numberandepisode_numberpick out the episode.episode_titleandair_datecome along too, and either may be empty ornull. Sources index TV by series plus season and episode, so this is the shape you query with.imdb_idis oftennull. About half of Streamiva's Catalog has no imdb id, so searching bytitle+release_yearis the main path, not a fallback you can skip.release_yearcan benulltoo. Decide what a title with no year matches. One safe rule is to accept a title-only match when exactly one entry matches.titleis a search hint, not an exact key. It is the English title whatever the viewer's language. When the Catalog has no English title it is the Russian one, else the Hebrew one, else an empty string. Match it loosely, and don't strip non-Latin letters. A normalizer like[^a-z0-9]turns every Hebrew or Russian title into"", so it matches nothing, or matches every other non-Latin title. To search English release names for a title that came in Russian or Hebrew, look up its original title bytmdb_idwith TMDB's own API.What each field holds:
Field Movie Episode media_type"movie""tv_episode"tmdb_idint int, the series' imdb_idstring or nullstring or null, the series'titlestring, can be ""string, can be "", the series'release_yearint or nullint or null, the year the series beganseason_number,episode_numbernot sent int. Season 0holds specials.episode_titlenot sent string, ""ornullair_datenot sent "YYYY-MM-DD"ornullpreferred_audio_languageoptional, see below optional, see below A partial answer's re-fetch sends the same body,
request_idincluded.preferred_audio_languageis a recall hint. When the viewer's language names an audio language they want first, the request carries it as a lowercase ISO 639-1 code:"ru"for a Russian screen,"en"for an English one, and the title's original language for any other. It is absent when there is no preference, so treat a missing field as "no hint", never as English. Use it to look where that language's dubs are, for example to search a Russian tracker first for"ru", or to keep those releases inside your 50. Don't filter by it and don't reorder for it: Streamiva ranks the merged list itself, and a release in the preferred language already ranks above seeders. Set each candidate'slanguageoraudiofrom what the release states, never from the hint.Nothing identifies the viewer. You get the opaque per-install
request_idand the title's identifying fields. You get no account or profile id, no email address and no IP address, because Streamiva calls you from its own servers. You get no Streamiva credentials and noconfig_schemavalues either.
Response
{
"candidates": [
{
"kind": "magnet",
"infohash": "0e876ce2a1a504f849ca72a5e2bc07347b3bc957",
"label": "Big Buck Bunny (2008) 720p AVI",
"source": "example-tracker",
"seeders": 412,
"size_bytes": 332243668,
"container": "avi",
"resolution": "720p"
},
{
"kind": "torrent",
"url": "https://archive.org/download/BigBuckBunny_124/BigBuckBunny_124_archive.torrent",
"label": "Big Buck Bunny (2008) Internet Archive",
"source": "archive.org"
},
{
"kind": "direct",
"url": "https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4",
"label": "Big Buck Bunny (2008) 720p",
"source": "archive.org",
"size_bytes": 61878609,
"language": "en",
"container": "MP4",
"video_codec": "H.264",
"audio_codec": "AC3 5.1",
"resolution": "720p",
"audio": [
{ "codec": "ac3", "language": "en", "channels": 5.1 },
{ "codec": "aac", "language": "fr" }
]
}
],
"complete": true
}
Candidate kinds
Each candidate has exactly one identifier, chosen by its kind:
kind |
Identifier | What Streamiva does with it |
|---|---|---|
magnet |
infohash: 40 hex characters, either case. A full magnet URI, or the 32-character base32 form, is also accepted in infohash and reduced to hex. A magnet URI in url drops the candidate. |
Its Torrent Streamer joins the swarm with Streamiva's own trackers. Any trackers or web seeds in a magnet URI are discarded, and the URI goes no further. |
torrent |
url of a .torrent file |
The backend fetches the file as soon as your answer arrives, under the url rules. It reduces the file to its infohash, and from then on the candidate is a magnet. Trackers inside the file are discarded. |
direct |
url of a video file |
The Torrent Streamer relays it when the viewer presses play, under the same url rules. It requests the bytes with Range, a few seconds of video at a time. |
- A
magnetcarriesinfohashand nourl. Atorrentordirectcarriesurland noinfohash. A candidate with both, or with neither, is dropped. hlsis reserved and isn't accepted yet. Anhlscandidate is dropped, and so is any other kind. Don't build onhlsuntil this page lists it.- There is no "link out" kind. To send viewers to another site, use a Viewing Source.
Candidate fields
| Field | Type | Notes |
|---|---|---|
kind |
string | "magnet", "torrent" or "direct". Required. |
infohash |
string | magnet only. See the table above. |
url |
string | torrent and direct only. http:// or https://. Plain http:// is allowed here, unlike your own endpoints, which must be https://. |
label |
string? | The release name as you found it. Shown to the viewer as-is, and parsed by Streamiva (see Merging). |
source |
string? | A short name for where this came from ("yts", "eztv", …), so a viewer can tell two near-identical releases apart. |
seeders |
int? | The single most useful optional field: the best predictor of whether a torrent will play at all. |
size_bytes |
int? | The whole torrent's size, as your source lists it, including for a season pack. |
language |
string? | The release's main spoken language. It is shown to the viewer as you send it and isn't normalized, so "en" and "English" are not matched as one. Prefer an ISO 639-1 code ("en", "he"). |
container, video_codec, audio_codec, resolution |
string? | Free strings that Streamiva normalizes; see below. They carry more weight than they look like they do. A TV may decode one release and not the one beside it, and the viewer needs these fields to choose. |
audio |
list? | Every audio track of the release, the default one included (audio_codec and language repeat the default track). Each track is {codec, language, channels?}. codec and language are required. channels is a number such as 2, 5.1 or 7.1, and a numeric string such as "5.1" is accepted. A malformed track (no language, or "channels": "2ch") is dropped on its own; it does not take the candidate with it. |
file_index |
int? | The 0-based position of the file to play in a multi-file torrent. Today it is only a hint, and you can leave it out. Streamiva makes its own choice: the largest video for a movie, and the file whose name states the episode for an episode. |
Integers must not be negative.
Free strings are normalized, never rejected. Send what the release says, in any spelling.
Streamiva lowercases the value and ignores spaces, dots, hyphens and underscores before matching
it against a short alias list, so "H.264", "x264" and "AVC" are all h264. This is the
whole list:
| Field | Value | Also matched |
|---|---|---|
container |
mp4 |
m4v |
mkv |
matroska |
|
webm, avi, mov, wmv, m2ts |
||
ts |
mpegts |
|
video_codec |
h264 |
x264, avc, avc1 |
hevc |
h265, x265, hvc1, hev1 |
|
mpeg4 |
xvid, divx |
|
av1, vp9, vp8, mpeg2 |
||
audio_codec (and audio[].codec) |
aac |
aac lc, he-aac |
ac3 |
dd, dolby digital |
|
eac3 |
e-ac-3, ddp, dd+, ddplus, dolby digital plus |
|
dts |
dts lossy |
|
dts-hd |
dts-hd ma, dts-ma, dts lossless |
|
pcm |
lpcm |
|
truehd, flac, mp3, opus, vorbis |
||
resolution |
2160p, 1440p, 1080p, 720p, 576p, 480p, 360p, 240p |
The number alone (720) or with i (1080i). 4K and UHD count as 2160p, and FHD as 1080p. |
A value it doesn't recognize becomes "unknown", and the candidate is kept and shown. Anything
added to a value counts as part of it, so these all become "unknown":
- a channel count (
"AC3 5.1","DD+ 7.1","AAC2.0"): put it in anaudiotrack'schannelsfield instead; - a bit depth or a profile (
"HEVC 10bit","VP9 Profile 2"); - a bitrate (
"MP3 128kbps"); - a frame size or a vague label (
"1280x720","SD").
Send the bare codec, container or resolution when you can tell it. A field you leave out stays empty; it is never guessed.
Each candidate is validated on its own. One that fails costs only itself: a row that isn't
an object, an unknown kind, a bad infohash, or a negative size. The rest of your answer
survives. Unknown fields are ignored, so you can add fields without breaking an older
Streamiva. There is no version field.
URLs are fetched by Streamiva, never by a viewer
No url you return ever reaches a viewer's app. Streamiva never redirects a viewer to your host, and a viewer's IP address never reaches it. What that means for you:
There are no headers. A candidate has no field for request headers. Streamiva sends only its own minimal ones: a
User-Agent,Accept-Encoding: identity, andRangefordirect. No cookies and nothing of the viewer's are sent. If your host needs authorization, put it in the url, for example as a signed query string that expires.Every fetch follows the same rules, whichever plugin it came from. There is no trusted tier.
- Only
httpandhttpsare allowed. - Every address the host resolves to must be public. Private, loopback, link-local and multicast addresses are refused. The check runs after DNS resolution, so a public name pointing at a private address is refused too.
- Each redirect hop is checked again, and there can be at most 3.
- A compressed response is refused.
- Only
A
.torrenturl is fetched while your answer is being processed.- All of one answer's
.torrentfiles share a 1.5-second limit. One that isn't in by then is dropped, and the rest of the answer stands. - Each file must be at most 4 MB and list at most 10,000 files. Only v1 (BitTorrent 1)
.torrentfiles are read. - Only the first 50
.torrenturls in an answer are fetched.
- All of one answer's
A
directurl is probed with one single-byteRangerequest when the viewer picks it.- It must report its size, through
Content-RangeorContent-Length. - It must serve a
video/*type,application/octet-stream,application/mp4orapplication/x-matroska. - It must answer within 10 seconds, and the file can be at most 200 GiB.
- It should honour
Range, because that is how a viewer seeks. A host that ignoresRangestill plays, but at a cost.
- It must report its size, through
A refused url costs only its own candidate, and nothing tells you about it. Streamiva has no endpoint that checks a url for you, so check your urls from outside your own network before you list the plugin. This request asks the way Streamiva's probe does:
curl -sS -o /dev/null -D - --max-redirs 3 -L -r 0-0 -H 'Accept-Encoding: identity' 'https://your-host/film.mp4'The final response should be a
206(or a200) with aContent-RangeorContent-Length, an acceptedContent-Type, and noContent-Encoding. For a.torrenturl, leave out-r 0-0: the file must come back with noContent-Encoding, within the size limit. ItsContent-Typeisn't checked.
Limits
The trust boundary baseline applies: a 3-second ceiling with no retries, and a 256 KB cap on the response body. On top of that:
- At most 50 candidates. Streamiva caps the list at 50 after parsing and merging, so anything beyond that is cut. Send your best 50, best first.
- "Best" is yours to decide. Streamiva keeps your order and never re-ranks it. Some things
to weigh:
- Only torrents have
seeders. A plain sort by seeders puts everydirectandtorrentcandidate below even a one-seeder magnet. Give them a rank of their own, for example by how reliable their host has been. - For an episode, put a release of exactly that episode above a season pack. A pack plays only when one of its files names the episode (see Season packs).
- Only torrents have
- Answer well inside the 3 seconds. The reference plugin answers at 0.8 s with whatever
its sources have returned, and finishes the rest in the background (see
completebelow).
complete: saying "more is coming"
complete is optional, and leaving it out means true. A value other than true or
false also counts as absent. A plugin that asks one fast source never has to think about it.
A plugin that asks several sources can answer early with "complete": false, then keep going.
Here is what happens next:
- Streamiva shows the viewer what you sent, and tells the viewer's app to fetch again once, about 2.5 s after the app received the partial list. Counting your 0.8 s answer, the re-fetch reaches you roughly 3.3 s after the first request.
- That second request reaches you, with the same body. Streamiva keeps a partial answer for 45 s at most, and never serves it in place of asking you again. It uses that copy only if you fail to answer the second time.
- So by then your own cache should hold the full list, marked complete. If the second answer is partial too, the viewer sees it as it is, and there is no third fetch. Sources still running later than that serve the next viewer instead.
TUTORIAL-STREAM.md shows this pattern in about 60 lines.
Merging with other plugins
A viewer may have several stream plugins installed. Their answers are merged into one list:
- Order: the viewer's install order, then each plugin's own order. So put your best guess first, and don't assume something downstream will re-rank it.
- Duplicates: candidates naming the same torrent (the same infohash) become one row. It keeps
the position of the first copy and the copy with the most seeders. Two
directortorrentcandidates with the same url are one row as well. This applies inside your own answer too, and the 50 cap counts rows after duplicates are merged. - Contradictions: Streamiva parses each
labelwith a release-name parser, the same kind torrent sites use, and drops a candidate whose name proves it is something else.- For a movie, that is a year more than one year away from
release_year. The parser takes the last year in the name, soBlade Runner 2049 (2017)and1917.2019.1080pare read as 2017 and 2019. With no release year after it, a year in the title is taken as the year:Blade Runner 2049 1080pis read as 2049 and dropped. So keep the release year in the label, after the title, as release names do. A name that says it is not the film is dropped too, for exampleThe.Odyssey.2026.720p.TUBI.WEB-DL.AAC2.0-NOtLAN (NOT The Chris Nolan FILM). - For an episode, it is a different season, or a different episode within the stated season.
S01E01,1x01,Season 1,S02.COMPLETE,S01E01E02,S01E01-E03andS01-S03are all understood. A multi-season pack names every season in its range, soS01-S03is kept for a season-2 request. - A label that states nothing is kept, for example
Complete Series. So is an absolute-numbered episode, which has no season.
- For a movie, that is a year more than one year away from
- Season packs: a pack can answer an episode request. When the viewer picks it, Streamiva looks for a file whose name states that episode. If there is none, that candidate is refused and the viewer moves on to the next one.
Caching
The cache key is tmdb_id for a movie, or {tmdb_id}:{season_number}:{episode_number} for an
episode. imdb_id is never part of it. A request with preferred_audio_language adds
:audio={code}, so an answer shaped by one hint is never served to a request with another, or
with none.
| Your answer | How long Streamiva keeps it |
|---|---|
| Complete, including a complete empty answer | Your cache_max_age_seconds. A missing value means 15 minutes, and the ceiling is 7 days. |
complete: false |
45 s at most, and only as a stand-in (see above). |
| A timeout, an error status or an invalid body | Not kept. You'll be asked again on the next request. |
An empty answer is a valid answer. {"candidates": []} means "nothing found", and it is
kept as long as any other complete answer.
So don't say "complete" when your own code failed. The usual advice is to answer every
failure with a 200 and an empty body. For stream, a catch-all that returns
{"candidates": [], "complete": true} turns a bug or a down source into "nothing exists",
cached for your full TTL. Answer your own failures with {"candidates": [], "complete": false}
instead. That is still a 200 with a valid body, and Streamiva keeps it for 45 s at most. A
source that fails while others succeed costs only itself, so that answer can still be complete.
Take the TTL seriously. A stream plugin usually queries live third-party sites on every miss. The cache is the only thing standing between a popular title and a burst of identical searches leaving one IP address, both Streamiva's and yours. Declare 900 or more. Streamiva doesn't enforce a minimum today, but don't count on that staying true.
Failure semantics, summarized
Nothing your server does can raise a visible error in Streamiva. A timeout, a 500, an oversized body, invalid JSON, or a response that fails schema validation are all treated identically: no row, no viewing source, no subtitles, no stream candidates from that plugin for that request. Debug by calling your own endpoint directly, not by watching Streamiva's UI for an error message.