Fix legacy book details endpoint routing and rendering; improve mobile styling

This commit is contained in:
lgdeysel1980@gmail.com 2026-04-21 13:55:52 +02:00
parent 72f13096ac
commit be839bf8ca
4 changed files with 232 additions and 11 deletions

View File

@ -1,5 +1,10 @@
/* CSS for the mobile version of the content server webpage */
body {
font-family: sans-serif;
background-color: #f6f3e9;
}
.body {
font-family: sans-serif;
}
@ -16,10 +21,10 @@
padding: 0.5em;
font-size: larger;
border: 1px solid black;
text-color: black;
color: #f6f3e9;
text-decoration: none;
margin-right: 0.5em;
background-color: #ddd;
background-color: #39322b;
border-top: 1px solid ThreeDLightShadow;
border-right: 1px solid ButtonShadow;
border-bottom: 1px solid ButtonShadow;

View File

@ -13,7 +13,7 @@ from calibre.constants import __appname__
from calibre.db.view import sanitize_sort_field_name
from calibre.ebooks.metadata import authors_to_string
from calibre.srv.content import book_filename, get
from calibre.srv.errors import HTTPBadRequest, HTTPRedirect
from calibre.srv.errors import HTTPBadRequest, HTTPRedirect, BookNotFound
from calibre.srv.routes import endpoint
from calibre.srv.utils import get_library_data, http_date
from calibre.utils.cleantext import clean_xml_chars
@ -153,9 +153,18 @@ def build_index(rd, books, num, search, sort, order, start, total, url_base, fie
)
for book in books:
# Link to book details page (legacy-safe)
book_link = ctx.url_for('/legacy/book', book_id=book.id, library_id=library_id)
thumbnail = E.td(
E.img(type='image/jpeg', border='0', src=ctx.url_for('/get', what='thumb', book_id=book.id, library_id=library_id),
class_='thumbnail')
E.a(
E.img(
type='image/jpeg', border='0',
src=ctx.url_for('/get', what='thumb', book_id=book.id, library_id=library_id),
class_='thumbnail'
),
href=book_link # Make cover clickable
)
)
data = E.td()
@ -167,7 +176,8 @@ def build_index(rd, books, num, search, sort, order, start, total, url_base, fie
fmt.lower(),
href=ctx.url_for('/legacy/get', what=fmt, book_id=book.id, library_id=library_id, filename=book_filename(rd, book.id, book, fmt))
),
class_='button')
class_='button'
)
s.tail = ''
data.append(s)
@ -186,8 +196,13 @@ def build_index(rd, books, num, search, sort, order, start, total, url_base, fie
if val:
ctext += f'{name}=[{val}] '
first = E.span(f'{book.title} {series} by {authors_to_string(book.authors)}', class_='first-line')
# Make title clickable
first = E.span(
E.a(f'{book.title} {series} by {authors_to_string(book.authors)}', href=book_link),
class_='first-line'
)
div.append(first)
ds = '' if is_date_undefined(book.timestamp) else strftime('%d %b, %Y', t=dt_as_local(book.timestamp).timetuple())
second = E.span(f'{ds} {tags} {ctext}', class_='second-line')
div.append(second)
@ -204,6 +219,7 @@ def build_index(rd, books, num, search, sort, order, start, total, url_base, fie
'but it may not work well on a small screen')),
style='text-align:center')
)
return E.html(
E.head(
E.title(__appname__ + ' Library'),
@ -211,9 +227,9 @@ def build_index(rd, books, num, search, sort, order, start, total, url_base, fie
E.link(rel='stylesheet', type='text/css', href=ctx.url_for('/static', what='mobile.css')),
E.link(rel='apple-touch-icon', href=ctx.url_for('/static', what='calibre.png')),
E.meta(name='robots', content='noindex')
), # End head
),
body
) # End html
)
# }}}
@ -288,3 +304,25 @@ def legacy_get(ctx, rd, what, book_id, library_id, filename):
# https://www.mobileread.com/forums/showthread.php?t=364015
rd.outheaders.pop('Content-Disposition', '')
return ans
from calibre.srv.routes import endpoint
from calibre.srv.legacy_book_details import render_legacy_book_details
@endpoint('/legacy/book/{book_id}/{library_id}')
def legacy_book(ctx, rd, book_id, library_id):
# Set library_id in query to match get_library_data expectations
rd.query['library_id'] = library_id
db, library_id, library_map, default_library = get_library_data(ctx, rd)
try:
book_id = int(book_id)
except Exception:
raise HTTPRedirect(ctx.url_for('/mobile'))
with db.safe_read_lock:
if not ctx.has_id(rd, db, book_id):
raise BookNotFound(book_id, db)
mi = db.get_metadata(book_id, get_cover=False)
rd.outheaders['Last-Modified'] = http_date(timestampfromdt(db.last_modified()))
html_str = render_legacy_book_details(ctx, mi, library_id)
rd.outheaders.set('Content-Type', 'text/html; charset=UTF-8', replace_all=True)
return html_str.encode('utf-8')

