<?php
/**
 * 哈TV 直播流代理 (hatv.php)
 * 播放器访问 http://localhost:1234/hatv.php?id=xx 即可播放,
 * 后台自动: 拉频道列表(缓存) → 转发上游 HLS → 重写 m3u8 相对链接 → 透传 ts 分片
 *
 * 用法:
 *   /hatv.php                          → 全部频道 m3u8 (可导入 IPTV 播放器)
 *   /hatv.php?id=0                     → 第 0 路流的 master playlist (代理重写)
 *   /hatv.php?id=0&f=chunklist_xxx.m3u8?checkCode=... → 分片列表 (代理重写)
 *   /hatv.php?id=0&f=media_xxx.ts?...  → ts 分片 (二进制透传, 支持 Range)
 *
 * 可选参数: &cust=账号 &pwd=密码 &mac=MAC   (默认 60999/abcdefg)
 */

error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);
set_time_limit(120);

// ============ 配置区 ============
// 多账号合并: 每账号独立授权, 自动选可播账号
$ACCOUNTS = [
    ['cust' => '60999',  'pwd' => 'abcdefg', 'mac' => ''],
    ['cust' => '59420',  'pwd' => '000000',  'mac' => ''],
    ['cust' => '1229999','pwd' => '2018',    'mac' => ''],
];
// 单账号覆盖 (可选, 传 &cust=&pwd= 时只用该账号)
$CUST = trim($_GET['cust'] ?? '');
$PWD  = trim($_GET['pwd']  ?? '');
if ($CUST !== '') {
    $ACCOUNTS = [['cust' => $CUST, 'pwd' => $PWD !== '' ? $PWD : '2018', 'mac' => trim($_GET['mac'] ?? '')]];
}
$API_HOST = 'stb.topmso.com.tw';
$API_IPS  = ['58.99.33.12', '58.99.33.1', '58.99.33.2'];
$CACHE_TTL = 600;                       // 频道列表缓存秒数
$CACHE_DIR = __DIR__;                   // 缓存文件目录
// ================================

header('Access-Control-Allow-Origin: *');

/** 随机 MAC 地址 (XX:XX:XX:XX:XX:XX) */
function random_mac(): string {
    $h = '';
    for ($i = 0; $i < 6; $i++) {
        $h .= ($i ? ':' : '') . str_pad(dechex(random_int(0, 255)), 2, '0', STR_PAD_LEFT);
    }
    return $h;
}

/** GET 请求返回 [body, httpcode] (IP 直连 + Host 头) */
function http_get_raw(string $url, string $ip = ''): array {
    $headers = [];
    if ($ip !== '') {
        $host = parse_url($url, PHP_URL_HOST);
        $port = parse_url($url, PHP_URL_PORT) ?: 80;
        $url  = str_replace("$host:$port", "$ip:$port", $url);
        $headers[] = "Host: $host";
    }
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_CONNECTTIMEOUT => 8,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36',
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
    ]);
    $body = curl_exec($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err  = curl_error($ch);
    curl_close($ch);
    if ($body === false) throw new RuntimeException($err);
    return [$body, $code];
}

/** 拉取频道 XML (多 IP × 端口自动回退) */
function fetch_channels_xml(string $cust, string $mac): string {
    global $API_HOST, $API_IPS;
    if ($mac === '') $mac = random_mac();
    $path = '/csr_mobile_client_web/ottLiveStreamGroupAction.do'
        . '?method=getLiveStreamGroupForPhone_byChannel'
        . '&ottCustNo=' . rawurlencode($cust);
    if (strlen($mac) >= 6) $path .= '&mac=' . rawurlencode($mac);

    $errs = [];
    foreach (['8085', '8080'] as $port) {
        foreach ($API_IPS as $ip) {
            try { [$body] = http_get_raw("http://$API_HOST:$port$path", $ip); return $body; }
            catch (Throwable $e) { $errs[] = "$ip:$port/" . $e->getMessage(); }
        }
    }
    throw new RuntimeException('API 连接全部失败: ' . implode('; ', array_slice($errs, 0, 4)));
}

