mirror of
https://github.com/standardebooks/web.git
synced 2025-07-12 17:42:29 -04:00
Refactor feed functions out of Library and add some enums
This commit is contained in:
parent
66c44cbdbe
commit
90b70b3235
8 changed files with 117 additions and 101 deletions
8
lib/Enums/FeedCollectionType.php
Normal file
8
lib/Enums/FeedCollectionType.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?
|
||||
namespace Enums;
|
||||
|
||||
enum FeedCollectionType: string{
|
||||
case Authors = 'authors';
|
||||
case Collections = 'collections';
|
||||
case Subjects = 'subjects';
|
||||
}
|
|
@ -5,4 +5,12 @@ enum FeedType: string{
|
|||
case Atom = 'atom';
|
||||
case Opds = 'opds';
|
||||
case Rss = 'rss';
|
||||
|
||||
public function GetDisplayName(): string{
|
||||
return match($this){
|
||||
self::Atom => 'Atom 1.0',
|
||||
self::Opds => 'OPDS 1.2',
|
||||
self::Rss => 'RSS 2.0',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
52
lib/Feed.php
52
lib/Feed.php
|
@ -4,6 +4,7 @@ use Safe\DateTimeImmutable;
|
|||
use function Safe\exec;
|
||||
use function Safe\file_get_contents;
|
||||
use function Safe\file_put_contents;
|
||||
use function Safe\glob;
|
||||
use function Safe\tempnam;
|
||||
use function Safe\unlink;
|
||||
|
||||
|
@ -64,4 +65,55 @@ abstract class Feed{
|
|||
|
||||
file_put_contents($this->Path, $feed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?array<stdClass>
|
||||
*
|
||||
* @throws Exceptions\AppException
|
||||
*/
|
||||
public static function RebuildFeedsCache(?Enums\FeedType $returnType = null, ?Enums\FeedCollectionType $returnCollectionType = null): ?array{
|
||||
$retval = null;
|
||||
$collator = Collator::create('en_US'); // Used for sorting letters with diacritics like in author names
|
||||
if($collator === null){
|
||||
throw new Exceptions\AppException('Couldn\'t create collator object when rebuilding feeds cache.');
|
||||
}
|
||||
|
||||
foreach(Enums\FeedType::cases() as $type){
|
||||
foreach(Enums\FeedCollectionType::cases() as $collectionType){
|
||||
$files = glob(WEB_ROOT . '/feeds/' . $type->value . '/' . $collectionType->value . '/*.xml');
|
||||
|
||||
$feeds = [];
|
||||
|
||||
foreach($files as $file){
|
||||
$obj = new stdClass();
|
||||
$obj->Url = '/feeds/' . $type->value . '/' . $collectionType->value . '/' . basename($file, '.xml');
|
||||
|
||||
$obj->Label = exec('attr -g se-label ' . escapeshellarg($file)) ?: null;
|
||||
if($obj->Label == null){
|
||||
$obj->Label = basename($file, '.xml');
|
||||
}
|
||||
|
||||
$obj->LabelSort = exec('attr -g se-label-sort ' . escapeshellarg($file)) ?: null;
|
||||
if($obj->LabelSort == null){
|
||||
$obj->LabelSort = basename($file, '.xml');
|
||||
}
|
||||
|
||||
$feeds[] = $obj;
|
||||
}
|
||||
|
||||
usort($feeds, function(stdClass $a, stdClass $b) use($collator): int{
|
||||
$result = $collator->compare($a->LabelSort, $b->LabelSort);
|
||||
return $result === false ? 0 : $result;
|
||||
});
|
||||
|
||||
if($type == $returnType && $collectionType == $returnCollectionType){
|
||||
$retval = $feeds;
|
||||
}
|
||||
|
||||
apcu_store('feeds-index-' . $type->value . '-' . $collectionType->value, $feeds);
|
||||
}
|
||||
}
|
||||
|
||||
return $retval;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -180,57 +180,4 @@ class Library{
|
|||
|
||||
return ['months' => $months, 'subjects' => $subjects, 'collections' => $collections, 'authors' => $authors];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<stdClass>
|
||||
*
|
||||
* @throws Exceptions\AppException
|
||||
*/
|
||||
public static function RebuildFeedsCache(?string $returnType = null, ?string $returnClass = null): ?array{
|
||||
$feedTypes = ['opds', 'atom', 'rss'];
|
||||
$feedClasses = ['authors', 'collections', 'subjects'];
|
||||
$retval = null;
|
||||
$collator = Collator::create('en_US'); // Used for sorting letters with diacritics like in author names
|
||||
if($collator === null){
|
||||
throw new Exceptions\AppException('Couldn\'t create collator object when rebuilding feeds cache.');
|
||||
}
|
||||
|
||||
foreach($feedTypes as $type){
|
||||
foreach($feedClasses as $class){
|
||||
$files = glob(WEB_ROOT . '/feeds/' . $type . '/' . $class . '/*.xml');
|
||||
|
||||
$feeds = [];
|
||||
|
||||
foreach($files as $file){
|
||||
$obj = new stdClass();
|
||||
$obj->Url = '/feeds/' . $type . '/' . $class . '/' . basename($file, '.xml');
|
||||
|
||||
$obj->Label = exec('attr -g se-label ' . escapeshellarg($file)) ?: null;
|
||||
if($obj->Label == null){
|
||||
$obj->Label = basename($file, '.xml');
|
||||
}
|
||||
|
||||
$obj->LabelSort = exec('attr -g se-label-sort ' . escapeshellarg($file)) ?: null;
|
||||
if($obj->LabelSort == null){
|
||||
$obj->LabelSort = basename($file, '.xml');
|
||||
}
|
||||
|
||||
$feeds[] = $obj;
|
||||
}
|
||||
|
||||
usort($feeds, function(stdClass $a, stdClass $b) use($collator): int{
|
||||
$result = $collator->compare($a->LabelSort, $b->LabelSort);
|
||||
return $result === false ? 0 : $result;
|
||||
});
|
||||
|
||||
if($type == $returnType && $class == $returnClass){
|
||||
$retval = $feeds;
|
||||
}
|
||||
|
||||
apcu_store('feeds-index-' . $type . '-' . $class, $feeds);
|
||||
}
|
||||
}
|
||||
|
||||
return $retval;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,9 +37,11 @@ function SaveFeed(Feed $feed, bool $force, ?string $label = null, ?string $label
|
|||
* @param array<string, array<string, string>> $collections
|
||||
* @param array<string, array<Ebook>> $ebooks
|
||||
*/
|
||||
function CreateOpdsCollectionFeed(string $name, string $url, string $description, array $collections, array $ebooks, DateTimeImmutable $now, string $webRoot, OpdsNavigationFeed $opdsRoot, bool $force): void{
|
||||
function CreateOpdsCollectionFeed(Enums\FeedCollectionType $collectionType, string $url, string $description, array $collections, array $ebooks, string $webRoot, OpdsNavigationFeed $opdsRoot, bool $force): void{
|
||||
$collator = Collator::create('en_US'); // Used for sorting letters with diacritics, like in author names.
|
||||
|
||||
$name = preg_replace('/s$/', '', $collectionType->value);
|
||||
|
||||
if($collator === null){
|
||||
return;
|
||||
}
|
||||
|
@ -52,20 +54,20 @@ function CreateOpdsCollectionFeed(string $name, string $url, string $description
|
|||
// Create the collections navigation document.
|
||||
$collectionNavigationEntries = [];
|
||||
foreach($collections as $collection){
|
||||
$entry = new OpdsNavigationEntry($collection['name'], str_replace('%s', $collection['name'], $description), $url . '/' . $collection['id'], $now, 'subsection', 'navigation');
|
||||
$entry = new OpdsNavigationEntry($collection['name'], str_replace('%s', $collection['name'], $description), $url . '/' . $collection['id'], NOW, 'subsection', 'navigation');
|
||||
$entry->SortTitle = $collection['sortedname'];
|
||||
$collectionNavigationEntries[] = $entry;
|
||||
}
|
||||
$collectionsFeed = new OpdsNavigationFeed('Standard Ebooks by ' . ucfirst($name), 'Browse Standard Ebooks by ' . $name . '.', $url, $webRoot . $url . '/index.xml', $collectionNavigationEntries, $opdsRoot);
|
||||
$collectionsFeed->Subtitle = 'Browse Standard Ebooks by ' . $name . '.';
|
||||
SaveFeed($collectionsFeed, $force, null, null, $now);
|
||||
SaveFeed($collectionsFeed, $force, null, null, NOW);
|
||||
|
||||
// Now generate each individual collection feed.
|
||||
foreach($collectionNavigationEntries as $collectionNavigationEntry){
|
||||
$id = basename($collectionNavigationEntry->Id);
|
||||
usort($ebooks[$id], 'SortByUpdatedDesc');
|
||||
$collectionFeed = new OpdsAcquisitionFeed($collectionNavigationEntry->Title . ' Ebooks', $collectionNavigationEntry->Description, $url . '/' . $id, $webRoot . $url . '/' . $id . '.xml', $ebooks[$id], $collectionsFeed);
|
||||
SaveFeed($collectionFeed, $force, $collectionNavigationEntry->Title, $collectionNavigationEntry->SortTitle, $now);
|
||||
SaveFeed($collectionFeed, $force, $collectionNavigationEntry->Title, $collectionNavigationEntry->SortTitle, NOW);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,13 +169,13 @@ $opdsRoot = new OpdsNavigationFeed('Standard Ebooks', 'The Standard Ebooks catal
|
|||
SaveFeed($opdsRoot, $force, null, null, NOW);
|
||||
|
||||
// Create the Subjects feeds.
|
||||
CreateOpdsCollectionFeed('subject', '/feeds/opds/subjects', 'Standard Ebooks in the “%s” subject, most-recently-released first.', $subjects, $ebooksBySubject, NOW, $webRoot, $opdsRoot, $force);
|
||||
CreateOpdsCollectionFeed(Enums\FeedCollectionType::Subjects, '/feeds/opds/subjects', 'Standard Ebooks in the “%s” subject, most-recently-released first.', $subjects, $ebooksBySubject, $webRoot, $opdsRoot, $force);
|
||||
|
||||
// Create the Collections feeds.
|
||||
CreateOpdsCollectionFeed('collection', '/feeds/opds/collections', 'Standard Ebooks in the “%s” collection, most-recently-released first.', $collections, $ebooksByCollection, NOW, $webRoot, $opdsRoot, $force);
|
||||
CreateOpdsCollectionFeed(Enums\FeedCollectionType::Collections, '/feeds/opds/collections', 'Standard Ebooks in the “%s” collection, most-recently-released first.', $collections, $ebooksByCollection, $webRoot, $opdsRoot, $force);
|
||||
|
||||
// Create the Author feeds.
|
||||
CreateOpdsCollectionFeed('author', '/feeds/opds/authors', 'Standard Ebooks by %s, most-recently-released first.', $authors, $ebooksByAuthor, NOW, $webRoot, $opdsRoot, $force);
|
||||
CreateOpdsCollectionFeed(Enums\FeedCollectionType::Authors, '/feeds/opds/authors', 'Standard Ebooks by %s, most-recently-released first.', $authors, $ebooksByAuthor, $webRoot, $opdsRoot, $force);
|
||||
|
||||
// Create the All feed.
|
||||
$allFeed = new OpdsAcquisitionFeed('All Standard Ebooks', 'All Standard Ebooks, most-recently-updated first. This is a Complete Acquisition Feed as defined in OPDS 1.2 §2.5.', '/feeds/opds/all', $webRoot . '/feeds/opds/all.xml', $allEbooks, $opdsRoot, true);
|
||||
|
|
|
@ -32,7 +32,7 @@ if [ "${type}" = "bulk-downloads" ]; then
|
|||
fi
|
||||
|
||||
if [ "${type}" = "feeds" ]; then
|
||||
echo "<?php require_once('Core.php'); Library::RebuildFeedsCache(); ?>" > /tmp/rebuild-cache.php
|
||||
echo "<?php require_once('Core.php'); Feed::RebuildFeedsCache(); ?>" > /tmp/rebuild-cache.php
|
||||
fi
|
||||
|
||||
sudo -u www-data env SCRIPT_FILENAME=/tmp/rebuild-cache.php REQUEST_METHOD=GET cgi-fcgi -bind -connect "/run/php/standardebooks.org.sock" &> /dev/null
|
||||
|
|
|
@ -2,47 +2,50 @@
|
|||
use function Safe\apcu_fetch;
|
||||
use function Safe\preg_replace;
|
||||
|
||||
$class = HttpInput::Str(GET, 'class') ?? '';
|
||||
$type = HttpInput::Str(GET, 'type') ?? '';
|
||||
$collectionType = Enums\FeedCollectionType::tryFrom(HttpInput::Str(GET, 'class') ?? '');
|
||||
$type = Enums\FeedType::tryFrom(HttpInput::Str(GET, 'type') ?? '');
|
||||
|
||||
if($class != 'authors' && $class != 'collections' && $class != 'subjects'){
|
||||
if($collectionType === null){
|
||||
Template::Emit404();
|
||||
}
|
||||
|
||||
if($type != 'rss' && $type != 'atom'){
|
||||
if($type === null || ($type != Enums\FeedType::Rss && $type != Enums\FeedType::Atom)){
|
||||
Template::Emit404();
|
||||
}
|
||||
|
||||
$feeds = [];
|
||||
|
||||
$lcTitle = preg_replace('/s$/', '', $class);
|
||||
$lcTitle = preg_replace('/s$/', '', $collectionType->value);
|
||||
$ucTitle = ucfirst($lcTitle);
|
||||
$ucType = 'RSS 2.0';
|
||||
if($type === 'atom'){
|
||||
$ucType = 'Atom 1.0';
|
||||
}
|
||||
|
||||
try{
|
||||
/** @var array<stdClass> $feeds */
|
||||
$feeds = apcu_fetch('feeds-index-' . $type . '-' . $class);
|
||||
$feeds = apcu_fetch('feeds-index-' . $type->value . '-' . $collectionType->value);
|
||||
}
|
||||
catch(Safe\Exceptions\ApcuException){
|
||||
/** @var array<stdClass> $feeds */
|
||||
$feeds = Library::RebuildFeedsCache($type, $class);
|
||||
$feeds = Feed::RebuildFeedsCache($type, $collectionType);
|
||||
|
||||
if($feeds === null){
|
||||
Template::Emit404();
|
||||
}
|
||||
}
|
||||
?><?= Template::Header(['title' => $ucType . ' Ebook Feeds by ' . $ucTitle, 'description' => 'A list of available ' . $ucType . ' feeds of Standard Ebooks ebooks by ' . $lcTitle . '.']) ?>
|
||||
?><?= Template::Header(['title' => $type->GetDisplayName() . ' Ebook Feeds by ' . $ucTitle, 'description' => 'A list of available ' . $type->GetDisplayName() . ' feeds of Standard Ebooks ebooks by ' . $lcTitle . '.']) ?>
|
||||
<main>
|
||||
<article>
|
||||
<h1><?= $ucType ?> Ebook Feeds by <?= $ucTitle ?></h1>
|
||||
<h1><?= $type->GetDisplayName() ?> Ebook Feeds by <?= $ucTitle ?></h1>
|
||||
<?= Template::FeedHowTo() ?>
|
||||
<section id="ebooks-by-<?= $lcTitle ?>">
|
||||
<h2>Ebooks by <?= $lcTitle ?></h2>
|
||||
<ul class="feed">
|
||||
<? foreach($feeds as $feed){ ?>
|
||||
<li>
|
||||
<p><a href="<?= Formatter::EscapeHtml($feed->Url) ?>"><?= Formatter::EscapeHtml($feed->Label) ?></a></p>
|
||||
<p class="url"><? if(isset(Session::$User->Email)){ ?>https://<?= rawurlencode(Session::$User->Email) ?>@<?= SITE_DOMAIN ?><? }else{ ?><?= SITE_URL ?><? } ?><?= Formatter::EscapeHtml($feed->Url) ?></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<a href="<?= Formatter::EscapeHtml($feed->Url) ?>"><?= Formatter::EscapeHtml($feed->Label) ?></a>
|
||||
</p>
|
||||
<p class="url">
|
||||
<? if(isset(Session::$User->Email)){ ?>https://<?= rawurlencode(Session::$User->Email) ?>@<?= SITE_DOMAIN ?><? }else{ ?><?= SITE_URL ?><? } ?><?= Formatter::EscapeHtml($feed->Url) ?>
|
||||
</p>
|
||||
</li>
|
||||
<? } ?>
|
||||
</ul>
|
||||
</section>
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
<?
|
||||
/**
|
||||
* GET /collections/<collection>/feeds
|
||||
* GET /ebooks/<author>/feeds
|
||||
*/
|
||||
|
||||
use function Safe\exec;
|
||||
|
||||
$author = HttpInput::Str(GET, 'author');
|
||||
$collection = HttpInput::Str(GET, 'collection');
|
||||
$name = null;
|
||||
$collectionType = null;
|
||||
$target = null;
|
||||
$feedTitle = '';
|
||||
$feedUrl = '';
|
||||
|
@ -12,40 +17,40 @@ $description = '';
|
|||
$label = null;
|
||||
|
||||
if($author !== null){
|
||||
$name = 'authors';
|
||||
$collectionType = Enums\FeedCollectionType::Authors;
|
||||
$target = $author;
|
||||
}
|
||||
|
||||
if($collection !== null){
|
||||
$name = 'collections';
|
||||
$collectionType = Enums\FeedCollectionType::Collections;
|
||||
$target = $collection;
|
||||
}
|
||||
|
||||
try{
|
||||
if($target === null || $name === null){
|
||||
if($target === null || $collectionType === null){
|
||||
throw new Exceptions\CollectionNotFoundException();
|
||||
}
|
||||
|
||||
$file = WEB_ROOT . '/feeds/opds/' . $name . '/' . $target . '.xml';
|
||||
$file = WEB_ROOT . '/feeds/opds/' . $collectionType->value . '/' . $target . '.xml';
|
||||
if(!is_file($file)){
|
||||
throw new Exceptions\CollectionNotFoundException();
|
||||
}
|
||||
|
||||
$label = exec('attr -g se-label ' . escapeshellarg($file)) ?: basename($file, '.xml');
|
||||
|
||||
if($name == 'authors'){
|
||||
if($collectionType == Enums\FeedCollectionType::Authors){
|
||||
$title = 'Ebook feeds for ' . $label;
|
||||
$description = 'A list of available ebook feeds for ebooks by ' . $label . '.';
|
||||
$feedTitle = 'Standard Ebooks - Ebooks by ' . $label;
|
||||
}
|
||||
|
||||
if($name == 'collections'){
|
||||
if($collectionType == Enums\FeedCollectionType::Collections){
|
||||
$title = 'Ebook feeds for the ' . $label . ' collection';
|
||||
$description = 'A list of available ebook feeds for ebooks in the ' . $label . ' collection.';
|
||||
$feedTitle = 'Standard Ebooks - Ebooks in the ' . $label . ' collection';
|
||||
}
|
||||
|
||||
$feedUrl = '/' . $name . '/' . $target;
|
||||
$feedUrl = '/' . $collectionType->value . '/' . $target;
|
||||
}
|
||||
catch(Exceptions\CollectionNotFoundException){
|
||||
Template::Emit404();
|
||||
|
@ -57,18 +62,7 @@ catch(Exceptions\CollectionNotFoundException){
|
|||
<?= Template::FeedHowTo() ?>
|
||||
<? foreach(Enums\FeedType::cases() as $feedType){ ?>
|
||||
<section id="ebooks-by-<?= $feedType->value ?>">
|
||||
<h2>
|
||||
<? if($feedType == Enums\FeedType::Rss){ ?>
|
||||
RSS 2.0
|
||||
<? } ?>
|
||||
<? if($feedType == Enums\FeedType::Atom){ ?>
|
||||
Atom 1.0
|
||||
<? } ?>
|
||||
<? if($feedType == Enums\FeedType::Opds){ ?>
|
||||
OPDS 1.2
|
||||
<? } ?>
|
||||
Feed
|
||||
</h2>
|
||||
<h2><?= $feedType->GetDisplayName() ?> Feed</h2>
|
||||
<? if($feedType == Enums\FeedType::Opds){ ?>
|
||||
<p>Import this feed into your ereader app to get access to these ebooks directly in your ereader.</p>
|
||||
<? } ?>
|
||||
|
@ -80,8 +74,10 @@ catch(Exceptions\CollectionNotFoundException){
|
|||
<? } ?>
|
||||
<ul class="feed">
|
||||
<li>
|
||||
<p><a href="/feeds/<?= $feedType->value ?>/<?= $name ?>/<?= $target?>"><?= Formatter::EscapeHtml($label) ?></a></p>
|
||||
<p class="url"><? if(isset(Session::$User->Email)){ ?>https://<?= rawurlencode(Session::$User->Email) ?>@<?= SITE_DOMAIN ?><? }else{ ?><?= SITE_URL ?><? } ?>/feeds/<?= $feedType->value ?>/<?= $name ?>/<?= $target?></p>
|
||||
<p>
|
||||
<a href="/feeds/<?= $feedType->value ?>/<?= $collectionType->value ?>/<?= $target?>"><?= Formatter::EscapeHtml($label) ?></a>
|
||||
</p>
|
||||
<p class="url"><? if(isset(Session::$User->Email)){ ?>https://<?= rawurlencode(Session::$User->Email) ?>@<?= SITE_DOMAIN ?><? }else{ ?><?= SITE_URL ?><? } ?>/feeds/<?= $feedType->value ?>/<?= $collectionType->value ?>/<?= $target?></p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue