mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Move mobile server templates to lxml from genshi
This commit is contained in:
parent
359c0cd40e
commit
73753b67d8
@ -451,6 +451,20 @@ def prepare_string_for_xml(raw, attribute=False):
|
|||||||
def isbytestring(obj):
|
def isbytestring(obj):
|
||||||
return isinstance(obj, (str, bytes))
|
return isinstance(obj, (str, bytes))
|
||||||
|
|
||||||
|
def human_readable(size):
|
||||||
|
""" Convert a size in bytes into a human readable form """
|
||||||
|
divisor, suffix = 1, "B"
|
||||||
|
for i, candidate in enumerate(('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')):
|
||||||
|
if size < 1024**(i+1):
|
||||||
|
divisor, suffix = 1024**(i), candidate
|
||||||
|
break
|
||||||
|
size = str(float(size)/divisor)
|
||||||
|
if size.find(".") > -1:
|
||||||
|
size = size[:size.find(".")+2]
|
||||||
|
if size.endswith('.0'):
|
||||||
|
size = size[:-2]
|
||||||
|
return size + " " + suffix
|
||||||
|
|
||||||
if isosx:
|
if isosx:
|
||||||
import glob, shutil
|
import glob, shutil
|
||||||
fdir = os.path.expanduser('~/.fonts')
|
fdir = os.path.expanduser('~/.fonts')
|
||||||
|
@ -229,19 +229,6 @@ def info_dialog(parent, title, msg, det_msg='', show=False):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
def human_readable(size):
|
|
||||||
""" Convert a size in bytes into a human readable form """
|
|
||||||
divisor, suffix = 1, "B"
|
|
||||||
for i, candidate in enumerate(('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')):
|
|
||||||
if size < 1024**(i+1):
|
|
||||||
divisor, suffix = 1024**(i), candidate
|
|
||||||
break
|
|
||||||
size = str(float(size)/divisor)
|
|
||||||
if size.find(".") > -1:
|
|
||||||
size = size[:size.find(".")+2]
|
|
||||||
if size.endswith('.0'):
|
|
||||||
size = size[:-2]
|
|
||||||
return size + " " + suffix
|
|
||||||
|
|
||||||
class Dispatcher(QObject):
|
class Dispatcher(QObject):
|
||||||
'''Convenience class to ensure that a function call always happens in the
|
'''Convenience class to ensure that a function call always happens in the
|
||||||
|
@ -13,11 +13,10 @@ from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \
|
|||||||
QAbstractButton, QPainter, QLineEdit, QComboBox, \
|
QAbstractButton, QPainter, QLineEdit, QComboBox, \
|
||||||
QMenu, QStringListModel, QCompleter, QStringList
|
QMenu, QStringListModel, QCompleter, QStringList
|
||||||
|
|
||||||
from calibre.gui2 import human_readable, NONE, \
|
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, dynamic
|
||||||
error_dialog, pixmap_to_data, dynamic
|
|
||||||
|
|
||||||
from calibre.gui2.filename_pattern_ui import Ui_Form
|
from calibre.gui2.filename_pattern_ui import Ui_Form
|
||||||
from calibre import fit_image
|
from calibre import fit_image, human_readable
|
||||||
from calibre.utils.fonts import fontconfig
|
from calibre.utils.fonts import fontconfig
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.ebooks.metadata.meta import metadata_from_filename
|
from calibre.ebooks.metadata.meta import metadata_from_filename
|
||||||
|
@ -5,34 +5,143 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import re, copy
|
import re
|
||||||
import __builtin__
|
import __builtin__
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
from lxml import html
|
||||||
|
from lxml.html.builder import HTML, HEAD, TITLE, STYLE, LINK, DIV, IMG, BODY, \
|
||||||
|
OPTION, SELECT, INPUT, FORM, SPAN, TABLE, TR, TD, A, HR
|
||||||
|
|
||||||
from calibre.utils.genshi.template import MarkupTemplate
|
|
||||||
from calibre.library.server.utils import strftime
|
from calibre.library.server.utils import strftime
|
||||||
from calibre.ebooks.metadata import fmt_sidx
|
from calibre.ebooks.metadata import fmt_sidx
|
||||||
|
from calibre.constants import __appname__
|
||||||
|
from calibre import human_readable
|
||||||
|
|
||||||
# Templates {{{
|
def CLASS(*args, **kwargs): # class is a reserved word in Python
|
||||||
MOBILE_BOOK = '''\
|
kwargs['class'] = ' '.join(args)
|
||||||
<tr xmlns:py="http://genshi.edgewall.org/">
|
return kwargs
|
||||||
<td class="thumbnail">
|
|
||||||
<img type="image/jpeg" src="/get/thumb/${r[FM['id']]}" border="0"/>
|
|
||||||
</td>
|
def build_search_box(num, search, sort, order): # {{{
|
||||||
<td>
|
div = DIV(id='search_box')
|
||||||
<py:for each="format in r[FM['formats']].split(',')">
|
form = FORM('Show ', method='get', action='mobile')
|
||||||
<span class="button"><a href="/get/${format}/${authors}-${r[FM['title']]}_${r[FM['id']]}.${format}">${format.lower()}</a></span>
|
div.append(form)
|
||||||
</py:for>
|
|
||||||
${r[FM['title']]}${(' ['+r[FM['series']]+'-'+r[FM['series_index']]+']') if r[FM['series']] else ''} by ${authors} - ${r[FM['size']]/1024}k - ${r[FM['publisher']] if r[FM['publisher']] else ''} ${pubdate} ${'['+r[FM['tags']]+']' if r[FM['tags']] else ''}
|
num_select = SELECT(name='num')
|
||||||
</td>
|
for option in (5, 10, 25, 100):
|
||||||
</tr>
|
kwargs = {'value':str(option)}
|
||||||
|
if option == num:
|
||||||
|
kwargs['SELECTED'] = 'SELECTED'
|
||||||
|
num_select.append(OPTION(str(option), **kwargs))
|
||||||
|
num_select.tail = ' books matching '
|
||||||
|
form.append(num_select)
|
||||||
|
|
||||||
|
searchf = INPUT(name='search', id='s', value=search if search else '')
|
||||||
|
searchf.tail = ' sorted by '
|
||||||
|
form.append(searchf)
|
||||||
|
|
||||||
|
sort_select = SELECT(name='sort')
|
||||||
|
for option in ('date','author','title','rating','size','tags','series'):
|
||||||
|
kwargs = {'value':option}
|
||||||
|
if option == sort:
|
||||||
|
kwargs['SELECTED'] = 'SELECTED'
|
||||||
|
sort_select.append(OPTION(option, **kwargs))
|
||||||
|
form.append(sort_select)
|
||||||
|
|
||||||
|
order_select = SELECT(name='order')
|
||||||
|
for option in ('ascending','descending'):
|
||||||
|
kwargs = {'value':option}
|
||||||
|
if option == order:
|
||||||
|
kwargs['SELECTED'] = 'SELECTED'
|
||||||
|
order_select.append(OPTION(option, **kwargs))
|
||||||
|
form.append(order_select)
|
||||||
|
|
||||||
|
form.append(INPUT(id='go', type='submit', value='Search'))
|
||||||
|
|
||||||
|
return div
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def build_navigation(start, num, total, url_base): # {{{
|
||||||
|
end = min((start+num-1), total)
|
||||||
|
tagline = SPAN('Books %d to %d of %d'%(start, end, total),
|
||||||
|
style='display: block; text-align: center;')
|
||||||
|
left_buttons = TD(CLASS('button', style='text-align:left'))
|
||||||
|
right_buttons = TD(CLASS('button', style='text-align:right'))
|
||||||
|
|
||||||
|
if start > 1:
|
||||||
|
for t,s in [('First', 1), ('Previous', max(start-(num+1),1))]:
|
||||||
|
left_buttons.append(A(t, href='%s;start=%d'%(url_base, s)))
|
||||||
|
|
||||||
|
if total > start + num:
|
||||||
|
for t,s in [('Next', start+num), ('Last', total-num+1)]:
|
||||||
|
right_buttons.append(A(t, href='%s;start=%d'%(url_base, s)))
|
||||||
|
|
||||||
|
buttons = TABLE(
|
||||||
|
TR(left_buttons, right_buttons),
|
||||||
|
CLASS('buttons'))
|
||||||
|
return DIV(tagline, buttons, CLASS('navigation'))
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def build_index(books, num, search, sort, order, start, total, url_base):
|
||||||
|
logo = DIV(IMG(src='/static/calibre.png', alt=__appname__), id='logo')
|
||||||
|
|
||||||
|
search_box = build_search_box(num, search, sort, order)
|
||||||
|
navigation = build_navigation(start, num, total, url_base)
|
||||||
|
bookt = TABLE(id='listing')
|
||||||
|
|
||||||
|
body = BODY(
|
||||||
|
logo,
|
||||||
|
search_box,
|
||||||
|
navigation,
|
||||||
|
HR(CLASS('spacer')),
|
||||||
|
bookt
|
||||||
|
)
|
||||||
|
|
||||||
|
# Book list {{{
|
||||||
|
for book in books:
|
||||||
|
thumbnail = TD(
|
||||||
|
IMG(type='image/jpeg', border='0', src='/get/thumb/%s' %
|
||||||
|
book['id']),
|
||||||
|
CLASS('thumbnail'))
|
||||||
|
|
||||||
|
data = TD()
|
||||||
|
last = None
|
||||||
|
for fmt in book['formats'].split(','):
|
||||||
|
s = SPAN(
|
||||||
|
A(
|
||||||
|
fmt.lower(),
|
||||||
|
href='/get/%s/%s-%s_%d.%s' % (fmt, book['authors'],
|
||||||
|
book['title'], book['id'], fmt)
|
||||||
|
),
|
||||||
|
CLASS('button'))
|
||||||
|
s.tail = u'\u202f' #
|
||||||
|
last = s
|
||||||
|
data.append(s)
|
||||||
|
|
||||||
|
series = u'[%s - %s]'%(book['series'], book['series_index']) \
|
||||||
|
if book['series'] else ''
|
||||||
|
tags = u'[%s]'%book['tags'] if book['tags'] else ''
|
||||||
|
|
||||||
|
text = u'\u202f%s %s by %s - %s - %s %s' % (book['title'], series,
|
||||||
|
book['authors'], book['size'], book['timestamp'], tags)
|
||||||
|
|
||||||
|
if last is None:
|
||||||
|
data.text = text
|
||||||
|
else:
|
||||||
|
last.tail += text
|
||||||
|
|
||||||
|
bookt.append(TR(thumbnail, data))
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
return HTML(
|
||||||
|
HEAD(
|
||||||
|
TITLE(__appname__ + ' Library'),
|
||||||
|
LINK(rel='icon', href='http://calibre-ebook.com/favicon.ico',
|
||||||
|
type='image/x-icon'),
|
||||||
|
STYLE( # {{{
|
||||||
'''
|
'''
|
||||||
|
|
||||||
MOBILE = MarkupTemplate('''\
|
|
||||||
<html xmlns:py="http://genshi.edgewall.org/">
|
|
||||||
<head>
|
|
||||||
<style>
|
|
||||||
.navigation table.buttons {
|
.navigation table.buttons {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -109,64 +218,11 @@ div.navigation {
|
|||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
''', type='text/css') # }}}
|
||||||
<link rel="icon" href="http://calibre-ebook.com/favicon.ico" type="image/x-icon" />
|
), # End head
|
||||||
</head>
|
body
|
||||||
<body>
|
) # End html
|
||||||
<div id="logo">
|
|
||||||
<img src="/static/calibre.png" alt="Calibre" />
|
|
||||||
</div>
|
|
||||||
<div id="search_box">
|
|
||||||
<form method="get" action="/mobile">
|
|
||||||
Show <select name="num">
|
|
||||||
<py:for each="option in [5,10,25,100]">
|
|
||||||
<option py:if="option == num" value="${option}" SELECTED="SELECTED">${option}</option>
|
|
||||||
<option py:if="option != num" value="${option}">${option}</option>
|
|
||||||
</py:for>
|
|
||||||
</select>
|
|
||||||
books matching <input name="search" id="s" value="${search}" /> sorted by
|
|
||||||
|
|
||||||
<select name="sort">
|
|
||||||
<py:for each="option in ['date','author','title','rating','size','tags','series']">
|
|
||||||
<option py:if="option == sort" value="${option}" SELECTED="SELECTED">${option}</option>
|
|
||||||
<option py:if="option != sort" value="${option}">${option}</option>
|
|
||||||
</py:for>
|
|
||||||
</select>
|
|
||||||
<select name="order">
|
|
||||||
<py:for each="option in ['ascending','descending']">
|
|
||||||
<option py:if="option == order" value="${option}" SELECTED="SELECTED">${option}</option>
|
|
||||||
<option py:if="option != order" value="${option}">${option}</option>
|
|
||||||
</py:for>
|
|
||||||
</select>
|
|
||||||
<input id="go" type="submit" value="Search"/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="navigation">
|
|
||||||
<span style="display: block; text-align: center;">Books ${start} to ${ min((start+num-1) , total) } of ${total}</span>
|
|
||||||
<table class="buttons">
|
|
||||||
<tr>
|
|
||||||
<td class="button" style="text-align:left;">
|
|
||||||
<a py:if="start > 1" href="${url_base};start=1">First</a>
|
|
||||||
<a py:if="start > 1" href="${url_base};start=${max(start-(num+1),1)}">Previous</a>
|
|
||||||
</td>
|
|
||||||
<td class="button" style="text-align: right;">
|
|
||||||
<a py:if=" total > (start + num) " href="${url_base};start=${start+num}">Next</a>
|
|
||||||
<a py:if=" total > (start + num) " href="${url_base};start=${total-num+1}">Last</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<hr class="spacer" />
|
|
||||||
<table id="listing">
|
|
||||||
<py:for each="book in books">
|
|
||||||
${Markup(book)}
|
|
||||||
</py:for>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
''')
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class MobileServer(object):
|
class MobileServer(object):
|
||||||
'A view optimized for browsers in mobile devices'
|
'A view optimized for browsers in mobile devices'
|
||||||
@ -195,26 +251,31 @@ class MobileServer(object):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise cherrypy.HTTPError(400, 'num: %s is not an integer'%num)
|
raise cherrypy.HTTPError(400, 'num: %s is not an integer'%num)
|
||||||
ids = self.db.data.parse(search) if search and search.strip() else self.db.data.universal_set()
|
ids = self.db.data.parse(search) if search and search.strip() else self.db.data.universal_set()
|
||||||
ids = sorted(ids)
|
|
||||||
FM = self.db.FIELD_MAP
|
FM = self.db.FIELD_MAP
|
||||||
items = copy.deepcopy([r for r in iter(self.db) if r[FM['id']] in ids])
|
items = [r for r in iter(self.db) if r[FM['id']] in ids]
|
||||||
if sort is not None:
|
if sort is not None:
|
||||||
self.sort(items, sort, (order.lower().strip() == 'ascending'))
|
self.sort(items, sort, (order.lower().strip() == 'ascending'))
|
||||||
|
|
||||||
book, books = MarkupTemplate(MOBILE_BOOK), []
|
books = []
|
||||||
for record in items[(start-1):(start-1)+num]:
|
for record in items[(start-1):(start-1)+num]:
|
||||||
if record[FM['formats']] is None:
|
book = {'formats':record[FM['formats']], 'size':record[FM['size']]}
|
||||||
record[FM['formats']] = ''
|
if not book['formats']:
|
||||||
if record[FM['size']] is None:
|
book['formats'] = ''
|
||||||
record[FM['size']] = 0
|
if not book['size']:
|
||||||
|
book['size'] = 0
|
||||||
|
book['size'] = human_readable(book['size'])
|
||||||
|
|
||||||
aus = record[FM['authors']] if record[FM['authors']] else __builtin__._('Unknown')
|
aus = record[FM['authors']] if record[FM['authors']] else __builtin__._('Unknown')
|
||||||
authors = '|'.join([i.replace('|', ',') for i in aus.split(',')])
|
authors = '|'.join([i.replace('|', ',') for i in aus.split(',')])
|
||||||
record[FM['series_index']] = \
|
book['authors'] = authors
|
||||||
fmt_sidx(float(record[FM['series_index']]))
|
book['series_index'] = fmt_sidx(float(record[FM['series_index']]))
|
||||||
ts, pd = strftime('%Y/%m/%d %H:%M:%S', record[FM['timestamp']]), \
|
book['series'] = record[FM['series']]
|
||||||
strftime('%Y/%m/%d %H:%M:%S', record[FM['pubdate']])
|
book['tags'] = record[FM['tags']]
|
||||||
books.append(book.generate(r=record, authors=authors, timestamp=ts,
|
book['title'] = record[FM['title']]
|
||||||
pubdate=pd, FM=FM).render('xml').decode('utf-8'))
|
for x in ('timestamp', 'pubdate'):
|
||||||
|
book[x] = strftime('%Y/%m/%d %H:%M:%S', record[FM[x]])
|
||||||
|
book['id'] = record[FM['id']]
|
||||||
|
books.append(book)
|
||||||
updated = self.db.last_modified()
|
updated = self.db.last_modified()
|
||||||
|
|
||||||
cherrypy.response.headers['Content-Type'] = 'text/html; charset=utf-8'
|
cherrypy.response.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
@ -223,8 +284,8 @@ class MobileServer(object):
|
|||||||
|
|
||||||
url_base = "/mobile?search=" + search+";order="+order+";sort="+sort+";num="+str(num)
|
url_base = "/mobile?search=" + search+";order="+order+";sort="+sort+";num="+str(num)
|
||||||
|
|
||||||
return MOBILE.generate(books=books, start=start, updated=updated,
|
return html.tostring(build_index(books, num, search, sort, order,
|
||||||
search=search, sort=sort, order=order, num=num, FM=FM,
|
start, len(ids), url_base),
|
||||||
total=len(ids), url_base=url_base).render('html')
|
encoding='utf-8', include_meta_content_type=True,
|
||||||
|
pretty_print=True)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user