parse EpisodeAction message from AntennaPod

persist and list with database

create episodeAction list reponse (with mocked timestamp, started and total)

create episodeActions with received values

update existing episodeActions by unique episode link

receive and store subscription changes

deal with multiple subscription changes in single request

split database into subdirectories

only return subscription changes younger then passed parameter since

parse passed timestamp

parse passed timestamp for episode_actions listing

only return list of urls for subscription changes

align list endpoint naming schema

store userId with episode actions and subscriptions

return json object on application root route
This commit is contained in:
thrillfall 2021-06-27 13:19:26 +02:00
commit 72bf365285
31 changed files with 2667 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
vendor/*

23
appinfo/info.xml Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0"?>
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>gpoddersync</id>
<name>G Podder Sync</name>
<summary>replicate GPodder API</summary>
<description><![CDATA[Expose GPodder API to sync podcast consumer apps like AntennaPod]]></description>
<version>0.0.1</version>
<licence>agpl</licence>
<author mail="thrillfall@disroot.org" >Thrillfall</author>
<namespace>GPodderSync</namespace>
<category>integration</category>
<bugs>https://github.com/thrillfall</bugs>
<dependencies>
<nextcloud min-version="15" max-version="15"/>
</dependencies>
<navigations>
<navigation>
<name>G Podder Sync</name>
<route>gpoddersync.page.index</route>
</navigation>
</navigations>
</info>

23
appinfo/routes.php Normal file
View File

@ -0,0 +1,23 @@
<?php
/**
* Create your routes in here. The name is the lowercase name of the controller
* without the controller part, the stuff after the hash is the method.
* e.g. page#index -> OCA\GPodderSync\Controller\PageController->index()
*
* The controller class has to be registered in the application.php file since
* it's instantiated in there
*/
return [
'routes' => [
['name' => 'episode_action#create', 'url' => '/episode_action/create', 'verb' => 'POST'],
['name' => 'episode_action#list', 'url' => '/episode_action', 'verb' => 'GET'],
['name' => 'subscription_change#list', 'url' => '/subscriptions', 'verb' => 'GET'],
['name' => 'subscription_change#create', 'url' => '/subscription_change/create', 'verb' => 'POST'],
['name' => 'version#index', 'url' => '/', 'verb' => 'GET'],
]
];

1561
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Controller;
use DateTime;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use GuzzleHttp\Psr7\Response;
use OCA\GPodderSync\Core\EpisodeAction\EpisodeActionReader;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionEntity;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionRepository;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionWriter;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
class EpisodeActionController extends Controller {
/**
* @var EpisodeActionRepository
*/
private EpisodeActionRepository $episodeActionRepository;
/**
* @var EpisodeActionWriter
*/
private EpisodeActionWriter $episodeActionWriter;
/**
* @var EpisodeActionReader
*/
private EpisodeActionReader $episodeActionReader;
private $userId;
public function __construct(
string $AppName,
IRequest $request,
$UserId,
EpisodeActionRepository $episodeActionRepository,
EpisodeActionWriter $episodeActionWriter,
EpisodeActionReader $episodeActionReader
) {
parent::__construct($AppName, $request);
$this->episodeActionRepository = $episodeActionRepository;
$this->episodeActionWriter = $episodeActionWriter;
$this->episodeActionReader = $episodeActionReader;
$this->userId = $UserId;
}
/**
*
* @NoAdminRequired
* @NoCSRFRequired
*
* @return Response
*/
public function create($data) {
$episodeAction = $this->episodeActionReader->fromString($data);
$episodeActionEntity = new EpisodeActionEntity();
$episodeActionEntity->setPodcast($episodeAction->getPodcast());
$episodeActionEntity->setEpisode($episodeAction->getEpisode());
$episodeActionEntity->setAction($episodeAction->getAction());
$episodeActionEntity->setPosition($episodeAction->getPosition());
$episodeActionEntity->setStarted($episodeAction->getStarted());
$episodeActionEntity->setTotal($episodeAction->getTotal());
$episodeActionEntity->setTimestamp($episodeAction->getTimestamp());
$episodeActionEntity->setUserId($this->userId);
try {
return $this->episodeActionWriter->save($episodeActionEntity);
} catch (UniqueConstraintViolationException $ex) {
$IdEpisodeActionEntityToUpdate = $this->episodeActionRepository->findByEpisode($episodeAction->getEpisode(), $this->userId)->getId();
$episodeActionEntity->setId($IdEpisodeActionEntityToUpdate);
return $this->episodeActionWriter->update($episodeActionEntity);
}
}
/**
*
* @NoAdminRequired
* @NoCSRFRequired
*
* @param int $since
* @return JSONResponse
*/
public function list(int $since): JSONResponse {
$sinceDatetime = $this->createDateTimeFromTimestamp($since);
return new JSONResponse([
"actions" => $this->episodeActionRepository->findAll($sinceDatetime, $this->userId),
"timestamp" => time()
]);
}
/**
* @param int|null $since
*
* @return DateTime
*/
private function createDateTimeFromTimestamp(?int $since): DateTime {
return ($since)
? (new \DateTime)->setTimestamp($since)
: (new \DateTime('-1 week'));
}
}