/** 解析 XML → 扁平流列表 [ {name, bra, bran, io} ] (纯 regex) */
function parse_channels(string $xml): array {
    $byGid = [];
    if (preg_match_all('/<groupDetail>(.*?)<\/groupDetail>/s', $xml, $gd)) {
        foreach ($gd[1] as $block) {
            $gid = $name = '';
            if (preg_match('/<liveGid>([^<]*)<\/liveGid>/', $block, $m)) $gid = trim($m[1]);
            if (preg_match('/<liveName><!\[CDATA\[(.*?)\]\]><\/liveName>/s', $block, $m)) $name = trim($m[1]);
            elseif (preg_match('/<liveName>([^<]*)<\/liveName>/', $block, $m)) $name = trim($m[1]);
            $byGid[$gid][] = $name;
        }
    }

    $list = [];
    if (preg_match_all('/<barate>(.*?)<\/barate>/s', $xml, $bs)) {
        foreach ($bs[1] as $block) {
            $gid = $bra = $bran = $io = '';
            if (preg_match('/<ba_live_gid>([^<]*)<\/ba_live_gid>/', $block, $m)) $gid = trim($m[1]);
            if (preg_match('/<bra>([^<]*)<\/bra>/', $block, $m)) $bra = trim($m[1]);
            if (preg_match('/<bran>([^<]*)<\/bran>/', $block, $m)) $bran = trim($m[1]);
            if (preg_match('/<io>([^<]*)<\/io>/', $block, $m)) $io = trim($m[1]);
            $names = $byGid[$gid] ?? [];
            $name = $names[intdiv(count($list), 3)] ?? ($names ? end($names) : '');
            $list[] = ['name' => $name, 'bra' => $bra, 'bran' => $bran, 'io' => $io];
        }
    }
    return $list;
}

/**
 * 合并多账号频道列表 → [ {name, bra, bran, streams:[{io,cust,pwd}]} ]
 * 每频道 3 码率条目 (低/高/自動), streams 含各账号的 io 候选
 */
function merge_channels(): array {
    global $ACCOUNTS;
    $byName = [];   // name => [bra/bran 列表]
    $streams = [];  // name_bra => [ {io,cust,pwd}, ... ]

    foreach ($ACCOUNTS as $acc) {
        try {
            $list = parse_channels(fetch_channels_xml($acc['cust'], $acc['mac'] ?? ''));
        } catch (Throwable $e) {
            continue;   // 该账号拉取失败 → 跳过
        }
        foreach ($list as $c) {
            if ($c['name'] === '' || $c['io'] === '') continue;
            $key = $c['name'] . '|' . $c['bra'];
            if (!isset($streams[$key])) {
                $byName[$key] = ['name' => $c['name'], 'bra' => $c['bra'], 'bran' => $c['bran']];
                $streams[$key] = [];
            }
            $streams[$key][] = ['io' => $c['io'], 'cust' => $acc['cust'], 'pwd' => $acc['pwd']];
        }
    }

    // 保持每频道 3 码率顺序 (低/高/自動), 码率缺失时补空
    $out = [];
    $chanKeys = [];   // name => [bra => key]
    foreach ($byName as $key => $meta) {
        $chanKeys[$meta['name']][$meta['bra']] = $key;
    }
    foreach ($chanKeys as $chName => $bras) {
        foreach (['350', '1000', '9999'] as $bra) {
            $key = $bras[$bra] ?? null;
            if ($key === null) continue;
            $out[] = $byName[$key] + ['streams' => $streams[$key]];
        }
    }
    return $out;
}

/** 合并频道列表 (文件缓存 + TTL, $force=true 强制刷新, 失败时用旧缓存兜底) */
function get_channels(bool $force = false): array {
    global $CACHE_TTL, $CACHE_DIR;
    $file = $CACHE_DIR . '/.hatv_cache_multi.json';

    if (!$force && is_file($file) && time() - filemtime($file) < $CACHE_TTL) {
        $d = json_decode((string)file_get_contents($file), true);
        if (is_array($d) && $d) return $d;
    }
    try {
        $list = merge_channels();
        if ($list) {
            @file_put_contents($file, json_encode($list, JSON_UNESCAPED_UNICODE));
            return $list;
        }
    } catch (Throwable $e) {
        if (is_file($file)) {
            $d = json_decode((string)file_get_contents($file), true);
            if (is_array($d) && $d) return $d;
        }
        throw $e;
    }
    throw new RuntimeException('频道列表为空');
}

/** 删除缓存文件 */
function invalidate_cache(): void {
    global $CACHE_DIR;
    $file = $CACHE_DIR . '/.hatv_cache_multi.json';
    if (is_file($file)) @unlink($file);
}

/** 构造最终播放 URL (抓包格式: checkCode&aa&as&mmmm&dr&dt&cust_type) */
function build_final_url(string $io, string $cust, string $pwd, string $mac): string {
    $p = parse_url($io);
    parse_str($p['query'] ?? '', $q);
    $code = $q['checkCode'] ?? '';
    $base = $p['scheme'] . '://' . $p['host']
        . (isset($p['port']) ? ':' . $p['port'] : '')
        . $p['path'];
    return $base
        . '?checkCode=' . urlencode($code)
        . '&aa=' . urlencode($cust)
        . '&as='  . urlencode($pwd)
        . '&mmmm=' . urlencode($mac)
        . '&dr=123'
        . '&dt='
        . '&cust_type=NE';
}

