provide episode_action timestamps as UTC in api response

This commit is contained in:
thrillfall 2021-10-05 20:45:06 +02:00
parent aa024e55f8
commit ac1acf079b
9 changed files with 142 additions and 25 deletions

View File

@ -12,6 +12,7 @@ class EpisodeAction {
private int $position; private int $position;
private int $total; private int $total;
private ?string $guid; private ?string $guid;
private ?int $id;
public function __construct( public function __construct(
string $podcast, string $podcast,
@ -21,7 +22,8 @@ class EpisodeAction {
int $started, int $started,
int $position, int $position,
int $total, int $total,
?string $guid ?string $guid,
?int $id
) { ) {
$this->podcast = $podcast; $this->podcast = $podcast;
$this->episode = $episode; $this->episode = $episode;
@ -31,6 +33,7 @@ class EpisodeAction {
$this->position = $position; $this->position = $position;
$this->total = $total; $this->total = $total;
$this->guid = $guid; $this->guid = $guid;
$this->id = $id;
} }
/** /**
@ -88,5 +91,13 @@ class EpisodeAction {
return $this->guid; return $this->guid;
} }
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
} }

View File

@ -44,6 +44,7 @@ class EpisodeActionReader
(int)$matches["position"], (int)$matches["position"],
(int)$matches["total"], (int)$matches["total"],
$matches["guid"] ?? null, $matches["guid"] ?? null,
null,
); );
break; break;
} }

View File

@ -55,11 +55,11 @@ class EpisodeActionSaver
return $episodeActionEntities; 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) return \DateTime::createFromFormat('D F d H:i:s T Y', $timestamp)
->setTimezone(new DateTimeZone('UTC')) ->setTimezone(new DateTimeZone('UTC'))
->format("Y-m-d\TH:i:s"); ->format("U");
} }
private function updateEpisodeAction( private function updateEpisodeAction(
@ -68,23 +68,23 @@ class EpisodeActionSaver
): EpisodeActionEntity ): EpisodeActionEntity
{ {
$identifier = $episodeActionEntity->getGuid() ?? $episodeActionEntity->getEpisode(); $identifier = $episodeActionEntity->getGuid() ?? $episodeActionEntity->getEpisode();
$episodeActionEntityToUpdate = $this->episodeActionRepository->findByEpisodeIdentifier( $episodeActionToUpdate = $this->episodeActionRepository->findByEpisodeIdentifier(
$identifier, $identifier,
$userId $userId
); );
if ($episodeActionEntityToUpdate === null && $episodeActionEntity->getGuid() !== null) { if ($episodeActionToUpdate === null && $episodeActionEntity->getGuid() !== null) {
$episodeActionEntityToUpdate = $this->getOldEpisodeActionByEpisodeUrl($episodeActionEntity->getEpisode(), $userId); $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); 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( return $this->episodeActionRepository->findByEpisodeIdentifier(
$episodeUrl, $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) { if ($existingGuid !== null && $episodeActionEntity->getGuid() == null) {
$episodeActionEntity->setGuid($existingGuid); $episodeActionEntity->setGuid($existingGuid);
} }
@ -110,7 +110,7 @@ class EpisodeActionSaver
$episodeActionEntity->setPosition($episodeAction->getPosition()); $episodeActionEntity->setPosition($episodeAction->getPosition());
$episodeActionEntity->setStarted($episodeAction->getStarted()); $episodeActionEntity->setStarted($episodeAction->getStarted());
$episodeActionEntity->setTotal($episodeAction->getTotal()); $episodeActionEntity->setTotal($episodeAction->getTotal());
$episodeActionEntity->setTimestamp($this->convertTimestampTo($episodeAction->getTimestamp())); $episodeActionEntity->setTimestampEpoch($this->convertTimestampToUnixEpoch($episodeAction->getTimestamp()));
$episodeActionEntity->setUserId($userId); $episodeActionEntity->setUserId($userId);
return $episodeActionEntity; return $episodeActionEntity;

View File

@ -33,8 +33,12 @@ class EpisodeActionEntity extends Entity implements JsonSerializable {
'position' => $this->position, 'position' => $this->position,
'started' => $this->started, 'started' => $this->started,
'total' => $this->total, 'total' => $this->total,
'timestamp' => (new \DateTime($this->timestamp))->format("Y-m-d\TH:i:s"), 'timestamp' => $this->timestampEpoch,
'timestamp_epoch' => $this->timestampEpoch,
]; ];
} }
public function getTimestampEpoch() : int
{
return (int) $this->timestampEpoch;
}
} }

View File

@ -3,10 +3,12 @@ declare(strict_types=1);
namespace OCA\GPodderSync\Db\EpisodeAction; namespace OCA\GPodderSync\Db\EpisodeAction;
use OCA\GPodderSync\Core\EpisodeAction\EpisodeAction;
use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection; use OCP\IDBConnection;
use Safe\DateTime;
class EpisodeActionMapper extends \OCP\AppFramework\Db\QBMapper class EpisodeActionMapper extends \OCP\AppFramework\Db\QBMapper
{ {
@ -30,6 +32,7 @@ class EpisodeActionMapper extends \OCP\AppFramework\Db\QBMapper
); );
return $this->findEntities($qb); return $this->findEntities($qb);
} }
public function findByEpisodeIdentifier(string $episodeIdentifier, string $userId) : ?EpisodeActionEntity public function findByEpisodeIdentifier(string $episodeIdentifier, string $userId) : ?EpisodeActionEntity
@ -48,7 +51,7 @@ class EpisodeActionMapper extends \OCP\AppFramework\Db\QBMapper
); );
try { try {
/** @var EpisodeActionEntity $episodeActionEntity*/ /** @var EpisodeActionEntity $episodeActionEntity */
$episodeActionEntity = $this->findEntity($qb); $episodeActionEntity = $this->findEntity($qb);
return $episodeActionEntity; return $episodeActionEntity;
@ -58,4 +61,6 @@ class EpisodeActionMapper extends \OCP\AppFramework\Db\QBMapper
return null; return null;
} }
} }

View File

@ -3,6 +3,8 @@ declare(strict_types=1);
namespace OCA\GPodderSync\Db\EpisodeAction; namespace OCA\GPodderSync\Db\EpisodeAction;
use OCA\GPodderSync\Core\EpisodeAction\EpisodeAction;
class EpisodeActionRepository { class EpisodeActionRepository {
/** /**
* @var EpisodeActionMapper * @var EpisodeActionMapper
@ -14,11 +16,47 @@ class EpisodeActionRepository {
} }
public function findAll(\DateTime $sinceTimestamp, string $userId) : array { 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 { public function findByEpisodeIdentifier(string $identifier, string $userId): ?EpisodeAction {
return $this->episodeActionMapper->findByEpisodeIdentifier($identifier, $userId); $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(),
);
} }
} }

View File

@ -15,6 +15,7 @@ class Version0005Date20211004110900 extends SimpleMigrationStep {
$schema = $schemaClosure(); $schema = $schemaClosure();
$table = $schema->getTable('gpodder_episode_action'); $table = $schema->getTable('gpodder_episode_action');
$table->changeColumn('timestamp', ['notnull' => false]);
$table->addColumn('timestamp_epoch', Types::INTEGER, [ $table->addColumn('timestamp_epoch', Types::INTEGER, [
'notnull' => false, 'notnull' => false,
'default' => 0, 'default' => 0,

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace tests\Integration;
use OCA\GPodderSync\Core\EpisodeAction\EpisodeActionSaver;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionRepository;
use OCP\AppFramework\App;
use OCP\AppFramework\IAppContainer;
class EpisodeActionRepositoryTest extends \Test\TestCase
{
private const USER_ID_0 = "testuser0";
private IAppContainer $container;
public function setUp(): void {
parent::setUp();
$app = new App('gpoddersync');
$this->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());
}
}

View File

@ -7,6 +7,7 @@ use OC\AllConfig;
use OC\Log; use OC\Log;
use OC\Migration\SimpleOutput; use OC\Migration\SimpleOutput;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionEntity; use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionEntity;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionMapper;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionRepository; use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionRepository;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionWriter; use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionWriter;
use OCA\GPodderSync\Migration\TimestampMigration; use OCA\GPodderSync\Migration\TimestampMigration;
@ -28,7 +29,7 @@ class TimestampMigrationTest extends TestCase
const TEST_GUID_1234 = "test_uuid_1234"; const TEST_GUID_1234 = "test_uuid_1234";
const ADMIN = "admin"; const ADMIN = "admin";
private EpisodeActionWriter $episodeActionWriter; private EpisodeActionWriter $episodeActionWriter;
private EpisodeActionRepository $episodeActionRepository; private EpisodeActionMapper $episodeActionMapper;
private IDBConnection $dbConnection; private IDBConnection $dbConnection;
private IConfig $migrationConfig; private IConfig $migrationConfig;
@ -38,7 +39,7 @@ class TimestampMigrationTest extends TestCase
$app = new App('gpoddersync'); $app = new App('gpoddersync');
$this->container = $app->getContainer(); $this->container = $app->getContainer();
$this->episodeActionWriter = $this->container->get(EpisodeActionWriter::class); $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->dbConnection = $this->container->get(IDBConnection::class);
$this->migrationConfig = $this->container->get(AllConfig::class ); $this->migrationConfig = $this->container->get(AllConfig::class );
} }
@ -66,19 +67,19 @@ class TimestampMigrationTest extends TestCase
$episodeActionEntity->setGuid($guid); $episodeActionEntity->setGuid($guid);
$this->episodeActionWriter->save($episodeActionEntity); $this->episodeActionWriter->save($episodeActionEntity);
$episodeActionEntityBeforeConversion = $this->episodeActionRepository->findByEpisodeIdentifier($guid, self::ADMIN); $episodeActionBeforeConversion = $this->episodeActionMapper->findByEpisodeIdentifier($guid, self::ADMIN);
$this->assertEquals( $this->assertEquals(
0, 0,
$episodeActionEntityBeforeConversion->getTimestampEpoch() $episodeActionBeforeConversion->getTimestampEpoch()
); );
$timestampMigration = new TimestampMigration($this->dbConnection, $this->migrationConfig); $timestampMigration = new TimestampMigration($this->dbConnection, $this->migrationConfig);
$timestampMigration->run(new SimpleOutput(new Log(new TestWriter()), "gpoddersync")); $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( $this->assertSame(
(int)(new \DateTime($episodeActionEntity->getTimestamp()))->format("U"), 1629676736,
$episodeActionEntityAfterConversion->getTimestampEpoch() $episodeActionAfterConversion->getTimestampEpoch()
); );
} }