View File

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Controller;
use DateTime;
use OCA\GPodderSync\Core\SubscriptionChange\SubscriptionChangeSaver;
use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeEntity;
use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeRepository;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
class SubscriptionChangeController extends Controller {
private string $AppName;
/**
* @var SubscriptionChangeSaver
*/
private SubscriptionChangeSaver $subscriptionChangeSaver;
/**
* @var SubscriptionChangeRepository
*/
private SubscriptionChangeRepository $subscriptionChangeRepository;
private $userId;
public function __construct(
string $AppName,
IRequest $request,
$UserId,
SubscriptionChangeSaver $subscriptionChangeSaver,
SubscriptionChangeRepository $subscriptionChangeRepository
) {
parent::__construct($AppName, $request);
$this->subscriptionChangeSaver = $subscriptionChangeSaver;
$this->subscriptionChangeRepository = $subscriptionChangeRepository;
$this->userId = $UserId;
}
/**
*
* @NoAdminRequired
* @NoCSRFRequired
*
* @return void
*/
public function create($add, $remove) {
return $this->subscriptionChangeSaver->saveSubscriptionChanges($add, $remove, $this->userId);
}
/**
*
* @NoAdminRequired
* @NoCSRFRequired
*
* @param int $since
* @return JSONResponse
* @throws \Exception
*/
public function list(int $since = null): JSONResponse {
$sinceDatetime = $this->createDateTimeFromTimestamp($since);
return new JSONResponse([
"add" => $this->extractUrlList($this->subscriptionChangeRepository->findAllSubscribed($sinceDatetime, $this->userId)),
"remove" => $this->extractUrlList($this->subscriptionChangeRepository->findAllUnSubscribed($sinceDatetime, $this->userId)),
"timestamp" => time()
]);
}
/**
* @param int|null $since
*
* @return DateTime
*/
private function createDateTimeFromTimestamp(?int $since): DateTime {
return ($since)
? (new \DateTime)->setTimestamp($since)
: (new \DateTime('-1 week'));
}
/**
* @param array $allSubscribed
*
* @return mixed
*/
private function extractUrlList(array $allSubscribed): array {
return array_map(function (SubscriptionChangeEntity $subscription) {
return $subscription->getUrl();
}, $allSubscribed);
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Controller;
use DateTime;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use GuzzleHttp\Psr7\Response;
use OCA\GPodderSync\Core\EpisodeAction\EpisodeActionReader;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionEntity;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionRepository;
use OCA\GPodderSync\Db\EpisodeAction\EpisodeActionWriter;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
class VersionController extends Controller {
public function __construct(
string $AppName,
IRequest $request,
$UserId
) {
parent::__construct($AppName, $request);
$this->userId = $UserId;
}
/**
*
* @NoAdminRequired
* @NoCSRFRequired
*
* @return Response
*/
public function index() {
return new JSONResponse(["version" => "0.1"]);
}
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Core\EpisodeAction;
class EpisodeAction {
private string $podcast;
private string $episode;
private string $action;
private string $timestamp;
private int $started;
private int $position;
private int $total;
public function __construct(
string $podcast,
string $episode,
string $action,
string $timestamp,
int $started,
int $position,
int $total
) {
$this->podcast = $podcast;
$this->episode = $episode;
$this->action = $action;
$this->timestamp = $timestamp;
$this->started = $started;
$this->position = $position;
$this->total = $total;
}
/**
* @return string
*/
public function getPodcast(): string {
return $this->podcast;
}
/**
* @return string
*/
public function getEpisode(): string {
return $this->episode;
}
/**
* @return string
*/
public function getAction(): string {
return $this->action;
}
/**
* @return string
*/
public function getTimestamp(): string {
return $this->timestamp;
}
/**
* @return int
*/
public function getStarted(): int {
return $this->started;
}
/**
* @return int
*/
public function getPosition(): int {
return $this->position;
}
/**
* @return int
*/
public function getTotal(): int {
return $this->total;
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Core\EpisodeAction;
class EpisodeActionReader {
public function fromString(string $episodeActionString): EpisodeAction {
file_put_contents('actionreader.log', var_export($episodeActionString, true), FILE_APPEND);
preg_match(
'/\[EpisodeAction{(podcast=\')(?<podcast>.*?)(\', episode=\')(?<episode>.*?)(\', action=)(?<action>.*?)(, timestamp=)(?<timestamp>.*?)(, started=)(?<started>.*?)(, position=)(?<position>.*?)(, total=)(?<total>.*?)}]*/',
$episodeActionString,
$matches
);
file_put_contents('actionreader.log', var_export($matches, true), FILE_APPEND);
return new EpisodeAction(
$matches["podcast"],
$matches["episode"],
$matches["action"],
$matches["timestamp"],
(int)$matches["started"],
(int)$matches["position"],
(int)$matches["total"],
);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Core\SubscriptionChange;
class SubscriptionChange {
private string $url;
private bool $isSubscribed;
public function __construct(
string $url,
bool $isSubscribed
) {
$this->url = $url;
$this->isSubscribed = $isSubscribed;
}
/**
* @return bool
*/
public function isSubscribed(): bool {
return $this->isSubscribed;
}
/**
* @return string
*/
public function getUrl(): string {
return $this->url;
}
public function __toString() : String {
return $this->url;
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Core\SubscriptionChange;
class SubscriptionChangeRequestParser {
/**
* @var SubscriptionChangesReader
*/
private SubscriptionChangesReader $subscriptionChangeReader;
public function __construct(SubscriptionChangesReader $subscriptionChangeReader) {
$this->subscriptionChangeReader = $subscriptionChangeReader;
}
/**
* @param string $urlsSubscribed
* @param string $urlsUnsubscribed
*
* @return SubscriptionChange[]
*/
public function createSubscriptionChangeList(string $urlsSubscribed, string $urlsUnsubscribed): array {
$urlsToSubscribe = $this->subscriptionChangeReader->fromString($urlsSubscribed, true);
$urlsToDelete = $this->subscriptionChangeReader->fromString($urlsUnsubscribed, false);
/** @var \OCA\GPodderSync\Core\SubscriptionChange\SubscriptionChange[] $subscriptionChanges */
return array_merge($urlsToSubscribe, $urlsToDelete);
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Core\SubscriptionChange;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeEntity;
use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeRepository;
use OCA\GPodderSync\Db\SubscriptionChange\SubscriptionChangeWriter;
class SubscriptionChangeSaver {
/**
* @var SubscriptionChangesReader
*/
private SubscriptionChangesReader $subscriptionChangeReader;
/**
* @var SubscriptionChangeRepository
*/
private SubscriptionChangeRepository $subscriptionChangeRepository;
/**
* @var SubscriptionChangeWriter
*/
private SubscriptionChangeWriter $subscriptionChangeWriter;
/**
* @var SubscriptionChangeRequestParser
*/
private SubscriptionChangeRequestParser $subscriptionChangeRequestParser;
public function __construct(
SubscriptionChangeRequestParser $subscriptionChangeRequestParser,
SubscriptionChangeRepository $subscriptionChangeRepository,
SubscriptionChangeWriter $subscriptionChangeWriter
) {
$this->subscriptionChangeRepository = $subscriptionChangeRepository;
$this->subscriptionChangeWriter = $subscriptionChangeWriter;
$this->subscriptionChangeRequestParser = $subscriptionChangeRequestParser;
}
public function saveSubscriptionChanges(string $urlsSubscribed, string $urlsUnsubscribed, string $userId) : void {
$subscriptionChanges = $this->subscriptionChangeRequestParser->createSubscriptionChangeList($urlsSubscribed, $urlsUnsubscribed);
foreach ($subscriptionChanges as $urlChangedSubscriptionStatus) {
$subscriptionChangeEntity = new SubscriptionChangeEntity();
$subscriptionChangeEntity->setUrl($urlChangedSubscriptionStatus->getUrl());
$subscriptionChangeEntity->setSubscribed($urlChangedSubscriptionStatus->isSubscribed());
$subscriptionChangeEntity->setUpdated((new \DateTime())->format("Y-m-d\TH:i:s"));
$subscriptionChangeEntity->setUserId($userId);
try {
$this->subscriptionChangeWriter->create($subscriptionChangeEntity);
} catch (UniqueConstraintViolationException $ex) {
$idEpisodeActionEntityToUpdate = $this->subscriptionChangeRepository->findByUrl($subscriptionChangeEntity->getUrl(), $userId)->getId();
$subscriptionChangeEntity->setId($idEpisodeActionEntityToUpdate);
$this->subscriptionChangeWriter->update($subscriptionChangeEntity);
}
}
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Core\SubscriptionChange;
class SubscriptionChangesReader {
/**
* @param string $raw
*
* @return array|SubscriptionChange[]
*/
public function fromString(string $raw, bool $subscribed):? array {
$urls = str_replace(["[", "]", " "], "", $raw);
$urlList = explode(",", $urls);
if ($urlList[0] === "") {
return [];
}
$subscriptionChanges = [];
foreach ($urlList as $url) {
$subscriptionChanges[] = new SubscriptionChange($url, $subscribed);
}
return $subscriptionChanges;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Db\EpisodeAction;
use JsonSerializable;
use OCP\AppFramework\Db\Entity;
class EpisodeActionEntity extends Entity implements JsonSerializable {
protected $podcast;
protected $episode;
protected $action;
protected $position;
protected $started;
protected $total;
protected $timestamp;
protected $userId;
public function __construct() {
$this->addType('id','integer');
}
public function jsonSerialize() {
return [
'id' => $this->id,
'podcast' => $this->podcast,
'episode' => $this->episode,
'action' => $this->action,
'position' => $this->position,
'started' => $this->started,
'total' => $this->total,
'timestamp' => (new \DateTime($this->timestamp))->format("Y-m-d\TH:i:s"),
];
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Db\EpisodeAction;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
class EpisodeActionMapper extends \OCP\AppFramework\Db\QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'gpoddersync_episode_action', EpisodeActionEntity::class);
}
public function findAll(\DateTime $sinceTimestamp, string $userId): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()->gt('timestamp', $qb->createNamedParameter($sinceTimestamp, IQueryBuilder::PARAM_DATE))
)
->andWhere(
$qb->expr()->eq('user_id', $qb->createNamedParameter($userId))
);
return $this->findEntities($qb);
}
public function findByEpisode(string $episode, string $userId) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()->eq('episode', $qb->createNamedParameter($episode))
)
->andWhere(
$qb->expr()->eq('user_id', $qb->createNamedParameter($userId))
);
try {
return $this->findEntity($qb);
} catch (DoesNotExistException $e) {
} catch (MultipleObjectsReturnedException $e) {
}
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Db\EpisodeAction;
class EpisodeActionRepository {
/**
* @var EpisodeActionMapper
*/
private EpisodeActionMapper $episodeActionMapper;
public function __construct(EpisodeActionMapper $episodeActionMapper) {
$this->episodeActionMapper = $episodeActionMapper;
}
public function findAll(\DateTime $sinceTimestamp, string $userId) : array {
return $this->episodeActionMapper->findAll($sinceTimestamp, $userId);
}
public function findByEpisode(string $episode, string $userId): EpisodeActionEntity {
return $this->episodeActionMapper->findByEpisode($episode, $userId);
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Db\EpisodeAction;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
class EpisodeActionWriter {
/**
* @var EpisodeActionMapper
*/
private EpisodeActionMapper $episodeActionMapper;
public function __construct(EpisodeActionMapper $episodeActionMapper) {
$this->episodeActionMapper = $episodeActionMapper;
}
public function save(EpisodeActionEntity $episodeActionEntity): EpisodeActionEntity {
return $this->episodeActionMapper->insert($episodeActionEntity);
}
public function update(EpisodeActionEntity $episodeActionEntity) {
return $this->episodeActionMapper->update($episodeActionEntity);
}
public function purge() {
foreach ($this->episodeActionMapper->findAll() as $entity) {
$this->episodeActionMapper->delete($entity);
}
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Db\SubscriptionChange;
use JsonSerializable;
use OCP\AppFramework\Db\Entity;
class SubscriptionChangeEntity extends Entity implements JsonSerializable {
protected $url;
protected $subscribed;
protected $updated;
protected $userId;
public function __construct() {
$this->addType('id','integer');
$this->addType('subscribed','boolean');
}
public function jsonSerialize() {
return [
'id' => $this->id,
'url' => $this->url,
'subscribed' => $this->subscribed,
'updated' => $this->updated,
];
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Db\SubscriptionChange;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
class SubscriptionChangeMapper extends \OCP\AppFramework\Db\QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'gpoddersync_subscriptions', SubscriptionChangeEntity::class);
}
public function findAll(string $userId): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()->eq('user_id', $qb->createNamedParameter($userId))
);
return $this->findEntities($qb);
}
public function findByUrl(string $url, string $userId): SubscriptionChangeEntity {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()->eq('url', $qb->createNamedParameter($url))
)
->andWhere(
$qb->expr()->eq('user_id', $qb->createNamedParameter($userId))
);
try {
return $this->findEntity($qb);
} catch (DoesNotExistException $e) {
} catch (MultipleObjectsReturnedException $e) {
}
}
public function remove(SubscriptionChangeEntity $subscriptionChangeEntity) {
$this->delete($subscriptionChangeEntity);
}
public function findAllSubscriptionState(bool $subscribed, \DateTime $sinceTimestamp, string $userId) {
$qb = $this->db->getQueryBuilder();
$qb->select('url')
->from($this->getTableName())
->where(
$qb->expr()->eq('subscribed', $qb->createNamedParameter($subscribed, IQueryBuilder::PARAM_BOOL))
)->andWhere(
$qb->expr()->gt('updated', $qb->createNamedParameter($sinceTimestamp, IQueryBuilder::PARAM_DATE))
)
->andWhere(
$qb->expr()->eq('user_id', $qb->createNamedParameter($userId))
);
return $this->findEntities($qb);
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Db\SubscriptionChange;
class SubscriptionChangeRepository {
/**
* @var SubscriptionChangeMapper
*/
private SubscriptionChangeMapper $subscriptionChangeMapper;
public function __construct(SubscriptionChangeMapper $subscriptionChangeMapper) {
$this->subscriptionChangeMapper = $subscriptionChangeMapper;
}
public function findAll() : array {
return $this->subscriptionChangeMapper->findAll();
}
public function findByUrl(string $episode, string $userId): SubscriptionChangeEntity {
return $this->subscriptionChangeMapper->findByUrl($episode, $userId);
}
public function findAllSubscribed(\DateTime $sinceTimestamp, string $userId) {
return $this->subscriptionChangeMapper->findAllSubscriptionState(true, $sinceTimestamp, $userId);
}
public function findAllUnSubscribed(\DateTime $sinceTimestamp, string $userId) {
return $this->subscriptionChangeMapper->findAllSubscriptionState(false, $sinceTimestamp, $userId);
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Db\SubscriptionChange;
class SubscriptionChangeWriter {
/**
* @var SubscriptionChangeMapper
*/
private SubscriptionChangeMapper $subscriptionChangeMapper;
public function __construct(SubscriptionChangeMapper $subscriptionChangeMapper) {
$this->subscriptionChangeMapper = $subscriptionChangeMapper;
}
public function purge() {
foreach ($this->subscriptionChangeMapper->findAll() as $entity) {
$this->subscriptionChangeMapper->delete($entity);
}
}
public function create(SubscriptionChangeEntity $subscriptionChangeEntity): SubscriptionChangeEntity{
return $this->subscriptionChangeMapper->insert($subscriptionChangeEntity);
}
public function update(SubscriptionChangeEntity $subscriptionChangeEntity): SubscriptionChangeEntity{
return $this->subscriptionChangeMapper->update($subscriptionChangeEntity);
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Migration;
use Closure;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
class Version0001Date20210520063113 extends \OCP\Migration\SimpleMigrationStep {
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('gpoddersync_episode_action')) {
$table = $schema->createTable('gpoddersync_episode_action');
$table->addColumn('id', 'integer', [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('podcast', 'string', [
'notnull' => true,
'length' => 500
]);
$table->addColumn('episode', 'string', [
'notnull' => true,
'length' => 500,
'unique' => true,
]);
$table->addColumn('action', 'string', [
'notnull' => true,
'length' => 5
]);
$table->addColumn('position', 'integer', [
'notnull' => true,
]);
$table->addColumn('started', Types::INTEGER, [
'notnull' => true,
]);
$table->addColumn('total', Types::INTEGER, [
'notnull' => true,
]);
$table->addColumn('timestamp', Types::DATETIME_MUTABLE, [
'notnull' => true,
]);
$table->addColumn('user_id', Types::STRING, [
'notnull' => true,
'length' => 200,
]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['episode', 'user_id'], 'gpoddersync_episode_user_id');
}
return $schema;
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Migration;
use Closure;
use Doctrine\DBAL\Types\Types;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
class Version0002Date20210524131313 extends \OCP\Migration\SimpleMigrationStep {
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('gpoddersync_subscriptions')) {
$table = $schema->createTable('gpoddersync_subscriptions');
$table->addColumn('id', 'integer', [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('url', Types::STRING, [
'notnull' => true,
'length' => 500
]);
$table->addColumn('subscribed', Types::BOOLEAN, [
'notnull' => true,
]);
$table->addColumn('updated', Types::DATETIME_MUTABLE, [
'notnull' => true,
]);
$table->addColumn('user_id', Types::STRING, [
'notnull' => true,
'length' => 200,
]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['url', "user_id"], 'subscriptions_url_user');
}
return $schema;
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Migration;
use Closure;
use Doctrine\DBAL\Types\Types;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
class Version0003Date20210524131313 extends \OCP\Migration\SimpleMigrationStep {
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$schema->dropTable('gpoddersync_subscriptions');
$schema->dropTable('gpoddersync_episode_action');
return $schema;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace OCA\GPodderSync\Tests\Integration\Controller;
use OCP\AppFramework\App;
use Test\TestCase;
/**
* This test shows how to make a small Integration Test. Query your class
* directly from the container, only pass in mocks if needed and run your tests
* against the database
*/
class AppTest extends TestCase {
private $container;
public function setUp() {
parent::setUp();
$app = new App('gpoddersync');
$this->container = $app->getContainer();
}
public function testAppInstalled() {
$appManager = $this->container->query('OCP\App\IAppManager');
$this->assertTrue($appManager->isInstalled('gpoddersync'));
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Tests\Unit\Core\EpisodeAction;
use OCA\GPodderSync\Core\EpisodeAction\EpisodeActionReader;
use PHPUnit_Framework_TestCase;
class EpisodeActionReaderTest extends PHPUnit_Framework_TestCase {
public function testCreateFromString(): void {
$reader = new EpisodeActionReader();
$episodeAction = $reader->fromString('[EpisodeAction{podcast=\'https://feeds.simplecast.com/wEl4UUJZ\', episode=\'https://chrt.fm/track/47G541/injector.simplecastaudio.com/f16c3da7-cf46-4a42-99b7-8467255c6086/episodes/e8e24c01-6157-40e8-9b5a-45d539aeb7e6/audio/128/default.mp3?aid=rss_feed&awCollectionId=f16c3da7-cf46-4a42-99b7-8467255c6086&awEpisodeId=e8e24c01-6157-40e8-9b5a-45d539aeb7e6&feed=wEl4UUJZ\', action=PLAY, timestamp=Tue May 18 23:45:11 GMT+02:00 2021, started=31, position=36, total=2474}]');
$this->assertSame("https://feeds.simplecast.com/wEl4UUJZ", $episodeAction->getPodcast());
$this->assertSame("https://chrt.fm/track/47G541/injector.simplecastaudio.com/f16c3da7-cf46-4a42-99b7-8467255c6086/episodes/e8e24c01-6157-40e8-9b5a-45d539aeb7e6/audio/128/default.mp3?aid=rss_feed&awCollectionId=f16c3da7-cf46-4a42-99b7-8467255c6086&awEpisodeId=e8e24c01-6157-40e8-9b5a-45d539aeb7e6&feed=wEl4UUJZ", $episodeAction->getEpisode());
$this->assertSame("PLAY", $episodeAction->getAction());
$this->assertSame("Tue May 18 23:45:11 GMT+02:00 2021", $episodeAction->getTimestamp());
$this->assertSame(31, $episodeAction->getStarted());
$this->assertSame(36, $episodeAction->getPosition());
$this->assertSame(2474, $episodeAction->getTotal());
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Tests\Unit\Core\SubscriptionChange;
use OCA\GPodderSync\Core\SubscriptionChange\SubscriptionChangesReader;
use PHPUnit_Framework_TestCase;
class SubscriptionChangeReaderTest extends PHPUnit_Framework_TestCase {
public function testCreateFromString(): void {
$reader = new SubscriptionChangesReader();
$subscriptionChange = $reader->fromString('[https://feeds.megaphone.fm/HSW8286374095]', true);
$this->assertCount(1, $subscriptionChange);
$this->assertSame("https://feeds.megaphone.fm/HSW8286374095", $subscriptionChange[0]->getUrl());
}
public function testCreateFromEmptyString(): void {
$reader = new SubscriptionChangesReader();
$subscriptionChange = $reader->fromString('[]', true);
$this->assertCount(0, $subscriptionChange);
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace OCA\GPodderSync\Tests\Unit\Core\SubscriptionChange;
use OCA\GPodderSync\Core\SubscriptionChange\SubscriptionChangeRequestParser;
use OCA\GPodderSync\Core\SubscriptionChange\SubscriptionChangesReader;
use PHPUnit_Framework_TestCase;
class SubscriptionChangeRequestParserTest extends PHPUnit_Framework_TestCase {
public function testSubscriptionRequestConvertsToSubscriptionChangeList() {
$subscriptionChangesParser = new SubscriptionChangeRequestParser(
new SubscriptionChangesReader(),
);
$subscriptionChanges = $subscriptionChangesParser->createSubscriptionChangeList('[https://feeds.simplecast.com/54nAGcIl]','[]');
$this->assertCount(1, $subscriptionChanges);
$this->assertSame('https://feeds.simplecast.com/54nAGcIl', $subscriptionChanges[0]->getUrl());
$this->assertTrue($subscriptionChanges[0]->isSubscribed());
}
public function testSubscriptionRequestWithMultipleChangesConvertsToSubscriptionChangeList() {
$subscriptionChangesParser = new SubscriptionChangeRequestParser(
new SubscriptionChangesReader(),
);
$subscriptionChanges = $subscriptionChangesParser->createSubscriptionChangeList(
'[https://podcastfeeds.nbcnews.com/dateline-nbc,https://feeds.megaphone.fm/ADL9840290619]',
'[]');
$this->assertCount(2, $subscriptionChanges);
}
}

19
tests/bootstrap.php Normal file
View File

@ -0,0 +1,19 @@
<?php
if (!defined('PHPUNIT_RUN')) {
define('PHPUNIT_RUN', 1);
}
require_once __DIR__.'/../../../lib/base.php';
// Fix for "Autoload path not allowed: .../tests/lib/testcase.php"
\OC::$loader->addValidRoot(OC::$SERVERROOT . '/tests');
// Fix for "Autoload path not allowed: .../gpoddersync/tests/testcase.php"
\OC_App::loadApp('gpoddersync');
if(!class_exists('PHPUnit_Framework_TestCase')) {
require_once('PHPUnit/Autoload.php');
}
OC_Hook::clear();

7
vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit101c320a2bec76e94ce31673caacea82::getLoader();

1
vendor/bin/phpunit vendored Symbolic link
View File

@ -0,0 +1 @@
../phpunit/phpunit/phpunit