Further refinment to OPDS/RSS generation and also add Atom feeds

This commit is contained in:
Alex Cabal 2022-06-23 15:03:52 -05:00
parent d75d847004
commit f9fd6c8a02
23 changed files with 733 additions and 859 deletions

4
.gitignore vendored
View file

@ -2,7 +2,11 @@ ebooks/*
www/ebooks/* www/ebooks/*
www/images/covers/* www/images/covers/*
www/opds/*.xml www/opds/*.xml
www/opds/subjects/*.xml
www/rss/*.xml www/rss/*.xml
www/rss/subjects/*.xml
www/atom/*.xml
www/atom/subjects/*.xml
vendor/ vendor/
composer.lock composer.lock
.vagrant/ .vagrant/

View file

@ -8,39 +8,19 @@ use function Safe\unlink;
class AtomFeed extends Feed{ class AtomFeed extends Feed{
public $Id; public $Id;
public $Updated = null;
public $Subtitle = null;
public function __construct(string $url, string $title, string $path, array $entries){ public function __construct(string $title, string $subtitle, string $url, string $path, array $entries){
parent::__construct($url, $title, $path, $entries); parent::__construct($title, $url, $path, $entries);
$this->Id = 'https://standardebooks.org' . $url; $this->Subtitle = $subtitle;
} $this->Id = $url;
$this->Stylesheet = '/atom/style';
private function Sha1Entries(string $xmlString): string{
try{
$xml = new SimpleXMLElement(str_replace('xmlns=', 'ns=', $xmlString));
$xml->registerXPathNamespace('dc', 'http://purl.org/dc/elements/1.1/');
$xml->registerXPathNamespace('schema', 'http://schema.org/');
// Remove any <updated> elements, we don't want to compare against those.
foreach($xml->xpath('/feed/updated') ?: [] as $element){
unset($element[0]);
}
$output = '';
foreach($xml->xpath('/feed/entry') ?: [] as $entry){
$output .= $entry->asXml();
}
return sha1(preg_replace('/\s/ius', '', $output));
}
catch(Exception $ex){
// Invalid XML
return '';
}
} }
protected function GetXmlString(): string{ protected function GetXmlString(): string{
if($this->XmlString === null){ if($this->XmlString === null){
$feed = Template::AtomFeed(['id' => $this->Id, 'url' => $this->Url, 'title' => $this->Title, 'entries' => $this->Entries]); $feed = Template::AtomFeed(['id' => $this->Id, 'url' => $this->Url, 'title' => $this->Title, 'subtitle' => $this->Subtitle, 'updatedTimestamp' => $this->Updated, 'entries' => $this->Entries]);
$this->XmlString = $this->CleanXmlString($feed); $this->XmlString = $this->CleanXmlString($feed);
} }
@ -49,6 +29,44 @@ class AtomFeed extends Feed{
} }
protected function HasChanged(string $path): bool{ protected function HasChanged(string $path): bool{
return !is_file($path) || ($this->Sha1Entries($this->GetXmlString()) != $this->Sha1Entries(file_get_contents($path))); if(!is_file($path)){
return true;
}
$currentEntries = [];
foreach($this->Entries as $entry){
$obj = new StdClass();
if(is_a($entry, 'Ebook')){
$obj->Updated = $entry->ModifiedTimestamp->format('Y-m-d\TH:i:s\Z');
$obj->Id = SITE_URL . $entry->Url;
}
else{
$obj->Updated = $entry->Updated !== null ? $entry->Updated->format('Y-m-d\TH:i:s\Z') : '';
$obj->Id = $entry->Id;
}
$currentEntries[] = $obj;
}
$oldEntries = [];
try{
$xml = new SimpleXMLElement(str_replace('xmlns=', 'ns=', file_get_contents($path)));
foreach($xml->xpath('/feed/entry') ?: [] as $entry){
$obj = new StdClass();
$obj->Updated = $entry->updated;
$obj->Id = $entry->id;
$oldEntries[] = $obj;
}
}
catch(Exception $ex){
// Invalid XML
return true;
}
return $currentEntries != $oldEntries;
}
public function Save(): void{
parent::Save();
} }
} }

View file

@ -12,7 +12,7 @@ class Feed{
public $Stylesheet = null; public $Stylesheet = null;
protected $XmlString = null; protected $XmlString = null;
public function __construct(string $url, string $title, string $path, array $entries){ public function __construct(string $title, string $url, string $path, array $entries){
$this->Url = $url; $this->Url = $url;
$this->Title = $title; $this->Title = $title;
$this->Path = $path; $this->Path = $path;
@ -38,7 +38,16 @@ class Feed{
return ''; return '';
} }
function Save(): void{ public function SaveIfChanged(): void{
// Did we actually update the feed? If so, write to file and update the index
if($this->HasChanged($this->Path)){
// Files don't match, save the file
$this->Updated = new DateTime();
$this->Save();
}
}
public function Save(): void{
$feed = $this->GetXmlString(); $feed = $this->GetXmlString();
file_put_contents($this->Path, $feed); file_put_contents($this->Path, $feed);

View file

@ -33,10 +33,10 @@ class Formatter{
} }
public static function ToPlainText(?string $text): string{ public static function ToPlainText(?string $text): string{
return htmlspecialchars(trim($text), ENT_QUOTES, 'UTF-8'); return htmlspecialchars(trim($text), ENT_QUOTES, 'utf-8');
} }
public static function ToPlainXmlText(?string $text): string{ public static function ToPlainXmlText(?string $text): string{
return htmlspecialchars(trim($text), ENT_QUOTES|ENT_XML1, 'UTF-8'); return htmlspecialchars(trim($text), ENT_QUOTES|ENT_XML1, 'utf-8');
} }
} }

View file

@ -4,21 +4,16 @@ use Safe\DateTime;
class OpdsAcquisitionFeed extends OpdsFeed{ class OpdsAcquisitionFeed extends OpdsFeed{
public $IsCrawlable; public $IsCrawlable;
public function __construct(string $url, string $title, string $path, array $entries, ?OpdsNavigationFeed $parent, bool $isCrawlable = false){ public function __construct(string $title, string $subtitle, string $url, string $path, array $entries, ?OpdsNavigationFeed $parent, bool $isCrawlable = false){
parent::__construct($url, $title, $path, $entries, $parent); parent::__construct($title, $subtitle, $url, $path, $entries, $parent);
$this->IsCrawlable = $isCrawlable; $this->IsCrawlable = $isCrawlable;
} }
protected function GetXmlString(): string{ protected function GetXmlString(): string{
if($this->XmlString === null){ if($this->XmlString === null){
$this->XmlString = $this->CleanXmlString(Template::OpdsAcquisitionFeed(['id' => $this->Id, 'url' => $this->Url, 'title' => $this->Title, 'parentUrl' => $this->Parent ? $this->Parent->Url : null, 'updatedTimestamp' => $this->Updated, 'isCrawlable' => $this->IsCrawlable, 'entries' => $this->Entries])); $this->XmlString = $this->CleanXmlString(Template::OpdsAcquisitionFeed(['id' => $this->Id, 'url' => $this->Url, 'title' => $this->Title, 'parentUrl' => $this->Parent ? $this->Parent->Url : null, 'updatedTimestamp' => $this->Updated, 'isCrawlable' => $this->IsCrawlable, 'subtitle' => $this->Subtitle, 'entries' => $this->Entries]));
} }
return $this->XmlString; return $this->XmlString;
} }
public function Save(): void{
$this->Updated = new DateTime();
$this->SaveIfChanged();
}
} }

View file

@ -2,23 +2,25 @@
use function Safe\file_put_contents; use function Safe\file_put_contents;
class OpdsFeed extends AtomFeed{ class OpdsFeed extends AtomFeed{
public $Updated = null;
public $Parent = null; // OpdsNavigationFeed class public $Parent = null; // OpdsNavigationFeed class
public function __construct(string $url, string $title, string $path, array $entries, ?OpdsNavigationFeed $parent){ public function __construct(string $title, string $subtitle, string $url, string $path, array $entries, ?OpdsNavigationFeed $parent){
parent::__construct($url, $title, $path, $entries); parent::__construct($title, $subtitle, $url, $path, $entries);
$this->Parent = $parent; $this->Parent = $parent;
$this->Stylesheet = '/opds/style'; $this->Stylesheet = '/opds/style';
} }
protected function SaveUpdatedTimestamp(string $entryId, DateTime $updatedTimestamp): void{ protected function SaveUpdatedTimestamp(string $entryId, DateTime $updatedTimestamp): void{
// Only save the updated timestamp for the given entry ID in this file // Only save the updated timestamp for the given entry ID in this file
foreach($this->Entries as $entry){ foreach($this->Entries as $entry){
if($entry->Id == $entryId){ if(is_a($entry, 'OpdsNavigationEntry')){
if($entry->Id == SITE_URL . $entryId){
$entry->Updated = $updatedTimestamp; $entry->Updated = $updatedTimestamp;
} }
} }
}
$this->Updated = $updatedTimestamp;
$this->XmlString = null; $this->XmlString = null;
file_put_contents($this->Path, $this->GetXmlString()); file_put_contents($this->Path, $this->GetXmlString());
@ -29,17 +31,19 @@ class OpdsFeed extends AtomFeed{
} }
} }
protected function SaveIfChanged(): void{ public function SaveIfChanged(): void{
// Did we actually update the feed? If so, write to file and update the index // Did we actually update the feed? If so, write to file and update the index
if($this->HasChanged($this->Path)){ if($this->HasChanged($this->Path)){
// Files don't match, save the file and update the parent navigation feed with the last updated timestamp // Files don't match, save the file and update the parent navigation feed with the last updated timestamp
$this->Updated = new DateTime();
if($this->Parent !== null){ if($this->Parent !== null){
$this->Parent->SaveUpdatedTimestamp($this->Id, $this->Updated); $this->Parent->SaveUpdatedTimestamp($this->Id, $this->Updated);
} }
// Save our own file // Save our own file
parent::Save(); $this->Save();
} }
} }
} }

View file

@ -8,8 +8,8 @@ class OpdsNavigationEntry{
public $Description; public $Description;
public $Title; public $Title;
public function __construct(string $url, string $rel, string $type, ?DateTime $updated, string $title, string $description){ public function __construct(string $title, string $description, string $url, ?DateTime $updated, string $rel, string $type){
$this->Id = 'https://standardebooks.org' . $url; $this->Id = SITE_URL . $url;
$this->Url = $url; $this->Url = $url;
$this->Rel = $rel; $this->Rel = $rel;
$this->Type = $type; $this->Type = $type;

View file

@ -4,10 +4,8 @@ use Safe\DateTime;
use function Safe\file_get_contents; use function Safe\file_get_contents;
class OpdsNavigationFeed extends OpdsFeed{ class OpdsNavigationFeed extends OpdsFeed{
public function __construct(string $url, string $title, string $path, array $entries, ?OpdsNavigationFeed $parent){ public function __construct(string $title, string $subtitle, string $url, string $path, array $entries, ?OpdsNavigationFeed $parent){
parent::__construct($url, $title, $path, $entries, $parent); parent::__construct($title, $subtitle, $url, $path, $entries, $parent);
$this->Entries = $entries;
// If the file already exists, try to fill in the existing updated timestamps from the file. // If the file already exists, try to fill in the existing updated timestamps from the file.
// That way, if the file has changed, we only update the changed entry, // That way, if the file has changed, we only update the changed entry,
@ -33,14 +31,9 @@ class OpdsNavigationFeed extends OpdsFeed{
protected function GetXmlString(): string{ protected function GetXmlString(): string{
if($this->XmlString === null){ if($this->XmlString === null){
$this->XmlString = $this->CleanXmlString(Template::OpdsNavigationFeed(['id' => $this->Id, 'url' => $this->Url, 'title' => $this->Title, 'parentUrl' => $this->Parent ? $this->Parent->Url : null, 'updatedTimestamp' => $this->Updated, 'entries' => $this->Entries])); $this->XmlString = $this->CleanXmlString(Template::OpdsNavigationFeed(['id' => $this->Id, 'url' => $this->Url, 'title' => $this->Title, 'parentUrl' => $this->Parent ? $this->Parent->Url : null, 'updatedTimestamp' => $this->Updated, 'subtitle' => $this->Subtitle, 'entries' => $this->Entries]));
} }
return $this->XmlString; return $this->XmlString;
} }
public function Save(): void{
$this->Updated = new DateTime();
$this->SaveIfChanged();
}
} }

View file

@ -2,8 +2,8 @@
class RssFeed extends Feed{ class RssFeed extends Feed{
public $Description; public $Description;
public function __construct(string $url, string $title, string $path, string $description, array $entries){ public function __construct(string $title, string $description, string $url, string $path, array $entries){
parent::__construct($url, $title, $path, $entries); parent::__construct($title, $url, $path, $entries);
$this->Description = $description; $this->Description = $description;
$this->Stylesheet = '/rss/style'; $this->Stylesheet = '/rss/style';
} }
@ -17,4 +17,44 @@ class RssFeed extends Feed{
return $this->XmlString; return $this->XmlString;
} }
protected function HasChanged(string $path): bool{
// RSS doesn't have information about when an item was updated,
// only when it was first published. So, we approximate on whether the feed
// has changed by looking at the length of the enclosed file.
// This can sometimes be the same even if the file has changed, but most of the time
// it also changes.
if(!is_file($path)){
return true;
}
$currentEntries = [];
foreach($this->Entries as $entry){
$obj = new StdClass();
$obj->Size = (string)filesize(WEB_ROOT . $entry->EpubUrl);
$obj->Id = preg_replace('/^url:/ius', '', $entry->Identifier);
$currentEntries[] = $obj;
}
$oldEntries = [];
try{
$xml = new SimpleXMLElement(str_replace('xmlns=', 'ns=', file_get_contents($path)));
$xml->registerXPathNamespace('atom', 'http://www.w3.org/2005/Atom');
foreach($xml->xpath('/rss/channel/item') ?: [] as $entry){
$obj = new StdClass();
foreach($entry->xpath('enclosure') ?: [] as $enclosure){
$obj->Size = (string)$enclosure['length'];
}
$obj->Id = (string)$entry->guid;
$oldEntries[] = $obj;
}
}
catch(Exception $ex){
// Invalid XML
return true;
}
return $currentEntries != $oldEntries;
}
} }

View file

@ -390,12 +390,9 @@ if [ "${feeds}" = "true" ]; then
"${scriptsDir}/generate-feeds" --webroot "${webRoot}" --weburl "${webUrl}" "${scriptsDir}/generate-feeds" --webroot "${webRoot}" --weburl "${webUrl}"
sudo chown --recursive se:committers "${webRoot}/www/opds/"* sudo chown --recursive se:committers "${webRoot}"/www/{atom,rss,opds}/{*.xml,subjects}
sudo chmod --recursive 664 "${webRoot}/www/opds/"*.xml sudo chmod --recursive 664 "${webRoot}"/www/{atom,rss,opds}/{*.xml,subjects/*.xml}
sudo chmod --recursive 664 "${webRoot}/www/opds/"*/*.xml sudo chmod 775 "${webRoot}"/www/{atom,rss,opds}/subjects
sudo chown --recursive se:committers "${webRoot}/www/rss/"*
sudo chmod --recursive 664 "${webRoot}/www/rss/"*.xml
sudo chmod 775 "${webRoot}/www/opds/subjects"
if [ "${verbose}" = "true" ]; then if [ "${verbose}" = "true" ]; then
printf "Done.\n" printf "Done.\n"

