Refactor feed functions out of Library and add some enums

This commit is contained in:
Alex Cabal 2024-11-15 21:09:42 -06:00
parent 66c44cbdbe
commit 90b70b3235
8 changed files with 117 additions and 101 deletions

View file

@ -0,0 +1,8 @@
<?
namespace Enums;
enum FeedCollectionType: string{
case Authors = 'authors';
case Collections = 'collections';
case Subjects = 'subjects';
}

View file

@ -5,4 +5,12 @@ enum FeedType: string{
case Atom = 'atom'; case Atom = 'atom';
case Opds = 'opds'; case Opds = 'opds';
case Rss = 'rss'; 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',
};
}
} }

View file

@ -4,6 +4,7 @@ use Safe\DateTimeImmutable;
use function Safe\exec; use function Safe\exec;
use function Safe\file_get_contents; use function Safe\file_get_contents;
use function Safe\file_put_contents; use function Safe\file_put_contents;
use function Safe\glob;
use function Safe\tempnam; use function Safe\tempnam;
use function Safe\unlink; use function Safe\unlink;
@ -64,4 +65,55 @@ abstract class Feed{
file_put_contents($this->Path, $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;
}
} }

View file

@ -180,57 +180,4 @@ class Library{
return ['months' => $months, 'subjects' => $subjects, 'collections' => $collections, 'authors' => $authors]; 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;
}
} }

View file

