Skip to content

Commit

Permalink
Use the kepub url extension to provide the ebook to kobo
Browse files Browse the repository at this point in the history
  • Loading branch information
ragusa87 committed Aug 18, 2024
1 parent 417a234 commit 5d5780b
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 22 deletions.
4 changes: 2 additions & 2 deletions src/Controller/Kobo/KoboDownloadController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ public function __construct(
}

#[Route('/v1/download/{id}.{extension}', name: 'download', requirements: ['bookId' => '\d+', 'extension' => '[A-Za-z0-9]+'], methods: ['GET'])]
public function download(KoboDevice $kobo, Book $book): Response
public function download(KoboDevice $kobo, Book $book, string $extension): Response
{
$this->assertCanDownload($kobo, $book);

return $this->downloadHelper->getResponse($book);
return $this->downloadHelper->getResponse($book, $extension);
}

private function assertCanDownload(KoboDevice $kobo, Book $book): void
Expand Down
29 changes: 20 additions & 9 deletions src/Kobo/DownloadHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
use App\Entity\KoboDevice;
use App\Exception\BookFileNotFound;
use App\Kobo\ImageProcessor\CoverTransformer;
use App\Kobo\Messenger\KepubifyMessage;
use App\Kobo\Kepubify\KepubifyMessage;
use App\Kobo\Response\MetadataResponseService;
use App\Service\BookFileSystemManager;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
Expand Down Expand Up @@ -43,12 +44,12 @@ public function getCoverSize(Book $book): int
return $this->fileSystemManager->getCoverSize($book) ?? 0;
}

public function getUrlForKoboDevice(Book $book, KoboDevice $kobo): string
public function getUrlForKoboDevice(Book $book, KoboDevice $kobo, string $extension): string
{
return $this->urlGenerator->generate('app_kobodownload', [
'id' => $book->getId(),
'accessKey' => $kobo->getAccessKey(),
'extension' => $book->getExtension(),
'extension' => $extension,
], UrlGeneratorInterface::ABSOLUTE_URL);
}

Expand Down Expand Up @@ -91,27 +92,37 @@ public function getCoverResponse(Book $book, int $width, int $height, bool $gray
return $response;
}