View file

@ -4,6 +4,7 @@ require_once('/standardebooks.org/web/lib/Core.php');
use function Safe\krsort; use function Safe\krsort;
use function Safe\getopt; use function Safe\getopt;
use function Safe\mkdir;
use function Safe\preg_replace; use function Safe\preg_replace;
use function Safe\sort; use function Safe\sort;
@ -19,6 +20,18 @@ $subjects = [];
$ebooksBySubject = []; $ebooksBySubject = [];
$ebooksPerNewestEbooksFeed = 30; $ebooksPerNewestEbooksFeed = 30;
if(!is_dir(WEB_ROOT . '/opds/subjects')){
mkdir(WEB_ROOT . '/opds/subjects');
}
if(!is_dir(WEB_ROOT . '/rss/subjects')){
mkdir(WEB_ROOT . '/rss/subjects');
}
if(!is_dir(WEB_ROOT . '/atom/subjects')){
mkdir(WEB_ROOT . '/atom/subjects');
}
// Iterate over all ebooks to build the various feeds // Iterate over all ebooks to build the various feeds
foreach($contentFiles as $path){ foreach($contentFiles as $path){
if($path == '') if($path == '')
@ -50,73 +63,97 @@ foreach($contentFiles as $path){
} }
} }
krsort($newestEbooks);
$newestEbooks = array_slice($newestEbooks, 0, $ebooksPerNewestEbooksFeed);
$now = new DateTime(); $now = new DateTime();
// Create OPDS feeds // Create OPDS feeds
$opdsRootEntries = [ $opdsRootEntries = [
new OpdsNavigationEntry( new OpdsNavigationEntry(
'/opds/new-releases',
'http://opds-spec.org/sort/new',
'acquisition',
$now,
'Newest ' . number_format($ebooksPerNewestEbooksFeed) . ' Standard Ebooks', 'Newest ' . number_format($ebooksPerNewestEbooksFeed) . ' Standard Ebooks',
'A list of the ' . number_format($ebooksPerNewestEbooksFeed) . ' newest Standard Ebooks, most-recently-released first.'), 'The ' . number_format($ebooksPerNewestEbooksFeed) . ' latest Standard Ebooks, most-recently-released first.',
new OpdsNavigationEntry( '/opds/new-releases',
'/opds/subjects',
'subsection',
'navigation',
$now, $now,
'http://opds-spec.org/sort/new',
'acquisition'
),
new OpdsNavigationEntry(
'Standard Ebooks by Subject', 'Standard Ebooks by Subject',
'Browse Standard Ebooks by subject.'), 'Browse Standard Ebooks by subject.',
new OpdsNavigationEntry( '/opds/subjects',
'/opds/all',
'http://opds-spec.org/crawlable',
'acquisition',
$now, $now,
'subsection',
'navigation'),
new OpdsNavigationEntry(
'All Standard Ebooks', 'All Standard Ebooks',
'A list of all Standard Ebooks, most-recently-updated first. This is a Complete Acquisition Feed as defined in OPDS 1.2 §2.5.') 'All Standard Ebooks, most-recently-updated first. This is a Complete Acquisition Feed as defined in OPDS 1.2 §2.5.',
'/opds/all',
$now,
'http://opds-spec.org/crawlable',
'acquisition')
]; ];
$opdsRoot = new OpdsNavigationFeed('/opds', 'Standard Ebooks', WEB_ROOT . '/opds/index.xml', $opdsRootEntries, null); $opdsRoot = new OpdsNavigationFeed('Standard Ebooks', 'The navigation root for the Standard Ebooks OPDS feed.', '/opds', WEB_ROOT . '/opds/index.xml', $opdsRootEntries, null);
$opdsRoot->Save(); $opdsRoot->SaveIfChanged();
// Create the subjects navigation document // Create the subjects navigation document
sort($subjects); sort($subjects);
$subjectNavigationEntries = []; $subjectNavigationEntries = [];
foreach($subjects as $subject){ foreach($subjects as $subject){
$summary = number_format(sizeof($ebooksBySubject[$subject])) . ' Standard Ebook'; $subjectNavigationEntries[] = new OpdsNavigationEntry($subject, 'Standard Ebooks tagged with “' . strtolower($subject) . ',” most-recently-released first.', '/opds/subjects/' . Formatter::MakeUrlSafe($subject), $now, 'subsection', 'navigation');
if(sizeof($ebooksBySubject[$subject]) != 1){
$summary .= 's';
} }
$summary .= ' tagged with “' . strtolower($subject) . ',” most-recently-released first.'; $subjectsFeed = new OpdsNavigationFeed('Standard Ebooks by Subject', 'Browse Standard Ebooks by subject.', '/opds/subjects', WEB_ROOT . '/opds/subjects/index.xml', $subjectNavigationEntries, $opdsRoot);
$subjectsFeed->Subtitle = 'Browse Standard Ebooks by subject.';
// We leave the updated timestamp blank, as it will be filled in when we generate the individual feeds $subjectsFeed->SaveIfChanged();
$subjectNavigationEntries[] = new OpdsNavigationEntry('/opds/subjects/' . Formatter::MakeUrlSafe($subject), 'subsection', 'navigation', $now, $subject, $summary);
}
$subjectsFeed = new OpdsNavigationFeed('/opds/subjects', 'Standard Ebooks by Subject', WEB_ROOT . '/opds/subjects/index.xml', $subjectNavigationEntries, $opdsRoot);
$subjectsFeed->Save();
// Now generate each individual subject feed // Now generate each individual subject feed
foreach($ebooksBySubject as $subject => $ebooks){ foreach($subjectNavigationEntries as $subjectNavigationEntry){
krsort($ebooks); krsort($ebooksBySubject[$subjectNavigationEntry->Title]);
$subjectFeed = new OpdsAcquisitionFeed('/opds/subjects/' . Formatter::MakeUrlSafe((string)$subject), (string)$subject, WEB_ROOT . '/opds/subjects/' . Formatter::MakeUrlSafe((string)$subject) . '.xml', $ebooks, $subjectsFeed); $subjectFeed = new OpdsAcquisitionFeed($subjectNavigationEntry->Title . ' Ebooks', $subjectNavigationEntry->Description, '/opds/subjects/' . Formatter::MakeUrlSafe($subjectNavigationEntry->Title), WEB_ROOT . '/opds/subjects/' . Formatter::MakeUrlSafe($subjectNavigationEntry->Title) . '.xml', $ebooksBySubject[$subjectNavigationEntry->Title], $subjectsFeed);
$subjectFeed->Save(); $subjectFeed->SaveIfChanged();
} }
// Create the 'all' feed // Create the 'all' feed
krsort($allEbooks); krsort($allEbooks);
$allFeed = new OpdsAcquisitionFeed('/opds/all', 'All Standard Ebooks', WEB_ROOT . '/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.', '/opds/all', WEB_ROOT . '/opds/all.xml', $allEbooks, $opdsRoot, true);
$allFeed->Save(); $allFeed->SaveIfChanged();
// Create the 'newest' feed // Create the 'newest' feed
krsort($newestEbooks); $newestFeed = new OpdsAcquisitionFeed('Newest ' . number_format($ebooksPerNewestEbooksFeed) . ' Standard Ebooks', 'The ' . number_format($ebooksPerNewestEbooksFeed) . ' latest Standard Ebooks, most-recently-released first.', '/opds/new-releases', WEB_ROOT . '/opds/new-releases.xml', $newestEbooks, $opdsRoot);
$newestEbooks = array_slice($newestEbooks, 0, $ebooksPerNewestEbooksFeed); $newestFeed->SaveIfChanged();
$newestFeed = new OpdsAcquisitionFeed('/opds/new-releases', 'Newest ' . number_format($ebooksPerNewestEbooksFeed) . ' Standard Ebooks', WEB_ROOT . '/opds/new-releases.xml', $newestEbooks, $opdsRoot);
$newestFeed->Save();
// Now create RSS feeds // Now create RSS feeds
// Create the 'newest' feed // Create the 'newest' feed
$newestFeed = new RssFeed('/rss/new-releases', 'Newest ' . number_format($ebooksPerNewestEbooksFeed) . ' Standard Ebooks', WEB_ROOT . '/rss/new-releases.xml', 'A list of the ' . number_format($ebooksPerNewestEbooksFeed) . ' latest Standard Ebooks ebook releases, most-recently-released first.', $newestEbooks); $newestRssFeed = new RssFeed('Standard Ebooks - Newest Ebooks', 'The ' . number_format($ebooksPerNewestEbooksFeed) . ' latest Standard Ebooks, most-recently-released first.', '/rss/new-releases', WEB_ROOT . '/rss/new-releases.xml', $newestEbooks);
$newestFeed->Save(); $newestRssFeed->SaveIfChanged();
// Create the 'all' feed
$allRssFeed = new RssFeed('Standard Ebooks - All Ebooks', 'All Standard Ebooks, most-recently-released first.', '/rss/all', WEB_ROOT . '/rss/all.xml', $allEbooks);
$allRssFeed->SaveIfChanged();
// Generate each individual subject feed
foreach($ebooksBySubject as $subject => $ebooks){
krsort($ebooks);
$subjectRssFeed = new RssFeed('Standard Ebooks - ' . (string)$subject . ' Ebooks', 'Standard Ebooks tagged with “' . strtolower($subject) . ',” most-recently-released first.', '/rss/subjects/' . Formatter::MakeUrlSafe((string)$subject), WEB_ROOT . '/rss/subjects/' . Formatter::MakeUrlSafe((string)$subject) . '.xml', $ebooks);
$subjectRssFeed->SaveIfChanged();
}
// Now create the Atom feeds
// Create the 'newest' feed
$newestAtomFeed = new AtomFeed('Standard Ebooks - Newest Ebooks', 'The ' . number_format($ebooksPerNewestEbooksFeed) . ' latest Standard Ebooks, most-recently-released first.', '/atom/new-releases', WEB_ROOT . '/atom/new-releases.xml', $newestEbooks);
$newestAtomFeed->SaveIfChanged();
// Create the 'all' feed
$allAtomFeed = new AtomFeed('Standard Ebooks - All Ebooks', 'All Standard Ebooks, most-recently-released first.', '/atom/all', WEB_ROOT . '/atom/all.xml', $allEbooks);
$allAtomFeed->SaveIfChanged();
// Generate each individual subject feed
foreach($ebooksBySubject as $subject => $ebooks){
krsort($ebooks);
$subjectAtomFeed = new AtomFeed('Standard Ebooks - ' . (string)$subject . ' Ebooks', 'Standard Ebooks tagged with “' . strtolower($subject) . ',” most-recently-released first.', '/atom/subjects/' . Formatter::MakeUrlSafe((string)$subject), WEB_ROOT . '/atom/subjects/' . Formatter::MakeUrlSafe((string)$subject) . '.xml', $ebooks);
$subjectAtomFeed->SaveIfChanged();
}
?> ?>

