Add placeholders for ebooks

This commit is contained in:
Mike Colagrosso 2024-12-13 11:45:14 -06:00 committed by Alex Cabal
parent cf5f488cae
commit 1ab95df084
52 changed files with 1192 additions and 237 deletions

View file

@ -41,14 +41,15 @@ use function Safe\shell_exec;
* @property string $TextSinglePageUrl
* @property string $TextSinglePageSizeFormatted
* @property string $IndexableText
* @property EbookPlaceholder $EbookPlaceholder
*/
class Ebook{
use Traits\Accessor;
public int $EbookId;
public string $Identifier;
public string $WwwFilesystemPath;
public string $RepoFilesystemPath;
public ?string $WwwFilesystemPath = null;
public ?string $RepoFilesystemPath = null;
public ?string $KindleCoverUrl = null;
public ?string $EpubUrl = null;
public ?string $AdvancedEpubUrl = null;
@ -58,17 +59,17 @@ class Ebook{
public string $Title;
public ?string $FullTitle = null;
public ?string $AlternateTitle = null;
public string $Description;
public string $LongDescription;
public string $Language;
public int $WordCount;
public float $ReadingEase;
public ?string $Description = null;
public ?string $LongDescription = null;
public ?string $Language = null;
public ?int $WordCount = null;
public ?float $ReadingEase = null;
public ?string $GitHubUrl = null;
public ?string $WikipediaUrl = null;
/** When the ebook was published. */
public DateTimeImmutable $EbookCreated;
public ?DateTimeImmutable $EbookCreated = null;
/** When the ebook was updated. */
public DateTimeImmutable $EbookUpdated;
public ?DateTimeImmutable $EbookUpdated = null;
/** When the database row was created. */
public DateTimeImmutable $Created;
/** When the database row was updated. */
@ -116,6 +117,7 @@ class Ebook{
protected string $_TextSinglePageUrl;
protected string $_TextSinglePageSizeFormatted;
protected string $_IndexableText;
protected ?EbookPlaceholder $_EbookPlaceholder = null;
// *******
// GETTERS
@ -318,7 +320,7 @@ class Ebook{
protected function GetUrl(): string{
if(!isset($this->_Url)){
$this->_Url = str_replace(WEB_ROOT, '', $this->WwwFilesystemPath);
$this->_Url = str_replace(EBOOKS_IDENTIFIER_ROOT, '', $this->Identifier);
}
return $this->_Url;
@ -554,7 +556,7 @@ class Ebook{
protected function GetTextSinglePageSizeFormatted(): string{
if(!isset($this->_TextSinglePageSizeFormatted)){
$bytes = $this->TextSinglePageByteCount;
$sizes = array('B', 'KB', 'MB', 'GB', 'TB', 'PB');
$sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
$index = 0;
while($bytes >= 1024 && $index < count($sizes) - 1){
@ -608,6 +610,18 @@ class Ebook{
return $this->_IndexableText;
}
protected function GetEbookPlaceholder(): ?EbookPlaceholder{
if(!isset($this->_EbookPlaceholder)){
$this->_EbookPlaceholder = Db::Query('
SELECT *
from EbookPlaceholders
where EbookId = ?
', [$this->EbookId], EbookPlaceholder::class)[0] ?? null;
}
return $this->_EbookPlaceholder;
}
// ***********
// ORM METHODS
@ -970,6 +984,59 @@ class Ebook{
return $ebook;
}
/**
* Joins the `Name` properites of `Contributor` objects as a URL slug, e.g.,
*
* ```
* ([0] => Contributor Object ([Name] => William Wordsworth), ([1] => Contributor Object ([Name] => Samuel Coleridge)))
* ```
*
* returns `william-wordsworth_samuel-taylor-coleridge`.
*
* @param array<Contributor> $contributors
*/
protected static function GetContributorsUrlSlug(array $contributors): string{
return implode('_', array_map('Formatter::MakeUrlSafe', array_column($contributors, 'Name')));
}
/**
* Populates the `Identifier` property based on the `Title`, `Authors`, `Translators`, and `Illustrators`. Used when creating ebook placeholders.
*
* @throws Exceptions\InvalidEbookIdentifierException
*/
public function FillIdentifierFromTitleAndContributors(): void{
if(!isset($this->Authors) || sizeof($this->Authors) == 0){
throw new Exceptions\InvalidEbookIdentifierException('Authors required');
}
if(!isset($this->Title)){
throw new Exceptions\InvalidEbookIdentifierException('Title required');
}
$authorString = Ebook::GetContributorsUrlSlug($this->Authors);
$titleString = Formatter::MakeUrlSafe($this->Title);
$translatorString = '';
$illustratorString = '';
if(isset($this->Translators) && sizeof($this->Translators) > 0){
$translatorString = Ebook::GetContributorsUrlSlug($this->Translators);
}
if(isset($this->Illustrators) && sizeof($this->Illustrators) > 0){
$illustratorString = Ebook::GetContributorsUrlSlug($this->Illustrators);
}
$this->Identifier = EBOOKS_IDENTIFIER_PREFIX . $authorString . '/' . $titleString;
if($translatorString != ''){
$this->Identifier .= '/' . $translatorString;
}
if($illustratorString != ''){
$this->Identifier .= '/' . $illustratorString;
}
}
// *******
// METHODS
@ -996,13 +1063,12 @@ class Ebook{
$error->Add(new Exceptions\EbookIdentifierRequiredException());
}
$this->WwwFilesystemPath = trim($this->WwwFilesystemPath ?? '');
if($this->WwwFilesystemPath == ''){
$this->WwwFilesystemPath = null;
}
if(isset($this->WwwFilesystemPath)){
$this->WwwFilesystemPath = trim($this->WwwFilesystemPath);
if($this->WwwFilesystemPath == ''){
$error->Add(new Exceptions\EbookWwwFilesystemPathRequiredException());
}
if(strlen($this->WwwFilesystemPath) > EBOOKS_MAX_LONG_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Ebook WwwFilesystemPath'));
}
@ -1011,17 +1077,13 @@ class Ebook{
$error->Add(new Exceptions\InvalidEbookWwwFilesystemPathException($this->WwwFilesystemPath));
}
}
else{
$error->Add(new Exceptions\EbookWwwFilesystemPathRequiredException());
$this->RepoFilesystemPath = trim($this->RepoFilesystemPath ?? '');
if($this->RepoFilesystemPath == ''){
$this->RepoFilesystemPath = null;
}
if(isset($this->RepoFilesystemPath)){
$this->RepoFilesystemPath = trim($this->RepoFilesystemPath);
if($this->RepoFilesystemPath == ''){
$error->Add(new Exceptions\EbookRepoFilesystemPathRequiredException());
}
if(strlen($this->RepoFilesystemPath) > EBOOKS_MAX_LONG_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Ebook RepoFilesystemPath'));
}
@ -1030,9 +1092,6 @@ class Ebook{
$error->Add(new Exceptions\InvalidEbookRepoFilesystemPathException($this->RepoFilesystemPath));
}
}
else{
$error->Add(new Exceptions\EbookRepoFilesystemPathRequiredException());
}
$this->KindleCoverUrl = trim($this->KindleCoverUrl ?? '');
if($this->KindleCoverUrl == ''){
@ -1157,51 +1216,36 @@ class Ebook{
$error->Add(new Exceptions\StringTooLongException('Ebook AlternateTitle'));
}
if(isset($this->Description)){
$this->Description = trim($this->Description);
if($this->Description == ''){
$error->Add(new Exceptions\EbookDescriptionRequiredException());
}
}
else{
$error->Add(new Exceptions\EbookDescriptionRequiredException());
$this->Description = trim($this->Description ?? '');
if($this->Description == ''){
$this->Description = null;
}
if(isset($this->LongDescription)){
$this->LongDescription = trim($this->LongDescription);
if($this->LongDescription == ''){
$error->Add(new Exceptions\EbookLongDescriptionRequiredException());
}
if(isset($this->Description) && strlen($this->Description) > EBOOKS_MAX_STRING_LENGTH){
$error->Add(new Exceptions\StringTooLongException('Ebook Description'));
}
else{
$error->Add(new Exceptions\EbookLongDescriptionRequiredException());
$this->LongDescription = trim($this->LongDescription ?? '');
if($this->LongDescription == ''){
$this->LongDescription = null;
}
$this->Language = trim($this->Language ?? '');
if($this->Language == ''){
$this->Language = null;
}
if(isset($this->Language)){
$this->Language = trim($this->Language);
if($this->Language == ''){
$error->Add(new Exceptions\EbookLanguageRequiredException());
}
if(strlen($this->Language) > 10){
$error->Add(new Exceptions\StringTooLongException('Ebook Language: ' . $this->Language));
}
}
else{
$error->Add(new Exceptions\EbookLanguageRequiredException());
}
if(isset($this->WordCount)){
if($this->WordCount <= 0){
$error->Add(new Exceptions\InvalidEbookWordCountException('Invalid Ebook WordCount: ' . $this->WordCount));
}
}
else{
$error->Add(new Exceptions\EbookWordCountRequiredException());
}
if(isset($this->ReadingEase)){
// In theory, Flesch reading ease can be negative, but in practice it's positive.
@ -1209,9 +1253,6 @@ class Ebook{
$error->Add(new Exceptions\InvalidEbookReadingEaseException('Invalid Ebook ReadingEase: ' . $this->ReadingEase));
}
}
else{
$error->Add(new Exceptions\EbookReadingEaseRequiredException());
}
$this->GitHubUrl = trim($this->GitHubUrl ?? '');
if($this->GitHubUrl == ''){
@ -1248,9 +1289,6 @@ class Ebook{
$error->Add(new Exceptions\InvalidEbookCreatedDatetimeException($this->EbookCreated));
}
}
else{
$error->Add(new Exceptions\EbookCreatedDatetimeRequiredException());
}
if(isset($this->EbookUpdated)){
if($this->EbookUpdated > NOW){
@ -1258,18 +1296,12 @@ class Ebook{
}
}
else{
$error->Add(new Exceptions\EbookUpdatedDatetimeRequiredException());
}
if(isset($this->TextSinglePageByteCount)){
if($this->TextSinglePageByteCount <= 0){
$error->Add(new Exceptions\InvalidEbookTextSinglePageByteCountException('Invalid Ebook TextSinglePageByteCount: ' . $this->TextSinglePageByteCount));
}
}
else{
$error->Add(new Exceptions\EbookTextSinglePageByteCountRequiredException());
}
if(isset($this->IndexableText)){
$this->IndexableText = trim($this->IndexableText ?? '');
@ -1282,6 +1314,23 @@ class Ebook{
$error->Add(new Exceptions\EbookIndexableTextRequiredException());
}
if(isset($this->EbookPlaceholder)){
try{
$this->EbookPlaceholder->Validate();
}
catch(Exceptions\ValidationException $ex){
$error->Add($ex);
}
}
if($this->IsPlaceholder() && !isset($this->EbookPlaceholder)){
$error->Add(new Exceptions\EbookMissingPlaceholderException());
}
if(!$this->IsPlaceholder() && isset($this->EbookPlaceholder)){
$error->Add(new Exceptions\EbookUnexpectedPlaceholderException());
}
if($error->HasExceptions){
throw $error;
}
@ -1289,6 +1338,7 @@ class Ebook{
/**
* @throws Exceptions\ValidationException
* @throws Exceptions\DuplicateEbookException
*/
public function CreateOrUpdate(): void{
try{
@ -1550,6 +1600,10 @@ class Ebook{
return $string;
}
public function IsPlaceholder(): bool{
return $this->WwwFilesystemPath === null;
}
/**
* If the given list of elements has an element that is not `''`, return that value; otherwise, return `null`.
*
@ -1572,10 +1626,19 @@ class Ebook{
/**
* @throws Exceptions\ValidationException
* @throws Exceptions\DuplicateEbookException If an `Ebook` with the given identifier already exists.
*/
public function Create(): void{
$this->Validate();
try{
Ebook::GetByIdentifier($this->Identifier);
throw new Exceptions\DuplicateEbookException($this->Identifier);
}
catch(Exceptions\EbookNotFoundException){
// Pass.
}
$this->CreateTags();
$this->CreateLocSubjects();
$this->CreateCollections();
@ -1623,6 +1686,7 @@ class Ebook{
$this->AddSources();
$this->AddContributors();
$this->AddTocEntries();
$this->AddEbookPlaceholder();
}
/**
@ -1690,6 +1754,9 @@ class Ebook{
$this->RemoveTocEntries();
$this->AddTocEntries();
$this->RemoveEbookPlaceholder();
$this->AddEbookPlaceholder();
}
private function RemoveTags(): void{
@ -1848,6 +1915,24 @@ class Ebook{
}
}
private function RemoveEbookPlaceholder(): void{
Db::Query('
DELETE from EbookPlaceholders
where EbookId = ?
', [$this->EbookId]
);
}
/**
* @throws Exceptions\ValidationException
*/
private function AddEbookPlaceholder(): void{
if(isset($this->EbookPlaceholder)){
$this->EbookPlaceholder->EbookId = $this->EbookId;
$this->EbookPlaceholder->Create();
}
}
// ***********
// ORM METHODS
// ***********
@ -1929,6 +2014,9 @@ class Ebook{
}
/**
* Queries for books in a collection.
*
* Puts ebooks without a `SequenceNumber` at the end of the list, which is more common in a collection with both published and placeholder ebooks.
* @return array<Ebook>
*/
public static function GetAllByCollection(string $collection): array{
@ -1938,13 +2026,16 @@ class Ebook{
inner join CollectionEbooks ce using (EbookId)
inner join Collections c using (CollectionId)
where c.UrlName = ?
order by ce.SequenceNumber, e.EbookCreated desc
order by ce.SequenceNumber is null, ce.SequenceNumber, e.EbookCreated desc
', [$collection], Ebook::class);
return $ebooks;
}
/**
* Queries for related to books to be shown, e.g., in a carousel.
*
* Filters out placeholder books because they are not useful for browsing.
* @return array<Ebook>
*/
public static function GetAllByRelated(Ebook $ebook, int $count, ?EbookTag $relatedTag): array{
@ -1955,6 +2046,7 @@ class Ebook{
inner join EbookTags et using (EbookId)
where et.TagId = ?
and et.EbookId != ?
and e.WwwFilesystemPath is not null
order by rand()
limit ?
', [$relatedTag->TagId, $ebook->EbookId, $count], Ebook::class);
@ -1964,6 +2056,7 @@ class Ebook{
SELECT *
from Ebooks
where EbookId != ?
and WwwFilesystemPath is not null
order by rand()
limit ?
', [$ebook->EbookId, $count], Ebook::class);
@ -1977,25 +2070,43 @@ class Ebook{
*
* @return array{ebooks: array<Ebook>, ebooksCount: int}
*/
public static function GetAllByFilter(string $query = null, array $tags = [], Enums\EbookSortType $sort = null, int $page = 1, int $perPage = EBOOKS_PER_PAGE): array{
public static function GetAllByFilter(string $query = null, array $tags = [], Enums\EbookSortType $sort = null, int $page = 1, int $perPage = EBOOKS_PER_PAGE, Enums\EbookReleaseStatusFilter $releaseStatusFilter = Enums\EbookReleaseStatusFilter::All): array{
$limit = $perPage;
$offset = (($page - 1) * $perPage);
$joinContributors = '';
$joinTags = '';
$params = [];
$whereCondition = 'where true';
switch($releaseStatusFilter){
case Enums\EbookReleaseStatusFilter::Released:
$whereCondition = 'where e.WwwFilesystemPath is not null';
break;
case Enums\EbookReleaseStatusFilter::Placeholder:
$whereCondition = 'where e.WwwFilesystemPath is null';
break;
case Enums\EbookReleaseStatusFilter::All:
default:
if($query !== null && $query != ''){
// If the query is present, show both released and placeholder ebooks.
$whereCondition = 'where true';
}else{
// If there is no query, hide placeholder ebooks.
$whereCondition = 'where e.WwwFilesystemPath is not null';
}
break;
}
$orderBy = 'e.EbookCreated desc';
if($sort == Enums\EbookSortType::AuthorAlpha){
$joinContributors = 'inner join Contributors con using (EbookId)';
$whereCondition .= ' and con.MarcRole = "aut"';
$orderBy = 'con.SortName, e.EbookCreated desc';
$orderBy = 'e.WwwFilesystemPath is null, con.SortName, e.EbookCreated desc'; // Put placeholders at the end
}
elseif($sort == Enums\EbookSortType::ReadingEase){
$orderBy = 'e.ReadingEase desc';
}
elseif($sort == Enums\EbookSortType::Length){
$orderBy = 'e.WordCount';
$orderBy = 'e.WwwFilesystemPath is null, e.WordCount'; // Put placeholders at the end
}
if(sizeof($tags) > 0 && !in_array('all', $tags)){ // 0 tags means "all ebooks"