From c103e9d9d4994a458ee774b044fd4fbd1cb1b649 Mon Sep 17 00:00:00 2001 From: Sergio Mendolia Date: Tue, 19 Sep 2023 16:22:23 +0200 Subject: [PATCH] Allow to recheck all covers --- composer.json | 4 +- composer.lock | 54 +++++++++- docker-compose.yml | 2 +- src/Command/BooksExtractCoverCommand.php | 79 ++++++++++++++ src/Service/BookFileSystemManager.php | 126 ++++++++++++++++++++--- src/Service/BookManager.php | 31 +----- 6 files changed, 251 insertions(+), 45 deletions(-) create mode 100644 src/Command/BooksExtractCoverCommand.php diff --git a/composer.json b/composer.json index 90d6aa85..b324dda7 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "doctrine/orm": "^2.14", "easycorp/easyadmin-bundle": "^4.7", "gedmo/doctrine-extensions": "^3.11", + "gemorroj/archive7z": "^5.6", "google/apiclient": "^2.15", "kiwilan/php-ebook": "^2.0", "knplabs/knp-markdown-bundle": "^1.10", @@ -60,7 +61,8 @@ "symfony/webpack-encore-bundle": "^2.0", "symfony/yaml": "^6.2", "twig/extra-bundle": "^2.12|^3.0", - "twig/twig": "^2.12|^3.0" + "twig/twig": "^2.12|^3.0", + "ext-imagick": "*" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index ca2fa987..07a0ba78 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fe8e1b02dc8d8a2ffd767b2414d31183", + "content-hash": "2b73405f6a5be0438d968be9cc0bbf6b", "packages": [ { "name": "andanteproject/page-filter-form-bundle", @@ -1864,6 +1864,58 @@ ], "time": "2023-09-06T13:16:12+00:00" }, + { + "name": "gemorroj/archive7z", + "version": "5.6.0", + "source": { + "type": "git", + "url": "https://github.com/Gemorroj/Archive7z.git", + "reference": "e7733839542fc8b7b7a7106327ae9c00e8a09c80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Gemorroj/Archive7z/zipball/e7733839542fc8b7b7a7106327ae9c00e8a09c80", + "reference": "e7733839542fc8b7b7a7106327ae9c00e8a09c80", + "shasum": "" + }, + "require": { + "php": ">=8.0.2", + "symfony/process": "^5.4||^6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.0", + "phpstan/phpstan": "^1.0", + "phpunit/phpunit": "^9.5.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "Archive7z\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Gemorroj" + } + ], + "description": "7z cli wrapper", + "keywords": [ + "7-zip", + "7z", + "7zip", + "archive", + "p7zip" + ], + "support": { + "issues": "https://github.com/Gemorroj/Archive7z/issues", + "source": "https://github.com/Gemorroj/Archive7z/tree/5.6.0" + }, + "time": "2023-07-03T17:03:31+00:00" + }, { "name": "google/apiclient", "version": "v2.15.1", diff --git a/docker-compose.yml b/docker-compose.yml index d45dc73b..9ecf675f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: biblioteca: - image: ghcr.io/biblioverse/biblioteca-docker:1.0.4 + image: ghcr.io/biblioverse/biblioteca-docker:1.0.6 ports: - 48480:8080 depends_on: diff --git a/src/Command/BooksExtractCoverCommand.php b/src/Command/BooksExtractCoverCommand.php new file mode 100644 index 00000000..d1a5b203 --- /dev/null +++ b/src/Command/BooksExtractCoverCommand.php @@ -0,0 +1,79 @@ +addOption('force', 'f', InputOption::VALUE_NONE, 'Force the extraction of the cover') + ->addArgument('book-id', InputOption::VALUE_REQUIRED, 'Which book to extract the cover from, use "all" for all books') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $bookId = $input->getArgument('book-id'); + $force = $input->getOption('force'); + if ($bookId === 'all') { + $books = $this->bookRepository->findAll(); + } else { + $books = $this->bookRepository->findBy(['id' => $bookId]); + } + + if (count($books) === 0) { + $io->error('No books found'); + + return Command::FAILURE; + } + + $io->note(sprintf('Processing: %s book(s)', count($books))); + + $progressBar = new ProgressBar($output, count($books)); + $progressBar->start(); + $fs = new Filesystem(); + foreach ($books as $book) { + /* @var Book $book */ + $progressBar->advance(); + + if ($force === true || $book->getImageFilename() === null || !$fs->exists($book->getImagePath().$book->getImageFilename())) { + try { + $book = $this->fileSystemManager->extractCover($book); + $this->entityManager->persist($book); + } catch (\Exception $e) { + $io->error($e->getMessage()); + continue; + } + } + } + $this->entityManager->flush(); + + $progressBar->finish(); + + return Command::SUCCESS; + } +} diff --git a/src/Service/BookFileSystemManager.php b/src/Service/BookFileSystemManager.php index 7521ce58..a02b3b13 100644 --- a/src/Service/BookFileSystemManager.php +++ b/src/Service/BookFileSystemManager.php @@ -3,6 +3,8 @@ namespace App\Service; use App\Entity\Book; +use Archive7z\Archive7z; +use Kiwilan\Ebook\Ebook; use Psr\Log\LoggerInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; @@ -52,7 +54,9 @@ public function getAllBooksFiles(): \Iterator public function getBookFile(Book $book): \SplFileInfo { $finder = new Finder(); - $finder->files()->name($book->getBookFilename())->in($this->getBooksDirectory().$book->getBookPath()); + $filename = str_replace(['[', ']'], '*', $book->getBookFilename()); + + $finder->files()->name($filename)->in($this->getBooksDirectory().$book->getBookPath()); $return = iterator_to_array($finder->getIterator()); if (0 === count($return)) { @@ -120,7 +124,7 @@ private function calculateFilePath(Book $book): string $author = mb_strtolower($main); $title = mb_strtolower($this->slugger->slug($book->getTitle())); $serie = null !== $book->getSerie() ? mb_strtolower($this->slugger->slug($book->getSerie())) : null; - $letter = $author[0]; + $letter = substr($main, 0, 1); $path = [$letter]; $path[] = $author; @@ -207,20 +211,24 @@ public function removeEmptySubFolders(string $path = null): bool } $empty = true; - $files = glob($path.DIRECTORY_SEPARATOR.'{,.}[!.,!..]*', GLOB_MARK | GLOB_BRACE); - if (false !== $files && count($files) > 0) { - foreach ($files as $file) { - if (is_dir($file)) { - if (!$this->removeEmptySubFolders($file)) { - $empty = false; - } - } else { - $empty = false; - } + $finder = new Finder(); + + $files = $finder->in($path)->ignoreDotFiles(true)->files(); + $directories = $finder->in($path)->ignoreDotFiles(true)->directories(); + + if ($files->count() > 0 || $directories->count() > 0) { + $empty = false; + } + + foreach ($directories as $directory) { + if (!$this->removeEmptySubFolders($directory->getRealPath())) { + $empty = false; } } + if ($empty) { - rmdir($path); + $fs = new Filesystem(); + $fs->remove($path); } return $empty; @@ -311,4 +319,96 @@ public function deleteBookFiles(Book $book): void $this->removeEmptySubFolders($this->getCoverDirectory().$book->getImagePath()); } } + + public function extractCover(Book $book): Book + { + $filesystem = new Filesystem(); + $bookFile = $this->getBookFile($book); + switch ($book->getExtension()) { + case 'epub': + $ebook = Ebook::read($bookFile->getRealPath()); + if ($ebook === null) { + break; + } + $cover = $ebook->getCover(); + + if ($cover === null || $cover->getPath() === null) { + break; + } + $coverContent = $cover->getContent(); + + $coverFileName = explode('/', $cover->getPath()); + $coverFileName = end($coverFileName); + $ext = explode('.', $coverFileName); + $book->setImageExtension(end($ext)); + + $coverPath = $this->getCalculatedImagePath($book, true); + $coverFileName = $this->getCalculatedImageName($book); + + $filesystem = new Filesystem(); + $filesystem->mkdir($coverPath); + + $coverFile = file_put_contents($coverPath.$coverFileName, $coverContent); + + if (false !== $coverFile) { + $book->setImagePath($this->getCalculatedImagePath($book, false)); + $book->setImageFilename($coverFileName); + } + break; + case 'cbr': + case 'cbz': + $archive = new Archive7z($bookFile->getRealPath()); + $entries = []; + foreach ($archive->getEntries() as $entry) { + if (str_contains($entry->getPath(), '.jpg') || str_contains($entry->getPath(), '.jpeg')) { + $entries[] = $entry->getPath(); + } + } + ksort($entries); + if (count($entries) === 0) { + break; + } + + $archive->setOutputDirectory('/tmp')->extractEntry($entries[0]); // extract the archive + + $filesystem->mkdir($this->getCalculatedImagePath($book, true)); + $checksum = $this->getFileChecksum(new \SplFileInfo('/tmp/'.$entries[0])); + $filesystem->rename( + '/tmp/'.$entries[0], + $this->getCalculatedImagePath($book, true).$this->getCalculatedImageName($book, $checksum), + true); + + $book->setImagePath($this->getCalculatedImagePath($book, false)); + $book->setImageFilename($this->getCalculatedImageName($book, $checksum)); + $book->setImageExtension('jpg'); + + break; + case 'pdf': + $checksum = md5(''.time()); + + try { + $im = new \Imagick($bookFile->getRealPath().'[0]'); // 0-first page, 1-second page + } catch (\Exception $e) { + $this->logger->error('Could not extract cover', ['book' => $bookFile->getRealPath(), 'exception' => $e->getMessage()]); + break; + } + + $im->setImageColorspace(255); // prevent image colors from inverting + $im->setImageFormat('jpeg'); + $im->thumbnailImage(300, 460); // width and height + $book->setImageExtension('jpg'); + $filesystem->mkdir($this->getCalculatedImagePath($book, true)); + $im->writeImage($this->getCalculatedImagePath($book, true).$this->getCalculatedImageName($book, $checksum)); + $im->clear(); + $im->destroy(); + $book->setImagePath($this->getCalculatedImagePath($book, false)); + $book->setImageFilename($this->getCalculatedImageName($book, $checksum)); + + break; + default: + throw new \RuntimeException('Extension not implemented'); + } + + return $book; + } } diff --git a/src/Service/BookManager.php b/src/Service/BookManager.php index 27757d4a..054ce451 100644 --- a/src/Service/BookManager.php +++ b/src/Service/BookManager.php @@ -6,7 +6,6 @@ use Kiwilan\Ebook\Ebook; use Kiwilan\Ebook\EbookCover; use Kiwilan\Ebook\Tools\BookAuthor; -use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\KernelInterface; /** @@ -62,34 +61,8 @@ public function createBook(\SplFileInfo $file): Book $book->setBookPath(''); $book->setBookFilename(''); - $book = $this->updateBookLocation($book, $file); - /** @var ?EbookCover $cover */ - $cover = $extractedMetadata['cover']; - - if (null !== $cover && null !== $cover->getPath()) { - $coverContent = $cover->getContent(); - - $coverFileName = explode('/', $cover->getPath()); - $coverFileName = end($coverFileName); - $ext = explode('.', $coverFileName); - $book->setImageExtension(end($ext)); - - $coverPath = $this->fileSystemManager->getCalculatedImagePath($book, true); - $coverFileName = $this->fileSystemManager->getCalculatedImageName($book); - - $filesystem = new Filesystem(); - $filesystem->mkdir($coverPath); - - $coverFile = file_put_contents($coverPath.$coverFileName, $coverContent); - - if (false !== $coverFile) { - $book->setImagePath($this->fileSystemManager->getCalculatedImagePath($book, false)); - $book->setImageFilename($coverFileName); - } - } - - return $book; + return $this->updateBookLocation($book, $file); } public function updateBookLocation(Book $book, \SplFileInfo $file): Book @@ -138,7 +111,7 @@ public function extractEbookMetadata(\SplFileInfo $file): array } return [ - 'title' => $ebook->getTitle() ?? $file->getFilename(), // string + 'title' => $ebook->getTitle() ?? $file->getBasename('.'.$file->getExtension()), // string 'authors' => $ebook->getAuthors(), // BookAuthor[] (`name`: string, `role`: string) 'main_author' => $ebook->getAuthorMain(), // ?BookAuthor => First BookAuthor (`name`: string, `role`: string) 'description' => $ebook->getDescription(), // ?string