50
templates/AtomFeed.php Normal file
View file

@ -0,0 +1,50 @@
<?
$subtitle = $subtitle ?? null;
// Note that the XSL stylesheet gets stripped during `se clean` when we generate the feed.
// `se clean` will also start adding empty namespaces everywhere if we include the stylesheet declaration first.
// We have to add it programmatically when saving the feed file.
print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
<id><?= SITE_URL . Formatter::ToPlainXmlText($id) ?></id>
<link href="<?= SITE_URL . Formatter::ToPlainXmlText($url) ?>" rel="self" type="application/atom+xml"/>
<title><?= Formatter::ToPlainXmlText($title) ?></title>
<? if($subtitle !== null){ ?><subtitle><?= Formatter::ToPlainXmlText($subtitle) ?></subtitle><? } ?>
<icon><?= SITE_URL ?>/images/logo.png</icon>
<updated><?= $updatedTimestamp->format('Y-m-d\TH:i:s\Z') ?></updated>
<author>
<name>Standard Ebooks</name>
<uri><?= SITE_URL ?></uri>
</author>
<? foreach($entries as $entry){ ?>
<entry>
<id><?= SITE_URL . $entry->Url ?></id>
<title><?= Formatter::ToPlainXmlText($entry->Title) ?></title>
<? foreach($entry->Authors as $author){ ?>
<author>
<name><?= Formatter::ToPlainXmlText($author->Name) ?></name>
<uri><?= SITE_URL . Formatter::ToPlainXmlText($entry->AuthorsUrl) ?></uri>
</author>
<? } ?>
<published><?= $entry->Timestamp->format('Y-m-d\TH:i:s\Z') ?></published>
<updated><?= $entry->ModifiedTimestamp->format('Y-m-d\TH:i:s\Z') ?></updated>
<rights>Public domain in the United States. Users located outside of the United States must check their local laws before using this ebook. Original content released to the public domain via the Creative Commons CC0 1.0 Universal Public Domain Dedication.</rights>
<summary type="text"><?= Formatter::ToPlainXmlText($entry->Description) ?></summary>
<content type="html"><?= Formatter::ToPlainXmlText($entry->LongDescription) ?></content>
<? foreach($entry->LocTags as $subject){ ?>
<category scheme="http://purl.org/dc/terms/LCSH" term="<?= Formatter::ToPlainXmlText($subject) ?>"/>
<? } ?>
<? foreach($entry->Tags as $subject){ ?>
<category scheme="https://standardebooks.org/vocab/subjects" term="<?= Formatter::ToPlainXmlText($subject->Name) ?>"/>
<? } ?>
<media:thumbnail url="<?= SITE_URL . $entry->Url ?>/downloads/cover-thumbnail.jpg" height="525" width="350"/>
<link href="<?= SITE_URL . $entry->Url ?>" rel="alternate" title="This ebooks page at Standard Ebooks" type="application/xhtml+xml"/>
<? if(file_exists(WEB_ROOT . $entry->EpubUrl)){ ?><link href="<?= SITE_URL . $entry->EpubUrl ?>" length="<?= filesize(WEB_ROOT . $entry->EpubUrl) ?>" rel="enclosure" title="Recommended compatible epub" type="application/epub+zip" /><? } ?>
<? if(file_exists(WEB_ROOT . $entry->AdvancedEpubUrl)){ ?><link href="<?= SITE_URL . $entry->AdvancedEpubUrl ?>" length="<?= filesize(WEB_ROOT . $entry->AdvancedEpubUrl) ?>" rel="enclosure" title="Advanced epub" type="application/epub+zip" /><? } ?>
<? if(file_exists(WEB_ROOT . $entry->KepubUrl)){ ?><link href="<?= SITE_URL . $entry->KepubUrl ?>" length="<?= filesize(WEB_ROOT . $entry->KepubUrl) ?>" rel="enclosure" title="Kobo Kepub epub" type="application/kepub+zip" /><? } ?>
<? if(file_exists(WEB_ROOT . $entry->Azw3Url)){ ?><link href="<?= SITE_URL . $entry->Azw3Url ?>" length="<?= filesize(WEB_ROOT . $entry->Azw3Url) ?>" rel="enclosure" title="Amazon Kindle azw3" type="application/x-mobipocket-ebook" /><? } ?>
<? if(file_exists(WEB_ROOT . $entry->TextSinglePageUrl)){ ?><link href="<?= SITE_URL . $entry->TextSinglePageUrl ?>" length="<?= filesize(WEB_ROOT . $entry->TextSinglePageUrl) ?>" rel="enclosure" title="XHTML" type="application/xhtml+xml" /><? } ?>
</entry>
<? } ?>
</feed>

