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/index.php
|
||||
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>
|
||||
<a href="https://github.com/standardebooks">GitHub</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/patrons-circle/downloads">Bulk downloads</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/feeds">Ebook Feeds</a>
|
||||
</li>
|
||||
|
|
|
@ -246,6 +246,7 @@ main > section.narrow > *{
|
|||
main > section.narrow > h1,
|
||||
main > section.narrow > hgroup,
|
||||
main > section.narrow.has-hero > picture{
|
||||
box-sizing: border-box;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
|
@ -671,10 +672,49 @@ ul.message.error li:only-child{
|
|||
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,
|
||||
a.button:focus,
|
||||
input[type="email"]:focus,
|
||||
input[type="text"]:focus,
|
||||
input[type="month"]:focus,
|
||||
input[type="search"]:focus,
|
||||
label.checkbox:focus-within,
|
||||
select:focus,
|
||||
|
@ -1199,6 +1239,11 @@ footer > p{
|
|||
display: inlie-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
footer > p:first-child{
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
footer > p:first-child::before{
|
||||
font-family: "Fork Awesome";
|
||||
content: "\f0e0";
|
||||
|
@ -1571,6 +1616,7 @@ label.search{
|
|||
|
||||
select,
|
||||
input[type="text"],
|
||||
input[type="month"],
|
||||
input[type="email"],
|
||||
input[type="search"]{
|
||||
-webkit-appearance: none;
|
||||
|
@ -1674,6 +1720,7 @@ nav a[rel],
|
|||
a.button,
|
||||
button,
|
||||
input[type="text"],
|
||||
input[type="month"],
|
||||
input[type="email"],
|
||||
input[type="search"],
|
||||
select{
|
||||
|
@ -1689,6 +1736,8 @@ button:hover{
|
|||
|
||||
input[type="text"]:focus,
|
||||
input[type="text"]:hover,
|
||||
input[type="month"]:focus,
|
||||
input[type="month"]:hover,
|
||||
input[type="email"]:focus,
|
||||
input[type="email"]:hover,
|
||||
input[type="search"]:focus,
|
||||
|
@ -1701,6 +1750,7 @@ select:hover{
|
|||
}
|
||||
|
||||
input[type="text"]:user-invalid,
|
||||
input[type="month"]:user-invalid,
|
||||
input[type="email"]:user-invalid,
|
||||
input[type="search"]:user-invalid{
|
||||
border-color: #ff0000;
|
||||
|
@ -1708,6 +1758,7 @@ input[type="search"]:user-invalid{
|
|||
}
|
||||
|
||||
input[type="text"]:-moz-ui-invalid,
|
||||
input[type="month"]:-moz-ui-invalid,
|
||||
input[type="email"]:-moz-ui-invalid,
|
||||
input[type="search"]:-moz-ui-invalid{
|
||||
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){
|
||||
form[action="/ebooks"]{
|
||||
grid-template-columns: auto auto 1fr 1fr;
|
||||
|
@ -3119,6 +3176,10 @@ ul.feed p{
|
|||
.votes tr + tr{
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.bulk-downloads > ul{
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 470px){
|
||||
|
@ -3281,6 +3342,7 @@ ul.feed p{
|
|||
a.button,
|
||||
button,
|
||||
input[type="text"],
|
||||
input[type="month"],
|
||||
input[type="email"],
|
||||
input[type="search"],
|
||||
select,
|
||||
|
@ -3290,6 +3352,8 @@ ul.feed p{
|
|||
button:hover,
|
||||
input[type="text"]:focus,
|
||||
input[type="text"]:hover,
|
||||
input[type="month"]:focus,
|
||||
input[type="month"]:hover,
|
||||
input[type="email"]:focus,
|
||||
input[type="email"]:hover,
|
||||
input[type="search"]:focus,
|
||||
|
|
|
@ -55,6 +55,7 @@ article.ebook section aside.donation{
|
|||
|
||||
select,
|
||||
input[type="text"],
|
||||
input[type="month"],
|
||||
input[type="email"],
|
||||
input[type="search"]{
|
||||
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