登录 白背景
<?php
/*
新版本接口 ps 页内数量
https://api.bilibili.com/x/space/arc/search?mid=223146252&ps=30&tid=0&pn=1&keyword=&order=pubdate&jsonp=jsonp
fakedanmu
https://api.bilibili.com/x/v2/dm/ajax?callback=jQuery17208966159722380538_1624984205756&aid=BV1464y197zG&jsonp=jsonp&_=1624984533780
https://api.bilibili.com/x/v2/dm/ajax?aid=BV1464y197zG
*/
// ini_set("display_errors", "On");//打开错误提示
// ini_set("error_reporting", E_ALL);//显示所有错误
date_default_timezone_set('Asia/Shanghai');
//页面展示视频数量
define('SHOW_COUNT', 30);
//一个up主最多获取视频个数
define('SINGLE_LIMIT', 3);

//bilibili关注id

$UP = [
    //=====科普学习=====
    //李永乐老师
    //9458053,
    //妈咪说
    //223146252,
    //张召忠
    // 33683045,
    //芳斯塔芙
    72270557,
    //zettaranc
    // 326246517,
    //表象的本质
    //363584502,
    //奇闻观察室
    454719565,
    //硬核的半佛仙人
    //37663924,
    //毕导THU
    254463269,
    //智慧君
    // 20259914,
    //天天鉴宝
    429392011,
    //戴建业老师
    532741557,
    //TED超级演讲家
    // 359516438,
    //苑举正教授
    518566904,
    //罗翔说刑法
    517327498,
    //老师好我叫何同学
    // 163637592,
    //无穷小亮的科普日常
    14804670,
    //老爸评测
    347667464,
    //老奇好好奇
    35894872,
    
    //=====工作和技术=====
    //阿里达摩院扫地僧
    //483104470,
    //小米公司
    23920239,
    //小米商城官方
    // 60253266,
    //小米情报官
    383423248,
    
    //=====资讯=====
    //央视新闻
    456664753,
    //秀才侃娱乐
    //41288240,
    //观察者网
    10330740,
    //喵喵折App
    // 338748561,
    //郭杰瑞
    176037767,
    
    //=====鬼畜========
    //OELoop
    6171645,
    //倒悬的橘子
    40966108,
    
    //=======游戏========
    //上海滩许van强
    23604445,
    //LPL战术小黑板
    594534726,
    //靠谱电竞->靠谱盘点
    279991456,
    //STEAM情报局
    473519710,
    //Steam官方特惠
    // 518876755,
    //哔哩哔哩英雄联盟赛事
    50329118,
    //CD_PROJEKT_RED
    //271442527,
    //罗汉解说
    51896064,
    
    //=====杨超越=====
    //午夜出动
    353407160,
    //杨超越视频站
    // 107961740,
    //杨超越盖世无敌可爱
    // 4981142,
    //杨超越
    // 630836232,
    
    
    //=====美食=====
    //品诺美食开课啦
    352637817,
    //厨师长农国栋
    415479453,
    //美食作家王刚
    290526283,
    //真探唐仁杰
    544336675,
    //华农兄弟
    // 250858633,
    //吃货请闭眼
    // 94114029,
    //盗月社食遇记
    //99157282,
    
    //=====数码=====
    //科技美学-大家测
    346098914,
    //科技美学
    3766866,
    //钟文泽
    25910292,
    //远古时代装机猿
    //35359510,
    //极客湾Geekerwan
    25876945,
    //阿狸才不是受
    //453972,
    //蒸気火鸡
    39180492,
    
    //=====VLOG生活=====
    //峰哥亡命天涯
    35847683,
    //孝警阿特
    477531206,
    //拜托了小翔哥
    353539995,
    //麻雀喳喳野行记
    120122306,
    //敬汉卿
    9824766,
    //机智的党妹
    //466272,
    //伢伢gagako
    //632887,
    //视角姬
    //52250,
    //大胡子魏
    // 438880209,
    
    //=====音乐=====
    //bibipiano
    // 419743655,
    //郎朗piano
    // 479893558,
    
    //=====搞笑=====
    //手工耿
    280793434,
    //凉风
    14110780,
    //朱一旦
    //437316738,
    //陈翔六点半
    19286458,
    //惊天碉堡团
    // 471870511,
    
    //=====影视解说=====
    //导演小策
    81824112,
    //木鱼水心
    927587,
    //电影最TOP
    17819768,
    //哔哩哔哩电影
    15773384,
    //YouTube精选字幕组
    //487511093,
    //大象放映室
    49246269,
];
class bili{
    private $UP;
    private $last_bvid;
    private $old_file;
    private $old_json;
    private $new_json;
    private $type;
    private $up_img;
    private $test = false;
    
    //我的关注
    const MY_FOLLOW = 'myFollow';
    //排行榜
    const NOW_RANKING = 'nowRanking';
    //每周必看
    const WEEK_RANKING = 'weekRanking';
    const HOT_RANKING = 'hotRanking';
    const TESTING = 'testing';
    
