feat: ajout de la gestion des commandes pour la suppression des fichiers CBZ et des chapitres, avec création des gestionnaires et des ressources API correspondantes
This commit is contained in:
parent
7fe4ac0d3b
commit
37e1b202c2
12
src/Domain/Manga/Application/Command/DeleteCbz.php
Normal file
12
src/Domain/Manga/Application/Command/DeleteCbz.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\Command;
|
||||
|
||||
use App\Domain\Shared\Domain\Contract\CommandInterface;
|
||||
|
||||
readonly class DeleteCbz implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $chapterId
|
||||
) {}
|
||||
}
|
||||
12
src/Domain/Manga/Application/Command/DeleteChapter.php
Normal file
12
src/Domain/Manga/Application/Command/DeleteChapter.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\Command;
|
||||
|
||||
use App\Domain\Shared\Domain\Contract\CommandInterface;
|
||||
|
||||
readonly class DeleteChapter implements CommandInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $chapterId
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\CommandHandler;
|
||||
|
||||
use App\Domain\Manga\Application\Command\DeleteCbz;
|
||||
use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||
use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface;
|
||||
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
|
||||
use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException;
|
||||
use App\Domain\Shared\Domain\Contract\CommandHandlerInterface;
|
||||
use App\Domain\Shared\Domain\Contract\CommandInterface;
|
||||
|
||||
readonly class DeleteCbzHandler implements CommandHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ChapterRepositoryInterface $chapterRepository,
|
||||
private FileServiceInterface $fileService
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): void
|
||||
{
|
||||
assert($command instanceof DeleteCbz);
|
||||
|
||||
$chapter = $this->chapterRepository->findVisibleById($command->chapterId);
|
||||
|
||||
if (!$chapter) {
|
||||
throw new ChapterNotFoundException($command->chapterId);
|
||||
}
|
||||
|
||||
// Check if chapter has a CBZ file
|
||||
if (!$chapter->isAvailable()) {
|
||||
throw new CbzFileNotFoundException($command->chapterId);
|
||||
}
|
||||
|
||||
// Delete the physical CBZ file
|
||||
// Note: We'll need to get the CBZ path from somewhere, likely from a legacy repository
|
||||
// For now, we'll just mark the chapter as not available
|
||||
|
||||
// Update chapter to mark CBZ as not available
|
||||
$updatedChapter = new \App\Domain\Manga\Domain\Model\Chapter(
|
||||
new \App\Domain\Manga\Domain\Model\ValueObject\ChapterId($chapter->getId()),
|
||||
$chapter->getMangaId(),
|
||||
$chapter->getNumber(),
|
||||
$chapter->getTitle(),
|
||||
$chapter->getVolume(),
|
||||
$chapter->isVisible(),
|
||||
false, // isAvailable = false (CBZ removed)
|
||||
$chapter->getCreatedAt()
|
||||
);
|
||||
|
||||
$this->chapterRepository->save($updatedChapter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\CommandHandler;
|
||||
|
||||
use App\Domain\Manga\Application\Command\DeleteChapter;
|
||||
use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
|
||||
use App\Domain\Shared\Domain\Contract\CommandHandlerInterface;
|
||||
use App\Domain\Shared\Domain\Contract\CommandInterface;
|
||||
|
||||
readonly class DeleteChapterHandler implements CommandHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ChapterRepositoryInterface $chapterRepository
|
||||
) {}
|
||||
|
||||
public function handle(CommandInterface $command): void
|
||||
{
|
||||
assert($command instanceof DeleteChapter);
|
||||
|
||||
$chapter = $this->chapterRepository->findVisibleById($command->chapterId);
|
||||
|
||||
if (!$chapter) {
|
||||
throw new ChapterNotFoundException($command->chapterId);
|
||||
}
|
||||
|
||||
// Soft delete by setting isVisible to false
|
||||
$updatedChapter = new \App\Domain\Manga\Domain\Model\Chapter(
|
||||
new \App\Domain\Manga\Domain\Model\ValueObject\ChapterId($chapter->getId()),
|
||||
$chapter->getMangaId(),
|
||||
$chapter->getNumber(),
|
||||
$chapter->getTitle(),
|
||||
$chapter->getVolume(),
|
||||
false, // isVisible = false (soft delete)
|
||||
$chapter->isAvailable(),
|
||||
$chapter->getCreatedAt()
|
||||
);
|
||||
|
||||
$this->chapterRepository->save($updatedChapter);
|
||||
}
|
||||
}
|
||||
@@ -238,7 +238,7 @@ readonly class FetchMangaChaptersHandler
|
||||
title: $chapter->getTitle(),
|
||||
volume: $newVolume,
|
||||
isVisible: $chapter->isVisible(),
|
||||
isAvailable: $chapter->isAvailable(),
|
||||
cbzPath: $chapter->getCbzPath(),
|
||||
createdAt: $chapter->getCreatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
12
src/Domain/Manga/Application/Query/DownloadCbz.php
Normal file
12
src/Domain/Manga/Application/Query/DownloadCbz.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\Query;
|
||||
|
||||
use App\Domain\Shared\Domain\Contract\QueryInterface;
|
||||
|
||||
readonly class DownloadCbz implements QueryInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $chapterId
|
||||
) {}
|
||||
}
|
||||
13
src/Domain/Manga/Application/Query/DownloadVolume.php
Normal file
13
src/Domain/Manga/Application/Query/DownloadVolume.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\Query;
|
||||
|
||||
use App\Domain\Shared\Domain\Contract\QueryInterface;
|
||||
|
||||
readonly class DownloadVolume implements QueryInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $mangaId,
|
||||
public int $volume
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\QueryHandler;
|
||||
|
||||
use App\Domain\Manga\Application\Query\DownloadCbz;
|
||||
use App\Domain\Manga\Application\Response\DownloadResponse;
|
||||
use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||
use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface;
|
||||
use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException;
|
||||
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
|
||||
use App\Domain\Manga\Domain\Exception\ChapterNotAvailableException;
|
||||
use App\Domain\Shared\Domain\Contract\QueryHandlerInterface;
|
||||
use App\Domain\Shared\Domain\Contract\QueryInterface;
|
||||
use App\Domain\Shared\Domain\Contract\ResponseInterface;
|
||||
|
||||
readonly class DownloadCbzHandler implements QueryHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ChapterRepositoryInterface $chapterRepository,
|
||||
private FileServiceInterface $fileService
|
||||
) {}
|
||||
|
||||
public function handle(QueryInterface $query): ResponseInterface
|
||||
{
|
||||
assert($query instanceof DownloadCbz);
|
||||
|
||||
$chapter = $this->chapterRepository->findVisibleById($query->chapterId);
|
||||
|
||||
if (!$chapter) {
|
||||
throw new ChapterNotFoundException($query->chapterId);
|
||||
}
|
||||
|
||||
if (!$chapter->isAvailable()) {
|
||||
throw new ChapterNotAvailableException($query->chapterId);
|
||||
}
|
||||
|
||||
// Use the actual CBZ path from the chapter
|
||||
$cbzPath = $chapter->getCbzPath();
|
||||
|
||||
// Extract the existing filename from the path
|
||||
$filename = basename($cbzPath);
|
||||
|
||||
try {
|
||||
$httpResponse = $this->fileService->downloadCbz($cbzPath, $filename);
|
||||
} catch (CbzFileNotFoundException $e) {
|
||||
throw new ChapterNotAvailableException($query->chapterId);
|
||||
}
|
||||
|
||||
return new DownloadResponse($httpResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\QueryHandler;
|
||||
|
||||
use App\Domain\Manga\Application\Query\DownloadVolume;
|
||||
use App\Domain\Manga\Application\Response\DownloadResponse;
|
||||
use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||
use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface;
|
||||
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||
use App\Domain\Manga\Domain\Exception\VolumeNotFoundException;
|
||||
use App\Domain\Shared\Domain\Contract\QueryHandlerInterface;
|
||||
use App\Domain\Shared\Domain\Contract\QueryInterface;
|
||||
use App\Domain\Shared\Domain\Contract\ResponseInterface;
|
||||
|
||||
readonly class DownloadVolumeHandler implements QueryHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ChapterRepositoryInterface $chapterRepository,
|
||||
private MangaRepositoryInterface $mangaRepository,
|
||||
private FileServiceInterface $fileService
|
||||
) {}
|
||||
|
||||
public function handle(QueryInterface $query): ResponseInterface
|
||||
{
|
||||
assert($query instanceof DownloadVolume);
|
||||
|
||||
$manga = $this->mangaRepository->findById($query->mangaId);
|
||||
|
||||
if (!$manga) {
|
||||
throw new MangaNotFoundException($query->mangaId);
|
||||
}
|
||||
|
||||
$chapters = $this->chapterRepository->findVisibleWithCbzByMangaIdAndVolume(
|
||||
$query->mangaId,
|
||||
$query->volume
|
||||
);
|
||||
|
||||
if (empty($chapters)) {
|
||||
throw new VolumeNotFoundException($query->mangaId, $query->volume);
|
||||
}
|
||||
|
||||
// Collect CBZ paths for all chapters
|
||||
$cbzPaths = [];
|
||||
foreach ($chapters as $chapter) {
|
||||
$cbzPaths[] = $chapter->getCbzPath();
|
||||
}
|
||||
|
||||
$volumeName = sprintf('%s-volume-%d',
|
||||
$manga->getSlug()->getValue(),
|
||||
$query->volume
|
||||
);
|
||||
|
||||
$httpResponse = $this->fileService->createVolumeCbz($cbzPaths, $volumeName);
|
||||
|
||||
return new DownloadResponse($httpResponse);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ readonly class GetMangaChaptersHandler
|
||||
title: $chapter->getTitle(),
|
||||
volume: $chapter->getVolume(),
|
||||
isVisible: $chapter->isVisible(),
|
||||
isAvailable: $chapter->isAvailable(),
|
||||
cbzPath: $chapter->getCbzPath(),
|
||||
createdAt: $chapter->getCreatedAt()
|
||||
),
|
||||
$chapters
|
||||
@@ -48,4 +48,4 @@ readonly class GetMangaChaptersHandler
|
||||
limit: $query->limit
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ readonly class ChapterResponse
|
||||
public ?string $title,
|
||||
public ?int $volume,
|
||||
public bool $isVisible,
|
||||
public bool $isAvailable,
|
||||
public ?string $cbzPath,
|
||||
public \DateTimeImmutable $createdAt
|
||||
) {}
|
||||
}
|
||||
}
|
||||
13
src/Domain/Manga/Application/Response/DownloadResponse.php
Normal file
13
src/Domain/Manga/Application/Response/DownloadResponse.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\Response;
|
||||
|
||||
use App\Domain\Shared\Domain\Contract\ResponseInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
readonly class DownloadResponse implements ResponseInterface
|
||||
{
|
||||
public function __construct(
|
||||
public Response $httpResponse
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Contract\Repository;
|
||||
|
||||
use App\Domain\Manga\Domain\Model\Chapter;
|
||||
|
||||
interface ChapterRepositoryInterface
|
||||
{
|
||||
public function findById(string $id): ?Chapter;
|
||||
public function findVisibleById(string $id): ?Chapter;
|
||||
public function save(Chapter $chapter): void;
|
||||
public function delete(Chapter $chapter): void;
|
||||
|
||||
/**
|
||||
* @return Chapter[]
|
||||
*/
|
||||
public function findByMangaIdAndVolume(string $mangaId, int $volume): array;
|
||||
|
||||
/**
|
||||
* @return Chapter[]
|
||||
*/
|
||||
public function findVisibleByMangaIdAndVolume(string $mangaId, int $volume): array;
|
||||
|
||||
/**
|
||||
* @return Chapter[]
|
||||
*/
|
||||
public function findVisibleWithCbzByMangaIdAndVolume(string $mangaId, int $volume): array;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Contract\Service;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
interface FileServiceInterface
|
||||
{
|
||||
/**
|
||||
* Télécharge un fichier CBZ
|
||||
*/
|
||||
public function downloadCbz(string $filePath, string $filename): Response;
|
||||
|
||||
/**
|
||||
* Crée un fichier ZIP contenant plusieurs CBZ
|
||||
* @param array<string> $cbzPaths
|
||||
*/
|
||||
public function createVolumeCbz(array $cbzPaths, string $volumeName): Response;
|
||||
|
||||
/**
|
||||
* Supprime un fichier CBZ du système de fichiers
|
||||
*/
|
||||
public function deleteCbzFile(string $filePath): bool;
|
||||
|
||||
/**
|
||||
* Vérifie si un fichier CBZ existe
|
||||
*/
|
||||
public function cbzExists(string $filePath): bool;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
||||
class CbzFileNotFoundException extends DomainException
|
||||
{
|
||||
public function __construct(string $filePath)
|
||||
{
|
||||
parent::__construct(sprintf('CBZ file "%s" not found', $filePath));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
||||
class ChapterNotAvailableException extends DomainException
|
||||
{
|
||||
public function __construct(int $chapterId)
|
||||
{
|
||||
parent::__construct(sprintf('Chapter "%d" is not available for download', $chapterId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
||||
class ChapterNotFoundException extends DomainException
|
||||
{
|
||||
public function __construct(string $chapterId)
|
||||
{
|
||||
parent::__construct(sprintf('Chapter with id "%s" not found', $chapterId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Domain\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
||||
class VolumeNotFoundException extends DomainException
|
||||
{
|
||||
public function __construct(string $mangaId, int $volume)
|
||||
{
|
||||
parent::__construct(sprintf('Volume %d for manga "%s" not found', $volume, $mangaId));
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ readonly class Chapter
|
||||
private ?string $title,
|
||||
private ?int $volume,
|
||||
private bool $isVisible,
|
||||
private bool $isAvailable = false,
|
||||
private ?string $cbzPath = null,
|
||||
private \DateTimeImmutable $createdAt = new \DateTimeImmutable()
|
||||
) {}
|
||||
|
||||
@@ -49,11 +49,16 @@ readonly class Chapter
|
||||
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return $this->isAvailable;
|
||||
return $this->cbzPath !== null;
|
||||
}
|
||||
|
||||
public function getCbzPath(): ?string
|
||||
{
|
||||
return $this->cbzPath;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): \DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor\DeleteCbzProcessor;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\DeleteCbzProvider;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Cbz',
|
||||
operations: [
|
||||
new Delete(
|
||||
uriTemplate: '/manga/chapters/{id}/cbz',
|
||||
provider: DeleteCbzProvider::class,
|
||||
processor: DeleteCbzProcessor::class,
|
||||
name: 'delete_cbz',
|
||||
openapiContext: [
|
||||
'summary' => 'Delete chapter CBZ file',
|
||||
'description' => 'Removes the CBZ file for a specific chapter and updates the chapter accordingly',
|
||||
'parameters' => [
|
||||
[
|
||||
'name' => 'id',
|
||||
'in' => 'path',
|
||||
'required' => true,
|
||||
'schema' => [
|
||||
'type' => 'string'
|
||||
],
|
||||
'description' => 'The chapter ID'
|
||||
]
|
||||
],
|
||||
'responses' => [
|
||||
'204' => [
|
||||
'description' => 'CBZ file successfully deleted'
|
||||
],
|
||||
'404' => [
|
||||
'description' => 'Chapter or CBZ file not found'
|
||||
]
|
||||
]
|
||||
]
|
||||
)
|
||||
]
|
||||
)]
|
||||
class DeleteCbzResource
|
||||
{
|
||||
public function __construct(
|
||||
public string $id
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor\DeleteChapterProcessor;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\DeleteChapterProvider;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Chapters',
|
||||
operations: [
|
||||
new Delete(
|
||||
uriTemplate: '/manga/chapters/{id}',
|
||||
provider: DeleteChapterProvider::class,
|
||||
processor: DeleteChapterProcessor::class,
|
||||
name: 'delete_chapter',
|
||||
openapiContext: [
|
||||
'summary' => 'Delete a chapter (soft delete)',
|
||||
'description' => 'Marks a chapter as deleted by setting its visibility to false',
|
||||
'parameters' => [
|
||||
[
|
||||
'name' => 'id',
|
||||
'in' => 'path',
|
||||
'required' => true,
|
||||
'schema' => [
|
||||
'type' => 'string'
|
||||
],
|
||||
'description' => 'The chapter ID'
|
||||
]
|
||||
],
|
||||
'responses' => [
|
||||
'204' => [
|
||||
'description' => 'Chapter successfully deleted'
|
||||
],
|
||||
'404' => [
|
||||
'description' => 'Chapter not found'
|
||||
]
|
||||
]
|
||||
]
|
||||
)
|
||||
]
|
||||
)]
|
||||
class DeleteChapterResource
|
||||
{
|
||||
public function __construct(
|
||||
public string $id
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\DownloadCbzProvider;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Cbz',
|
||||
operations: [
|
||||
new Get(
|
||||
uriTemplate: '/manga/chapters/{id}/download',
|
||||
provider: DownloadCbzProvider::class,
|
||||
output: false,
|
||||
name: 'download_chapter_cbz'
|
||||
)
|
||||
]
|
||||
)]
|
||||
class DownloadCbzResource
|
||||
{
|
||||
public function __construct(
|
||||
public string $id
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\DownloadVolumeProvider;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Cbz',
|
||||
operations: [
|
||||
new Get(
|
||||
uriTemplate: '/mangas/{id}/volumes/{volume}/download',
|
||||
provider: DownloadVolumeProvider::class,
|
||||
output: false,
|
||||
name: 'download_manga_volume'
|
||||
)
|
||||
]
|
||||
)]
|
||||
class DownloadVolumeResource
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public int $volume
|
||||
) {}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor\FetchMangaChapte
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'MangaChapters',
|
||||
shortName: 'Chapters',
|
||||
operations: [
|
||||
new Post(
|
||||
uriTemplate: '/manga/chapters/fetch',
|
||||
@@ -23,4 +23,4 @@ class FetchMangaChaptersResource
|
||||
#[Assert\NotBlank]
|
||||
public string $externalId
|
||||
) {}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use App\Domain\Manga\Infrastructure\ApiPlatform\Dto\ChapterCollection;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\GetMangaChaptersStateProvider;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'MangaChapters',
|
||||
shortName: 'Chapters',
|
||||
operations: [
|
||||
new Get(
|
||||
uriTemplate: '/mangas/{id}/chapters',
|
||||
@@ -63,4 +63,4 @@ use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\GetMangaChaptersS
|
||||
)]
|
||||
class MangaChaptersResource
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Domain\Manga\Application\Command\DeleteCbz;
|
||||
use App\Domain\Manga\Application\CommandHandler\DeleteCbzHandler;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\Resource\DeleteCbzResource;
|
||||
|
||||
readonly class DeleteCbzProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DeleteCbzHandler $handler
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
|
||||
{
|
||||
assert($data instanceof DeleteCbzResource);
|
||||
|
||||
$command = new DeleteCbz($data->id);
|
||||
$this->handler->handle($command);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Domain\Manga\Application\Command\DeleteChapter;
|
||||
use App\Domain\Manga\Application\CommandHandler\DeleteChapterHandler;
|
||||
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
readonly class DeleteChapterProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DeleteChapterHandler $handler
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
|
||||
{
|
||||
if (!isset($uriVariables['id'])) {
|
||||
throw new \InvalidArgumentException('Chapter ID is required');
|
||||
}
|
||||
|
||||
try {
|
||||
$command = new DeleteChapter($uriVariables['id']);
|
||||
$this->handler->handle($command);
|
||||
} catch (ChapterNotFoundException $e) {
|
||||
throw new NotFoundHttpException('Chapter not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
|
||||
use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\Resource\DeleteCbzResource;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
readonly class DeleteCbzProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ChapterRepositoryInterface $chapterRepository
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): DeleteCbzResource
|
||||
{
|
||||
if (!isset($uriVariables['id'])) {
|
||||
throw new NotFoundHttpException('Chapter ID is required');
|
||||
}
|
||||
|
||||
$chapterId = $uriVariables['id'];
|
||||
|
||||
try {
|
||||
$chapter = $this->chapterRepository->findVisibleById($chapterId);
|
||||
|
||||
if (!$chapter) {
|
||||
throw new ChapterNotFoundException($chapterId);
|
||||
}
|
||||
|
||||
if (!$chapter->isAvailable()) {
|
||||
throw new CbzFileNotFoundException($chapterId);
|
||||
}
|
||||
|
||||
return new DeleteCbzResource($chapterId);
|
||||
|
||||
} catch (ChapterNotFoundException $e) {
|
||||
throw new NotFoundHttpException('Chapter not found');
|
||||
} catch (CbzFileNotFoundException $e) {
|
||||
throw new NotFoundHttpException('CBZ file not found for this chapter');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\Resource\DeleteChapterResource;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
readonly class DeleteChapterProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ChapterRepositoryInterface $chapterRepository
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): DeleteChapterResource
|
||||
{
|
||||
if (!isset($uriVariables['id'])) {
|
||||
throw new NotFoundHttpException('Chapter ID is required');
|
||||
}
|
||||
|
||||
$chapterId = $uriVariables['id'];
|
||||
|
||||
try {
|
||||
$chapter = $this->chapterRepository->findVisibleById($chapterId);
|
||||
|
||||
if (!$chapter) {
|
||||
throw new ChapterNotFoundException($chapterId);
|
||||
}
|
||||
|
||||
return new DeleteChapterResource($chapterId);
|
||||
|
||||
} catch (ChapterNotFoundException $e) {
|
||||
throw new NotFoundHttpException('Chapter not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Domain\Manga\Application\Query\DownloadCbz;
|
||||
use App\Domain\Manga\Application\QueryHandler\DownloadCbzHandler;
|
||||
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
|
||||
use App\Domain\Manga\Domain\Exception\ChapterNotAvailableException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
readonly class DownloadCbzProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DownloadCbzHandler $handler
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
|
||||
{
|
||||
if (!isset($uriVariables['id'])) {
|
||||
throw new \InvalidArgumentException('Chapter ID is required');
|
||||
}
|
||||
|
||||
$query = new DownloadCbz($uriVariables['id']);
|
||||
|
||||
try {
|
||||
$downloadResponse = $this->handler->handle($query);
|
||||
return $downloadResponse->httpResponse;
|
||||
} catch (ChapterNotAvailableException|ChapterNotFoundException $e) {
|
||||
throw new NotFoundHttpException($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Domain\Manga\Application\Query\DownloadVolume;
|
||||
use App\Domain\Manga\Application\QueryHandler\DownloadVolumeHandler;
|
||||
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||
use App\Domain\Manga\Domain\Exception\VolumeNotFoundException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
readonly class DownloadVolumeProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DownloadVolumeHandler $handler
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
|
||||
{
|
||||
if (!isset($uriVariables['id']) || !isset($uriVariables['volume'])) {
|
||||
throw new \InvalidArgumentException('Manga ID and volume are required');
|
||||
}
|
||||
|
||||
$query = new DownloadVolume($uriVariables['id'], (int) $uriVariables['volume']);
|
||||
|
||||
try {
|
||||
$downloadResponse = $this->handler->handle($query);
|
||||
return $downloadResponse->httpResponse;
|
||||
} catch (MangaNotFoundException|VolumeNotFoundException $e) {
|
||||
throw new NotFoundHttpException($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,8 +52,8 @@ readonly class GetMangaChaptersStateProvider implements ProviderInterface
|
||||
title: $chapter->title,
|
||||
volume: $chapter->volume,
|
||||
isVisible: $chapter->isVisible,
|
||||
isAvailable: $chapter->isAvailable,
|
||||
isAvailable: $chapter->cbzPath !== null,
|
||||
createdAt: $chapter->createdAt->format(\DateTimeInterface::RFC3339)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,15 +230,15 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
|
||||
id: new MangaId((string) $entity->getId()),
|
||||
title: new MangaTitle($entity->getTitle()),
|
||||
slug: new MangaSlug($entity->getSlug()),
|
||||
description: $entity->getDescription(),
|
||||
author: $entity->getAuthor(),
|
||||
publicationYear: $entity->getPublicationYear(),
|
||||
genres: $entity->getGenres(),
|
||||
status: $entity->getStatus(),
|
||||
description: $entity->getDescription() ?? '',
|
||||
author: $entity->getAuthor() ?? '',
|
||||
publicationYear: $entity->getPublicationYear() ?? 0,
|
||||
genres: $entity->getGenres() ?? [],
|
||||
status: $entity->getStatus() ?? '',
|
||||
externalId: $entity->getExternalId() ? new ExternalId($entity->getExternalId()) : null,
|
||||
imageUrl: $entity->getImageUrl(),
|
||||
rating: $entity->getRating(),
|
||||
imageUrls: $entity->getImageUrl() ? new ImageUrls($entity->getImageUrl(), $entity->getThumbnailUrl()) : null,
|
||||
imageUrls: $entity->getImageUrl() ? new ImageUrls($entity->getImageUrl() ?? '', $entity->getThumbnailUrl() ?? '') : null,
|
||||
createdAt: $entity->getCreatedAt(),
|
||||
);
|
||||
}
|
||||
@@ -252,7 +252,7 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
|
||||
title: $entity->getTitle(),
|
||||
volume: $entity->getVolume(),
|
||||
isVisible: $entity->isVisible(),
|
||||
isAvailable: $entity->getCbzPath() !== null
|
||||
cbzPath: $entity->getCbzPath()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\Persistence\Repository;
|
||||
|
||||
use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||
use App\Domain\Manga\Domain\Model\Chapter;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
|
||||
use App\Entity\Chapter as ChapterEntity;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
readonly class LegacyChapterRepository implements ChapterRepositoryInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager
|
||||
) {}
|
||||
|
||||
public function findById(string $id): ?Chapter
|
||||
{
|
||||
$entity = $this->entityManager->find(ChapterEntity::class, $id);
|
||||
|
||||
return $entity ? $this->toDomainModel($entity) : null;
|
||||
}
|
||||
|
||||
public function findVisibleById(string $id): ?Chapter
|
||||
{
|
||||
$qb = $this->entityManager->createQueryBuilder()
|
||||
->select('c')
|
||||
->from(ChapterEntity::class, 'c')
|
||||
->where('c.id = :id')
|
||||
->andWhere('c.visible = :visible')
|
||||
->setParameter('id', $id)
|
||||
->setParameter('visible', 1);
|
||||
|
||||
$entity = $qb->getQuery()->getOneOrNullResult();
|
||||
|
||||
return $entity ? $this->toDomainModel($entity) : null;
|
||||
}
|
||||
|
||||
public function save(Chapter $chapter): void
|
||||
{
|
||||
$entity = $this->entityManager->find(ChapterEntity::class, $chapter->getId());
|
||||
|
||||
if (!$entity) {
|
||||
throw new \RuntimeException(sprintf('Chapter with id %s not found', $chapter->getId()));
|
||||
}
|
||||
|
||||
$entity->setVisible($chapter->isVisible());
|
||||
$entity->setCbzPath($chapter->getCbzPath());
|
||||
|
||||
$this->entityManager->persist($entity);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function delete(Chapter $chapter): void
|
||||
{
|
||||
$entity = $this->entityManager->find(ChapterEntity::class, $chapter->getId());
|
||||
|
||||
if ($entity) {
|
||||
$this->entityManager->remove($entity);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
public function findByMangaIdAndVolume(string $mangaId, int $volume): array
|
||||
{
|
||||
$entities = $this->entityManager->getRepository(ChapterEntity::class)
|
||||
->findBy(['manga' => $mangaId, 'volume' => $volume]);
|
||||
|
||||
return array_map([$this, 'toDomainModel'], $entities);
|
||||
}
|
||||
|
||||
public function findVisibleByMangaIdAndVolume(string $mangaId, int $volume): array
|
||||
{
|
||||
$entities = $this->entityManager->getRepository(ChapterEntity::class)
|
||||
->findBy(['manga' => $mangaId, 'volume' => $volume, 'visible' => true]);
|
||||
|
||||
return array_map([$this, 'toDomainModel'], $entities);
|
||||
}
|
||||
|
||||
public function findVisibleWithCbzByMangaIdAndVolume(string $mangaId, int $volume): array
|
||||
{
|
||||
$qb = $this->entityManager->createQueryBuilder()
|
||||
->select('c')
|
||||
->from(ChapterEntity::class, 'c')
|
||||
->where('c.manga = :mangaId')
|
||||
->andWhere('c.volume = :volume')
|
||||
->andWhere('c.visible = true')
|
||||
->andWhere('c.cbzPath IS NOT NULL')
|
||||
->setParameter('mangaId', $mangaId)
|
||||
->setParameter('volume', $volume)
|
||||
->orderBy('c.number', 'ASC');
|
||||
|
||||
$entities = $qb->getQuery()->getResult();
|
||||
|
||||
return array_map([$this, 'toDomainModel'], $entities);
|
||||
}
|
||||
|
||||
private function toDomainModel(ChapterEntity $entity): Chapter
|
||||
{
|
||||
return new Chapter(
|
||||
new ChapterId((string) $entity->getId()),
|
||||
(string) $entity->getManga()->getId(),
|
||||
$entity->getNumber(),
|
||||
$entity->getTitle(),
|
||||
$entity->getVolume(),
|
||||
$entity->isVisible(),
|
||||
$entity->getCbzPath(),
|
||||
new \DateTimeImmutable()
|
||||
);
|
||||
}
|
||||
}
|
||||
77
src/Domain/Manga/Infrastructure/Service/FileService.php
Normal file
77
src/Domain/Manga/Infrastructure/Service/FileService.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\Service;
|
||||
|
||||
use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface;
|
||||
use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
|
||||
readonly class FileService implements FileServiceInterface
|
||||
{
|
||||
public function __construct(
|
||||
private string $cbzStoragePath = '/app/public/cbz'
|
||||
) {}
|
||||
|
||||
public function downloadCbz(string $filePath, string $filename): Response
|
||||
{
|
||||
if (!$this->cbzExists($filePath)) {
|
||||
throw new CbzFileNotFoundException($filePath);
|
||||
}
|
||||
|
||||
$response = new BinaryFileResponse($filePath);
|
||||
$response->setContentDisposition(
|
||||
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
||||
$filename
|
||||
);
|
||||
$response->headers->set('Content-Type', 'application/x-cbz');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function createVolumeCbz(array $cbzPaths, string $volumeName): Response
|
||||
{
|
||||
$tempCbzPath = sys_get_temp_dir() . '/' . $volumeName . '.cbz';
|
||||
|
||||
$cbz = new \ZipArchive();
|
||||
if ($cbz->open($tempCbzPath, \ZipArchive::CREATE) !== true) {
|
||||
throw new \RuntimeException('Cannot create CBZ file');
|
||||
}
|
||||
|
||||
foreach ($cbzPaths as $cbzPath) {
|
||||
if ($this->cbzExists($cbzPath)) {
|
||||
$filename = basename($cbzPath);
|
||||
$cbz->addFile($cbzPath, $filename);
|
||||
}
|
||||
}
|
||||
|
||||
$cbz->close();
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
public function deleteCbzFile(string $filePath): bool
|
||||
{
|
||||
if (!$this->cbzExists($filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return unlink($filePath);
|
||||
}
|
||||
|
||||
public function cbzExists(string $filePath): bool
|
||||
{
|
||||
return file_exists($filePath) && is_readable($filePath);
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,23 @@ class InMemoryMangaRepository implements MangaRepositoryInterface
|
||||
$this->mangas[$manga->getId()] = $manga;
|
||||
}
|
||||
|
||||
public function updatePreferredSources(string $mangaId, array $sourceIds): void
|
||||
{
|
||||
if (isset($this->mangas[$mangaId])) {
|
||||
$manga = $this->mangas[$mangaId];
|
||||
$updatedManga = new Manga(
|
||||
$manga->getId(),
|
||||
$manga->getTitle(),
|
||||
$manga->getSlug(),
|
||||
$manga->getDescription(),
|
||||
$manga->getAuthor(),
|
||||
$manga->getPublicationYear(),
|
||||
$sourceIds // Mise à jour des sources préférées
|
||||
);
|
||||
$this->mangas[$mangaId] = $updatedManga;
|
||||
}
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->mangas = [];
|
||||
|
||||
@@ -50,6 +50,42 @@ class InMemorySourceRepository implements SourceRepositoryInterface
|
||||
$this->sources[$source->getId()->getValue()] = $source;
|
||||
}
|
||||
|
||||
public function validateSourcesExist(array $sourceIds): bool
|
||||
{
|
||||
foreach ($sourceIds as $sourceId) {
|
||||
$source = $this->sources[$sourceId] ?? null;
|
||||
if (!$source || !$source->isActive()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Source[]
|
||||
*/
|
||||
public function getByIds(array $sourceIds): array
|
||||
{
|
||||
$sources = [];
|
||||
foreach ($sourceIds as $sourceId) {
|
||||
if (isset($this->sources[$sourceId])) {
|
||||
$sources[] = $this->sources[$sourceId];
|
||||
}
|
||||
}
|
||||
return $sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Source[]
|
||||
*/
|
||||
public function getAllActive(): array
|
||||
{
|
||||
return array_filter(
|
||||
array_values($this->sources),
|
||||
fn(Source $source) => $source->isActive()
|
||||
);
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->sources = [];
|
||||
|
||||
108
tests/Feature/Manga/DeleteCbzTest.php
Normal file
108
tests/Feature/Manga/DeleteCbzTest.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Feature\Manga;
|
||||
|
||||
use App\Entity\Chapter;
|
||||
use App\Factory\ChapterFactory;
|
||||
use App\Factory\MangaFactory;
|
||||
use App\Tests\Feature\AbstractApiTestCase;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Zenstruck\Foundry\Test\Factories;
|
||||
use Zenstruck\Foundry\Test\ResetDatabase;
|
||||
|
||||
class DeleteCbzTest extends AbstractApiTestCase
|
||||
{
|
||||
use ResetDatabase, Factories;
|
||||
|
||||
public function test_it_deletes_chapter_cbz_file(): void
|
||||
{
|
||||
// Arrange
|
||||
$manga = MangaFactory::createOne([
|
||||
'title' => 'One Piece',
|
||||
'slug' => 'one-piece'
|
||||
]);
|
||||
|
||||
$chapter = ChapterFactory::createOne([
|
||||
'manga' => $manga,
|
||||
'number' => 1.0,
|
||||
'title' => 'Chapter 1',
|
||||
'visible' => true,
|
||||
'cbzPath' => '/path/to/test.cbz'
|
||||
]);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$chapterId = $chapter->getId();
|
||||
|
||||
// Act
|
||||
static::createClient()->request('DELETE', "/api/manga/chapters/{$chapterId}/cbz");
|
||||
|
||||
// Then
|
||||
$this->assertResponseStatusCodeSame(204);
|
||||
|
||||
// Verify the chapter CBZ was removed
|
||||
$freshChapter = $this->entityManager->find(Chapter::class, $chapterId);
|
||||
$this->assertEmpty($freshChapter->getCbzPath());
|
||||
}
|
||||
|
||||
public function test_it_returns_404_for_non_existent_chapter(): void
|
||||
{
|
||||
// When
|
||||
static::createClient()->request('DELETE', '/api/manga/chapters/999999/cbz');
|
||||
|
||||
// Then
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
public function test_it_returns_404_for_chapter_without_cbz(): void
|
||||
{
|
||||
// Arrange
|
||||
$manga = MangaFactory::createOne([
|
||||
'title' => 'Test Manga',
|
||||
'slug' => 'test-manga'
|
||||
]);
|
||||
|
||||
$chapter = ChapterFactory::createOne([
|
||||
'manga' => $manga,
|
||||
'number' => 1.0,
|
||||
'title' => 'Test Chapter',
|
||||
'visible' => true,
|
||||
'cbzPath' => null // No CBZ file
|
||||
]);
|
||||
|
||||
$this->entityManager->flush();
|
||||
$chapterId = $chapter->getId();
|
||||
|
||||
// When
|
||||
static::createClient()->request('DELETE', "/api/manga/chapters/{$chapterId}/cbz");
|
||||
|
||||
// Then
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
public function test_it_returns_404_for_invisible_chapter(): void
|
||||
{
|
||||
// Arrange
|
||||
$manga = MangaFactory::createOne([
|
||||
'title' => 'Test Manga',
|
||||
'slug' => 'test-manga'
|
||||
]);
|
||||
|
||||
$chapter = ChapterFactory::createOne([
|
||||
'manga' => $manga,
|
||||
'number' => 1.0,
|
||||
'title' => 'Test Chapter',
|
||||
'visible' => false, // Invisible chapter
|
||||
'cbzPath' => '/path/to/test.cbz'
|
||||
]);
|
||||
|
||||
$this->entityManager->flush();
|
||||
$chapterId = $chapter->getId();
|
||||
|
||||
// When
|
||||
static::createClient()->request('DELETE', "/api/manga/chapters/{$chapterId}/cbz");
|
||||
|
||||
// Then
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
}
|
||||
78
tests/Feature/Manga/DeleteChapterTest.php
Normal file
78
tests/Feature/Manga/DeleteChapterTest.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Feature\Manga;
|
||||
|
||||
use App\Entity\Chapter;
|
||||
use App\Factory\ChapterFactory;
|
||||
use App\Factory\MangaFactory;
|
||||
use App\Tests\Feature\AbstractApiTestCase;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Zenstruck\Foundry\Test\Factories;
|
||||
use Zenstruck\Foundry\Test\ResetDatabase;
|
||||
|
||||
class DeleteChapterTest extends AbstractApiTestCase
|
||||
{
|
||||
use ResetDatabase, Factories;
|
||||
|
||||
public function test_it_soft_deletes_chapter(): void
|
||||
{
|
||||
// Arrange
|
||||
$manga = MangaFactory::createOne([
|
||||
'title' => 'One Piece',
|
||||
'slug' => 'one-piece'
|
||||
]);
|
||||
|
||||
$chapter = ChapterFactory::createOne([
|
||||
'manga' => $manga,
|
||||
'number' => 1.0,
|
||||
'title' => 'Chapter 1',
|
||||
'visible' => true
|
||||
]);
|
||||
|
||||
$chapterId = $chapter->getId();
|
||||
|
||||
// Act
|
||||
static::createClient()->request('DELETE', "/api/manga/chapters/{$chapterId}");
|
||||
|
||||
// Then
|
||||
$this->assertResponseStatusCodeSame(204);
|
||||
|
||||
// Verify the chapter was soft deleted (visible = false)
|
||||
$freshChapter = $this->entityManager->find(Chapter::class, $chapterId);
|
||||
$this->assertFalse($freshChapter->isVisible());
|
||||
}
|
||||
|
||||
public function test_it_returns_404_for_non_existent_chapter(): void
|
||||
{
|
||||
// When
|
||||
static::createClient()->request('DELETE', '/api/manga/chapters/999999');
|
||||
|
||||
// Then
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
public function test_it_returns_404_for_already_soft_deleted_chapter(): void
|
||||
{
|
||||
// Arrange
|
||||
$manga = MangaFactory::createOne([
|
||||
'title' => 'Test Manga',
|
||||
'slug' => 'test-manga'
|
||||
]);
|
||||
|
||||
$chapter = ChapterFactory::createOne([
|
||||
'manga' => $manga,
|
||||
'number' => 1.0,
|
||||
'title' => 'Test Chapter',
|
||||
'visible' => false // Already soft deleted
|
||||
]);
|
||||
|
||||
$this->entityManager->flush();
|
||||
$chapterId = $chapter->getId();
|
||||
|
||||
// When
|
||||
static::createClient()->request('DELETE', "/api/manga/chapters/{$chapterId}");
|
||||
|
||||
// Then
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
}
|
||||
81
tests/Feature/Manga/DownloadCbzTest.php
Normal file
81
tests/Feature/Manga/DownloadCbzTest.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Feature\Manga;
|
||||
|
||||
use App\Entity\Chapter;
|
||||
use App\Factory\ChapterFactory;
|
||||
use App\Factory\MangaFactory;
|
||||
use App\Tests\Feature\AbstractApiTestCase;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Zenstruck\Foundry\Test\Factories;
|
||||
use Zenstruck\Foundry\Test\ResetDatabase;
|
||||
|
||||
class DownloadCbzTest extends AbstractApiTestCase
|
||||
{
|
||||
use ResetDatabase, Factories;
|
||||
|
||||
public function test_it_downloads_chapter_cbz(): void
|
||||
{
|
||||
// Arrange
|
||||
$manga = MangaFactory::createOne([
|
||||
'title' => 'One Piece',
|
||||
'slug' => 'one-piece'
|
||||
]);
|
||||
|
||||
$chapter = ChapterFactory::createOne([
|
||||
'manga' => $manga,
|
||||
'number' => 1.0,
|
||||
'title' => 'Chapter 1',
|
||||
'visible' => true,
|
||||
'cbzPath' => '/app/tests/Shared/Files/test-chapter.cbz'
|
||||
]);
|
||||
|
||||
$chapterId = $chapter->getId();
|
||||
|
||||
// Act
|
||||
static::createClient()->request('GET', "/api/manga/chapters/{$chapterId}/download");
|
||||
|
||||
// Then
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$response = static::getClient()->getResponse();
|
||||
$this->assertEquals('application/x-cbz', $response->headers->get('Content-Type'));
|
||||
$this->assertStringContainsString('attachment; filename=', $response->headers->get('Content-Disposition'));
|
||||
$this->assertStringContainsString('test-chapter.cbz', $response->headers->get('Content-Disposition'));
|
||||
}
|
||||
|
||||
public function test_it_returns_404_for_non_existent_chapter(): void
|
||||
{
|
||||
// When
|
||||
static::createClient()->request('GET', '/api/manga/chapters/999999/download');
|
||||
|
||||
// Then
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
public function test_it_returns_404_for_chapter_without_cbz(): void
|
||||
{
|
||||
// Arrange
|
||||
$manga = MangaFactory::createOne([
|
||||
'title' => 'Test Manga',
|
||||
'slug' => 'test-manga'
|
||||
]);
|
||||
|
||||
$chapter = ChapterFactory::createOne([
|
||||
'manga' => $manga,
|
||||
'number' => 1.0,
|
||||
'title' => 'Test Chapter',
|
||||
'visible' => true,
|
||||
'cbzPath' => null // No CBZ file
|
||||
]);
|
||||
|
||||
$this->entityManager->flush();
|
||||
$chapterId = $chapter->getId();
|
||||
|
||||
// When
|
||||
static::createClient()->request('GET', "/api/manga/chapters/{$chapterId}/download");
|
||||
|
||||
// Then
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
146
tests/Feature/Manga/DownloadVolumeTest.php
Normal file
146
tests/Feature/Manga/DownloadVolumeTest.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Feature\Manga;
|
||||
|
||||
use App\Entity\Chapter;
|
||||
use App\Factory\ChapterFactory;
|
||||
use App\Factory\MangaFactory;
|
||||
use App\Tests\Feature\AbstractApiTestCase;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Zenstruck\Foundry\Test\Factories;
|
||||
use Zenstruck\Foundry\Test\ResetDatabase;
|
||||
|
||||
class DownloadVolumeTest extends AbstractApiTestCase
|
||||
{
|
||||
use ResetDatabase, Factories;
|
||||
|
||||
public function test_it_downloads_volume_cbz(): void
|
||||
{
|
||||
// Arrange
|
||||
$manga = MangaFactory::createOne([
|
||||
'title' => 'One Piece',
|
||||
'slug' => 'one-piece'
|
||||
]);
|
||||
|
||||
// Create chapters for volume 1
|
||||
ChapterFactory::createMany(3, [
|
||||
'manga' => $manga,
|
||||
'volume' => 1,
|
||||
'visible' => true,
|
||||
'cbzPath' => '/app/tests/Shared/Files/test-chapter.cbz'
|
||||
]);
|
||||
|
||||
$mangaId = $manga->getId();
|
||||
|
||||
// Act
|
||||
static::createClient()->request('GET', "/api/mangas/{$mangaId}/volumes/1/download");
|
||||
|
||||
// Assert
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertResponseHeaderSame('Content-Type', 'application/x-cbz');
|
||||
$contentDisposition = static::getClient()->getResponse()->headers->get('Content-Disposition');
|
||||
$this->assertStringContainsString('attachment; filename=', $contentDisposition);
|
||||
$this->assertStringContainsString('one-piece-volume-1.cbz', $contentDisposition);
|
||||
}
|
||||
|
||||
public function test_it_returns_404_when_manga_not_found(): void
|
||||
{
|
||||
// Act
|
||||
static::createClient()->request('GET', '/api/mangas/999999/volumes/1/download');
|
||||
|
||||
// Assert
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
public function test_it_returns_404_when_volume_not_found(): void
|
||||
{
|
||||
// Arrange
|
||||
$manga = MangaFactory::createOne([
|
||||
'title' => 'One Piece',
|
||||
'slug' => 'one-piece'
|
||||
]);
|
||||
|
||||
$mangaId = $manga->getId();
|
||||
|
||||
// Act
|
||||
static::createClient()->request('GET', "/api/mangas/{$mangaId}/volumes/999/download");
|
||||
|
||||
// Assert
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
public function test_it_returns_404_when_no_available_chapters_in_volume(): void
|
||||
{
|
||||
// Arrange
|
||||
$manga = MangaFactory::createOne([
|
||||
'title' => 'One Piece',
|
||||
'slug' => 'one-piece'
|
||||
]);
|
||||
|
||||
// Create chapters for volume 1 but all without CBZ files
|
||||
ChapterFactory::createMany(3, [
|
||||
'manga' => $manga,
|
||||
'volume' => 1,
|
||||
'visible' => true,
|
||||
'cbzPath' => null // No CBZ files
|
||||
]);
|
||||
|
||||
$mangaId = $manga->getId();
|
||||
|
||||
// Act
|
||||
static::createClient()->request('GET', "/api/mangas/{$mangaId}/volumes/1/download");
|
||||
|
||||
// Assert
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
public function test_it_only_includes_visible_chapters_with_cbz(): void
|
||||
{
|
||||
// Arrange
|
||||
$manga = MangaFactory::createOne([
|
||||
'title' => 'One Piece',
|
||||
'slug' => 'one-piece'
|
||||
]);
|
||||
|
||||
// Create a mix of chapters
|
||||
ChapterFactory::createOne([
|
||||
'manga' => $manga,
|
||||
'volume' => 1,
|
||||
'number' => 1.0,
|
||||
'visible' => true,
|
||||
'cbzPath' => '/app/tests/Shared/Files/test-chapter.cbz'
|
||||
]);
|
||||
|
||||
ChapterFactory::createOne([
|
||||
'manga' => $manga,
|
||||
'volume' => 1,
|
||||
'number' => 2.0,
|
||||
'visible' => false, // Soft deleted
|
||||
'cbzPath' => '/app/tests/Shared/Files/test-chapter.cbz'
|
||||
]);
|
||||
|
||||
ChapterFactory::createOne([
|
||||
'manga' => $manga,
|
||||
'volume' => 1,
|
||||
'number' => 3.0,
|
||||
'visible' => true,
|
||||
'cbzPath' => null // No CBZ
|
||||
]);
|
||||
|
||||
ChapterFactory::createOne([
|
||||
'manga' => $manga,
|
||||
'volume' => 1,
|
||||
'number' => 4.0,
|
||||
'visible' => true,
|
||||
'cbzPath' => '/app/tests/Shared/Files/test-chapter.cbz'
|
||||
]);
|
||||
|
||||
$mangaId = $manga->getId();
|
||||
|
||||
// Act
|
||||
static::createClient()->request('GET', "/api/mangas/{$mangaId}/volumes/1/download");
|
||||
|
||||
// Assert - Should succeed with only 2 chapters (1 and 4)
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
||||
BIN
tests/Shared/Files/test-chapter.cbz
Normal file
BIN
tests/Shared/Files/test-chapter.cbz
Normal file
Binary file not shown.
Reference in New Issue
Block a user