From 63d411a2e6179b6dbf5fb58d6a422438a28d779b Mon Sep 17 00:00:00 2001 From: Mike Colagrosso Date: Sat, 20 Apr 2024 23:21:48 -0600 Subject: [PATCH] Initial code changes to insert/update Ebook records --- lib/Collection.php | 4 +- lib/Constants.php | 2 + lib/Ebook.php | 405 +++++++++++++++++- lib/EbookSourceType.php | 20 +- lib/EbookTag.php | 54 ++- .../EbookIdentifierRequiredException.php | 5 + .../EbookTitleRequiredException.php | 5 + lib/Library.php | 18 + lib/LocSubject.php | 43 ++ lib/Tag.php | 3 +- 10 files changed, 531 insertions(+), 28 deletions(-) create mode 100644 lib/Exceptions/EbookIdentifierRequiredException.php create mode 100644 lib/Exceptions/EbookTitleRequiredException.php create mode 100644 lib/LocSubject.php diff --git a/lib/Collection.php b/lib/Collection.php index 7f09ba6d..63e99700 100644 --- a/lib/Collection.php +++ b/lib/Collection.php @@ -3,13 +3,15 @@ use function Safe\preg_replace; class Collection{ public string $Name; + public string $UrlName; public string $Url; public ?int $SequenceNumber = null; public ?string $Type = null; public function __construct(string $name){ $this->Name = $name; - $this->Url = '/collections/' . Formatter::MakeUrlSafe($this->Name); + $this->UrlName = Formatter::MakeUrlSafe($this->Name); + $this->Url = '/collections/' . $this->UrlName; } public function GetSortedName(): string{ diff --git a/lib/Constants.php b/lib/Constants.php index 7d112cb0..fb246cd9 100644 --- a/lib/Constants.php +++ b/lib/Constants.php @@ -31,6 +31,8 @@ const DATABASE_DEFAULT_DATABASE = 'se'; const DATABASE_DEFAULT_HOST = 'localhost'; const EBOOKS_PER_PAGE = 12; +const EBOOKS_MAX_STRING_LENGTH = 250; +const EBOOK_SINGLE_PAGE_SIZE_WARNING = 3 *1024 * 1024; // 3145728 const ARTWORK_THUMBNAIL_HEIGHT = 350; const ARTWORK_THUMBNAIL_WIDTH = 350; diff --git a/lib/Ebook.php b/lib/Ebook.php index fd494372..84184ca1 100644 --- a/lib/Ebook.php +++ b/lib/Ebook.php @@ -11,7 +11,23 @@ use function Safe\preg_replace; use function Safe\sprintf; use function Safe\shell_exec; +/** + * @property array $GitCommits + * @property array $EbookTags + * @property array $LocSubjects + * @property array $Collections + * @property array $Sources + * @property array $Authors + * @property array $Illustrators + * @property array $Translators + * @property array $Contributors + * @property ?array $TocEntries + * @property string $IndexableText + */ class Ebook{ + use Traits\Accessor; + + public ?int $EbookId = null; public string $WwwFilesystemPath; public string $RepoFilesystemPath; public string $Url; @@ -25,8 +41,8 @@ class Ebook{ public $GitCommits = []; /** @var array $Tags */ public $Tags = []; - /** @var array $LocTags */ - public $LocTags = []; + /** @var array $LocSubjects */ + public $LocSubjects = []; /** @var array $Collections */ public $Collections = []; public string $Identifier; @@ -72,8 +88,49 @@ class Ebook{ public ?string $TextSinglePageUrl; public ?string $TextSinglePageSizeNumber = null; public ?string $TextSinglePageSizeUnit = null; + public ?int $TextSinglePageByteCount = null; /** @var ?array $TocEntries */ public $TocEntries = null; // A list of non-Roman ToC entries ONLY IF the work has the 'se:is-a-collection' metadata element, null otherwise + protected ?string $_IndexableText = null; + + // ******* + // GETTERS + // ******* + + protected function GetIndexableText(): string{ + if($this->_IndexableText === null){ + $this->_IndexableText = $this->FullTitle ?? $this->Title; + + $this->_IndexableText .= ' ' . $this->AlternateTitle; + + foreach($this->Collections as $collection){ + $this->_IndexableText .= ' ' . $collection->Name; + } + + foreach($this->Authors as $author){ + $this->_IndexableText .= ' ' . $author->Name; + } + + foreach($this->Tags as $tag){ + $this->_IndexableText .= ' ' . $tag->Name; + } + + foreach($this->LocSubjects as $subject){ + $this->_IndexableText .= ' ' . $subject->Name; + } + + if($this->TocEntries !== null){ + foreach($this->TocEntries as $item){ + $this->_IndexableText .= ' ' . $item; + } + } + + $this->_IndexableText .= ' ' . $this->Description; + $this->_IndexableText .= ' ' . $this->LongDescription; + } + + return $this->_IndexableText; + } /** * @throws Exceptions\EbookNotFoundException @@ -130,11 +187,11 @@ class Ebook{ try{ // PHP Safe throws an exception from filesize() if the file doesn't exist, but PHP still // emits a warning. So, just silence the warning. - $bytes = @filesize($this->WwwFilesystemPath . '/text/single-page.xhtml'); + $this->TextSinglePageByteCount = @filesize($this->WwwFilesystemPath . '/text/single-page.xhtml'); $sizes = 'BKMGTP'; - $factor = intval(floor((strlen((string)$bytes) - 1) / 3)); + $factor = intval(floor((strlen((string)$this->TextSinglePageByteCount) - 1) / 3)); try{ - $this->TextSinglePageSizeNumber = sprintf('%.1f', $bytes / pow(1024, $factor)); + $this->TextSinglePageSizeNumber = sprintf('%.1f', $this->TextSinglePageByteCount / pow(1024, $factor)); } catch(\DivisionByZeroError){ $this->TextSinglePageSizeNumber = '0'; @@ -250,7 +307,9 @@ class Ebook{ // Get SE tags foreach($xml->xpath('/package/metadata/meta[@property="se:subject"]') ?: [] as $tag){ - $this->Tags[] = new EbookTag($tag); + $ebookTag = new EbookTag(); + $ebookTag->Name = $tag; + $this->Tags[] = $ebookTag; } $includeToc = sizeof($xml->xpath('/package/metadata/meta[@property="se:is-a-collection"]') ?: []) > 0; @@ -285,8 +344,10 @@ class Ebook{ } // Get LoC tags - foreach($xml->xpath('/package/metadata/dc:subject') ?: [] as $tag){ - $this->LocTags[] = (string)$tag; + foreach($xml->xpath('/package/metadata/dc:subject') ?: [] as $subject){ + $locSubject = new LocSubject(); + $locSubject->Name = $subject; + $this->LocSubjects[] = $locSubject; } // Figure out authors and contributors @@ -519,6 +580,60 @@ class Ebook{ // METHODS // ******* + public function Validate(): void{ + $error = new Exceptions\ValidationException(); + + if($this->Identifier === null || $this->Identifier == ''){ + $error->Add(new Exceptions\EbookIdentifierRequiredException()); + } + + if($this->Identifier !== null && strlen($this->Identifier) > EBOOKS_MAX_STRING_LENGTH){ + $error->Add(new Exceptions\StringTooLongException('Ebook Identifier')); + } + + if($this->Title === null || $this->Title == ''){ + $error->Add(new Exceptions\EbookTitleRequiredException()); + } + + if($this->Title !== null && strlen($this->Title) > EBOOKS_MAX_STRING_LENGTH){ + $error->Add(new Exceptions\StringTooLongException('Ebook Title')); + } + + // TODO: Add more validation. + + if($error->HasExceptions){ + throw $error; + } + } + + public function CreateOrUpdate(): void{ + $existingEbook = Library::GetEbookByIdentifier($this->Identifier); + + if($existingEbook === null){ + $this->Create(); + return; + } + + $this->EbookId = $existingEbook->EbookId; + $this->Save(); + } + + private function InsertTagStrings(): void{ + $tags = []; + foreach($this->Tags as $ebookTag){ + $tags[] = EbookTag::GetOrCreate($ebookTag); + } + $this->Tags = $tags; + } + + private function InsertLocSubjectStrings(): void{ + $subjects = []; + foreach($this->LocSubjects as $locSubject){ + $subjects[] = LocSubject::GetOrCreate($locSubject); + } + $this->LocSubjects = $subjects; + } + public function GetCollectionPosition(Collection $collection): ?int{ foreach($this->Collections as $c){ if($c->Name == $collection->Name){ @@ -549,8 +664,8 @@ class Ebook{ $searchString .= ' ' . $tag->Name; } - foreach($this->LocTags as $tag){ - $searchString .= ' ' . $tag; + foreach($this->LocSubjects as $subject){ + $searchString .= ' ' . $subject->Name; } if($this->TocEntries !== null){ @@ -816,4 +931,274 @@ class Ebook{ return false; } + + // *********** + // ORM METHODS + // *********** + + public function Create(): void{ + $this->Validate(); + + $this->InsertTagStrings(); + $this->InsertLocSubjectStrings(); + + Db::Query(' + INSERT into Ebooks (Identifier, WwwFilesystemPath, RepoFilesystemPath, KindleCoverUrl, EpubUrl, + AdvancedEpubUrl, KepubUrl, Azw3Url, DistCoverUrl, Title, FullTitle, AlternateTitle, + Description, LongDescription, Language, WordCount, ReadingEase, GitHubUrl, WikipediaUrl, + EbookCreated, EbookUpdated, TextSinglePageByteCount, IndexableText) + values (?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?) + ', [$this->Identifier, $this->WwwFilesystemPath, $this->RepoFilesystemPath, $this->KindleCoverUrl, $this->EpubUrl, + $this->AdvancedEpubUrl, $this->KepubUrl, $this->Azw3Url, $this->DistCoverUrl, $this->Title, + $this->FullTitle, $this->AlternateTitle, $this->Description, $this->LongDescription, + $this->Language, $this->WordCount, $this->ReadingEase, $this->GitHubUrl, $this->WikipediaUrl, + $this->Created, $this->Updated, $this->TextSinglePageByteCount, $this->IndexableText]); + + $this->EbookId = Db::GetLastInsertedId(); + + $this->InsertTags(); + $this->InsertLocSubjects(); + $this->InsertGitCommits(); + $this->InsertCollections(); + $this->InsertSources(); + $this->InsertContributors(); + $this->InsertTocEntries(); + } + + public function Save(): void{ + $this->Validate(); + + $this->InsertTagStrings(); + $this->InsertLocSubjectStrings(); + + Db::Query(' + UPDATE Ebooks + set + Identifier = ?, + WwwFilesystemPath = ?, + RepoFilesystemPath = ?, + KindleCoverUrl = ?, + EpubUrl = ?, + AdvancedEpubUrl = ?, + KepubUrl = ?, + Azw3Url = ?, + DistCoverUrl = ?, + Title = ?, + FullTitle = ?, + AlternateTitle = ?, + Description = ?, + LongDescription = ?, + Language = ?, + WordCount = ?, + ReadingEase = ?, + GitHubUrl = ?, + WikipediaUrl = ?, + EbookCreated = ?, + EbookUpdated = ?, + TextSinglePageByteCount = ?, + IndexableText = ? + where + EbookId = ? + ', [$this->Identifier, $this->WwwFilesystemPath, $this->RepoFilesystemPath, $this->KindleCoverUrl, $this->EpubUrl, + $this->AdvancedEpubUrl, $this->KepubUrl, $this->Azw3Url, $this->DistCoverUrl, $this->Title, + $this->FullTitle, $this->AlternateTitle, $this->Description, $this->LongDescription, + $this->Language, $this->WordCount, $this->ReadingEase, $this->GitHubUrl, $this->WikipediaUrl, + $this->Created, $this->Updated, $this->TextSinglePageByteCount, $this->IndexableText, + $this->EbookId]); + + $this->DeleteTags(); + $this->InsertTags(); + + $this->DeleteLocSubjects(); + $this->InsertLocSubjects(); + + $this->DeleteGitCommits(); + $this->InsertGitCommits(); + + $this->DeleteCollections(); + $this->InsertCollections(); + + $this->DeleteSources(); + $this->InsertSources(); + + $this->DeleteContributors(); + $this->InsertContributors(); + + $this->DeleteTocEntries(); + $this->InsertTocEntries(); + } + + private function DeleteTags(): void{ + Db::Query(' + DELETE from EbookTags + where + EbookId = ? + ', [$this->EbookId] + ); + } + + private function InsertTags(): void{ + foreach($this->Tags as $tag){ + Db::Query(' + INSERT into EbookTags (EbookId, TagId) + values (?, + ?) + ', [$this->EbookId, $tag->TagId]); + } + } + + private function DeleteLocSubjects(): void{ + Db::Query(' + DELETE from EbookLocSubjects + where + EbookId = ? + ', [$this->EbookId] + ); + } + + private function InsertLocSubjects(): void{ + foreach($this->LocSubjects as $locSubject){ + Db::Query(' + INSERT into EbookLocSubjects (EbookId, LocSubjectId) + values (?, + ?) + ', [$this->EbookId, $locSubject->LocSubjectId]); + } + } + + private function DeleteGitCommits(): void{ + Db::Query(' + DELETE from GitCommits + where + EbookId = ? + ', [$this->EbookId] + ); + } + + private function InsertGitCommits(): void{ + foreach($this->GitCommits as $commit){ + Db::Query(' + INSERT into GitCommits (EbookId, Created, Message, Hash) + values (?, + ?, + ?, + ?) + ', [$this->EbookId, $commit->Created, $commit->Message, $commit->Hash]); + } + } + + private function DeleteCollections(): void{ + Db::Query(' + DELETE from Collections + where + EbookId = ? + ', [$this->EbookId] + ); + } + + private function InsertCollections(): void{ + foreach($this->Collections as $collection){ + Db::Query(' + INSERT into Collections (EbookId, Name, UrlName, SequenceNumber, Type) + values (?, + ?, + ?, + ?, + ?) + ', [$this->EbookId, $collection->Name, $collection->UrlName, $collection->SequenceNumber, $collection->Type]); + } + } + + private function DeleteSources(): void{ + Db::Query(' + DELETE from EbookSources + where + EbookId = ? + ', [$this->EbookId] + ); + } + + private function InsertSources(): void{ + foreach($this->Sources as $source){ + Db::Query(' + INSERT into EbookSources (EbookId, Type, Url) + values (?, + ?, + ?) + ', [$this->EbookId, $source->Type->value, $source->Url]); + } + } + + private function DeleteContributors(): void{ + Db::Query(' + DELETE from Contributors + where + EbookId = ? + ', [$this->EbookId] + ); + } + + private function InsertContributors(): void{ + $allContributors = array_merge($this->Authors, $this->Illustrators, $this->Translators, $this->Contributors); + foreach($allContributors as $sortOrder => $contributor){ + Db::Query(' + INSERT into Contributors (EbookId, Name, UrlName, SortName, WikipediaUrl, MarcRole, FullName, + NacoafUrl, SortOrder) + values (?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?) + ', [$this->EbookId, $contributor->Name, $contributor->UrlName, $contributor->SortName, + $contributor->WikipediaUrl, $contributor->MarcRole, $contributor->FullName, + $contributor->NacoafUrl, $sortOrder]); + } + } + + private function DeleteTocEntries(): void{ + Db::Query(' + DELETE from TocEntries + where + EbookId = ? + ', [$this->EbookId] + ); + } + + private function InsertTocEntries(): void{ + if($this->TocEntries !== null){ + foreach($this->TocEntries as $tocEntry){ + Db::Query(' + INSERT into TocEntries (EbookId, TocEntry) + values (?, + ?) + ', [$this->EbookId, $tocEntry]); + } + } + } } diff --git a/lib/EbookSourceType.php b/lib/EbookSourceType.php index 42fbb472..a39fcd9c 100644 --- a/lib/EbookSourceType.php +++ b/lib/EbookSourceType.php @@ -1,12 +1,12 @@ Name = $name; - $this->UrlName = Formatter::MakeUrlSafe($this->Name); - $this->_Url = '/subjects/' . $this->UrlName; - } - // ******* // GETTERS // ******* + protected function GetUrlName(): string{ + if($this->_UrlName === null){ + $this->_UrlName = Formatter::MakeUrlSafe($this->Name); + } + + return $this->_UrlName; + } protected function GetUrl(): string{ if($this->_Url === null){ @@ -17,4 +18,45 @@ class EbookTag extends Tag{ return $this->_Url; } + + // ******* + // METHODS + // ******* + public function Validate(): void{ + $error = new Exceptions\ValidationException(); + + if(strlen($this->Name) > EBOOKS_MAX_STRING_LENGTH){ + $error->Add(new Exceptions\StringTooLongException('Ebook tag: '. $this->Name)); + } + + if($error->HasExceptions){ + throw $error; + } + } + + public function Create(): void{ + $this->Validate(); + + Db::Query(' + INSERT into Tags (Name) + values (?) + ', [$this->Name]); + $this->TagId = Db::GetLastInsertedId(); + } + + public static function GetOrCreate(EbookTag $ebookTag): EbookTag{ + $result = Db::Query(' + SELECT * + from Tags + where Name = ? + ', [$ebookTag->Name], 'EbookTag'); + + if(isset($result[0])){ + return $result[0]; + } + else{ + $ebookTag->Create(); + return $ebookTag; + } + } } diff --git a/lib/Exceptions/EbookIdentifierRequiredException.php b/lib/Exceptions/EbookIdentifierRequiredException.php new file mode 100644 index 00000000..81a39c97 --- /dev/null +++ b/lib/Exceptions/EbookIdentifierRequiredException.php @@ -0,0 +1,5 @@ +Name) > EBOOKS_MAX_STRING_LENGTH){ + $error->Add(new Exceptions\StringTooLongException('LoC subject: '. $this->Name)); + } + + if($error->HasExceptions){ + throw $error; + } + } + + public function Create(): void{ + $this->Validate(); + + Db::Query(' + INSERT into LocSubjects (Name) + values (?) + ', [$this->Name]); + $this->LocSubjectId = Db::GetLastInsertedId(); + } + + public static function GetOrCreate(LocSubject $locSubject): LocSubject{ + $result = Db::Query(' + SELECT * + from LocSubjects + where Name = ? + ', [$locSubject->Name], 'LocSubject'); + + if(isset($result[0])){ + return $result[0]; + } + else{ + $locSubject->Create(); + return $locSubject; + } + } +} diff --git a/lib/Tag.php b/lib/Tag.php index 43465b0c..61a3a04d 100644 --- a/lib/Tag.php +++ b/lib/Tag.php @@ -1,12 +1,13 @@