mirror of
https://github.com/standardebooks/web.git
synced 2025-07-08 15:50:29 -04:00
Split bulk downloads into file type and cache output
This commit is contained in:
parent
55985b0c2f
commit
12b79b5dcd
11 changed files with 395 additions and 178 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -14,4 +14,5 @@ composer.lock
|
||||||
www/manual/*
|
www/manual/*
|
||||||
!www/manual/index.php
|
!www/manual/index.php
|
||||||
config/php/fpm/standardebooks.org-secrets.ini
|
config/php/fpm/standardebooks.org-secrets.ini
|
||||||
www/patrons-circle/downloads/*.zip
|
www/patrons-circle/downloads/months
|
||||||
|
www/patrons-circle/downloads/subjects
|
||||||
|
|
|
@ -51,16 +51,16 @@ class Formatter{
|
||||||
$output = number_format(round($bytes / 1048576, 1), 1) . 'M';
|
$output = number_format(round($bytes / 1048576, 1), 1) . 'M';
|
||||||
}
|
}
|
||||||
elseif($bytes >= 1024){
|
elseif($bytes >= 1024){
|
||||||
$output = number_format($bytes / 1024, 2) . 'KB';
|
$output = number_format($bytes / 1024, 0) . 'KB';
|
||||||
}
|
}
|
||||||
elseif($bytes > 1){
|
elseif($bytes > 1){
|
||||||
$output = $bytes . ' bytes';
|
$output = $bytes . 'b';
|
||||||
}
|
}
|
||||||
elseif($bytes == 1){
|
elseif($bytes == 1){
|
||||||
$output = $bytes . ' byte';
|
$output = $bytes . 'b';
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
$output = '0 bytes';
|
$output = '0b';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $output;
|
return $output;
|
||||||
|
|
146
scripts/generate-bulk-downloads
Executable file
146
scripts/generate-bulk-downloads
Executable file
|
@ -0,0 +1,146 @@
|
||||||
|
#!/usr/bin/php
|
||||||
|
<?
|
||||||
|
require_once('/standardebooks.org/web/lib/Core.php');
|
||||||
|
|
||||||
|
use function Safe\mkdir;
|
||||||
|
|
||||||
|
$longopts = ['webroot:'];
|
||||||
|
$options = getopt('', $longopts);
|
||||||
|
$webRoot = $options['webroot'] ?? WEB_ROOT;
|
||||||
|
|
||||||
|
$ebooksByMonth = [];
|
||||||
|
$lastUpdatedTimestampsByMonth = [];
|
||||||
|
$subjects = [];
|
||||||
|
$ebooksBySubject = [];
|
||||||
|
$lastUpdatedTimestampsBySubject = [];
|
||||||
|
|
||||||
|
function CreateZip(string $filePath, array $ebooks, string $type, string $webRoot, ?string $subject = null, ?string $month = null): void{
|
||||||
|
$tempFilename = tempnam(sys_get_temp_dir(), "se-ebooks");
|
||||||
|
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
|
||||||
|
if($zip->open($tempFilename, ZipArchive::CREATE) !== true){
|
||||||
|
print('Can\'t open file: ' . $tempFilename . "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($ebooks as $ebook){
|
||||||
|
if($type == 'epub' && $ebook->EpubUrl !== null){
|
||||||
|
$ebookFilePath = $webRoot . '/' . $ebook->EpubUrl;
|
||||||
|
$zip->addFile($ebookFilePath, basename($ebookFilePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($type == 'azw3' && $ebook->Azw3Url !== null){
|
||||||
|
$ebookFilePath = $webRoot . '/' . $ebook->Azw3Url;
|
||||||
|
$zip->addFile($ebookFilePath, basename($ebookFilePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($type == 'kepub' && $ebook->KepubUrl !== null){
|
||||||
|
$ebookFilePath = $webRoot . '/' . $ebook->KepubUrl;
|
||||||
|
$zip->addFile($ebookFilePath, basename($ebookFilePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($type == 'epub-advanced' && $ebook->AdvancedEpubUrl !== null){
|
||||||
|
$ebookFilePath = $webRoot . '/' . $ebook->AdvancedEpubUrl;
|
||||||
|
$zip->addFile($ebookFilePath, basename($ebookFilePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($type == 'xhtml' && $ebook->TextSinglePageUrl !== null){
|
||||||
|
$ebookFilePath = $webRoot . '/' . $ebook->TextSinglePageUrl . '.xhtml';
|
||||||
|
|
||||||
|
// Strip the navigation header that was added as part of the deploy process
|
||||||
|
$xhtml = file_get_contents($ebookFilePath);
|
||||||
|
$xhtml = preg_replace('|<body><header><nav>.+?</nav></header>|ius', '<body>', $xhtml);
|
||||||
|
|
||||||
|
$zip->addFromString(str_replace('single-page', $ebook->UrlSafeIdentifier, basename($ebookFilePath)), $xhtml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
$dir = dirname($filePath);
|
||||||
|
if(!is_dir($dir)){
|
||||||
|
mkdir($dir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
rename($tempFilename, $filePath);
|
||||||
|
|
||||||
|
// Set a filesystem attribute for the number of ebooks in the file. This will be used
|
||||||
|
// to display that number on the downloads page.
|
||||||
|
exec('attr -q -s se-ebook-count -V ' . escapeshellarg(sizeof($ebooks)) . ' ' . escapeshellarg($filePath));
|
||||||
|
|
||||||
|
exec('attr -q -s se-ebook-type -V ' . escapeshellarg($type) . ' ' . escapeshellarg($filePath));
|
||||||
|
|
||||||
|
// If we're passed a subject, add it as a file attribute too
|
||||||
|
if($subject !== null){
|
||||||
|
exec('attr -q -s se-subject -V ' . escapeshellarg($subject) . ' ' . escapeshellarg($filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($month !== null){
|
||||||
|
exec('attr -q -s se-month -V ' . escapeshellarg($month) . ' ' . escapeshellarg($filePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over all ebooks and arrange them by publication month
|
||||||
|
foreach(Library::GetEbooksFromFilesystem($webRoot) as $ebook){
|
||||||
|
$timestamp = $ebook->Created->format('Y-m');
|
||||||
|
$updatedTimestamp = $ebook->Updated->getTimestamp();
|
||||||
|
|
||||||
|
if(!isset($ebooksByMonth[$timestamp])){
|
||||||
|
$ebooksByMonth[$timestamp] = [];
|
||||||
|
$lastUpdatedTimestampsByMonth[$timestamp] = $updatedTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to the 'ebooks by month' list
|
||||||
|
$ebooksByMonth[$timestamp][] = $ebook;
|
||||||
|
|
||||||
|
if($updatedTimestamp > $lastUpdatedTimestampsByMonth[$timestamp]){
|
||||||
|
$lastUpdatedTimestampsByMonth[$timestamp] = $updatedTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to the 'books by subject' list
|
||||||
|
foreach($ebook->Tags as $tag){
|
||||||
|
// Add the book's subjects to the main subjects list
|
||||||
|
if(!in_array($tag->Name, $subjects)){
|
||||||
|
$subjects[] = $tag->Name;
|
||||||
|
$lastUpdatedTimestampsBySubject[$tag->Name] = $updatedTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort this ebook by subject
|
||||||
|
$ebooksBySubject[$tag->Name][] = $ebook;
|
||||||
|
|
||||||
|
if($updatedTimestamp > $lastUpdatedTimestampsBySubject[$tag->Name]){
|
||||||
|
$lastUpdatedTimestampsBySubject[$tag->Name] = $updatedTimestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$types = ['epub', 'epub-advanced', 'azw3', 'kepub', 'xhtml'];
|
||||||
|
|
||||||
|
foreach($ebooksByMonth as $month => $ebooks){
|
||||||
|
foreach($types as $type){
|
||||||
|
$filename = 'se-ebooks-' . $month . '-' . $type . '.zip';
|
||||||
|
$filePath = $webRoot . '/patrons-circle/downloads/months/' . $month . '/' . $filename;
|
||||||
|
|
||||||
|
// If the file doesn't exist, or if the content.opf last updated time is newer than the file modification time
|
||||||
|
if(!file_exists($filePath) || filemtime($filePath) < $lastUpdatedTimestampsByMonth[$month]){
|
||||||
|
print('Creating ' . $filePath . "\n");
|
||||||
|
|
||||||
|
CreateZip($filePath, $ebooks, $type, $webRoot, null, $month);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($ebooksBySubject as $subject => $ebooks){
|
||||||
|
foreach($types as $type){
|
||||||
|
$urlSafeSubject = Formatter::MakeUrlSafe($subject);
|
||||||
|
$filename = 'se-ebooks-' . $urlSafeSubject . '-' . $type . '.zip';
|
||||||
|
$filePath = $webRoot . '/patrons-circle/downloads/subjects/' . $urlSafeSubject . '/'. $filename;
|
||||||
|
|
||||||
|
// If the file doesn't exist, or if the content.opf last updated time is newer than the file modification time
|
||||||
|
if(!file_exists($filePath) || filemtime($filePath) < $lastUpdatedTimestampsBySubject[$subject]){
|
||||||
|
print('Creating ' . $filePath . "\n");
|
||||||
|
|
||||||
|
CreateZip($filePath, $ebooks, $type, $webRoot, $subject, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,7 +29,7 @@ $allEbooks = [];
|
||||||
$newestEbooks = [];
|
$newestEbooks = [];
|
||||||
$subjects = [];
|
$subjects = [];
|
||||||
$ebooksBySubject = [];
|
$ebooksBySubject = [];
|
||||||
$ebooksPerNewestEbooksFeed = 30;
|
$ebooksPerNewestEbooksFeed = 15;
|
||||||
|
|
||||||
if(!is_dir($webRoot . '/feeds/opds/subjects')){
|
if(!is_dir($webRoot . '/feeds/opds/subjects')){
|
||||||
mkdir($webRoot . '/feeds/opds/subjects');
|
mkdir($webRoot . '/feeds/opds/subjects');
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
#!/usr/bin/php
|
|
||||||
<?
|
|
||||||
require_once('/standardebooks.org/web/lib/Core.php');
|
|
||||||
|
|
||||||
$longopts = ['webroot:'];
|
|
||||||
$options = getopt('', $longopts);
|
|
||||||
$webRoot = $options['webroot'] ?? WEB_ROOT;
|
|
||||||
|
|
||||||
$ebooksByMonth = [];
|
|
||||||
$lastUpdatedTimestamps = [];
|
|
||||||
|
|
||||||
// Iterate over all ebooks and arrange them by publication month
|
|
||||||
foreach(Library::GetEbooksFromFilesystem($webRoot) as $ebook){
|
|
||||||
$timestamp = $ebook->Created->format('Y-m');
|
|
||||||
$updatedTimestamp = $ebook->Updated->getTimestamp();
|
|
||||||
|
|
||||||
if(!isset($ebooksByMonth[$timestamp])){
|
|
||||||
$ebooksByMonth[$timestamp] = [];
|
|
||||||
$lastUpdatedTimestamps[$timestamp] = $updatedTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
$ebooksByMonth[$timestamp][] = $ebook;
|
|
||||||
if($updatedTimestamp > $lastUpdatedTimestamps[$timestamp]){
|
|
||||||
$lastUpdatedTimestamps[$timestamp] = $updatedTimestamp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach($ebooksByMonth as $month => $ebooks){
|
|
||||||
$filename = 'se-ebooks-' . $month . '.zip';
|
|
||||||
$filePath = $webRoot . '/patrons-circle/downloads/' . $filename;
|
|
||||||
|
|
||||||
// If the file doesn't exist, or if the content.opf last updated time is newer than the file modification time
|
|
||||||
if(!file_exists($filePath) || filemtime($filePath) < $lastUpdatedTimestamps[$month]){
|
|
||||||
print('Creating ' . $filePath . "\n");
|
|
||||||
|
|
||||||
$tempFilename = tempnam(sys_get_temp_dir(), "se-ebooks");
|
|
||||||
|
|
||||||
$zip = new ZipArchive();
|
|
||||||
|
|
||||||
if($zip->open($tempFilename, ZipArchive::CREATE) !== true){
|
|
||||||
print('Can\'t open file: ' . $tempFilename . "\n");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach($ebooks as $ebook){
|
|
||||||
if($ebook->EpubUrl !== null){
|
|
||||||
$ebookFilePath = $webRoot . '/' . $ebook->EpubUrl;
|
|
||||||
$zip->addFile($ebookFilePath, $ebook->UrlSafeIdentifier . '/' . basename($ebookFilePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
if($ebook->Azw3Url !== null){
|
|
||||||
$ebookFilePath = $webRoot . '/' . $ebook->Azw3Url;
|
|
||||||
$zip->addFile($ebookFilePath, $ebook->UrlSafeIdentifier . '/' . basename($ebookFilePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
if($ebook->KepubUrl !== null){
|
|
||||||
$ebookFilePath = $webRoot . '/' . $ebook->KepubUrl;
|
|
||||||
$zip->addFile($ebookFilePath, $ebook->UrlSafeIdentifier . '/' . basename($ebookFilePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
if($ebook->AdvancedEpubUrl !== null){
|
|
||||||
$ebookFilePath = $webRoot . '/' . $ebook->AdvancedEpubUrl;
|
|
||||||
$zip->addFile($ebookFilePath, $ebook->UrlSafeIdentifier . '/' . basename($ebookFilePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
if($ebook->TextSinglePageUrl !== null){
|
|
||||||
$ebookFilePath = $webRoot . '/' . $ebook->TextSinglePageUrl . '.xhtml';
|
|
||||||
|
|
||||||
// Strip the navigation header that was added as part of the deploy process
|
|
||||||
$xhtml = file_get_contents($ebookFilePath);
|
|
||||||
$xhtml = preg_replace('|<body><header><nav>.+?</nav></header>|ius', '<body>', $xhtml);
|
|
||||||
|
|
||||||
$zip->addFromString($ebook->UrlSafeIdentifier . '/' . str_replace('single-page', $ebook->UrlSafeIdentifier, basename($ebookFilePath)), $xhtml);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$zip->close();
|
|
||||||
|
|
||||||
rename($tempFilename, $filePath);
|
|
||||||
|
|
||||||
// Set a filesystem attribute for the number of ebooks in the file. This will be used
|
|
||||||
// to display that number on the downloads page.
|
|
||||||
exec('attr -q -s ebook-count -V ' . escapeshellarg(sizeof($ebooks)) . ' ' . escapeshellarg($filePath));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -675,66 +675,76 @@ ul.message.error li:only-child{
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bulk-downloads > p{
|
.bulk-downloads > p,
|
||||||
|
.bulk-downloads > section > h2{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 40rem;
|
max-width: 40rem;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.download-list thead{
|
.download-list{
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-list .mid-header{
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.download-list table thead td{
|
.download-list thead tr.mid-header:first-child > *{
|
||||||
padding-top: 0;
|
padding-top: 1rem;
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.download-list table td{
|
.download-list .mid-header th:last-child{
|
||||||
padding: .25rem;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.download-list table td + td{
|
.download-list td,
|
||||||
text-align: right;
|
.download-list th{
|
||||||
|
padding: .25rem .5rem;
|
||||||
|
hyphens: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.download-list tbody tr:not(:last-child) td{
|
.download-list th{
|
||||||
border-bottom: 1px dashed var(--table-border);
|
font-weight: normal;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.download-list tbody td + td{
|
.download-list .number{
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-list td.download{
|
||||||
|
padding-right: 0;
|
||||||
|
color: var(--body-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-list td.download + td{
|
||||||
|
padding-left: .25rem;
|
||||||
|
font-size: .75em;
|
||||||
color: var(--sub-text);
|
color: var(--sub-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
main > section.narrow > ul.download-list{
|
.download-list tbody .row-header{
|
||||||
width: auto;
|
font-weight: bold;
|
||||||
max-width: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.download-list{
|
.download-list tbody tr td,
|
||||||
display: grid;
|
.download-list tbody tr th{
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
border-top: 1px dashed var(--table-border);
|
||||||
gap: 4rem;
|
|
||||||
list-style: none;
|
|
||||||
margin-top: 4rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.download-list > li{
|
.download-list tbody tr.year-header > *,
|
||||||
margin: 0;
|
.download-list tbody tr.year-header + tr > *,
|
||||||
|
.download-list tbody tr.mid-header tr > *,
|
||||||
|
.download-list tbody tr.mid-header + tr td,
|
||||||
|
.download-list tbody tr.mid-header + tr th{
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.download-list ul{
|
.download-list .year-header th{
|
||||||
list-style: none;
|
padding-top: 4rem;
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.download-list > li li:first-of-type{
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.download-list p.header{
|
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-family: "League Spartan", Arial, sans-serif;
|
font-family: "League Spartan", Arial, sans-serif;
|
||||||
margin-top: 4rem;
|
margin-top: 4rem;
|
||||||
|
@ -742,8 +752,6 @@ ul.download-list p.header{
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--header);
|
color: var(--header);
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2507,6 +2515,14 @@ aside button.close:active{
|
||||||
right: -1px;
|
right: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feeds-alert{
|
||||||
|
border-top: 1px dashed var(--sub-text);
|
||||||
|
padding-top: 2rem;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes progress{
|
@keyframes progress{
|
||||||
0%{
|
0%{
|
||||||
background-position: -60px 0px;
|
background-position: -60px 0px;
|
||||||
|
@ -2735,8 +2751,10 @@ ul.feed p{
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(max-width: 1200px){
|
@media(max-width: 1200px){
|
||||||
ul.download-list{
|
.download-list{
|
||||||
grid-template-columns: 1fr 1fr;
|
overflow-x: scroll;
|
||||||
|
display: block; /* needed to make overflow work */
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2918,10 +2936,6 @@ ul.feed p{
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.download-list{
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(max-width: 730px){
|
@media(max-width: 730px){
|
||||||
|
|
|
@ -42,10 +42,13 @@ require_once('Core.php');
|
||||||
<p>Get notified of new ebooks in your news client with our <a href="/feeds/atom">Atom</a> or <a href="/feeds/rss">RSS</a> feeds.</p>
|
<p>Get notified of new ebooks in your news client with our <a href="/feeds/atom">Atom</a> or <a href="/feeds/rss">RSS</a> feeds.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>Parse and process the feeds to use our ebooks in your personal software projects. (Organizations, see <a href="#corporate-sponsors">corporate sponsorship</a> instead.)</p>
|
<p>Parse and process the feeds to use our ebooks in your personal software projects.</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Access to <a href="/patrons-circle/downloads">bulk ebook downloads</a> to easily download entire months’ worth of ebooks at once.</p>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p>The ability to submit a book for inclusion on our <a href="/contribute/wanted-ebooks">Wanted Ebooks list</a>, once per quarter. (Submissions must conform to our <a href="/contribute/collections-policy">collections policy</a> and are subject to approval.)</p>
|
<p>The ability to submit a book for inclusion on our <a href="/contribute/wanted-ebooks">Wanted Ebooks list</a>, once per quarter. (Submissions must conform to our <a href="/contribute/collections-policy">collections policy</a> and are subject to approval.)</p>
|
||||||
</li>
|
</li>
|
||||||
|
@ -53,6 +56,7 @@ require_once('Core.php');
|
||||||
<p><strong>The right to periodically vote on a selection from our <a href="/contribute/wanted-ebooks">Wanted Ebooks list</a> to choose an ebook for immediate production.</strong> The resulting ebook will be a permanent addition to our <a href="/ebooks">online catalog of free digital literature</a>.</p>
|
<p><strong>The right to periodically vote on a selection from our <a href="/contribute/wanted-ebooks">Wanted Ebooks list</a> to choose an ebook for immediate production.</strong> The resulting ebook will be a permanent addition to our <a href="/ebooks">online catalog of free digital literature</a>.</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p><em>Membership in the Patrons Circle is limited to individuals only. Organizations, see <a href="#corporate-sponsors">corporate sponsorship</a> instead.</em></p>
|
||||||
<p class="button-row">
|
<p class="button-row">
|
||||||
<a href="https://fundraising.fracturedatlas.org/standard-ebooks/monthly_support" class="button">Donate $10/month or more</a>
|
<a href="https://fundraising.fracturedatlas.org/standard-ebooks/monthly_support" class="button">Donate $10/month or more</a>
|
||||||
<a href="https://fundraising.fracturedatlas.org/donor_intents/new?donation_intent=cd005756-7327-463d-bd53-a08acc5eaa4a" class="button">Donate $100 or more</a>
|
<a href="https://fundraising.fracturedatlas.org/donor_intents/new?donation_intent=cd005756-7327-463d-bd53-a08acc5eaa4a" class="button">Donate $100 or more</a>
|
||||||
|
@ -141,6 +145,9 @@ require_once('Core.php');
|
||||||
<li>
|
<li>
|
||||||
<p>Get access to our OPDS, Atom, and RSS <a href="/feeds">ebook feeds</a> for use by your organization for the duration of your sponsorship. We can also produce different kinds of feeds to meet your needs, like <abbr class="acronym">ONIX</abbr> feeds.</p>
|
<p>Get access to our OPDS, Atom, and RSS <a href="/feeds">ebook feeds</a> for use by your organization for the duration of your sponsorship. We can also produce different kinds of feeds to meet your needs, like <abbr class="acronym">ONIX</abbr> feeds.</p>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<p>Get access to <a href="/patrons-circle/downloads">bulk ebook downloads</a> to easily download large categories of ebooks, all at once.</p>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>To inquire about sponsorship options, contact the <a href="/about#editor-in-chief">Standard Ebooks Editor-in-Chief</a>.</p>
|
<p>To inquire about sponsorship options, contact the <a href="/about#editor-in-chief">Standard Ebooks Editor-in-Chief</a>.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -151,6 +151,7 @@ catch(Exceptions\InvalidCollectionException $ex){
|
||||||
<a<? if($page < ceil($totalEbooks / $perPage)){ ?> href="/ebooks/?page=<?= $page + 1 ?><? if($queryString != ''){ ?>&<?= $queryString ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a>
|
<a<? if($page < ceil($totalEbooks / $perPage)){ ?> href="/ebooks/?page=<?= $page + 1 ?><? if($queryString != ''){ ?>&<?= $queryString ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a>
|
||||||
</nav>
|
</nav>
|
||||||
<? } ?>
|
<? } ?>
|
||||||
|
<p class="feeds-alert">We also have <a href="/patrons-circle/downloads">bulk ebook downloads</a> available, as well as <a href="/feeds">ebook catalog feeds</a> for use directly in your ereader app or RSS reader.</p>
|
||||||
<? if(sizeof($ebooks) > 0 && $query == '' && sizeof($tags) == 0 && $collection === null && $page == 1){ ?>
|
<? if(sizeof($ebooks) > 0 && $query == '' && sizeof($tags) == 0 && $collection === null && $page == 1){ ?>
|
||||||
<?= Template::ContributeAlert() ?>
|
<?= Template::ContributeAlert() ?>
|
||||||
<? } ?>
|
<? } ?>
|
||||||
|
|
|
@ -54,7 +54,7 @@ if($type == 'atom'){
|
||||||
</ul>
|
</ul>
|
||||||
<? } ?>
|
<? } ?>
|
||||||
<? if($type == 'opds'){ ?>
|
<? if($type == 'opds'){ ?>
|
||||||
<p><a href="https://en.wikipedia.org/wiki/Open_Publication_Distribution_System">OPDS feeds</a> are designed for use with ereading apps on your phone or tablet, or with ereading systems like <a href="http://koreader.rocks/">KOreader</a>. Add our OPDS feed to your ereading app to search, browse, and download from our entire catalog, directly in your ereader.</p>
|
<p><a href="https://en.wikipedia.org/wiki/Open_Publication_Distribution_System">OPDS feeds</a>, 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.</p>
|
||||||
<p>They’re also perfect for scripting, or for libraries or other organizations who wish to download and process our catalog of ebooks.</p>
|
<p>They’re also perfect for scripting, or for libraries or other organizations who wish to download and process our catalog of ebooks.</p>
|
||||||
<? }elseif($type == 'rss'){ ?>
|
<? }elseif($type == 'rss'){ ?>
|
||||||
<p>RSS feeds are the predecessors of <a href="/feeds/atom">Atom feeds</a>. They contain less information than Atom feeds, but might be better supported by some news readers.</p>
|
<p>RSS feeds are the predecessors of <a href="/feeds/atom">Atom feeds</a>. They contain less information than Atom feeds, but might be better supported by some news readers.</p>
|
||||||
|
|
|
@ -14,7 +14,7 @@ require_once('Core.php');
|
||||||
<?= Template::FeedHowTo() ?>
|
<?= Template::FeedHowTo() ?>
|
||||||
<section id="opds-feeds">
|
<section id="opds-feeds">
|
||||||
<h2>OPDS 1.2 feeds</h2>
|
<h2>OPDS 1.2 feeds</h2>
|
||||||
<p><a href="https://en.wikipedia.org/wiki/Open_Publication_Distribution_System">OPDS feeds</a>, or “catalogs,” can be added to ereading apps on phones and tablets, or to ereading systems like <a href="http://koreader.rocks/">KOreader</a>. Add our OPDS feed to your ereading app to search, browse, and download from our entire catalog, directly in your ereader.</p>
|
<p><a href="https://en.wikipedia.org/wiki/Open_Publication_Distribution_System">OPDS feeds</a>, 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.</p>
|
||||||
<p>They’re also perfect for scripting, or for libraries or other organizations who wish to download and process our catalog of ebooks.</p>
|
<p>They’re also perfect for scripting, or for libraries or other organizations who wish to download and process our catalog of ebooks.</p>
|
||||||
<ul class="feed">
|
<ul class="feed">
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -2,96 +2,229 @@
|
||||||
require_once('Core.php');
|
require_once('Core.php');
|
||||||
|
|
||||||
use Safe\DateTime;
|
use Safe\DateTime;
|
||||||
|
use function Safe\apcu_fetch;
|
||||||
use function Safe\filemtime;
|
use function Safe\filemtime;
|
||||||
use function Safe\filesize;
|
use function Safe\filesize;
|
||||||
use function Safe\glob;
|
use function Safe\glob;
|
||||||
use function Safe\gmdate;
|
use function Safe\gmdate;
|
||||||
|
use function Safe\preg_match;
|
||||||
|
use function Safe\sort;
|
||||||
use function Safe\rsort;
|
use function Safe\rsort;
|
||||||
|
use function Safe\usort;
|
||||||
|
|
||||||
$ex = null;
|
$forbiddenException = null;
|
||||||
|
|
||||||
if(isset($_SERVER['PHP_AUTH_USER'])){
|
if(isset($_SERVER['PHP_AUTH_USER'])){
|
||||||
// We get here if the user entered an invalid HTTP Basic Auth username,
|
// We get here if the user entered an invalid HTTP Basic Auth username,
|
||||||
// and this page was served as the 401 page.
|
// 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');
|
// Process ebooks by year
|
||||||
rsort($files);
|
|
||||||
|
|
||||||
$years = [];
|
$years = [];
|
||||||
|
|
||||||
foreach($files as $file){
|
try{
|
||||||
$obj = new stdClass();
|
$years = apcu_fetch('bulk-downloads-years');
|
||||||
$date = new DateTime(str_replace('se-ebooks-', '', basename($file, '.zip')) . '-01');
|
}
|
||||||
$updated = new DateTime('@' . filemtime($file));
|
catch(Safe\Exceptions\ApcuException $ex){
|
||||||
$obj->Month = $date->format('F');
|
// Nothing in the cache, generate the files
|
||||||
$obj->Url = '/patrons-circle/downloads/' . basename($file);
|
|
||||||
$obj->Size = Formatter::ToFileSize(filesize($file));
|
$files = glob(WEB_ROOT . '/patrons-circle/downloads/months/*/*.zip');
|
||||||
$obj->Updated = $updated->format('M i');
|
rsort($files);
|
||||||
// The count of ebooks in each file is stored as a filesystem attribute
|
|
||||||
$obj->Count = exec('attr -g ebook-count ' . escapeshellarg($file)) ?: null;
|
foreach($files as $file){
|
||||||
if($obj->Count !== null){
|
$obj = new stdClass();
|
||||||
$obj->Count = intval($obj->Count);
|
$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')){
|
// Sort the downloads by filename extension
|
||||||
$obj->Updated = $obj->Updated . $updated->format(', Y');
|
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.']) ?>
|
?><?= Template::Header(['title' => 'Bulk Ebook Downloads', 'highlight' => '', 'description' => 'Download zip files containing all of the Standard Ebooks released in a given month.']) ?>
|
||||||
<main>
|
<main>
|
||||||
<section class="narrow bulk-downloads has-hero">
|
<section class="bulk-downloads has-hero">
|
||||||
<h1>Bulk Ebook Downloads</h1>
|
<h1>Bulk Ebook Downloads</h1>
|
||||||
<picture>
|
<picture>
|
||||||
<source srcset="/images/the-shop-of-the-bookdealer@2x.avif 2x, /images/the-shop-of-the-bookdealer.avif 1x" type="image/avif"/>
|
<source srcset="/images/the-shop-of-the-bookdealer@2x.avif 2x, /images/the-shop-of-the-bookdealer.avif 1x" type="image/avif"/>
|
||||||
<source srcset="/images/the-shop-of-the-bookdealer@2x.jpg 2x, /images/the-shop-of-the-bookdealer.jpg 1x" type="image/jpg"/>
|
<source srcset="/images/the-shop-of-the-bookdealer@2x.jpg 2x, /images/the-shop-of-the-bookdealer.jpg 1x" type="image/jpg"/>
|
||||||
<img src="/images/the-shop-of-the-bookdealer@2x.jpg" alt="A gentleman in regency-era dress buys books from a bookseller."/>
|
<img src="/images/the-shop-of-the-bookdealer@2x.jpg" alt="A gentleman in regency-era dress buys books from a bookseller."/>
|
||||||
</picture>
|
</picture>
|
||||||
<? if($ex !== null){ ?>
|
<? if($forbiddenException !== null){ ?>
|
||||||
<ul class="message error">
|
<ul class="message error">
|
||||||
<li>
|
<li>
|
||||||
<p><?= Formatter::ToPlainText($ex->getMessage()) ?></p>
|
<p><?= Formatter::ToPlainText($forbiddenException->getMessage()) ?></p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<? } ?>
|
<? } ?>
|
||||||
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks that were released in a given month of Standard Ebooks history. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks that were released in a given month of Standard Ebooks history. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
||||||
<p>These zip files contain each ebook in every format we offer, and are updated once daily with the latest versions of each ebook.</p>
|
<p>These zip files contain each ebook in every format we offer, and are updated once daily with the latest versions of each ebook.</p>
|
||||||
<p>If you’re a Patrons Circle member, when prompted enter your email address and leave the password blank to download these files.</p>
|
<p>If you’re a Patrons Circle member, when prompted enter your email address and leave the password blank to download these files.</p>
|
||||||
<ul class="download-list">
|
|
||||||
<? foreach($years as $year => $items){ ?>
|
<section id="downloads-by-subject">
|
||||||
<li>
|
<h2>Downloads by subject</h2>
|
||||||
<p class="header"><?= Formatter::ToPlainText((string)$year) ?></p>
|
<table class="download-list">
|
||||||
<table>
|
|
||||||
<thead>
|
<thead>
|
||||||
<td></td>
|
<tr class="mid-header">
|
||||||
<td>Ebooks</td>
|
<td></td>
|
||||||
<td>Size</td>
|
<th scope="col">Ebooks</th>
|
||||||
<td>Updated</td>
|
<th scope="col">Updated</th>
|
||||||
|
<th scope="col" colspan="10">Download</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<? foreach($items as $item){ ?>
|
<? foreach($subjects as $subject => $items){ ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a download="" href="<?= Formatter::ToPlainText($item->Url) ?>"><?= Formatter::ToPlainText($item->Month) ?></a></td>
|
<td class="row-header"><?= Formatter::ToPlainText($subject) ?></td>
|
||||||
<td><?= Formatter::ToPlainText(number_format($item->Count)) ?></td>
|
<td class="number"><?= Formatter::ToPlainText(number_format($items[0]->Count)) ?></td>
|
||||||
<td><?= Formatter::ToPlainText($item->Size) ?></td>
|
<td class="number"><?= Formatter::ToPlainText($items[0]->UpdatedString) ?></td>
|
||||||
<td><?= Formatter::ToPlainText($item->Updated) ?></td>
|
|
||||||
|
<? foreach($items as $item){ ?>
|
||||||
|
<td class="download"><a href="<?= $item->Url ?>" download=""><?= $item->Type ?></a></td>
|
||||||
|
<td>(<?= Formatter::ToPlainText($item->Size) ?>)</td>
|
||||||
|
<? } ?>
|
||||||
</tr>
|
</tr>
|
||||||
<? } ?>
|
<? } ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</li>
|
</section>
|
||||||
<? } ?>
|
|
||||||
</ul>
|
<section id="downloads-by-year">
|
||||||
|
<h2>Downloads by year</h2>
|
||||||
|
<table class="download-list">
|
||||||
|
<tbody>
|
||||||
|
<? foreach($years as $year => $months){
|
||||||
|
$yearHeader = Formatter::ToPlainText((string)$year);
|
||||||
|
?>
|
||||||
|
<tr class="year-header">
|
||||||
|
<th colspan="13" scope="colgroup" id="<?= $yearHeader ?>"><?= Formatter::ToPlainText((string)$year) ?></th>
|
||||||
|
</tr>
|
||||||
|
<tr class="mid-header">
|
||||||
|
<td></td>
|
||||||
|
<th id="<?= $yearHeader ?>-ebooks" scope="col">Ebooks</th>
|
||||||
|
<th id="<?= $yearHeader ?>-updated" scope="col">Updated</th>
|
||||||
|
<th id="<?= $yearHeader ?>-download" colspan="10" scope="col">Download</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<? foreach($months as $month => $items){
|
||||||
|
$monthHeader = $items[0]->Month;
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<th class="row-header" headers="<?= $yearHeader ?>" id="<?= $monthHeader ?>"><?= Formatter::ToPlainText($month) ?></th>
|
||||||
|
<td class="number" headers="<?= $yearHeader ?> <?= $monthHeader ?> <?= $yearHeader ?>-ebooks"><?= Formatter::ToPlainText(number_format($items[0]->Count)) ?></td>
|
||||||
|
<td class="number" headers="<?= $yearHeader ?> <?= $monthHeader ?> <?= $yearHeader ?>-updated"><?= Formatter::ToPlainText($items[0]->UpdatedString) ?></td>
|
||||||
|
<? foreach($items as $item){ ?>
|
||||||
|
<td headers="<?= $yearHeader ?> <?= $monthHeader ?> <?= $yearHeader ?>-download" class="download"><a href="<?= $item->Url ?>" download=""><?= $item->Type ?></a></td>
|
||||||
|
<td headers="<?= $yearHeader ?> <?= $monthHeader ?> <?= $yearHeader ?>-download">(<?= Formatter::ToPlainText($item->Size) ?>)</td>
|
||||||
|
<? } ?>
|
||||||
|
</tr>
|
||||||
|
<? } ?>
|
||||||
|
|
||||||
|
<? } ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<?= Template::Footer() ?>
|
<?= Template::Footer() ?>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue