<?php
/**
 * 采集共享函数库（由 collect.php 与 API.php 引入）
 * 位置：PPQSQL/collect_lib.php（与调用方同目录，故 __DIR__ 即为 PPQSQL 目录）
 */

if (!defined('PPQSQL_LIB_LOADED')) {
    define('PPQSQL_LIB_LOADED', 1);

    // 版权完整性校验: 盗版时停用采集/绑定核心
    if (!defined('PPQ_ROOT')) define('PPQ_ROOT', __DIR__ . '/../');
    if (file_exists(__DIR__ . '/../Php/Public/Integrity.php')) {
        include(__DIR__ . '/../Php/Public/Integrity.php');
    }
    if (!empty($GLOBALS['PPQ_PIRACY'])) {
        // 盗版副本: 核心采集/更新功能停用
        function incremental_collect($target_total = 500, $daily_mode = false, $batch_cap = 0) {
            return array('ok' => false, 'msg' => '未授权副本，采集功能已停用。请联系啪啪雀 papaq.xyz 获取正版。');
        }
        function bind_categories() {
            return array('ok' => false, 'msg' => '未授权副本，绑定功能已停用。请联系啪啪雀 papaq.xyz 获取正版。');
        }
        return; // 中止后续定义
    }

    $PPQSQL_DIR  = __DIR__;
    $HOME_DIR   = $PPQSQL_DIR . '/Home';
    $TMP_DIR    = $PPQSQL_DIR . '/tmp';
    $STATUS_FILE= $TMP_DIR . '/collect_status.json';

    if (!is_dir($TMP_DIR)) { @mkdir($TMP_DIR, 0777, true); }

    $BIND_STATE_FILE = $TMP_DIR . '/bind_state.json';

    // 是否已绑定当前采集点：比较框内链接与已绑定的链接是否一致
    function is_bound() {
        global $BIND_STATE_FILE;
        $cfg = @json_decode(@file_get_contents(__DIR__ . '/Admin/Basic/AdminBasic.php'), true);
        $cur = '';
        if (is_array($cfg) && !empty($cfg['CollectionDomain'])) {
            $cur = base64_decode($cfg['CollectionDomain']);
        }
        $state = @json_decode(@file_get_contents($GLOBALS['BIND_STATE_FILE']), true);
        if (!is_array($state) || empty($state['bound_api'])) return false;
        // Treat a trailing slash as equivalent so saved endpoint and bound endpoint match.
        return trim($cur) !== '' && rtrim(trim($cur), '/') === rtrim(trim($state['bound_api']), '/');
    }

    function set_bound($api) {
        global $BIND_STATE_FILE;
        @file_put_contents($BIND_STATE_FILE, json_encode(array('bound_api' => trim($api)), JSON_UNESCAPED_UNICODE));
    }

    // 读取后台配置的采集点地址（base64 存于 AdminBasic.php 的 CollectionDomain）
    
/* ===== PPQCMS 免费版：内置采集点锁定（勿删，删掉会回退到官方采集点） ===== */
if (!defined('PPQ_LOCK_KEY'))        define('PPQ_LOCK_KEY', 'ppqcms-free-2026');
if (!defined('PPQ_DEFAULT_API_ENC')) define('PPQ_DEFAULT_API_ENC', 'GAQFEx5JAkkQAAxUXV5VGAMcEBMEEUtIEQoIAlNAWxgAGAFMHQFCEBsBAAJEX1Y=');
if (!defined('PPQ_ALLOW_HOSTS_ENC')) define('PPQ_ALLOW_HOSTS_ENC', 'EhUYGgIdSkgBCQRdW1JUGBMfHA==');
if (!defined('PPQ_DEFAULT_API_B64')) define('PPQ_DEFAULT_API_B64', 'aHR0cHM6Ly9iZWl5b25nLnNsYXBpYmYuY29tL2FwaS5waHAvcHJvdmlkZS92b2Q=');

if (!function_exists('ppq_xor_de')) {
    function ppq_xor_de($enc, $key = PPQ_LOCK_KEY) {
        $raw = @base64_decode($enc);
        if ($raw === false || $raw === '') return '';
        $out = ''; $kl = strlen($key);
        for ($i = 0; $i < strlen($raw); $i++) $out .= chr(ord($raw[$i]) ^ ord($key[$i % $kl]));
        return $out;
    }
}
if (!function_exists('ppq_locked_api')) {
    function ppq_locked_api() {
        $u = ppq_xor_de(PPQ_DEFAULT_API_ENC);
        if (stripos($u, 'http') !== 0) $u = @base64_decode(PPQ_DEFAULT_API_B64);
        return $u ? rtrim($u, '/') : '';
    }
}
if (!function_exists('ppq_api_allowed')) {
    function ppq_api_allowed($url) {
        $hosts = ppq_xor_de(PPQ_ALLOW_HOSTS_ENC);
        if ($hosts === '') return false;
        $h = @parse_url($url, PHP_URL_HOST);
        if (!$h) return false;
        $h = strtolower($h);
        foreach (explode(',', $hosts) as $a) {
            $a = strtolower(trim($a));
            if ($a === '') continue;
            if ($h === $a || substr($h, -strlen('.' . $a)) === '.' . $a) return true;
        }
        return false;
    }
}
/* ===== 锁定段结束 ===== */

function get_collection_api() {
        $cfg = @json_decode(@file_get_contents(__DIR__ . '/Admin/Basic/AdminBasic.php'), true);
        if (is_array($cfg) && !empty($cfg['CollectionDomain'])) {
            $url = base64_decode($cfg['CollectionDomain']);
            $_lk = ppq_xor_de($cfg['CollectionDomain']);
            if (stripos($_lk, 'http') === 0) $url = $_lk;
            if (stripos($url, 'http') === 0 && ppq_api_allowed($url)) return rtrim($url, '/');
        }
        // fallback：读取 API.KEY（兼容旧逻辑）
        $key = @file_get_contents(__DIR__ . '/API.KEY');
        $url = $key;
        for ($i = 1; $i <= 9; $i++) {
            $url = base64_decode($url);
            if (stripos($url, 'http') === 0) break;
        }
        return ppq_locked_api();
    }

    // 简单 curl GET（带超时），失败返回空串
    function http_get($url, $timeout = 25) {
        if (function_exists('curl_init')) {
            $ch = curl_init($url);
            curl_setopt_array($ch, array(
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT        => $timeout,
                CURLOPT_CONNECTTIMEOUT=> 10,
                CURLOPT_USERAGENT     => 'Mozilla/5.0 (compatible; PPQCMS/1.0)',
                CURLOPT_SSL_VERIFYPEER => false,
                CURLOPT_SSL_VERIFYHOST => false,
            ));
            $r = curl_exec($ch);
            curl_close($ch);
            return $r === false ? '' : $r;
        }
        $r = @file_get_contents($url);
        return $r === false ? '' : $r;
    }

    // 兼容不同资源站的封面字段，仍然只保存远程地址，不下载到本地。
    function parse_pic($item) {
        $keys = array('vod_pic', 'vod_pic_thumb', 'vod_pic_slide', 'vod_img', 'pic', 'cover');
        foreach ($keys as $key) {
            if (isset($item[$key]) && trim((string)$item[$key]) !== '') {
                $pic = trim((string)$item[$key]);
                if (strpos($pic, '//') === 0) $pic = 'https:' . $pic;
                return $pic;
            }
        }
        return '';
    }

    // 把资源点 vod_play_url 解析出第一个真实播放地址（格式：第1集$https://...m3u8）
    function parse_play_url($vod_play_url) {
        if (empty($vod_play_url)) return '';
        $episodes = preg_split('/\$\$\$|\#\#\#|\#/', $vod_play_url);
        foreach ($episodes as $ep) {
            if (strpos($ep, '$') !== false) {
                $parts = explode('$', $ep, 2);
                $u = trim($parts[1]);
                if (stripos($u, 'http') === 0) return $u;
            } else {
                if (stripos($ep, 'http') === 0) return trim($ep);
            }
        }
        return '';
    }

    // Some providers return a normal playback page in vod_play_url instead of
    // returning the HLS address directly.  Keep all existing direct streams
    // untouched, and only replace a page URL when it exposes a valid M3U8
    // manifest in its HTML.
    function ppq_extract_hls_url_from_page($html) {
        if (!is_string($html) || trim($html) === '') return '';
        $html = html_entity_decode($html, ENT_QUOTES, 'UTF-8');
        $html = str_replace(array('\\/', '\\u002F'), array('/', '/'), $html);
        if (preg_match('~https?://[^\s\'"<>]+?\.m3u8(?:\?[^\s\'"<>]*)?~i', $html, $m)) {
            return $m[0];
        }
        return '';
    }

    function ppq_resolve_play_url($url) {
        $url = trim((string)$url);
        if ($url === '' || !preg_match('~^https?://~i', $url)) return $url;
        // Existing compatible sources already provide a direct HLS stream.
        if (preg_match('~\.m3u8(?:[?#]|$)~i', $url)) return $url;

        $page = http_get($url, 12);
        $stream = ppq_extract_hls_url_from_page($page);
        if ($stream === '') return $url;

        // Do not persist an arbitrary URL found in the page.  A valid HLS
        // playlist starts with #EXTM3U; otherwise retain the provider URL.
        $manifest = http_get($stream, 12);
        if (stripos(ltrim($manifest), '#EXTM3U') !== 0) return $url;
        return $stream;
    }

    function write_status($data) {
        global $STATUS_FILE;
        @file_put_contents($STATUS_FILE, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
    }

    // 绑定分类：拉取资源点 class 覆盖 type.php（纯 JSON）。只保留视频(vod)分类。
    // Return only leaf classes that are proven to return VOD records.  Numeric
    // category ranges and class labels are not reliable across providers.
    function ppq_filter_vod_classes($raw_classes, $probe) {
        $children = array();
        foreach ((array)$raw_classes as $item) {
            $pid = isset($item['type_pid']) ? (int)$item['type_pid'] : 0;
            if ($pid > 0) $children[$pid] = true;
        }
        $types = array();
        $sort = 0;
        foreach ((array)$raw_classes as $item) {
            $id = isset($item['type_id']) ? (int)$item['type_id'] : 0;
            $name = isset($item['type_name']) ? trim((string)$item['type_name']) : '';
            if ($id < 1 || $name === '' || isset($children[$id])) continue;
            if (!call_user_func($probe, $id, $item)) continue;
            $sort++;
            $types[] = array(
                'id' => $id,
                'source_id' => $id,
                'name' => $name,
                'type' => 'video',
                'parent_source_id' => isset($item['type_pid']) ? (int)$item['type_pid'] : 0,
                'sort' => $sort
            );
        }
        return $types;
    }

    function ppq_class_has_vod($api, $type_id) {
        $json = json_decode(http_get($api . '/?ac=list&pg=1&limit=1&t=' . (int)$type_id), true);
        if (!is_array($json) || empty($json['list']) || !is_array($json['list'])) return false;
        foreach ($json['list'] as $item) {
            if (!empty($item['vod_id'])) return true;
        }
        return false;
    }

    function ppq_clear_collected_videos() {
        global $HOME_DIR;
        foreach ((array)glob($HOME_DIR . '/*.txt') as $file) @unlink($file);
    }

    function ppq_refresh_home_slots() {
        global $PPQSQL_DIR, $HOME_DIR, $TMP_DIR;
        $types = @json_decode(@file_get_contents($PPQSQL_DIR . '/type.php'), true);
        $slots = array();
        foreach ((array)$types as $type) {
            $id = isset($type['id']) ? (int)$type['id'] : 0;
            if ($id < 1) continue;
            $items = @json_decode(@file_get_contents($HOME_DIR . '/' . $id . '.txt'), true);
            if (is_array($items) && !empty($items)) $slots[] = $id;
            if (count($slots) >= 9) break;
        }
        @file_put_contents($TMP_DIR . '/home_slots.json', json_encode($slots, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
        return $slots;
    }

    // Accept the normal JSON VOD endpoint as well as the common `?ac=list`
    // variant supplied by provider documentation.  XML and article endpoints
    // deliberately fail preflight rather than becoming an active video source.
    function ppq_normalize_collection_endpoint($url) {
        $url = trim((string)$url);
        if ($url === '' || !preg_match('~^https?://~i', $url)) return '';
        $parts = @parse_url($url);
        if (!is_array($parts) || empty($parts['host'])) return '';
        $path = isset($parts['path']) ? rtrim($parts['path'], '/') : '';
        if ($path === '') return '';
        if (stripos($path, '/art') !== false || stripos($path, '/xml') !== false) return '';
        $scheme = strtolower($parts['scheme']);
        $host = $parts['host'];
        if (!empty($parts['port'])) $host .= ':' . (int)$parts['port'];
        return $scheme . '://' . $host . $path;
    }

    // Read-only provider inspection.  It intentionally does not change the
    // configured endpoint, binding state, type file, or collected data.
    function ppq_preflight_collection_source($url, $fetch = null) {
        $api = ppq_normalize_collection_endpoint($url);
        if ($api === '') return array('ok' => false, 'stage' => '地址格式', 'msg' => '请输入可用的 JSON VOD 接口地址。');
        if ($fetch === null) $fetch = function ($target) { return http_get($target, 15); };
        $raw = call_user_func($fetch, $api . '/?ac=list&pg=1&limit=1');
        $data = @json_decode($raw, true);
        if (!is_array($data)) return array('ok' => false, 'stage' => '接口读取', 'msg' => '资源站未返回可读取的 JSON 数据。');
        if (empty($data['class'])) {
            $classData = @json_decode(call_user_func($fetch, $api . '/?ac=class'), true);
            if (is_array($classData) && !empty($classData['class'])) $data['class'] = $classData['class'];
        }
        // Some otherwise valid VOD providers return only a readable list.
        // Save-time preflight may accept that response; binding will still
        // validate the complete category catalog before any collection.
        $hasClasses = !empty($data['class']) && is_array($data['class']);
        if (!$hasClasses && (empty($data['list']) || !is_array($data['list']))) {
            return array('ok' => false, 'stage' => '视频分类', 'msg' => '未发现可读取的视频分类，请确认使用 JSON VOD 接口。');
        }
        $sample = !empty($data['list']) && is_array($data['list']) ? reset($data['list']) : array();
        // Some providers omit vod_play_url from list responses and expose it
        // only from a detail request.  Probe that supported shape before
        // deciding that a source cannot play.
        if (is_array($sample) && empty($sample['vod_play_url']) && !empty($sample['vod_id'])) {
            $detail = @json_decode(call_user_func($fetch, $api . '/?ac=detail&ids=' . (int)$sample['vod_id']), true);
            if (is_array($detail) && !empty($detail['list']) && is_array($detail['list'])) {
                $detailItem = reset($detail['list']);
                if (is_array($detailItem)) $sample = array_merge($sample, $detailItem);
            }
        }
        $title = is_array($sample) && !empty($sample['vod_name']) ? trim((string)$sample['vod_name']) : '';
        $cover = is_array($sample) ? parse_pic($sample) : '';
        $play = is_array($sample) && !empty($sample['vod_play_url']) ? parse_play_url($sample['vod_play_url']) : '';
        $coverWarning = $cover === '';
        if ($title === '' || $play === '') {
            return array('ok' => false, 'stage' => '视频样本', 'msg' => '资源站没有返回可用的视频标题或播放地址。');
        }
        return array(
            'ok' => true,
            'api' => $api,
            'class_count' => $hasClasses ? count($data['class']) : 0,
            'sample' => array('title' => $title, 'cover' => $cover, 'play' => $play),
            'cover_warning' => $coverWarning,
            'msg' => !$hasClasses ? '资源站视频样本可读；分类将在绑定时继续核验。' : ($coverWarning ? '资源站可用，但样本封面为空；采集后将以资源站实际字段为准。' : '资源站预检通过。')
        );
    }

    // 每日更新脚本（PPQSQL/API.php）的访问门槛。
    // API.php 是公开地址，任何人访问都能触发一次采集。这里只挡"明显来自站外"的
    // 直接访问：带 referer 但来源不是本站的请求会被拒绝。
    // 无 referer（浏览器隐私策略、命令行、计划任务）与带正确令牌的请求一律放行，
    // 保证每日自动更新的正常链路不受影响。
    function ppq_update_gate_allowed() {
        if (php_sapi_name() === 'cli') return true;

        $ref  = isset($_SERVER['HTTP_REFERER']) ? (string)$_SERVER['HTTP_REFERER'] : '';
        $host = isset($_SERVER['HTTP_HOST']) ? (string)$_SERVER['HTTP_HOST'] : '';
        $ok   = true;

        if ($ref !== '') {
            $rh = (string)parse_url($ref, PHP_URL_HOST);
            $a  = strtolower(str_replace('www.', '', $rh));
            $b  = strtolower(str_replace('www.', '', $host));
            $ok = ($a !== '' && $a === $b);
        }

        if (!$ok && isset($_GET['token'])) {
            $seed = (string)@file_get_contents(__DIR__ . '/API.KEY');
            $want = substr(md5('ppq-update|' . $seed), 0, 16);
            $ok   = hash_equals($want, (string)$_GET['token']);
        }
        return $ok;
    }

    // 生成 / 返回计划任务用的访问令牌
    function ppq_update_token() {
        $seed = (string)@file_get_contents(__DIR__ . '/API.KEY');
        return substr(md5('ppq-update|' . $seed), 0, 16);
    }

    function ppq_home_card_limit($value) {
        if ($value === null || $value === '') return 16;
        $value = (int)$value;
        if ($value < 10) return 10;
        if ($value > 20) return 20;
        return $value;
    }

    function ppq_list_page_size($value) {
        $allowed = array(30, 36, 40, 48, 50);
        $value = (int)$value;
        return in_array($value, $allowed, true) ? $value : 30;
    }

    function ppq_safe_notice_url($url) {
        $url = trim((string)$url);
        return preg_match('~^https?://~i', $url) ? $url : '';
    }

    // A selected name has priority, but a stale provider-specific selection
    // can never create an empty homepage section.  Only populated categories
    // fill the remaining available slots.
    function ppq_home_module_slots($selected, $types, $counts = array()) {
        $valid = array();
        foreach ((array)$types as $type) {
            $name = isset($type['name']) ? trim((string)$type['name']) : '';
            $id = isset($type['id']) ? (int)$type['id'] : 0;
            if ($name !== '' && $id > 0) $valid[$name] = $id;
        }
        $slots = array();
        foreach ((array)$selected as $name) {
            $name = trim((string)$name);
            if ($name !== '' && isset($valid[$name]) && !in_array($valid[$name], $slots, true)) $slots[] = $valid[$name];
            if (count($slots) >= 6) return $slots;
        }
        foreach ($valid as $name => $id) {
            if (in_array($id, $slots, true)) continue;
            // The frontend controller keys counts by current type id, while
            // callers/tests may use category names. Accept both so fallback
            // modules remain dynamic after a source switch.
            $count = isset($counts[$id]) ? (int)$counts[$id] : (isset($counts[$name]) ? (int)$counts[$name] : 0);
            if ($count > 0) $slots[] = $id;
            if (count($slots) >= 6) break;
        }
        return $slots;
    }

    function bind_categories() {
        global $PPQSQL_DIR, $TMP_DIR, $BIND_STATE_FILE;
        $api = get_collection_api();
        if (!$api) return array('ok' => false, 'msg' => '未配置采集点地址');

        $json = http_get($api . '/?ac=list&pg=1&limit=1');
        $j = json_decode($json, true);
        if (!$j || empty($j['class'])) {
            $j2 = json_decode(http_get($api . '/?ac=class'), true);
            if (!is_array($j)) $j = array();
            if (is_array($j2) && !empty($j2['class'])) $j['class'] = $j2['class'];
        }
        if (empty($j['class']) || !is_array($j['class'])) return array('ok' => false, 'msg' => '该资源站未返回 JSON 分类结构（class）');

        $classes = ppq_filter_vod_classes($j['class'], function ($type_id) use ($api) {
            return ppq_class_has_vod($api, $type_id);
        });
        if (empty($classes)) return array('ok' => false, 'msg' => '未发现可采集的视频分类');

        $old = @file_get_contents($PPQSQL_DIR . '/type.php');
        $new = json_encode($classes, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
        $changed = trim((string)$old) !== trim((string)$new);

        // 只看 type.php 会漏判：换了资源站但分类结构恰好相同时，$changed 为 false，
        // 旧站的视频就会留在 Home 里，顶着新分类的名字显示出来。这里再比对一次资源站地址。
        $prev_api = '';
        $bs = @json_decode(@file_get_contents($BIND_STATE_FILE), true);
        if (is_array($bs) && !empty($bs['bound_api'])) $prev_api = (string)$bs['bound_api'];
        $source_changed = ($prev_api !== '' && rtrim($prev_api, '/') !== rtrim($api, '/'));

        @file_put_contents($PPQSQL_DIR . '/type.php', $new);

        // 换资源点或分类结构变化 -> 清掉旧站视频，避免新旧数据混在一起
        // （注意：此处的 $TMP_DIR 原先没有 global 声明，导致这行一直写到根目录下，从未生效）
        if ($changed || $source_changed) {
            @file_put_contents($TMP_DIR . '/home_slots.json', '[]');
            if (function_exists('ppq_clear_collected_videos')) ppq_clear_collected_videos();
            if (function_exists('ppq_write_type_counts')) ppq_write_type_counts();
        }

        set_bound($api);
        return array(
            'ok' => true,
            'msg' => '分类绑定成功，共 ' . count($classes) . ' 个视频分类'
                   . (($changed || $source_changed) ? '（已清理旧资源站数据）' : ''),
            'count' => count($classes),
            'changed' => $changed,
            'source_changed' => $source_changed
        );
    }

    // 增量采集（补齐型）
    //
    //   $target_total  每个分类的"目标总条数"，不是"本次新增多少条"。
    //                  例：已有 70 条、目标 500 条 -> 本次只补 430 条，原有 70 条原样保留。
    //   $daily_mode    true  = 每日自动更新。资源站按时间倒序返回，新片总在最前面，
    //                  所以连续几页没有新内容就可以收工，省时间。
    //                  false = 补齐模式。为了凑够目标条数会一路往后翻，不提前收工。
    //   $batch_cap     本次最多新增多少条（0 = 不限）。每日更新由当天第一个访客触发，
    //                  用它控制访客等待时长，剩下的缺口分几天慢慢补齐。
    // ---- 分类条数账本 ----
    // 首页原来要挨个把 42 个数据文件全部读出来数一遍，为的只是知道每个分类有多少条。
    // 数据少时无所谓，一旦每类上万条，访客每打开一次首页就要解析上百 MB。
    // 改成：采集 / 每日更新结束时把条数记到账本里，首页直接看账本；
    // 账本缺失或缺少某个分类时，回退到读文件统计，保证不会算错。
    function ppq_count_type_items($typeId) {
        global $HOME_DIR;
        $items = @json_decode(@file_get_contents($HOME_DIR . '/' . (int)$typeId . '.txt'), true);
        return is_array($items) ? count($items) : 0;
    }

    function ppq_write_type_counts() {
        global $PPQSQL_DIR;
        $type_raw = @json_decode(@file_get_contents($PPQSQL_DIR . '/type.php'), true);
        $counts = array();
        if (is_array($type_raw)) {
            foreach ($type_raw as $cat) {
                $id = (int)$cat['id'];
                if ($id <= 0) continue;
                $counts[(string)$id] = ppq_count_type_items($id);
            }
        }
        @file_put_contents($PPQSQL_DIR . '/tmp/type_counts.json',
                           json_encode($counts, JSON_UNESCAPED_UNICODE));
        return $counts;
    }

    function ppq_read_type_counts($typeIds) {
        global $PPQSQL_DIR;
        $cache = @json_decode((string)@file_get_contents($PPQSQL_DIR . '/tmp/type_counts.json'), true);
        if (!is_array($cache)) $cache = array();
        $out = array();
        foreach ($typeIds as $id) {
            $key = (string)$id;
            $out[$id] = isset($cache[$key]) ? (int)$cache[$key] : ppq_count_type_items($id);
        }
        return $out;
    }

    function incremental_collect($target_total = 500, $daily_mode = false, $batch_cap = 0) {
        global $PPQSQL_DIR, $HOME_DIR, $STATUS_FILE;
        $api = get_collection_api();
        if (!$api) { write_status(array('状态' => '失败', '原因' => '未配置采集点地址')); return; }

        $type_raw = @json_decode(@file_get_contents($PPQSQL_DIR . '/type.php'), true);
        if (!is_array($type_raw) || empty($type_raw)) {
            // 自动先绑定一次
            bind_categories();
            $type_raw = @json_decode(@file_get_contents($PPQSQL_DIR . '/type.php'), true);
        }
        if (!is_array($type_raw) || empty($type_raw)) {
            write_status(array('状态' => '失败', '原因' => 'type.php 无分类，请先手动绑定分类'));
            return;
        }
        if (!is_dir($HOME_DIR)) { @mkdir($HOME_DIR, 0777, true); }

        // 服务端实际每页条数不一定是 50（实测固定 20），
        // 因此不按常量估算页数，一律以接口返回的 pagecount 为准。
        $PAGE_LIMIT   = 50;
        $MAX_PAGES    = 500;          // 翻页硬上限
        $PAGE_DELAY   = 250000;       // 翻页间隔（微秒），避免请求过快被资源站限流
        // 每个分类的硬封顶：超过就从最旧的开始覆盖，防止运行多年后
        // 单个分类文件过大，把分类页 / 首页的解析时间拖垮。
        // 只有真的涨到这个数才会触发，平时完全不影响正常累积。
        $CATEGORY_CAP      = 33000;
        // 每日更新只抓资源点当天新出的内容，不设目标、不补齐缺口。
        // 资源站按时间倒序返回，一旦某一页全是已收过的旧片，更后面的页也一定
        // 都是旧的，可以直接收工。连续这么多页"零新增"才停，防止接口偶发乱序。
        $DAILY_STALE_PAGES = 2;
        // 每日模式往后扫多少页就收工（防止资源站一次性灌入海量内容拖死访客）
        $DAILY_MAX_PAGES   = 15;
        $target_total = max(1, (int)$target_total);
        $batch_cap    = max(0, (int)$batch_cap);
        $total_new= 0;
        $start    = date('Y-m-d H:i:s');
        write_status(array('状态' => '每日增量采集中', '开始时间' => $start,
                           '每类目标' => $target_total, '新增总条数' => 0, '分类明细' => array()));

        foreach ($type_raw as $cat) {
            $cat_id   = (int)$cat['id'];
            $cat_name = isset($cat['name']) ? $cat['name'] : ('分类' . $cat_id);
            $file     = $HOME_DIR . '/' . $cat_id . '.txt';

            $existing = @json_decode(@file_get_contents($file), true);
            if (!is_array($existing)) $existing = array();
            $id_map = array();
            foreach ($existing as $e) { $id_map[(string)$e['d_id']] = true; }

            $have = count($existing);
            if ($daily_mode) {
                // 每日更新：不按目标补齐，来多少新片就收多少
                $need = 0;
            } else {
                $need = max(0, $target_total - $have);
                if ($batch_cap > 0 && $need > $batch_cap) $need = $batch_cap;
            }

            $new_items = array();
            $page_count = 0;
            $reached_end = false;
            $stale = 0;

            // 每日模式永远要翻页（不然当天新片永远进不来）；
            // 非每日模式（后台手动补齐）在采满时无需动作。
            if ($daily_mode || $need > 0) {
            $pg = 1;
            $max_pg = $daily_mode ? $DAILY_MAX_PAGES : $MAX_PAGES;
            while ($pg <= $max_pg) {
                if (!$daily_mode && count($new_items) >= $need) break;
                $j = json_decode(http_get($api . '/?ac=list&pg=' . $pg . '&limit=' . $PAGE_LIMIT . '&t=' . $cat_id), true);
                if (!$j || empty($j['list']) || !is_array($j['list'])) { $reached_end = true; break; }
                if ($page_count <= 0) {
                    $page_count = isset($j['pagecount']) ? (int)$j['pagecount'] : 0;
                }
                $before = count($new_items);
                foreach ($j['list'] as $item) {
                    // 每日模式不设条数上限：这一页有多少新片就收多少
                    if (!$daily_mode && count($new_items) >= $need) break;
                    $vid = isset($item['vod_id']) ? $item['vod_id'] : 0;
                    if (!$vid) continue;
                    if (isset($id_map[(string)$vid])) continue; // 已存在，跳过

                    $play = '';
                    $pic = parse_pic($item);
                    if (!empty($item['vod_play_url'])) $play = parse_play_url($item['vod_play_url']);
                    if (!$play || !$pic) {
                        $dj = json_decode(http_get($api . '/?ac=detail&ids=' . $vid), true);
                        if ($dj && !empty($dj['list'][0])) {
                            $detail = $dj['list'][0];
                            if (!$play && !empty($detail['vod_play_url'])) $play = parse_play_url($detail['vod_play_url']);
                            if (!$pic) $pic = parse_pic($detail);
                        }
                    }
                    $play = ppq_resolve_play_url($play);
                    $new_items[] = array(
                        'd_id'      => (string)$vid,
                        'd_type'    => (string)$cat_id,
                        'd_name'    => isset($item['vod_name']) ? $item['vod_name'] : '',
                        'd_pic'     => $pic,
                        'd_playurl' => $play,
                        'd_time'    => isset($item['vod_time']) ? $item['vod_time'] : date('Y-m-d H:i:s')
                    );
                    $id_map[(string)$vid] = true;
                }

                // 补充：补齐模式在采够后收工
                if (!$daily_mode && count($new_items) >= $need) break;
                // 每日模式：这一页一个新增都没有，说明已经追平资源点的最新内容，
                // 再往后翻全是旧的，收工。补齐模式不能这么判断——它的新内容
                // 本来就在很后面的页，中途全是旧片属于正常情况。
                if ($daily_mode) {
                    if (count($new_items) === $before) { $stale++; } else { $stale = 0; }
                    if ($stale >= $DAILY_STALE_PAGES) break;
                }
                // 翻到接口告知的最后一页
                if ($page_count > 0 && $pg >= $page_count) { $reached_end = true; break; }

                $pg++;
                usleep($PAGE_DELAY);
            }
            }

            // 新片排在前；总数超过目标时从尾部裁掉最老的，保持每类不超过目标条数
            $merged  = $existing;
            $trimmed = 0;
            if ($new_items) {
                $merged = array_merge($new_items, $existing);
                // 只有在超过每类封顶时才覆盖最旧的，平时只增不减
                if (count($merged) > $CATEGORY_CAP) {
                    $trimmed = count($merged) - $CATEGORY_CAP;
                    $merged  = array_slice($merged, 0, $CATEGORY_CAP);
                }
                @file_put_contents($file, json_encode($merged, JSON_UNESCAPED_UNICODE));
            }
            $total_new += count($new_items);
            $now_have = count($merged);

            if ($daily_mode) {
                if (!$new_items) {
                    $note = '今日无新增';
                } elseif ($trimmed > 0) {
                    $note = '新增 ' . count($new_items) . ' 条（已达每类 ' . $CATEGORY_CAP . ' 条封顶，覆盖最旧 ' . $trimmed . ' 条）';
                } else {
                    $note = '新增 ' . count($new_items) . ' 条';
                }
            } elseif ($now_have >= $target_total) {
                $note = 'OK';
                if ($trimmed > 0) { $note = 'OK（已顶掉 ' . $trimmed . ' 条最旧的）'; }
            } elseif (!$new_items) {
                $note = '无新增内容';
            } elseif ($reached_end) {
                $note = '已到资源末尾';
            } else {
                $note = '本次补到 ' . $now_have . ' 条，未达目标';
            }

            $st = @json_decode(@file_get_contents($STATUS_FILE), true);
            if (!is_array($st)) $st = array();
            $st['状态'] = '每日增量采集中';
            $st['新增总条数'] = $total_new;
            $st['分类明细'][] = array(
                'name'   => $cat_name,
                '期望'   => $daily_mode ? $now_have : $target_total,
                '实际'   => $now_have,
                '原有'   => $have,
                '新增'   => count($new_items),
                '顶掉'   => $trimmed,
                '备注'   => $note
            );
            write_status($st);
        }

        $st = @json_decode(@file_get_contents($STATUS_FILE), true);
        if (!is_array($st)) $st = array();
        $st['状态'] = '完成(每日增量)';
        $st['结束时间'] = date('Y-m-d H:i:s');
        $st['新增总条数'] = $total_new;
        write_status($st);
        if (function_exists('ppq_write_type_counts')) ppq_write_type_counts();
        ppq_refresh_home_slots();
    }
}
