Merged in feature_upgrade_and_rss (pull request #36)

Implementing RSS feed draft.
This commit is contained in:
Alexey Eschenko 2020-04-04 00:02:13 +00:00
commit 234f16b312
8 changed files with 233 additions and 6 deletions

View file

@ -19,6 +19,7 @@
"excelwebzone/recaptcha-bundle": "^1.5",
"sensio/framework-extra-bundle": "^5.1",
"sentry/sentry-symfony": "^3.4",
"suin/php-rss-writer": "^1.6",
"symfony/console": "^4.1",
"symfony/dotenv": "^4.1",
"symfony/expression-language": "^4.1",

51
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9c91e35bbd8ba2b05ea2a98ac74d5b97",
"content-hash": "4d9cea51c5990806109db1a193618598",
"packages": [
{
"name": "clue/stream-filter",
@ -3440,6 +3440,55 @@
],
"time": "2020-03-16T09:07:07+00:00"
},
{
"name": "suin/php-rss-writer",
"version": "1.6.0",
"source": {
"type": "git",
"url": "https://github.com/suin/php-rss-writer.git",
"reference": "78f45e44a2a7cb0d82e4b9efb6f7b7a075b9051c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/suin/php-rss-writer/zipball/78f45e44a2a7cb0d82e4b9efb6f7b7a075b9051c",
"reference": "78f45e44a2a7cb0d82e4b9efb6f7b7a075b9051c",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"eher/phpunit": ">=1.6",
"mockery/mockery": ">=0.7.2",
"suin/xoopsunit": ">=1.2"
},
"type": "library",
"autoload": {
"psr-0": {
"Suin\\RSSWriter": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "suin",
"email": "suinyeze@gmail.com"
}
],
"description": "Yet another simple RSS writer library for PHP 5.4 or later.",
"homepage": "https://github.com/suin/php-rss-writer",
"keywords": [
"feed",
"generator",
"php",
"rss",
"writer"
],
"time": "2017-07-13T10:47:50+00:00"
},
{
"name": "symfony/cache",
"version": "v5.0.7",

View file

@ -89,4 +89,13 @@ api_v1_torrents_show:
requirements:
method: GET
_format: json
id: '\d+'
id: '\d+'
api_v1_rss_last:
path: /api/v1/feed/rss/last
controller: App\Api\V1\Controller\RssController::last
defaults:
_format: xml
requirements:
method: GET
_format: xml

View file

@ -0,0 +1,21 @@
<?php
namespace App\Api\V1\Controller;
use App\Feed\RssGenerator;
use Symfony\Component\HttpFoundation\{Request, Response};
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class RssController extends AbstractController
{
private const CONTENT_TYPE = 'application/rss+xml';
public function last(Request $request, RssGenerator $generator): Response
{
$page = (int) $request->query->get('page', '1');
$xml = $generator->generateLast($page);
return new Response($xml, 200, ['Content-Type' => self::CONTENT_TYPE]);
}
}

133
src/Feed/RssGenerator.php Normal file
View file

@ -0,0 +1,133 @@
<?php
namespace App\Feed;
use App\Magnet\MagnetGenerator;
use App\Magnetico\Entity\Torrent;
use App\Magnetico\Repository\TorrentRepository;
use App\Pager\PagelessDoctrineORMAdapter;
use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Pagerfanta;
use Suin\RSSWriter\{Channel, Feed, Item};
use Symfony\Component\Routing\{Generator\UrlGeneratorInterface, RouterInterface};
/**
* Generates RSS feed.
*
* @see https://www.bittorrent.org/beps/bep_0036.html
*/
class RssGenerator
{
private const PER_PAGE = 1000;
private const MIME_TYPE = 'application/x-bittorrent';
private TorrentRepository $repo;
private RouterInterface $router;
private MagnetGenerator $magnetGenerator;
public function __construct(TorrentRepository $repo, RouterInterface $router, MagnetGenerator $magnetGenerator)
{
$this->repo = $repo;
$this->router = $router;
$this->magnetGenerator = $magnetGenerator;
}
public function generateLast(int $page): string
{
$qb = $this->createLastTorrentsQueryBuilder();
$pager = new Pagerfanta(new PagelessDoctrineORMAdapter($qb));
$pager
->setAllowOutOfRangePages(true)
->setCurrentPage($page)
->setMaxPerPage(self::PER_PAGE)
;
$feed = $this->createFeedFromTorrents($pager->getCurrentPageResults());
return $feed->render();
}
private function createFeedFromTorrents(\Traversable $torrents): Feed
{
$feed = new Feed();
$channel = $this->createChannel();
$feed->addChannel($channel);
foreach ($this->createItemsFromTorrents($torrents) as $item) {
$channel->addItem($item);
}
// TODO feed pagination
return $feed;
}
private function createChannel(): Channel
{
$time = time();
$channel = new Channel();
$channel
->title('Last')
->description('Last torrents')
->url($this->generateUrl('index'))
->feedUrl($this->generateUrl('api_v1_rss_last'))
->language('en-US')
->pubDate($time)
->lastBuildDate($time)
->ttl(15)
;
return $channel;
}
/**
* @param Torrent[]|\Traversable $torrents
*
* @return Item[]
*/
private function createItemsFromTorrents(\Traversable $torrents): array
{
$items = [];
foreach ($torrents as $torrent) {
$items[] = $this->createItemFromTorrent($torrent);
}
return $items;
}
private function createItemFromTorrent(Torrent $torrent): Item
{
$item = new Item();
$item
->title($torrent->getName())
->description($torrent->getInfoHash())
->url($this->generateUrl('torrents_show', ['id' => $torrent->getId()]))
->enclosure(
$this->magnetGenerator->generate($torrent->getInfoHash(), $torrent->getName()),
$torrent->getTotalSize(),
self::MIME_TYPE
)
->guid($torrent->getInfoHash())
;
return $item;
}
private function createLastTorrentsQueryBuilder(): QueryBuilder
{
$qb = $this->repo->createQueryBuilder('t');
$qb
->select('t')
->orderBy('t.id', 'DESC');
return $qb;
}
private function generateUrl(string $route, array $parameters = []): string
{
return $this->router->generate($route, $parameters, UrlGeneratorInterface::ABSOLUTE_URL);
}
}

View file

@ -36,13 +36,18 @@ class ApiTokenAuthenticator extends AbstractGuardAuthenticator
public function supports(Request $request): bool
{
return $request->headers->has(self::TOKEN_HEADER);
// Let's also support cookies and query params for some cases like torrent clients.
return $request->headers->has(self::TOKEN_HEADER) ||
$request->cookies->has(self::TOKEN_HEADER) ||
$request->query->has(self::TOKEN_HEADER);
}
public function getCredentials(Request $request)
{
return [
'token' => $request->headers->get(self::TOKEN_HEADER),
'token' => $request->headers->get(self::TOKEN_HEADER) ?:
$request->cookies->get(self::TOKEN_HEADER) ?:
$request->query->get(self::TOKEN_HEADER),
];
}
@ -101,7 +106,12 @@ class ApiTokenAuthenticator extends AbstractGuardAuthenticator
public function createAuthenticatedToken(UserInterface $user, $providerKey)
{
$tokenKey = $this->requestStack->getCurrentRequest()->headers->get(self::TOKEN_HEADER);
$request = $this->requestStack->getCurrentRequest();
$tokenKey = $request->headers->get(self::TOKEN_HEADER) ?:
$request->cookies->get(self::TOKEN_HEADER) ?:
$request->query->get(self::TOKEN_HEADER)
;
return new AuthenticatedApiToken(
$user,

View file

@ -4,9 +4,10 @@ namespace App\Security\Token;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Guard\Token\GuardTokenInterface;
/** This token stores ApiToken key even after eraseCredentials() called */
class AuthenticatedApiToken extends PreAuthenticatedToken
class AuthenticatedApiToken extends PreAuthenticatedToken implements GuardTokenInterface
{
/** @var string|null This token is stored only for this request and will not be erased by eraseCredentials() or serialized */
private $tokenKey;

View file

@ -210,6 +210,9 @@
"config/packages/sentry.yaml"
]
},
"suin/php-rss-writer": {
"version": "1.6.0"
},
"symfony/cache": {
"version": "v4.1.0"
},