View file

@ -1,37 +1,36 @@
<entry> <entry>
<id><?= SITE_URL . $ebook->Url ?></id> <id><?= SITE_URL . $entry->Url ?></id>
<title><?= Formatter::ToPlainXmlText($ebook->Title) ?></title> <dc:identifier><?= Formatter::ToPlainXmlText($entry->Identifier) ?></dc:identifier>
<? foreach($ebook->Authors as $author){ ?> <title><?= Formatter::ToPlainXmlText($entry->Title) ?></title>
<? foreach($entry->Authors as $author){ ?>
<author> <author>
<name><?= Formatter::ToPlainXmlText($author->Name) ?></name> <name><?= Formatter::ToPlainXmlText($author->Name) ?></name>
<uri><?= SITE_URL . htmlspecialchars($ebook->AuthorsUrl, ENT_QUOTES|ENT_XML1, 'utf-8') ?></uri> <uri><?= SITE_URL . Formatter::ToPlainXmlText($entry->AuthorsUrl) ?></uri>
<? if($author->FullName !== null){ ?><schema:alternateName><?= Formatter::ToPlainXmlText($author->FullName) ?></schema:alternateName><? } ?> <? if($author->FullName !== null){ ?><schema:alternateName><?= Formatter::ToPlainXmlText($author->FullName) ?></schema:alternateName><? } ?>
<? if($author->WikipediaUrl !== null){ ?><schema:sameAs><?= Formatter::ToPlainXmlText($author->WikipediaUrl) ?></schema:sameAs><? } ?> <? if($author->WikipediaUrl !== null){ ?><schema:sameAs><?= Formatter::ToPlainXmlText($author->WikipediaUrl) ?></schema:sameAs><? } ?>
<? if($author->NacoafUrl !== null){ ?><schema:sameAs><?= Formatter::ToPlainXmlText($author->NacoafUrl) ?></schema:sameAs><? } ?> <? if($author->NacoafUrl !== null){ ?><schema:sameAs><?= Formatter::ToPlainXmlText($author->NacoafUrl) ?></schema:sameAs><? } ?>
</author> </author>
<? } ?> <? } ?>
<dc:issued><?= $ebook->Timestamp->format('Y-m-d\TH:i:s\Z') ?></dc:issued> <published><?= $entry->Timestamp->format('Y-m-d\TH:i:s\Z') ?></published>
<updated><?= $ebook->ModifiedTimestamp->format('Y-m-d\TH:i:s\Z') ?></updated> <dc:issued><?= $entry->Timestamp->format('Y-m-d\TH:i:s\Z') ?></dc:issued>
<dc:language><?= Formatter::ToPlainXmlText($ebook->Language) ?></dc:language> <updated><?= $entry->ModifiedTimestamp->format('Y-m-d\TH:i:s\Z') ?></updated>
<dc:language><?= Formatter::ToPlainXmlText($entry->Language) ?></dc:language>
<dc:publisher>Standard Ebooks</dc:publisher> <dc:publisher>Standard Ebooks</dc:publisher>
<? foreach($ebook->Sources as $source){ ?>
<dc:source><?= Formatter::ToPlainXmlText($source->Url) ?></dc:source>
<? } ?>
<rights>Public domain in the United States. Users located outside of the United States must check their local laws before using this ebook. Original content released to the public domain via the Creative Commons CC0 1.0 Universal Public Domain Dedication.</rights> <rights>Public domain in the United States. Users located outside of the United States must check their local laws before using this ebook. Original content released to the public domain via the Creative Commons CC0 1.0 Universal Public Domain Dedication.</rights>
<summary type="text"><?= Formatter::ToPlainXmlText($ebook->Description) ?></summary> <summary type="text"><?= Formatter::ToPlainXmlText($entry->Description) ?></summary>
<content type="text/html"><?= $ebook->LongDescription ?></content> <content type="html"><?= Formatter::ToPlainXmlText($entry->LongDescription) ?></content>
<? foreach($ebook->LocTags as $subject){ ?> <? foreach($entry->LocTags as $subject){ ?>
<category scheme="http://purl.org/dc/terms/LCSH" term="<?= Formatter::ToPlainXmlText($subject) ?>"/> <category scheme="http://purl.org/dc/terms/LCSH" term="<?= Formatter::ToPlainXmlText($subject) ?>"/>
<? } ?> <? } ?>
<? foreach($ebook->Tags as $subject){ ?> <? foreach($entry->Tags as $subject){ ?>
<category scheme="https://standardebooks.org/vocab/subjects" term="<?= Formatter::ToPlainXmlText($subject->Name) ?>"/> <category scheme="https://standardebooks.org/vocab/subjects" term="<?= Formatter::ToPlainXmlText($subject->Name) ?>"/>
<? } ?> <? } ?>
<link href="<?= SITE_URL . $ebook->Url ?>/downloads/cover.jpg" rel="http://opds-spec.org/image" type="image/jpeg"/> <link href="<?= SITE_URL . $entry->Url ?>/downloads/cover.jpg" rel="http://opds-spec.org/image" type="image/jpeg"/>
<link href="<?= SITE_URL . $ebook->Url ?>/downloads/cover-thumbnail.jpg" rel="http://opds-spec.org/image/thumbnail" type="image/jpeg"/> <link href="<?= SITE_URL . $entry->Url ?>/downloads/cover-thumbnail.jpg" rel="http://opds-spec.org/image/thumbnail" type="image/jpeg"/>
<link href="<?= SITE_URL . $ebook->Url ?>" rel="related" title="This ebooks page at Standard Ebooks" type="text/html"/> <link href="<?= SITE_URL . $entry->Url ?>" rel="related" title="This ebooks page at Standard Ebooks" type="application/xhtml+xml"/>
<link href="<?= SITE_URL . $ebook->EpubUrl ?>" rel="http://opds-spec.org/acquisition/open-access" title="Recommended compatible epub" type="application/epub+zip" /> <? if(file_exists(WEB_ROOT . $entry->EpubUrl)){ ?><link href="<?= SITE_URL . $entry->EpubUrl ?>" length="<?= filesize(WEB_ROOT . $entry->EpubUrl) ?>" rel="http://opds-spec.org/acquisition/open-access" title="Recommended compatible epub" type="application/epub+zip" /><? } ?>
<link href="<?= SITE_URL . $ebook->AdvancedEpubUrl ?>" rel="http://opds-spec.org/acquisition/open-access" title="Advanced epub" type="application/epub+zip" /> <? if(file_exists(WEB_ROOT . $entry->AdvancedEpubUrl)){ ?><link href="<?= SITE_URL . $entry->AdvancedEpubUrl ?>" length="<?= filesize(WEB_ROOT . $entry->AdvancedEpubUrl) ?>" rel="http://opds-spec.org/acquisition/open-access" title="Advanced epub" type="application/epub+zip" /><? } ?>
<link href="<?= SITE_URL . $ebook->KepubUrl ?>" rel="http://opds-spec.org/acquisition/open-access" title="Kobo Kepub epub" type="application/kepub+zip" /> <? if(file_exists(WEB_ROOT . $entry->KepubUrl)){ ?><link href="<?= SITE_URL . $entry->KepubUrl ?>" length="<?= filesize(WEB_ROOT . $entry->KepubUrl) ?>" rel="http://opds-spec.org/acquisition/open-access" title="Kobo Kepub epub" type="application/kepub+zip" /><? } ?>
<link href="<?= SITE_URL . $ebook->Azw3Url ?>" rel="http://opds-spec.org/acquisition/open-access" title="Amazon Kindle azw3" type="application/x-mobipocket-ebook" /> <? if(file_exists(WEB_ROOT . $entry->Azw3Url)){ ?><link href="<?= SITE_URL . $entry->Azw3Url ?>" length="<?= filesize(WEB_ROOT . $entry->Azw3Url) ?>" rel="http://opds-spec.org/acquisition/open-access" title="Amazon Kindle azw3" type="application/x-mobipocket-ebook" /><? } ?>
<link href="<?= SITE_URL . $ebook->TextSinglePageUrl ?>" rel="http://opds-spec.org/acquisition/open-access" title="XHTML" type="application/xhtml+xml" /> <? if(file_exists(WEB_ROOT . $entry->TextSinglePageUrl)){ ?><link href="<?= SITE_URL . $entry->TextSinglePageUrl ?>" length="<?= filesize(WEB_ROOT . $entry->TextSinglePageUrl) ?>" rel="http://opds-spec.org/acquisition/open-access" title="XHTML" type="application/xhtml+xml" /><? } ?>
</entry> </entry>