View File

@ -0,0 +1,180 @@
from html import escape
from calibre.ebooks.metadata import authors_to_string
from calibre.utils.date import strftime, dt_as_local, is_date_undefined
def safe_date(dt, fmt='%d %b %Y'):
if not dt or is_date_undefined(dt):
return ''
return strftime(fmt, t=dt_as_local(dt).timetuple())
def render_legacy_book_details(ctx, mi, library_id):
book_id = mi.id
title = escape(mi.title or 'Unknown')
authors = escape(authors_to_string(mi.authors or []))
series = ''
if mi.series:
series = f'{escape(mi.series)}' + (f' [{mi.series_index}]' if mi.series_index is not None else '')
tags = mi.tags or []
tags_html = ', '.join(f'<a href="/mobile?library_id={library_id}&search=tags:%22%3D{escape(tag)}%22">{escape(tag)}</a>' for tag in tags)
comments = mi.comments or ''
# Formats
formats_html = ''
if mi.formats:
links = []
for fmt in mi.formats:
if not fmt or fmt.lower().startswith('original_'):
continue
url = ctx.url_for(
'/legacy/get',
what=fmt,
book_id=book_id,
library_id=library_id
)
links.append(f'<a href="{url}" class="download-button" download="{title}.{fmt.lower()}">Download {fmt}</a>')
formats_html = ' '.join(links)
cover_url = ctx.url_for(
'/get',
what='cover',
book_id=book_id,
library_id=library_id
)
# Build metadata table
metadata_rows = []
# Authors
if mi.authors:
author_links = []
for author in mi.authors:
search_url = f"/mobile?library_id={library_id}&search=authors:%22%3D{escape(author)}%22"
author_links.append(f'<a href="{search_url}">{escape(author)}</a>')
metadata_rows.append(f'<tr><td>Authors</td><td>{" ".join(author_links)}</td></tr>')
# Series
if series:
series_name = escape(mi.series)
search_url = f"/mobile?library_id={library_id}&search=series:%22%3D{series_name}%22"
metadata_rows.append(f'<tr><td>Series</td><td><a href="{search_url}">{series}</a></td></tr>')
# Tags
if tags:
metadata_rows.append(f'<tr><td>Tags</td><td>{tags_html}</td></tr>')
if mi.publisher:
metadata_rows.append(f'<tr><td>Publisher</td><td>{escape(mi.publisher)}</td></tr>')
if mi.pubdate:
date_str = safe_date(mi.pubdate)
search_url = f"/mobile?library_id={library_id}&search=pubdate:%22%3D{mi.pubdate.isoformat()}%22"
metadata_rows.append(f'<tr><td>Published</td><td><a href="{search_url}">{date_str}</a></td></tr>')
if mi.timestamp:
date_str = safe_date(mi.timestamp)
search_url = f"/mobile?library_id={library_id}&search=timestamp:%22%3D{mi.timestamp.isoformat()}%22"
metadata_rows.append(f'<tr><td>Date</td><td><a href="{search_url}">{date_str}</a></td></tr>')
if mi.rating and mi.rating > 0:
stars = '' * int(round(mi.rating))
search_url = f"/mobile?library_id={library_id}&search=rating:%22%3D{int(mi.rating)}%22"
metadata_rows.append(f'<tr><td>Rating</td><td><a href="{search_url}">{stars}</a></td></tr>')
if mi.languages:
lang_links = []
for lang in mi.languages:
search_url = f"/mobile?library_id={library_id}&search=languages:%22%3D{lang}%22"
lang_links.append(f'<a href="{search_url}">{lang}</a>')
metadata_rows.append(f'<tr><td>Languages</td><td>{", ".join(lang_links)}</td></tr>')
# Identifiers
if mi.identifiers:
id_links = []
for key, value in mi.identifiers.items():
if key.lower() in ('amazon', 'mobi-asin'):
url = f'https://www.amazon.com/dp/{value}'
display = 'Amazon.com'
elif key.lower() == 'goodreads':
url = f'https://www.goodreads.com/book/show/{value}'
display = 'Goodreads'
else:
url = f'#{key}:{value}' # fallback
display = f'{key}: {value}'
id_links.append(f'<a href="{url}" target="_blank">{display}</a>')
metadata_rows.append(f'<tr><td>Identifiers</td><td>{", ".join(id_links)}</td></tr>')
if mi.formats:
fmt_links = []
for fmt in mi.formats:
if not fmt or fmt.lower().startswith('original_'):
continue
fmt_links.append(f'<a href="javascript:void(0)" data-format="{fmt}" data-book-id="{book_id}">{fmt}</a>')
metadata_rows.append(f'<tr><td>Formats</td><td>{", ".join(fmt_links)}</td></tr>')
metadata_table = '<table class="metadata">' + ''.join(metadata_rows) + '</table>' if metadata_rows else ''
return f'''
<html>
<head>
<title>{title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {{ font-family: sans-serif; margin: 0; padding: 0; background-color: #f6f3e9; color: var(--calibre-color-window-foreground); }}
.top-bar {{ position: fixed; top: 0; left: 0; width: 100%; background: #39322b; color: #f6f3e9; padding: 0.5em; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); z-index: 1; }}
.top-bar a {{ text-decoration: none; color: #f6f3e9; }}
.top-bar .title {{ font-weight: bold; flex-grow: 1; margin-left: 0.5em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }}
.content {{ margin-top: 4em; padding: 1em; }}
.book-details {{ display: flex; flex-wrap: wrap; align-items: flex-start; }}
.cover {{ margin-right: 1em; margin-bottom: 1em; max-width: 200px; flex-shrink: 0; }}
.cover img {{ max-width: 100%; height: auto; border-radius: 10px; }}
.info {{ flex-grow: 1; min-width: 200px; }}
.metadata {{ width: 100%; border-collapse: collapse; margin-top: 1em; }}
.metadata td {{ padding: 0.5em; border-bottom: 1px solid #ddd; vertical-align: top; }}
.metadata td:first-child {{ font-weight: bold; width: 30%; }}
.metadata a {{ color: var(--calibre-color-link); text-decoration: none; }}
.metadata a:hover {{ text-decoration: underline; }}
.formats {{ margin-top: 1em; }}
.download-button {{ display: inline-block; padding: 0.5em 1em; background: #39322b; color: #f6f3e9; text-decoration: none; border-radius: 4px; margin-right: 0.5em; }}
.download-button:hover {{ background: #2a2520; }}
.description {{ margin-top: 2em; word-wrap: break-word; }}
@media (max-width: 600px) {{
.top-bar {{ font-size: 0.9em; padding: 0.4em; }}
.content {{ margin-top: 3.5em; padding: 0.5em; }}
.cover {{ max-width: 120px; margin-right: 0.5em; }}
.info {{ min-width: 150px; }}
.metadata td {{ padding: 0.3em; font-size: 0.9em; }}
.metadata td:first-child {{ width: 35%; }}
.download-button {{ padding: 0.4em 0.8em; font-size: 0.9em; }}
}}
</style>
</head>
<body>
<div class="top-bar">
<a href="javascript:history.back()" title="Back">&larr; Back</a>
<span class="title">{title}</span>
</div>
<div class="content">
<div class="book-details">
<div class="cover">
<img src="{cover_url}" alt="{title}" />
</div>
<div class="info">
<h1>{title}</h1>
<div class="formats">{formats_html}</div>
{metadata_table}
</div>
</div>
{f'<div class="description"><h2>Description</h2>{comments}</div>' if comments else ''}
</div>
</body>
</html>
'''

View File

@ -52,7 +52,6 @@ json.loads, json.dumps = jsonlib.loads, jsonlib.dumps
def route_key(route):
return route.partition('{')[0].rstrip('/')
def endpoint(route,
methods=default_methods,
types=None,
@ -105,7 +104,6 @@ def endpoint(route,
return f
return annotate
class Route:
var_pat = None