Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-13 15:46:17 +00:00
parent d873c0ccbf
commit f35bb526eb
22 changed files with 6318 additions and 3679 deletions
+1182
View File
@@ -0,0 +1,1182 @@
<?php
/*
首发地址:https://t.me/iptvofficalgroup
转发或转化本代码,务必注明出处,否则生个孩子没屁眼
使用方法:
M3U列表: /ppv.php?m3u=1
TXT列表: /ppv.php?txt=1
*/
const PPV_STREAMS = 'https://api.ppv.st/api/streams';
const EMBED_FETCH = 'https://embedindia.st/fetch';
const EMBED_ORIGIN = 'https://embedindia.st';
const BROWSER_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36';
const SEC_CH_UA = '"Google Chrome";v="148", "Chromium";v="148", "Not)A;Brand";v="24"';
const SEC_CH_UA_PLATFORM = '"Windows"';
const DEFAULT_ID = 'rally-tv';
const ROOM_TTL = 600;
const SOURCE_TTL = 300;
const TOKEN_MARGIN = 90;
const PLAYLIST_TTL = 600;
function starts_with(string $s, string $prefix): bool
{
return strncmp($s, $prefix, strlen($prefix)) === 0;
}
function ends_with(string $s, string $suffix): bool
{
$n = strlen($suffix);
return $n === 0 || substr($s, -$n) === $suffix;
}
function has_text(string $s, string $needle): bool
{
return strpos($s, $needle) !== false;
}
function path_slug(string $slug): string
{
$parts = [];
foreach (explode('/', trim($slug, '/')) as $part) {
if ($part !== '') {
$parts[] = rawurlencode($part);
}
}
return implode('/', $parts);
}
function embed_url(string $slug): string
{
return EMBED_ORIGIN . '/embed/' . path_slug($slug);
}
function normalize_embed_slug(string $slug): string
{
$slug = trim($slug);
if ($slug === '') {
return '';
}
if (preg_match('~^[a-z][a-z0-9+.-]*://~i', $slug)) {
$path = parse_url($slug, PHP_URL_PATH);
if (!is_string($path)) {
return '';
}
$slug = $path;
} else {
$cut = strcspn($slug, '?#');
$slug = substr($slug, 0, $cut);
}
$slug = trim(urldecode($slug), '/');
if (starts_with($slug, 'embed/')) {
$slug = substr($slug, 6);
}
return trim($slug, '/');
}
function input_slug(string $id): ?string
{
$id = trim($id);
if ($id === '') {
return null;
}
$path = parse_url($id, PHP_URL_PATH);
if (is_string($path) && preg_match('~/embed/(.+)$~', $path, $m)) {
return normalize_embed_slug($m[1]);
}
if (strpos($id, '/') !== false && !preg_match('~^[a-z][a-z0-9+.-]*://~i', $id)) {
return normalize_embed_slug($id);
}
return null;
}
function stream_slug(array $item): string
{
$slug = normalize_embed_slug((string)($item['uri_name'] ?? ''));
if ($slug !== '') {
return $slug;
}
$iframe = input_slug((string)($item['iframe'] ?? ''));
return is_string($iframe) ? $iframe : '';
}
function obj(array $pairs): stdClass
{
$o = new stdClass();
foreach ($pairs as $k => $v) {
$o->{(string)$k} = $v;
}
return $o;
}
function indians_data(): array
{
return [
'timezone' => 'Asia/Shanghai',
'timezoneOffset' => -480,
'userAgent' => BROWSER_UA,
'screenSize' => '1920x1080',
'pixelDepth' => 24,
'touch' => false,
'deviceMemory' => 8,
'platform' => 'Win32',
'touchPoints' => 0,
'hardwareConcurrency' => 8,
'intlDisplayNames' => 'supported',
'mimeTypes' => ['application/pdf', 'text/pdf'],
'plugins' => ['PDF Viewer', 'Chrome PDF Viewer', 'Chromium PDF Viewer', 'Microsoft Edge PDF Viewer', 'WebKit built-in PDF'],
'webgl' => [
'vendor' => 'Google Inc. (Intel)',
'renderer' => 'ANGLE (Intel, Intel(R) UHD Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)',
'params' => obj([
3379 => 16384,
3410 => 8,
3412 => 8,
3413 => 8,
3414 => 24,
3415 => 0,
7936 => 'WebKit',
7937 => 'WebKit WebGL',
7938 => 'WebGL 1.0 (OpenGL ES 2.0 Chromium)',
33901 => obj([0 => 1, 1 => 511]),
33902 => obj([0 => 1, 1 => 1]),
34024 => 16384,
34076 => 16384,
34921 => 16,
34930 => 16,
35660 => 16,
35661 => 32,
35724 => 'WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)',
36347 => 1024,
36348 => 30,
36349 => 1024,
]),
'exts' => [
'ANGLE_instanced_arrays',
'EXT_blend_minmax',
'EXT_clip_control',
'EXT_color_buffer_half_float',
'EXT_depth_clamp',
'EXT_disjoint_timer_query',
'EXT_float_blend',
'EXT_frag_depth',
'EXT_polygon_offset_clamp',
'EXT_shader_texture_lod',
'EXT_texture_compression_bptc',
'EXT_texture_compression_rgtc',
'EXT_texture_filter_anisotropic',
'EXT_texture_mirror_clamp_to_edge',
'EXT_sRGB',
'KHR_parallel_shader_compile',
'OES_element_index_uint',
'OES_fbo_render_mipmap',
'OES_standard_derivatives',
'OES_texture_float',
'OES_texture_float_linear',
'OES_texture_half_float',
'OES_texture_half_float_linear',
'OES_vertex_array_object',
'WEBGL_blend_func_extended',
'WEBGL_color_buffer_float',
'WEBGL_compressed_texture_s3tc',
'WEBGL_compressed_texture_s3tc_srgb',
'WEBGL_debug_renderer_info',
'WEBGL_debug_shaders',
'WEBGL_depth_texture',
'WEBGL_draw_buffers',
'WEBGL_lose_context',
'WEBGL_multi_draw',
],
],
];
}
function indians_hash(): string
{
static $hash = null;
if ($hash !== null) {
return $hash;
}
$json = json_encode(indians_data(), JSON_UNESCAPED_SLASHES);
if (!is_string($json)) {
throw new RuntimeException('bad gid json');
}
$hash = hash('sha256', $json);
return $hash;
}
function embed_headers(string $slug, bool $token): array
{
$headers = [
'Origin: ' . EMBED_ORIGIN,
'Referer: ' . embed_url($slug),
'Accept: */*',
'User-Agent: ' . BROWSER_UA,
'sec-ch-ua: ' . SEC_CH_UA,
'sec-ch-ua-mobile: ?0',
'sec-ch-ua-platform: ' . SEC_CH_UA_PLATFORM,
];
if ($token) {
$headers[] = 'indians: ' . indians_hash();
}
return $headers;
}
function die_soft(int $code, string $text): void
{
http_response_code($code);
header('Content-Type: text/plain; charset=utf-8');
echo $text;
exit;
}
function http_call(string $url, string $method = 'GET', ?string $body = null, array $headers = []): array
{
$ch = curl_init($url);
$bag = [];
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_TIMEOUT => 18,
CURLOPT_USERAGENT => BROWSER_UA,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_HEADERFUNCTION => static function ($ch, string $line) use (&$bag): int {
$p = strpos($line, ':');
if ($p !== false) {
$bag[strtolower(trim(substr($line, 0, (int)$p)))] = trim(substr($line, (int)$p + 1));
}
return strlen($line);
},
]);
if ($method !== 'GET') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body ?? '');
}
if ($headers) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
$raw = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($raw === false || $code < 200 || $code >= 300) {
throw new RuntimeException($err !== '' ? $err : "http {$code}");
}
return [$raw, $bag, $code];
}
function stream_segment(string $url): void
{
@set_time_limit(0);
while (ob_get_level() > 0) {
@ob_end_flush();
}
header('Content-Type: video/MP2T');
header('Cache-Control: public, max-age=3600');
header('X-Accel-Buffering: no');
header('Connection: close');
flush();
$buf = '';
$started = false;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_TIMEOUT => 0,
CURLOPT_USERAGENT => BROWSER_UA,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_HTTPHEADER => [
'Accept: */*',
'Origin: ' . EMBED_ORIGIN,
'Referer: ' . EMBED_ORIGIN . '/',
'User-Agent: ' . BROWSER_UA,
],
CURLOPT_WRITEFUNCTION => static function ($ch, string $chunk) use (&$buf, &$started): int {
if ($started) {
echo $chunk;
flush();
return strlen($chunk);
}
$buf .= $chunk;
$off = mpegts_offset($buf);
if ($off > 0 || strlen($buf) >= 4096) {
$started = true;
echo substr($buf, $off);
$buf = '';
flush();
}
return strlen($chunk);
},
]);
$ok = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if (!$started && $buf !== '') {
$off = mpegts_offset($buf);
echo substr($buf, $off);
flush();
}
if ($ok === false || $code < 200 || $code >= 300) {
throw new RuntimeException($err !== '' ? $err : "segment http {$code}");
}
}
function cache_dir(): string
{
$dir = sys_get_temp_dir() . '/ppv_fetch_cache';
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
return $dir;
}
function cache_path(string $kind, string $key): string
{
return cache_dir() . '/' . $kind . '-' . sha1($key) . '.json';
}
function cache_get(string $kind, string $key)
{
$file = cache_path($kind, $key);
if (!is_file($file)) {
return null;
}
$raw = file_get_contents($file);
$box = is_string($raw) ? json_decode($raw, true) : null;
if (!is_array($box) || ($box['until'] ?? 0) <= time()) {
@unlink($file);
return null;
}
return $box['data'] ?? null;
}
function cache_set(string $kind, string $key, $data, int $until): void
{
if ($until <= time()) {
return;
}
$file = cache_path($kind, $key);
$tmp = $file . '.' . getmypid() . '.tmp';
file_put_contents($tmp, json_encode(['until' => $until, 'data' => $data], JSON_UNESCAPED_SLASHES), LOCK_EX);
rename($tmp, $file);
}
function enc_varint(int $n): string
{
$s = '';
while (true) {
$b = $n & 0x7f;
$n >>= 7;
if ($n === 0) {
return $s . chr($b);
}
$s .= chr($b | 0x80);
}
}
function get_varint(string $s, int &$i): int
{
$n = 0;
$shift = 0;
$len = strlen($s);
while ($i < $len) {
$b = ord($s[$i++]);
$n |= ($b & 0x7f) << $shift;
if ($b < 0x80) {
return $n;
}
$shift += 7;
if ($shift > 63) {
throw new RuntimeException('bad varint');
}
}
throw new RuntimeException('short varint');
}
function pb_put(int $field, string $value): string
{
return enc_varint(($field << 3) | 2) . enc_varint(strlen($value)) . $value;
}
function pb_read(string $s): array
{
$i = 0;
$out = [];
$len = strlen($s);
while ($i < $len) {
$tag = get_varint($s, $i);
$field = $tag >> 3;
$wire = $tag & 7;
if ($wire !== 2) {
throw new RuntimeException('bad wire');
}
$n = get_varint($s, $i);
if ($i + $n > $len) {
throw new RuntimeException('short field');
}
$out[$field] = substr($s, (int)$i, (int)$n);
$i += $n;
}
return $out;
}
function shift_payload(string $s): string
{
$s = trim($s);
$o = '';
$len = strlen($s);
for ($i = 0; $i < $len; $i++) {
$c = ord($s[$i]);
$o .= ($c >= 33 && $c <= 126) ? chr((($c - 33 + 71) % 94) + 33) : $s[$i];
}
return $o;
}
function u32(int $n): int
{
return $n & 0xffffffff;
}
function le32(string $s, int $i): int
{
return ord($s[$i]) | (ord($s[$i + 1]) << 8) | (ord($s[$i + 2]) << 16) | (ord($s[$i + 3]) << 24);
}
function put32(int $n): string
{
return pack('V', u32($n));
}
function rotl32(int $n, int $bits): int
{
$n = u32($n);
return u32(($n << $bits) | ($n >> (32 - $bits)));
}
function chacha_qr(array &$x, int $a, int $b, int $c, int $d): void
{
$x[$a] = u32($x[$a] + $x[$b]);
$x[$d] = rotl32($x[$d] ^ $x[$a], 16);
$x[$c] = u32($x[$c] + $x[$d]);
$x[$b] = rotl32($x[$b] ^ $x[$c], 12);
$x[$a] = u32($x[$a] + $x[$b]);
$x[$d] = rotl32($x[$d] ^ $x[$a], 8);
$x[$c] = u32($x[$c] + $x[$d]);
$x[$b] = rotl32($x[$b] ^ $x[$c], 7);
}
function chacha_block(string $key, string $nonce, int $counter): string
{
$c = 'expand 32-byte k';
$s = [
le32($c, 0), le32($c, 4), le32($c, 8), le32($c, 12),
le32($key, 0), le32($key, 4), le32($key, 8), le32($key, 12),
le32($key, 16), le32($key, 20), le32($key, 24), le32($key, 28),
u32($counter), le32($nonce, 0), le32($nonce, 4), le32($nonce, 8),
];
$x = $s;
for ($i = 0; $i < 10; $i++) {
chacha_qr($x, 0, 4, 8, 12);
chacha_qr($x, 1, 5, 9, 13);
chacha_qr($x, 2, 6, 10, 14);
chacha_qr($x, 3, 7, 11, 15);
chacha_qr($x, 0, 5, 10, 15);
chacha_qr($x, 1, 6, 11, 12);
chacha_qr($x, 2, 7, 8, 13);
chacha_qr($x, 3, 4, 9, 14);
}
$out = '';
for ($i = 0; $i < 16; $i++) {
$out .= put32($x[$i] + $s[$i]);
}
return $out;
}
function chacha_stream(string $key, string $nonce, int $len, int $counter): string
{
$out = '';
while (strlen($out) < $len) {
$out .= chacha_block($key, $nonce, $counter);
$counter = u32($counter + 1);
}
return substr($out, 0, $len);
}
function pad16(string $s): string
{
$n = strlen($s) % 16;
return $n === 0 ? '' : str_repeat("\0", 16 - $n);
}
function le64(int $n): string
{
$lo = $n & 0xffffffff;
$hi = intdiv($n, 4294967296);
return pack('V2', $lo, $hi);
}
function poly1305_mac(string $msg, string $key): string
{
if (PHP_INT_SIZE < 8) {
throw new RuntimeException('need 64-bit php');
}
$t0 = le32($key, 0);
$t1 = le32($key, 4);
$t2 = le32($key, 8);
$t3 = le32($key, 12);
$r0 = $t0 & 0x3ffffff;
$r1 = (($t0 >> 26) | ($t1 << 6)) & 0x3ffff03;
$r2 = (($t1 >> 20) | ($t2 << 12)) & 0x3ffc0ff;
$r3 = (($t2 >> 14) | ($t3 << 18)) & 0x3f03fff;
$r4 = ($t3 >> 8) & 0x00fffff;
$s1 = $r1 * 5;
$s2 = $r2 * 5;
$s3 = $r3 * 5;
$s4 = $r4 * 5;
$h0 = 0;
$h1 = 0;
$h2 = 0;
$h3 = 0;
$h4 = 0;
$mask = 0x3ffffff;
$len = strlen($msg);
for ($pos = 0; $pos < $len; $pos += 16) {
$n = min(16, $len - $pos);
$block = substr($msg, $pos, $n);
$hibit = 1 << 24;
if ($n < 16) {
$block .= "\x01" . str_repeat("\0", 15 - $n);
$hibit = 0;
}
$t0 = le32($block, 0);
$t1 = le32($block, 4);
$t2 = le32($block, 8);
$t3 = le32($block, 12);
$h0 += $t0 & $mask;
$h1 += (($t0 >> 26) | ($t1 << 6)) & $mask;
$h2 += (($t1 >> 20) | ($t2 << 12)) & $mask;
$h3 += (($t2 >> 14) | ($t3 << 18)) & $mask;
$h4 += (($t3 >> 8) & 0x00ffffff) | $hibit;
$d0 = ($h0 * $r0) + ($h1 * $s4) + ($h2 * $s3) + ($h3 * $s2) + ($h4 * $s1);
$d1 = ($h0 * $r1) + ($h1 * $r0) + ($h2 * $s4) + ($h3 * $s3) + ($h4 * $s2);
$d2 = ($h0 * $r2) + ($h1 * $r1) + ($h2 * $r0) + ($h3 * $s4) + ($h4 * $s3);
$d3 = ($h0 * $r3) + ($h1 * $r2) + ($h2 * $r1) + ($h3 * $r0) + ($h4 * $s4);
$d4 = ($h0 * $r4) + ($h1 * $r3) + ($h2 * $r2) + ($h3 * $r1) + ($h4 * $r0);
$c = $d0 >> 26;
$h0 = $d0 & $mask;
$d1 += $c;
$c = $d1 >> 26;
$h1 = $d1 & $mask;
$d2 += $c;
$c = $d2 >> 26;
$h2 = $d2 & $mask;
$d3 += $c;
$c = $d3 >> 26;
$h3 = $d3 & $mask;
$d4 += $c;
$c = $d4 >> 26;
$h4 = $d4 & $mask;
$h0 += $c * 5;
$c = $h0 >> 26;
$h0 &= $mask;
$h1 += $c;
}
$c = $h1 >> 26;
$h1 &= $mask;
$h2 += $c;
$c = $h2 >> 26;
$h2 &= $mask;
$h3 += $c;
$c = $h3 >> 26;
$h3 &= $mask;
$h4 += $c;
$c = $h4 >> 26;
$h4 &= $mask;
$h0 += $c * 5;
$c = $h0 >> 26;
$h0 &= $mask;
$h1 += $c;
$g0 = $h0 + 5;
$c = $g0 >> 26;
$g0 &= $mask;
$g1 = $h1 + $c;
$c = $g1 >> 26;
$g1 &= $mask;
$g2 = $h2 + $c;
$c = $g2 >> 26;
$g2 &= $mask;
$g3 = $h3 + $c;
$c = $g3 >> 26;
$g3 &= $mask;
$g4 = $h4 + $c - (1 << 26);
if ($g4 >= 0) {
$h0 = $g0;
$h1 = $g1;
$h2 = $g2;
$h3 = $g3;
$h4 = $g4;
}
$f0 = (($h0 | ($h1 << 26)) & 0xffffffff) + le32($key, 16);
$w0 = $f0 & 0xffffffff;
$f1 = ((($h1 >> 6) | ($h2 << 20)) & 0xffffffff) + le32($key, 20) + ($f0 >> 32);
$w1 = $f1 & 0xffffffff;
$f2 = ((($h2 >> 12) | ($h3 << 14)) & 0xffffffff) + le32($key, 24) + ($f1 >> 32);
$w2 = $f2 & 0xffffffff;
$f3 = ((($h3 >> 18) | ($h4 << 8)) & 0xffffffff) + le32($key, 28) + ($f2 >> 32);
$w3 = $f3 & 0xffffffff;
return put32($w0) . put32($w1) . put32($w2) . put32($w3);
}
function aead_input(string $aad, string $ciphertext): string
{
return $aad . pad16($aad) . $ciphertext . pad16($ciphertext) . le64(strlen($aad)) . le64(strlen($ciphertext));
}
function bxor_stream(string $a, string $b): string
{
$out = '';
$len = strlen($a);
for ($i = 0; $i < $len; $i++) {
$out .= chr(ord($a[$i]) ^ ord($b[$i]));
}
return $out;
}
function open_payload(string $payload, string $island): string
{
$packed = base64_decode(shift_payload($payload), true);
if ($packed === false || strlen($packed) < 28) {
throw new RuntimeException('bad payload');
}
$nonce = substr($packed, 0, 12);
$box = substr($packed, 12);
$key = substr($island, 0, 32);
$key = str_pad($key, 32, "\0");
$tag = substr($box, -16);
$ciphertext = substr($box, 0, -16);
$poly_key = chacha_stream($key, $nonce, 32, 0);
$got = poly1305_mac(aead_input('', $ciphertext), $poly_key);
if (!hash_equals($got, $tag)) {
throw new RuntimeException('bad tag');
}
return bxor_stream($ciphertext, chacha_stream($key, $nonce, strlen($ciphertext), 1));
}
function room_slug(string $id): string
{
$direct = input_slug($id);
if (is_string($direct) && $direct !== '') {
return $direct;
}
$hit = cache_get('room', $id);
if (is_string($hit) && $hit !== '') {
return $hit;
}
[$json] = http_call(PPV_STREAMS);
$data = json_decode($json, true);
if (!is_array($data)) {
throw new RuntimeException('bad streams json');
}
$want = preg_replace('/^ppv-/', '', trim($id));
foreach (($data['streams'] ?? []) as $cat) {
foreach (($cat['streams'] ?? []) as $item) {
$num = isset($item['id']) ? (string)$item['id'] : '';
$slug = stream_slug($item);
if ($num === $want || ('ppv-' . $num) === $id || $slug === $id) {
if ($slug !== '') {
cache_set('room', $id, $slug, time() + ROOM_TTL);
return $slug;
}
}
// 检查子流
foreach (($item['substreams'] ?? []) as $sub) {
$subNum = isset($sub['id']) ? (string)$sub['id'] : '';
$subSlug = stream_slug($sub);
if ($subNum === $want || ('ppv-' . $subNum) === $id || $subSlug === $id) {
if ($subSlug !== '') {
cache_set('room', $id, $subSlug, time() + ROOM_TTL);
return $subSlug;
}
}
}
}
}
throw new RuntimeException('room not found');
}
function fresh_url(string $slug): string
{
$body = pb_put(1, $slug);
[$bin, $headers] = http_call(EMBED_FETCH, 'POST', $body, array_merge(['Content-Type: application/octet-stream'], embed_headers($slug, true)));
$island = $headers['island'] ?? '';
if ($island === '') {
throw new RuntimeException('no island');
}
$fields = pb_read($bin);
if (!isset($fields[1])) {
throw new RuntimeException('no payload');
}
return open_payload($fields[1], $island);
}
function hls_get(string $url, string $slug): string
{
[$text] = http_call($url, 'GET', null, embed_headers($slug, false));
if (!starts_with(ltrim($text), '#EXTM3U')) {
throw new RuntimeException('not m3u8');
}
return $text;
}
function dot_path(string $path): string
{
$lead = starts_with($path, '/');
$tail = ends_with($path, '/');
$stack = [];
foreach (explode('/', $path) as $part) {
if ($part === '' || $part === '.') {
continue;
}
if ($part === '..') {
array_pop($stack);
continue;
}
$stack[] = $part;
}
$out = ($lead ? '/' : '') . implode('/', $stack);
return $tail && $out !== '/' ? $out . '/' : ($out === '' ? ($lead ? '/' : '') : $out);
}
function url_join(string $base, string $ref): string
{
$ref = trim($ref);
if ($ref === '' || preg_match('~^[a-z][a-z0-9+.-]*:~i', $ref)) {
return $ref;
}
$b = parse_url($base);
if (!$b || empty($b['scheme']) || empty($b['host'])) {
throw new RuntimeException('bad base url');
}
if (starts_with($ref, '//')) {
return $b['scheme'] . ':' . $ref;
}
$scheme = $b['scheme'];
$host = $b['host'];
$port = isset($b['port']) ? ':' . $b['port'] : '';
if (starts_with($ref, '/')) {
return "{$scheme}://{$host}{$port}" . dot_path($ref);
}
$dir = isset($b['path']) ? preg_replace('~/[^/]*$~', '/', $b['path']) : '/';
return "{$scheme}://{$host}{$port}" . dot_path($dir . $ref);
}
function quote_uris(string $line, string $base): string
{
return preg_replace_callback('/URI="([^"]+)"/', static function (array $m) use ($base): string {
return 'URI="' . url_join($base, $m[1]) . '"';
}, $line);
}
function maybe_m3u8(string $uri): bool
{
$path = parse_url($uri, PHP_URL_PATH);
return stripos($path !== false && $path !== null ? $path : $uri, '.m3u8') !== false;
}
function stream_score(string $line): int
{
$score = 0;
if (preg_match('/BANDWIDTH=(\d+)/', $line, $m)) {
$score += (int)$m[1];
}
if (preg_match('/RESOLUTION=(\d+)x(\d+)/', $line, $m)) {
$score += ((int)$m[1] * (int)$m[2]) * 10;
}
return $score;
}
function m3u8_refs(string $text, string $base): array
{
$refs = [];
$pending = 0;
foreach (explode("\n", str_replace(["\r\n", "\r"], "\n", $text)) as $line) {
$trim = trim($line);
if ($trim === '') {
continue;
}
if (starts_with($trim, '#')) {
if (starts_with($trim, '#EXT-X-STREAM-INF')) {
$pending = stream_score($trim);
}
if (has_text($line, 'URI="')) {
preg_match_all('/URI="([^"]+)"/', $line, $hits);
foreach ($hits[1] ?? [] as $uri) {
if (maybe_m3u8($uri)) {
$refs[] = [
'url' => url_join($base, $uri),
'rank' => has_text($trim, 'I-FRAME') ? 0 : 1,
'score' => stream_score($trim),
];
}
}
}
continue;
}
if (maybe_m3u8($trim)) {
$refs[] = [
'url' => url_join($base, $trim),
'rank' => $pending > 0 ? 3 : 2,
'score' => $pending,
];
}
$pending = 0;
}
usort($refs, static function (array $a, array $b): int {
return ($b['rank'] <=> $a['rank']) ?: ($b['score'] <=> $a['score']);
});
return $refs;
}
function final_m3u8(string $url, string $slug): array
{
$seen = [];
for ($i = 0; $i < 8; $i++) {
if (isset($seen[$url])) {
throw new RuntimeException('m3u8 loop');
}
$seen[$url] = true;
$text = hls_get($url, $slug);
$refs = m3u8_refs($text, $url);
if (!$refs) {
return [$text, $url];
}
$url = $refs[0]['url'];
}
throw new RuntimeException('m3u8 too deep');
}
function secure_until(string $url): ?int
{
$path = parse_url($url, PHP_URL_PATH);
if (!is_string($path)) {
return null;
}
$parts = explode('/', $path);
$n = count($parts);
for ($i = 0; $i < $n; $i++) {
if ($parts[$i] === 'secure' && isset($parts[$i + 3]) && ctype_digit($parts[$i + 3])) {
return ((int)$parts[$i + 3]) - TOKEN_MARGIN;
}
}
return null;
}
function source_until(string $source, string $final): int
{
$until = time() + SOURCE_TTL;
foreach ([$source, $final] as $url) {
$end = secure_until($url);
if ($end !== null) {
$until = min($until, $end);
}
}
return max(time() + 20, $until);
}
function live_m3u8(string $slug): array
{
$hit = cache_get('source', $slug);
if (is_array($hit) && isset($hit['final']) && is_string($hit['final'])) {
try {
$text = hls_get($hit['final'], $slug);
if (!m3u8_refs($text, $hit['final'])) {
return [$text, $hit['final'], 'HIT'];
}
} catch (Throwable $e) {
}
}
$source = fresh_url($slug);
[$text, $final] = final_m3u8($source, $slug);
cache_set('source', $slug, ['source' => $source, 'final' => $final], source_until($source, $final));
return [$text, $final, 'MISS'];
}
function abs_m3u8(string $text, string $base): string
{
$text = str_replace(["\r\n", "\r"], "\n", $text);
$out = [];
foreach (explode("\n", $text) as $line) {
$trim = trim($line);
if ($trim === '') {
$out[] = $line;
} elseif (starts_with($trim, '#')) {
$out[] = has_text($line, 'URI="') ? quote_uris($line, $base) : $line;
} else {
$url = url_join($base, $trim);
$out[] = segment_needs_proxy($url) ? segment_proxy_url($url) : $url;
}
}
return rtrim(implode("\n", $out)) . "\n";
}
function segment_needs_proxy(string $url): bool
{
$path = parse_url($url, PHP_URL_PATH);
if (!is_string($path)) {
return false;
}
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
return $ext === '';
}
function segment_proxy_url(string $url): string
{
return self_url() . '?seg=' . rawurlencode($url) . '&ext=.ts';
}
function mpegts_offset(string $bin): int
{
$len = strlen($bin);
$max = min(4096, $len - 188 * 5);
for ($off = 0; $off <= $max; $off++) {
$ok = true;
for ($i = 0; $i < 5; $i++) {
if (ord($bin[$off + $i * 188]) !== 0x47) {
$ok = false;
break;
}
}
if ($ok) {
return $off;
}
}
return 0;
}
function output_segment(string $url): void
{
if (!preg_match('~^https?://~i', $url)) {
die_soft(400, 'bad segment url');
}
stream_segment($url);
}
// ==================== 列表功能 ====================
function self_url(): string
{
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$path = parse_url($uri, PHP_URL_PATH) ?? '/';
return "{$scheme}://{$host}{$path}";
}
function fetch_streams(): array
{
$hit = cache_get('streams_list', 'all');
if (is_array($hit)) {
return $hit;
}
[$json] = http_call(PPV_STREAMS);
$data = json_decode($json, true);
if (!is_array($data) || !isset($data['streams'])) {
throw new RuntimeException('bad streams json');
}
cache_set('streams_list', 'all', $data['streams'], time() + 300);
return $data['streams'];
}
// 将Unix时间戳转换为北京时间字符串 (格式: m/d H:i)
function format_beijing_time(int $timestamp): string
{
if ($timestamp <= 0) {
return '';
}
$dt = new DateTime('@' . $timestamp);
$dt->setTimezone(new DateTimeZone('Asia/Shanghai'));
return $dt->format('n/j H:i');
}
function build_channels(array $streamsData): array
{
$channels = [];
foreach ($streamsData as $cat) {
$category = $cat['category'] ?? 'Unknown';
foreach (($cat['streams'] ?? []) as $item) {
$uri = stream_slug($item);
if ($uri === '') continue;
$startTime = format_beijing_time($item['starts_at'] ?? 0);
$channels[] = [
'id' => 'ppv-' . ($item['id'] ?? ''),
'name' => $item['name'] ?? 'Unknown',
'category' => $category,
'uri_name' => $uri,
'logo' => $item['poster'] ?? '',
'tag' => $item['tag'] ?? '',
'always_live' => ($item['always_live'] ?? 0) === 1,
'start_time' => $startTime, // 比赛开始时间
];
// 子流
foreach (($item['substreams'] ?? []) as $sub) {
$subUri = stream_slug($sub);
if ($subUri === '') continue;
// 子流使用主流的开始时间
$channels[] = [
'id' => 'ppv-' . ($sub['id'] ?? ''),
'name' => ($sub['name'] ?? 'Unknown') . ' [' . ($sub['tag'] ?? 'Sub') . ']',
'category' => $category,
'uri_name' => $subUri,
'logo' => $sub['poster'] ?? ($item['poster'] ?? ''),
'tag' => $sub['tag'] ?? '',
'always_live' => ($item['always_live'] ?? 0) === 1,
'start_time' => $startTime,
];
}
}
}
return $channels;
}
function output_json(array $channels): void
{
header('Content-Type: application/json; charset=utf-8');
$base = self_url();
$out = [];
foreach ($channels as $c) {
$out[] = [
'id' => $c['id'],
'name' => $c['name'],
'category' => $c['category'],
'uri_name' => $c['uri_name'],
'logo' => $c['logo'],
'start_time' => $c['start_time'], // 包含开始时间
'play_url' => $base . '?id=' . rawurlencode($c['uri_name']),
];
}
echo json_encode($out, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
function output_m3u(array $channels): void
{
header('Content-Type: application/vnd.apple.mpegurl; charset=utf-8');
header('Content-Disposition: attachment; filename="ppv.m3u"');
$base = self_url();
$key = $base . '|m3u';
$hit = cache_get('playlist_m3u', $key);
if (is_string($hit)) {
header('X-PPV-Playlist-Cache: HIT');
echo $hit;
return;
}
$text = "#EXTM3U\n";
foreach ($channels as $c) {
$logo = $c['logo'] ? ' tvg-logo="' . htmlspecialchars($c['logo']) . '"' : '';
$group = htmlspecialchars($c['category']);
// 在频道名称前加上比赛开始时间 [m/d H:i]
$timePrefix = $c['start_time'] ? '[' . $c['start_time'] . '] ' : '';
$name = htmlspecialchars($timePrefix . $c['name']);
$text .= "#EXTINF:-1{$logo} group-title=\"{$group}\",{$name}\n";
$text .= $base . '?id=' . rawurlencode($c['uri_name']) . "\n";
}
cache_set('playlist_m3u', $key, $text, time() + PLAYLIST_TTL);
header('X-PPV-Playlist-Cache: MISS');
echo $text;
}
function output_txt(array $channels): void
{
header('Content-Type: text/plain; charset=utf-8');
$base = self_url();
$key = $base . '|txt';
$hit = cache_get('playlist_txt', $key);
if (is_string($hit)) {
header('X-PPV-Playlist-Cache: HIT');
echo $hit;
return;
}
$currentCat = '';
$text = '';
foreach ($channels as $c) {
if ($c['category'] !== $currentCat) {
$currentCat = $c['category'];
$text .= "\n{$currentCat},#genre#\n";
}
$timePrefix = $c['start_time'] ? '[' . $c['start_time'] . '] ' : '';
$name = str_replace(',', '', $timePrefix . $c['name']);
$text .= "{$name},{$base}?id=" . rawurlencode($c['uri_name']) . "\n";
}
cache_set('playlist_txt', $key, $text, time() + PLAYLIST_TTL);
header('X-PPV-Playlist-Cache: MISS');
echo $text;
}
// ==================== 主逻辑 ====================
if (isset($_GET['seg'])) {
try {
output_segment(trim((string)$_GET['seg']));
exit;
} catch (Throwable $e) {
die_soft(502, $e->getMessage());
}
}
// 列表模式
if (isset($_GET['list'])) {
try {
$streams = fetch_streams();
$channels = build_channels($streams);
output_json($channels);
exit;
} catch (Throwable $e) {
die_soft(502, $e->getMessage());
}
}
if (isset($_GET['m3u'])) {
try {
$streams = fetch_streams();
$channels = build_channels($streams);
output_m3u($channels);
exit;
} catch (Throwable $e) {
die_soft(502, $e->getMessage());
}
}
if (isset($_GET['txt'])) {
try {
$streams = fetch_streams();
$channels = build_channels($streams);
output_txt($channels);
exit;
} catch (Throwable $e) {
die_soft(502, $e->getMessage());
}
}
// 播放单频道
$id = isset($_GET['id']) ? trim((string)$_GET['id']) : DEFAULT_ID;
if ($id === '') {
$id = DEFAULT_ID;
}
try {
$slug = room_slug($id);
[$m3u8, $base, $cache] = live_m3u8($slug);
header('Content-Type: application/vnd.apple.mpegurl; charset=utf-8');
header('Cache-Control: no-store');
header('X-PPV-Cache: ' . $cache);
echo abs_m3u8($m3u8, $base);
} catch (Throwable $e) {
die_soft(502, $e->getMessage());
}