View file

@ -9,6 +9,7 @@
*/ */
$isCrawlable = $isCrawlable ?? false; $isCrawlable = $isCrawlable ?? false;
$subtitle = $subtitle ?? null;
// Note that the XSL stylesheet gets stripped during `se clean` when we generate the feed. // Note that the XSL stylesheet gets stripped during `se clean` when we generate the feed.
// `se clean` will also start adding empty namespaces everywhere if we include the stylesheet declaration first. // `se clean` will also start adding empty namespaces everywhere if we include the stylesheet declaration first.
@ -16,14 +17,14 @@ $isCrawlable = $isCrawlable ?? false;
print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
?> ?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:schema="http://schema.org/"<? if($isCrawlable){ ?> xmlns:fh="http://purl.org/syndication/history/1.0"<? } ?>> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:schema="http://schema.org/"<? if($isCrawlable){ ?> xmlns:fh="http://purl.org/syndication/history/1.0"<? } ?>>
<id><?= Formatter::ToPlainXmlText($id) ?></id> <id><?= SITE_URL . Formatter::ToPlainXmlText($id) ?></id>
<link href="<?= SITE_URL . htmlspecialchars($url, ENT_QUOTES|ENT_XML1, 'utf-8') ?>" rel="self" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/> <link href="<?= SITE_URL . Formatter::ToPlainXmlText($url) ?>" rel="self" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<link href="<?= SITE_URL ?>/opds" rel="start" type="application/atom+xml;profile=opds-catalog;kind=navigation"/> <link href="<?= SITE_URL ?>/opds" rel="start" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link href="<?= SITE_URL ?><?= Formatter::ToPlainXmlText($parentUrl) ?>" rel="up" type="application/atom+xml;profile=opds-catalog;kind=navigation"/> <link href="<?= SITE_URL ?><?= Formatter::ToPlainXmlText($parentUrl) ?>" rel="up" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link href="<?= SITE_URL ?>/opds/all" rel="http://opds-spec.org/crawlable" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/> <link href="<?= SITE_URL ?>/opds/all" rel="http://opds-spec.org/crawlable" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<link href="<?= SITE_URL ?>/ebooks/opensearch" rel="search" type="application/opensearchdescription+xml"/> <link href="<?= SITE_URL ?>/ebooks/opensearch" rel="search" type="application/opensearchdescription+xml"/>
<title><?= Formatter::ToPlainXmlText($title) ?></title> <title><?= Formatter::ToPlainXmlText($title) ?></title>
<subtitle>Free and liberated ebooks, carefully produced for the true book lover.</subtitle> <? if($subtitle !== null){ ?><subtitle><?= Formatter::ToPlainXmlText($subtitle) ?></subtitle><? } ?>
<icon><?= SITE_URL ?>/images/logo.png</icon> <icon><?= SITE_URL ?>/images/logo.png</icon>
<updated><?= $updatedTimestamp->format('Y-m-d\TH:i:s\Z') ?></updated> <updated><?= $updatedTimestamp->format('Y-m-d\TH:i:s\Z') ?></updated>
<? if($isCrawlable){ ?><fh:complete/><? } ?> <? if($isCrawlable){ ?><fh:complete/><? } ?>
@ -31,7 +32,7 @@ print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
<name>Standard Ebooks</name> <name>Standard Ebooks</name>
<uri><?= SITE_URL ?></uri> <uri><?= SITE_URL ?></uri>
</author> </author>
<? foreach($entries as $ebook){ ?> <? foreach($entries as $entry){ ?>
<?= Template::OpdsAcquisitionEntry(['ebook' => $ebook]) ?> <?= Template::OpdsAcquisitionEntry(['entry' => $entry]) ?>
<? } ?> <? } ?>
</feed> </feed>

