diff --git a/lib/Enums/FeedCollectionType.php b/lib/Enums/FeedCollectionType.php new file mode 100644 index 00000000..f2f01147 --- /dev/null +++ b/lib/Enums/FeedCollectionType.php @@ -0,0 +1,8 @@ + 'Atom 1.0', + self::Opds => 'OPDS 1.2', + self::Rss => 'RSS 2.0', + }; + } } diff --git a/lib/Feed.php b/lib/Feed.php index 0eb9b860..953fe07f 100644 --- a/lib/Feed.php +++ b/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 + * + * @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; + } } diff --git a/lib/Library.php b/lib/Library.php index f35667ef..9b0e5f49 100644 --- a/lib/Library.php +++ b/lib/Library.php @@ -180,57 +180,4 @@ class Library{ return ['months' => $months, 'subjects' => $subjects, 'collections' => $collections, 'authors' => $authors]; } - - /** - * @return array - * - * @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; - } } diff --git a/scripts/generate-feeds b/scripts/generate-feeds index 4bafec85..907e3a0b 100755 --- a/scripts/generate-feeds +++ b/scripts/generate-feeds @@ -37,9 +37,11 @@ function SaveFeed(Feed $feed, bool $force, ?string $label = null, ?string $label * @param array> $collections * @param array> $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); diff --git a/scripts/rebuild-cache b/scripts/rebuild-cache index 40f1480d..f363df96 100755 --- a/scripts/rebuild-cache +++ b/scripts/rebuild-cache @@ -32,7 +32,7 @@ if [ "${type}" = "bulk-downloads" ]; then fi if [ "${type}" = "feeds" ]; then - echo "" > /tmp/rebuild-cache.php + echo "" > /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 diff --git a/www/feeds/collection.php b/www/feeds/collection.php index 2b1d5876..1650f17b 100644 --- a/www/feeds/collection.php +++ b/www/feeds/collection.php @@ -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 $feeds */ - $feeds = apcu_fetch('feeds-index-' . $type . '-' . $class); + $feeds = apcu_fetch('feeds-index-' . $type->value . '-' . $collectionType->value); } catch(Safe\Exceptions\ApcuException){ - /** @var array $feeds */ - $feeds = Library::RebuildFeedsCache($type, $class); + $feeds = Feed::RebuildFeedsCache($type, $collectionType); + + if($feeds === null){ + Template::Emit404(); + } } -?> $ucType . ' Ebook Feeds by ' . $ucTitle, 'description' => 'A list of available ' . $ucType . ' feeds of Standard Ebooks ebooks by ' . $lcTitle . '.']) ?> +?> $type->GetDisplayName() . ' Ebook Feeds by ' . $ucTitle, 'description' => 'A list of available ' . $type->GetDisplayName() . ' feeds of Standard Ebooks ebooks by ' . $lcTitle . '.']) ?>
-

Ebook Feeds by

+

GetDisplayName() ?> Ebook Feeds by

Ebooks by

    -
  • -

    Label) ?>

    -

    Email)){ ?>https://Email) ?>@Url) ?>

    -
  • +
  • +

    + Label) ?> +

    +

    + Email)){ ?>https://Email) ?>@Url) ?> +

    +
diff --git a/www/feeds/get.php b/www/feeds/get.php index 0148a9a5..423196e0 100644 --- a/www/feeds/get.php +++ b/www/feeds/get.php @@ -1,9 +1,14 @@ /feeds + * GET /ebooks//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){
-

- - RSS 2.0 - - - Atom 1.0 - - - OPDS 1.2 - - Feed -

+

GetDisplayName() ?> Feed

Import this feed into your ereader app to get access to these ebooks directly in your ereader.

@@ -80,8 +74,10 @@ catch(Exceptions\CollectionNotFoundException){
  • -

    -

    Email)){ ?>https://Email) ?>@/feeds/value ?>//

    +

    + +

    +

    Email)){ ?>https://Email) ?>@/feeds/value ?>/value ?>/