Merged in feature_upgrade_and_rss (pull request #36)
Implementing RSS feed draft.
This commit is contained in:
commit
234f16b312
|
@ -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
51
composer.lock
generated
|
@ -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",
|
||||
|
|
|
@ -90,3 +90,12 @@ api_v1_torrents_show:
|
|||
method: GET
|
||||
_format: json
|
||||
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
|
||||
|
|
21
src/Api/V1/Controller/RssController.php
Normal file
21
src/Api/V1/Controller/RssController.php
Normal 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
133
src/Feed/RssGenerator.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -210,6 +210,9 @@
|
|||
"config/packages/sentry.yaml"
|
||||
]
|
||||
},
|
||||
"suin/php-rss-writer": {
|
||||
"version": "1.6.0"
|
||||
},
|
||||
"symfony/cache": {
|
||||
"version": "v4.1.0"
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue