mirror of
https://gitlab.com/vyla-entertainment/sdk.git
synced 2026-11-13 21:57:27 -05:00
149 lines
5.1 KiB
JavaScript
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;
|
|
}
|
|
} |