Merge from trunk

This commit is contained in:
Sengian 2011-01-06 09:11:30 +01:00
commit 7b09e65c5d
37 changed files with 928 additions and 165 deletions

View File

@ -69,9 +69,12 @@ categories_use_field_for_author_name = 'author'
# avg_rating: the averate rating of all the books referencing this item
# sort: the sort value. For authors, this is the author_sort for that author
# category: the category (e.g., authors, series) that the item is in.
categories_collapsed_name_template = '{first.sort:shorten(4,'',0)} - {last.sort:shorten(4,'',0)}'
categories_collapsed_rating_template = '{first.avg_rating:4.2f:ifempty(0)} - {last.avg_rating:4.2f:ifempty(0)}'
categories_collapsed_popularity_template = '{first.count:d} - {last.count:d}'
# Note that the "r'" in front of the { is necessary if there are backslashes
# (\ characters) in the template. It doesn't hurt anything to leave it there
# even if there aren't any backslashes.
categories_collapsed_name_template = r'{first.sort:shorten(4,'',0)} - {last.sort:shorten(4,'',0)}'
categories_collapsed_rating_template = r'{first.avg_rating:4.2f:ifempty(0)} - {last.avg_rating:4.2f:ifempty(0)}'
categories_collapsed_popularity_template = r'{first.count:d} - {last.count:d}'
# Set whether boolean custom columns are two- or three-valued.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,86 @@
__license__ = 'GPL v3'
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
'''
www.arabianbusiness.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Arabian_Business(BasicNewsRecipe):
title = 'Arabian Business'
__author__ = 'Darko Miletic'
description = 'Comprehensive Guide to Middle East Business & Gulf Industry News including,Banking & Finance,Construction,Energy,Media & Marketing,Real Estate,Transportation,Travel,Technology,Politics,Healthcare,Lifestyle,Jobs & UAE guide.Top Gulf & Dubai Business News.'
publisher = 'Arabian Business Publishing Ltd.'
category = 'ArabianBusiness.com,Arab Business News,Middle East Business News,Middle East Business,Arab Media News,Industry Events,Middle East Industry News,Arab Business Industry,Dubai Business News,Financial News,UAE Business News,Middle East Press Releases,Gulf News,Arab News,GCC Business News,Banking Finance,Media Marketing,Construction,Oil Gas,Retail,Transportation,Travel Hospitality,Photos,Videos,Life Style,Fashion,United Arab Emirates,UAE,Dubai,Sharjah,Abu Dhabi,Qatar,KSA,Saudi Arabia,Bahrain,Kuwait,Oman,Europe,South Asia,America,Asia,news'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'utf8'
use_embedded_content = False
language = 'en'
remove_empty_feeds = True
publication_type = 'newsportal'
masthead_url = 'http://www.arabianbusiness.com/skins/ab.main/gfx/arabianbusiness_logo_sm.gif'
extra_css = """
body{font-family: Georgia,serif }
img{margin-bottom: 0.4em; margin-top: 0.4em; display:block}
.byline,.dateline{font-size: small; display: inline; font-weight: bold}
ul{list-style: none outside none;}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags_before=dict(attrs={'id':'article-title'})
remove_tags = [
dict(name=['meta','link','base','iframe','embed','object'])
,dict(attrs={'class':'printfooter'})
]
remove_attributes=['lang']
feeds = [
(u'Africa' , u'http://www.arabianbusiness.com/world/Africa/?service=rss' )
,(u'Americas' , u'http://www.arabianbusiness.com/world/americas/?service=rss' )
,(u'Asia Pacific' , u'http://www.arabianbusiness.com/world/asia-pacific/?service=rss' )
,(u'Europe' , u'http://www.arabianbusiness.com/world/europe/?service=rss' )
,(u'Middle East' , u'http://www.arabianbusiness.com/world/middle-east/?service=rss' )
,(u'South Asia' , u'http://www.arabianbusiness.com/world/south-asia/?service=rss' )
,(u'Banking & Finance', u'http://www.arabianbusiness.com/industries/banking-finance/?service=rss' )
,(u'Construction' , u'http://www.arabianbusiness.com/industries/construction/?service=rss' )
,(u'Education' , u'http://www.arabianbusiness.com/industries/education/?service=rss' )
,(u'Energy' , u'http://www.arabianbusiness.com/industries/energy/?service=rss' )
,(u'Healthcare' , u'http://www.arabianbusiness.com/industries/healthcare/?service=rss' )
,(u'Media' , u'http://www.arabianbusiness.com/industries/media/?service=rss' )
,(u'Real Estate' , u'http://www.arabianbusiness.com/industries/real-estate/?service=rss' )
,(u'Retail' , u'http://www.arabianbusiness.com/industries/retail/?service=rss' )
,(u'Technology' , u'http://www.arabianbusiness.com/industries/technology/?service=rss' )
,(u'Transport' , u'http://www.arabianbusiness.com/industries/transport/?service=rss' )
,(u'Travel' , u'http://www.arabianbusiness.com/industries/travel-hospitality/?service=rss')
,(u'Equities' , u'http://www.arabianbusiness.com/markets/equities/?service=rss' )
,(u'Commodities' , u'http://www.arabianbusiness.com/markets/commodities/?service=rss' )
,(u'Currencies' , u'http://www.arabianbusiness.com/markets/currencies/?service=rss' )
,(u'Market Data' , u'http://www.arabianbusiness.com/markets/market-data/?service=rss' )
,(u'Comment' , u'http://www.arabianbusiness.com/opinion/comment/?service=rss' )
,(u'Think Tank' , u'http://www.arabianbusiness.com/opinion/think-tank/?service=rss' )
,(u'Arts' , u'http://www.arabianbusiness.com/lifestyle/arts/?service=rss' )
,(u'Cars' , u'http://www.arabianbusiness.com/lifestyle/cars/?service=rss' )
,(u'Food' , u'http://www.arabianbusiness.com/lifestyle/food/?service=rss' )
,(u'Sport' , u'http://www.arabianbusiness.com/lifestyle/sport/?service=rss' )
]
def print_version(self, url):
return url + '?service=printer&page='
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
for alink in soup.findAll('a'):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
return soup

View File

@ -0,0 +1,70 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'Gerardo Diez'
__copyright__ = 'Gerardo Diez<gerardo.diez.garcia@gmail.com>'
description = 'Main daily newspaper from Spain - v1.00 (05, Enero 2011)'
__docformat__ = 'restructuredtext en'
'''
deia.com
'''
from calibre.web.feeds.recipes import BasicNewsRecipe
class Deia(BasicNewsRecipe):
title ='Deia'
__author__ ='Gerardo Diez'
publisher ='Editorial Iparraguirre, S.A'
category ='news, politics, finances, world, spain, euskadi'
publication_type ='newspaper'
oldest_article =1
max_articles_per_feed =100
simultaneous_downloads =10
cover_url ='http://2.bp.blogspot.com/_RjrWzC6tI14/TM6jrPLaBZI/AAAAAAAAFaI/ayffwxidFEY/s1600/2009-10-13-logo-deia.jpg'
timefmt ='[%a, %d %b, %Y]'
encoding ='utf8'
language ='es_ES'
remove_javascript =True
remove_tags_after =dict(id='Texto')
remove_tags_before =dict(id='Texto')
remove_tags =[dict(name='div', attrs={'class':['Herramientas ', 'Multimedia']})]
no_stylesheets =True
extra_css ='h1 {margin-bottom: .15em;font-size: 2.7em; font-family: Georgia, "Times New Roman", Times, serif;} .Antetitulo {margin: 1em 0;text-transform: uppercase;color: #999;} .PieFoto {margin: .1em 0;padding: .5em .5em .5em .5em;background: #F0F0F0;} .PieFoto p {margin-bottom: 0;font-family: Georgia,"Times New Roman",Times,serif;font-weight: bold; font-style: italic; color: #666;}'
keep_only_tags =[dict(name='div', attrs={'class':['Texto ', 'NoticiaFicha ']})]
feeds = [
(u'Bizkaia' ,u'http://www.deia.com/index.php/services/rss?seccion=bizkaia'),
(u'Bilbao' ,u'http://www.deia.com/index.php/services/rss?seccion=bilbao'),
(u'Hemendik eta Handik' ,u'http://www.deia.com/index.php/services/rss?seccion=hemendik-eta-handik'),
(u'Margen Derecha' ,u'http://www.deia.com/index.php/services/rss?seccion=margen-derecha'),
(u'Encartaciones y Margen Izquierda' ,u'http://www.deia.com/index.php/services/rss?seccion=margen-izquierda-encartaciones'),
(u'Costa' ,u'http://www.deia.com/index.php/services/rss?seccion=costa'),
(u'Duranguesado' ,u'http://www.deia.com/index.php/services/rss?seccion=duranguesado'),
(u'Llodio-Nervión' ,u'http://www.deia.com/index.php/services/rss?seccion=llodio-nervion'),
(u'Arratia-Nervión' ,u'http://www.deia.com/index.php/services/rss?seccion=arratia-nervion'),
(u'Uribe-Txorierri' ,u'http://www.deia.com/index.php/services/rss?seccion=uribe-txorierri'),
(u'Ecos de sociedad' ,u'http://www.deia.com/index.php/services/rss?seccion=ecos-de-sociedad'),
(u'Sucesos' ,u'http://www.deia.com/index.php/services/rss?seccion=sucesos'),
(u'Política' ,u'http://www.deia.com/index.php/services/rss?seccion=politica'),
(u'Euskadi' ,u'http://www.deia.com/index.php/services/rss?seccion=politica/euskadi'),
(u'España' ,u'http://www.deia.com/index.php/services/rss?seccion=politica/espana'),
(u'Sociedad',u'http://www.deia.com/index.php/services/rss?seccion=sociedad'),
(u'Euskadi' ,u'http://www.deia.com/index.php/services/rss?seccion=socidad/euskadi'),
(u'Sociedad.España' ,u'http://www.deia.com/index.php/services/rss?seccion=sociedad/espana'),
(u'Ocio y Cultura' ,u'http://www.deia.com/index.php/services/rss?seccion=ocio-y-cultura'),
#(u'Cultura' ,u'http://www.deia.com/index.php/services/rss?seccion=cultura'),
#(u'Ocio' ,u'http://www.deia.com/index.php/services/rss?seccion=ocio'),
(u'On' ,u'http://www.deia.com/index.php/services/rss?seccion=on'),
(u'Agenda' ,u'http://www.deia.com/index.php/services/rss?seccion=agenda'),
(u'Comunicación' ,u'http://www.deia.com/index.php/services/rss?seccion=comunicacion'),
(u'Viajes' ,u'http://www.deia.com/index.php/services/rss?seccion=viajes'),
(u'¡Mundo!' ,u'http://www.deia.com/index.php/services/rss?seccion=que-mundo'),
(u'Humor' ,u'http://www.deia.com/index.php/services/rss?seccion=humor'),
(u'Opinión' ,u'http://www.deia.com/index.php/services/rss?seccion=opinion'),
(u'Editorial' ,u'http://www.deia.com/index.php/services/rss?seccion=editorial'),
(u'Tribuna abierta' ,u'http://www.deia.com/index.php/services/rss?seccion=tribuna-abierta'),
(u'Colaboración' ,u'http://www.deia.com/index.php/services/rss?seccion=colaboracion'),
(u'Columnistas' ,u'http://www.deia.com/index.php/services/rss?seccion=columnistas'),
(u'Deportes' ,u'http://www.deia.com/index.php/services/rss?seccion=deportes'),
(u'Athletic' ,u'http://www.deia.com/index.php/services/rss?seccion=athletic'),
(u'Economía' ,'http://www.deia.com/index.php/services/rss?seccion=economia'),
(u'Mundo' ,u'http://www.deia.com/index.php/services/rss?seccion=mundo')]

View File

@ -0,0 +1,23 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1293122276(BasicNewsRecipe):
title = u'Smarter Planet | Tumblr for eReaders'
__author__ = 'Jack Mason'
author = 'IBM Global Business Services'
publisher = 'IBM'
category = 'news, technology, IT, internet of things, analytics'
oldest_article = 7
max_articles_per_feed = 30
no_stylesheets = True
use_embedded_content = False
masthead_url = 'http://30.media.tumblr.com/tumblr_l70dow9UmU1qzs4rbo1_r3_250.jpg'
remove_tags_before = dict(id='item')
remove_tags_after = dict(id='item')
remove_tags = [dict(attrs={'class':['sidebar', 'about', 'footer', 'description,' 'disqus', 'nav', 'notes', 'disqus_thread']}),
dict(id=['sidebar', 'footer', 'disqus', 'nav', 'notes', 'likes_container', 'description', 'disqus_thread', 'about']),
dict(name=['script', 'noscript', 'style'])]
feeds = [(u'Smarter Planet Tumblr', u'http://smarterplanet.tumblr.com/mobile/rss')]

View File

@ -0,0 +1,115 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.thesundaytimes.co.uk
'''
import urllib
from calibre.web.feeds.news import BasicNewsRecipe
class TimesOnline(BasicNewsRecipe):
title = 'The Sunday Times UK'
__author__ = 'Darko Miletic'
description = 'news from United Kingdom and World'
language = 'en_GB'
publisher = 'Times Newspapers Ltd'
category = 'news, politics, UK'
oldest_article = 3
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
encoding = 'utf-8'
delay = 1
needs_subscription = True
publication_type = 'newspaper'
masthead_url = 'http://www.thesundaytimes.co.uk/sto/public/images/logos/logo-home.gif'
INDEX = 'http://www.thesundaytimes.co.uk'
PREFIX = u'http://www.thesundaytimes.co.uk/sto/'
extra_css = """
.author-name,.authorName{font-style: italic}
.published-date,.multi-position-photo-text{font-family: Arial,Helvetica,sans-serif;
font-size: small; color: gray;
display:block; margin-bottom: 0.5em}
body{font-family: Georgia,"Times New Roman",Times,serif}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
def get_browser(self):
br = BasicNewsRecipe.get_browser()
br.open('http://www.timesplus.co.uk/tto/news/?login=false&url=http://www.thesundaytimes.co.uk/sto/')
if self.username is not None and self.password is not None:
data = urllib.urlencode({ 'userName':self.username
,'password':self.password
,'keepMeLoggedIn':'false'
})
br.open('https://www.timesplus.co.uk/iam/app/authenticate',data)
return br
remove_tags = [
dict(name=['object','link','iframe','base','meta'])
,dict(attrs={'class':'tools comments-parent' })
]
remove_attributes=['lang']
keep_only_tags = [
dict(attrs={'class':'standard-content'})
,dict(attrs={'class':'f-author'})
,dict(attrs={'id':'bodycopy'})
]
remove_tags_after=dict(attrs={'class':'tools_border'})
feeds = [
(u'UK News' , PREFIX + u'news/uk_news/' )
,(u'World' , PREFIX + u'news/world_news/' )
,(u'Politics' , PREFIX + u'news/Politics/' )
,(u'Focus' , PREFIX + u'news/focus/' )
,(u'Insight' , PREFIX + u'news/insight/' )
,(u'Ireland' , PREFIX + u'news/ireland/' )
,(u'Columns' , PREFIX + u'comment/columns/' )
,(u'Arts' , PREFIX + u'culture/arts/' )
,(u'Books' , PREFIX + u'culture/books/' )
,(u'Film and TV' , PREFIX + u'culture/film_and_tv/' )
,(u'Sport' , PREFIX + u'sport/' )
,(u'Business' , PREFIX + u'business' )
,(u'Money' , PREFIX + u'business/money/' )
,(u'Style' , PREFIX + u'style/' )
,(u'Travel' , PREFIX + u'travel/' )
,(u'Clarkson' , PREFIX + u'ingear/clarkson/' )
,(u'Cars' , PREFIX + u'ingear/cars/' )
,(u'Bikes' , PREFIX + u'ingear/2_Wheels/' )
,(u'Tech' , PREFIX + u'ingear/Tech___Games/' )
,(u'Magazine' , PREFIX + u'Magazine/' )
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return self.adeify_images(soup)
def parse_index(self):
totalfeeds = []
lfeeds = self.get_feeds()
for feedobj in lfeeds:
feedtitle, feedurl = feedobj
self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
articles = []
soup = self.index_to_soup(feedurl)
for atag in soup.findAll('a',href=True):
parentName = atag.parent.name
title = self.tag_to_string(atag).strip()
if (parentName == 'h2' or parentName == 'h3') and title is not None and title != '':
url = self.INDEX + atag['href']
articles.append({
'title' :title
,'date' :''
,'url' :url
,'description':''
})
totalfeeds.append((feedtitle, articles))
return totalfeeds

View File

@ -121,7 +121,7 @@ if iswindows:
poppler_lib_dirs = consolidate('POPPLER_LIB_DIR', sw_lib_dir)
popplerqt4_lib_dirs = poppler_lib_dirs
poppler_libs = ['poppler']
magick_inc_dirs = [os.path.join(prefix, 'build', 'ImageMagick-6.5.6')]
magick_inc_dirs = [os.path.join(prefix, 'build', 'ImageMagick-6.6.6')]
magick_lib_dirs = [os.path.join(magick_inc_dirs[0], 'VisualMagick', 'lib')]
magick_libs = ['CORE_RL_wand_', 'CORE_RL_magick_']
podofo_inc = os.path.join(sw_inc_dir, 'podofo')

View File

@ -18,7 +18,7 @@ QT_DLLS = ['Core', 'Gui', 'Network', 'Svg', 'WebKit', 'Xml', 'XmlPatterns']
LIBUSB_DIR = 'C:\\libusb'
LIBUNRAR = 'C:\\Program Files\\UnrarDLL\\unrar.dll'
SW = r'C:\cygwin\home\kovid\sw'
IMAGEMAGICK = os.path.join(SW, 'build', 'ImageMagick-6.5.6',
IMAGEMAGICK = os.path.join(SW, 'build', 'ImageMagick-6.6.6',
'VisualMagick', 'bin')
VERSION = re.sub('[a-z]\d+', '', __version__)

View File

@ -301,12 +301,14 @@ int projectType = MULTITHREADEDDLL;
Run configure.bat in a visual studio command prompt
Run configure.exe generated by configure.bat
Edit magick/magick-config.h
Undefine ProvideDllMain and MAGICKCORE_X11_DELEGATE
Now open VisualMagick/VisualDynamicMT.sln set to Release
Remove the CORE_xlib project
Remove the CORE_xlib and UTIL_Imdisplay project CORE_Magick++
calibre
---------

View File

@ -43,8 +43,8 @@ class Stage3(Command):
description = 'Stage 3 of the publish process'
sub_commands = ['upload_user_manual', 'upload_demo', 'sdist',
'upload_to_mobileread', 'upload_to_google_code',
'tag_release', 'upload_to_server', 'upload_to_sourceforge',
'upload_to_google_code', 'tag_release', 'upload_to_server',
'upload_to_sourceforge', 'upload_to_mobileread',
]
class Stage4(Command):

View File

@ -80,6 +80,100 @@ class Plugin(object): # {{{
'''
pass
def config_widget(self):
'''
Implement this method and :meth:`save_settings` in your plugin to
use a custom configuration dialog, rather then relying on the simple
string based default customization.
This method, if implemented, must return a QWidget. The widget can have
an optional method validate() that takes no arguments and is called
immediately after the user clicks OK. Changes are applied if and only
if the method returns True.
'''
raise NotImplementedError()
def save_settings(self, config_widget):
'''
Save the settings specified by the user with config_widget.
:param config_widget: The widget returned by :meth:`config_widget`.
'''
raise NotImplementedError()
def do_user_config(self, parent=None):
'''
This method shows a configuration dialog for this plugin. It returns
True if the user clicks OK, False otherwise. The changes are
automatically applied.
'''
from PyQt4.Qt import QDialog, QDialogButtonBox, QVBoxLayout, \
QLabel, Qt, QLineEdit
from calibre.gui2 import gprefs
prefname = 'plugin config dialog:'+self.type + ':' + self.name
geom = gprefs.get(prefname, None)
config_dialog = QDialog(parent)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
v = QVBoxLayout(config_dialog)
def size_dialog():
if geom is None:
config_dialog.resize(config_dialog.sizeHint())
else:
config_dialog.restoreGeometry(geom)
button_box.accepted.connect(config_dialog.accept)
button_box.rejected.connect(config_dialog.reject)
config_dialog.setWindowTitle(_('Customize') + ' ' + self.name)
try:
config_widget = self.config_widget()
except NotImplementedError:
config_widget = None
if config_widget is not None:
v.addWidget(config_widget)
v.addWidget(button_box)
size_dialog()
config_dialog.exec_()
if config_dialog.result() == QDialog.Accepted:
if hasattr(config_widget, 'validate'):
if config_widget.validate():
self.save_settings(config_widget)
else:
self.save_settings(config_widget)
else:
from calibre.customize.ui import plugin_customization, \
customize_plugin
help_text = self.customization_help(gui=True)
help_text = QLabel(help_text, config_dialog)
help_text.setWordWrap(True)
help_text.setTextInteractionFlags(Qt.LinksAccessibleByMouse
| Qt.LinksAccessibleByKeyboard)
help_text.setOpenExternalLinks(True)
v.addWidget(help_text)
sc = plugin_customization(self)
if not sc:
sc = ''
sc = sc.strip()
sc = QLineEdit(sc, config_dialog)
v.addWidget(sc)
v.addWidget(button_box)
size_dialog()
config_dialog.exec_()
if config_dialog.result() == QDialog.Accepted:
sc = unicode(sc.text()).strip()
customize_plugin(self, sc)
geom = bytearray(config_dialog.saveGeometry())
gprefs[prefname] = geom
return config_dialog.result()
def load_resources(self, names):
'''
If this plugin comes in a ZIP file (user added plugin), this method

View File

@ -259,7 +259,7 @@ class EEEREADER(USBMS):
PRODUCT_ID = [0x178f]
BCD = [0x0319]
EBOOK_DIR_MAIN = 'Books'
EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'Book'
VENDOR_NAME = 'LINUX'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET'

View File

@ -61,14 +61,26 @@ class PRS505(USBMS):
ALL_BY_TITLE = _('All by title')
ALL_BY_AUTHOR = _('All by author')
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of metadata fields '
EXTRA_CUSTOMIZATION_MESSAGE = [
_('Comma separated list of metadata fields '
'to turn into collections on the device. Possibilities include: ')+\
'series, tags, authors' +\
_('. Two special collections are available: %s:%s and %s:%s. Add '
'these values to the list to enable them. The collections will be '
'given the name provided after the ":" character.')%(
'abt', ALL_BY_TITLE, 'aba', ALL_BY_AUTHOR)
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['series', 'tags'])
'abt', ALL_BY_TITLE, 'aba', ALL_BY_AUTHOR),
_('Upload separate cover thumbnails for books (newer readers)') +
':::'+_('Normally, the SONY readers get the cover image from the'
' ebook file itself. With this option, calibre will send a '
'separate cover image to the reader, useful if you are '
'sending DRMed books in which you cannot change the cover.'
' WARNING: This option should only be used with newer '
'SONY readers: 350, 650, 950 and newer.'),
]
EXTRA_CUSTOMIZATION_DEFAULT = [
', '.join(['series', 'tags']),
False
]
plugboard = None
plugboard_func = None
@ -159,7 +171,7 @@ class PRS505(USBMS):
opts = self.settings()
if opts.extra_customization:
collections = [x.strip() for x in
opts.extra_customization.split(',')]
opts.extra_customization[0].split(',')]
else:
collections = []
debug_print('PRS505: collection fields:', collections)
@ -186,8 +198,12 @@ class PRS505(USBMS):
self.plugboard_func = pb_func
def upload_cover(self, path, filename, metadata, filepath):
return # Disabled as the SONY's don't need this thumbnail anyway and
# older models don't auto delete it
opts = self.settings()
if not opts.extra_customization[1]:
# Building thumbnails disabled
debug_print('PRS505: not uploading covers')
return
debug_print('PRS505: uploading covers')
if metadata.thumbnail and metadata.thumbnail[-1]:
path = path.replace('/', os.sep)
is_main = path.startswith(self._main_prefix)

View File

@ -10,7 +10,21 @@ from calibre.utils.config import Config, ConfigProxy
class DeviceConfig(object):
HELP_MESSAGE = _('Configure Device')
#: Can be None, a string or a list of strings. When it is a string
#: that string is used for the help text and the actual customization value
#: can be read from ``dev.settings().extra_customization``.
#: If it a list of strings, then dev.settings().extra_customization will
#: also be a list. In this case, you *must* ensure that
#: EXTRA_CUSTOMIZATION_DEFAULT is also a list. The list can contain either
#: boolean values or strings, in which case a checkbox or line edit will be
#: used for them in the config widget, automatically.
#: If a string contains ::: then the text after it is interpreted as the
#: tooltip
EXTRA_CUSTOMIZATION_MESSAGE = None
#: The default value for extra customization. If you set
#: EXTRA_CUSTOMIZATION_MESSAGE you *must* set this as well.
EXTRA_CUSTOMIZATION_DEFAULT = None
SUPPORTS_SUB_DIRS = False
@ -73,16 +87,33 @@ class DeviceConfig(object):
if cls.SUPPORTS_USE_AUTHOR_SORT:
proxy['use_author_sort'] = config_widget.use_author_sort()
if cls.EXTRA_CUSTOMIZATION_MESSAGE:
ec = unicode(config_widget.opt_extra_customization.text()).strip()
if not ec:
ec = None
if isinstance(cls.EXTRA_CUSTOMIZATION_MESSAGE, list):
ec = []
for i in range(0, len(cls.EXTRA_CUSTOMIZATION_MESSAGE)):
if hasattr(config_widget.opt_extra_customization[i], 'isChecked'):
ec.append(config_widget.opt_extra_customization[i].isChecked())
else:
ec.append(unicode(config_widget.opt_extra_customization[i].text()).strip())
else:
ec = unicode(config_widget.opt_extra_customization.text()).strip()
if not ec:
ec = None
proxy['extra_customization'] = ec
st = unicode(config_widget.opt_save_template.text())
proxy['save_template'] = st
@classmethod
def settings(cls):
return cls._config().parse()
opts = cls._config().parse()
if isinstance(cls.EXTRA_CUSTOMIZATION_DEFAULT, list):
if opts.extra_customization is None:
opts.extra_customization = []
if not isinstance(opts.extra_customization, list):
opts.extra_customization = [opts.extra_customization]
for i,d in enumerate(cls.EXTRA_CUSTOMIZATION_DEFAULT):
if i >= len(opts.extra_customization):
opts.extra_customization.append(d)
return opts
@classmethod
def save_template(cls):

View File

@ -16,6 +16,7 @@ from calibre.ebooks.metadata.book import TOP_LEVEL_CLASSIFIERS
from calibre.ebooks.metadata.book import ALL_METADATA_FIELDS
from calibre.library.field_metadata import FieldMetadata
from calibre.utils.date import isoformat, format_date
from calibre.utils.icu import sort_key
from calibre.utils.formatter import TemplateFormatter
@ -38,15 +39,16 @@ class SafeFormat(TemplateFormatter):
def get_value(self, key, args, kwargs):
try:
key = key.lower()
if key != 'title_sort':
key = field_metadata.search_term_to_field_key(key.lower())
key = field_metadata.search_term_to_field_key(key)
b = self.book.get_user_metadata(key, False)
if b and b['datatype'] == 'int' and self.book.get(key, 0) == 0:
v = ''
elif b and b['datatype'] == 'float' and self.book.get(key, 0.0) == 0.0:
v = ''
else:
ign, v = self.book.format_field(key.lower(), series_with_index=False)
ign, v = self.book.format_field(key, series_with_index=False)
if v is None:
return ''
if v == '':
@ -489,7 +491,7 @@ class Metadata(object):
return authors_to_string(self.authors)
def format_tags(self):
return u', '.join([unicode(t) for t in self.tags])
return u', '.join([unicode(t) for t in sorted(self.tags, key=sort_key)])
def format_rating(self):
return unicode(self.rating)
@ -529,7 +531,7 @@ class Metadata(object):
orig_res = res
datatype = cmeta['datatype']
if datatype == 'text' and cmeta['is_multiple']:
res = u', '.join(res)
res = u', '.join(sorted(res, key=sort_key))
elif datatype == 'series' and series_with_index:
if self.get_extra(key) is not None:
res = res + \
@ -559,7 +561,7 @@ class Metadata(object):
elif key == 'series_index':
res = self.format_series_index(res)
elif datatype == 'text' and fmeta['is_multiple']:
res = u', '.join(res)
res = u', '.join(sorted(res, key=sort_key))
elif datatype == 'series' and series_with_index:
res = res + ' [%s]'%self.format_series_index()
elif datatype == 'datetime':

View File

@ -55,10 +55,10 @@ class Query(object):
BASE_URL = 'http://isbndb.com/api/books.xml?'
def __init__(self, key, title=None, author=None, publisher=None, isbn=None,
keywords=None, max_results=40):
keywords=None, max_results=30):
assert not(title is None and author is None and publisher is None and \
isbn is None and keywords is None)
assert (max_results < 41)
assert (max_results < 31)
if title == _('Unknown'):
title=None

View File

@ -18,7 +18,6 @@ class xISBN(object):
self._data = []
self._map = {}
self.br = browser()
self.isbn_pat = re.compile(r'[^0-9X]', re.IGNORECASE)
def purify(self, isbn):
@ -26,7 +25,7 @@ class xISBN(object):
def fetch_data(self, isbn):
url = self.QUERY%isbn
data = self.br.open_novisit(url).read()
data = browser().open_novisit(url).read()
data = json.loads(data)
if data.get('stat', None) != 'ok':
return []

View File

@ -103,7 +103,7 @@ class CoverManager(object):
from calibre.ebooks import calibre_cover
img_data = calibre_cover(title, authors_to_string(authors),
series_string=series_string)
id, href = self.oeb.manifest.generate('cover_image',
id, href = self.oeb.manifest.generate('cover',
'cover_image.jpg')
item = self.oeb.manifest.add(id, href, guess_type('t.jpg')[0],
data=img_data)

View File

@ -127,9 +127,6 @@
</property>
</widget>
</item>
<item row="5" column="1" colspan="3">
<widget class="QLineEdit" name="opt_input_encoding"/>
</item>
<item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="opt_remove_paragraph_spacing">
<property name="text">
@ -244,8 +241,22 @@
</property>
</widget>
</item>
<item row="5" column="1" colspan="3">
<widget class="EncodingComboBox" name="opt_input_encoding">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>EncodingComboBox</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../../../resources/images.qrc"/>
<include location="../../../../resources/images.qrc"/>

View File

@ -4,7 +4,10 @@ __license__ = 'GPL 3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import QWidget, QListWidgetItem, Qt, QVariant, SIGNAL
import textwrap
from PyQt4.Qt import QWidget, QListWidgetItem, Qt, QVariant, SIGNAL, \
QLabel, QLineEdit, QCheckBox
from calibre.gui2 import error_dialog
from calibre.gui2.device_drivers.configwidget_ui import Ui_ConfigWidget
@ -46,12 +49,38 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
else:
self.opt_use_author_sort.hide()
if extra_customization_message:
self.extra_customization_label.setText(extra_customization_message)
if settings.extra_customization:
self.opt_extra_customization.setText(settings.extra_customization)
else:
self.extra_customization_label.setVisible(False)
self.opt_extra_customization.setVisible(False)
def parse_msg(m):
msg, _, tt = m.partition(':::') if m else ('', '', '')
return msg.strip(), textwrap.fill(tt.strip(), 100)
if isinstance(extra_customization_message, list):
self.opt_extra_customization = []
for i, m in enumerate(extra_customization_message):
label_text, tt = parse_msg(m)
if isinstance(settings.extra_customization[i], bool):
self.opt_extra_customization.append(QCheckBox(label_text))
self.opt_extra_customization[-1].setToolTip(tt)
self.opt_extra_customization[i].setChecked(bool(settings.extra_customization[i]))
else:
self.opt_extra_customization.append(QLineEdit(self))
l = QLabel(label_text)
l.setToolTip(tt)
l.setBuddy(self.opt_extra_customization[i])
l.setWordWrap(True)
self.opt_extra_customization[i].setText(settings.extra_customization[i])
self.extra_layout.addWidget(l)
self.extra_layout.addWidget(self.opt_extra_customization[i])
else:
self.opt_extra_customization = QLineEdit()
label_text, tt = parse_msg(extra_customization_message)
l = QLabel(label_text)
l.setToolTip(tt)
l.setBuddy(self.opt_extra_customization)
l.setWordWrap(True)
if settings.extra_customization:
self.opt_extra_customization.setText(settings.extra_customization)
self.extra_layout.addWidget(l)
self.extra_layout.addWidget(self.opt_extra_customization)
self.opt_save_template.setText(settings.save_template)

View File

@ -98,20 +98,7 @@
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="extra_customization_label">
<property name="text">
<string>Extra customization</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>opt_extra_customization</cstring>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLineEdit" name="opt_extra_customization"/>
<layout class="QVBoxLayout" name="extra_layout"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label">

View File

@ -321,7 +321,8 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
if (f in ['author_sort'] or
(fm[f]['datatype'] in ['text', 'series', 'enumeration']
and fm[f].get('search_terms', None)
and f not in ['formats', 'ondevice', 'sort'])):
and f not in ['formats', 'ondevice', 'sort']) or
fm[f]['datatype'] in ['int', 'float', 'bool'] ):
self.all_fields.append(f)
self.writable_fields.append(f)
if f in ['sort'] or fm[f]['datatype'] == 'composite':
@ -431,12 +432,14 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
val = mi.get('title_sort', None)
else:
val = mi.get(field, None)
if isinstance(val, (int, float, bool)):
val = str(val)
if val is None:
val = [] if fm['is_multiple'] else ['']
elif not fm['is_multiple']:
val = [val]
elif field == 'authors':
val = [v.replace(',', '|') for v in val]
val = [v.replace('|', ',') for v in val]
else:
val = []
return val
@ -566,17 +569,11 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
dest_val = mi.get(dest, '')
if dest_val is None:
dest_val = []
elif isinstance(dest_val, list):
if dest == 'authors':
dest_val = [v.replace(',', '|') for v in dest_val]
else:
elif not isinstance(dest_val, list):
dest_val = [dest_val]
else:
dest_val = []
if len(val) > 0:
if src == 'authors':
val = [v.replace(',', '|') for v in val]
if dest_mode == 1:
val.extend(dest_val)
elif dest_mode == 2:

View File

@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import re, copy
from PyQt4.QtGui import QDialog, QDialogButtonBox
from PyQt4.Qt import QDialog, QDialogButtonBox, QCompleter, Qt
from calibre.gui2.dialogs.search_ui import Ui_Dialog
from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
@ -22,6 +22,28 @@ class SearchDialog(QDialog, Ui_Dialog):
key=lambda x: sort_key(x if x[0] != '#' else x[1:]))
self.general_combo.addItems(searchables)
all_authors = db.all_authors()
all_authors.sort(key=lambda x : sort_key(x[1]))
for i in all_authors:
id, name = i
name = name.strip().replace('|', ',')
self.authors_box.addItem(name)
self.authors_box.setEditText('')
self.authors_box.completer().setCompletionMode(QCompleter.PopupCompletion)
self.authors_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive)
all_series = db.all_series()
all_series.sort(key=lambda x : sort_key(x[1]))
for i in all_series:
id, name = i
self.series_box.addItem(name)
self.series_box.setEditText('')
self.series_box.completer().setCompletionMode(QCompleter.PopupCompletion)
self.series_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive)
all_tags = db.all_tags()
self.tags_box.update_tags_cache(all_tags)
self.box_last_values = copy.deepcopy(box_values)
if self.box_last_values:
for k,v in self.box_last_values.items():
@ -121,26 +143,34 @@ class SearchDialog(QDialog, Ui_Dialog):
return tok
def box_search_string(self):
mk = self.matchkind.currentIndex()
if mk == CONTAINS_MATCH:
self.mc = ''
elif mk == EQUALS_MATCH:
self.mc = '='
else:
self.mc = '~'
ans = []
self.box_last_values = {}
title = unicode(self.title_box.text()).strip()
self.box_last_values['title_box'] = title
if title:
ans.append('title:"' + title + '"')
ans.append('title:"' + self.mc + title + '"')
author = unicode(self.authors_box.text()).strip()
self.box_last_values['authors_box'] = author
if author:
ans.append('author:"' + author + '"')
ans.append('author:"' + self.mc + author + '"')
series = unicode(self.series_box.text()).strip()
self.box_last_values['series_box'] = series
if series:
ans.append('series:"' + series + '"')
self.mc = '='
ans.append('series:"' + self.mc + series + '"')
tags = unicode(self.tags_box.text())
self.box_last_values['tags_box'] = tags
tags = self.tokens(tags)
tags = [t.strip() for t in tags.split(',') if t.strip()]
if tags:
tags = ['tags:' + t for t in tags]
tags = ['tags:"=' + t + '"' for t in tags]
ans.append('(' + ' or '.join(tags) + ')')
general = unicode(self.general_box.text())
self.box_last_values['general_box'] = general

View File

@ -21,7 +21,7 @@
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>What kind of match to use:</string>
<string>&amp;What kind of match to use:</string>
</property>
<property name="buddy">
<cstring>matchkind</cstring>
@ -228,7 +228,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="title_box">
<widget class="EnLineEdit" name="title_box">
<property name="toolTip">
<string>Enter the title.</string>
</property>
@ -265,21 +265,21 @@
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="authors_box">
<widget class="EnComboBox" name="authors_box">
<property name="toolTip">
<string>Enter an author's name. Only one author can be used.</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="series_box">
<widget class="EnComboBox" name="series_box">
<property name="toolTip">
<string>Enter a series name, without an index. Only one series name can be used.</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="tags_box">
<widget class="TagsLineEdit" name="tags_box">
<property name="toolTip">
<string>Enter tags separated by spaces</string>
</property>
@ -348,6 +348,23 @@
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>EnLineEdit</class>
<extends>QLineEdit</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>EnComboBox</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>TagsLineEdit</class>
<extends>QLineEdit</extends>
<header>widgets.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>all</tabstop>
<tabstop>phrase</tabstop>

View File

@ -57,7 +57,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
(_('Never'), 'never')]
r('toolbar_text', gprefs, choices=choices)
choices = [(_('Disabled'), 'disabled'), (_('By first letter'), 'first letter'),
choices = [(_('Disabled'), 'disable'), (_('By first letter'), 'first letter'),
(_('Partitioned'), 'partition')]
r('tags_browser_partition_method', gprefs, choices=choices)
r('tags_browser_collapse_at', gprefs)

View File

@ -8,13 +8,12 @@ __docformat__ = 'restructuredtext en'
import textwrap, os
from PyQt4.Qt import Qt, QModelIndex, QAbstractItemModel, QVariant, QIcon, \
QBrush, QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QLineEdit
QBrush
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.plugins_ui import Ui_Form
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
disable_plugin, customize_plugin, \
plugin_customization, add_plugin, \
disable_plugin, plugin_customization, add_plugin, \
remove_plugin
from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files
@ -129,6 +128,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.plugin_view.setModel(self._plugin_model)
self.plugin_view.setStyleSheet(
"QTreeView::item { padding-bottom: 10px;}")
self.plugin_view.doubleClicked.connect(self.double_clicked)
self.toggle_plugin_button.clicked.connect(self.toggle_plugin)
self.customize_plugin_button.clicked.connect(self.customize_plugin)
self.remove_plugin_button.clicked.connect(self.remove_plugin)
@ -138,6 +138,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
def toggle_plugin(self, *args):
self.modify_plugin(op='toggle')
def double_clicked(self, index):
if index.parent().isValid():
self.modify_plugin(op='customize')
def customize_plugin(self, *args):
self.modify_plugin(op='customize')
@ -184,49 +188,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
_('Plugin: %s does not need customization')%plugin.name).exec_()
return
self.changed_signal.emit()
config_dialog = QDialog(self)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
v = QVBoxLayout(config_dialog)
button_box.accepted.connect(config_dialog.accept)
button_box.rejected.connect(config_dialog.reject)
config_dialog.setWindowTitle(_('Customize') + ' ' + plugin.name)
if hasattr(plugin, 'config_widget'):
config_widget = plugin.config_widget()
v.addWidget(config_widget)
v.addWidget(button_box)
config_dialog.exec_()
if config_dialog.result() == QDialog.Accepted:
if hasattr(config_widget, 'validate'):
if config_widget.validate():
plugin.save_settings(config_widget)
else:
plugin.save_settings(config_widget)
self._plugin_model.refresh_plugin(plugin)
else:
help_text = plugin.customization_help(gui=True)
help_text = QLabel(help_text, config_dialog)
help_text.setWordWrap(True)
help_text.setTextInteractionFlags(Qt.LinksAccessibleByMouse
| Qt.LinksAccessibleByKeyboard)
help_text.setOpenExternalLinks(True)
v.addWidget(help_text)
sc = plugin_customization(plugin)
if not sc:
sc = ''
sc = sc.strip()
sc = QLineEdit(sc, config_dialog)
v.addWidget(sc)
v.addWidget(button_box)
config_dialog.exec_()
if config_dialog.result() == QDialog.Accepted:
sc = unicode(sc.text()).strip()
customize_plugin(plugin, sc)
if plugin.do_user_config():
self._plugin_model.refresh_plugin(plugin)
elif op == 'remove':
if remove_plugin(plugin):

View File

@ -616,6 +616,31 @@ class ComboBoxWithHelp(QComboBox):
QComboBox.hidePopup(self)
self.set_state()
class EncodingComboBox(QComboBox):
'''
A combobox that holds text encodings support
by Python. This is only populated with the most
common and standard encodings. There is no good
way to programatically list all supported encodings
using encodings.aliases.aliases.keys(). It
will not work.
'''
ENCODINGS = ['', 'cp1252', 'latin1', 'utf-8', '', 'ascii', 'big5', 'cp1250', 'cp1251', 'cp1253',
'cp1254', 'cp1255', 'cp1256', 'euc_jp', 'euc_kr', 'gb2312', 'gb18030',
'hz', 'iso2022_jp', 'iso2022_kr', 'iso8859_5', 'shift_jis',
]
def __init__(self, parent=None):
QComboBox.__init__(self, parent)
self.setEditable(True)
self.setLineEdit(EnLineEdit(self))
for item in self.ENCODINGS:
self.addItem(item)
class PythonHighlighter(QSyntaxHighlighter):
Rules = []

View File

@ -132,6 +132,38 @@ def _match(query, value, matchkind):
pass
return False
class CacheRow(list):
def __init__(self, db, composites, val):
self.db = db
self._composites = composites
list.__init__(self, val)
self._must_do = len(composites) > 0
def __getitem__(self, col):
if self._must_do:
is_comp = False
if isinstance(col, slice):
start = 0 if col.start is None else col.start
step = 1 if col.stop is None else col.stop
for c in range(start, col.stop, step):
if c in self._composites:
is_comp = True
break
elif col in self._composites:
is_comp = True
if is_comp:
id = list.__getitem__(self, 0)
self._must_do = False
mi = self.db.get_metadata(id, index_is_id=True)
for c in self._composites:
self[c] = mi.get(self._composites[c])
return list.__getitem__(self, col)
def __getslice__(self, i, j):
return self.__getitem__(slice(i, j))
class ResultCache(SearchQueryParser): # {{{
'''
@ -139,7 +171,12 @@ class ResultCache(SearchQueryParser): # {{{
'''
def __init__(self, FIELD_MAP, field_metadata):
self.FIELD_MAP = FIELD_MAP
self._map = self._data = self._map_filtered = []
self.composites = {}
for key in field_metadata:
if field_metadata[key]['datatype'] == 'composite':
self.composites[field_metadata[key]['rec_index']] = key
self._data = []
self._map = self._map_filtered = []
self.first_sort = True
self.search_restriction = ''
self.field_metadata = field_metadata
@ -148,10 +185,6 @@ class ResultCache(SearchQueryParser): # {{{
self.build_date_relop_dict()
self.build_numeric_relop_dict()
self.composites = []
for key in field_metadata:
if field_metadata[key]['datatype'] == 'composite':
self.composites.append((key, field_metadata[key]['rec_index']))
def __getitem__(self, row):
return self._data[self._map_filtered[row]]
@ -583,13 +616,10 @@ class ResultCache(SearchQueryParser): # {{{
'''
for id in ids:
try:
self._data[id] = db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0]
self._data[id] = CacheRow(db, self.composites,
db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0])
self._data[id].append(db.book_on_device_string(id))
self._data[id].append(None)
if len(self.composites) > 0:
mi = db.get_metadata(id, index_is_id=True)
for k,c in self.composites:
self._data[id][c] = mi.get(k, None)
except IndexError:
return None
try:
@ -603,13 +633,10 @@ class ResultCache(SearchQueryParser): # {{{
return
self._data.extend(repeat(None, max(ids)-len(self._data)+2))
for id in ids:
self._data[id] = db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0]
self._data[id] = CacheRow(db, self.composites,
db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0])
self._data[id].append(db.book_on_device_string(id))
self._data[id].append(None)
if len(self.composites) > 0:
mi = db.get_metadata(id, index_is_id=True)
for k,c in self.composites:
self._data[id][c] = mi.get(k)
self._map[0:0] = ids
self._map_filtered[0:0] = ids
@ -630,16 +657,11 @@ class ResultCache(SearchQueryParser): # {{{
temp = db.conn.get('SELECT * FROM meta2')
self._data = list(itertools.repeat(None, temp[-1][0]+2)) if temp else []
for r in temp:
self._data[r[0]] = r
self._data[r[0]] = CacheRow(db, self.composites, r)
for item in self._data:
if item is not None:
item.append(db.book_on_device_string(item[0]))
item.append(None)
if len(self.composites) > 0:
mi = db.get_metadata(item[0], index_is_id=True)
for k,c in self.composites:
item[c] = mi.get(k)
self._map = [i[0] for i in self._data if i is not None]
if field is not None:
self.sort(field, ascending)
@ -669,13 +691,7 @@ class ResultCache(SearchQueryParser): # {{{
fields = [('timestamp', False)]
keyg = SortKeyGenerator(fields, self.field_metadata, self._data)
# For efficiency, the key generator returns a plain value if only one
# field is in the sort field list. Because the normal cmp function will
# always assume asc, we must deal with asc/desc here.
if len(fields) == 1:
self._map.sort(key=keyg, reverse=not fields[0][1])
else:
self._map.sort(key=keyg)
self._map.sort(key=keyg)
tmap = list(itertools.repeat(False, len(self._data)))
for x in self._map_filtered:
@ -708,8 +724,6 @@ class SortKeyGenerator(object):
def __call__(self, record):
values = tuple(self.itervals(self.data[record]))
if len(values) == 1:
return values[0]
return SortKey(self.orders, values)
def itervals(self, record):
@ -732,6 +746,11 @@ class SortKeyGenerator(object):
val = (self.string_sort_key(val), sidx)
elif dt in ('text', 'comments', 'composite', 'enumeration'):
if val:
sep = fm['is_multiple']
if sep:
val = sep.join(sorted(val.split(sep),
key=self.string_sort_key))
val = self.string_sort_key(val)
elif dt == 'bool':

View File

@ -15,7 +15,7 @@ from calibre.customize import CatalogPlugin
from calibre.customize.conversion import OptionRecommendation, DummyReporter
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
from calibre.ebooks.chardet import substitute_entites
from calibre.ebooks.oeb.base import RECOVER_PARSER, XHTML_NS
from calibre.ebooks.oeb.base import XHTML_NS
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.config import config_dir
from calibre.utils.date import format_date, isoformat, now as nowf

View File

@ -133,7 +133,15 @@ class CustomColumns(object):
def adapt_bool(x, d):
if isinstance(x, (str, unicode, bytes)):
x = bool(int(x))
x = x.lower()
if x == 'true':
x = True
elif x == 'false':
x = False
elif x == 'none':
x = None
else:
x = bool(int(x))
return x
def adapt_enum(x, d):
@ -142,9 +150,17 @@ class CustomColumns(object):
v = None
return v
def adapt_number(x, d):
if isinstance(x, (str, unicode, bytes)):
if x.lower() == 'none':
return None
if d['datatype'] == 'int':
return int(x)
return float(x)
self.custom_data_adapters = {
'float': lambda x,d : x if x is None else float(x),
'int': lambda x,d : x if x is None else int(x),
'float': adapt_number,
'int': adapt_number,
'rating':lambda x,d : x if x is None else min(10., max(0., float(x))),
'bool': adapt_bool,
'comments': lambda x,d: adapt_text(x, {'is_multiple':False}),

View File

@ -131,6 +131,8 @@ class SafeFormat(TemplateFormatter):
return self.composite_values[key]
if key in kwargs:
val = kwargs[key]
if isinstance(val, list):
val = ','.join(val)
return val.replace('/', '_').replace('\\', '_')
return ''
except:

View File

@ -101,7 +101,19 @@ def html_to_lxml(raw):
root = html.fragment_fromstring(raw)
root.set('xmlns', "http://www.w3.org/1999/xhtml")
raw = etree.tostring(root, encoding=None)
return etree.fromstring(raw)
try:
return etree.fromstring(raw)
except:
for x in root.iterdescendants():
remove = []
for attr in x.attrib:
if ':' in attr:
remove.append(attr)
for a in remove:
del x.attrib[a]
raw = etree.tostring(root, encoding=None)
return etree.fromstring(raw)
def CATALOG_ENTRY(item, item_kind, base_href, version, updated,
ignore_count=False, add_kind=False):

View File

@ -150,7 +150,7 @@ The example shows several things:
* program mode is used if the expression begins with ``:'`` and ends with ``'``. Anything else is assumed to be single-function.
* the variable ``$`` stands for the field the expression is operating upon, ``#series`` in this case.
* functions must be given all their arguments. There is no default value. This is true for the standard builtin functions, and is a significant difference from single-function mode.
* functions must be given all their arguments. There is no default value. For example, the standard builtin functions must be given an additional initial parameter indicating the source field, which is a significant difference from single-function mode.
* white space is ignored and can be used anywhere within the expression.
* constant strings are enclosed in matching quotes, either ``'`` or ``"``.
@ -204,7 +204,7 @@ For various values of series_index, the program returns:
All the functions listed under single-function mode can be used in program mode, noting that unlike the functions described below you must supply a first parameter providing the value the function is to act upon.
The following functions are available in addition to those described in single-function mode. With the exception of the ``id`` parameter of assign, all parameters can be statements (sequences of expressions):
The following functions are available in addition to those described in single-function mode. Remember from the example above that the single-function mode functions require an additional first parameter specifying the field to operate on. With the exception of the ``id`` parameter of assign, all parameters can be statements (sequences of expressions):
* ``add(x, y)`` -- returns x + y. Throws an exception if either x or y are not numbers.
* ``assign(id, val)`` -- assigns val to id, then returns val. id must be an identifier, not an expression

View File

@ -431,7 +431,10 @@ class TemplateFormatter(string.Formatter):
return prefix + val + suffix
def vformat(self, fmt, args, kwargs):
ans = string.Formatter.vformat(self, fmt, args, kwargs)
if fmt.startswith('program:'):
ans = self._eval_program(None, fmt[8:])
else:
ans = string.Formatter.vformat(self, fmt, args, kwargs)
return self.compress_spaces.sub(' ', ans).strip()
########## a formatter guaranteed not to throw and exception ############
@ -441,10 +444,7 @@ class TemplateFormatter(string.Formatter):
self.book = book
self.composite_values = {}
try:
if fmt.startswith('program:'):
ans = self._eval_program(None, fmt[8:])
else:
ans = self.vformat(fmt, [], kwargs).strip()
ans = self.vformat(fmt, [], kwargs).strip()
except Exception, e:
if DEBUG:
traceback.print_exc()
@ -468,6 +468,7 @@ class EvalFormatter(TemplateFormatter):
A template formatter that uses a simple dict instead of an mi instance
'''
def get_value(self, key, args, kwargs):
key = key.lower()
return kwargs.get(key, _('No such variable ') + key)
eval_formatter = EvalFormatter()

View File

@ -106,13 +106,16 @@ class Image(_magick.Image): # {{{
return ans
def load(self, data):
return _magick.Image.load(self, bytes(data))
data = bytes(data)
if not data:
raise ValueError('Cannot open image from empty data string')
return _magick.Image.load(self, data)
def open(self, path_or_file):
if not hasattr(path_or_file, 'read') and \
path_or_file.lower().endswith('.wmf'):
# Special handling for WMF files as ImageMagick seems
# to hand while reading them from a blob on linux
# to hang while reading them from a blob on linux
if isinstance(path_or_file, unicode):
path_or_file = path_or_file.encode(filesystem_encoding)
return _magick.Image.read(self, path_or_file)
@ -121,6 +124,8 @@ class Image(_magick.Image): # {{{
data = data.read()
else:
data = open(data, 'rb').read()
if not data:
raise ValueError('%r is an empty file'%path_or_file)
self.load(data)
@dynamic_property

View File

@ -0,0 +1,9 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

200
src/calibre/utils/wmf/wmf.c Normal file
View File

@ -0,0 +1,200 @@
#define UNICODE
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <libwmf/api.h>
#include <libwmf/svg.h>
typedef struct {
char *data;
size_t len;
size_t pos;
} buf;
//This code is taken mostly from the Abiword wmf plugin
// returns unsigned char cast to int, or EOF
static int wmf_WMF_read(void * context) {
char c;
buf *info = (buf*)context;
if (info->pos == info->len)
return EOF;
c = info->data[pos];
info->pos++;
return (int)c;
}
// returns (-1) on error, else 0
static int wmf_WMF_seek(void * context, long pos) {
buf* info = (buf*) context;
if (pos < 0 || (size_t)pos > info->len) return -1;
info->pos = (size_t)pos;
return 0;
}
// returns (-1) on error, else pos
static long wmf_WMF_tell(void * context) {
buf* info = (buf*) context;
return (long) info->pos;
}
#define CLEANUP if(API) { if (stream) wmf_free(API, stream); wmf_api_destroy(API); };
static PyObject *
wmf_render(PyObject *self, PyObject *args) {
char *data;
Py_ssize_t sz;
PyObject *ans;
unsigned int disp_width = 0;
unsigned int disp_height = 0;
float wmf_width;
float wmf_height;
float ratio_wmf;
float ratio_bounds;
unsigned long flags;
unsigned int max_width = 1600;
unsigned int max_height = 1200;
unsigned long max_flags = 0;
static const char* Default_Description = "wmf2svg";
wmf_error_t err;
wmf_svg_t* ddata = 0;
wmfAPI* API = 0;
wmfD_Rect bbox;
wmfAPI_Options api_options;
buf read_info;
char *stream = NULL;
unsigned long stream_len = 0;
if (!PyArg_ParseTuple(args, "s#", &data, &sz))
return NULL;
flags = WMF_OPT_IGNORE_NONFATAL | WMF_OPT_FUNCTION;
api_options.function = wmf_svg_function;
err = wmf_api_create(&API, flags, &api_options);
if (err != wmf_E_None) {
CLEANUP;
return PyErr_NoMemory();
}
read_info.data = data;
read_info.len = sz;
read_info.pos = 0;
err = wmf_bbuf_input(API, wmf_WMF_read, wmf_WMF_seek, wmf_WMF_tell, (void *) &read_info);
if (err != wmf_E_None) {
CLEANUP;
PyErr_SetString(PyExc_Exception, "Failed to initialize WMF input");
return NULL;
}
err = wmf_scan(API, 0, &(bbox));
if (err != wmf_E_None)
{
CLEANUP;
PyErr_SetString(PyExc_ValueError, "Failed to scan the WMF");
return NULL;
}
/* Okay, got this far, everything seems cool.
*/
ddata = WMF_SVG_GetData (API);
ddata->out = wmf_stream_create(API, NULL);
ddata->Description = (char *)Default_Description;
ddata->bbox = bbox;
wmf_display_size(API, &disp_width, &disp_height, 96, 96);
wmf_width = (float) disp_width;
wmf_height = (float) disp_height;
if ((wmf_width <= 0) || (wmf_height <= 0)) {
CLEANUP;
PyErr_SetString(PyExc_ValueError, "Bad WMF image size");
return NULL;
}
if ((wmf_width > (float) max_width )
|| (wmf_height > (float) max_height)) {
ratio_wmf = wmf_height / wmf_width;
ratio_bounds = (float) max_height / (float) max_width;
if (ratio_wmf > ratio_bounds) {
ddata->height = max_height;
ddata->width = (unsigned int) ((float) ddata->height / ratio_wmf);
}
else {
ddata->width = max_width;
ddata->height = (unsigned int) ((float) ddata->width * ratio_wmf);
}
}
else {
ddata->width = (unsigned int) ceil ((double) wmf_width );
ddata->height = (unsigned int) ceil ((double) wmf_height);
}
ddata->flags |= WMF_SVG_INLINE_IMAGES;
ddata->flags |= WMF_GD_OUTPUT_MEMORY | WMF_GD_OWN_BUFFER;
err = wmf_play(API, 0, &(bbox));
if (err != wmf_E_None) {
CLEANUP;
PyErr_SetString(PyExc_ValueError, "Playing of the WMF file failed");
return NULL;
}
wmf_stream_destroy(API, ddata->out, &stream, &stream_len);
ans = Py_BuildValue("s#", stream, stream_len);
wmf_free(API, stream);
wmf_api_destroy (API);
return ans;
}
static PyMethodDef wmf_methods[] = {
{"render", wmf_render, METH_VARARGS,
"render(path) -> Render wmf as svg."
},
{NULL} /* Sentinel */
};
PyMODINIT_FUNC
initwmf(void)
{
PyObject* m;
m = Py_InitModule3("wmf", wmf_methods,
"Wrapper for the libwmf library");
}