mirror of
https://github.com/kovidgoyal/calibre.git
synced 2026-04-28 11:40:44 -04:00
Fix legacy book details endpoint routing and rendering; improve mobile styling
This commit is contained in:
parent
72f13096ac
commit
be839bf8ca
@ -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;
|
||||
|
||||
@ -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')
|
||||
180
src/calibre/srv/legacy_book_details.py
Normal file
180
src/calibre/srv/legacy_book_details.py
Normal 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">← 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>
|
||||
'''
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user