lh 1 miesiąc temu
rodzic
commit
493cc1a8a7
1 zmienionych plików z 246 dodań i 168 usunięć
  1. 246 168
      app/Http/Controllers/Anime/AnimeController.php

+ 246 - 168
app/Http/Controllers/Anime/AnimeController.php

@@ -1402,6 +1402,18 @@ class AnimeController extends BaseController
         $episodeId = $data['episode_id'];
         
         try {
+            // 获取动漫信息,判断是否为全能模式
+            $anime = DB::table('mp_animes')
+                ->where('id', $animeId)
+                ->select('ace_mode')
+                ->first();
+            
+            if (!$anime) {
+                Utils::throwError('20003:未找到动漫信息');
+            }
+            
+            $isAceMode = ($anime->ace_mode == 1);
+            
             // 获取剧集信息
             $episode = DB::table('mp_anime_episodes')
                 ->where('anime_id', $animeId)
@@ -1413,18 +1425,44 @@ class AnimeController extends BaseController
                 Utils::throwError('20003:未找到剧集信息');
             }
             
-            // 获取所有分镜图片和音频
-            $segments = DB::table('mp_episode_segments')
-                ->where('anime_id', $animeId)
-                ->where('episode_id', $episodeId)
-                ->whereNotNull('img_url')
-                ->where('img_url', '!=', '')
-                ->orderBy('segment_number')
-                ->select('segment_id', 'segment_number', 'img_url', 'video_url', 'audio_url', 'act_number', 'act_title')
-                ->get();
-            
-            if ($segments->isEmpty()) {
-                Utils::throwError('20003:该剧集暂无图片可导出');
+            // 根据模式查询和排序分镜数据
+            if ($isAceMode) {
+                // 全能模式:只查询有视频的分镜,按act_number排序
+                $segments = DB::table('mp_episode_segments')
+                    ->where('anime_id', $animeId)
+                    ->where('episode_id', $episodeId)
+                    ->whereNotNull('video_url')
+                    ->where('video_url', '!=', '')
+                    ->orderBy('act_number')
+                    ->select('id as act_id', 'act_number', 'video_url', 'act_title')
+                    ->get();
+                
+                if ($segments->isEmpty()) {
+                    Utils::throwError('20003:该剧集暂无视频可导出');
+                }
+            } else {
+                // 普通模式:查询所有分镜,按segment_number排序
+                $segments = DB::table('mp_episode_segments')
+                    ->where('anime_id', $animeId)
+                    ->where('episode_id', $episodeId)
+                    ->orderBy('segment_number')
+                    ->select('segment_id', 'segment_number', 'img_url', 'video_url', 'audio_url')
+                    ->get();
+                
+                if ($segments->isEmpty()) {
+                    Utils::throwError('20003:该剧集暂无分镜数据');
+                }
+                
+                // 过滤出有图片或视频的分镜
+                $validSegments = $segments->filter(function($segment) {
+                    return (!empty($segment->img_url) || !empty($segment->video_url));
+                });
+                
+                if ($validSegments->isEmpty()) {
+                    Utils::throwError('20003:该剧集暂无图片或视频可导出');
+                }
+                
+                $segments = $validSegments->values();
             }
             
             // 创建临时目录
@@ -1433,12 +1471,17 @@ class AnimeController extends BaseController
                 mkdir($tempDir, 0755, true);
             }
             
-            // 下载图片或视频
+            // 下载文件
             $downloadedFiles = [];
