diff --git a/resources/content_server/browse/browse.css b/resources/content_server/browse/browse.css
index bd713625b4..92ed4c3ce6 100644
--- a/resources/content_server/browse/browse.css
+++ b/resources/content_server/browse/browse.css
@@ -187,6 +187,9 @@ h2.library_name {
list-style-type: none;
margin: 0;
padding: 0;
+ margin-left: auto;
+ margin-right: auto;
+ display: block;
}
.toplevel li {
@@ -194,15 +197,20 @@ h2.library_name {
padding: 0.75em;
cursor: pointer;
font-size: larger;
+ float: left;
border-radius: 15px;
-moz-border-radius: 15px;
-webkit-border-radius: 15px;
+ display: inline;
+ width: 250px;
+ height: 48px;
+ overflow: hidden;
}
.toplevel li img {
vertical-align: middle;
- margin-right: 2em;
+ margin-right: 1em;
}
.toplevel li:hover {
@@ -214,7 +222,10 @@ h2.library_name {
}
-.toplevel li span { display: none }
+.toplevel li span.url { display: none }
+.toplevel li span.label {
+}
+
/* }}} */
@@ -406,12 +417,12 @@ h2.library_name {
margin-bottom: 2ex;
}
-#book_details_dialog .details a {
+.details .right .formats a {
color: blue;
text-decoration: none;
}
-#book_details_dialog .details a:hover {
+.details .right .formats a:hover {
color: red;
}
diff --git a/resources/content_server/browse/browse.js b/resources/content_server/browse/browse.js
index 367b8341d9..29b84ac2d7 100644
--- a/resources/content_server/browse/browse.js
+++ b/resources/content_server/browse/browse.js
@@ -1,5 +1,35 @@
// Cookies {{{
+/**
+ * Create a cookie with the given name and value and other optional parameters.
+ *
+ * @example $.cookie('the_cookie', 'the_value');
+ * @desc Set the value of a cookie.
+ * @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true });
+ * @desc Create a cookie with all available options.
+ * @example $.cookie('the_cookie', 'the_value');
+ * @desc Create a session cookie.
+ * @example $.cookie('the_cookie', null);
+ * @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain
+ * used when the cookie was set.
+ *
+ * @param String name The name of the cookie.
+ * @param String value The value of the cookie.
+ * @param Object options An object literal containing key/value pairs to provide optional cookie attributes.
+ * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object.
+ * If a negative value is specified (e.g. a date in the past), the cookie will be deleted.
+ * If set to null or omitted, the cookie will be a session cookie and will not be retained
+ * when the the browser exits.
+ * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie).
+ * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie).
+ * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will
+ * require a secure protocol (like HTTPS).
+ * @type undefined
+ *
+ * @name $.cookie
+ * @cat Plugins/Cookie
+ * @author Klaus Hartl/klaus.hartl@stilbuero.de
+ */
function cookie(name, value, options) {
if (typeof value != 'undefined') { // name and value given, set cookie
@@ -55,7 +85,7 @@ function init_sort_combobox() {
selectedList: 1,
click: function(event, ui){
$(this).multiselect("close");
- cookie(sort_cookie_name, ui.value, {expires: 365});
+ cookie(sort_cookie_name, ui.value);
window.location.reload();
}
});
@@ -74,13 +104,25 @@ function init() {
}
// Top-level feed {{{
+
+function toplevel_layout() {
+ var last = $(".toplevel li").last();
+ var title = $('.toplevel h3').first();
+ var bottom = last.position().top + last.height() - title.position().top;
+ $("#main").height(Math.max(200, bottom));
+}
+
function toplevel() {
$(".sort_select").hide();
$(".toplevel li").click(function() {
- var href = $(this).children("span").html();
+ var href = $(this).children("span.url").text();
window.location = href;
});
+
+ toplevel_layout();
+ $(window).resize(toplevel_layout);
+
}
// }}}
@@ -193,8 +235,10 @@ function load_page(elem) {
elem.show();
}
+function hidesort() {$("#content > .sort_select").hide();}
+
function booklist(hide_sort) {
- if (hide_sort) $("#content > .sort_select").hide();
+ if (hide_sort) hidesort();
var test = $("#booklist #page0").html();
if (!test) {
$("#booklist").html(render_error("No books found"));
@@ -233,3 +277,13 @@ function show_details(a_dom) {
}
// }}}
+
+function book() {
+ hidesort();
+ $('.details .left img').load(function() {
+ var img = $('.details .left img');
+ var height = $('#main').height();
+ height = Math.max(height, img.height() + 100);
+ $('#main').height(height);
+ });
+}
diff --git a/resources/content_server/browse/summary.html b/resources/content_server/browse/summary.html
index 4e9c9d2a77..de175d3b53 100644
--- a/resources/content_server/browse/summary.html
+++ b/resources/content_server/browse/summary.html
@@ -8,6 +8,7 @@
{stars}
{series}
{details}
+ {permalink}
{title}
{authors}
diff --git a/resources/recipes/malaysian_mirror.recipe b/resources/recipes/malaysian_mirror.recipe
new file mode 100644
index 0000000000..e61538431a
--- /dev/null
+++ b/resources/recipes/malaysian_mirror.recipe
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+__license__ = 'GPL v3'
+__author__ = 'Tony Stegall'
+__copyright__ = '2010, Tony Stegall or Tonythebookworm on mobiread.com'
+__version__ = '1'
+__date__ = '16, October 2010'
+__docformat__ = 'English'
+
+
+
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class MalaysianMirror(BasicNewsRecipe):
+ title = 'MalaysianMirror'
+ __author__ = 'Tonythebookworm'
+ description = 'The Pulse of the Nation'
+ language = 'en'
+ no_stylesheets = True
+ publisher = 'Tonythebookworm'
+ category = 'news'
+ use_embedded_content= False
+ no_stylesheets = True
+ oldest_article = 24
+
+ remove_javascript = True
+ remove_empty_feeds = True
+ conversion_options = {'linearize_tables' : True}
+ extra_css = '''
+ #content_heading{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
+
+ td{text-align:right; font-size:small;margin-top:0px;margin-bottom: 0px;}
+
+ #content_body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
+ '''
+
+ keep_only_tags = [dict(name='table', attrs={'class':['contentpaneopen']})
+ ]
+ remove_tags = [dict(name='table', attrs={'class':['buttonheading']})]
+ #######################################################################################################################
+
+
+ max_articles_per_feed = 10
+
+ '''
+ Make a variable that will hold the url for the main site because our links do not include the index
+ '''
+
+ INDEX = 'http://www.malaysianmirror.com'
+
+
+
+
+ def parse_index(self):
+ feeds = []
+ for title, url in [
+ (u"Media Buzz", u"http://www.malaysianmirror.com/media-buzz-front"),
+ (u"Life Style", u"http://www.malaysianmirror.com/lifestylefront"),
+ (u"Features", u"http://www.malaysianmirror.com/featurefront"),
+
+
+ ]:
+ articles = self.make_links(url)
+ if articles:
+ feeds.append((title, articles))
+ return feeds
+
+ def make_links(self, url):
+ title = 'Temp'
+ current_articles = []
+ soup = self.index_to_soup(url)
+ # print 'The soup is: ', soup
+ for item in soup.findAll('div', attrs={'class':'contentheading'}):
+ #print 'item is: ', item
+ link = item.find('a')
+ #print 'the link is: ', link
+ if link:
+ url = self.INDEX + link['href']
+ title = self.tag_to_string(link)
+ #print 'the title is: ', title
+ #print 'the url is: ', url
+ #print 'the title is: ', title
+ current_articles.append({'title': title, 'url': url, 'description':'', 'date':''}) # append all this
+ return current_articles
+
+ def preprocess_html(self, soup):
+ for item in soup.findAll(attrs={'style':True}):
+ del item['style']
+ return soup
+
diff --git a/src/calibre/devices/udisks.py b/src/calibre/devices/udisks.py
new file mode 100644
index 0000000000..ba26c2b56c
--- /dev/null
+++ b/src/calibre/devices/udisks.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+__license__ = 'GPL v3'
+__copyright__ = '2010, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+import dbus
+import os
+
+def node_mountpoint(node):
+
+ def de_mangle(raw):
+ return raw.replace('\\040', ' ').replace('\\011', '\t').replace('\\012',
+ '\n').replace('\\0134', '\\')
+
+ for line in open('/proc/mounts').readlines():
+ line = line.split()
+ if line[0] == node:
+ return de_mangle(line[1])
+ return None
+
+
+class UDisks(object):
+
+ def __init__(self):
+ if os.environ.get('CALIBRE_DISABLE_UDISKS', False):
+ raise Exception('User has aborted use of UDISKS')
+ self.bus = dbus.SystemBus()
+ self.main = dbus.Interface(self.bus.get_object('org.freedesktop.UDisks',
+ '/org/freedesktop/UDisks'), 'org.freedesktop.UDisks')
+
+ def device(self, device_node_path):
+ devpath = self.main.FindDeviceByDeviceFile(device_node_path)
+ return dbus.Interface(self.bus.get_object('org.freedesktop.UDisks',
+ devpath), 'org.freedesktop.UDisks.Device')
+
+ def mount(self, device_node_path):
+ d = self.device(device_node_path)
+ try:
+ return unicode(d.FilesystemMount('',
+ ['auth_no_user_interaction', 'rw', 'noexec', 'nosuid',
+ 'sync', 'nodev', 'uid=1000', 'gid=1000']))
+ except:
+ # May be already mounted, check
+ mp = node_mountpoint(str(device_node_path))
+ if mp is None:
+ raise
+ return mp
+
+ def unmount(self, device_node_path):
+ d = self.device(device_node_path)
+ d.FilesystemUnmount(['force'])
+
+ def eject(self, device_node_path):
+ parent = device_node_path
+ while parent[-1] in '0123456789':
+ parent = parent[:-1]
+ devices = [str(x) for x in self.main.EnumerateDeviceFiles()]
+ for d in devices:
+ if d.startswith(parent) and d != parent:
+ try:
+ self.unmount(d)
+ except:
+ import traceback
+ print 'Failed to unmount:', d
+ traceback.print_exc()
+ d = self.device(parent)
+ d.DriveEject([])
+
+def mount(node_path):
+ u = UDisks()
+ u.mount(node_path)
+
+def eject(node_path):
+ u = UDisks()
+ u.eject(node_path)
+
+if __name__ == '__main__':
+ import sys
+ dev = sys.argv[1]
+ print 'Testing with node', dev
+ u = UDisks()
+ print 'Mounted at:', u.mount(dev)
+ print 'Ejecting'
+ u.eject(dev)
+
+
diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py
index 6fcfb9e7f0..6f938cbcbd 100644
--- a/src/calibre/devices/usbms/device.py
+++ b/src/calibre/devices/usbms/device.py
@@ -530,16 +530,8 @@ class Device(DeviceConfig, DevicePlugin):
return drives
def node_mountpoint(self, node):
-
- def de_mangle(raw):
- return raw.replace('\\040', ' ').replace('\\011', '\t').replace('\\012',
- '\n').replace('\\0134', '\\')
-
- for line in open('/proc/mounts').readlines():
- line = line.split()
- if line[0] == node:
- return de_mangle(line[1])
- return None
+ from calibre.devices.udisks import node_mountpoint
+ return node_mountpoint(node)
def find_largest_partition(self, path):
node = path.split('/')[-1]
@@ -585,6 +577,13 @@ class Device(DeviceConfig, DevicePlugin):
label += ' (%d)'%extra
def do_mount(node, label):
+ try:
+ from calibre.devices.udisks import mount
+ mount(node)
+ return 0
+ except:
+ pass
+
cmd = 'calibre-mount-helper'
if getattr(sys, 'frozen_path', False):
cmd = os.path.join(sys.frozen_path, cmd)
@@ -617,6 +616,7 @@ class Device(DeviceConfig, DevicePlugin):
if not mp.endswith('/'): mp += '/'
self._linux_mount_map[main] = mp
self._main_prefix = mp
+ self._linux_main_device_node = main
cards = [(carda, '_card_a_prefix', 'carda'),
(cardb, '_card_b_prefix', 'cardb')]
for card, prefix, typ in cards:
@@ -732,6 +732,11 @@ class Device(DeviceConfig, DevicePlugin):
pass
def eject_linux(self):
+ try:
+ from calibre.devices.udisks import eject
+ return eject(self._linux_main_device_node)
+ except:
+ pass
drives = self.find_device_nodes()
for drive in drives:
if drive:
diff --git a/src/calibre/ebooks/metadata/toc.py b/src/calibre/ebooks/metadata/toc.py
index 8c6f3f6baf..0ed527d26a 100644
--- a/src/calibre/ebooks/metadata/toc.py
+++ b/src/calibre/ebooks/metadata/toc.py
@@ -2,7 +2,7 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
-import os, glob, re
+import os, glob, re, functools
from urlparse import urlparse
from urllib import unquote
from uuid import uuid4
@@ -11,7 +11,7 @@ from lxml import etree
from lxml.builder import ElementMaker
from calibre.constants import __appname__, __version__
-from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, BeautifulSoup
+from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.ebooks.chardet import xml_to_unicode
NCX_NS = "http://www.daisy.org/z3986/2005/ncx/"
@@ -26,14 +26,6 @@ E = ElementMaker(namespace=NCX_NS, nsmap=NSMAP)
C = ElementMaker(namespace=CALIBRE_NS, nsmap=NSMAP)
-class NCXSoup(BeautifulStoneSoup):
-
- NESTABLE_TAGS = {'navpoint':[]}
-
- def __init__(self, raw):
- BeautifulStoneSoup.__init__(self, raw,
- convertEntities=BeautifulSoup.HTML_ENTITIES,
- selfClosingTags=['meta', 'content'])
class TOC(list):
@@ -166,40 +158,60 @@ class TOC(list):
def read_ncx_toc(self, toc):
self.base_path = os.path.dirname(toc)
- raw = xml_to_unicode(open(toc, 'rb').read(), assume_utf8=True)[0]
- soup = NCXSoup(raw)
+ raw = xml_to_unicode(open(toc, 'rb').read(), assume_utf8=True,
+ strip_encoding_pats=True)[0]
+ root = etree.fromstring(raw, parser=etree.XMLParser(recover=True,
+ no_network=True))
+ xpn = {'re': 'http://exslt.org/regular-expressions'}
+ XPath = functools.partial(etree.XPath, namespaces=xpn)
+
+ def get_attr(node, default=None, attr='playorder'):
+ for name, val in node.attrib.items():
+ if name and val and name.lower().endswith(attr):
+ return val
+ return default
+
+ nl_path = XPath('./*[re:match(local-name(), "navlabel$", "i")]')
+ txt_path = XPath('./*[re:match(local-name(), "text$", "i")]')
+ content_path = XPath('./*[re:match(local-name(), "content$", "i")]')
+ np_path = XPath('./*[re:match(local-name(), "navpoint$", "i")]')
def process_navpoint(np, dest):
- play_order = np.get('playOrder', None)
- if play_order is None:
- play_order = int(np.get('playorder', 1))
+ try:
+ play_order = int(get_attr(np, 1))
+ except:
+ play_order = 1
href = fragment = text = None
- nl = np.find(re.compile('navlabel'))
- if nl is not None:
+ nl = nl_path(np)
+ if nl:
+ nl = nl[0]
text = u''
- for txt in nl.findAll(re.compile('text')):
- text += u''.join([unicode(s) for s in txt.findAll(text=True)])
- content = np.find(re.compile('content'))
- if content is None or not content.has_key('src') or not txt:
+ for txt in txt_path(nl):
+ text += etree.tostring(txt, method='text',
+ encoding=unicode, with_tail=False)
+ content = content_path(np)
+ if not content or not text:
+ return
+ content = content[0]
+ src = get_attr(content, attr='src')
+ if src is None:
return
- purl = urlparse(unquote(content['src']))
+ purl = urlparse(unquote(content.get('src')))
href, fragment = purl[2], purl[5]
nd = dest.add_item(href, fragment, text)
nd.play_order = play_order
- for c in np:
- if 'navpoint' in getattr(c, 'name', ''):
- process_navpoint(c, nd)
+ for c in np_path(np):
+ process_navpoint(c, nd)
- nm = soup.find(re.compile('navmap'))
- if nm is None:
+ nm = XPath('//*[re:match(local-name(), "navmap$", "i")]')(root)
+ if not nm:
raise ValueError('NCX files must have a element.')
+ nm = nm[0]
- for elem in nm:
- if 'navpoint' in getattr(elem, 'name', ''):
- process_navpoint(elem, self)
-
+ for child in np_path(nm):
+ process_navpoint(child, self)
def read_html_toc(self, toc):
self.base_path = os.path.dirname(toc)
diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py
index 5881bc9207..247e6945e6 100644
--- a/src/calibre/library/server/browse.py
+++ b/src/calibre/library/server/browse.py
@@ -6,13 +6,13 @@ __copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
import operator, os, json
-from urllib import quote
from binascii import hexlify, unhexlify
import cherrypy
from calibre.constants import filesystem_encoding
-from calibre import isbytestring, force_unicode, prepare_string_for_xml as xml
+from calibre import isbytestring, force_unicode, fit_image, \
+ prepare_string_for_xml as xml
from calibre.utils.ordered_dict import OrderedDict
from calibre.utils.filenames import ascii_filename
from calibre.utils.config import prefs
@@ -133,9 +133,12 @@ def get_category_items(category, items, db, datatype): # {{{
desc = ''
if i.count > 0:
desc += '[' + _('%d books')%i.count + ']'
- href = '/browse/matches/%s/%s'%(category, id_)
+ q = i.category
+ if not q:
+ q = category
+ href = '/browse/matches/%s/%s'%(q, id_)
return templ.format(xml(name), rating,
- xml(desc), xml(quote(href)), rstring)
+ xml(desc), xml(href), rstring)
items = list(map(item, items))
return '\n'.join([''] + items + ['
'])
@@ -194,6 +197,8 @@ class BrowseServer(object):
self.browse_search)
connect('browse_details', base_href+'/details/{id}',
self.browse_details)
+ connect('browse_book', base_href+'/book/{id}',
+ self.browse_book)
connect('browse_category_icon', base_href+'/icon/{name}',
self.browse_icon)
@@ -218,10 +223,10 @@ class BrowseServer(object):
sort_opts, added = [], set([])
displayed_custom_fields = custom_fields_to_display(self.db)
for x in fm.sortable_field_keys():
- if x == 'ondevice':
+ if x in ('ondevice', 'formats', 'sort'):
continue
if fm[x]['is_custom'] and x not in displayed_custom_fields:
- continue
+ continue
if x == 'comments' or fm[x]['datatype'] == 'comments':
continue
n = fm[x]['name']
@@ -268,23 +273,31 @@ class BrowseServer(object):
# Catalogs {{{
def browse_icon(self, name='blank.png'):
- try:
- data = I(name, data=True)
- except:
- raise cherrypy.HTTPError(404, 'no icon named: %r'%name)
- img = Image()
- img.load(data)
- img.size = (48, 48)
cherrypy.response.headers['Content-Type'] = 'image/png'
cherrypy.response.headers['Last-Modified'] = self.last_modified(self.build_time)
- return img.export('png')
+ if not hasattr(self, '__browse_icon_cache__'):
+ self.__browse_icon_cache__ = {}
+ if name not in self.__browse_icon_cache__:
+ try:
+ data = I(name, data=True)
+ except:
+ raise cherrypy.HTTPError(404, 'no icon named: %r'%name)
+ img = Image()
+ img.load(data)
+ width, height = img.size
+ scaled, width, height = fit_image(width, height, 48, 48)
+ if scaled:
+ img.size = (width, height)
+
+ self.__browse_icon_cache__[name] = img.export('png')
+ return self.__browse_icon_cache__[name]
def browse_toplevel(self):
categories = self.categories_cache()
category_meta = self.db.field_metadata
cats = [
- (_('Newest'), 'newest', 'blank.png'),
+ (_('Newest'), 'newest', 'forward.png'),
]
def getter(x):
@@ -292,7 +305,7 @@ class BrowseServer(object):
displayed_custom_fields = custom_fields_to_display(self.db)
for category in sorted(categories,
- cmp=lambda x,y: cmp(getter(x), getter(y))):
+ cmp=lambda x,y: cmp(getter(x), getter(y))):
if len(categories[category]) == 0:
continue
if category == 'formats':
@@ -313,9 +326,10 @@ class BrowseServer(object):
icon = 'blank.png'
cats.append((meta['name'], category, icon))
- cats = [('
{0}'
- '/browse/category/{1}')
- .format(xml(x, True), xml(quote(y)), xml(_('Browse books by')),
+ cats = [('
'
+ '{0}'
+ '/browse/category/{1}')
+ .format(xml(x, True), xml(y), xml(_('Browse books by')),
src='/browse/icon/'+z)
for x, y, z in cats]
@@ -452,7 +466,8 @@ class BrowseServer(object):
sort = 'title'
self.sort(items, 'title', True)
if sort != 'title':
- ascending = fm[sort]['datatype'] not in ('rating', 'datetime')
+ ascending = fm[sort]['datatype'] not in ('rating', 'datetime',
+ 'series')
self.sort(items, sort, ascending)
return sort
@@ -464,14 +479,17 @@ class BrowseServer(object):
if category not in categories and category != 'newest':
raise cherrypy.HTTPError(404, 'category not found')
+ fm = self.db.field_metadata
try:
- category_name = self.db.field_metadata[category]['name']
+ category_name = fm[category]['name']
+ dt = fm[category]['datatype']
except:
if category != 'newest':
raise
category_name = _('Newest')
+ dt = None
- hide_sort = 'false'
+ hide_sort = 'true' if dt == 'series' else 'false'
if category == 'search':
which = unhexlify(cid)
try:
@@ -482,11 +500,16 @@ class BrowseServer(object):
ids = list(self.db.data.iterallids())
hide_sort = 'true'
else:
- ids = self.db.get_books_for_category(category, cid)
+ q = category
+ if q == 'news':
+ q = 'tags'
+ ids = self.db.get_books_for_category(q, cid)
items = [self.db.data._data[x] for x in ids]
if category == 'newest':
list_sort = 'timestamp'
+ if dt == 'series':
+ list_sort = category
sort = self.browse_sort_book_list(items, list_sort)
ids = [x[0] for x in items]
html = render_book_list(ids, suffix=_('in') + ' ' + category_name)
@@ -568,23 +591,19 @@ class BrowseServer(object):
args['series'] = args['series']
args['details'] = xml(_('Details'), True)
args['details_tt'] = xml(_('Show book details'), True)
+ args['permalink'] = xml(_('Permalink'), True)
+ args['permalink_tt'] = xml(_('A permanent link to this book'), True)
summs.append(self.browse_summary_template.format(**args))
return json.dumps('\n'.join(summs), ensure_ascii=False)
- @Endpoint(mimetype='application/json; charset=utf-8')
- def browse_details(self, id=None):
- try:
- id_ = int(id)
- except:
- raise cherrypy.HTTPError(404, 'invalid id: %r'%id)
-
+ def browse_render_details(self, id_):
try:
mi = self.db.get_metadata(id_, index_is_id=True)
except:
- ans = _('This book has been deleted')
+ return _('This book has been deleted')
else:
args, fmt, fmts, fname = self.browse_get_book_args(mi, id_)
args['formats'] = ''
@@ -625,13 +644,34 @@ class BrowseServer(object):
u'') % (xml(c[0]),
c[1]) for c in comments]
comments = u''%('\n\n'.join(comments))
- ans = self.browse_details_template.format(id=id_,
+
+ return self.browse_details_template.format(id=id_,
title=xml(mi.title, True), fields=fields,
formats=args['formats'], comments=comments)
+ @Endpoint(mimetype='application/json; charset=utf-8')
+ def browse_details(self, id=None):
+ try:
+ id_ = int(id)
+ except:
+ raise cherrypy.HTTPError(404, 'invalid id: %r'%id)
+
+ ans = self.browse_render_details(id_)
+
return json.dumps(ans, ensure_ascii=False)
+ @Endpoint()
+ def browse_book(self, id=None, category_sort=None):
+ try:
+ id_ = int(id)
+ except:
+ raise cherrypy.HTTPError(404, 'invalid id: %r'%id)
+
+ ans = self.browse_render_details(id_)
+ return self.browse_template('').format(
+ title='', script='book();', main=ans)
+
# }}}
diff --git a/src/calibre/manual/customize.rst b/src/calibre/manual/customize.rst
index c35defc0b0..e0f799f572 100644
--- a/src/calibre/manual/customize.rst
+++ b/src/calibre/manual/customize.rst
@@ -24,6 +24,7 @@ Environment variables
* ``CALIBRE_OVERRIDE_DATABASE_PATH`` - allows you to specify the full path to metadata.db. Using this variable you can have metadata.db be in a location other than the library folder. Useful if your library folder is on a networked drive that does not support file locking.
* ``CALIBRE_DEVELOP_FROM`` - Used to run from a calibre development environment. See :ref:`develop`.
* ``CALIBRE_OVERRIDE_LANG`` - Used to force the language used by the interface (ISO 639 language code)
+ * ``CALIBRE_DISABLE_UDISKS`` - Used to disable the use of udisks for mounting/ejecting. Set it to 1 to use calibre-mount-helper instead.
* ``SYSFS_PATH`` - Use if sysfs is mounted somewhere other than /sys
* ``http_proxy`` - Used on linux to specify an HTTP proxy