Add bulk download page

This commit is contained in:
Alex Cabal 2022-07-08 22:09:18 -05:00
parent 6c603016bd
commit 8090f3f9f7
14 changed files with 224 additions and 0 deletions

1
.gitignore vendored
View file

@ -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

View 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);
}
}

View file

@ -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>

View file

@ -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,

View file

@ -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;

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View 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 youre 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() ?>