From ac1acf079b2581962462dce50cf676b8ab476160 Mon Sep 17 00:00:00 2001 From: thrillfall Date: Tue, 5 Oct 2021 20:45:06 +0200 Subject: [PATCH] provide episode_action timestamps as UTC in api response --- lib/Core/EpisodeAction/EpisodeAction.php | 13 ++++- .../EpisodeAction/EpisodeActionReader.php | 1 + lib/Core/EpisodeAction/EpisodeActionSaver.php | 22 ++++---- lib/Db/EpisodeAction/EpisodeActionEntity.php | 8 ++- lib/Db/EpisodeAction/EpisodeActionMapper.php | 7 ++- .../EpisodeAction/EpisodeActionRepository.php | 44 ++++++++++++++- .../Version0005Date20211004110900.php | 1 + .../EpisodeActionRepositoryTest.php | 56 +++++++++++++++++++ .../Migration/TimestampMigrationTest.php | 15 ++--- 9 files changed, 142 insertions(+), 25 deletions(-) create mode 100644 tests/Integration/EpisodeActionRepositoryTest.php diff --git a/lib/Core/EpisodeAction/EpisodeAction.php b/lib/Core/EpisodeAction/EpisodeAction.php index 19ef12a..40ce336 100644 --- a/lib/Core/EpisodeAction/EpisodeAction.php +++ b/lib/Core/EpisodeAction/EpisodeAction.php @@ -12,6 +12,7 @@ class EpisodeAction { private int $position; private int $total; private ?string $guid; + private ?int $id; public function __construct( string $podcast, @@ -21,7 +22,8 @@ class EpisodeAction { int $started, int $position, int $total, - ?string $guid + ?string $guid, + ?int $id ) { $this->podcast = $podcast; $this->episode = $episode; @@ -31,6 +33,7 @@ class EpisodeAction { $this->position = $position; $this->total = $total; $this->guid = $guid; + $this->id = $id; } /** @@ -88,5 +91,13 @@ class EpisodeAction { return $this->guid; } + /** + * @return int + */ + public function getId(): int + { + return $this->id; + } + } diff --git a/lib/Core/EpisodeAction/EpisodeActionReader.php b/lib/Core/EpisodeAction/EpisodeActionReader.php index b006430..eb2ae18 100644 --- a/lib/Core/EpisodeAction/EpisodeActionReader.php +++ b/lib/Core/EpisodeAction/EpisodeActionReader.php @@ -44,6 +44,7 @@ class EpisodeActionReader (int)$matches["position"], (int)$matches["total"], $matches["guid"] ?? null, + null, ); break; } diff --git a/lib/Core/EpisodeAction/EpisodeActionSaver.php b/lib/Core/EpisodeAction/EpisodeActionSaver.php index 5d1a95a..a796b2d 100644 --- a/lib/Core/EpisodeAction/EpisodeActionSaver.php +++ b/lib/Core/EpisodeAction/EpisodeActionSaver.php @@ -55,11 +55,11 @@ class EpisodeActionSaver return $episodeActionEntities; } - private function convertTimestampTo(string $timestamp): string + private function convertTimestampToUnixEpoch(string $timestamp): string { return \DateTime::createFromFormat('D F d H:i:s T Y', $timestamp) ->setTimezone(new DateTimeZone('UTC')) - ->format("Y-m-d\TH:i:s"); + ->format("U"); } private function updateEpisodeAction( @@ -68,23 +68,23 @@ class EpisodeActionSaver ): EpisodeActionEntity { $identifier = $episodeActionEntity->getGuid() ?? $episodeActionEntity->getEpisode(); - $episodeActionEntityToUpdate = $this->episodeActionRepository->findByEpisodeIdentifier( + $episodeActionToUpdate = $this->episodeActionRepository->findByEpisodeIdentifier( $identifier, $userId ); - if ($episodeActionEntityToUpdate === null && $episodeActionEntity->getGuid() !== null) { - $episodeActionEntityToUpdate = $this->getOldEpisodeActionByEpisodeUrl($episodeActionEntity->getEpisode(), $userId); + if ($episodeActionToUpdate === null && $episodeActionEntity->getGuid() !== null) { + $episodeActionToUpdate = $this->getOldEpisodeActionByEpisodeUrl($episodeActionEntity->getEpisode(), $userId); } - $episodeActionEntity->setId($episodeActionEntityToUpdate->getId()); + $episodeActionEntity->setId($episodeActionToUpdate->getId()); - $this->ensureGuidDoesNotGetNulledWithOldData($episodeActionEntityToUpdate, $episodeActionEntity); + $this->ensureGuidDoesNotGetNulledWithOldData($episodeActionToUpdate, $episodeActionEntity); return $this->episodeActionWriter->update($episodeActionEntity); } - private function getOldEpisodeActionByEpisodeUrl(string $episodeUrl, string $userId): ?EpisodeActionEntity + private function getOldEpisodeActionByEpisodeUrl(string $episodeUrl, string $userId): ?EpisodeAction { return $this->episodeActionRepository->findByEpisodeIdentifier( $episodeUrl, @@ -92,9 +92,9 @@ class EpisodeActionSaver ); } - private function ensureGuidDoesNotGetNulledWithOldData(EpisodeActionEntity $episodeActionEntityToUpdate, EpisodeActionEntity $episodeActionEntity): void + private function ensureGuidDoesNotGetNulledWithOldData(EpisodeAction $episodeActionToUpdate, EpisodeActionEntity $episodeActionEntity): void { - $existingGuid = $episodeActionEntityToUpdate->getGuid(); + $existingGuid = $episodeActionToUpdate->getGuid(); if ($existingGuid !== null && $episodeActionEntity->getGuid() == null) { $episodeActionEntity->setGuid($existingGuid); } @@ -110,7 +110,7 @@ class EpisodeActionSaver $episodeActionEntity->setPosition($episodeAction->getPosition()); $episodeActionEntity->setStarted($episodeAction->getStarted()); $episodeActionEntity->setTotal($episodeAction->getTotal()); - $episodeActionEntity->setTimestamp($this->convertTimestampTo($episodeAction->getTimestamp())); + $episodeActionEntity->setTimestampEpoch($this->convertTimestampToUnixEpoch($episodeAction->getTimestamp())); $episodeActionEntity->setUserId($userId); return $episodeActionEntity; diff --git a/lib/Db/EpisodeAction/EpisodeActionEntity.php b/lib/Db/EpisodeAction/EpisodeActionEntity.php index 7bee97f..da35ffd 100644 --- a/lib/Db/EpisodeAction/EpisodeActionEntity.php +++ b/lib/Db/EpisodeAction/EpisodeActionEntity.php @@ -33,8 +33,12 @@ class EpisodeActionEntity extends Entity implements JsonSerializable { 'position' => $this->position, 'started' => $this->started, 'total' => $this->total, - 'timestamp' => (new \DateTime($this->timestamp))->format("Y-m-d\TH:i:s"), - 'timestamp_epoch' => $this->timestampEpoch, + 'timestamp' => $this->timestampEpoch, ]; } + + public function getTimestampEpoch() : int + { + return (int) $this->timestampEpoch; + } } diff --git a/lib/Db/EpisodeAction/EpisodeActionMapper.php b/lib/Db/EpisodeAction/EpisodeActionMapper.php index cda8a15..26d85c0 100644 --- a/lib/Db/EpisodeAction/EpisodeActionMapper.php +++ b/lib/Db/EpisodeAction/EpisodeActionMapper.php @@ -3,10 +3,12 @@ declare(strict_types=1); namespace OCA\GPodderSync\Db\EpisodeAction; +use OCA\GPodderSync\Core\EpisodeAction\EpisodeAction; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +use Safe\DateTime; class EpisodeActionMapper extends \OCP\AppFramework\Db\QBMapper { @@ -30,6 +32,7 @@ class EpisodeActionMapper extends \OCP\AppFramework\Db\QBMapper ); return $this->findEntities($qb); + } public function findByEpisodeIdentifier(string $episodeIdentifier, string $userId) : ?EpisodeActionEntity @@ -48,7 +51,7 @@ class EpisodeActionMapper extends \OCP\AppFramework\Db\QBMapper ); try { - /** @var EpisodeActionEntity $episodeActionEntity*/ + /** @var EpisodeActionEntity $episodeActionEntity */ $episodeActionEntity = $this->findEntity($qb); return $episodeActionEntity; @@ -58,4 +61,6 @@ class EpisodeActionMapper extends \OCP\AppFramework\Db\QBMapper return null; } + + } diff --git a/lib/Db/EpisodeAction/EpisodeActionRepository.php b/lib/Db/EpisodeAction/EpisodeActionRepository.php index 797f3f9..cea3e1c 100644 --- a/lib/Db/EpisodeAction/EpisodeActionRepository.php +++ b/lib/Db/EpisodeAction/EpisodeActionRepository.php @@ -3,6 +3,8 @@ declare(strict_types=1); namespace OCA\GPodderSync\Db\EpisodeAction; +use OCA\GPodderSync\Core\EpisodeAction\EpisodeAction; + class EpisodeActionRepository { /** * @var EpisodeActionMapper @@ -14,11 +16,47 @@ class EpisodeActionRepository { } public function findAll(\DateTime $sinceTimestamp, string $userId) : array { - return $this->episodeActionMapper->findAll($sinceTimestamp, $userId); + $episodeActions = []; + foreach ($this->episodeActionMapper->findAll($sinceTimestamp, $userId) as $entity) { + $episodeActions[] = $this->mapEntityToEpisodeAction($entity); + } + return $episodeActions; } - public function findByEpisodeIdentifier(string $identifier, string $userId): ?EpisodeActionEntity { - return $this->episodeActionMapper->findByEpisodeIdentifier($identifier, $userId); + public function findByEpisodeIdentifier(string $identifier, string $userId): ?EpisodeAction { + $episodeActionEntity = $this->episodeActionMapper->findByEpisodeIdentifier($identifier, $userId); + + if ($episodeActionEntity === null) { + return null; + } + + return $this->mapEntityToEpisodeAction( + $episodeActionEntity + ); + } + + /** + * @param EpisodeActionEntity $episodeActionEntity + * @return EpisodeAction + * @throws \Safe\Exceptions\DatetimeException + * + */ + private function mapEntityToEpisodeAction(EpisodeActionEntity $episodeActionEntity): EpisodeAction + { + return new EpisodeAction( + $episodeActionEntity->getPodcast(), + $episodeActionEntity->getEpisode(), + $episodeActionEntity->getAction(), + \DateTime::createFromFormat( + "U", + (string)$episodeActionEntity->getTimestampEpoch()) + ->format("Y-m-d\TH:i:s"), + $episodeActionEntity->getStarted(), + $episodeActionEntity->getPosition(), + $episodeActionEntity->getTotal(), + $episodeActionEntity->getGuid(), + $episodeActionEntity->getId(), + ); } } diff --git a/lib/Migration/Version0005Date20211004110900.php b/lib/Migration/Version0005Date20211004110900.php index cc04bd5..09f4f4a 100644 --- a/lib/Migration/Version0005Date20211004110900.php +++ b/lib/Migration/Version0005Date20211004110900.php @@ -15,6 +15,7 @@ class Version0005Date20211004110900 extends SimpleMigrationStep { $schema = $schemaClosure(); $table = $schema->getTable('gpodder_episode_action'); + $table->changeColumn('timestamp', ['notnull' => false]); $table->addColumn('timestamp_epoch', Types::INTEGER, [ 'notnull' => false, 'default' => 0, diff --git a/tests/Integration/EpisodeActionRepositoryTest.php b/tests/Integration/EpisodeActionRepositoryTest.php new file mode 100644 index 0000000..f6f4fd8 --- /dev/null +++ b/tests/Integration/EpisodeActionRepositoryTest.php @@ -0,0 +1,56 @@ +container = $app->getContainer(); + } + + public function testTimestampOutputIsUTCHumandReadable() : void + { + /** @var EpisodeActionSaver $episodeActionSaver */ + $episodeActionSaver = $this->container->get(EpisodeActionSaver::class); + + $episodeUrl = uniqid("test_https://dts.podtrac.com/"); + + $timestampHumanReadable = "2021-08-22T23:58:56"; + $guid = uniqid("test_gid://art19-episode-locator/V0/Ktd"); + + $savedEpisodeActionEntity = $episodeActionSaver->saveEpisodeActions( + "[EpisodeAction{podcast='https://rss.art19.com/dr-death-s3-miracle-man', episode='{$episodeUrl}', guid='{$guid}', action=PLAY, timestamp=Mon Aug 23 01:58:56 GMT+02:00 2021, started=47, position=54, total=2252}]", + self::USER_ID_0 + )[0]; + + self::assertSame(1629676736, $savedEpisodeActionEntity->getTimestampEpoch()); + + $timestampOutputFormatted = + (\DateTime::createFromFormat("U", (string)$savedEpisodeActionEntity->getTimestampEpoch())) + ->setTimezone(new \DateTimeZone('UTC')) + ->format('Y-m-d\TH:i:s'); + self::assertSame( + $timestampHumanReadable, + $timestampOutputFormatted + ); + + /** @var $episodeActionRepository EpisodeActionRepository */ + $episodeActionRepository = $this->container->get(EpisodeActionRepository::class); + + $retrievedEpisodeActionEntity = $episodeActionRepository->findByEpisodeIdentifier($guid, self::USER_ID_0); + self::assertSame('2021-08-22T23:58:56', $retrievedEpisodeActionEntity->getTimestamp()); + + } +} diff --git a/tests/Integration/Migration/TimestampMigrationTest.php b/tests/Integration/Migration/TimestampMigrationTest.php index b7e736e..76276b9 100644 --- a/tests/Integration/Migration/TimestampMigrationTest.php +++ b/tests/Integration/Migration/TimestampMigrationTest.php @@ -7,6 +7,7 @@ use OC\AllConfig; use OC\Log; use OC\Migration\SimpleOutput; use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionEntity; +use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionMapper; use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionRepository; use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionWriter; use OCA\GPodderSync\Migration\TimestampMigration; @@ -28,7 +29,7 @@ class TimestampMigrationTest extends TestCase const TEST_GUID_1234 = "test_uuid_1234"; const ADMIN = "admin"; private EpisodeActionWriter $episodeActionWriter; - private EpisodeActionRepository $episodeActionRepository; + private EpisodeActionMapper $episodeActionMapper; private IDBConnection $dbConnection; private IConfig $migrationConfig; @@ -38,7 +39,7 @@ class TimestampMigrationTest extends TestCase $app = new App('gpoddersync'); $this->container = $app->getContainer(); $this->episodeActionWriter = $this->container->get(EpisodeActionWriter::class); - $this->episodeActionRepository = $this->container->get(EpisodeActionRepository::class); + $this->episodeActionMapper = $this->container->get(EpisodeActionMapper::class); $this->dbConnection = $this->container->get(IDBConnection::class); $this->migrationConfig = $this->container->get(AllConfig::class ); } @@ -66,19 +67,19 @@ class TimestampMigrationTest extends TestCase $episodeActionEntity->setGuid($guid); $this->episodeActionWriter->save($episodeActionEntity); - $episodeActionEntityBeforeConversion = $this->episodeActionRepository->findByEpisodeIdentifier($guid, self::ADMIN); + $episodeActionBeforeConversion = $this->episodeActionMapper->findByEpisodeIdentifier($guid, self::ADMIN); $this->assertEquals( 0, - $episodeActionEntityBeforeConversion->getTimestampEpoch() + $episodeActionBeforeConversion->getTimestampEpoch() ); $timestampMigration = new TimestampMigration($this->dbConnection, $this->migrationConfig); $timestampMigration->run(new SimpleOutput(new Log(new TestWriter()), "gpoddersync")); - $episodeActionEntityAfterConversion = $this->episodeActionRepository->findByEpisodeIdentifier($guid, self::ADMIN); + $episodeActionAfterConversion = $this->episodeActionMapper->findByEpisodeIdentifier($guid, self::ADMIN); $this->assertSame( - (int)(new \DateTime($episodeActionEntity->getTimestamp()))->format("U"), - $episodeActionEntityAfterConversion->getTimestampEpoch() + 1629676736, + $episodeActionAfterConversion->getTimestampEpoch() ); }