    public function __construct($UP, $type){
        $this->UP = $UP;
        if(in_array($type, [self::MY_FOLLOW,self::NOW_RANKING,self::WEEK_RANKING,self::HOT_RANKING,self::TESTING])){
            $this->type = $type;
        }else{
            $this->type = self::HOT_RANKING;
        }
        //加载缓存json文件
        if (file_exists($this->getTypePath())){
            $this->old_file = file_get_contents($this->getTypePath());
            $this->old_json = json_decode($this->old_file, 1);
        }else{
            $this->old_json['last_time'] = 0;
            $this->old_json['weekNumber'] = 0;
            $this->old_json['GMT'] = gmdate("D, d M Y H:i:s")." GMT";
        }
    }
    //获取路径
    private static function getPath($name){
        return dirname(__FILE__) . DIRECTORY_SEPARATOR . $name;
    }
    //获取路径
    private function getTypePath($name = ''){
        return dirname(__FILE__) . DIRECTORY_SEPARATOR . 'bili_' . $this->type . '.json';
    }
    //主流程 外层echo
    public function execute(){
        if(self::TESTING == $this->type){
            return json_encode($_SERVER);
        }
        if(self::MY_FOLLOW == $this->type){
            $this->last_bvid = ($_GET['last_bvid']??0);
            $this->up_img = $this->getUPImg();
            $this->updateJson();
            file_put_contents($this->getTypePath(), json_encode($this->new_json));
            return $this->buildResp();
        }
        if(self::NOW_RANKING == $this->type){
            if(file_exists($this->getTypePath()) && !$this->ifUpdateJson(3600)){
                $this->checkCacheTime($this->old_json['GMT']);
                return $this->old_file;
            }
            $html = self::curlGet('https://www.bilibili.com/v/popular/rank/all');
            $html = explode("<script>window.__INITIAL_STATE__=", $html);
            $html = explode(";(function()", $html[1]);
            $jsonObj = json_decode($html[0]);
            $list = [];
            foreach($jsonObj->rankList as $item){
                $list[] = self::v_popular_rank_all($item, $item->owner->face);
            }
            $retData = json_encode($this->buildJson($list));
            file_put_contents($this->getTypePath(), $retData);
            return $retData;
        }
        if(self::WEEK_RANKING == $this->type){
            //每周五晚 18:00 更新
            //相同则不更新
            if(file_exists($this->getTypePath()) && $this->old_json['weekNumber'] == self::getWeekNumNow()){
                $this->checkCacheTime($this->old_json['GMT']);
                return $this->old_file;
            }
            $jsonObj = self::biliGet('https://api.bilibili.com/x/web-interface/popular/series/one?number=' . self::getWeekNumNow());
            $list = [];
            foreach($jsonObj->list as $item){
                $list[] = self::v_popular_rank_all($item, $item->owner->face);
            }
            $retData = json_encode($this->buildJson($list, ['weekNumber' => $jsonObj->config->number]));
            file_put_contents($this->getTypePath(), $retData);
            return $retData;
        }
        if(self::HOT_RANKING == $this->type){
            if(file_exists($this->getTypePath()) && !$this->ifUpdateJson(3600)){
                $this->checkCacheTime($this->old_json['GMT']);
                return $this->old_file;
            }
            $jsonObj = self::biliGet('https://api.bilibili.com/x/web-interface/popular?ps=50&pn=1');
            $list = [];
            foreach($jsonObj->list as $item){
                $list[] = self::v_popular_rank_all($item, $item->owner->face);
            }
            $retData = json_encode($this->buildJson($list));
            file_put_contents($this->getTypePath(), $retData);
            return $retData;
        }
        return 'fatal';
    }
    private function checkCacheTime($GMT){
        header("Last-Modified: " . $GMT);
        if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $GMT == $_SERVER['HTTP_IF_MODIFIED_SINCE']){
            header("HTTP/1.1 304 Not Modified");
            exit();
        }
    }
    //返回值
    private function buildResp() : string{
        $ret = $this->new_json;
        //当前端aid与当前一致,则不需要重写前端页面
        if(!empty($this->new_json['list']) && $this->last_bvid == $this->new_json['list'][0]['bvid']){
            $ret['ifRefresh'] = false;
        }
        $ret['ifRefresh'] = true;
        return json_encode($ret);
    }
    //up主头像
    private function getUPImg() : array{
        $imgSavePath = self::getPath('bili_up_img.json');
        //文件缓存
        if(file_exists($imgSavePath)){
            $ret = json_decode(file_get_contents($imgSavePath),1);
        }else{
            $ret = [];
        }
        $update_flag = false;
        foreach($this->UP as $item){
            if(is_numeric($item)){
                //数字配置
                $mid = $item;
            }else if(is_array($item) && isset($item['mid'])){
                //数组配置
                $mid = $item['mid'];
            }else if(is_array($item) && empty($item['mid'])){
                //搜索型配置,指定upId作为头像 暂无搜索型
                $mid = $item['upimg_id'];
            }
            if(isset($ret[$mid])){
                //操作存量数据 todo delete
                $ret[$mid] = self::getImgName($ret[$mid]);
                continue;
            }
            $update_flag = true;
            $url = 'https://api.bilibili.com/x/space/acc/info?mid=' . $mid;
            $respObj = self::biliGet($url);
            //使用参数设置头像为64*64
            $ret[$mid] = self::getImgName($respObj->face);
        }
        if($update_flag){
            file_put_contents($imgSavePath, json_encode($ret));
        }
        return $ret;
    }
    
    //判断是否更新文件
    private function ifUpdateJson($timeSec = 600) : bool{
        //文件不存在
        if($this->old_json['last_time'] == 0){
            return true;
        }
        //文件更新时间超时
        if($this->old_json['last_time'] + $timeSec < time()){
            return true;
        }
        return false;
    }
    //更新json信息,并更新cover图片
    private function updateJson(){
        if(!$this->ifUpdateJson()){
            $this->new_json = $this->old_json;
            return;
        }
        //parse配置信息
        $jsonData = [];
        foreach($this->UP as $item){
            if(is_int($item)){
                $jsonData = array_merge($jsonData, $this->upList(['mid'=>$item]));

            }else if(is_array($item) && isset($item['mid'])){
                $jsonData = array_merge($jsonData, $this->upList($item));

            }else if(is_array($item) && empty($item['mid'])){
                continue;
            }
        }
        krsort($jsonData);
        $jsonData = array_values(array_slice($jsonData, 0, SHOW_COUNT));
        $this->new_json = self::buildJson($jsonData);
    }
    //构造返回值
    private static function buildJson(&$list, $extend = []){
        $ret = [
            'list' => $list,
            'last_time_str' => date('Y-m-d H:i:s'),
            'last_time' => time(),
            'GMT' => gmdate("D, d M Y H:i:s")." GMT",
        ];
        foreach($extend as $key => $val){
            $ret[$key] = $val;
        }
        return $ret;
    }
    //根据up主id检索视频信息
    private function upList($conf) : array{
        $param = [
            'tid'=>'0',
            //最多显示SINGLE_LIMIT个
            'ps'=>SINGLE_LIMIT,
            //第一页
            'pn'=>'1',
            //在当前up的投稿中搜索
            'keyword'=>isset($conf['keyword']) ? urlencode($conf['keyword']) : '', 
            //按照时间排序
            'order'=>'pubdate',
            //up主id
            'mid'=>$conf['mid'],
        ];
        $url = 'https://api.bilibili.com/x/space/arc/search?';
        foreach($param as $key=>$item2){
            $url .= $key . '=' . $item2 . '&';
        }
        $respData = self::biliGet($url);
        $ret = [];
        foreach($respData->list->vlist as $item){
            $key = sprintf("%s%012d", $item->created, $item->mid);
            $ret[$key] = self::x_space_arc_search($item, $this->up_img[$conf['mid']]);
            $ret[$key]['mid'] = $conf['mid'];
        }
        return $ret;
    }
    //格式化
    private static function v_popular_rank_all(&$json,&$head) : array{
        return [
            'bvid' => $json->bvid,
            'title' => $json->title,
            'desc' => $json->desc,//\n点赞:{$json->stat->like}\n投币:{$json->stat->coin}\n收藏:{$json->stat->favorite}
            'author' => $json->owner->name,
            'mid' => $json->owner->mid,
            'pic' => self::getImgName($json->pic),
            'created' => isset($json->created) ? $json->created : $json->pubdate,
            'face' => self::getImgName($head),
        ];
    }
    //格式化
    private static function x_space_arc_search(&$json,&$head) : array{
        return [
            'bvid' => $json->bvid,
            'title' => $json->title,
            'desc' => $json->description,
            'author' => $json->author,
            'mid' => $json->mid,//可能为合作视频
            'pic' => self::getImgName($json->pic),
            'created' => isset($json->created) ? $json->created : $json->pubdate,
            'face' => self::getImgName($head),
        ];
    }
    //获取周数自2019
    private static function getWeekNumNow() : int{
        $time = time() - strtotime("2019-03-22 18:05:00");
        return (int)($time / (3600*24*7));
    }
    //图片链接
    private static function getImgName($url) : string{
        return str_replace("http://",'//',$url);
    }
    //返回json.data
    private static function biliGet($url) : object {
        $obj = json_decode(self::curlGet($url));
        return $obj->data;
    }
    //请求url
    private static function curlGet($url) : string{
        //初始化
        $ch = curl_init();
        //设置选项,包括URL
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);//跟随跳转
        curl_setopt($ch, CURLOPT_USERAGENT,'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36');
        curl_setopt($ch, CURLOPT_REFERER, 'https://www.bilibili.com');
        //执行并获取HTML文档内容
        $output = curl_exec($ch);
        //释放curl句柄
        curl_close($ch);
        //打印获得的数据
        return $output;
    }
}

//更新json 并展示
if (!empty($_GET)) {
    $obj = new bili($UP, $_GET['type']);
}else{
    $obj = new bili($UP, bili::MY_FOLLOW);
}

echo $obj->execute();