394 lines
17 KiB
PHP
394 lines
17 KiB
PHP
<?php
|
||
namespace app\command;
|
||
use think\console\Command;
|
||
use think\console\Input;
|
||
use think\console\Output;
|
||
use think\console\input\Option;
|
||
use think\console\input\Argument;
|
||
use think\console\input\InputOption;
|
||
use Aria2;
|
||
use think\facade\Cache;
|
||
use think\facade\Log;
|
||
use GuzzleHttp\Client;
|
||
|
||
class Zm extends Command
|
||
{
|
||
private $aria2 = null;
|
||
private $completed = false;
|
||
private $user_id = '8ROYK0G3vGLyyEFjr9DtEQ==';
|
||
|
||
protected function configure()
|
||
{
|
||
// 指令配置
|
||
$this->setName('zm')
|
||
->setDescription('zm command');
|
||
}
|
||
|
||
protected function execute(Input $input, Output $output)
|
||
{
|
||
// 输出结果
|
||
// $output->writeln("Hello, {$name}!");
|
||
// $output->writeln("Option: {$option}");
|
||
$this->aria2 = new Aria2('http://127.0.0.1:16800/jsonrpc','11223344'); //实例化aria2对象
|
||
$this->aria2->setOption('enable-async-dns', true); //设置异步dns解析
|
||
$this->aria2->setOption('enable-http-pipelining', true); //设置http管道化
|
||
$this->aria2->setOption('enable-http-keep-alive', true); //设置http保持连接
|
||
$this->completed = false;
|
||
while($this->completed === false){
|
||
try {
|
||
$this->start();
|
||
} catch (\Exception $e) {
|
||
halt($e);
|
||
sleep(3);
|
||
}
|
||
}
|
||
dump('下载完成!');
|
||
}
|
||
protected function start()
|
||
{
|
||
$savepath = 'E:/zm';
|
||
$lable = $this->getLable();
|
||
if($lable['code'] == 0){
|
||
$leftLabel = $lable['data']['leftLabel'];
|
||
foreach ($leftLabel[0]['children'] as $key => $course) {
|
||
$lable_title = $course['title'];
|
||
$lable_id = $course['id'];
|
||
$next = true;
|
||
$curr_page = 1;
|
||
$curr_total = 0;
|
||
while($next){
|
||
$curr = $this->getCurr($lable_id,$curr_page);
|
||
$currs = $curr['data']['curriculum'];
|
||
foreach ($currs as $key => $value) {
|
||
$curr_total++;
|
||
$curr_item = $value;
|
||
$curr_item_title = $curr_item['title'];
|
||
$c = $savepath . '/' . $lable_title . '/' . $curr_item_title;
|
||
// dump('存储路径',$c);
|
||
is_dir($c) || mkdir($c,0755,true);
|
||
|
||
$author = $savepath . '/' . $lable_title . '/' . $curr_item_title . '/' . '作者.txt';
|
||
$this->savetxt($author,$curr_item['author']);
|
||
|
||
$introduce = $savepath . '/' . $lable_title . '/' . $curr_item_title . '/' . '介绍.txt';
|
||
$this->savetxt($introduce,$curr_item['introduce']);
|
||
|
||
$cover = $curr_item['cover'];
|
||
$this->download($cover,$c,basename($cover));
|
||
|
||
$cover_name = basename($cover);
|
||
$cover_path = "{$c}/{$cover_name}-下载地址.txt";
|
||
$this->savetxt($cover_path,$cover);
|
||
$curr_details = $this->getCurrDetails($curr_item['id']);
|
||
foreach($curr_details['data']['curriculum']['allClassSectionTrue'] as $class){
|
||
$class_path = $c . '/' . $class['title'];
|
||
is_dir($class_path) || mkdir($class_path,0755,true);
|
||
$class_details = $this->getClassDetails($class['id']);
|
||
if($class_details == false){
|
||
dump('课程未授权,跳过下载',$class['title']);
|
||
break;
|
||
}
|
||
dump($class_path,$class_details);
|
||
$class_id = $class_details['data']['classSection']['id'];
|
||
|
||
if(!empty($class_details['data']['classSection']['video'])){
|
||
$file_name = $this->getFileNameFromUrl($class_details['data']['classSection']['video']);
|
||
$downloadurl = $class_path . '/' . $file_name . '-下载地址.txt';
|
||
$this->savetxt($downloadurl,$class_details['data']['classSection']['video']);
|
||
$this->download($class_details['data']['classSection']['video'],$class_path,$file_name);
|
||
}
|
||
if(!empty($class_details['data']['classSection']['courseware'])){
|
||
$file_name = $this->getFileNameFromUrl($class_details['data']['classSection']['courseware']);
|
||
$downloadurl = $class_path . '/' . $file_name . '-下载地址.txt';
|
||
$this->savetxt($downloadurl,$class_details['data']['classSection']['courseware']);
|
||
$this->download($class_details['data']['classSection']['courseware'],$class_path,$file_name);
|
||
}
|
||
if(!empty($class_details['data']['classSection']['construction_draw'])){
|
||
$file_name = $this->getFileNameFromUrl($class_details['data']['classSection']['construction_draw']);
|
||
$downloadurl = $class_path . '/' . $file_name . '-下载地址.txt';
|
||
$this->savetxt($downloadurl,$class_details['data']['classSection']['construction_draw']);
|
||
$this->download($class_details['data']['classSection']['construction_draw'],$class_path,$file_name);
|
||
}
|
||
if(!empty($class_details['data']['classSection']['construction_draw_3d'])){
|
||
$file_name = $this->getFileNameFromUrl($class_details['data']['classSection']['construction_draw_3d']);
|
||
$downloadurl = $class_path . '/' . $file_name . '-下载地址.txt';
|
||
$this->savetxt($downloadurl,urldecode($class_details['data']['classSection']['construction_draw_3d']));
|
||
$this->download($class_details['data']['classSection']['construction_draw_3d'],$class_path,$file_name);
|
||
}
|
||
|
||
foreach($class_details['data']['classSection']['allEnclosure'] as $enclosure){
|
||
$enclosure['url'] = urldecode($enclosure['url']);
|
||
$parts = explode("{$class_id}/", $enclosure['url']);
|
||
|
||
$path = parse_url($enclosure['url'], PHP_URL_PATH);
|
||
$parts = explode('/', $path);
|
||
$storage = $class_path . '/' . urldecode($parts[count($parts)-2]);
|
||
is_dir($storage) || mkdir($storage,0755,true);
|
||
$downloadurl = $storage . '/' . urldecode(end($parts)) . '-下载地址.txt';
|
||
$this->savetxt($downloadurl,urldecode($enclosure['url']));
|
||
$this->download($enclosure['url'],$storage,basename($enclosure['url']));
|
||
}
|
||
}
|
||
}
|
||
$curr_page++;
|
||
if($curr_total >= $curr['data']['total']){
|
||
$next = false;
|
||
break;
|
||
}
|
||
}
|
||
// exit();
|
||
}
|
||
dump($leftLabel);
|
||
}else{
|
||
dump($lable['msg']);
|
||
exit();
|
||
}
|
||
exit();
|
||
}
|
||
private function checkfile($url,$file)
|
||
{
|
||
dump($url,$file);
|
||
if(is_file($file)){
|
||
$size = filesize($file);
|
||
if($size > 0){
|
||
return true;
|
||
}else{
|
||
dump('文件大小为0,重新下载'.$file);
|
||
unlink($file);
|
||
return 2;
|
||
}
|
||
}
|
||
$key = 'zm_check_header_' . md5($url);
|
||
try {
|
||
if(($head = Cache::get($key,null)) === NULL ){
|
||
$head = [];
|
||
|
||
$client = new Client(['timeout' => 5.0,]);
|
||
try {
|
||
// 发送 HEAD 请求,并添加自定义请求头
|
||
$response = $client->head($url, [
|
||
'headers' => [
|
||
'User-Agent'=>'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.489.400 QQBrowser/13.7.6351.400',
|
||
'referer'=>'https://edu.zmrobo.com/courses',
|
||
],
|
||
]);
|
||
|
||
// 读取响应状态码
|
||
$statusCode = $response->getStatusCode();
|
||
if($statusCode != 200){
|
||
dump('文件head请求失败,状态码:'.$statusCode);
|
||
return 0;
|
||
}
|
||
// 获取全部响应头(返回关联数组)
|
||
$headers = $response->getHeaders();
|
||
foreach ($headers as $name => $values) {
|
||
$head[$name] = implode(', ', $values);
|
||
}
|
||
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
||
dump("文件head请求失败: " . $e->getMessage());
|
||
return 0;
|
||
}
|
||
Cache::set($key,$head,86400);
|
||
}
|
||
$kk = 'zm_file_' . pathinfo($file,PATHINFO_BASENAME);
|
||
if(isset($head['Content-Length']) && ($size = intval($head['Content-Length']))){
|
||
$kk .= $size;
|
||
if(is_file($file) && ($size !== filesize($file))){
|
||
dump('文件大小不符,重新下载'.$file);
|
||
unlink($file);
|
||
return 2;
|
||
}
|
||
}
|
||
if(isset($head['ETag'])){
|
||
$t = $kk . md5($head['ETag']);
|
||
if(Cache::has($t)){
|
||
return Cache::get($t);
|
||
}
|
||
}
|
||
if(isset($head['Content-MD5'])){
|
||
$t = $kk . md5($head['Content-MD5']);
|
||
if(Cache::has($t)){
|
||
return Cache::get($t);
|
||
}
|
||
}
|
||
} catch (Exception $e) {
|
||
dump('检查文件失败:',$url,$file,$e);
|
||
exit;
|
||
}
|
||
return 3;
|
||
}
|
||
protected function download($downloadurl,$dir,$filename)
|
||
{
|
||
$result = $this->checkfile($downloadurl,$dir . '/' . $filename);
|
||
|
||
if($result === true){
|
||
dump('文件已存在,跳过下载:' . $filename);
|
||
return;
|
||
}
|
||
$num = 1;
|
||
while($num > 0){
|
||
$activeDownloads = $this->aria2->getGlobalStat();
|
||
$num = intval($activeDownloads['result']['numWaiting']);
|
||
if($num > 50){
|
||
dump('当前任务过多,等待任务数:',$num);
|
||
sleep(1);
|
||
}else{
|
||
break;
|
||
}
|
||
}
|
||
$result = $this->aria2->addUri(
|
||
[$downloadurl],
|
||
['dir'=>$dir,
|
||
'header'=>[
|
||
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.489.400 QQBrowser/13.7.6351.400',
|
||
'referer: https://edu.zmrobo.com/courses',
|
||
],
|
||
'out'=>$filename]
|
||
);
|
||
}
|
||
public function getLable($sign = 'null')
|
||
{
|
||
$client = new Client();
|
||
$result = $client->get("https://edu.zmrobo.com/api/label",[
|
||
'headers'=>[
|
||
'User-Agent'=>'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.489.400 QQBrowser/13.7.6351.400',
|
||
'referer'=>'https://edu.zmrobo.com/courses',
|
||
'E-Date'=>time(),
|
||
'Sign'=>$sign,
|
||
]
|
||
]);
|
||
$result = json_decode($result->getBody()->getContents(),true);
|
||
if($result['code'] == -3){
|
||
return $this->getLable($result['sign']);
|
||
}else{
|
||
return $result;
|
||
}
|
||
}
|
||
public function getCurr($label_id,$page,$sign = 'null')
|
||
{
|
||
$client = new Client();
|
||
$options = [
|
||
'headers'=>[
|
||
'User-Agent'=>'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.489.400 QQBrowser/13.7.6351.400',
|
||
'referer'=>'https://edu.zmrobo.com/courses',
|
||
'E-Date'=>time(),
|
||
'Content-Type'=>'application/json; charset=utf-8',
|
||
'Origin'=>'https://edu.zmrobo.com',
|
||
'Sign'=>$sign,
|
||
],
|
||
'body'=>json_encode([
|
||
'label'=>[[$label_id]],
|
||
'page'=>$page,
|
||
])
|
||
];
|
||
$result = $client->post("https://edu.zmrobo.com/api/curriculum/list",$options);
|
||
$result = json_decode($result->getBody()->getContents(),true);
|
||
if($result['code'] == -3){
|
||
return $this->getCurr($label_id,$page,$result['sign']);
|
||
}else if($result['code'] == 0){
|
||
return $result;
|
||
}else{
|
||
dump($result['msg']);
|
||
exit();
|
||
}
|
||
}
|
||
|
||
|
||
public function getCurrDetails($curr_id,$sign = 'null')
|
||
{
|
||
$client = new Client();
|
||
$options = [
|
||
'headers'=>[
|
||
'User-Agent'=>'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.489.400 QQBrowser/13.7.6351.400',
|
||
'referer'=>'https://edu.zmrobo.com/courses',
|
||
'E-Date'=>time(),
|
||
'Content-Type'=>'application/json; charset=utf-8',
|
||
'Origin'=>'https://edu.zmrobo.com',
|
||
'Sign'=>$sign,
|
||
],
|
||
'body'=>json_encode([
|
||
'id'=>$curr_id,
|
||
'user_id'=>$this->user_id,
|
||
])
|
||
];
|
||
$result = $client->post("https://edu.zmrobo.com/api/curriculum/details",$options);
|
||
$result = json_decode($result->getBody()->getContents(),true);
|
||
if($result['code'] == -3){
|
||
return $this->getCurrDetails($curr_id,$result['sign']);
|
||
}else if($result['code'] == 0){
|
||
return $result;
|
||
}else{
|
||
dump($result['msg']);
|
||
exit();
|
||
}
|
||
}
|
||
public function getClassDetails($curr_id,$sign = 'null')
|
||
{
|
||
$client = new Client();
|
||
$options = [
|
||
'headers'=>[
|
||
'User-Agent'=>'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.489.400 QQBrowser/13.7.6351.400',
|
||
'referer'=>'https://edu.zmrobo.com/courses',
|
||
'E-Date'=>time(),
|
||
'Content-Type'=>'application/json',
|
||
'Origin'=>'https://edu.zmrobo.com',
|
||
'Sign'=>$sign,
|
||
],
|
||
'body'=>json_encode([
|
||
'id'=>$curr_id,
|
||
'user_id'=>$this->user_id,
|
||
'token'=>"7aa22a83fece93ed3e5a3119b085ced9",
|
||
])
|
||
];
|
||
$result = $client->post("https://edu.zmrobo.com/api/class/details",$options);
|
||
$result = json_decode($result->getBody()->getContents(),true);
|
||
if($result['code'] == -3){
|
||
return $this->getClassDetails($curr_id,$result['sign']);
|
||
}else if($result['code'] == 0){
|
||
return $result;
|
||
}else if($result['code'] == -5){
|
||
return false;
|
||
}else{
|
||
dump($result['msg']);
|
||
exit();
|
||
}
|
||
}
|
||
|
||
|
||
|
||
private function savetxt(string $file,string $content)
|
||
{
|
||
if(is_file($file)){
|
||
unlink($file);
|
||
}
|
||
$myfile = fopen($file, "w");
|
||
fwrite($myfile, $content);
|
||
fclose($myfile);
|
||
// dump('已保存文本内容:'.$file);
|
||
}
|
||
/**
|
||
* 从URL中提取并解码文件名
|
||
* @param string $url 包含文件名的URL
|
||
* @param bool $throwExceptions 是否抛出异常 (默认true)
|
||
* @return string|null 成功返回文件名,失败返回null(仅当$throwExceptions=false时)
|
||
* @throws \InvalidArgumentException
|
||
*/
|
||
function getFileNameFromUrl(string $url, bool $throwExceptions = true): ?string
|
||
{
|
||
// 解析URL结构
|
||
$parsed = parse_url($url);
|
||
|
||
// 错误检测逻辑
|
||
if ($parsed === false || !isset($parsed['path'])) {
|
||
$errorMsg = "无效的URL格式: " . json_encode($url);
|
||
if ($throwExceptions) {
|
||
throw new \InvalidArgumentException($errorMsg);
|
||
}
|
||
trigger_error($errorMsg, E_USER_WARNING);
|
||
return null;
|
||
}
|
||
|
||
// 提取并解码文件名
|
||
return urldecode(basename($parsed['path']));
|
||
}
|
||
} |