A Rust reimplementation of the Stremio Service, reverse-engineered from the original server.js and the mobile app.
The Stremio desktop app bundles a local HTTP server (server.js + stremio-runtime) that handles
torrent streaming, subtitle proxying, HLS transcoding, and other media duties. The app talks to it
over localhost:11470 and expects a specific JSON/streaming HTTP contract.
This project reverse-engineers that contract and reimplements the server in Rust, with the goal of
being a drop-in replacement: the desktop shell launches this binary instead of the bundled
Node.js server.js runtime and never knows the difference.
The torrent engine is powered by librqbit rather than the original Node.js torrent-stream.
- The original Node.js server has known stability and performance issues, particularly around uTP peer connectivity.
- A native binary starts faster, uses less memory, and doesn't require bundling a JS runtime.
- Rust's async I/O is a natural fit for the byte-range streaming and concurrent torrent sessions the server needs to handle.
This is a work in progress based on black-box reverse engineering. The HTTP contract was
inferred by reading server.js, the Android APK's API wrapper, and observing live traffic and not
from any official documentation or source access.
- Core torrent streaming and all P0 desktop playback routes are working.
- HLS transcoding, casting, archive sources, and other P1/P2 features are not yet implemented.
- Behaviour may diverge from the original in untested edge cases.
See the endpoint map below for a full breakdown of what is and isn't implemented yet.
This is meant to be used with my Stremio fork,
but can also be used with the original shell. To use it, build this project in release mode
or download a release binary, name it stremio-runtime.exe, and replace the original runtime.
By default the runtime starts in single-user mode and cleans up inactive torrents immediately
when the stream is no longer needed. Pass --multi-user to keep torrents around longer for
shared-machine setups where another user may still want the same stream or torrent data.
This file maps the HTTP contract exposed by the original Stremio streaming server so a new Rust
StremioService can replace it without changing the shell/client first.
Sources used:
server.jscom/stremio/common/api/StreamingServerApi.javacom/stremio/core/types/resource/Stream.java
Legend: ✅ Done ·
- ✅ Binds to
127.0.0.1:11470, increments up to11474on conflict - ✅ Prints
EngineFS server started at http://127.0.0.1:<port>on stdout - ✅ CORS — all origins,
GET/POST/HEAD/OPTIONS,max-age=1728000 - ✅ Reads/writes Stremio
server-settings.json - ✅ Cache reaper honors
cacheSizeand removes inactive cache entries periodically and at startup - ❌ HTTPS endpoint on port
12470
- ✅
GET /favicon.ico - ✅
GET /heartbeat - ✅
GET /settings - ✅
POST /settings - ✅
GET /stats.json—?sys=1loadavg/cpus fields not populated - ✅
GET /removeAll - ✅
ALL /create— hex blob, HTTP URL, and local file path - ✅
ALL /{infoHash}/create— trackers, filters, guessFileIdx, peers - ✅
GET /{infoHash}/stats.json - ✅
GET /{infoHash}/remove - ✅
GET /{infoHash}/{idx}/stats.json - ✅
GET /{infoHash}/{idx}— range, HEAD,external=1,download=1,subtitles=,tr=,f= - ✅
GET /{infoHash}/{idx}/{*filename}— filename-style stream URL - ✅ Streams requested byte ranges while downloading; seek requests reprioritize the active window
- ✅ Multi-file torrents select only the requested/guessed video file when possible
- ✅
GET /subtitles.{ext}— full proxy with SRT/VTT parsing, SRT→VTT conversion, andoffset=<ms> - ✅
GET /opensubHash— computes OpenSubtitles hash fromvideoUrl= - ✅
GET /subtitlesTracks— fetchessubsUrland returns parsed cue tracks - ❌
GET /tracks/:url
⚠️ GET /probe— returns null result; no ffprobe invocation⚠️ GET /hlsv2/probe— returns null result; no ffprobe invocation- ❌
GET /hlsv2/:id/:track.m3u8 - ❌
GET /hlsv2/:id/:track/init.mp4 - ❌
GET /hlsv2/:id/:track/segment:n.:ext - ❌
GET /hlsv2/:id/burn - ❌
GET /hlsv2/status - ❌
GET /hlsv2/:id/destroy - ❌ HLSv2 compat routes (
/:infoHash/:videoId/:playlist) - ❌ Legacy HLS routes (
/:first/:second/hls.m3u8,stream.m3u8, segments, etc.)
- ✅
GET /— 307 redirect to web UI with?streamingServer= - ✅
GET /network-info - ✅
GET /device-info - ✅
GET /hwaccel-profiler ⚠️ GET /get-https— always returns 500 "Cannot get valid certificate"
- ✅
GET /local-addon/manifest.json - ✅
GET /local-addon/meta/other/bt:<infoHash>.json— creates/loads magnet metadata, returns playable videos - ✅ Best-effort Cinemeta/Metahub enrichment for local
bt:metadata - ✅
GET /local-addon/catalog/other/local.json— lists indexed local files when local addon is enabled - ✅
GET /local-addon/meta/other/local:<imdbId>.json— returns local-file meta with playable file videos - ✅
GET /local-addon/stream/{movie|series}/tt*.json— returns local-file, indexed.torrent, and active-torrent streams - ✅ Local indexing scans
appPath/localFilesplus OS-wide discovery (Windows Search,mdfind, orfind) - ✅ Local
.torrentfiles are parsed intobt:catalog/meta/stream entries with tracker sources
⚠️ GET /casting— returns a static VLC entry; no real device discovery- ❌
GET /casting/transcode/GET /casting/convert - ❌
GET /casting/:devID - ❌
ALL /casting/:devID/player
- ✅
ALL /proxy/:opts/:pathname*— HTTP proxy with redirect handling and playlist URL rewriting - ❌
GET /yt/:id.json/GET /yt/:id - ❌
GET /samples/:key.:container - ❌ Archive routes:
/rar,/zip,/7zip,/tar,/tgz - ❌
/nzb/* - ❌
/ftp/*
-
GET /probe— invoke ffprobe, return legacy probe model -
GET /tracks/:url— media track metadata endpoint - Better
/settingsparity for every desktop option value the shell may read - More exact
EngineStatsparity for selections, wires, and source counters
src/main.rs— process bootstrap, shared types, router wiring, and crate-root includes.src/runtime/settings.rs— settings persistence, defaults, cache-size helpers, and interface discovery.src/runtime/local_addon.rs— local addon catalog/meta/stream handling, file discovery, filename parsing, and Cinemeta matching.src/runtime/proxy.rs—/proxyoption parsing, redirect handling, header forwarding, and playlist rewriting.src/runtime/opensub.rs—/opensubHashrange fetching and OpenSubtitles hash calculation.src/runtime/subtitles.rs— subtitle proxying, cue parsing/rendering, offset support, and tracks response.src/runtime/app_routes.rs— small app/service routes such as settings, device info, manifest dispatch, and stubs.src/runtime/torrent_http.rs— torrent-facing HTTP routes: create, stats, remove, and stream responses.src/runtime/torrent_service.rs— torrent service internals, cache reaper, stats shaping, file guessing, and shared request helpers.
-
GET /hlsv2/probe— invoke ffprobe, return HLSv2 format+streams+samples model -
GET /hlsv2/:id/:track.m3u8— fMP4 HLS playlist generation -
GET /hlsv2/:id/:track/init.mp4— fMP4 init segment -
GET /hlsv2/:id/:track/segment:n.:ext— fMP4 media segments and VTT subtitle segments -
GET /hlsv2/:id/burn— embedded subtitle burn-in -
GET /hlsv2/status— converter session map -
GET /hlsv2/:id/destroy— tear down converter session - HLSv2 compat routes —
/:infoHash/:videoId/:playlistrewrite into hlsv2 router - Legacy HLS routes —
hls.m3u8,stream.m3u8,stream-q-*.m3u8,.tssegments,dlna,subs-*.m3u8,thumb.jpg -
GET /casting— real device discovery (Chromecast etc.) -
GET /casting/transcode/GET /casting/convert— ffmpeg transcode stream for casting -
GET /casting/:devID— device detail -
ALL /casting/:devID/player— cast player control
-
GET /+ HTTPS endpoint on port 12470 +GET /get-httpsreturning real cert info -
/rar/*,/zip/*,/7zip/*,/tar/*,/tgz/*— archive create/stream routes -
/nzb/*— NZB create/stream -
/ftp/*— FTP create/stream -
GET /yt/:id.json/GET /yt/:id— YouTube format resolution via yt-dlp -
GET /samples/:key.:container— bundled AV sample files for hwaccel profiling
- Default HTTP listen address is
127.0.0.1:11470. - If port
11470is unavailable, the old server increments up to11474. - Desktop shell expects stdout containing
EngineFS server started at http://127.0.0.1:<port>. - Optional HTTPS endpoint listens on
12470and is advertised by/get-https. - Request bodies are JSON and URL-encoded with an old 3 MB JSON limit.
- CORS is accepted for Stremio origins and localhost. EngineFS also handles preflight
OPTIONS. - Streaming responses must support
HEADwhere Express maps it to theGEThandler.
The Android API wrapper directly uses this subset:
| Method | Path | Inputs | Return |
|---|---|---|---|
GET |
/:infoHash/:fileIdx/stats.json |
infoHash, fileIdx path params. If fileIdx is absent in the stream model, Android sends -1. |
JSON StreamStatistics or null. |
GET |
/opensubHash |
Query videoUrl=<url>. |
JSON `{ "error": string |
GET |
/subtitles.vtt |
Query from=<subtitle-url>, optional offset=<milliseconds>. |
WebVTT text. |
GET |
/subtitles.srt |
Query from=<subtitle-url>, optional offset=<milliseconds>. |
SRT text. |
GET |
Torrent stream URL with external query |
Built from /:infoHash/:fileIdx?external=1. Client disables redirect following and reads Location. |
307 redirect to filename URL. |
Android StreamStatistics expects at least:
{
"infoHash": "hex string",
"peers": 0,
"queued": 0,
"unchoked": 0,
"downloaded": 0,
"downloadSpeed": 0,
"streamProgress": 0,
"streamLen": 0,
"streamName": ""
}The old server returns more fields. Keep extra fields when convenient, but the fields above are the client-critical ones.
The APK Stream source types that map to this service:
| Source | Fields | Server endpoint family |
|---|---|---|
Tramvai |
infoHash: string, `fileIdx: number |
null, announce: string[], fileMustInclude: string[]` |
Url |
url: string |
Usually direct playback, HLS/probe, or proxy helpers. |
External |
`externalUrl: string | null, androidTvUrl: string |
Rar |
rarUrls: ArchiveUrl[], `fileIdx: number |
null, fileMustInclude: string[]` |
Zip |
zipUrls: ArchiveUrl[], `fileIdx: number |
null, fileMustInclude: string[]` |
Zip7 |
zip7Urls: ArchiveUrl[], `fileIdx: number |
null, fileMustInclude: string[]` |
Tar |
tarUrls: ArchiveUrl[], `fileIdx: number |
null, fileMustInclude: string[]` |
Tgz |
tgzUrls: ArchiveUrl[], `fileIdx: number |
null, fileMustInclude: string[]` |
Nzb |
nzbUrl: string, servers: string[] |
/nzb/* |
ArchiveUrl |
url: string, `bytes: number |
null` |
These are mounted at the server root.
Handled by CORS middleware (tower-http CorsLayer).
Request:
- Header
Origin. - Header
Access-Control-Request-Headersoptional.
Response:
200empty body.- Headers:
Access-Control-Allow-Origin: *Access-Control-Allow-Methods: POST, GET, OPTIONSAccess-Control-Allow-Headers: <request header value or Range>Access-Control-Max-Age: 1728000
Response:
404Content-Type: application/json- Empty body.
Response:
200 application/jsonEngineStatsfor the torrent, ornullif not created.
Inputs:
infoHash: torrent infohash.idx: torrent file index.-1is accepted by clients but old stats only adds stream fields whenfiles[idx]exists.
Response:
200 application/jsonEngineStatsplus stream fields whenidxis a valid file index, ornull.
Inputs:
- Query
sys=1optional.⚠️ sys.loadavgandsys.cpusare not yet populated.
Response:
200 application/json- Object keyed by infohash. When
sys=1, includes:
{
"sys": {
"loadavg": [0, 0, 0],
"cpus": []
},
"<infohash>": {}
}Creates or returns a torrent engine for a magnet/infohash.
Request body is JSON. Known fields:
{
"announce": ["tracker-url"],
"fileMustInclude": ["name-fragment", "/regex/flags"],
"guessFileIdx": "video-id-or-name",
"peerSearch": {},
"connections": 55,
"uploads": 10,
"path": "cache-path"
}Behavior:
infoHashis lowercased.- The whole body is passed into the torrent engine as options.
- If files are known and
fileMustIncludehas values, the first matching file index is returned asguessedFileIdx. - If
guessFileIdxis provided and no match was found, server guesses file index from file list.
Response:
200 application/jsonEngineStats.
Creates an engine from a .torrent file rather than just an infohash.
Request body:
{
"blob": "hex-encoded .torrent bytes",
"from": "http://example/torrent-file.torrent or local path"
}Behavior:
- If
blobis a string, parse it as hex. - Else if
fromstarts withhttp, fetch it. - Else read
fromas a local file path. - Parse torrent metadata and create engine with
{ torrent: parsedTorrent }.
Responses:
200 application/jsonwithEngineStats.500empty body on parse/read/fetch error.
Destroys one engine.
Response:
200 application/json- Body
{}.
Destroys all engines.
Response:
200 application/json- Body
{}.
Streams a torrent file.
Inputs:
infoHash: torrent infohash.idx: numeric file index, filename, or-1for guessed selection.- Header
Range: bytes=start-endoptional. - Header
enginefs-prio: <number>optional. - Query
download=1optional. AddsContent-Disposition. - Query
external=1optional. Returns a redirect instead of streaming. - Query
subtitles=<seconds>optional. AddsCaptionInfo.sec. - Query
tr=<source>repeatable. Overrides/extends peer sources. - Query
f=<filter>repeatable. Adds file filters.
Responses:
307withLocation: /:infoHash/:filename[?download=1]whenexternalis present.200full stream when no range.206partial stream whenRangeis valid.HEADreturns headers only.
Common response headers:
Accept-Ranges: bytesContent-Type: <mime from filename>Content-Length: <bytes>Cache-Control: max-age=0, no-cacheContent-Range: bytes start-end/totalfor206transferMode.dlna.org: StreamingcontentFeatures.dlna.org: DLNA.ORG_OP=01;DLNA.ORG_CI=0;...
Same as GET /:infoHash/:idx, but * can carry a filename path for media players that prefer
stable filenames.
The old server shape is:
{
"infoHash": "hex string",
"name": "torrent name",
"peers": 0,
"unchoked": 0,
"queued": 0,
"unique": 0,
"connectionTries": 0,
"swarmPaused": false,
"swarmConnections": 0,
"swarmSize": 0,
"selections": [],
"wires": [
{
"requests": 0,
"address": "host:port",
"amInterested": false,
"isSeeder": false,
"downSpeed": 0,
"upSpeed": 0
}
],
"files": [
{
"path": "path/in/torrent.mkv",
"name": "file.mkv",
"length": 123,
"offset": 0
}
],
"downloaded": 0,
"uploaded": 0,
"downloadSpeed": 0,
"uploadSpeed": 0,
"sources": {},
"peerSearchRunning": false,
"opts": {},
"streamLen": 123,
"streamName": "file.mkv",
"streamProgress": 0,
"guessedFileIdx": 0
}Notes:
wiresisnullwhen a specificidxstats route is used.streamLen,streamName, andstreamProgressare only present for a valididx.uploadSpeedin the old server accidentally mirrorsdownloadSpeed.
Mounted at /hlsv2 unless disabled by environment. Not yet implemented except for the probe stub.
Every route with :id creates or reuses a converter. Converter creation query params:
mediaURL: source media URL, required.maxAudioChannels: integer, optional.forceTranscoding: truthy query param, optional.profile: hardware/transcode profile, optional.maxWidth: optional.videoCodecs: string or repeated query param.audioCodecs: string or repeated query param.
Inputs:
track: usuallyvideo0,audio0,subtitle0, etc.
Response:
200 application/vnd.apple.mpegurl- Playlist body.
500 application/jsonon failure:
{
"error": {
"code": 10,
"message": "Failed to read hls playlist: ..."
}
}Inputs:
trackmust start withvideooraudio.
Response:
200 video/mp4- fMP4 init segment.
500 application/jsonwith error code20on failure.
Inputs:
sequenceNumber: integer.ext:m4sfor video/audio tracks,vttfor subtitle tracks.
Response:
200 video/mp4for.m4s.200 text/vttfor.vtt.500 application/jsonwith error code30on failure.
Inputs:
- Query
url=<subtitle-url>. - Query
id=<subtitle-track-id>.
Response:
200empty body on success.500 application/jsonwith error code40on failure.
Inputs:
- Query
mediaURL=<url>, required. - Query
samples=1optional. Attempts to include MP4 or Matroska samples.
Response:
200 application/json
{
"format": {},
"streams": [],
"samples": {}
}Currently returns { "error": null, "result": null }. Real ffprobe invocation not yet wired.
500 application/jsonon failure. Old code labels the error asPROBE_FAILED.
Response:
200 application/json- Object keyed by converter id:
{
"<id>": {
"status": {},
"touched": "date"
}
}Response:
200empty body.
These are root routes that rewrite to /hlsv2:
GET /:infoHash/:videoId/:playlist
GET /:infoHash/:videoId/:playlist/:HLSSegment
Path constraints:
infoHash: 40 hex chars,file, orurl.playlist:hls.m3u8,videoN.m3u8,audioN.m3u8,subtitleN.m3u8.HLSSegment:init.mp4,segmentN.m4s, orsegmentN.vtt.
Rewrite behavior:
- Internal id is
encodeURIComponent(infoHash + "-" + videoId). hls.m3u8becomesmaster.m3u8.mediaURLis:file://<videoId>wheninfoHash=file<videoId>wheninfoHash=url<baseUrlLocal>/<infoHash>/<videoId>for torrents
maxAudioChannelsdefaults to2.
All use setHLSFrom:
- If query
from=<url>exists, use decodedfrom. - Else if
firstlength is 40, use<baseUrlLocal>/<first>/<second>. - Else if
firstisfileorurl, usesecond.
Routes:
| Method | Path | Return |
|---|---|---|
GET |
/:first/:second/hls.m3u8 |
HLS master playlist. |
GET |
/:first/:second/master.m3u8 |
HLS multi master playlist. |
GET |
/:first/:second/stream.m3u8 |
HLS stream playlist. |
GET |
/:first/:second/stream-q-:quality.m3u8 |
HLS stream playlist for quality. |
GET |
/:first/:second/stream-:stream.m3u8 |
HLS stream playlist for track. |
GET |
/:first/:second/stream-q-:quality/:seg.ts |
MPEG-TS segment. |
GET |
/:first/:second/stream-:stream/:seg.ts |
MPEG-TS segment. |
GET |
/:first/:second/mp4stream-q-:quality.m3u8 |
Optional MP4 HLS playlist. |
GET |
/:first/:second/mp4stream-q-:quality/:seg.mp4 |
Optional MP4 segment. |
GET |
/:first/:second/dlna |
DLNA MPEG-TS stream. |
GET |
/:first/:second/subs-:lang.m3u8 |
Subtitle playlist. |
GET |
/:first/:second/thumb.jpg |
Thumbnail. |
GET |
/thumb.jpg |
Thumbnail, using from query when present. |
Inputs:
- Query
url=<url>. - If the URL does not contain
://, old server prefixesbaseUrlLocal.
Response:
200 application/jsonwith probe result.500 application/jsonon error, but body is stillJSON.stringify(result).
Currently returns { "error": null, "result": null }. Real ffprobe invocation not yet wired.
Typical legacy probe model:
{
"container": "mkv",
"duration": 0,
"bitrate": 0,
"streams": [
{
"codec_type": "video",
"codec_name": "h264",
"size": [1920, 1080],
"stream": 0,
"default": true,
"bitrate": 0,
"fps": 23.976,
"lang": "eng"
}
]
}Inputs:
urlpath param is the media URL.
Response:
200 application/json- Track data array.
- On error, old server returns
200 [].
Inputs:
id: YouTube video id.
Response:
200 application/jsonwith chosen ytdl format if found.403 application/jsonwith{ "err": "message" }on ytdl error.404 application/jsonwith{}if no playable format URL.
Response:
301redirect to chosen format URL.403empty body on ytdl error.404empty body if no playable format URL.
Inputs:
- Query
subsUrl=<url>.
Response:
200 application/jsonor500 application/json.
{
"error": null,
"result": {
"tracks": [
{
"startTime": "date/string",
"endTime": "date/string",
"text": "caption"
}
]
}
}Fetches subsUrl, parses SRT/VTT cues, and returns timestamped tracks.
Inputs:
- Query
videoUrl=<url>.
Response:
200 application/jsonor500 application/json.
{
"error": null,
"result": {
"hash": "opensubtitles hash",
"size": 123
}
}Fetches the first and last 64 KiB of videoUrl with HTTP range requests and computes the standard
OpenSubtitles hash.
Inputs:
ext:vttorsrt.- Query
from=<subtitle-url>, required. - Query
offset=<milliseconds>, optional.
Response:
200 text/plain-ishsubtitle body.500empty body on errors or empty track list.
VTT starts with:
WEBVTT
0
HH:mm:ss.SSS --> HH:mm:ss.SSS
Text
SRT uses:
0
HH:mm:ss,SSS --> HH:mm:ss,SSS
Text
Response:
200 application/json
{
"availableInterfaces": ["192.168.1.10"]
}500 text/plainwith error message.
Response:
200 application/json
{
"availableHardwareAccelerations": []
}500 text/plainwith error message.
Response:
200 application/json
{
"options": {},
"values": {},
"baseUrl": "http://127.0.0.1:11470"
}Request body:
- JSON object with partial setting values.
Response:
200 application/json
{
"success": true
}Response:
200 application/json
{
"success": true
}Always returns 500. Real certificate provisioning is P2.
Inputs:
- Query
ipAddress=<ip>. - Query
authKey=<key>.
Response:
200 application/json
{
"ipAddress": "192.168.1.10",
"domain": "local.strem.io",
"port": 12470
}500 text/plainwith bodyCannot get valid certificate.
Response:
200 application/jsonwith profile array.500 text/plainwith bodyNo viable hardware acceleration profiles detected.
Requires Host header.
Response:
307redirect to configured web UI with querystreamingServer=<encoded current server URL>.
Mounted at /proxy.
Inputs:
opts: querystring encoded in a path segment.pathname: optional target path.- Original query string is forwarded to upstream.
Option keys:
d: destination base URL, required, such ashttps://example.com.h: destination request header override. Repeatable. FormatName:Value.r: response header override. Repeatable. FormatName:Value.
Request headers copied upstream:
acceptaccept-encodingaccept-languageconnectiontransfer-encodingrangeif-rangeuser-agent
Response headers copied back:
accept-rangescontent-typecontent-lengthcontent-rangeconnectiontransfer-encodinglast-modifiedetagserverdate
Behavior:
- Follows up to 5 redirects.
- Uses a proxy-specific HTTP client with automatic redirects disabled and invalid TLS certificates/hostnames accepted, matching the legacy proxy behavior.
- For
.m3u,.m3u8, or MPEGURL content, removescontent-length, setsaccept-ranges: none, ensures chunked transfer, and rewrites playlist URLs through/proxy.
Response:
- Upstream status and body, possibly rewritten for playlists.
Mounted at /local-addon.
Response:
200 application/json; charset=utf-8- Addon manifest JSON.
Inputs:
resource: one of the registered addon resources, typicallycatalog,meta,stream, orsubtitles.type: media type.id: resource id.extra: optional querystring-like segment parsed into an object.
Response:
200 application/json; charset=utf-8with handler result.500with{ "err": "handler error" }on handler error.catalog/other/localindexes enabled local addon files fromappPath/localFilesand OS discovery.meta/other/local:<imdbId>returns local-file metadata andfile://streams.meta/other/bt:<infoHash>returns torrent metadata from an indexed.torrentwhen present, otherwise creates/loads the magnet.stream/{movie|series}/tt...returns matching local files, indexed.torrentstreams, and active torrent streams.
The old server registers one route for each bundled AV sample:
GET /samples/:key.:container
Response:
200Content-Typefrom sample metadata.- Binary sample body.
These are mainly used by /hwaccel-profiler to test HLSv2 hardware acceleration.
Archive routes share a common create/stream pattern. They are mounted at:
/rar/zip/7zip/tar/tgz
Archive create body accepts arrays of either objects or tuples:
[
{ "url": "https://host/file.r00", "bytes": 123 },
["https://host/file.r01", 456],
"https://host/file.r02"
]For non-POST create, the old server expects ?lz=<lz-string encoded json> where decoded JSON is:
{
"urls": [{ "url": "https://host/file.rar", "bytes": 123 }],
"fileMustInclude": ["mkv"],
"maxFiles": 20,
"fileIdx": 0
}The generated key is sha256(lz) for lz create.
Response:
- POST:
200 application/jsonwith{ "key": "<key>" }. - Non-POST:
302 Location: /rar/stream?key=<sha256>&o=<encoded-options>. 500 text/plainon malformed data.
Inputs:
- Query
key=<key>, required unless direct URL query is supported by parser. - Query
o=<json options>, optional. - Header
Rangeoptional.
Options:
{
"fileMustInclude": ["mkv"],
"maxFiles": 20,
"fileIdx": 0
}Response:
204headers only forHEAD.200full stream.206range stream.500on parser/key errors.
Same create contract as /rar.
Stream behavior:
- Supports range for uncompressed entries by mapping the inner file offset.
- If the zip entry is compressed, only full-stream or
bytes=0-style access works. - Bad unsupported range can return
405. - Invalid range can return
416.
Same as /rar, using 7zip parser.
Same create contract as /rar.
Stream behavior:
- Supports byte ranges by mapping tar entry offset.
- Invalid range can return
416.
Same create contract as /rar.
Stream behavior:
- Usually non-seekable.
Accept-Ranges: none.- Only
bytes=0-or full-file style ranges are accepted. - Other ranges return
405.
Mounted at /nzb.
POST body:
{
"servers": ["nntp://user:pass@host:119/20"],
"nzbUrl": "https://host/file.nzb",
"nzbUrls": ["https://host/file1.nzb", "https://host/file2.nzb"]
}Non-POST query:
lz=<lz-string encoded json>, decoded shape is the same as POST body.
Behavior:
serversmust be a non-empty array.- Either
nzbUrlornzbUrlsis required. - Multiple NZB URLs are tried in chunks of five.
- The server detects direct video files or archive sets inside NZB metadata.
- If an archive is detected, it internally creates the matching archive key and redirects there.
Responses:
- POST success:
200 application/jsonwith{ "key": "<key>" }. - Non-POST success:
302 Location: <stream path>. 500 text/plainon malformed data or failed NZB checks.
Streams one file from the initialized NZB session.
Inputs:
- Header
Rangeoptional.
Response:
200or206stream.500on key/file/session error.
Inputs:
- Query
key=<key>.
Response:
- Redirects to the initialized stream path.
500if key has no stream.
Mounted at /ftp.
POST body:
{
"ftpUrl": "ftp://user:pass@host:21/path/file.mkv"
}Non-POST query:
lz=<lz-string encoded json>, decoded shape:
{
"ftpUrl": "ftp://user:pass@host:21/path/file.mkv"
}Behavior:
- Supports
ftpandftps. - Checks last modified, size, MIME, and FTP
RESTsupport. - Creates a stream URL
/ftp/stream/:key/:filename.
Responses:
- POST success:
200 application/jsonwith{ "key": "<key>" }. - Non-POST success:
302 Location: /ftp/stream/:key/:filename. 500on malformed data or connection error.
Inputs:
- Header
Rangeoptional.
Response:
200full stream.206range stream.405if range is requested but the FTP server does not support seeking, except fullbytes=0-.HEADreturns headers only.
Inputs:
- Query
key=<key>.
Response:
- Redirects to initialized
/ftp/stream/:key/:fileName. 500on missing key.
Mounted at /casting/ on non-Android platforms when casting is enabled.
Response:
200 application/json; charset=utf8- Array of discovered devices.
Currently returns a static VLC stub. Real device discovery not yet implemented.
Inputs:
- Query
video=<url>, required. - Query
fmp4=1, optional. Uses fragmented MP4 instead of MKV. - Query
audioTrack=<id>, optional. - Query
time=<seconds>, optional. - Query
subtitles=<url>, optional. - Query
subtitlesDelay=<seconds>, optional. - Query
flagRe=1, optional ffmpeg pacing flag. - Header
getmediainfo.secoptional. AddsMediaInfo.sec.
Response:
200chunked stream.400bodyprovide ?videowhen missingvideo.
Response:
200 application/json; charset=utf8with device object.404 text/plainbodyDevice not found.
Inputs can be query params or request body:
formats: callsprotocolsGet.audioTrack=<id>: switch audio.volume=<number>: set volume.time=<milliseconds>: seek.subtitlesSrc=<url>: set subtitles.subtitlesDelay=<milliseconds>: set subtitle delay.subtitlesSize=<number>: set subtitle font size.source=<url>: play source. Emptysourcecloses player.stop: stop.paused: truthy pauses, falsy resumes.
Response:
200 application/json; charset=utf8with media status or{}.
Media status shape:
{
"audio": [],
"audioTrack": null,
"volume": 100,
"time": 0,
"paused": false,
"state": 5,
"length": 0,
"source": null,
"subtitlesSrc": null,
"subtitlesDelay": 0,
"subtitlesSize": 2
}Route order matters in the old server:
- HLSv2 compatibility routes are registered before legacy HLS routes.
- Torrent routes are also root-level dynamic routes. Static routes like
/settings,/heartbeat,/opensubHash,/proxy,/local-addon,/hlsv2, and archive mounts must win over torrent/:infoHash/:idx. /:infoHash/:idx/*is used by filename-style stream URLs, especially afterexternal=1.
For Rust, use explicit static routes first, constrained regex routes second, and torrent catch-all routes last.
- HTTP layer:
axumoractix-web. - Torrent backend: prefer a mature libtorrent binding/process boundary over reimplementing BitTorrent and uTP. The old JavaScript failure mode is mostly in uTP/peer connectivity, not the HTTP API.
- Keep the HTTP endpoint JSON stable while swapping internals.
- Start with P0 routes and return realistic stub values for P1/P2 only if the current client does not call them.