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} {0}' - '/browse/category/{1}
  • ') - .format(xml(x, True), xml(quote(y)), xml(_('Browse books by')), + cats = [('
  • {0}' + '{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'
    %s
    ') % (xml(c[0]), c[1]) for c in comments] comments = u'
    %s
    '%('\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