Get notified of new ebooks in your news client with our Atom or RSS feeds.
-
Parse and process the feeds to use our ebooks in your personal software projects. (Organizations, see corporate sponsorship instead.)
+
Parse and process the feeds to use our ebooks in your personal software projects.
+
+
Access to bulk ebook downloads to easily download entire months’ worth of ebooks at once.
+
The ability to submit a book for inclusion on our Wanted Ebooks list, once per quarter. (Submissions must conform to our collections policy and are subject to approval.)
Get access to our OPDS, Atom, and RSS ebook feeds for use by your organization for the duration of your sponsorship. We can also produce different kinds of feeds to meet your needs, like ONIX feeds.
+
+
Get access to bulk ebook downloads to easily download large categories of ebooks, all at once.
OPDS feeds are designed for use with ereading apps on your phone or tablet, or with ereading systems like KOreader. Add our OPDS feed to your ereading app to search, browse, and download from our entire catalog, directly in your ereader.
+
OPDS feeds, or “catalogs,” can be added to ereading apps on phones and tablets to search, browse, and download from our entire catalog, directly in your ereader. Most modern ereading apps support OPDS catalogs.
They’re also perfect for scripting, or for libraries or other organizations who wish to download and process our catalog of ebooks.
}elseif($type == 'rss'){ ?>
RSS feeds are the predecessors of Atom feeds. They contain less information than Atom feeds, but might be better supported by some news readers.
OPDS feeds, or “catalogs,” can be added to ereading apps on phones and tablets, or to ereading systems like KOreader. Add our OPDS feed to your ereading app to search, browse, and download from our entire catalog, directly in your ereader.
+
OPDS feeds, or “catalogs,” can be added to ereading apps on phones and tablets to search, browse, and download from our entire catalog, directly in your ereader. Most modern ereading apps support OPDS catalogs.
They’re also perfect for scripting, or for libraries or other organizations who wish to download and process our catalog of ebooks.
diff --git a/www/patrons-circle/downloads/index.php b/www/patrons-circle/downloads/index.php
index 42a09abc..89d35dfd 100644
--- a/www/patrons-circle/downloads/index.php
+++ b/www/patrons-circle/downloads/index.php
@@ -2,96 +2,229 @@
require_once('Core.php');
use Safe\DateTime;
+use function Safe\apcu_fetch;
use function Safe\filemtime;
use function Safe\filesize;
use function Safe\glob;
use function Safe\gmdate;
+use function Safe\preg_match;
+use function Safe\sort;
use function Safe\rsort;
+use function Safe\usort;
-$ex = null;
+$forbiddenException = null;
if(isset($_SERVER['PHP_AUTH_USER'])){
// We get here if the user entered an invalid HTTP Basic Auth username,
// and this page was served as the 401 page.
- $ex = new Exceptions\InvalidPatronException();
+ $forbiddenException = new Exceptions\InvalidPatronException();
}
-$files = glob(WEB_ROOT . '/patrons-circle/downloads/*.zip');
-rsort($files);
-
+// Process ebooks by year
$years = [];
-foreach($files as $file){
- $obj = new stdClass();
- $date = new DateTime(str_replace('se-ebooks-', '', basename($file, '.zip')) . '-01');
- $updated = new DateTime('@' . filemtime($file));
- $obj->Month = $date->format('F');
- $obj->Url = '/patrons-circle/downloads/' . basename($file);
- $obj->Size = Formatter::ToFileSize(filesize($file));
- $obj->Updated = $updated->format('M i');
- // The count of ebooks in each file is stored as a filesystem attribute
- $obj->Count = exec('attr -g ebook-count ' . escapeshellarg($file)) ?: null;
- if($obj->Count !== null){
- $obj->Count = intval($obj->Count);
+try{
+ $years = apcu_fetch('bulk-downloads-years');
+}
+catch(Safe\Exceptions\ApcuException $ex){
+ // Nothing in the cache, generate the files
+
+ $files = glob(WEB_ROOT . '/patrons-circle/downloads/months/*/*.zip');
+ rsort($files);
+
+ foreach($files as $file){
+ $obj = new stdClass();
+ $obj->Updated = new DateTime('@' . filemtime($file));
+ $date = new DateTime();
+
+ preg_match('/se-ebooks-(\d+-\d+)/ius', basename($file), $matches);
+ if(sizeof($matches) == 2){
+ $date = new DateTime($matches[1] . '-01');
+ }
+
+ // The type of zip is stored as a filesystem attribute
+ $obj->Type = exec('attr -g se-ebook-type ' . escapeshellarg($file));
+ if($obj->Type == 'epub-advanced'){
+ $obj->Type = 'epub (advanced)';
+ }
+
+ $obj->Month = $date->format('Y-m');
+ $obj->Url = '/patrons-circle/downloads/months/' . $obj->Month . '/' . basename($file);
+ $obj->Size = Formatter::ToFileSize(filesize($file));
+
+ // The count of ebooks in each file is stored as a filesystem attribute
+ $obj->Count = exec('attr -g se-ebook-count ' . escapeshellarg($file)) ?: null;
+ if($obj->Count !== null){
+ $obj->Count = intval($obj->Count);
+ }
+
+ $obj->UpdatedString = $obj->Updated->format('M j');
+ if($obj->Updated->format('Y') != gmdate('Y')){
+ $obj->UpdatedString = $obj->Updated->format('M j, Y');
+ }
+
+ $year = $date->format('Y');
+ $month = $date->format('F');
+
+ if(!isset($years[$year])){
+ $years[$year] = [];
+ }
+
+ if(!isset($years[$year][$month])){
+ $years[$year][$month] = [];
+ }
+
+ $years[$year][$month][] = $obj;
}
- if($updated->format('Y') != gmdate('Y')){
- $obj->Updated = $obj->Updated . $updated->format(', Y');
+ // Sort the downloads by filename extension
+ foreach($years as $year => $months){
+ foreach($months as $month => $items){
+ usort($items, function($a, $b){ return $a->Type <=> $b->Type; });
+
+ // We have to reassign it because the foreach created a clone of the array
+ $years[$year][$month] = $items;
+ }
}
- $year = $date->format('Y');
+ apcu_store('bulk-downloads-years', $years, 43200); // 12 hours
+}
- if(!isset($years[$year])){
- $years[$year] = [];
+
+$subjects = [];
+// Process ebooks by subject
+try{
+ $subjects = apcu_fetch('bulk-downloads-subjects');
+}
+catch(Safe\Exceptions\ApcuException $ex){
+ // Nothing in the cache, generate the files
+ $files = glob(WEB_ROOT . '/patrons-circle/downloads/subjects/*/*.zip');
+ sort($files);
+
+ foreach($files as $file){
+ $obj = new stdClass();
+ $obj->Url = '/patrons-circle/downloads/' . basename($file);
+ $obj->Size = Formatter::ToFileSize(filesize($file));
+ $obj->Updated = new DateTime('@' . filemtime($file));
+
+ // The count of ebooks in each file is stored as a filesystem attribute
+ $obj->Count = exec('attr -g se-ebook-count ' . escapeshellarg($file)) ?: null;
+ if($obj->Count !== null){
+ $obj->Count = intval($obj->Count);
+ }
+
+ // The subject of the batch is stored as a filesystem attribute
+ $obj->Subject = exec('attr -g se-subject ' . escapeshellarg($file)) ?: null;
+ if($obj->Subject === null){
+ $obj->Subject = str_replace('se-ebooks-', '', basename($file, '.zip'));
+ }
+
+ // The type of zip is stored as a filesystem attribute
+ $obj->Type = exec('attr -g se-ebook-type ' . escapeshellarg($file));
+ if($obj->Type == 'epub-advanced'){
+ $obj->Type = 'epub (advanced)';
+ }
+
+ $obj->UpdatedString = $obj->Updated->format('M j');
+ if($obj->Updated->format('Y') != gmdate('Y')){
+ $obj->UpdatedString = $obj->Updated->format('M j, Y');
+ }
+
+ if(!isset($subjects[$obj->Subject])){
+ $subjects[$obj->Subject] = [];
+ }
+
+ $subjects[$obj->Subject][] = $obj;
}
- $years[$year][] = $obj;
+ // Subjects downloads are already correctly sorted
+
+ apcu_store('bulk-downloads-subjects', $subjects, 43200); // 12 hours
}
?>= Template::Header(['title' => 'Bulk Ebook Downloads', 'highlight' => '', 'description' => 'Download zip files containing all of the Standard Ebooks released in a given month.']) ?>
-
+
Patrons circle members can download zip files containing all of the ebooks that were released in a given month of Standard Ebooks history. You can join the Patrons Circle with a small donation in support of our continuing mission to create free, beautiful digital literature.
These zip files contain each ebook in every format we offer, and are updated once daily with the latest versions of each ebook.
If you’re a Patrons Circle member, when prompted enter your email address and leave the password blank to download these files.
-
- foreach($years as $year => $items){ ?>
-
-
= Formatter::ToPlainText((string)$year) ?>
-
+
+
+
Downloads by subject
+
-
-
Ebooks
-
Size
-
Updated
+
+
+
Ebooks
+
Updated
+
Download
+
- foreach($items as $item){ ?>
+ foreach($subjects as $subject => $items){ ?>