/**
 * 获取 id 对应的可播流地址 [io, cust, pwd]
 * 多账号自动选号: 逐账号验证最终 URL (HTTP<400 视为可播)
 * 全部失败 → 强制刷新列表重试一次
 */
function resolve_io(int $id): array {
    for ($attempt = 0; $attempt < 2; $attempt++) {
        $list = get_channels($attempt > 0);   // 第2次强制刷新
        if (isset($list[$id]) && !empty($list[$id]['streams'])) {
            foreach ($list[$id]['streams'] as $s) {
                if ($s['io'] === '') continue;
                $mac = random_mac();
                try {
                    $url = build_final_url($s['io'], $s['cust'], $s['pwd'], $mac);
                    [$body, $code] = http_get_raw($url);
                    if ($code < 400 && $body !== '') {
                        return [$s['io'], $s['cust'], $s['pwd']];
                    }
                } catch (Throwable $e) {
                    // 连接失败 → 试下一个账号
                }
            }
        }
        if ($attempt === 0) invalidate_cache();
    }
    throw new RuntimeException('该频道所有账号均不可播 (授权限制或流密钥失效)');
}

/** 相对路径 → 上游完整 URL */
function resolve_url(string $base, string $f): string {
    if (preg_match('#^https?://#', $f)) return $f;
    $p = parse_url($base);
    $dir = substr($p['path'], 0, strrpos($p['path'], '/') + 1);
    return $p['scheme'] . '://' . $p['host']
        . (isset($p['port']) ? ':' . $p['port'] : '')
        . $dir . $f;
}

/** 重写 m3u8: 所有 .m3u8/.ts 链接 → 本代理路径 */
function rewrite_m3u8(string $body, int $id): string {
    return preg_replace_callback(
        '#([A-Za-z0-9_\-./%]+\.(?:m3u8|ts)(?:\?[^\s"<]*)?)#',
        function ($m) use ($id) {
            return 'hatv.php?id=' . $id . '&f=' . urlencode($m[1]);
        },
        $body
    ) ?? $body;
}

/** ts 分片二进制透传 (支持 Range) */
function proxy_binary(string $url): void {
    $headers = [];
    if (!empty($_SERVER['HTTP_RANGE'])) $headers[] = 'Range: ' . $_SERVER['HTTP_RANGE'];

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_CONNECTTIMEOUT => 8,
        CURLOPT_TIMEOUT        => 60,
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36',
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
    ]);
    $body = curl_exec($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($body === false || $code >= 400) {
        http_response_code(502);
        exit('upstream error');
    }
    header('Content-Type: video/mp2t');
    header('Content-Length: ' . strlen($body));
    echo $body;
}

// ============ 主流程 ============
try {
    $id = isset($_GET['id']) ? (int)$_GET['id'] : null;
    $f  = trim($_GET['f'] ?? '');

    // 无 id → 输出全部频道 m3u8
    if ($id === null) {
        $list = get_channels();
        header('Content-Type: application/vnd.apple.mpegurl; charset=utf-8');
        header('Content-Disposition: attachment; filename="hatv_all.m3u8"');
        echo "#EXTM3U\n";
        foreach ($list as $i => $c) {
            echo "#EXTINF:-1," . ($c['name'] ?: "ch$i") . " ({$c['bran']})\n";
            echo "hatv.php?id=$i\n";
        }
        exit;
    }

    $list = get_channels();
    if (!isset($list[$id])) { http_response_code(404); exit('bad id'); }

    // 多账号自动选号: resolve_io 逐账号验证, 全部失效则强制刷新缓存重试
    [$base, $cust, $pwd] = resolve_io($id);
    $MAC = random_mac();

    // 无 f → 302 重定向到完整播放地址 (抓包验证格式):
    // {io_base}?checkCode=<令牌>&aa=<账号>&as=<密码>&mmmm=<MAC>&dr=123&dt=&cust_type=NE
    if ($f === '') {
        $final = build_final_url($base, $cust, $pwd, $MAC);
        header('Location: ' . $final, true, 302);
        exit;
    }

    // f → 分片列表或 ts 分片
    $url = resolve_url($base, $f);
    if (str_contains(strtolower($f), '.m3u8')) {
        [$body, $code] = http_get_raw($url);
        if ($code >= 400) { http_response_code($code); exit('upstream ' . $code); }
        header('Content-Type: application/vnd.apple.mpegurl; charset=utf-8');
        echo rewrite_m3u8($body, $id);
    } else {
        proxy_binary($url);
    }
} catch (Throwable $e) {
    http_response_code(500);
    header('Content-Type: text/plain; charset=utf-8');
    echo '代理错误: ' . $e->getMessage();
}