View file

@ -1,18 +1,21 @@
<? <?
$subtitle = $subtitle ?? null;
// Note that the XSL stylesheet gets stripped during `se clean` when we generate the feed. // Note that the XSL stylesheet gets stripped during `se clean` when we generate the feed.
// `se clean` will also start adding empty namespaces everywhere if we include the stylesheet declaration first. // `se clean` will also start adding empty namespaces everywhere if we include the stylesheet declaration first.
// We have to add it programmatically when saving the feed file. // We have to add it programmatically when saving the feed file.
print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
?> ?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
<id><?= Formatter::ToPlainXmlText($id) ?></id> <id><?= SITE_URL . Formatter::ToPlainXmlText($id) ?></id>
<link href="<?= SITE_URL . htmlspecialchars($url, ENT_QUOTES|ENT_XML1, 'utf-8') ?>" rel="self" type="application/atom+xml;profile=opds-catalog;kind=navigation"/> <link href="<?= SITE_URL . Formatter::ToPlainXmlText($url) ?>" rel="self" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link href="<?= SITE_URL ?>/opds" rel="start" type="application/atom+xml;profile=opds-catalog;kind=navigation"/> <link href="<?= SITE_URL ?>/opds" rel="start" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link href="<?= SITE_URL ?>/opds/all" rel="http://opds-spec.org/crawlable" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/> <link href="<?= SITE_URL ?>/opds/all" rel="http://opds-spec.org/crawlable" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<link href="<?= SITE_URL ?>/ebooks/opensearch" rel="search" type="application/opensearchdescription+xml"/> <link href="<?= SITE_URL ?>/ebooks/opensearch" rel="search" type="application/opensearchdescription+xml"/>
<? if($parentUrl !== null){ ?><link href="<?= SITE_URL ?><?= Formatter::ToPlainXmlText($parentUrl) ?>" rel="up" type="application/atom+xml;profile=opds-catalog;kind=navigation"/><? } ?> <? if($parentUrl !== null){ ?><link href="<?= SITE_URL ?><?= Formatter::ToPlainXmlText($parentUrl) ?>" rel="up" type="application/atom+xml;profile=opds-catalog;kind=navigation"/><? } ?>
<title><?= Formatter::ToPlainXmlText($title) ?></title> <title><?= Formatter::ToPlainXmlText($title) ?></title>
<subtitle>Free and liberated ebooks, carefully produced for the true book lover.</subtitle> <? if($subtitle !== null){ ?><subtitle><?= Formatter::ToPlainXmlText($subtitle) ?></subtitle><? } ?>
<icon><?= SITE_URL ?>/images/logo.png</icon> <icon><?= SITE_URL ?>/images/logo.png</icon>
<updated><?= $updatedTimestamp->format('Y-m-d\TH:i:s\Z') ?></updated> <updated><?= $updatedTimestamp->format('Y-m-d\TH:i:s\Z') ?></updated>
<author> <author>

View file

@ -5,7 +5,7 @@ use Safe\DateTime;
// `se clean` will also start adding empty namespaces everywhere if we include the stylesheet declaration first. // `se clean` will also start adding empty namespaces everywhere if we include the stylesheet declaration first.
// We have to add it programmatically when saving the feed file. // We have to add it programmatically when saving the feed file.
print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> ?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
<channel> <channel>
<title><?= Formatter::ToPlainXmlText($title) ?></title> <title><?= Formatter::ToPlainXmlText($title) ?></title>
<link><?= SITE_URL ?></link> <link><?= SITE_URL ?></link>
@ -33,6 +33,7 @@ print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
<? foreach($entry->Tags as $tag){ ?> <? foreach($entry->Tags as $tag){ ?>
<category domain="https://standardebooks.org/vocab/subjects"><?= Formatter::ToPlainXmlText($tag->Name) ?></category> <category domain="https://standardebooks.org/vocab/subjects"><?= Formatter::ToPlainXmlText($tag->Name) ?></category>
<? } ?> <? } ?>
<media:thumbnail url="<?= SITE_URL . $entry->Url ?>/downloads/cover-thumbnail.jpg" height="525" width="350"/>
<? if($entry->EpubUrl !== null){ ?> <? if($entry->EpubUrl !== null){ ?>
<enclosure url="<?= SITE_URL . Formatter::ToPlainXmlText($entry->EpubUrl) ?>" length="<?= filesize(WEB_ROOT . $entry->EpubUrl) ?>" type="application/epub+zip" /> <? /* Only one <enclosure> is allowed */ ?> <enclosure url="<?= SITE_URL . Formatter::ToPlainXmlText($entry->EpubUrl) ?>" length="<?= filesize(WEB_ROOT . $entry->EpubUrl) ?>" type="application/epub+zip" /> <? /* Only one <enclosure> is allowed */ ?>
<? } ?> <? } ?>

File diff suppressed because it is too large Load diff

83
www/atom/style.php Normal file
View file

@ -0,0 +1,83 @@
<?
require_once('Core.php');
// `text/xsl` is the only mime type recognized by Chrome for XSL stylesheets
header('Content-Type: text/xsl');
print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
?>
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
<xsl:output method="html" html-version="5.0" encoding="utf-8" indent="yes" doctype-system="about:legacy-compat"/> <? /* doctype-system outputs the HTML5 doctype */ ?>
<xsl:template match="/">
<?= Template::Header(['xmlDeclaration' => false]) ?>
<main class="opds">
<h1><xsl:value-of select="substring-after(/atom:feed/atom:title, 'Standard Ebooks - ')"/></h1>
<p>This page is an Atom 1.0 feed. The URL in your browsers address bar (<a class="url"><xsl:attribute name="href"><xsl:value-of select="/atom:feed/atom:link[@rel='self']/@href"/></xsl:attribute><xsl:value-of select="/atom:feed/atom:link[@rel='self']/@href"/></a>) can be used in any Atom client.</p>
<ol class="ebooks-list list">
<xsl:for-each select="/atom:feed/atom:entry">
<li>
<div class="thumbnail-container">
<a tabindex="-1">
<xsl:attribute name="href">
<xsl:value-of select="atom:link[@rel='related']/@href"/>
</xsl:attribute>
<img alt="" width="224" height="335">
<xsl:attribute name="src">
<xsl:value-of select="media:thumbnail/@url"/>
</xsl:attribute>
</img>
</a>
</div>
<p>
<a>
<xsl:attribute name="href">
<xsl:value-of select="atom:link[@rel='related']/@href"/>
</xsl:attribute>
<xsl:value-of select="atom:title"/>
</a>
</p>
<div>
<xsl:for-each select="atom:author">
<p class="author">
<a>
<xsl:attribute name="href">
<xsl:value-of select="atom:uri"/>
</xsl:attribute>
<xsl:value-of select="atom:name"/>
</a>
</p>
</xsl:for-each>
</div>
<ul class="tags">
<xsl:for-each select="atom:category[@scheme='https://standardebooks.org/vocab/subjects']">
<li>
<p><xsl:value-of select="@term"/></p>
</li>
</xsl:for-each>
</ul>
<div class="details">
<p>
<xsl:value-of select="atom:summary"/>
</p>
</div>
<p class="download">Read</p>
<ul>
<xsl:for-each select="atom:link[@rel='enclosure']">
<li>
<p>
<a>
<xsl:attribute name="href">
<xsl:value-of select="@href"/>
</xsl:attribute>
<xsl:value-of select="@title"/>
</a>
</p>
</li>
</xsl:for-each>
</ul>
</li>
</xsl:for-each>
</ol>
</main>
<?= Template::Footer() ?>
</xsl:template>
</xsl:stylesheet>

View file