-            foreach ($segments as $segment) {
+            foreach ($segments as $index => $segment) {
                 try {
-                    // 优先使用video_url,如果没有则使用img_url
-                    $mediaUrl = !empty($segment->video_url) ? $segment->video_url : $segment->img_url;
+                    if ($isAceMode) {
+                        // 全能模式:只下载视频
+                        $mediaUrl = $segment->video_url;
+                    } else {
+                        // 普通模式:优先使用video_url,如果没有则使用img_url
+                        $mediaUrl = !empty($segment->video_url) ? $segment->video_url : $segment->img_url;
+                    }
                     
                     if (empty($mediaUrl)) {
                         continue;
@@ -1451,15 +1494,17 @@ class AnimeController extends BaseController
                         $extension = !empty($segment->video_url) ? 'mp4' : 'jpg';
                     }
                     
-                    // 构建文件名:分镜序号.扩展名
-                    $fileName = sprintf(
-                        '%03d.%s',
-                        $segment->segment_number,
-                        $extension
-                    );
+                    // 构建文件名
+                    if ($isAceMode) {
+                        // 全能模式:使用act_number
+                        $fileName = sprintf('%03d.%s', $segment->act_number, $extension);
+                    } else {
+                        // 普通模式:使用segment_number
+                        $fileName = sprintf('%03d.%s', $segment->segment_number, $extension);
+                    }
                     $filePath = $tempDir . '/' . $fileName;
                     
-                    // 下载文件(使用file_get_contents,兼容性更好)
+                    // 下载文件
                     $context = stream_context_create([
                         'http' => [
                             'timeout' => 30,
@@ -1479,22 +1524,34 @@ class AnimeController extends BaseController
                         $downloadedFiles[] = $filePath;
                     } else {
                         $logData = [
-                            'segment_id' => $segment->segment_id,
                             'media_url' => $mediaUrl,
-                            'type' => !empty($segment->video_url) ? 'video' : 'image'
+                            'type' => $isAceMode ? 'video' : (!empty($segment->video_url) ? 'video' : 'image')
                         ];
-                        dLog('anime')->warning('文件下载失败', $logData);
+                        
+                        if ($isAceMode) {
+                            $logData['act_id'] = $segment->act_id;
+                            $logData['act_number'] = $segment->act_number;
+                        } else {
+                            $logData['segment_id'] = $segment->segment_id;
+                            $logData['segment_number'] = $segment->segment_number;
+                        }
+                        
                         logDB('anime', 'warning', '文件下载失败', $logData);
                     }
                     
                 } catch (\Exception $e) {
                     $logData = [
-                        'segment_id' => $segment->segment_id,
-                        'video_url' => $segment->video_url,
-                        'img_url' => $segment->img_url,
                         'error' => $e->getMessage()
                     ];
-                    dLog('anime')->error('下载文件异常', $logData);
+                    
+                    if ($isAceMode) {
+                        $logData['act_id'] = $segment->act_id;
+                        $logData['act_number'] = $segment->act_number;
+                    } else {
+                        $logData['segment_id'] = $segment->segment_id;
+                        $logData['segment_number'] = $segment->segment_number;
+                    }
+                    
                     logDB('anime', 'error', '下载文件异常', $logData);
                     continue;
                 }
@@ -1506,163 +1563,187 @@ class AnimeController extends BaseController
                     array_map('unlink', glob("$tempDir/*"));
                     rmdir($tempDir);
                 }
-                Utils::throwError('20003:没有成功下载任何图片');
+                Utils::throwError('20003:没有成功下载任何文件');
             }
             
-            // 处理音频合成
+            // 处理音频合成(仅普通模式)
             $mergedAudioPath = null;
-            try {
-                // 检查 FFmpeg 是否可用
-                exec('which ffmpeg 2>&1', $ffmpegCheck, $ffmpegReturnCode);
-                $hasFfmpeg = ($ffmpegReturnCode === 0);
-                
-                if ($hasFfmpeg) {
-                    // 下载所有分镜的音频文件
-                    $audioFiles = [];
-                    foreach ($segments as $segment) {
-                        if (!empty($segment->audio_url)) {
-                            try {
-                                $context = stream_context_create([
-                                    'http' => [
-                                        'timeout' => 30,
-                                        'follow_location' => 1,
-                                        'ignore_errors' => true
-                                    ],
-                                    'ssl' => [
-                                        'verify_peer' => false,
-                                        'verify_peer_name' => false
-                                    ]
-                                ]);
-                                
-                                $audioData = @file_get_contents($segment->audio_url, false, $context);
-                                
-                                if ($audioData !== false && !empty($audioData)) {
-                                    // 获取音频文件扩展名
-                                    $audioExtension = pathinfo(parse_url($segment->audio_url, PHP_URL_PATH), PATHINFO_EXTENSION);
-                                    if (empty($audioExtension)) {
-                                        $audioExtension = 'wav';
-                                    }
+            if (!$isAceMode) {
+                try {
+                    // 检查 FFmpeg 是否可用
+                    exec('which ffmpeg 2>&1', $ffmpegCheck, $ffmpegReturnCode);
+                    $hasFfmpeg = ($ffmpegReturnCode === 0);
+                    
+                    if ($hasFfmpeg) {
+                        // 下载所有分镜的音频文件
+                        $audioFiles = [];
+                        foreach ($segments as $segment) {
+                            if (!empty($segment->audio_url)) {
+                                try {
+                                    $context = stream_context_create([
+                                        'http' => [
+                                            'timeout' => 30,
+                                            'follow_location' => 1,
+                                            'ignore_errors' => true
+                                        ],
+                                        'ssl' => [
+                                            'verify_peer' => false,
+                                            'verify_peer_name' => false
+                                        ]
+                                    ]);
                                     
-                                    $audioFileName = sprintf('audio_%03d.%s', $segment->segment_number, $audioExtension);
-                                    $audioFilePath = $tempDir . '/' . $audioFileName;
-                                    file_put_contents($audioFilePath, $audioData);
-                                    $audioFiles[] = $audioFilePath;
-                                } else {
-                                    $logData = [
+                                    $audioData = @file_get_contents($segment->audio_url, false, $context);
+                                    
+                                    if ($audioData !== false && !empty($audioData)) {
+                                        // 获取音频文件扩展名
+                                        $audioExtension = pathinfo(parse_url($segment->audio_url, PHP_URL_PATH), PATHINFO_EXTENSION);
+                                        if (empty($audioExtension)) {
+                                            $audioExtension = 'wav';
+                                        }
+                                        
+                                        $audioFileName = sprintf('audio_%03d.%s', $segment->segment_number, $audioExtension);
+                                        $audioFilePath = $tempDir . '/' . $audioFileName;
+                                        file_put_contents($audioFilePath, $audioData);
+                                        $audioFiles[] = $audioFilePath;
+                                    } else {
+                                        logDB('anime', 'warning', '音频下载失败', [
+                                            'segment_id' => $segment->segment_id,
+                                            'audio_url' => $segment->audio_url
+                                        ]);
+                                    }
+                                } catch (\Exception $e) {
+                                    logDB('anime', 'error', '下载音频异常', [
                                         'segment_id' => $segment->segment_id,
-                                        'audio_url' => $segment->audio_url
-                                    ];
-                                    dLog('anime')->warning('音频下载失败', $logData);
-                                    logDB('anime', 'warning', '音频下载失败', $logData);
+                                        'audio_url' => $segment->audio_url,
+                                        'error' => $e->getMessage()
+                                    ]);
                                 }
-                            } catch (\Exception $e) {
-                                $logData = [
-                                    'segment_id' => $segment->segment_id,
-                                    'audio_url' => $segment->audio_url,
-                                    'error' => $e->getMessage()
-                                ];
-                                dLog('anime')->error('下载音频异常', $logData);
-                                logDB('anime', 'error', '下载音频异常', $logData);
                             }
                         }
-                    }
-                    
-                    // 如果有音频文件,则进行合成
-                    if (!empty($audioFiles)) {
-                        // 创建 FFmpeg concat 文件列表
-                        $concatFile = $tempDir . '/concat.txt';
-                        $concatContent = '';
-                        foreach ($audioFiles as $audioFile) {
-                            // 使用相对路径,避免路径中的特殊字符问题
-                            $concatContent .= "file '" . basename($audioFile) . "'\n";
-                        }
-                        file_put_contents($concatFile, $concatContent);
-                        
-                        // 合成音频文件
-                        $mergedAudioPath = $tempDir . '/tts.wav';
-                        
-                        // 使用 FFmpeg 合并音频,转换为统一的 WAV 格式
-                        $ffmpegCommand = sprintf(
-                            'cd %s && ffmpeg -f concat -safe 0 -i %s -ar 44100 -ac 2 -y %s 2>&1',
-                            escapeshellarg($tempDir),
-                            escapeshellarg(basename($concatFile)),
-                            escapeshellarg(basename($mergedAudioPath))
-                        );
-                        
-                        exec($ffmpegCommand, $ffmpegOutput, $ffmpegReturnCode);
-                        
-                        if ($ffmpegReturnCode !== 0 || !file_exists($mergedAudioPath)) {
-                            $logData = [
-                                'command' => $ffmpegCommand,
-                                'output' => implode("\n", $ffmpegOutput),
-                                'return_code' => $ffmpegReturnCode
-                            ];
-                            dLog('anime')->error('FFmpeg 合成音频失败', $logData);
-                            logDB('anime', 'error', 'FFmpeg 合成音频失败', $logData);
-                            $mergedAudioPath = null;
-                        } else {
-                            $logData = [
-                                'audio_count' => count($audioFiles),
-                                'output_file' => $mergedAudioPath,
-                                'file_size' => filesize($mergedAudioPath)
-                            ];
-                            dLog('anime')->info('音频合成成功', $logData);
-                            logDB('anime', 'info', '音频合成成功', $logData);
-                        }
                         
-                        // 清理临时音频文件和 concat 文件
-                        foreach ($audioFiles as $audioFile) {
-                            if (file_exists($audioFile)) {
-                                @unlink($audioFile);
+                        // 如果有音频文件,则进行合成
+                        if (!empty($audioFiles)) {
+                            // 创建 FFmpeg concat 文件列表
+                            $concatFile = $tempDir . '/concat.txt';
+                            $concatContent = '';
+                            foreach ($audioFiles as $audioFile) {
+                                // 使用相对路径,避免路径中的特殊字符问题
+                                $concatContent .= "file '" . basename($audioFile) . "'\n";
+                            }
+                            file_put_contents($concatFile, $concatContent);
+                            
+                            // 合成音频文件
+                            $mergedAudioPath = $tempDir . '/tts.wav';
+                            
+                            // 使用 FFmpeg 合并音频,转换为统一的 WAV 格式
+                            $ffmpegCommand = sprintf(
+                                'cd %s && ffmpeg -f concat -safe 0 -i %s -ar 44100 -ac 2 -y %s 2>&1',
+                                escapeshellarg($tempDir),
+                                escapeshellarg(basename($concatFile)),
+                                escapeshellarg(basename($mergedAudioPath))
+                            );
+                            
+                            exec($ffmpegCommand, $ffmpegOutput, $ffmpegReturnCode);
+                            
+                            if ($ffmpegReturnCode !== 0 || !file_exists($mergedAudioPath)) {
+                                logDB('anime', 'error', 'FFmpeg 合成音频失败', [
+                                    'command' => $ffmpegCommand,
+                                    'output' => implode("\n", $ffmpegOutput),
+                                    'return_code' => $ffmpegReturnCode
+                                ]);
+                                $mergedAudioPath = null;
+                            } else {
+                                logDB('anime', 'info', '音频合成成功', [
+                                    'audio_count' => count($audioFiles),
+                                    'output_file' => $mergedAudioPath,
+                                    'file_size' => filesize($mergedAudioPath)
+                                ]);
+                            }
+                            
+                            // 清理临时音频文件和 concat 文件
+                            foreach ($audioFiles as $audioFile) {
+                                if (file_exists($audioFile)) {
+                                    @unlink($audioFile);
+                                }
+                            }
+                            if (file_exists($concatFile)) {
+                                @unlink($concatFile);
                             }
                         }
-                        if (file_exists($concatFile)) {
-                            @unlink($concatFile);
-                        }
+                    } else {
+                        logDB('anime', 'warning', 'FFmpeg 未安装,跳过音频合成', []);
                     }
-                } else {
-                    dLog('anime')->warning('FFmpeg 未安装,跳过音频合成');
-                    logDB('anime', 'warning', 'FFmpeg 未安装,跳过音频合成', []);
+                } catch (\Exception $e) {
+                    logDB('anime', 'error', '音频合成过程异常', [
+                        'error' => $e->getMessage(),
+                        'trace' => $e->getTraceAsString()
+                    ]);
+                    // 音频合成失败不影响图片导出
+                    $mergedAudioPath = null;
                 }
-            } catch (\Exception $e) {
-                $logData = [
-                    'error' => $e->getMessage(),
-                    'trace' => $e->getTraceAsString()
-                ];
-                dLog('anime')->error('音频合成过程异常', $logData);
-                logDB('anime', 'error', '音频合成过程异常', $logData);
-                // 音频合成失败不影响图片导出
-                $mergedAudioPath = null;
             }
             
-            // 创建zip文件
-            $zipFileName = sprintf(
-                '%s.zip',
-                $episode->title ?: '未命名'
-            );
+            // 创建zip文件(使用安全的文件名)
+            $safeTitle = preg_replace('/[^\p{L}\p{N}\s\-_]/u', '', $episode->title ?: '未命名');
+            $safeTitle = trim($safeTitle) ?: '未命名';
+            $zipFileName = sprintf('%s_%s.zip', $safeTitle, time());
             $zipFilePath = storage_path('app/temp/' . $zipFileName);
             
+            // 确保 temp 目录存在
+            $tempZipDir = storage_path('app/temp');
+            if (!file_exists($tempZipDir)) {
+                mkdir($tempZipDir, 0755, true);
+            }
+            
             $zip = new \ZipArchive();
-            if ($zip->open($zipFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
+            $openResult = $zip->open($zipFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
+            
+            if ($openResult !== true) {
                 // 清理临时文件
                 array_map('unlink', $downloadedFiles);
+                if ($mergedAudioPath && file_exists($mergedAudioPath)) {
+                    @unlink($mergedAudioPath);
+                }
                 rmdir($tempDir);
+                
+                logDB('anime', 'error', '创建ZIP文件失败', [
+                    'zip_path' => $zipFilePath,
+                    'open_result' => $openResult
+                ]);
+                
                 Utils::throwError('20003:创建ZIP文件失败');
             }
             
-            // 添加图片文件到zip
+            // 添加文件到zip(使用完整路径确保文件正确添加)
             foreach ($downloadedFiles as $file) {
-                $zip->addFile($file, basename($file));
+                if (file_exists($file)) {
+                    $zip->addFile($file, basename($file));
+                }
             }
             
-            // 添加合成的音频文件到zip
-            if ($mergedAudioPath && file_exists($mergedAudioPath)) {
+            // 添加合成的音频文件到zip(仅普通模式)
+            if (!$isAceMode && $mergedAudioPath && file_exists($mergedAudioPath)) {
                 $zip->addFile($mergedAudioPath, 'tts.wav');
             }
             
+            // 关闭 ZIP 文件
             $zip->close();
             
-            // 清理临时图片文件
+            // 验证 ZIP 文件是否创建成功
+            if (!file_exists($zipFilePath) || filesize($zipFilePath) == 0) {
+                // 清理临时文件
+                array_map('unlink', $downloadedFiles);
+                if ($mergedAudioPath && file_exists($mergedAudioPath)) {
+                    @unlink($mergedAudioPath);
+                }
+                rmdir($tempDir);
+                
+                logDB('anime', 'error', 'ZIP文件创建后无效', ['zip_path' => $zipFilePath]);
+                
+                Utils::throwError('20003:ZIP文件创建失败或文件为空');
+            }
+            
+            // 清理临时文件
             array_map('unlink', $downloadedFiles);
             
             // 清理合成的音频文件
@@ -1672,17 +1753,20 @@ class AnimeController extends BaseController
             
             rmdir($tempDir);
             
+            // 清空所有输出缓冲区,避免污染ZIP文件
+            while (ob_get_level() > 0) {
+                ob_end_clean();
+            }
+            
             // 返回文件下载响应
             return response()->download($zipFilePath, $zipFileName)->deleteFileAfterSend(true);
             
         } catch (\Exception $e) {
-            $logData = [
+            logDB('anime', 'error', '导出分集图片失败', [
                 'anime_id' => $animeId,
                 'episode_id' => $episodeId,
                 'error' => $e->getMessage()
-            ];
-            dLog('anime')->error('导出分集图片失败', $logData);
-            logDB('anime', 'error', '导出分集图片失败', $logData);
+            ]);
             
             // 清理可能存在的临时文件
             if (isset($tempDir) && file_exists($tempDir)) {
@@ -2744,12 +2828,6 @@ class AnimeController extends BaseController
                         if (isset($videoParams['tail_frame_url'])) {
                             $parameters['last_frame_url'] = $videoParams['tail_frame_url'];
                         }
-                    } else {
-                        // 没有图片时,记录错误日志
-                        logDB('anime', 'error', '智帧20模型缺少首帧图片', [
-                            'act_id' => $act->id,
-                            'model' => $model
-                        ]);
                     }
 
                     if ($reference_images) {
@@ -2790,7 +2868,7 @@ class AnimeController extends BaseController
                 
                 // 更新分镜表的视频任务信息
                 DB::table('mp_episode_segments')
-                    ->where('act_id', $act->id)
+                    ->where('id', $act->id)
                     ->update([
                         'video_task_id' => $task->id,
                         'video_task_status' => '生成中',
@@ -3003,7 +3081,7 @@ class AnimeController extends BaseController
                                             }
                                             
                                             $updateResult = DB::table('mp_episode_segments')
-                                                ->where('act_id', $actId)
+                                                ->where('id', $actId)
                                                 ->update($actUpdateData);
                                             
                                             if ($updateResult) {
@@ -3055,7 +3133,7 @@ class AnimeController extends BaseController
                                     foreach ($actTasks as $actTask) {
                                         if ($actTask['task_id'] == $taskId) {
                                             DB::table('mp_episode_segments')
-                                                ->where('act_id', $actTask['act_id'])
+                                                ->where('id', $actTask['act_id'])
                                                 ->update([
                                                     'video_task_status' => '失败',
                                                     'updated_at' => date('Y-m-d H:i:s')