Added Books Book Collections Collection Endpoints

This commit is contained in:
th3r00t
2023-03-12 16:08:03 -04:00
parent af3fa3eec8
commit d0e71f4df2
7 changed files with 302 additions and 51 deletions

1
Pipfile vendored
View File

@@ -24,6 +24,7 @@ jinja2 = "*"
libsass = "*" libsass = "*"
nodejs-bin = "*" nodejs-bin = "*"
npm = "*" npm = "*"
brotlipy = "*"
[dev-packages] [dev-packages]
ptipython = "*" ptipython = "*"

130
Pipfile.lock generated vendored
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "c5c03b7e057380d9a18d1a52210153e3dbfe7eb1596a5488ff0d75efeffa419b" "sha256": "be870815622ed679c589971cb713382f5d56d03e73b72f4e7e35af74f63adfc4"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -40,6 +40,58 @@
"markers": "python_full_version >= '3.6.0'", "markers": "python_full_version >= '3.6.0'",
"version": "==4.11.2" "version": "==4.11.2"
}, },
"brotlipy": {
"hashes": [
"sha256:07194f4768eb62a4f4ea76b6d0df6ade185e24ebd85877c351daa0a069f1111a",
"sha256:08a16ebe2ffc52f645c076f96b138f185e74e5d59b4a65e84af17d5997d82890",
"sha256:091b299bf36dd6ef7a06570dbc98c0f80a504a56c5b797f31934d2ad01ae7d17",
"sha256:09ec3e125d16749b31c74f021aba809541b3564e5359f8c265cbae442810b41a",
"sha256:0be698678a114addcf87a4b9496c552c68a2c99bf93cf8e08f5738b392e82057",
"sha256:0fa6088a9a87645d43d7e21e32b4a6bf8f7c3939015a50158c10972aa7f425b7",
"sha256:1379347337dc3d20b2d61456d44ccce13e0625db2611c368023b4194d5e2477f",
"sha256:1ea4e578241504b58f2456a6c69952c88866c794648bdc74baee74839da61d44",
"sha256:22a53ccebcce2425e19f99682c12be510bf27bd75c9b77a1720db63047a77554",
"sha256:2699945a0a992c04fc7dc7fa2f1d0575a2c8b4b769f2874a08e8eae46bef36ae",
"sha256:2a80319ae13ea8dd60ecdc4f5ccf6da3ae64787765923256b62c598c5bba4121",
"sha256:2e5c64522364a9ebcdf47c5744a5ddeb3f934742d31e61ebfbbc095460b47162",
"sha256:36def0b859beaf21910157b4c33eb3b06d8ce459c942102f16988cca6ea164df",
"sha256:382971a641125323e90486244d6266ffb0e1f4dd920fbdcf508d2a19acc7c3b3",
"sha256:3a3e56ced8b15fbbd363380344f70f3b438e0fd1fcf27b7526b6172ea950e867",
"sha256:3c1d5e2cf945a46975bdb11a19257fa057b67591eb232f393d260e7246d9e571",
"sha256:4864ac52c116ea3e3a844248a9c9fbebb8797891cbca55484ecb6eed3ebeba24",
"sha256:4bac11c1ffba9eaa2894ec958a44e7f17778b3303c2ee9f99c39fcc511c26668",
"sha256:4e4638b49835d567d447a2cfacec109f9a777f219f071312268b351b6839436d",
"sha256:50ca336374131cfad20612f26cc43c637ac0bfd2be3361495e99270883b52962",
"sha256:5664fe14f3a613431db622172bad923096a303d3adce55536f4409c8e2eafba4",
"sha256:5de6f7d010b7558f72f4b061a07395c5c3fd57f0285c5af7f126a677b976a868",
"sha256:637847560d671657f993313ecc6c6c6666a936b7a925779fd044065c7bc035b9",
"sha256:653faef61241bf8bf99d73ca7ec4baa63401ba7b2a2aa88958394869379d67c7",
"sha256:786afc8c9bd67de8d31f46e408a3386331e126829114e4db034f91eacb05396d",
"sha256:79aaf217072840f3e9a3b641cccc51f7fc23037496bd71e26211856b93f4b4cb",
"sha256:79ab3bca8dd12c17e092273484f2ac48b906de2b4828dcdf6a7d520f99646ab3",
"sha256:7b21341eab7c939214e457e24b265594067a6ad268305289148ebaf2dacef325",
"sha256:7e31f7adcc5851ca06134705fcf3478210da45d35ad75ec181e1ce9ce345bb38",
"sha256:7ff18e42f51ebc9d9d77a0db33f99ad95f01dd431e4491f0eca519b90e9415a9",
"sha256:82f61506d001e626ec3a1ac8a69df11eb3555a4878599befcb672c8178befac8",
"sha256:890b973039ba26c3ad2e86e8908ab527ed64f9b1357f81a676604da8088e4bf9",
"sha256:8b39abc3256c978f575df5cd7893153277216474f303e26f0e43ba3d3969ef96",
"sha256:8ef230ca9e168ce2b7dc173a48a0cc3d78bcdf0bd0ea7743472a317041a4768e",
"sha256:9448227b0df082e574c45c983fa5cd4bda7bfb11ea6b59def0940c1647be0c3c",
"sha256:96bc59ff9b5b5552843dc67999486a220e07a0522dddd3935da05dc194fa485c",
"sha256:a07647886e24e2fb2d68ca8bf3ada398eb56fd8eac46c733d4d95c64d17f743b",
"sha256:ac1d66c9774ee62e762750e399a0c95e93b180e96179b645f28b162b55ae8adc",
"sha256:af65d2699cb9f13b26ec3ba09e75e80d31ff422c03675fcb36ee4dabe588fdc2",
"sha256:b4c98b0d2c9c7020a524ca5bbff42027db1004c6571f8bc7b747f2b843128e7a",
"sha256:b7cf5bb69e767a59acc3da0d199d4b5d0c9fed7bef3ffa3efa80c6f39095686b",
"sha256:c6cc0036b1304dd0073eec416cb2f6b9e37ac8296afd9e481cac3b1f07f9db25",
"sha256:d2c1c724c4ac375feb2110f1af98ecdc0e5a8ea79d068efb5891f621a5b235cb",
"sha256:dc6c5ee0df9732a44d08edab32f8a616b769cc5a4155a12d2d010d248eb3fb07",
"sha256:e5c549ae5928dda952463196180445c24d6fad2d73cb13bd118293aced31b771",
"sha256:fd1d1c64214af5d90014d82cee5d8141b13d44c92ada7a0c0ec0679c6f15a471"
],
"index": "pypi",
"version": "==0.7.0"
},
"bs4": { "bs4": {
"hashes": [ "hashes": [
"sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a" "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"
@@ -62,6 +114,75 @@
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==2022.12.7" "version": "==2022.12.7"
}, },
"cffi": {
"hashes": [
"sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5",
"sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef",
"sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104",
"sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426",
"sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405",
"sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375",
"sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a",
"sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e",
"sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc",
"sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf",
"sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185",
"sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497",
"sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3",
"sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35",
"sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c",
"sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83",
"sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21",
"sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca",
"sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984",
"sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac",
"sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd",
"sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee",
"sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a",
"sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2",
"sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192",
"sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7",
"sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585",
"sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f",
"sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e",
"sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27",
"sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b",
"sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e",
"sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e",
"sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d",
"sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c",
"sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415",
"sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82",
"sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02",
"sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314",
"sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325",
"sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c",
"sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3",
"sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914",
"sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045",
"sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d",
"sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9",
"sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5",
"sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2",
"sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c",
"sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3",
"sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2",
"sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8",
"sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d",
"sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d",
"sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9",
"sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162",
"sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76",
"sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4",
"sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e",
"sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9",
"sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6",
"sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b",
"sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01",
"sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"
],
"version": "==1.15.1"
},
"cfgv": { "cfgv": {
"hashes": [ "hashes": [
"sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426",
@@ -794,6 +915,13 @@
"index": "pypi", "index": "pypi",
"version": "==2022.1.3" "version": "==2022.1.3"
}, },
"pycparser": {
"hashes": [
"sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
"sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
],
"version": "==2.21"
},
"pydantic": { "pydantic": {
"hashes": [ "hashes": [
"sha256:012c99a9c0d18cfde7469aa1ebff922e24b0c706d03ead96940f5465f2c9cf62", "sha256:012c99a9c0d18cfde7469aa1ebff922e24b0c706d03ead96940f5465f2c9cf62",

View File

@@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from typing_extensions import Annotated from typing_extensions import Annotated
from sqlalchemy import func, DateTime, ForeignKey from sqlalchemy import func, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
import datetime import datetime
@@ -11,10 +11,14 @@ timestamp = Annotated[
class Base(DeclarativeBase): class Base(DeclarativeBase):
"""Base class for all models."""
pass pass
class Book(Base): class Book(Base):
"""Book model."""
__tablename__ = "books" __tablename__ = "books"
book_id: Mapped[int] = mapped_column(primary_key=True, nullable=False) book_id: Mapped[int] = mapped_column(primary_key=True, nullable=False)
@@ -33,7 +37,10 @@ class Book(Base):
publisher: Mapped[Optional[str]] publisher: Mapped[Optional[str]]
class Collection(Base): class Collection(Base):
"""Collection model."""
__tablename__ = "collections" __tablename__ = "collections"
collection: Mapped[str] collection: Mapped[str]

View File

@@ -165,7 +165,7 @@ class Storage:
_collections.append(_p) _collections.append(_p)
self.config.logger.info("Finished making collections.") self.config.logger.info("Finished making collections.")
def get_books(self, collection=None): def get_books(self, collection=None, skip=None, limit=None):
"""Get books from database. """Get books from database.
Parameters Parameters
@@ -180,11 +180,43 @@ class Storage:
session = Session(self.engine) session = Session(self.engine)
if collection: if collection:
_result = session.execute( _result = session.execute(
select(Book).join(Collection).where( select(Book).join(Collection)
Collection.collection == collection .where(Collection.collection_id == collection)
) .offset(skip).limit(limit)).all()
).all()
else: else:
_result = session.execute(select(Book)).all() _result = session.execute(
select(Book).offset(skip).limit(limit)).all()
session.close()
return _result
def get_book(self, book_id):
"""Get book from database.
Parameters
----------
book_id : int
Book ID to filter by.
Returns
-------
_result : ScalarResult Object
"""
session = Session(self.engine)
_result = session.execute(select(Book).where(Book.book_id == book_id)).first()
session.close()
return _result
def get_collections(self):
"""Get collections from database.
Returns
-------
_result : ScalarResult Object
"""
session = Session(self.engine)
_result = session.execute(
select(Collection)
.join(Book)
).all()
session.close() session.close()
return _result return _result

View File

@@ -2,9 +2,13 @@
import uvicorn import uvicorn
import os import os
import sass import sass
import datetime
# import gzip
# import brotli
from json import dumps
from base64 import b64encode from base64 import b64encode
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
@@ -18,15 +22,88 @@ templates = Jinja2Templates(directory="src/frontend/templates")
def base64decode(string) -> str: def base64decode(string) -> str:
"""Decode a base64 string.""" """Decode a base64 string."""
breakpoint()
try: try:
result = b64encode(string).decode("utf-8") result = b64encode(string).decode("utf-8")
except Exception: except Exception:
result = "static/images/placeholder.png" result = "None"
return result return result
def summarize(string) -> str:
"""Summarize a string."""
try:
if len(string) > 50:
return string[:50] + "..."
return string
except TypeError:
return "None"
def convertDateTime(timestamp: datetime) -> str:
"""Convert a datetime object to a string."""
return timestamp.strftime("%d/%m/%Y %H:%M:%S")
def books_tojson(obj) -> dumps:
"""Convert an object to a dictionary."""
_books: list = []
for book in obj:
_books.append({
"book_id": book[0].book_id,
"title": book[0].title,
"author": book[0].author,
"categories": book[0].categories,
"cover": base64decode(book[0].cover),
"pages": book[0].pages,
"progress": book[0].progress,
"file_name": book[0].file_name,
"description": book[0].description,
"date": convertDateTime(book[0].date),
"rights": book[0].rights,
"tags": book[0].tags,
"identifier": book[0].identifier,
"publisher": book[0].publisher,
})
# compressed = gzip.compress(dumps(_books).encode("utf-8"))
# compressed = gzip.compress(dumps(_books).encode())
return dumps(_books)
def book_tojson(book) -> dumps:
"""Convert a book object to a json."""
return dumps({
"book_id": book[0].book_id,
"title": book[0].title,
"author": book[0].author,
"categories": book[0].categories,
"cover": base64decode(book[0].cover),
"pages": book[0].pages,
"progress": book[0].progress,
"file_name": book[0].file_name,
"description": book[0].description,
"date": convertDateTime(book[0].date),
"rights": book[0].rights,
"tags": book[0].tags,
"identifier": book[0].identifier,
"publisher": book[0].publisher,
})
def collections_tojson(collection) -> dumps:
"""Convert a collections object to json."""
_collections = []
for _collection in collection:
_collections.append({
"collection_id": _collection[0].collection_id,
"book_id": _collection[0].book_id,
"collection": _collection[0].collection,
})
return dumps(_collections)
templates.env.filters["b64decode"] = base64decode templates.env.filters["b64decode"] = base64decode
templates.env.filters["summarize"] = summarize
templates.env.filters["books_tojson"] = books_tojson
class FastAPIServer(): class FastAPIServer():
@@ -63,28 +140,35 @@ class FastAPIServer():
route.operation_id = route.name route.operation_id = route.name
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
async def index(request: Request): async def index(request: Request, skip: int = 0, limit: int = 10):
storage = Storage(Config(os.path.abspath(os.getcwd()))) storage = Storage(Config(os.path.abspath(os.getcwd())))
books = storage.get_books() books = storage.get_books(collection=None, skip=skip, limit=limit)
"""Home page responder.""" """Home page responder."""
# _books = self.storage.get_books()
context = {"request": request, "books": books} context = {"request": request, "books": books}
return templates.TemplateResponse("index.html", context) return templates.TemplateResponse("index.html", context)
@app.get("/users/me") @app.get("/books", response_class=JSONResponse)
async def about_me(self): async def books(request: Request, skip: int = 0, limit: int = 10, collection=None):
"""About me page responder.""" storage = Storage(Config(os.path.abspath(os.getcwd())))
return {"user_id": "CurrentUser"} books = storage.get_books(collection, skip=skip, limit=limit)
headers = {"Accept-Encoding": "gzip"}
"""Home page responder."""
return JSONResponse(content=books_tojson(books))
@app.get("/users/{user_id}") @app.get("/book/{book_id}", response_class=JSONResponse)
async def about_user(self, user_id: int): async def book(request: Request, book_id: int):
"""About user page responder.""" storage = Storage(Config(os.path.abspath(os.getcwd())))
return {"user_id": user_id} book = storage.get_book(book_id)
"""Home page responder."""
return JSONResponse(content=book_tojson(book))
@app.get("/collections", response_class=JSONResponse)
async def collections(request: Request):
storage = Storage(Config(os.path.abspath(os.getcwd())))
collections = storage.get_collections()
"""Home page responder."""
return JSONResponse(content=collections_tojson(collections))
@app.get("/dev/test/echo/{_test_item_}")
async def echo_test(self, _test_item_):
"""Test echo responder function."""
return {"Test Object": _test_item_}
async def run(self): async def run(self):
"""Front end server entrypoint.""" """Front end server entrypoint."""

View File

@@ -30,6 +30,7 @@
"homepage": "https://github.com/th3r00t/pyShelf#readme", "homepage": "https://github.com/th3r00t/pyShelf#readme",
"dependencies": { "dependencies": {
"bulma": "^0.9.4", "bulma": "^0.9.4",
"pako": "^2.1.0",
"typescript": "^4.9.5" "typescript": "^4.9.5"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,34 +1,32 @@
<!doctype html> <!doctype html>
{% block javascript %}
<script type="text/javascript">
var books = {{ books|books_tojson }};
let inflatedJSON = {};
inflatedJSON = JSON.parse(pako.inflate(books, { to: 'string'}));
</script>
{% endblock %}
{% include 'header.html' %} {% include 'header.html' %}
{% include 'navigation.html' %} {% include 'navigation.html' %}
<section id="master"> <section id="master">
<div class="container is-dark"> <div class="container is-dark">
{% for book in books %} {% for book in books %}
<div class="card"> {% set cover = book[0].cover|b64decode %}
<div class="card-image"> {% if cover != 'None' %}
<div class="box is-dark book" id="{{book[0].id}}">
<div class="image book-thumbnail">
<figure class="image is-4by3"> <figure class="image is-4by3">
<img src="data:;base64,{{ book[0].cover|b64decode }}" alt="{{ book[0].title }}"> <img src="data:;base64,{{ book[0].cover|b64decode }}" alt="{{ book[0].title }}">
</figure> </figure>
</div> </div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img src="data:;base64,{{ book[0].cover|b64decode }}" alt="Placeholder image">
</figure>
</div>
<div class="media-content">
<p class="title is-4">{{ book[0].title }}</p>
<p class="subtitle is-6">{{ book[0].author }}</p>
</div>
</div>
<div class="content">
{{ book[0].description }}
<br>
<time datetime="2016-1-1">{{ book[0].date }}</time>
</div>
</div> </div>
{% else %}
<div class="box is-dark book" id="{{book[0].id}}">
<h3 class="title is-3">{{ book[0].title }}</h3>
<h4 class="subtitle is-4">{{ book[0].author }}</h4>
<p class="content">{{ book[0].description|summarize }}</p>
</div> </div>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
</section> </section>