Files
sdk/src/sources/netplayz.js

149 lines
5.1 KiB
JavaScript

import crypto from 'node:crypto';
import { USER_AGENT, getTmdbMetadata, resolveTmdbKey } from '../utils/helpers.js';
const API_BASE = 'https://api.hwnou.com';
const KEY = Buffer.from('fJATm*kgfJT!BgJB', 'utf8');
const OKHTTP_UA = 'okhttp/3.2.0';
const APK_SIGN_KEY = 'ED0955EB04E67A1D9F3305B95454FED485261475';
const STREAM_HEADERS = {
'User-Agent': USER_AGENT,
Referer: 'https://thugtv.online/',
Origin: 'https://thugtv.online',
};
function decrypt(raw) {
const decipher = crypto.createDecipheriv('aes-128-cbc', KEY, KEY);
const decoded = Buffer.from(raw, 'base64');
return Buffer.concat([decipher.update(decoded), decipher.final()]).toString('utf8');
}
function parseDecrypted(raw) {
const text = decrypt(raw).trim();
const quoted = text.replace(/(?<=[:\[,]\s*)(\d{16,})(?=\s*[,\]}])/g, '"$1"');
return JSON.parse(quoted);
}
async function hwnouFetch(url, { method = 'GET', body = null, signal } = {}) {
try {
const headers = {
Accept: '*/*',
'Accept-Language': 'en-US,en;q=0.9',
'User-Agent': OKHTTP_UA,
};
if (body) headers['Content-Type'] = 'application/json;charset=utf-8';
const res = await fetch(url, { method, body, headers, signal });
if (!res.ok) return null;
const text = await res.text();
if (!text) return null;
return parseDecrypted(text);
} catch {
return null;
}
}
function pickBestVideo(ep) {
const videos = (ep?.videos || []).filter(v => v && v.premiumProPermission === false);
if (!videos.length) return (ep?.videos || [])[0] || null;
videos.sort((a, b) => (b.resolution || 0) - (a.resolution || 0));
return videos[0];
}
function pickLanguageId(ep) {
const tracks = ep?.tracks || [];
const pref = tracks.find(t => t && t.languageId === 1003);
return (pref || tracks[0] || {}).languageId || null;
}
function normalizeTitle(title) {
return String(title).toLowerCase().replace(/ /g, '+');
}
export async function getStream(args) {
const { id, s, e, sdk, signal } = args || {};
if (!id) return null;
const isTv = s != null && e != null;
const episodeNum = isTv ? (parseInt(e, 10) || 1) : 1;
let title = args?.title;
if (!title) {
try {
const meta = await getTmdbMetadata(id, isTv, resolveTmdbKey(args, sdk));
title = meta?.title;
} catch { }
}
if (!title) return null;
try {
const searchUrl = `${API_BASE}/film-api/v2.1.2/movie/searchByKeyword?keyword=${encodeURIComponent(normalizeTitle(title))}&page=1&size=20&mode=1&secure=0&channel=IndiaA&appMarket=GuanWang&packageName=com.external.castle&clientType=1`;
const searchData = await hwnouFetch(searchUrl, { signal });
const rows = searchData?.data?.rows || [];
if (!rows.length) return null;
const wantMovie = !isTv;
const candidates = rows.filter(r => (r.movieType === 2) === wantMovie);
if (!candidates.length) return null;
const exact = candidates.find(r => String(r.title || '').toLowerCase() === String(title).toLowerCase());
const match = exact || candidates[0];
const movieId = match.id;
if (!movieId) return null;
const detailsUrl = `${API_BASE}/film-api/v2.1.2/movie?movieId=${movieId}&packageName=com.external.castle&clientType=1`;
const detailsData = await hwnouFetch(detailsUrl, { signal });
const episodes = detailsData?.data?.episodes || [];
if (!episodes.length) return null;
const ep = isTv
? (episodes.find(x => Number(x.number) === episodeNum) || episodes[0])
: episodes[0];
const video = pickBestVideo(ep);
if (!video) return null;
const languageId = pickLanguageId(ep);
const streamApiUrl = `${API_BASE}/film-api/v2.0.7/movie/getVideo2?clientType=1&packageName=PKG&channel=CHANNEL&lang=en-US`;
const streamBody = JSON.stringify({
appMarket: 'GuanWang',
woolUser: 'false',
isp: 'Android',
useVipCdn: 'false',
movieId,
episodeId: ep.id,
isNewUser: 'true',
resolution: video.resolution,
mode: '1',
clientType: '1',
apkSignKey: APK_SIGN_KEY,
androidVersion: '11',
networkType: 'NETWORK_WIFI',
packageName: 'com.external.castle',
languageId,
});
const streamData = await hwnouFetch(streamApiUrl, { method: 'POST', body: streamBody, signal });
const videoUrl = streamData?.data?.videoUrl;
if (!videoUrl) return null;
const quality = video.resolutionDescription || (video.resolution ? `${video.resolution}p` : 'Auto');
const type = videoUrl.includes('.m3u8') ? 'hls' : (videoUrl.includes('.mpd') ? 'dash' : 'mp4');
const allUrls = [{
url: videoUrl,
server: `NetPlayz - ${quality}`,
quality,
type,
headers: STREAM_HEADERS,
}];
return {
url: allUrls[0].url,
headers: allUrls[0].headers,
allUrls,
};
} catch {
return null;
}
}