ExportChapter.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. <?php
  2. namespace App\Console\Commands;
  3. use Illuminate\Console\Command;
  4. use Illuminate\Support\Facades\Storage;
  5. use App\Models\ProgressChapter;
  6. use App\Models\Channel;
  7. use App\Models\PaliText;
  8. use App\Models\Sentence;
  9. use App\Http\Api\ChannelApi;
  10. use App\Http\Api\MdRender;
  11. use App\Tools\Export;
  12. use Illuminate\Support\Facades\Log;
  13. use App\Tools\RedisClusters;
  14. use Illuminate\Support\Str;
  15. class ExportChapter extends Command
  16. {
  17. protected $exportStatusKey = 'export/status';
  18. protected $exportStatusExpiry = 3600;
  19. protected $currExportStatusKey = '';
  20. protected $realFilename = null;
  21. /**
  22. * The name and signature of the console command.
  23. * php artisan export:chapter 213 1913 a19eaf75-c63f-4b84-8125-1bce18311e23 213-1913 --format=html
  24. * php artisan export:chapter 168 915 19f53a65-81db-4b7d-8144-ac33f1217d34 168-915.html --format=html
  25. * php artisan export:chapter 168 915 19f53a65-81db-4b7d-8144-ac33f1217d34 168-915.html --format=html --origin=true
  26. * @var string
  27. */
  28. protected $signature = 'export:chapter {book} {para} {channel} {filename} {--origin=false} {--translation=true} {--debug} {--format=tex} ';
  29. /**
  30. * The console command description.
  31. *
  32. * @var string
  33. */
  34. protected $description = 'Command description';
  35. /**
  36. * Create a new command instance.
  37. *
  38. * @return void
  39. */
  40. public function __construct()
  41. {
  42. parent::__construct();
  43. }
  44. /**
  45. * progress: 0-1, error -1
  46. * message: string
  47. */
  48. protected function setStatus($progress,$message=''){
  49. $data = [
  50. 'progress'=>$progress,
  51. 'message'=>$message,
  52. 'content-type'=>'application/zip',
  53. ];
  54. if($this->realFilename){
  55. $data['filename'] = $this->realFilename;
  56. }
  57. RedisClusters::put($this->currExportStatusKey,
  58. $data,
  59. $this->exportStatusExpiry);
  60. $percent = (int)($progress * 100);
  61. $this->info("[{$percent}%]".$message);
  62. }
  63. public function getStatus($filename){
  64. return RedisClusters::get($this->exportStatusKey.'/'.$filename);
  65. }
  66. /**
  67. * Execute the console command.
  68. *
  69. * @return int
  70. */
  71. public function handle()
  72. {
  73. $this->info('task export chapter start');
  74. Log::debug('task export chapter start');
  75. if(\App\Tools\Tools::isStop()){
  76. return 0;
  77. }
  78. $m = new \Mustache_Engine(array('entity_flags'=>ENT_QUOTES,
  79. 'delimiters' => '[[ ]]',
  80. 'escape'=>function ($value){
  81. return $value;
  82. }));
  83. $tplParagraph = file_get_contents(resource_path("mustache/".$this->option('format')."/paragraph.".$this->option('format')));
  84. MdRender::init();
  85. $this->currExportStatusKey = $this->exportStatusKey . '/' . $this->argument('filename');
  86. $this->realFilename = $this->argument('filename').'.zip';
  87. switch ($this->option('format')) {
  88. case 'md':
  89. $renderFormat='markdown';
  90. break;
  91. case 'html':
  92. $renderFormat='html';
  93. break;
  94. default:
  95. $renderFormat=$this->option('format');
  96. break;
  97. }
  98. $book = $this->argument('book');
  99. $para = $this->argument('para');
  100. //zip压缩包里面的文件名
  101. $realFileName = "{$book}-{$para}.".$this->option('format');
  102. //获取原文channel
  103. $orgChannelId = ChannelApi::getSysChannel('_System_Pali_VRI_');
  104. $tranChannelsId = explode('_',$this->argument('channel'));
  105. $channelsId = array_merge([$orgChannelId],$tranChannelsId);
  106. $channels = array();
  107. $channelsIndex = array();
  108. foreach ($channelsId as $key => $id) {
  109. $channels[] = ChannelApi::getById($id);
  110. $channelsIndex[$id] = ChannelApi::getById($id);
  111. }
  112. $bookMeta = array();
  113. $bookMeta['book_author'] = "";
  114. foreach ($channels as $key => $channel) {
  115. $bookMeta['book_author'] .= $channel['name'] . ' ';
  116. }
  117. $chapter = PaliText::where('book',$book)
  118. ->where('paragraph',$para)->first();
  119. if(!$chapter){
  120. return $this->error("no data");
  121. }
  122. $currProgress = 0;
  123. $this->setStatus($currProgress,'start');
  124. if(empty($chapter->toc)){
  125. $bookMeta['title'] = "unknown";
  126. }else{
  127. $bookMeta['book_title'] = '';
  128. foreach ($channelsId as $key => $id) {
  129. $title = ProgressChapter::where('book',$book)->where('para',$para)
  130. ->where('channel_id',$id)
  131. ->value('title');
  132. $bookMeta['book_title'] .= $title;
  133. }
  134. $bookMeta['sub_title'] = $chapter->toc;
  135. }
  136. $subChapter = PaliText::where('book',$book)->where('parent',$para)
  137. ->where('level','<',8)
  138. ->orderBy('paragraph')
  139. ->get();
  140. if(count($subChapter) === 0){
  141. //没有子章节
  142. $subChapter = PaliText::where('book',$book)->where('paragraph',$para)
  143. ->where('level','<',8)
  144. ->orderBy('paragraph')
  145. ->get();
  146. }
  147. $chapterParagraph = PaliText::where('book',$book)->where('paragraph',$para)->value('chapter_len');
  148. if($chapterParagraph >0 ){
  149. $step = 0.9 / $chapterParagraph;
  150. }else{
  151. $step = 0.9;
  152. Log::error('段落长度不能为0',['book'=>$book,'para'=>$para]);
  153. }
  154. $outputChannelsId = [];
  155. if($this->option('origin') === 'true'){
  156. $outputChannelsId[] = $orgChannelId;
  157. }
  158. if($this->option('translation') === 'true'){
  159. $outputChannelsId = array_merge($outputChannelsId,$tranChannelsId);
  160. }
  161. $sections = array();
  162. foreach ($subChapter as $key => $sub) {
  163. # 看这个章节是否存在译文
  164. $hasChapter = false;
  165. if($this->option('origin') === 'true'){
  166. $hasChapter = true;
  167. }
  168. if($this->option('translation') === 'true'){
  169. foreach ($tranChannelsId as $id) {
  170. if(ProgressChapter::where('book',$book)->where('para',$sub->paragraph)
  171. ->where('channel_id',$id)
  172. ->exists()){
  173. $hasChapter = true;
  174. }
  175. }
  176. }
  177. if(!$hasChapter){
  178. //不存在需要导出的数据
  179. continue;
  180. }
  181. $filename = "{$sub->paragraph}.".$this->option('format');
  182. $bookMeta['sections'][] = ['filename'=>$filename];
  183. $paliTitle = PaliText::where('book',$book)
  184. ->where('paragraph',$sub->paragraph)
  185. ->value('toc');
  186. $sectionTitle = $paliTitle;
  187. if($this->option('translation') === 'true'){
  188. $chapter = ProgressChapter::where('book',$book)->where('para',$sub->paragraph)
  189. ->where('channel_id',$tranChannelsId[0])
  190. ->first();
  191. if($chapter && !empty($chapter->title)){
  192. $sectionTitle = $chapter->title;
  193. }
  194. }
  195. $content = array();
  196. $chapterStart = $sub->paragraph+1;
  197. $chapterEnd = $sub->paragraph + $sub->chapter_len;
  198. $chapterBody = PaliText::where('book',$book)
  199. ->whereBetween('paragraph',[$chapterStart,$chapterEnd])
  200. ->orderBy('paragraph')->get();
  201. foreach ($chapterBody as $body) {
  202. $currProgress += $step;
  203. $this->setStatus($currProgress,'export chapter '.$body->paragraph);
  204. $paraData = array();
  205. $paraData['translations'] = array();
  206. foreach ($outputChannelsId as $key => $channelId) {
  207. $translationData = Sentence::where('book_id',$book)
  208. ->where('paragraph',$body->paragraph)
  209. ->where('channel_uid',$channelId)
  210. ->orderBy('word_start')->get();
  211. $sentContent = array();
  212. foreach ($translationData as $sent) {
  213. $texText = MdRender::render($sent->content,
  214. [$sent->channel_uid],
  215. null,
  216. 'read',
  217. $channelsIndex[$channelId]['type'],
  218. $sent->content_type,
  219. $renderFormat
  220. );
  221. $sentContent[] = trim($texText);
  222. }
  223. $paraContent = implode(' ',$sentContent);
  224. if($channelsIndex[$channelId]['type'] === 'original'){
  225. $paraData['origin'] = $paraContent;
  226. }else{
  227. $paraData['translations'][] = ['content'=>$paraContent];
  228. }
  229. }
  230. if($body->level > 7){
  231. $content[] = $m->render($tplParagraph,$paraData);
  232. }else{
  233. $currLevel = $body->level - $sub->level;
  234. if($currLevel<=0){
  235. $currLevel = 1;
  236. }
  237. if(count($paraData['translations'])===0){
  238. $subSessionTitle = PaliText::where('book',$book)
  239. ->where('paragraph',$body->paragraph)
  240. ->value('toc');
  241. }else{
  242. $subSessionTitle = $paraData['translations'][0]['content'];
  243. }
  244. switch ($this->option('format')) {
  245. case 'tex':
  246. $subStr = array_fill(0,$currLevel,'sub');
  247. $content[] = '\\'. implode('',$subStr) . "section{".$subSessionTitle.'}';
  248. break;
  249. case 'md':
  250. $subStr = array_fill(0,$currLevel,'#');
  251. $content[] = implode('',$subStr) . " ".$subSessionTitle;
  252. break;
  253. case 'html':
  254. $level = $currLevel+2;
  255. $content[] = "<h{$currLevel}".$subSessionTitle."</h{$currLevel}";
  256. break;
  257. }
  258. }
  259. $content[] = "\n\n";
  260. }
  261. $sections[] = [
  262. 'name'=>$filename,
  263. 'body'=>[
  264. 'title'=>$sectionTitle,
  265. 'content'=>implode('',$content)
  266. ]
  267. ];
  268. }
  269. $this->setStatus(0.9,'export content done');
  270. Log::debug('导出结束');
  271. $tex = array();
  272. $tpl = file_get_contents(resource_path("mustache/".$this->option('format')."/main.".$this->option('format')));
  273. $texContent = $m->render($tpl,$bookMeta);
  274. $tex[] = ['name'=>'main.'.$this->option('format'),
  275. 'content'=>$texContent
  276. ];
  277. foreach ($sections as $key => $section) {
  278. $tpl = file_get_contents(resource_path("mustache/".$this->option('format')."/section.".$this->option('format')));
  279. $texContent = $m->render($tpl,$section['body']);
  280. $tex[] = ['name'=>$section['name'],
  281. 'content'=>$texContent
  282. ];
  283. }
  284. Log::debug('footnote start');
  285. //footnote
  286. $tplFile = resource_path("mustache/".$this->option('format')."/footnote.".$this->option('format'));
  287. if(isset($GLOBALS['note']) &&
  288. is_array($GLOBALS['note']) &&
  289. count($GLOBALS['note'])>0 &&
  290. file_exists($tplFile)){
  291. $tpl = file_get_contents($tplFile);
  292. $texContent = $m->render($tpl,['footnote'=>$GLOBALS['note']]);
  293. $tex[] = ['name'=>'footnote.'.$this->option('format'),
  294. 'content'=>$texContent
  295. ];
  296. }
  297. if($this->option('debug')){
  298. $dir = "export/".$this->option('format')."/{$book}-{$para}-{$channelId}/";
  299. foreach ($tex as $key => $section) {
  300. Storage::disk('local')->put($dir.$section['name'], $section['content']);
  301. }
  302. }
  303. Log::debug('footnote finished');
  304. $this->setStatus(0.95,'export content done.');
  305. //upload
  306. $fileDate = '';
  307. switch ($this->option('format')) {
  308. case 'tex':
  309. $data = Export::ToPdf($tex);
  310. if($data['ok']){
  311. $this->info($data['content-type']);
  312. $fileDate = $data['data'];
  313. }else{
  314. $this->error($data['code'].'-'.$data['message']);
  315. }
  316. break;
  317. case 'html':
  318. $file = array();
  319. foreach ($tex as $key => $section) {
  320. $file[] = $section['content'];
  321. }
  322. $fileDate = implode('',$file);
  323. break;
  324. }
  325. $zipDir = storage_path('app/export/zip');
  326. if(!is_dir($zipDir)){
  327. $res = mkdir($zipDir,0755,true);
  328. if(!$res){
  329. Log::error('mkdir fail path='.$zipDir);
  330. return 1;
  331. }
  332. }
  333. $zipFile = $zipDir.'/'.Str::uuid().'.zip';
  334. Log::debug('export chapter start zip file='.$zipFile);
  335. $zipOk = \App\Tools\Tools::zip($zipFile,[$realFileName=>$fileDate]);
  336. if(!$zipOk){
  337. Log::error('export chapter zip fail zip file='.$zipFile);
  338. $this->setStatus(0.99,'export chapter zip fail');
  339. $this->error('export chapter zip fail zip file='.$zipFile);
  340. //TODO 给客户端返回错误状态
  341. return 1;
  342. }
  343. $this->setStatus(0.96,'export chapter zip success');
  344. $bucket = config('mint.attachments.bucket_name.temporary');
  345. $tmpFile = $bucket.'/'. $this->realFilename ;
  346. Log::debug('upload start filename='.$tmpFile);
  347. $this->setStatus(0.97,'upload start ');
  348. $zipData = file_get_contents($zipFile);
  349. Storage::put($tmpFile, $zipData);
  350. $this->setStatus(1,'export chapter done');
  351. Log::debug('export chapter done, upload filename='.$tmpFile);
  352. unlink($zipFile);
  353. return 0;
  354. }
  355. }