public function getResponse(Book $book): Response
/**
* @throws NotFoundHttpException Book conversion failed
*/
public function getResponse(Book $book, string $format): Response
{
$bookPath = $this->getBookFilename($book);
if (false === $this->exists($book)) {
throw new BookFileNotFound($bookPath);
}

// Convert the file to kepub (Kobo's epub format) as a temporary file
$temporaryFile = $this->runKepubify($bookPath);
$temporaryFile = null;

if ($format === MetadataResponseService::KEPUB_FORMAT) {
$temporaryFile = $this->runKepubify($bookPath);
if ($temporaryFile === null) {
throw new NotFoundHttpException('The conversion to KEPUB failed');
}
}

$fileToStream = $temporaryFile ?? $bookPath;
$fileSize = filesize($fileToStream);

$response = (new BinaryFileResponse($fileToStream, Response::HTTP_OK))
->deleteFileAfterSend($temporaryFile !== null);

$filename = $book->getBookFilename();
$filename = basename($book->getBookFilename(), $book->getExtension()).strtolower($format);
$encodedFilename = rawurlencode($filename);
$simpleName = rawurlencode(sprintf('book-%s-%s', $book->getId(), preg_replace('/[^a-zA-Z0-9\.\-_]/', '_', $filename)));

$response->headers->set('Content-Type', match (strtolower($book->getExtension())) {
'epub', 'epub3' => 'application/epub+zip',
$response->headers->set('Content-Type', match (strtoupper($format)) {
MetadataResponseService::KEPUB_FORMAT, MetadataResponseService::EPUB_FORMAT, MetadataResponseService::EPUB3_FORMAT => 'application/epub+zip',
default => 'application/octet-stream'
});
$response->headers->set('Content-Disposition', HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, $encodedFilename, $simpleName));
Expand Down
37 changes: 37 additions & 0 deletions src/Kobo/Kepubify/KepubifyEnabler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace App\Kobo\Kepubify;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

class KepubifyEnabler
{
public function __construct(
#[Autowire(param: 'KEPUBIFY_BIN')]
private string $kepubifyBinary
) {
}

public function setKepubifyBinary(string $kepubifyBinary): void
{
$this->kepubifyBinary = $kepubifyBinary;
}

public function isEnabled(): bool
{
return trim($this->kepubifyBinary) !== '';
}

public function getKepubifyBinary(): string
{
return $this->kepubifyBinary;
}

public function disable(): string
{
$lastValue = $this->kepubifyBinary;
$this->kepubifyBinary = '';

return $lastValue;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace App\Kobo\Messenger;
namespace App\Kobo\Kepubify;

/**
* Convert an ebook with kepubify binary
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace App\Kobo\Messenger;
namespace App\Kobo\Kepubify;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
Expand All @@ -17,15 +17,14 @@ public function __construct(
#[Autowire(param: 'kernel.cache_dir')]
private readonly string $cacheDir,
private readonly LoggerInterface $logger,
#[Autowire(param: 'KEPUBIFY_BIN')]
private string $kepubifyBinary
private KepubifyEnabler $kepubifyEnabler
) {
}

public function __invoke(KepubifyMessage $message): void
{
// Disable kepubify if the path is not set
if (trim($this->kepubifyBinary) === '') {
if (false === $this->kepubifyEnabler->isEnabled()) {
return;
}

Expand All @@ -38,7 +37,7 @@ public function __invoke(KepubifyMessage $message): void
}

// Run the conversion
$process = new Process([$this->kepubifyBinary, '--output', $temporaryFile, $message->source]);
$process = new Process([$this->kepubifyEnabler->getKepubifyBinary(), '--output', $temporaryFile, $message->source]);
$this->logger->debug('Run kepubify command: {command}', ['command' => $process->getCommandLine()]);
$process->run();

Expand Down
18 changes: 15 additions & 3 deletions src/Kobo/Response/MetadataResponseService.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@
use App\Entity\Book;
use App\Entity\KoboDevice;
use App\Kobo\DownloadHelper;
use App\Kobo\Kepubify\KepubifyEnabler;
use App\Kobo\SyncToken;

class MetadataResponseService
{
public function __construct(protected DownloadHelper $downloadHelper)
public const KEPUB_FORMAT = 'KEPUB';
public const EPUB3_FORMAT = 'EPUB3';
public const EPUB_FORMAT = 'EPUB';

public function __construct(
protected DownloadHelper $downloadHelper,
protected KepubifyEnabler $kepubifyEnabler)
{
}

Expand All @@ -21,12 +28,17 @@ protected function getDownloadUrls(Book $book, KoboDevice $kobo, ?array $filters

$response = [];

$formats = ['EPUB3']; // EPUB3 is required for Kobo
$formats = [self::EPUB3_FORMAT]; // At least EPUB3 is required for Kobo
if ($this->kepubifyEnabler->isEnabled()) {
$formats[] = self::KEPUB_FORMAT;
}

foreach ($formats as $format) { // and ... EPUB3FL ?;
$extension = $format === self::KEPUB_FORMAT ? $format : $book->getExtension();
$response[] = [
'Format' => $format,
'Size' => $this->downloadHelper->getSize($book),
'Url' => $this->downloadHelper->getUrlForKoboDevice($book, $kobo),
'Url' => $this->downloadHelper->getUrlForKoboDevice($book, $kobo, $extension),
'Platform' => $platform,
];
}
Expand Down
63 changes: 61 additions & 2 deletions tests/Controller/Kobo/KoboDownloadControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
use App\Entity\Book;
use App\Entity\KoboDevice;
use App\Kobo\DownloadHelper;
use App\Kobo\Kepubify\KepubifyEnabler;
use App\Kobo\Response\MetadataResponseService;

class KoboDownloadControllerTest extends AbstractKoboControllerTest
{
Expand All @@ -14,6 +16,55 @@ public function testDownload(): void
$client = static::getClient();
$this->injectFakeFileSystemManager();

$book = $this->findByIdAndKobo(BookFixture::ID, $this->getKoboDevice());
self::assertNotNull($book, 'The book is not linked to the Kobo');

/** @var DownloadHelper $downloadHelper */
$downloadHelper = self::getContainer()->get(DownloadHelper::class);

self::assertTrue($downloadHelper->exists($book), 'The book file does not exist');

$client?->request('GET', sprintf('/kobo/%s/v1/download/%s.%s', $this->accessKey, BookFixture::ID, 'epub'));

self::assertResponseIsSuccessful();
self::assertResponseHeaderSame('Content-Type', 'application/epub+zip');
self::assertResponseHasHeader('Content-Length');
$expectedDisposition = "attachment; filename=book-1-TheOdysses.epub; filename*=utf-8''TheOdysses.epub";
self::assertResponseHeaderSame('Content-Disposition', $expectedDisposition, 'The Content-Disposition header is not as expected');

}

public function testDownloadKepubFailed(): void
{
$client = static::getClient();
$this->injectFakeFileSystemManager();

// Disable Kepubify conversion
$lastValue = $this->getKepubifyEnabler()->disable();

try {
$book = $this->findByIdAndKobo(BookFixture::ID, $this->getKoboDevice());
self::assertNotNull($book, 'The book is not linked to the Kobo');

/** @var DownloadHelper $downloadHelper */
$downloadHelper = self::getContainer()->get(DownloadHelper::class);

self::assertTrue($downloadHelper->exists($book), 'The book file does not exist');


$client?->request('GET', sprintf('/kobo/%s/v1/download/%s.%s', $this->accessKey, BookFixture::ID, MetadataResponseService::KEPUB_FORMAT));
self::assertResponseStatusCodeSame(404); // We can not download kepub as the conversion is disabled
} finally {
// Re-enable Kepubify conversion for other tests
$this->getKepubifyEnabler()->setKepubifyBinary($lastValue);
}
}

public function testDownloadKepub(): void
{
$client = static::getClient();
$this->injectFakeFileSystemManager();


$book = $this->findByIdAndKobo(BookFixture::ID, $this->getKoboDevice());
self::assertNotNull($book, 'The book is not linked to the Kobo');
Expand All @@ -23,13 +74,13 @@ public function testDownload(): void

self::assertTrue($downloadHelper->exists($book), 'The book file does not exist');

$client?->request('GET', sprintf('/kobo/%s/v1/download/%s.epub', $this->accessKey, BookFixture::ID));
$client?->request('GET', sprintf('/kobo/%s/v1/download/%s.'.MetadataResponseService::KEPUB_FORMAT, $this->accessKey, BookFixture::ID));

self::assertResponseIsSuccessful();
self::assertResponseHeaderSame('Content-Type', 'application/epub+zip');
self::assertResponseHasHeader('Content-Length');

$expectedDisposition = "attachment; filename=book-1-TheOdysses.epub; filename*=utf-8''TheOdysses.epub";
$expectedDisposition = "attachment; filename=book-1-TheOdysses.kepub; filename*=utf-8''TheOdysses.kepub";
self::assertResponseHeaderSame('Content-Disposition', $expectedDisposition, 'The Content-Disposition header is not as expected');

}
Expand All @@ -38,4 +89,12 @@ private function findByIdAndKobo(int $bookId, KoboDevice $kobo): ?Book
{
return $this->getEntityManager()->getRepository(Book::class)->findByIdAndKoboDevice($bookId, $kobo);
}

private function getKepubifyEnabler(): KepubifyEnabler
{
$service = self::getContainer()->get(KepubifyEnabler::class);
assert($service instanceof KepubifyEnabler);

return $service;
}
}

0 comments on commit 5d5780b

Please sign in to comment.