Files
pull-courseware/app/command/Zm.php
2025-04-18 15:38:58 +08:00

432 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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;
use GuzzleHttp\HandlerStack;
use Kevinrob\GuzzleCache\CacheMiddleware;
use Kevinrob\GuzzleCache\Strategy\Delegate\RequestMatcher;
use Kevinrob\GuzzleCache\Strategy\GreedyCacheStrategy;
use Kevinrob\GuzzleCache\Storage\Psr6CacheStorage;
use Symfony\Component\Cache\Adapter\RedisAdapter;
class Zm extends Command
{
private $aria2 = null;
private $completed = false;
private $client = null;
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保持连接
$cachePath = runtime_path() . 'guzzle_cache';
$this->client = new Client([
'verify' => false,
]);
$this->completed = false;
while($this->completed === false){
try {
$this->start();
} catch (\Exception $e) {
halt($e);
sleep(3);
}
}
dump('下载完成!');
}
protected function start()
{
$savepath = 'D:/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 = trim($savepath . '/' . $lable_title . '/' . $curr_item_title);
// dump('存储路径',$c);
try {
is_dir($c) || mkdir($c,0755,true);
} catch (\Exception $e) {
dump('创建目录失败:',$c,$e->getMessage());
throw $e;
}
$author = $c . '/' . '作者.txt';
$this->savetxt($author,$curr_item['author']);
$introduce = $c . '/' . '介绍.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 = trim($c . '/' . $class['title']);
try {
is_dir($class_path) || mkdir($class_path,0755,true);
} catch (\Exception $e) {
dump('创建目录失败:',$class_path,$e->getMessage());
throw $e;
}
$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 = [];
try {
// 发送 HEAD 请求,并添加自定义请求头
$response = $this->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')
{
$result = $this->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')
{
$cacheKey = 'zm_curr_' . $label_id . '_' . $page;
if(Cache::has($cacheKey)){
return Cache::get($cacheKey);
}
$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 = $this->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){
Cache::set($cacheKey,$result,86400);
return $result;
}else{
dump($result['msg']);
exit();
}
}
public function getCurrDetails($curr_id,$sign = 'null')
{
$cacheKey = 'zm_curr_details_' . $curr_id;
if(Cache::has($cacheKey)){
return Cache::get($cacheKey);
}
$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 = $this->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){
Cache::set($cacheKey,$result,86400);
return $result;
}else{
dump($result['msg']);
exit();
}
}
public function getClassDetails($curr_id,$sign = 'null')
{
$cacheKey = 'zm_class_details_' . $curr_id;
if(Cache::has($cacheKey)){
return Cache::get($cacheKey);
}
$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 = $this->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){
Cache::set($cacheKey,$result,86400);
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']));
}
}