Merge from trunk

This commit is contained in:
Charles Haley 2010-10-17 05:36:52 +01:00
commit 2c3b34b779
9 changed files with 381 additions and 80 deletions

View File

@ -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;
}

View File

@ -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);
});
}

View File

@ -8,6 +8,7 @@
<span class="rating_container">{stars}</span>
<span class="series">{series}</span>
<a href="#" onclick="show_details(this); return false;" title="{details_tt}">{details}</a>
<a href="/browse/book/{id}" title="{permalink_tt}">{permalink}</a>
</div>
<div class="title"><strong>{title}</strong></div>
<div class="authors">{authors}</div>

View File

@ -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

View File

@ -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 <kovid@kovidgoyal.net>'
__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)

View File

@ -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:

View File

@ -2,7 +2,7 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>'
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', ''):
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 <navmap> 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)

View File

@ -6,13 +6,13 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__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(['<div class="category-container">'] + items + ['</div>'])
@ -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,7 +223,7 @@ 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
@ -268,23 +273,31 @@ class BrowseServer(object):
# Catalogs {{{
def browse_icon(self, name='blank.png'):
cherrypy.response.headers['Content-Type'] = 'image/png'
cherrypy.response.headers['Last-Modified'] = self.last_modified(self.build_time)
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)
img.size = (48, 48)
cherrypy.response.headers['Content-Type'] = 'image/png'
cherrypy.response.headers['Last-Modified'] = self.last_modified(self.build_time)
width, height = img.size
scaled, width, height = fit_image(width, height, 48, 48)
if scaled:
img.size = (width, height)
return img.export('png')
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):
@ -313,9 +326,10 @@ class BrowseServer(object):
icon = 'blank.png'
cats.append((meta['name'], category, icon))
cats = [('<li title="{2} {0}"><img src="{src}" alt="{0}" /> {0}'
'<span>/browse/category/{1}</span></li>')
.format(xml(x, True), xml(quote(y)), xml(_('Browse books by')),
cats = [('<li title="{2} {0}"><img src="{src}" alt="{0}" />'
'<span class="label">{0}</span>'
'<span class="url">/browse/category/{1}</span></li>')
.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'<div class="comment">%s</div></div>') % (xml(c[0]),
c[1]) for c in comments]
comments = u'<div class="comments">%s</div>'%('\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)
# }}}

View File

@ -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