From 9fd3f2070e9dfe5d0f64d09524d53122922e56cd Mon Sep 17 00:00:00 2001 From: Laurent Constantin Date: Sat, 25 May 2024 10:49:49 +0200 Subject: [PATCH] fix(kobo): Support reading state with page number --- src/Controller/KoboStateController.php | 16 ++++ src/DataFixtures/BookFixture.php | 1 + src/Kobo/Request/Bookmark.php | 2 +- src/Kobo/Request/ReadingState.php | 2 +- src/Kobo/Request/ReadingStateLocation.php | 13 +++ src/Kobo/Request/ReadingStateStatistics.php | 10 ++ src/Kobo/Request/ReadingStateStatusInfo.php | 2 +- src/Kobo/Request/ReadingStates.php | 7 ++ .../Controller/AbstractKoboControllerTest.php | 1 + tests/Controller/KoboStateControllerTest.php | 95 ++++++++++++++++++- 10 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 src/Kobo/Request/ReadingStateLocation.php create mode 100644 src/Kobo/Request/ReadingStateStatistics.php diff --git a/src/Controller/KoboStateController.php b/src/Controller/KoboStateController.php index d533d65e..c4cf019b 100644 --- a/src/Controller/KoboStateController.php +++ b/src/Controller/KoboStateController.php @@ -60,13 +60,29 @@ public function state(Kobo $kobo, string $uuid, Request $request): Response|Json if ($interaction === false) { $interaction = new BookInteraction(); $interaction->setBook($book); + $interaction->setReadPages(0); // On a new interaction, we assume the user has read 0 pages + $interaction->setUser($kobo->getUser()); $interactions->add($interaction); $this->em->persist($interaction); } $interaction->setUpdated($state->lastModified); $interaction->setFinished($state->statusInfo?->status === ReadingStateStatusInfo::STATUS_FINISHED); + switch ($state->statusInfo?->status) { + case ReadingStateStatusInfo::STATUS_FINISHED: + $interaction->setReadPages($book->getPageNumber()); + break; + case ReadingStateStatusInfo::STATUS_READING: + $percent = $state->currentBookmark?->progressPercent; + $numPages = $percent !== null && $book->getPageNumber() !== null ? $book->getPageNumber() * $percent / 100 : null; + if ($numPages !== null) { + $interaction->setReadPages((int) $numPages); + } + break; + case null: + break; + } $this->em->flush(); return new StateResponse($book); diff --git a/src/DataFixtures/BookFixture.php b/src/DataFixtures/BookFixture.php index 39ec8687..b24eca59 100644 --- a/src/DataFixtures/BookFixture.php +++ b/src/DataFixtures/BookFixture.php @@ -10,6 +10,7 @@ class BookFixture extends Fixture implements DependentFixtureInterface { public const BOOK_REFERENCE = 'book-odyssey'; + public const BOOK_PAGE_REFERENCE = '7557680347007504212_1727-h-21.htm.xhtml'; public const ID = 1; public const UUID = '54c8fb05-cf05-4cb6-9482-bc25fa49fa80'; diff --git a/src/Kobo/Request/Bookmark.php b/src/Kobo/Request/Bookmark.php index 1a59234b..c2216e26 100644 --- a/src/Kobo/Request/Bookmark.php +++ b/src/Kobo/Request/Bookmark.php @@ -6,7 +6,7 @@ class Bookmark { public ?int $contentSourceProgressPercent = null; public ?\DateTime $lastModified = null; - public mixed $location; + public ?ReadingStateLocation $location = null; public ?int $progressPercent = null; } diff --git a/src/Kobo/Request/ReadingState.php b/src/Kobo/Request/ReadingState.php index 8c721e9c..f9e86949 100644 --- a/src/Kobo/Request/ReadingState.php +++ b/src/Kobo/Request/ReadingState.php @@ -7,6 +7,6 @@ class ReadingState public ?Bookmark $currentBookmark = null; public ?string $entitlementId = null; public ?\DateTimeImmutable $lastModified = null; - public mixed $statistics = null; + public ?ReadingStateStatistics $statistics = null; public ?ReadingStateStatusInfo $statusInfo = null; } diff --git a/src/Kobo/Request/ReadingStateLocation.php b/src/Kobo/Request/ReadingStateLocation.php new file mode 100644 index 00000000..096788bc --- /dev/null +++ b/src/Kobo/Request/ReadingStateLocation.php @@ -0,0 +1,13 @@ + $readingState + */ + public function __construct(array $readingState = []) + { + $this->readingStates = $readingState; + } /** @var array */ public array $readingStates = []; } diff --git a/tests/Controller/AbstractKoboControllerTest.php b/tests/Controller/AbstractKoboControllerTest.php index ffb6fee6..249a8603 100644 --- a/tests/Controller/AbstractKoboControllerTest.php +++ b/tests/Controller/AbstractKoboControllerTest.php @@ -30,6 +30,7 @@ protected function getEntityManager(): EntityManagerInterface{ protected function setUp(): void { + parent::setUp(); self::createClient(); $this->kobo = $this->loadKobo(); diff --git a/tests/Controller/KoboStateControllerTest.php b/tests/Controller/KoboStateControllerTest.php index a7b60cbb..09d8369d 100644 --- a/tests/Controller/KoboStateControllerTest.php +++ b/tests/Controller/KoboStateControllerTest.php @@ -3,12 +3,20 @@ namespace App\Tests\Controller; use App\DataFixtures\BookFixture; -use App\DataFixtures\ShelfFixture; use App\Entity\Book; -use App\Entity\Shelf; -use Doctrine\ORM\EntityManager; +use App\Entity\BookInteraction; +use App\Kobo\Request\Bookmark; +use App\Kobo\Request\ReadingState; +use App\Kobo\Request\ReadingStateLocation; +use App\Kobo\Request\ReadingStates; +use App\Kobo\Request\ReadingStateStatistics; +use App\Kobo\Request\ReadingStateStatusInfo; +use Symfony\Component\Serializer\SerializerInterface; -class KoboStateControllerTest extends AbstractKoboControllerTest +/** + * @phpstan-type ReadingStateCriteria array{'book':int, 'readPages': int, 'finished': boolean} + */ +class KoboStateControllerTest extends AbstractKoboControllerTest { public function testOpen() : void { @@ -24,6 +32,28 @@ public function testOpen() : void self::assertResponseHeaderSame('Connection', 'keep-alive'); } + /** + * @dataProvider readingStatesProvider + * @param ReadingStateCriteria $criteria + */ + public function testPutState(int $bookId, ReadingStates $readingStates, array $criteria) : void + { + $client = static::getClient(); + $serializer = $this->getSerializer(); + + $book = $this->getBookById($bookId); + self::assertNotNull($book, 'Book '.$bookId.' not found'); + self::assertNotNull($book->getUuid(), 'Book '.$bookId.' has no UUID'); + + $json = $serializer->serialize($readingStates, 'json'); + $client?->request('PUT', sprintf('/kobo/%s/v1/library/%s/state', $this->accessKey, $book->getUuid()), [],[],[] , $json); + + self::assertResponseIsSuccessful(); + + $interaction = $this->getEntityManager()->getRepository(BookInteraction::class)->findOneBy($criteria); + self::assertNotNull($interaction, 'No Interaction found matching your criteria'); + } + protected function getBookById(int $id): ?Book { $book = $this->getEntityManager()->getRepository(Book::class)->findOneBy(['id' => $id]); @@ -32,5 +62,62 @@ protected function getBookById(int $id): ?Book return $book; } + private function getSerializer(): SerializerInterface + { + $service = self::getContainer()->get('serializer'); + assert($service instanceof SerializerInterface); + + return $service; + } + + private function getReadingStates(string $bookUuid, int $percent = 50): ReadingStates + { + assert($percent >= 0 && $percent <= 100, 'Percent must be between 0 and 100'); + + $state = new ReadingState(); + $state->lastModified = new \DateTimeImmutable(); + $state->currentBookmark = new Bookmark(); + $state->currentBookmark->contentSourceProgressPercent = $state->currentBookmark->progressPercent = $percent; + $state->currentBookmark->location = new ReadingStateLocation(); + $state->currentBookmark->location->source = BookFixture::BOOK_PAGE_REFERENCE; + $state->currentBookmark->lastModified = new \DateTime(); + $state->entitlementId = $bookUuid; + $state->statusInfo = new ReadingStateStatusInfo(); + $state->statusInfo->status = $percent === 100 ? ReadingStateStatusInfo::STATUS_FINISHED : ReadingStateStatusInfo::STATUS_READING; + $state->statusInfo->lastModified = $state->lastModified; + $state->statistics = new ReadingStateStatistics(); + $state->statistics->remainingTimeMinutes = 100 * ($percent/100); + $state->statistics->spentReadingMinutes = 100 - $state->statistics->remainingTimeMinutes; + $state->statistics->lastModified = $state->lastModified; + + return new ReadingStates([$state]); + } + + /** + * @return array + */ + public function readingStatesProvider(): array + { + return [ + [ + BookFixture::ID, + $this->getReadingStates(BookFixture::UUID, 50), + [ + 'book' => BookFixture::ID, + 'readPages' => 15, + 'finished' => false, + ] + ], + [ + BookFixture::ID, + $this->getReadingStates(BookFixture::UUID, 100), + [ + 'book' => BookFixture::ID, + 'readPages' => 30, + 'finished' => true, + ] + ], + ]; + } } \ No newline at end of file