diff --git a/resources/content-server/mobile.css b/resources/content-server/mobile.css index e472805f54..116e90261f 100644 --- a/resources/content-server/mobile.css +++ b/resources/content-server/mobile.css @@ -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; diff --git a/src/calibre/srv/legacy.py b/src/calibre/srv/legacy.py index 76f0d84ce8..a7c335b978 100644 --- a/src/calibre/srv/legacy.py +++ b/src/calibre/srv/legacy.py @@ -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') \ No newline at end of file diff --git a/src/calibre/srv/legacy_book_details.py b/src/calibre/srv/legacy_book_details.py new file mode 100644 index 0000000000..90c004ef23 --- /dev/null +++ b/src/calibre/srv/legacy_book_details.py @@ -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'{escape(tag)}' 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'Download {fmt}') + + 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'{escape(author)}') + metadata_rows.append(f'Authors{" ".join(author_links)}') + + # 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'Series{series}') + + # Tags + if tags: + metadata_rows.append(f'Tags{tags_html}') + + if mi.publisher: + metadata_rows.append(f'Publisher{escape(mi.publisher)}') + + 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'Published{date_str}') + + 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'Date{date_str}') + + 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'Rating{stars}') + + 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'{lang}') + metadata_rows.append(f'Languages{", ".join(lang_links)}') + + # 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'{display}') + metadata_rows.append(f'Identifiers{", ".join(id_links)}') + + if mi.formats: + fmt_links = [] + for fmt in mi.formats: + if not fmt or fmt.lower().startswith('original_'): + continue + fmt_links.append(f'{fmt}') + metadata_rows.append(f'Formats{", ".join(fmt_links)}') + + metadata_table = '' + ''.join(metadata_rows) + '
' if metadata_rows else '' + + return f''' + + + {title} + + + + +
+ ← Back + {title} +
+
+
+
+ {title} +
+
+

{title}

+
{formats_html}
+ {metadata_table} +
+
+ {f'

Description

{comments}
' if comments else ''} +
+ + + ''' \ No newline at end of file diff --git a/src/calibre/srv/routes.py b/src/calibre/srv/routes.py index e43e09f88a..9dfd087943 100644 --- a/src/calibre/srv/routes.py +++ b/src/calibre/srv/routes.py @@ -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