Turns a TMDB movie or TV episode ID into HLS .m3u8 URLs from vidup.to.
Vidup does not expose streams through a simple API — playback depends on client-side code that negotiates with their servers and decrypts the response. This project runs that same client logic in Node to produce CDN manifest URLs for every available host on a title.
resolveStreamLive in src/resolve/resolver.js drives the pipeline.
flowchart TB
subgraph paths [src/resolve/paths.js]
IN[contentPath or type + id] --> CP["/movie/{id} or /tv/{id}/{s}/{e}"]
end
subgraph page [Page fetch]
CP --> GET["GET {ORIGIN}{contentPath}"]
GET --> EN[parsePageToken → en]
end
subgraph vm [src/vidup-vm]
EN --> SB[createVidupSandbox]
SB --> CH[sliceVidupChunk + VM_PRELUDE + VM_RUNNER]
CH --> RS[vm.runServers]
RS --> W[8 workers × vm.runStream]
end
W --> URL[streamUrl per host]
URL --> EVT[status · found · ready · fail · done]
GET -.-> VIDUP[(vidup.to)]
W -.-> VIDUP
src/resolve/paths.js turns your input into the path vidup.to uses for that title.
For a movie, pass the TMDB ID — the path becomes /movie/{id}. For TV, pass ID, season, and episode — the path becomes /tv/{id}/{season}/{episode}.
You can pass { type, id, season?, episode? }, a ready-made contentPath string, or query parameters through parseStreamRequest.
The resolver requests {ORIGIN}{contentPath} using the USER_AGENT from src/config/defaults.js.
Vidup embeds a short-lived token called en inside the page HTML. parsePageToken gathers the self.__next_f.push([1,"…"]) script chunks Next.js leaves on the page, joins them, unescapes the string, and pulls out "en".
If that token is missing, resolution stops with stage page-token and error en token missing.
createVidupSandbox (src/vidup-vm/sandbox.js) loads vendor/vidup-client-chunk-294.js — the same webpack chunk the site uses — and cuts out only the pieces needed for stream logic (src/vidup-vm/chunk.js).
Those pieces are wrapped with a small browser shim (VM_PRELUDE) and two entry functions (VM_RUNNER): runServers lists hosts, runStream decodes one host. The code runs inside a fake globalThis with stubbed document, navigator, localStorage, and a custom fetch.
That fetch is important: relative URLs are resolved against vidup.to, and every outbound request carries User-Agent, Origin, and Referer pointing at the title page — the same headers the real site sends.
vm.runServers(en) runs the site's oc function with the page token. It returns an array of hosts such as Premier, Zenith, or CineX.
Each host includes a display name, optional description, and a data field — an opaque slug the site uses in the next POST request. Nothing in data is meant to be parsed by hand.
If no hosts come back, resolution stops at stage servers.
The resolver runs eight workers against a shared queue. Each worker picks a host and calls vm.runStream(host.data):
- Build the POST path from the runtime string decoder inside the bundle.
- POST to vidup.to with the method and headers the bundle expects.
- Decrypt the response body and read out the final URL.
When a host succeeds, you get a streamUrl — a direct link to a .m3u8 file on a CDN. When a host fails, that host is skipped; the others keep going.
The first successful host triggers ready so playback can start while the rest are still probing. When all workers finish, done carries the full server list.
resolveStreamLive reports through a callback:
| Event | Meaning |
|---|---|
status |
A phase started — { step, text } for UI progress |
found |
One host decoded — { name, description, streamUrl, ms } |
ready |
First playable result — { ok, streamUrl, servers, source? }; source.title appears when the decode returns a title |
fail |
Nothing worked or an early step broke — { ok: false, stage, error } |
done |
All workers finished — { servers } with every host that succeeded |
The streamUrl in these events is always the raw CDN address. Resolve never rewrites or proxies it.
A streamUrl from resolve points at a third-party CDN, not at vidup.to. Those CDNs only serve data when the request looks like it came from the vidup site — specifically when Referer and Origin match vidup.to.
If your player fetches the .m3u8 and every .ts segment with those headers already set, you can use streamUrl as-is.
If not — for example a browser player that cannot set referer on segment requests — use proxyHls in src/hls/proxy.js. It fetches on your behalf with ORIGIN and USER_AGENT from src/config/defaults.js and returns the bytes.
flowchart TB
URL[streamUrl] --> FETCH[proxyHls + vidup headers]
FETCH --> KIND{response type}
KIND -->|".m3u8"| REW[rewrite media lines in playlist]
KIND -->|binary segment| STRIP[stripPngTs]
REW --> MAN[manifest to consumer]
STRIP --> SEG[TS to consumer]
REW --> FETCH
Manifests — when the fetched file is an HLS playlist, proxyHls walks each line. Comment lines stay as they are. Media lines are rewritten so the player asks the proxy for the next URL instead of hitting the CDN directly. That way every nested playlist and segment inherits the same vidup headers.
Segments — for binary responses, the proxy returns the payload after stripPngTs. Some CDNs wrap MPEG-TS data inside a PNG file; this helper peels off the PNG header and leaves plain transport stream bytes.
Resolve and playback never call each other. Resolve hands off a URL; playback is entirely the consumer's choice.
For educational and research purposes only. Do not use this project to violate terms of service, bypass access controls, or distribute copyrighted material. No liability for misuse.