Skip to content

sharoon7171/ployan-hls-stream-resolver

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 

Repository files navigation

Ployan HLS Stream Resolver

Node.js implementation of the Ployan embed → HLS handshake. Zero npm runtime dependencies.

Example embed page: flixhqz.com/movie/scream-7-1630860919/

Table of contents

Introduction

A watch page like https://flixhqz.com/movie/scream-7-1630860919/ is not the stream. It is a shell — title, layout, mediaId, and JavaScript that mounts the Ployan player. The HLS playlist URL never appears in the HTML. The player fetches it from a separate API host after passing an encrypted session token.

Three origins are involved:

Layer Role
Embed site (flixhqz.com) Serves HTML, exposes mediaId
Player API (from plyURL) Validates token, returns info
CDN (inside M3U8 responses) Serves .ts HLS segments
flowchart LR
  A[Embed URL] --> B[Scrape HTML]
  B --> C[Decode plyURL]
  B --> D[Read mediaId]
  C --> E[Seal token]
  D --> E
  E --> F["GET /get/{token}"]
  F --> G[master.m3u8 URL]
  G --> H[VLC / MPV / Proxy]
Loading

This repository reproduces that chain server-side and exposes it via GET /api/stream, GET /api/proxy, and a browser UI.

What gets decrypted

Ployan hides two values. Both get called “decryption” in writeups — they are different operations.

plyURL Session token
Transform Base64 decode AES-256-GCM seal
Hidden value Player API hostname {mediaId}+{episode}+1+{timestamp}
Direction Obfuscated string → https://… origin Plaintext → hex blob in /get/{token}
Who reverses it Anyone with the HTML Ployan server decrypts; this repo re-seals

Not decrypted anywhere in this project: /get JSON responses, M3U8 playlist text, or video segments.

How decryption works

Decode plyURL

The player API host is stored as base64 in page JavaScript:

const plyURL = "aHR0cHM6Ly9wbGF5ZXIuZXhhbXBsZS8=";

Obfuscation only — no key, no salt.

const PLY_RX = /const\s+plyURL\s*=\s*["']([A-Za-z0-9+/=]+)["']/;

Match → pad → Buffer.from(…, "base64") → strip trailing slash → origin.

Seal the session token

The Ployan client encrypts a payload before GET {origin}/get/{token}. This repo runs the same seal in src/ployan/hls.js.

Plaintext

{mediaId}+{episode}+1+{unixTimestamp}
Field Value
episode Defaults to 1
middle 1 Hardcoded server slot
unixTimestamp Math.floor(Date.now() / 1000) — stale tokens are rejected

PBKDF2-SHA256

Parameter Value
Password "player"
Salt 8 random bytes
Iterations 1000
Key length 32 bytes

AES-256-GCM

Parameter Value
IV 12 random bytes
Auth tag 16 bytes, appended to ciphertext

Wire format: {saltHex}-{ivHex}-{ciphertextHex}{tagHex}

function seal(plain) {
  const salt = randomBytes(8);
  const key = pbkdf2Sync("player", salt, 1000, 32, "sha256");
  const iv = randomBytes(12);
  const cipher = createCipheriv("aes-256-gcm", key, iv);
  const body = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
  return `${salt.toString("hex")}-${iv.toString("hex")}-${body.toString("hex")}${cipher.getAuthTag().toString("hex")}`;
}

function token(mediaId, episode) {
  return seal(`${mediaId}+${episode}+1+${Math.floor(Date.now() / 1000)}`);
}

The Ployan server splits on -, re-derives the key from salt + "player", decrypts, verifies the GCM tag, and validates the fields. Generate a new token per resolve.

How the stream is resolved

src/route/stream.js orchestrates:

scrape(url) → hls({ origin, mediaId, episode }) → { title, url, mode }

Scrape (src/embed/scrape.js) — one GET on the embed URL with Referer: {page-origin}/ and headers from src/env.js:

Field Source
mediaId #mid / #watch-block data-id, or URL -{digits} suffix
origin Decoded plyURL
title <title> before |

/get (src/ployan/hls.js) — sealed token from the previous section:

GET {origin}/get/{token}
Referer: {origin}/

M3U8 URL — when response has mode: "direct" and info:

{origin}/hls/{info}/master.m3u8

From there, standard HLS: master.m3u8 → media playlist → .ts segments on a CDN. VLC, MPV, and Safari handle that chain natively. This repo stops at the master URL. Other mode values return no playlist (url: null).

Why it cannot play directly in the browser

The resolved M3U8 URL works in VLC and MPV without this server. In-browser playback does not.

HLS support — only Safari plays HLS natively. Chrome and Firefox need hls.js (public/ui.js).

Cross-origin — M3U8 and .ts segments live on the player CDN, not on the UI origin. Browsers block or restrict those fetches (CORS, missing upstream headers).

How the proxy works

src/route/proxy.js serves GET /api/proxy?url={absolute-url}.

  1. Fetch upstream with Referer: {upstream-origin}/.
  2. If response is M3U8 (Content-Type, .m3u8 path, or #EXTM3U header):
    • Rewrite media lines and URI="…" tags to {host}/api/proxy?url={encoded-upstream}.
  3. Otherwise pass binary bytes through unchanged (.ts, .m4s, keys).

The browser only talks to your host. The UI plays through the proxied URL; external players can use the direct M3U8 from /api/stream instead.

Stack

Role Technology
plyURL decode Buffer base64
Token seal node:cryptorandomBytes, pbkdf2Sync, createCipheriv("aes-256-gcm")
Runtime Node.js ≥ 22, ES modules
HTTP node:http, native fetch
Browser HLS (UI) hls.js 1.5.20 from jsDelivr
Variable Default Purpose
PORT 3000 Listen port
HOST all interfaces Bind when set
USER_AGENT Chrome Android mobile Upstream identity

npm start

Code map

File Export
src/server.js HTTP entry
src/env.js headers(), port
src/net/fetch.js text(), json(), bytes()
src/embed/scrape.js scrape()
src/ployan/hls.js hls()
src/route/stream.js onStream()
src/route/proxy.js onProxy()
public/ui.js UI playback

REST API

GET /api/stream

Query Required Description
url yes Embed page URL
episode no Token payload episode (default 1)

200

{
  "title": "Movie Title",
  "url": "https://player.example/hls/abc123/master.m3u8",
  "mode": "direct"
}

Missing url query → 400. Upstream failure → 502 { "error": "…" }. CORS: *.

GET /api/proxy

Query Required Description
url yes Absolute upstream URL (M3U8 or segment)

Returns rewritten M3U8 or raw bytes.

Disclaimer

This project does not host, store, or distribute media. Embed sites and Ployan player hosts are independent services. The resolver reads public embed HTML and calls their APIs the same way a browser would.

You are responsible for complying with copyright law, site terms of service, and local regulations. No warranty. Use only on content you have the right to access.

About

Ployan HLS Stream Resolver — scrape embed pages, decode plyURL, seal AES-256-GCM tokens, resolve /get to M3U8, proxy HLS playback

Topics

Resources

Stars

Watchers

Forks

Contributors