forked from biblioverse/biblioteca
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(kobo): Kobo book conversion using calibre and kepubify
- Loading branch information
Showing
13 changed files
with
565 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
#FROM alpine:edge | ||
#RUN apk add --no-cache \ | ||
# build-base \ | ||
# python3 \ | ||
# python3-dev | ||
#RUN apk add calibre calibre-pyc --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ | ||
|
||
|
||
FROM ubuntu:oracular | ||
RUN apt-get update && apt-get install -y \ | ||
build-essential \ | ||
libegl1 \ | ||
libopengl0 \ | ||
libxcb-cursor0 \ | ||
python-is-python3 \ | ||
python3 \ | ||
python3-dev \ | ||
python3-venv \ | ||
wget \ | ||
&& rm -rf /var/lib/apt/lists/* | ||
RUN wget -nv -O- https://download.calibre-ebook.com/linux-installer.py | python -c "import sys; main=lambda:sys.stderr.write('Download failed\n'); exec(sys.stdin.read()); main()" | ||
|
||
RUN apt-get update && apt-get install -y \ | ||
libxkbcommon-x11-0 \ | ||
xserver-xorg-core \ | ||
libnss3-dev \ | ||
&& rm -rf /var/lib/apt/lists/* | ||
# Set up a virtual environment | ||
ENV VIRTUAL_ENV=/opt/venv | ||
RUN python3 -m venv $VIRTUAL_ENV | ||
ENV PATH="$VIRTUAL_ENV/bin:$PATH" | ||
ENV PATH="/opt/calibre:$PATH" | ||
|
||
# Install python modules | ||
RUN /opt/venv/bin/pip install --upgrade pip && \ | ||
/opt/venv/bin/pip install \ | ||
flask \ | ||
flask_cors | ||
# Copy the server script to the container | ||
COPY --link server.py /usr/local/bin/server.py | ||
|
||
## Install kepubify | ||
ENV KEPUBIFY_VER="4.0.4" | ||
RUN sh -c 'set -vx; if [ "$(uname -m)" = "x86_64" ];then kepubify_arch=64bit;elif [ "$(uname -m)" = "aarch64" ];then kepubify_arch=arm64;elif [ "$(uname -m)" = "armv7l" ];then kepubify_arch=arm; fi && wget https://github.com/pgaskin/kepubify/releases/download/v${KEPUBIFY_VER}/kepubify-linux-${kepubify_arch} -O /usr/local/bin/kepubify && chmod 755 /usr/local/bin/kepubify' | ||
|
||
EXPOSE 7654 | ||
ENTRYPOINT ["/bin/sh"] | ||
|
||
# Run the server | ||
CMD ["-c", ". /opt/venv/bin/activate && exec python3 /usr/local/bin/server.py"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
from flask import Flask, request, jsonify, send_file, make_response, render_template_string, redirect | ||
from flask_cors import CORS | ||
import os | ||
import shutil | ||
import subprocess | ||
import sys | ||
import uuid | ||
app = Flask(__name__) | ||
app.config['MAX_CONTENT_LENGTH'] = 4 * 1024 * 1024 * 1024 # 4GB max upload size | ||
|
||
CORS(app) | ||
|
||
# Static HTML form template | ||
convert_form = ''' | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>File Conversion Form</title> | ||
</head> | ||
<body> | ||
<h2>File Conversion Form</h2> | ||
<form action="/convert" method="post" enctype="multipart/form-data"> | ||
<label for="file">Select file:</label> | ||
<input type="file" id="file" name="file"><br><br> | ||
<label for="input_format">Input Format:</label> | ||
<input type="text" id="input_format" name="input_format" value="pdf"><br><br> | ||
<label for="output_format">Output Format:</label> | ||
<input type="text" id="output_format" name="output_format" value="epub"><br><br> | ||
<input type="submit" value="Convert"> | ||
</form> | ||
</body> | ||
</html> | ||
''' | ||
|
||
@app.route("/", methods=['GET']) | ||
def index(): | ||
return make_response(redirect("/convert")) | ||
|
||
@app.route('/convert', methods=['POST', 'GET']) | ||
def convert(): | ||
if request.method == 'GET': | ||
# Render the HTML form with text/html content type | ||
response = make_response(render_template_string(convert_form)) | ||
response.headers['Content-Type'] = 'text/html' | ||
return response | ||
|
||
data = request.files['file'] | ||
unique_prefix = str(uuid.uuid4()) | ||
input_format = request.form.get('input_format', 'pdf') | ||
output_format = request.form.get('output_format', 'epub') | ||
input_file = f"/tmp/pandoc/{unique_prefix}_input.{input_format}" | ||
output_file = f"/tmp/pandoc/{unique_prefix}_output.{output_format}" | ||
kobo_output_file = f"/tmp/pandoc/{unique_prefix}_kobo.epub" | ||
|
||
data.save(input_file) | ||
|
||
|
||
# Run ebook-convert conversion | ||
try: | ||
if output_format != input_format and not (output_format == 'kobo.epub' and input_format == 'epub'): | ||
# Run pandoc conversion with error handling | ||
subprocess.run(['ebook-convert', input_file, output_file], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||
else: | ||
shutil.copy(input_file, output_file) | ||
except subprocess.CalledProcessError as e: | ||
# Handle pandoc conversion error | ||
os.remove(input_file) | ||
stderr_str = e.stderr.decode('utf-8') | ||
return jsonify({"error": "Conversion failed", "details": str(e), "stderr": stderr_str}), 500 | ||
|
||
if 'kobo' in output_format.lower(): | ||
try: | ||
# Run pandoc conversion with error handling | ||
subprocess.run(['kepubify', '-o', kobo_output_file, output_file ], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||
os.remove(output_file) | ||
output_file = kobo_output_file | ||
except subprocess.CalledProcessError as e: | ||
# Handle pandoc conversion error | ||
os.remove(output_file) | ||
os.remove(input_file) | ||
stderr_str = e.stderr.decode('utf-8') | ||
return jsonify({"error": "Kobo Conversion failed", "details": str(e), "stderr": stderr_str}), 500 | ||
|
||
response = send_file(output_file, as_attachment=True) | ||
|
||
# Remove the temporary files after sending | ||
os.remove(input_file) | ||
os.remove(output_file) | ||
|
||
return response | ||
|
||
@app.route('/health', methods=['GET']) | ||
def health(): | ||
return jsonify({"status": "healthy"}) | ||
|
||
if __name__ == '__main__': | ||
port = int(sys.argv[1]) if len(sys.argv) > 1 else 7654 | ||
app.run(host='0.0.0.0', port=port) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace DoctrineMigrations; | ||
|
||
use Doctrine\DBAL\Schema\Schema; | ||
use Doctrine\Migrations\AbstractMigration; | ||
|
||
/** | ||
* Auto-generated Migration: Please modify to your needs! | ||
*/ | ||
final class Version20240716142454 extends AbstractMigration | ||
{ | ||
public function getDescription(): string | ||
{ | ||
return 'Add bookformatss'; | ||
} | ||
|
||
public function up(Schema $schema): void | ||
{ | ||
// this up() migration is auto-generated, please modify it to your needs | ||
$this->addSql('CREATE TABLE book_format (id INT AUTO_INCREMENT NOT NULL, book_id INT NOT NULL, type VARCHAR(255) NOT NULL, INDEX IDX_F76D795216A2B381 (book_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB'); | ||
$this->addSql('ALTER TABLE book_format ADD CONSTRAINT FK_F76D795216A2B381 FOREIGN KEY (book_id) REFERENCES book (id)'); | ||
$this->addSql('ALTER TABLE kobo_device DROP FOREIGN KEY FK_42E56EFA76ED395'); | ||
$this->addSql('DROP INDEX idx_42e56efa76ed395 ON kobo_device'); | ||
$this->addSql('CREATE INDEX IDX_2EB06A2BA76ED395 ON kobo_device (user_id)'); | ||
$this->addSql('ALTER TABLE kobo_device ADD CONSTRAINT FK_42E56EFA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)'); | ||
} | ||
|
||
public function down(Schema $schema): void | ||
{ | ||
// this down() migration is auto-generated, please modify it to your needs | ||
$this->addSql('ALTER TABLE book_format DROP FOREIGN KEY FK_F76D795216A2B381'); | ||
$this->addSql('DROP TABLE book_format'); | ||
$this->addSql('ALTER TABLE kobo_device DROP FOREIGN KEY FK_2EB06A2BA76ED395'); | ||
$this->addSql('DROP INDEX idx_2eb06a2ba76ed395 ON kobo_device'); | ||
$this->addSql('CREATE INDEX IDX_42E56EFA76ED395 ON kobo_device (user_id)'); | ||
$this->addSql('ALTER TABLE kobo_device ADD CONSTRAINT FK_2EB06A2BA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<?php | ||
|
||
namespace App\Command; | ||
|
||
use App\Repository\BookRepository; | ||
use App\Service\BookConverter; | ||
use App\Service\BookFormatStorage; | ||
use Symfony\Component\Console\Attribute\AsCommand; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\Console\Input\InputArgument; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\Console\Input\InputOption; | ||
use Symfony\Component\Console\Output\OutputInterface; | ||
|
||
#[AsCommand( | ||
name: 'app:book:convert', | ||
description: 'Add a short description for your command', | ||
)] | ||
class BookConvertCommand extends Command | ||
{ | ||
public function __construct( | ||
protected BookRepository $bookRepository, | ||
protected BookConverter $bookConverter) | ||
{ | ||
parent::__construct(); | ||
} | ||
|
||
protected function configure(): void | ||
{ | ||
$this | ||
->addArgument('bookId', InputArgument::REQUIRED, 'Book ID') | ||
->addOption('format', null, InputOption::VALUE_REQUIRED, 'Book format') | ||
; | ||
} | ||
|
||
protected function execute(InputInterface $input, OutputInterface $output): int | ||
{ | ||
$format = $input->getOption('format'); | ||
$format = !is_string($format) || $format === '' ? BookFormatStorage::FORMAT_EPUB_KOBO : $format; | ||
$id = $input->getArgument('bookId'); | ||
$book = $this->bookRepository->findOneBy(['id' => $id]); | ||
|
||
if ($book === null) { | ||
$output->writeln('Book not found'); | ||
|
||
return 1; | ||
} | ||
|
||
$this->bookConverter->convert($book, $format); | ||
|
||
return Command::SUCCESS; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.