mirror of
https://github.com/standardebooks/web.git
synced 2025-07-15 19:06:49 -04:00
Add bulk download page
This commit is contained in:
parent
6c603016bd
commit
8090f3f9f7
14 changed files with 224 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -14,3 +14,4 @@ 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
|
||||||
|
|
97
scripts/generate-monthly-downloads
Executable file
97
scripts/generate-monthly-downloads
Executable file
|
@ -0,0 +1,97 @@
|
||||||
|
#!/usr/bin/php
|
||||||
|
<?
|
||||||
|
require_once('/standardebooks.org/web/lib/Core.php');
|
||||||
|
|
||||||
|
$longopts = ['webroot:', 'weburl:'];
|
||||||
|
$options = getopt('', $longopts);
|
||||||
|
$webRoot = $options['webroot'] ?? '/standardebooks.org/web';
|
||||||
|
$webUrl = $options['weburl'] ?? 'https://standardebooks.org';
|
||||||
|
|
||||||
|
$contentFiles = explode("\n", trim(shell_exec('find ' . escapeshellarg($webRoot . '/www/ebooks/') . ' -name "content.opf" | sort') ?? ''));
|
||||||
|
$ebooksByMonth = [];
|
||||||
|
$lastUpdatedTimestamps = [];
|
||||||
|
|
||||||
|
if(!is_dir(WEB_ROOT . '/patrons-circle/downloads')){
|
||||||
|
mkdir(WEB_ROOT . '/patrons-circle/downloads');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over all ebooks and arrange them by publication month
|
||||||
|
foreach($contentFiles as $path){
|
||||||
|
if($path == '')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$ebookWwwFilesystemPath = '';
|
||||||
|
|
||||||
|
try{
|
||||||
|
$ebookWwwFilesystemPath = preg_replace('|/content\.opf|ius', '', $path);
|
||||||
|
|
||||||
|
$ebook = new Ebook($ebookWwwFilesystemPath);
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(\Exception $ex){
|
||||||
|
print('Failed to generate download for `' . $ebookWwwFilesystemPath . '`. Exception: ' . $ex->getMessage());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($ebooksByMonth as $month => $ebooks){
|
||||||
|
$filename = 'se-ebooks-' . $month . '.zip';
|
||||||
|
$filePath = $webRoot . '/www/patrons-circle/downloads/' . $filename;
|
||||||
|
|
||||||
|
// If the file doesn't exist, or if the content.opf last updated time is newer than the file creation 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 . '/www' . $ebook->EpubUrl;
|
||||||
|
$zip->addFile($ebookFilePath, $ebook->UrlSafeIdentifier . '/' . basename($ebookFilePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($ebook->Azw3Url !== null){
|
||||||
|
$ebookFilePath = $webRoot . '/www' . $ebook->Azw3Url;
|
||||||
|
$zip->addFile($ebookFilePath, $ebook->UrlSafeIdentifier . '/' . basename($ebookFilePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($ebook->KepubUrl !== null){
|
||||||
|
$ebookFilePath = $webRoot . '/www' . $ebook->KepubUrl;
|
||||||
|
$zip->addFile($ebookFilePath, $ebook->UrlSafeIdentifier . '/' . basename($ebookFilePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($ebook->AdvancedEpubUrl !== null){
|
||||||
|
$ebookFilePath = $webRoot . '/www' . $ebook->AdvancedEpubUrl;
|
||||||
|
$zip->addFile($ebookFilePath, $ebook->UrlSafeIdentifier . '/' . basename($ebookFilePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($ebook->TextSinglePageUrl !== null){
|
||||||
|
$ebookFilePath = $webRoot . '/www' . $ebook->TextSinglePageUrl . '.xhtml';
|
||||||
|
$zip->addFile($ebookFilePath, $ebook->UrlSafeIdentifier . '/' . str_replace('single-page', $ebook->UrlSafeIdentifier, basename($ebookFilePath)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
rename($tempFilename, $filePath);
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,9 @@
|
||||||
<li>
|
<li>
|
||||||
<a href="https://github.com/standardebooks">GitHub</a>
|
<a href="https://github.com/standardebooks">GitHub</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/patrons-circle/downloads">Bulk downloads</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/feeds">Ebook Feeds</a>
|
<a href="/feeds">Ebook Feeds</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -246,6 +246,7 @@ main > section.narrow > *{
|
||||||
main > section.narrow > h1,
|
main > section.narrow > h1,
|
||||||
main > section.narrow > hgroup,
|
main > section.narrow > hgroup,
|
||||||
main > section.narrow.has-hero > picture{
|
main > section.narrow.has-hero > picture{
|
||||||
|
box-sizing: border-box;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -671,10 +672,49 @@ ul.message.error li:only-child{
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bulk-downloads > p{
|
||||||
|
width: 100%;
|
||||||
|
max-width: 40rem;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-downloads i{
|
||||||
|
color: var(--sub-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-downloads > ul{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 4rem;
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 4rem;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-downloads > ul > li{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-downloads ul ul{
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-downloads > ul > li li:first-of-type{
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-downloads h2{
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.ebooks nav li.highlighted a:focus,
|
.ebooks nav li.highlighted a:focus,
|
||||||
a.button:focus,
|
a.button:focus,
|
||||||
input[type="email"]:focus,
|
input[type="email"]:focus,
|
||||||
input[type="text"]:focus,
|
input[type="text"]:focus,
|
||||||
|
input[type="month"]:focus,
|
||||||
input[type="search"]:focus,
|
input[type="search"]:focus,
|
||||||
label.checkbox:focus-within,
|
label.checkbox:focus-within,
|
||||||
select:focus,
|
select:focus,
|
||||||
|
@ -1199,6 +1239,11 @@ footer > p{
|
||||||
display: inlie-flex;
|
display: inlie-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
footer > p:first-child{
|
||||||
|
font-size: .9rem;
|
||||||
|
}
|
||||||
|
|
||||||
footer > p:first-child::before{
|
footer > p:first-child::before{
|
||||||
font-family: "Fork Awesome";
|
font-family: "Fork Awesome";
|
||||||
content: "\f0e0";
|
content: "\f0e0";
|
||||||
|
@ -1571,6 +1616,7 @@ label.search{
|
||||||
|
|
||||||
select,
|
select,
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
|
input[type="month"],
|
||||||
input[type="email"],
|
input[type="email"],
|
||||||
input[type="search"]{
|
input[type="search"]{
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
@ -1674,6 +1720,7 @@ nav a[rel],
|
||||||
a.button,
|
a.button,
|
||||||
button,
|
button,
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
|
input[type="month"],
|
||||||
input[type="email"],
|
input[type="email"],
|
||||||
input[type="search"],
|
input[type="search"],
|
||||||
select{
|
select{
|
||||||
|
@ -1689,6 +1736,8 @@ button:hover{
|
||||||
|
|
||||||
input[type="text"]:focus,
|
input[type="text"]:focus,
|
||||||
input[type="text"]:hover,
|
input[type="text"]:hover,
|
||||||
|
input[type="month"]:focus,
|
||||||
|
input[type="month"]:hover,
|
||||||
input[type="email"]:focus,
|
input[type="email"]:focus,
|
||||||
input[type="email"]:hover,
|
input[type="email"]:hover,
|
||||||
input[type="search"]:focus,
|
input[type="search"]:focus,
|
||||||
|
@ -1701,6 +1750,7 @@ select:hover{
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"]:user-invalid,
|
input[type="text"]:user-invalid,
|
||||||
|
input[type="month"]:user-invalid,
|
||||||
input[type="email"]:user-invalid,
|
input[type="email"]:user-invalid,
|
||||||
input[type="search"]:user-invalid{
|
input[type="search"]:user-invalid{
|
||||||
border-color: #ff0000;
|
border-color: #ff0000;
|
||||||
|
@ -1708,6 +1758,7 @@ input[type="search"]:user-invalid{
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"]:-moz-ui-invalid,
|
input[type="text"]:-moz-ui-invalid,
|
||||||
|
input[type="month"]:-moz-ui-invalid,
|
||||||
input[type="email"]:-moz-ui-invalid,
|
input[type="email"]:-moz-ui-invalid,
|
||||||
input[type="search"]:-moz-ui-invalid{
|
input[type="search"]:-moz-ui-invalid{
|
||||||
border-color: #ff0000;
|
border-color: #ff0000;
|
||||||
|
@ -2824,6 +2875,12 @@ ul.feed p{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media(max-width: 800px){
|
||||||
|
.bulk-downloads > ul{
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media(max-width: 730px){
|
@media(max-width: 730px){
|
||||||
form[action="/ebooks"]{
|
form[action="/ebooks"]{
|
||||||
grid-template-columns: auto auto 1fr 1fr;
|
grid-template-columns: auto auto 1fr 1fr;
|
||||||
|
@ -3119,6 +3176,10 @@ ul.feed p{
|
||||||
.votes tr + tr{
|
.votes tr + tr{
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bulk-downloads > ul{
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(max-width: 470px){
|
@media(max-width: 470px){
|
||||||
|
@ -3281,6 +3342,7 @@ ul.feed p{
|
||||||
a.button,
|
a.button,
|
||||||
button,
|
button,
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
|
input[type="month"],
|
||||||
input[type="email"],
|
input[type="email"],
|
||||||
input[type="search"],
|
input[type="search"],
|
||||||
select,
|
select,
|
||||||
|
@ -3290,6 +3352,8 @@ ul.feed p{
|
||||||
button:hover,
|
button:hover,
|
||||||
input[type="text"]:focus,
|
input[type="text"]:focus,
|
||||||
input[type="text"]:hover,
|
input[type="text"]:hover,
|
||||||
|
input[type="month"]:focus,
|
||||||
|
input[type="month"]:hover,
|
||||||
input[type="email"]:focus,
|
input[type="email"]:focus,
|
||||||
input[type="email"]:hover,
|
input[type="email"]:hover,
|
||||||
input[type="search"]:focus,
|
input[type="search"]:focus,
|
||||||
|
|
|
@ -55,6 +55,7 @@ article.ebook section aside.donation{
|
||||||
|
|
||||||
select,
|
select,
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
|
input[type="month"],
|
||||||
input[type="email"],
|
input[type="email"],
|
||||||
input[type="search"]{
|
input[type="search"]{
|
||||||
box-shadow: 1px 1px 0 rgba(0, 0, 0, .5) inset;
|
box-shadow: 1px 1px 0 rgba(0, 0, 0, .5) inset;
|
||||||
|
|
BIN
www/images/still-life-with-books.avif
Normal file
BIN
www/images/still-life-with-books.avif
Normal file
Binary file not shown.
BIN
www/images/still-life-with-books.jpg
Normal file
BIN
www/images/still-life-with-books.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
BIN
www/images/still-life-with-books@2x.avif
Normal file
BIN
www/images/still-life-with-books@2x.avif
Normal file
Binary file not shown.
BIN
www/images/still-life-with-books@2x.jpg
Normal file
BIN
www/images/still-life-with-books@2x.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 169 KiB |
BIN
www/images/the-shop-of-the-bookdealer.avif
Normal file
BIN
www/images/the-shop-of-the-bookdealer.avif
Normal file
Binary file not shown.
BIN
www/images/the-shop-of-the-bookdealer.jpg
Normal file
BIN
www/images/the-shop-of-the-bookdealer.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
BIN
www/images/the-shop-of-the-bookdealer@2x.avif
Normal file
BIN
www/images/the-shop-of-the-bookdealer@2x.avif
Normal file
Binary file not shown.
BIN
www/images/the-shop-of-the-bookdealer@2x.jpg
Normal file
BIN
www/images/the-shop-of-the-bookdealer@2x.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
58
www/patrons-circle/downloads/index.php
Normal file
58
www/patrons-circle/downloads/index.php
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?
|
||||||
|
require_once('Core.php');
|
||||||
|
|
||||||
|
$files = glob(WEB_ROOT . '/patrons-circle/downloads/*.zip');
|
||||||
|
rsort($files);
|
||||||
|
|
||||||
|
$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->Updated = $updated->format('M i');
|
||||||
|
|
||||||
|
if($updated->format('Y') != gmdate('Y')){
|
||||||
|
$obj->Updated = $obj->Updated . $updated->format(', Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
$year = $date->format('Y');
|
||||||
|
|
||||||
|
if(!isset($years[$year])){
|
||||||
|
$years[$year] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$years[$year][] = $obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
?><?= Template::Header(['title' => 'Download Ebooks by Month', 'highlight' => '', 'description' => 'Download zip files containing all of the Standard Ebooks released in a given month.']) ?>
|
||||||
|
<main>
|
||||||
|
<section class="bulk-downloads has-hero">
|
||||||
|
<h1>Download Ebooks by Month</h1>
|
||||||
|
<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.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 pile of moldering books."/>
|
||||||
|
</picture>
|
||||||
|
<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>If you’re a Patrons Circle member, when prompted enter your email address and no password to download these files.</p>
|
||||||
|
<ul>
|
||||||
|
<? foreach($years as $year => $items){ ?>
|
||||||
|
<li>
|
||||||
|
<section>
|
||||||
|
<h2><?= Formatter::ToPlainText($year) ?></h2>
|
||||||
|
<ul>
|
||||||
|
<? foreach($items as $item){ ?>
|
||||||
|
<li><p><a href="<?= Formatter::ToPlainText($item->Url) ?>"><?= Formatter::ToPlainText($item->Month) ?></a> <i>(Updated <?= Formatter::ToPlainText($obj->Updated) ?>)</i></p></li>
|
||||||
|
<? } ?>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</li>
|
||||||
|
<? } ?>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<?= Template::Footer() ?>
|
Loading…
Add table
Add a link
Reference in a new issue