fix(manga): générer le CBZ de téléchargement depuis les dossiers de pages
Les endpoints de téléchargement chapitre/volume plantaient (500 "file does not exist") car le FileService traitait `pagesDirectory` comme un CBZ. Le service reconstruit maintenant l'archive à la volée à partir des images du dossier, et le nom du fichier chapitre inclut le titre du manga et le numéro. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface;
|
||||
use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException;
|
||||
use App\Domain\Manga\Domain\Exception\ChapterNotAvailableException;
|
||||
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
|
||||
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||
use App\Domain\Shared\Domain\Contract\QueryHandlerInterface;
|
||||
use App\Domain\Shared\Domain\Contract\QueryInterface;
|
||||
use App\Domain\Shared\Domain\Contract\ResponseInterface;
|
||||
@@ -35,8 +36,19 @@ readonly class DownloadCbzHandler implements QueryHandlerInterface
|
||||
throw new ChapterNotAvailableException($query->chapterId);
|
||||
}
|
||||
|
||||
$manga = $this->mangaRepository->findById($chapter->getMangaId()->getValue());
|
||||
if (!$manga) {
|
||||
throw new MangaNotFoundException($chapter->getMangaId()->getValue());
|
||||
}
|
||||
|
||||
$pagesDirectory = $chapter->getPagesDirectory();
|
||||
$filename = basename($pagesDirectory);
|
||||
|
||||
$number = $chapter->getNumber();
|
||||
$formattedNumber = fmod($number, 1.0) === 0.0
|
||||
? sprintf('%03d', (int) $number)
|
||||
: rtrim(rtrim(sprintf('%06.2f', $number), '0'), '.');
|
||||
|
||||
$filename = sprintf('%s - Ch.%s', $manga->getTitle()->getValue(), $formattedNumber);
|
||||
|
||||
try {
|
||||
$httpResponse = $this->fileService->downloadCbz($pagesDirectory, $filename);
|
||||
|
||||
@@ -17,93 +17,103 @@ readonly class FileService implements FileServiceInterface
|
||||
|
||||
public function downloadCbz(string $filePath, string $filename): Response
|
||||
{
|
||||
if (!$this->cbzExists($filePath)) {
|
||||
if (!is_dir($filePath)) {
|
||||
throw new CbzFileNotFoundException($filePath);
|
||||
}
|
||||
|
||||
$response = new BinaryFileResponse($filePath);
|
||||
$images = $this->listImageFiles($filePath);
|
||||
if ([] === $images) {
|
||||
throw new CbzFileNotFoundException($filePath);
|
||||
}
|
||||
|
||||
$tempCbzPath = $this->createTempCbzPath($filename);
|
||||
|
||||
$cbz = new \ZipArchive();
|
||||
if (true !== $cbz->open($tempCbzPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE)) {
|
||||
throw new \RuntimeException('Cannot create CBZ file');
|
||||
}
|
||||
|
||||
$counter = 1;
|
||||
foreach ($images as $imagePath) {
|
||||
$extension = pathinfo($imagePath, PATHINFO_EXTENSION);
|
||||
$cbz->addFile($imagePath, sprintf('%04d.%s', $counter, $extension));
|
||||
++$counter;
|
||||
}
|
||||
|
||||
$cbz->close();
|
||||
|
||||
if (!file_exists($tempCbzPath)) {
|
||||
throw new \RuntimeException(sprintf('Failed to write CBZ file "%s"', $tempCbzPath));
|
||||
}
|
||||
|
||||
$downloadName = str_ends_with($filename, '.cbz') ? $filename : $filename.'.cbz';
|
||||
|
||||
$response = new BinaryFileResponse($tempCbzPath);
|
||||
$response->setContentDisposition(
|
||||
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
||||
$filename
|
||||
$downloadName
|
||||
);
|
||||
$response->headers->set('Content-Type', 'application/x-cbz');
|
||||
$response->deleteFileAfterSend();
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function createVolumeCbz(array $cbzPaths, string $volumeName): Response
|
||||
{
|
||||
$tempCbzPath = sys_get_temp_dir().'/'.$volumeName.'.cbz';
|
||||
$tempCbzPath = $this->createTempCbzPath($volumeName);
|
||||
|
||||
$cbz = new \ZipArchive();
|
||||
if (true !== $cbz->open($tempCbzPath, \ZipArchive::CREATE)) {
|
||||
if (true !== $cbz->open($tempCbzPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE)) {
|
||||
throw new \RuntimeException('Cannot create CBZ file');
|
||||
}
|
||||
|
||||
$imageCounter = 1;
|
||||
|
||||
foreach ($cbzPaths as $cbzPath) {
|
||||
if (!$this->cbzExists($cbzPath)) {
|
||||
$counter = 1;
|
||||
foreach ($cbzPaths as $directory) {
|
||||
if (!is_dir($directory)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sourceCbz = new \ZipArchive();
|
||||
if (true !== $sourceCbz->open($cbzPath)) {
|
||||
continue; // Skip if we can't open the CBZ
|
||||
foreach ($this->listImageFiles($directory) as $imagePath) {
|
||||
$extension = pathinfo($imagePath, PATHINFO_EXTENSION);
|
||||
$cbz->addFile($imagePath, sprintf('%04d.%s', $counter, $extension));
|
||||
++$counter;
|
||||
}
|
||||
|
||||
// Extract all images from the current CBZ
|
||||
for ($i = 0; $i < $sourceCbz->numFiles; ++$i) {
|
||||
$fileName = $sourceCbz->getNameIndex($i);
|
||||
$fileInfo = $sourceCbz->statIndex($i);
|
||||
|
||||
// Skip directories and non-image files
|
||||
if (0 === $fileInfo['size'] || !$this->isImageFile($fileName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the file content
|
||||
$imageContent = $sourceCbz->getFromIndex($i);
|
||||
if (false === $imageContent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get file extension
|
||||
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
|
||||
|
||||
// Create a new filename with proper ordering
|
||||
$newFileName = sprintf('%04d.%s', $imageCounter, $extension);
|
||||
|
||||
// Add the image to the volume CBZ
|
||||
$cbz->addFromString($newFileName, $imageContent);
|
||||
|
||||
++$imageCounter;
|
||||
}
|
||||
|
||||
$sourceCbz->close();
|
||||
}
|
||||
|
||||
$cbz->close();
|
||||
|
||||
if (1 === $counter || !file_exists($tempCbzPath)) {
|
||||
if (file_exists($tempCbzPath)) {
|
||||
@unlink($tempCbzPath);
|
||||
}
|
||||
throw new \RuntimeException(sprintf('No images found to build volume "%s"', $volumeName));
|
||||
}
|
||||
|
||||
$response = new BinaryFileResponse($tempCbzPath);
|
||||
$response->setContentDisposition(
|
||||
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
||||
$volumeName.'.cbz'
|
||||
);
|
||||
$response->headers->set('Content-Type', 'application/x-cbz');
|
||||
|
||||
// Clean up temp file after sending
|
||||
$response->deleteFileAfterSend();
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function isImageFile(string $fileName): bool
|
||||
private function createTempCbzPath(string $name): string
|
||||
{
|
||||
$imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
|
||||
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
|
||||
$safeName = preg_replace('/[^A-Za-z0-9_.-]/', '_', $name) ?? 'archive';
|
||||
|
||||
return in_array($extension, $imageExtensions);
|
||||
return sys_get_temp_dir().'/'.uniqid($safeName.'_', true).'.cbz';
|
||||
}
|
||||
|
||||
private function listImageFiles(string $directory): array
|
||||
{
|
||||
$files = glob(rtrim($directory, '/').'/*.{jpg,jpeg,png,gif,bmp,webp,JPG,JPEG,PNG,GIF,BMP,WEBP}', GLOB_BRACE) ?: [];
|
||||
natsort($files);
|
||||
|
||||
return array_values($files);
|
||||
}
|
||||
|
||||
public function deleteCbzFile(string $filePath): bool
|
||||
|
||||
Reference in New Issue
Block a user