mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Content Server: Add a mobile friendly interface. Now if you access the content server using a mobile browser, a list of books that is easy to browse on small/e-ink screens is returned. Fixes #3870 (Add a mobile user interface for calibre)
This commit is contained in:
parent
919f675039
commit
8a0aad39f9
@ -24,7 +24,7 @@ except ImportError:
|
||||
from calibre.constants import __version__, __appname__
|
||||
from calibre.utils.genshi.template import MarkupTemplate
|
||||
from calibre import fit_image, guess_type, prepare_string_for_xml, \
|
||||
strftime as _strftime
|
||||
strftime as _strftime, prints
|
||||
from calibre.library import server_config as config
|
||||
from calibre.library.database2 import LibraryDatabase2, FIELD_MAP
|
||||
from calibre.utils.config import config_dir
|
||||
@ -77,6 +77,159 @@ class LibraryServer(object):
|
||||
</book>
|
||||
''')
|
||||
|
||||
MOBILE_UA = re.compile('(?i)(?:iPhone|Opera Mini|NetFront|webOS|Mobile|Android|imode|DoCoMo|Minimo|Blackberry|MIDP|Symbian)')
|
||||
|
||||
MOBILE_BOOK = textwrap.dedent('''\
|
||||
<tr xmlns:py="http://genshi.edgewall.org/">
|
||||
<td class="thumbnail">
|
||||
<img type="image/jpeg" src="/get/thumb/${r[0]}" border="0"/>
|
||||
</td>
|
||||
<td>
|
||||
<py:for each="format in r[13].split(',')">
|
||||
<span class="button"><a href="/get/${format}/${authors}-${r[1]}_${r[0]}.${format}">${format.lower()}</a></span>
|
||||
</py:for>
|
||||
${r[1]} by ${authors} - ${r[6]/1024}k - ${r[3] if r[3] else ''} ${pubdate} ${'['+r[7]+']' if r[7] else ''}
|
||||
</td>
|
||||
</tr>
|
||||
''')
|
||||
|
||||
MOBILE = MarkupTemplate(textwrap.dedent('''\
|
||||
<html xmlns:py="http://genshi.edgewall.org/">
|
||||
<head>
|
||||
<style>
|
||||
.navigation table.buttons {
|
||||
width: 100%;
|
||||
}
|
||||
.navigation .button {
|
||||
width: 50%;
|
||||
}
|
||||
.button a, .button:visited a {
|
||||
padding: 0.5em;
|
||||
font-size: 1.25em;
|
||||
border: 1px solid black;
|
||||
text-color: black;
|
||||
background-color: #ddd;
|
||||
border-top: 1px solid ThreeDLightShadow;
|
||||
border-right: 1px solid ButtonShadow;
|
||||
border-bottom: 1px solid ButtonShadow;
|
||||
border-left: 1 px solid ThreeDLightShadow;
|
||||
-moz-border-radius: 0.25em;
|
||||
-webkit-border-radius: 0.25em;
|
||||
}
|
||||
|
||||
.button:hover a {
|
||||
border-top: 1px solid #666;
|
||||
border-right: 1px solid #CCC;
|
||||
border-bottom: 1 px solid #CCC;
|
||||
border-left: 1 px solid #666;
|
||||
|
||||
|
||||
}
|
||||
div.navigation {
|
||||
padding-bottom: 1em;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#search_box {
|
||||
border: 1px solid #393;
|
||||
-moz-border-radius: 0.5em;
|
||||
-webkit-border-radius: 0.5em;
|
||||
padding: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
float: right;
|
||||
}
|
||||
|
||||
#listing {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
#listing td {
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
#listing td.thumbnail {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
#listing tr:nth-child(even) {
|
||||
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
#listing .button a{
|
||||
display: inline-block;
|
||||
width: 2.5em;
|
||||
padding-left: 0em;
|
||||
padding-right: 0em;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#logo {
|
||||
float: left;
|
||||
}
|
||||
#spacer {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
</style>
|
||||
<link rel="icon" href="http://calibre.kovidgoyal.net/chrome/site/favicon.ico" type="image/x-icon" />
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
'''))
|
||||
|
||||
LIBRARY = MarkupTemplate(textwrap.dedent('''\
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<library xmlns:py="http://genshi.edgewall.org/" start="$start" num="${len(books)}" total="$total" updated="${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}">
|
||||
@ -534,6 +687,52 @@ class LibraryServer(object):
|
||||
next_link=next_link, updated=updated, id='urn:calibre:main').render('xml')
|
||||
|
||||
|
||||
@expose
|
||||
def mobile(self, start='1', num='25', sort='date', search='',
|
||||
_=None, order='descending'):
|
||||
'''
|
||||
Serves metadata from the calibre database as XML.
|
||||
|
||||
:param sort: Sort results by ``sort``. Can be one of `title,author,rating`.
|
||||
:param search: Filter results by ``search`` query. See :class:`SearchQueryParser` for query syntax
|
||||
:param start,num: Return the slice `[start:start+num]` of the sorted and filtered results
|
||||
:param _: Firefox seems to sometimes send this when using XMLHttpRequest with no caching
|
||||
'''
|
||||
try:
|
||||
start = int(start)
|
||||
except ValueError:
|
||||
raise cherrypy.HTTPError(400, 'start: %s is not an integer'%start)
|
||||
try:
|
||||
num = int(num)
|
||||
except ValueError:
|
||||
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 = sorted(ids)
|
||||
items = [r for r in iter(self.db) if r[0] in ids]
|
||||
if sort is not None:
|
||||
self.sort(items, sort, (order.lower().strip() == 'ascending'))
|
||||
|
||||
book, books = MarkupTemplate(self.MOBILE_BOOK), []
|
||||
for record in items[(start-1):(start-1)+num]:
|
||||
aus = record[2] if record[2] else __builtin__._('Unknown')
|
||||
authors = '|'.join([i.replace('|', ',') for i in aus.split(',')])
|
||||
record[10] = fmt_sidx(float(record[10]))
|
||||
ts, pd = strftime('%Y/%m/%d %H:%M:%S', record[5]), \
|
||||
strftime('%Y/%m/%d %H:%M:%S', record[FIELD_MAP['pubdate']])
|
||||
books.append(book.generate(r=record, authors=authors, timestamp=ts,
|
||||
pubdate=pd).render('xml').decode('utf-8'))
|
||||
updated = self.db.last_modified()
|
||||
|
||||
cherrypy.response.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
||||
|
||||
|
||||
url_base = "/mobile?search=" + search+";order="+order+";sort="+sort+";num="+str(num)
|
||||
|
||||
return self.MOBILE.generate(books=books, start=start, updated=updated, search=search, sort=sort, order=order, num=num,
|
||||
total=len(ids), url_base=url_base).render('html')
|
||||
|
||||
|
||||
@expose
|
||||
def library(self, start='0', num='50', sort=None, search=None,
|
||||
_=None, order='ascending'):
|
||||
@ -584,10 +783,22 @@ class LibraryServer(object):
|
||||
cherrypy.request.headers.get('Stanza-Device-Name', 919) != 919 or \
|
||||
cherrypy.request.headers.get('Want-OPDS-Catalog', 919) != 919 or \
|
||||
ua.startswith('Stanza')
|
||||
return self.stanza(search=kwargs.get('search', None), sortby=kwargs.get('sortby',None), authorid=kwargs.get('authorid',None),
|
||||
|
||||
# A better search would be great
|
||||
want_mobile = self.MOBILE_UA.search(ua) is not None
|
||||
if self.opts.develop and not want_mobile:
|
||||
prints('User agent:', ua)
|
||||
|
||||
if want_opds:
|
||||
return self.stanza(search=kwargs.get('search', None), sortby=kwargs.get('sortby',None), authorid=kwargs.get('authorid',None),
|
||||
tagid=kwargs.get('tagid',None),
|
||||
seriesid=kwargs.get('seriesid',None),
|
||||
offset=kwargs.get('offset', 0)) if want_opds else self.static('index.html')
|
||||
offset=kwargs.get('offset', 0))
|
||||
|
||||
if want_mobile:
|
||||
return self.mobile()
|
||||
|
||||
return self.static('index.html')
|
||||
|
||||
|
||||
@expose
|
||||
|
Loading…
x
Reference in New Issue
Block a user