@ -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<string, string>> $collections
* @param array<string, array<Ebook>> $ebooks * @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. $collator = Collator::create('en_US'); // Used for sorting letters with diacritics, like in author names.
$name = preg_replace('/s$/', '', $collectionType->value);
if($collator === null){ if($collator === null){
return; return;
} }
@ -52,20 +54,20 @@ function CreateOpdsCollectionFeed(string $name, string $url, string $description
// Create the collections navigation document. // Create the collections navigation document.
$collectionNavigationEntries = []; $collectionNavigationEntries = [];
foreach($collections as $collection){ 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']; $entry->SortTitle = $collection['sortedname'];
$collectionNavigationEntries[] = $entry; $collectionNavigationEntries[] = $entry;
} }
$collectionsFeed = new OpdsNavigationFeed('Standard Ebooks by ' . ucfirst($name), 'Browse Standard Ebooks by ' . $name . '.', $url, $webRoot . $url . '/index.xml', $collectionNavigationEntries, $opdsRoot); $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 . '.'; $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. // Now generate each individual collection feed.
foreach($collectionNavigationEntries as $collectionNavigationEntry){ foreach($collectionNavigationEntries as $collectionNavigationEntry){
$id = basename($collectionNavigationEntry->Id); $id = basename($collectionNavigationEntry->Id);
usort($ebooks[$id], 'SortByUpdatedDesc'); usort($ebooks[$id], 'SortByUpdatedDesc');
$collectionFeed = new OpdsAcquisitionFeed($collectionNavigationEntry->Title . ' Ebooks', $collectionNavigationEntry->Description, $url . '/' . $id, $webRoot . $url . '/' . $id . '.xml', $ebooks[$id], $collectionsFeed); $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); SaveFeed($opdsRoot, $force, null, null, NOW);
// Create the Subjects feeds. // 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. // 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. // 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. // 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); $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);

View file

@ -32,7 +32,7 @@ if [ "${type}" = "bulk-downloads" ]; then
fi fi
if [ "${type}" = "feeds" ]; then 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 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 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

View file

@ -2,47 +2,50 @@
use function Safe\apcu_fetch; use function Safe\apcu_fetch;
use function Safe\preg_replace; use function Safe\preg_replace;
$class = HttpInput::Str(GET, 'class') ?? ''; $collectionType = Enums\FeedCollectionType::tryFrom(HttpInput::Str(GET, 'class') ?? '');
$type = HttpInput::Str(GET, 'type') ?? ''; $type = Enums\FeedType::tryFrom(HttpInput::Str(GET, 'type') ?? '');
if($class != 'authors' && $class != 'collections' && $class != 'subjects'){ if($collectionType === null){
Template::Emit404(); Template::Emit404();
} }
if($type != 'rss' && $type != 'atom'){ if($type === null || ($type != Enums\FeedType::Rss && $type != Enums\FeedType::Atom)){
Template::Emit404(); Template::Emit404();
} }
$feeds = []; $feeds = [];
$lcTitle = preg_replace('/s$/', '', $class); $lcTitle = preg_replace('/s$/', '', $collectionType->value);
$ucTitle = ucfirst($lcTitle); $ucTitle = ucfirst($lcTitle);
$ucType = 'RSS 2.0';
if($type === 'atom'){
$ucType = 'Atom 1.0';
}
try{ try{
/** @var array<stdClass> $feeds */ /** @var array<stdClass> $feeds */
$feeds = apcu_fetch('feeds-index-' . $type . '-' . $class); $feeds = apcu_fetch('feeds-index-' . $type->value . '-' . $collectionType->value);
} }
catch(Safe\Exceptions\ApcuException){ catch(Safe\Exceptions\ApcuException){
/** @var array<stdClass> $feeds */ $feeds = Feed::RebuildFeedsCache($type, $collectionType);
$feeds = Library::RebuildFeedsCache($type, $class);
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> <main>
<article> <article>
<h1><?= $ucType ?> Ebook Feeds by <?= $ucTitle ?></h1> <h1><?= $type->GetDisplayName() ?> Ebook Feeds by <?= $ucTitle ?></h1>
<?= Template::FeedHowTo() ?> <?= Template::FeedHowTo() ?>
<section id="ebooks-by-<?= $lcTitle ?>"> <section id="ebooks-by-<?= $lcTitle ?>">
<h2>Ebooks by <?= $lcTitle ?></h2> <h2>Ebooks by <?= $lcTitle ?></h2>
<ul class="feed"> <ul class="feed">
<? foreach($feeds as $feed){ ?> <? foreach($feeds as $feed){ ?>
<li> <li>
<p><a href="<?= Formatter::EscapeHtml($feed->Url) ?>"><?= Formatter::EscapeHtml($feed->Label) ?></a></p> <p>
<p class="url"><? if(isset(Session::$User->Email)){ ?>https://<?= rawurlencode(Session::$User->Email) ?>@<?= SITE_DOMAIN ?><? }else{ ?><?= SITE_URL ?><? } ?><?= Formatter::EscapeHtml($feed->Url) ?></p> <a href="<?= Formatter::EscapeHtml($feed->Url) ?>"><?= Formatter::EscapeHtml($feed->Label) ?></a>
</li> </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> </ul>
</section> </section>

View file

@ -1,9 +1,14 @@
<? <?
/**
* GET /collections/<collection>/feeds
* GET /ebooks/<author>/feeds
*/
use function Safe\exec; use function Safe\exec;
$author = HttpInput::Str(GET, 'author'); $author = HttpInput::Str(GET, 'author');
$collection = HttpInput::Str(GET, 'collection'); $collection = HttpInput::Str(GET, 'collection');
$name = null; $collectionType = null;
$target = null; $target = null;
$feedTitle = ''; $feedTitle = '';
$feedUrl = ''; $feedUrl = '';
@ -12,40 +17,40 @@ $description = '';
$label = null; $label = null;
if($author !== null){ if($author !== null){
$name = 'authors'; $collectionType = Enums\FeedCollectionType::Authors;
$target = $author; $target = $author;
} }
if($collection !== null){ if($collection !== null){
$name = 'collections'; $collectionType = Enums\FeedCollectionType::Collections;
$target = $collection; $target = $collection;
} }
try{ try{
if($target === null || $name === null){ if($target === null || $collectionType === null){
throw new Exceptions\CollectionNotFoundException(); 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)){ if(!is_file($file)){
throw new Exceptions\CollectionNotFoundException(); throw new Exceptions\CollectionNotFoundException();
} }
$label = exec('attr -g se-label ' . escapeshellarg($file)) ?: basename($file, '.xml'); $label = exec('attr -g se-label ' . escapeshellarg($file)) ?: basename($file, '.xml');
if($name == 'authors'){ if($collectionType == Enums\FeedCollectionType::Authors){
$title = 'Ebook feeds for ' . $label; $title = 'Ebook feeds for ' . $label;
$description = 'A list of available ebook feeds for ebooks by ' . $label . '.'; $description = 'A list of available ebook feeds for ebooks by ' . $label . '.';
$feedTitle = 'Standard Ebooks - Ebooks by ' . $label; $feedTitle = 'Standard Ebooks - Ebooks by ' . $label;
} }
if($name == 'collections'){ if($collectionType == Enums\FeedCollectionType::Collections){
$title = 'Ebook feeds for the ' . $label . ' collection'; $title = 'Ebook feeds for the ' . $label . ' collection';
$description = 'A list of available ebook feeds for ebooks in the ' . $label . ' collection.'; $description = 'A list of available ebook feeds for ebooks in the ' . $label . ' collection.';
$feedTitle = 'Standard Ebooks - Ebooks in the ' . $label . ' collection'; $feedTitle = 'Standard Ebooks - Ebooks in the ' . $label . ' collection';
} }
$feedUrl = '/' . $name . '/' . $target; $feedUrl = '/' . $collectionType->value . '/' . $target;
} }
catch(Exceptions\CollectionNotFoundException){ catch(Exceptions\CollectionNotFoundException){
Template::Emit404(); Template::Emit404();
@ -57,18 +62,7 @@ catch(Exceptions\CollectionNotFoundException){
<?= Template::FeedHowTo() ?> <?= Template::FeedHowTo() ?>
<? foreach(Enums\FeedType::cases() as $feedType){ ?> <? foreach(Enums\FeedType::cases() as $feedType){ ?>
<section id="ebooks-by-<?= $feedType->value ?>"> <section id="ebooks-by-<?= $feedType->value ?>">
<h2> <h2><?= $feedType->GetDisplayName() ?> Feed</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>
<? if($feedType == Enums\FeedType::Opds){ ?> <? if($feedType == Enums\FeedType::Opds){ ?>
<p>Import this feed into your ereader app to get access to these ebooks directly in your ereader.</p> <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"> <ul class="feed">
<li> <li>
<p><a href="/feeds/<?= $feedType->value ?>/<?= $name ?>/<?= $target?>"><?= Formatter::EscapeHtml($label) ?></a></p> <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> <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> </li>
</ul> </ul>
</section> </section>