@ -1190,14 +1190,11 @@ main.ebooks > aside.alert + ol{
margin-top: 4rem; margin-top: 4rem;
} }
.rss .download,
.opds .download{ .opds .download{
font-weight: bold; font-weight: bold;
margin-top: 1rem; margin-top: 1rem;
} }
.rss .download + ul,
.rss .download + ul li,
.opds .download + ul, .opds .download + ul,
.opds .download + ul > li{ .opds .download + ul > li{
margin-top: 0; margin-top: 0;
@ -1242,6 +1239,10 @@ ol.ebooks-list.list > li .thumbnail-container{
grid-row: 1 / span 3; grid-row: 1 / span 3;
} }
ol.ebooks-list.list.rss > li .thumbnail-container{
grid-row: 1 / span 5;
}
.opds ol.ebooks-list.list > li .thumbnail-container{ .opds ol.ebooks-list.list > li .thumbnail-container{
grid-row: 1 / span 6; grid-row: 1 / span 6;
} }
@ -1409,8 +1410,7 @@ ul.tags{
margin-top: 1rem; margin-top: 1rem;
} }
.opds ul.tags, .opds ul.tags{
.rss ul.tags{
justify-content: flex-start; justify-content: flex-start;
margin: .5rem auto; margin: .5rem auto;
} }
@ -1420,8 +1420,7 @@ ul.tags li{
} }
ul.tags li a, ul.tags li a,
.opds ul.tags li p, .opds ul.tags li p{
.rss ul.tags li p{
border: 1px solid var(--body-text); border: 1px solid var(--body-text);
border-radius: 5px; border-radius: 5px;
padding: .25rem .5rem; padding: .25rem .5rem;
@ -2502,10 +2501,6 @@ ul.feed p{
font-size: .8rem; font-size: .8rem;
} }
.rss > li p:last-child{
margin-top: 0;
}
@media (hover: none) and (pointer: coarse){ /* target ipads and smartphones without a mouse */ @media (hover: none) and (pointer: coarse){ /* target ipads and smartphones without a mouse */
/* For iPad, unset the height so it matches the other elements */ /* For iPad, unset the height so it matches the other elements */
select[multiple]{ select[multiple]{

View file

@ -6,30 +6,21 @@ require_once('Core.php');
// But, serving that mime type doesn't display the feed in a browser; rather, it downloads it as an attachment. // But, serving that mime type doesn't display the feed in a browser; rather, it downloads it as an attachment.
// `text/xml` is the de facto RSS mime type, used by most popular feeds and recognized by all RSS readers. // `text/xml` is the de facto RSS mime type, used by most popular feeds and recognized by all RSS readers.
// It also displays the feed in a browser so we can style it with XSLT. // It also displays the feed in a browser so we can style it with XSLT.
// This is the same for OPDS (whose de jure mime type is `application/atom+xml`). // This is the same for Atom/OPDS (whose de jure mime type is `application/atom+xml`).
$subjects = ["Adventure", "Autobiography", "Biography", "Childrens", "Comedy", "Drama", "Fantasy", "Fiction", "Horror", "Memoir", "Mystery", "Nonfiction", "Philosophy", "Poetry", "Satire", "Science Fiction", "Shorts", "Spirituality", "Tragedy", "Travel"];
?><?= Template::Header(['description' => 'A list of available feeds of Standard Ebooks ebooks.']) ?> ?><?= Template::Header(['description' => 'A list of available feeds of Standard Ebooks ebooks.']) ?>
<main> <main>
<article> <article>
<h1>Ebook Feeds</h1> <h1>Ebook Feeds</h1>
<p>We offers several feeds that you can use to get notified about new ebooks, or to browse and download from our catalog directly in your ereader.</p> <p>We offers several feeds that you can use to get notified about new ebooks, or to browse and download from our catalog directly in your ereader.</p>
<section id="rss-feeds">
<h2>RSS feeds</h2>
<p>RSS feeds can be read by one of the many <a href="https://en.wikipedia.org/wiki/Comparison_of_feed_aggregators">RSS clients</a> available for download, like <a href="https://www.thunderbird.net/en-US/">Thunderbird</a>.</p>
<ul class="feed">
<li>
<p><a href="/rss/new-releases">New releases</a> (RSS 2.0)</p>
<p class="url"><?= SITE_URL ?>/rss/new-releases</p>
<p>A list of the thirty latest Standard Ebooks ebook releases, most-recently-released first.</p>
</li>
</ul>
</section>
<section id="opds-feeds"> <section id="opds-feeds">
<h2>OPDS feeds</h2> <h2>OPDS 1.2 feeds</h2>
<p><a href="https://en.wikipedia.org/wiki/Open_Publication_Distribution_System">OPDS feeds</a> are designed for use with ereading systems like <a href="http://koreader.rocks/">KOreader</a> or <a href="https://calibre-ebook.com">Calibre</a>, or with ereaders like <a href="https://johnfactotum.github.io/foliate/">Foliate</a>. They allow you to search, browse, and download from our catalog, directly in your ereader. Theyre also perfect for organizations who wish to download and process our catalog efficiently.</p> <p><a href="https://en.wikipedia.org/wiki/Open_Publication_Distribution_System">OPDS feeds</a> are designed for use with ereading systems like <a href="http://koreader.rocks/">KOreader</a> or <a href="https://calibre-ebook.com">Calibre</a>, or with ereaders like <a href="https://johnfactotum.github.io/foliate/">Foliate</a>. They allow you to search, browse, and download from our catalog, directly in your ereader. Theyre also perfect for organizations who wish to download and process our catalog efficiently.</p>
<ul class="feed"> <ul class="feed">
<li> <li>
<p><a href="/opds">The Standard Ebooks OPDS feed</a> (OPDS 1.2)</p> <p><a href="/opds">The Standard Ebooks OPDS feed</a></p>
<p class="url"><?= SITE_URL ?>/opds</p> <p class="url"><?= SITE_URL ?>/opds</p>
</li> </li>
</ul> </ul>
@ -45,6 +36,63 @@ require_once('Core.php');
</ul> </ul>
</section> </section>
</section> </section>
<section id="atom-feeds">
<h2>Atom 1.0 feeds</h2>
<p>Atom feeds can be read by one of the many <a href="https://en.wikipedia.org/wiki/Comparison_of_feed_aggregators">RSS clients</a> available for download, like <a href="https://www.thunderbird.net/en-US/">Thunderbird</a>. They contain more information than regular RSS feeds. Most RSS clients can read both Atom and RSS feeds.</p>
<p>Note that some RSS readers may show these feeds ordered by when an ebook was last updated, even though the feeds are ordered by when an ebook was first released. You should be able to change the sort order in your RSS reader.</p>
<ul class="feed">
<li>
<p><a href="/atom/new-releases">New releases</a></p>
<p class="url"><?= SITE_URL ?>/atom/new-releases</p>
<p>The thirty latest Standard Ebooks, most-recently-released first.</p>
</li>
<li>
<p><a href="/atom/all">All ebooks</a></p>
<p class="url"><?= SITE_URL ?>/atom/all</p>
<p>All Standard Ebooks, most-recently-released first.</p>
</li>
</ul>
<section id="atom-ebooks-by-subject">
<h3>Ebooks by subject</h3>
<ul class="feed">
<? foreach($subjects as $subject){ ?>
<li>
<p><a href="/atom/subjects/<?= Formatter::MakeUrlSafe($subject) ?>"><?= Formatter::ToPlainText($subject) ?></a></p>
<p><a href="/atom/subjects/<?= Formatter::MakeUrlSafe($subject) ?>"></a></p>
<p class="url"><?= SITE_URL ?>/atom/subjects/<?= Formatter::MakeUrlSafe($subject) ?></p>
</li>
<? } ?>
</ul>
</section>
</section>
<section id="rss-feeds">
<h2>RSS 2.0 feeds</h2>
<p>RSS feeds are an alternative to Atom feeds. They contain less information than Atom feeds, but might be better supported by some RSS readers.</p>
<ul class="feed">
<li>
<p><a href="/rss/new-releases">New releases</a></p>
<p class="url"><?= SITE_URL ?>/rss/new-releases</p>
<p>The thirty latest Standard Ebooks, most-recently-released first.</p>
</li>
<li>
<p><a href="/rss/all">All ebooks</a></p>
<p class="url"><?= SITE_URL ?>/rss/all</p>
<p>All Standard Ebooks, most-recently-released first.</p>
</li>
</ul>
<section id="rss-ebooks-by-subject">
<h3>Ebooks by subject</h3>
<ul class="feed">
<? foreach($subjects as $subject){ ?>
<li>
<p><a href="/rss/subjects/<?= Formatter::MakeUrlSafe($subject) ?>"><?= Formatter::ToPlainText($subject) ?></a></p>
<p><a href="/rss/subjects/<?= Formatter::MakeUrlSafe($subject) ?>"></a></p>
<p class="url"><?= SITE_URL ?>/rss/subjects/<?= Formatter::MakeUrlSafe($subject) ?></p>
</li>
<? } ?>
</ul>
</section>
</section>
</article> </article>
</main> </main>
<?= Template::Footer() ?> <?= Template::Footer() ?>

View file

@ -16,25 +16,25 @@ catch(\Exception $ex){
include(WEB_ROOT . '/404.php'); include(WEB_ROOT . '/404.php');
exit(); exit();
} }
print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<?xml-stylesheet href=\"/opds/style\" type=\"text/xsl\"?>\n");
?> ?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:schema="http://schema.org/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:schema="http://schema.org/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">
<id>https://standardebooks.org/opds/all?query=<?= urlencode($query) ?></id> <id><?= SITE_URL ?>/opds/all?query=<?= urlencode($query) ?></id>
<link href="https://standardebooks.org/opds/all?query=<?= urlencode($query) ?>" rel="self" type="application/atom+xml;profile=opds-catalog"/> <link href="<?= SITE_URL ?>/opds/all?query=<?= urlencode($query) ?>" rel="self" type="application/atom+xml;profile=opds-catalog"/>
<link href="https://standardebooks.org/ebooks/ebooks?query=<?= urlencode($query) ?>" rel="alternate" type="text/html"/> <link href="<?= SITE_URL ?>/ebooks/ebooks?query=<?= urlencode($query) ?>" rel="alternate" type="text/html"/>
<link href="https://standardebooks.org/opds" rel="start" type="application/atom+xml;profile=opds-catalog;kind=navigation"/> <link href="<?= SITE_URL ?>/opds" rel="start" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link href="https://standardebooks.org/opds/all" rel="http://opds-spec.org/crawlable" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/> <link href="<?= SITE_URL ?>/opds/all" rel="http://opds-spec.org/crawlable" type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<link href="https://standardebooks.org/ebooks/opensearch" rel="search" type="application/opensearchdescription+xml"/> <link href="<?= SITE_URL ?>/ebooks/opensearch" rel="search" type="application/opensearchdescription+xml"/>
<title>Standard Ebooks OPDS Search Results</title> <title>Search Results</title>
<subtitle>Free and liberated ebooks, carefully produced for the true book lover.</subtitle> <subtitle>Results for <?= Formatter::ToPlainXmlText($query) ?>”.</subtitle>
<icon>/images/logo.png</icon> <icon><?= SITE_URL ?>/images/logo.png</icon>
<updated><?= (new Datetime())->Format('Y-m-d\TH:i:s\Z') ?></updated> <updated><?= (new Datetime())->Format('Y-m-d\TH:i:s\Z') ?></updated>
<author> <author>
<name>Standard Ebooks</name> <name>Standard Ebooks</name>
<uri>https://standardebooks.org</uri> <uri><?= SITE_URL ?></uri>
</author> </author>
<opensearch:totalResults><?= sizeof($ebooks) ?></opensearch:totalResults> <opensearch:totalResults><?= sizeof($ebooks) ?></opensearch:totalResults>
<? foreach($ebooks as $ebook){ ?> <? foreach($ebooks as $ebook){ ?>
<?= Template::OpdsAcquisitionEntry(['ebook' => $ebook]) ?> <?= Template::OpdsAcquisitionEntry(['entry' => $ebook]) ?>
<? } ?> <? } ?>
</feed> </feed>

View file

@ -11,7 +11,7 @@ print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
<?= Template::Header(['xmlDeclaration' => false]) ?> <?= Template::Header(['xmlDeclaration' => false]) ?>
<main class="opds"> <main class="opds">
<h1><xsl:value-of select="/atom:feed/atom:title"/></h1> <h1><xsl:value-of select="/atom:feed/atom:title"/></h1>
<p>This page is an OPDS feed. The URL in your browsers address bar (<a class="url"><xsl:attribute name="href"><xsl:value-of select="/atom:feed/atom:link[@rel='self']/@href"/></xsl:attribute><xsl:value-of select="/atom:feed/atom:link[@rel='self']/@href"/></a>) can be used in any OPDS client.</p> <p>This page is an OPDS 1.2 feed. The URL in your browsers address bar (<a class="url"><xsl:attribute name="href"><xsl:value-of select="/atom:feed/atom:link[@rel='self']/@href"/></xsl:attribute><xsl:value-of select="/atom:feed/atom:link[@rel='self']/@href"/></a>) can be used in any OPDS client.</p>
<xsl:if test="/atom:feed/atom:entry[./atom:link[starts-with(@type, 'application/atom+xml;profile=opds-catalog;kind=')]]"> <xsl:if test="/atom:feed/atom:entry[./atom:link[starts-with(@type, 'application/atom+xml;profile=opds-catalog;kind=')]]">
<ol class="rss"> <ol class="rss">
<xsl:for-each select="/atom:feed/atom:entry[./atom:link[starts-with(@type, 'application/atom+xml;profile=opds-catalog;kind=')]]"> <xsl:for-each select="/atom:feed/atom:entry[./atom:link[starts-with(@type, 'application/atom+xml;profile=opds-catalog;kind=')]]">
@ -38,7 +38,7 @@ print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
<xsl:attribute name="href"> <xsl:attribute name="href">
<xsl:value-of select="atom:link[@rel='related']/@href"/> <xsl:value-of select="atom:link[@rel='related']/@href"/>
</xsl:attribute> </xsl:attribute>
<img alt="The cover for the Standard Ebooks edition of Winnie-the-Pooh, by A. A. Milne" width="224" height="335"> <img alt="" width="224" height="335">
<xsl:attribute name="src"> <xsl:attribute name="src">
<xsl:value-of select="atom:link[@rel='http://opds-spec.org/image/thumbnail']/@href"/> <xsl:value-of select="atom:link[@rel='http://opds-spec.org/image/thumbnail']/@href"/>
</xsl:attribute> </xsl:attribute>

View file

@ -5,23 +5,37 @@ require_once('Core.php');
header('Content-Type: text/xsl'); header('Content-Type: text/xsl');
print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n") print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
?> ?>
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:atom="http://www.w3.org/2005/Atom"> <xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
<xsl:output method="html" html-version="5.0" encoding="utf-8" indent="yes" doctype-system="about:legacy-compat"/> <? /* doctype-system outputs the HTML5 doctype */ ?> <xsl:output method="html" html-version="5.0" encoding="utf-8" indent="yes" doctype-system="about:legacy-compat"/> <? /* doctype-system outputs the HTML5 doctype */ ?>
<xsl:template match="/"> <xsl:template match="/">
<?= Template::Header(['xmlDeclaration' => false]) ?> <?= Template::Header(['xmlDeclaration' => false]) ?>
<main> <main class="opds">
<h1><xsl:value-of select="substring-after(/rss/channel/title, 'Standard Ebooks - ')"/></h1> <h1><xsl:value-of select="substring-after(/rss/channel/title, 'Standard Ebooks - ')"/></h1>
<p><xsl:value-of select="/rss/channel/description"/></p> <p><xsl:value-of select="/rss/channel/description"/></p>
<p>This page is an RSS feed. The URL in your browsers address bar (<a class="url"><xsl:attribute name="href"><xsl:value-of select="/rss/channel/atom:link/@href"/></xsl:attribute><xsl:value-of select="/rss/channel/atom:link/@href"/></a>) can be used in any RSS reader.</p> <p>This page is an RSS 2.0 feed. The URL in your browsers address bar (<a class="url"><xsl:attribute name="href"><xsl:value-of select="/rss/channel/atom:link/@href"/></xsl:attribute><xsl:value-of select="/rss/channel/atom:link/@href"/></a>) can be used in any RSS reader.</p>
<ol class="rss"> <ol class="ebooks-list list rss">
<xsl:for-each select="/rss/channel/item"> <xsl:for-each select="/rss/channel/item">
<li> <li>
<div class="thumbnail-container">
<a tabindex="-1">
<xsl:attribute name="href">
<xsl:value-of select="link"/>
</xsl:attribute>
<img alt="" width="224" height="335">
<xsl:attribute name="src">
<xsl:value-of select="media:thumbnail/@url"/>
</xsl:attribute>
</img>
</a>
</div>
<p>
<a> <a>
<xsl:attribute name="href"> <xsl:attribute name="href">
<xsl:value-of select="link"/> <xsl:value-of select="link"/>
</xsl:attribute> </xsl:attribute>
<xsl:value-of select="title"/> <xsl:value-of select="title"/>
</a> </a>
</p>
<ul class="tags"> <ul class="tags">
<xsl:for-each select="category"> <xsl:for-each select="category">
<li> <li>
@ -29,9 +43,11 @@ print("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
</li> </li>
</xsl:for-each> </xsl:for-each>
</ul> </ul>
<div class="details">
<p> <p>
<xsl:value-of select="description"/> <xsl:value-of select="description"/>
</p> </p>
</div>
<xsl:if test="enclosure"> <xsl:if test="enclosure">
<p class="download">Read</p> <p class="download">Read</p>
<ul> <ul>