Skip to content

Commit

Permalink
Add export book support
Browse files Browse the repository at this point in the history
  • Loading branch information
kencx committed Jul 8, 2023
1 parent 0217eb5 commit b6fc998
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 9 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ instance/*
.pytest_cache/*
.coverage
testdata/*
exports/*

calibre/*
library/*
Expand Down
69 changes: 69 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ All endpoints are wrappers for `calibredb`
* [POST Empty Book](#post-empty-book)
* [PUT Book](#put-book)
* [DELETE Book](#delete-book)
* [Export Book](#export-book)

<h3 id="get-book">GET <code>/books/{id}</code></h3>

Expand Down Expand Up @@ -788,3 +789,71 @@ resp = requests.delete("localhost:5000/books/1")

[Return to top](#)
</details>

<h3 id="export-book">GET <code>/export/{id}</code></h3>

<details>

<summary>
Export book with id
</summary>

#### Request

* Methods: `GET`
* Parameters: `id > 0`

#### Responses

##### Success

* Code: `200 OK`

##### Error

* Condition: id is invalid
* Code: `400 Bad Request`
* Content:

```json
{
"error": "400 Bad Request: id cannot be <= 0"
}
```

* Condition: Book does not exist
* Code: `404 Not Found`
* Content:

```json
{
"error": "404 Not Found: No book with id 1 present"
}
```

<details>
<summary>
Examples
</summary>
<br>

Curl

```console
$ curl http://localhost:5000/export/1 -o foo.epub
```

Python

```python
import requests

resp = requests.get("localhost:5000/export/1", stream=True)
with open("foo.epub", "wb") as f:
for chunk in resp:
f.write(chunk)
```
</details>
<br>

[Return to top](#)
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ $ curl --get --data-urlencode "search=title:~^foo.*bar$" \
# add ebook file to library
$ curl -X POST -H "Content-Type:multipart/form-data" \
--form "file=@foo.epub" http://localhost:5000/books

# download ebook from library
$ curl http://localhost:5000/export/1 -o bar.epub
```

See [API.md](API.md) for
Expand Down
34 changes: 31 additions & 3 deletions calibre_rest/calibre.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,13 +707,41 @@ def _handle_update_flags(self, cmd: str, book: Book = None) -> str:
cmd += f" --field {value}"
return cmd

def export(self, ids: list[int]) -> str:
"""Export books from calibre database to filesystem
def export(
self, ids: list[int], exports_dir: str = "/exports", formats: list[str] = None
) -> None:
"""Export books from calibre database to filesystem.
Args:
ids (list[int]): List of book IDs
exports_dir (str): Directory to export all files to
formats (list[str]): List of formats to export for given id
Raises:
KeyError: when any id does not exist
"""
pass
for id in ids:
validate_id(id)

cmd = f"{self.cdb_with_lib} export --dont-write-opf --dont-save-cover --single-dir"

if exports_dir != "":
cmd += f" --to-dir={exports_dir}"

# NOTE: if format does not exist, there is no error
if formats is not None:
cmd += f" --formats {','.join(formats)}"

cmd += f' {",".join([str(i) for i in ids])}'

try:
self._run(cmd)
except CalibreRuntimeError as e:
match = re.search(r"No book with id .* present", e.stderr)
if match is not None:
raise KeyError(match.group(0)) from e
else:
raise CalibreRuntimeError from e


def join_list(lst: list, sep: str) -> str:
Expand Down
31 changes: 25 additions & 6 deletions calibre_rest/routes.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import json
import os
import os.path as path
import tempfile
from os import path

from flask import abort
from flask import current_app as app
from flask import jsonify, make_response, request
from flask import jsonify, make_response, request, send_from_directory
from werkzeug.exceptions import HTTPException
from werkzeug.utils import secure_filename

Expand Down Expand Up @@ -181,10 +182,28 @@ def delete_book(id):
return response(200, "")


# export
@app.route("/export/<int:id>")
def export_book(id):
pass
@app.route("/export/<int:id>", methods=["GET"])
def export_book(id=None):
"""Export existing book from calibre library to file."""
ids = request.args.getlist("id") or None
if ids is None:
ids = [id]

with tempfile.TemporaryDirectory() as exports_dir:
try:
calibredb.export(ids, exports_dir)
except KeyError as e:
abort(404, e)

# check exports_dir for files
files = [
f for f in os.listdir(exports_dir) if path.isfile(path.join(exports_dir, f))
]

if len(files) <= 0:
abort(500)
else:
return send_from_directory(exports_dir, files[0], as_attachment=True)


# export --all
Expand Down
24 changes: 24 additions & 0 deletions tests/integration/routes_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,30 @@ def test_update_book_valid_data(url, seed_book):
assert book["tags"] == ["foo", "bar"]


def test_export_book_invalid_id(url):
check_error(
"GET",
f"{url}/export/0",
HTTPStatus.BAD_REQUEST,
"cannot be <= 0",
)


def test_export_book_not_exists(url):
check_error(
"GET",
f"{url}/export/10",
HTTPStatus.NOT_FOUND,
"No book with id",
)


def test_export_book(url, seed_book):
resp = requests.get(f"{url}/export/{seed_book}", stream=True)
assert resp.status_code == HTTPStatus.OK
assert resp.text == "hello world!\n"


def get(url: str, id: int | str, code: IntEnum, mappings: dict = None):
resp = requests.get(f"{url}/books/{id}")
assert resp.status_code == code
Expand Down

0 comments on commit b6fc998

Please sign in to comment.