diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 4ae0278133..32aeba9122 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -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. diff --git a/resources/images/news/arabian_business.png b/resources/images/news/arabian_business.png new file mode 100644 index 0000000000..e949830988 Binary files /dev/null and b/resources/images/news/arabian_business.png differ diff --git a/resources/recipes/arabian_business.recipe b/resources/recipes/arabian_business.recipe new file mode 100644 index 0000000000..8b41c99e68 --- /dev/null +++ b/resources/recipes/arabian_business.recipe @@ -0,0 +1,86 @@ +__license__ = 'GPL v3' +__copyright__ = '2011, Darko Miletic ' +''' +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 diff --git a/resources/recipes/deia.recipe b/resources/recipes/deia.recipe new file mode 100644 index 0000000000..980d59d3d1 --- /dev/null +++ b/resources/recipes/deia.recipe @@ -0,0 +1,70 @@ +#!/usr/bin/env python +__license__ = 'GPL v3' +__author__ = 'Gerardo Diez' +__copyright__ = 'Gerardo Diez' +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')] + diff --git a/resources/recipes/ibm_smarter_planet.recipe b/resources/recipes/ibm_smarter_planet.recipe new file mode 100644 index 0000000000..2e5c46fb80 --- /dev/null +++ b/resources/recipes/ibm_smarter_planet.recipe @@ -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')] + diff --git a/resources/recipes/sunday_times.recipe b/resources/recipes/sunday_times.recipe new file mode 100644 index 0000000000..1f20f73cd9 --- /dev/null +++ b/resources/recipes/sunday_times.recipe @@ -0,0 +1,115 @@ + +__license__ = 'GPL v3' +__copyright__ = '2010, Darko Miletic ' +''' +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 diff --git a/setup/build_environment.py b/setup/build_environment.py index d6581a907d..10ab1b0735 100644 --- a/setup/build_environment.py +++ b/setup/build_environment.py @@ -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') diff --git a/setup/installer/windows/freeze.py b/setup/installer/windows/freeze.py index 7d8ea4d80a..e9e47816fd 100644 --- a/setup/installer/windows/freeze.py +++ b/setup/installer/windows/freeze.py @@ -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__) diff --git a/setup/installer/windows/notes.rst b/setup/installer/windows/notes.rst index b9aef39657..5dfd956ce2 100644 --- a/setup/installer/windows/notes.rst +++ b/setup/installer/windows/notes.rst @@ -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 --------- diff --git a/setup/publish.py b/setup/publish.py index fb873a90d9..ba8a4992a7 100644 --- a/setup/publish.py +++ b/setup/publish.py @@ -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): diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 770d405203..13e1f20a2d 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -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 diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py index 2a0fdf6433..ecd12ac61d 100644 --- a/src/calibre/devices/misc.py +++ b/src/calibre/devices/misc.py @@ -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' diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index 6652d581d4..98a7241a36 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -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) diff --git a/src/calibre/devices/usbms/deviceconfig.py b/src/calibre/devices/usbms/deviceconfig.py index e074387175..940ea96f38 100644 --- a/src/calibre/devices/usbms/deviceconfig.py +++ b/src/calibre/devices/usbms/deviceconfig.py @@ -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): diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index e3fb8092e6..17f2c6705c 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -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': diff --git a/src/calibre/ebooks/metadata/isbndb.py b/src/calibre/ebooks/metadata/isbndb.py index 787d70eb51..a6457d35d4 100644 --- a/src/calibre/ebooks/metadata/isbndb.py +++ b/src/calibre/ebooks/metadata/isbndb.py @@ -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 diff --git a/src/calibre/ebooks/metadata/xisbn.py b/src/calibre/ebooks/metadata/xisbn.py index 21ea0ee79f..2ee74396c7 100644 --- a/src/calibre/ebooks/metadata/xisbn.py +++ b/src/calibre/ebooks/metadata/xisbn.py @@ -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 [] diff --git a/src/calibre/ebooks/oeb/transforms/cover.py b/src/calibre/ebooks/oeb/transforms/cover.py index a3cf9e9f83..0d82e9ad73 100644 --- a/src/calibre/ebooks/oeb/transforms/cover.py +++ b/src/calibre/ebooks/oeb/transforms/cover.py @@ -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) diff --git a/src/calibre/gui2/convert/look_and_feel.ui b/src/calibre/gui2/convert/look_and_feel.ui index 367233e2c0..0edc324dc5 100644 --- a/src/calibre/gui2/convert/look_and_feel.ui +++ b/src/calibre/gui2/convert/look_and_feel.ui @@ -127,9 +127,6 @@ - - - @@ -244,8 +241,22 @@ + + + + true + + + + + + EncodingComboBox + QComboBox +
widgets.h
+
+
diff --git a/src/calibre/gui2/device_drivers/configwidget.py b/src/calibre/gui2/device_drivers/configwidget.py index 64321e1a46..7b440db7fc 100644 --- a/src/calibre/gui2/device_drivers/configwidget.py +++ b/src/calibre/gui2/device_drivers/configwidget.py @@ -4,7 +4,10 @@ __license__ = 'GPL 3' __copyright__ = '2009, John Schember ' __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) diff --git a/src/calibre/gui2/device_drivers/configwidget.ui b/src/calibre/gui2/device_drivers/configwidget.ui index 1e8ee75852..f4902a7387 100644 --- a/src/calibre/gui2/device_drivers/configwidget.ui +++ b/src/calibre/gui2/device_drivers/configwidget.ui @@ -98,20 +98,7 @@ - - - Extra customization - - - true - - - opt_extra_customization - - - - - + diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 9dbc3dee5e..e1ee4327f3 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -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: diff --git a/src/calibre/gui2/dialogs/search.py b/src/calibre/gui2/dialogs/search.py index 8e8fd09652..62a0f8a9f1 100644 --- a/src/calibre/gui2/dialogs/search.py +++ b/src/calibre/gui2/dialogs/search.py @@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal ' 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 diff --git a/src/calibre/gui2/dialogs/search.ui b/src/calibre/gui2/dialogs/search.ui index 7bb4c15363..6848a45506 100644 --- a/src/calibre/gui2/dialogs/search.ui +++ b/src/calibre/gui2/dialogs/search.ui @@ -21,7 +21,7 @@ - What kind of match to use: + &What kind of match to use: matchkind @@ -228,7 +228,7 @@ - + Enter the title. @@ -265,21 +265,21 @@ - + Enter an author's name. Only one author can be used. - + Enter a series name, without an index. Only one series name can be used. - + Enter tags separated by spaces @@ -348,6 +348,23 @@ + + + EnLineEdit + QLineEdit +
widgets.h
+
+ + EnComboBox + QComboBox +
widgets.h
+
+ + TagsLineEdit + QLineEdit +
widgets.h
+
+
all phrase diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 263d19325d..37ed90cc61 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -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) diff --git a/src/calibre/gui2/preferences/plugins.py b/src/calibre/gui2/preferences/plugins.py index b18159cce5..2fe2b3bf01 100644 --- a/src/calibre/gui2/preferences/plugins.py +++ b/src/calibre/gui2/preferences/plugins.py @@ -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): diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index bc3c23876f..d87bb45f7a 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -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 = [] diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index a32c45191f..980c9f1fa9 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -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': diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index ae61d7cf52..0a5d5284e2 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -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 diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 07ea407460..ba218c3ecc 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -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}), diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index 3179551b45..20c0a7e59c 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -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: diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py index fd8c50c594..ab0853add9 100644 --- a/src/calibre/library/server/opds.py +++ b/src/calibre/library/server/opds.py @@ -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): diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index 8a3bd854b1..1bf08c11f9 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -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 diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 4fe8ad2e4f..f4e687b419 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -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() diff --git a/src/calibre/utils/magick/__init__.py b/src/calibre/utils/magick/__init__.py index bf0f48db7d..834a798de5 100644 --- a/src/calibre/utils/magick/__init__.py +++ b/src/calibre/utils/magick/__init__.py @@ -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 diff --git a/src/calibre/utils/wmf/__init__.py b/src/calibre/utils/wmf/__init__.py new file mode 100644 index 0000000000..68dfb8d2b5 --- /dev/null +++ b/src/calibre/utils/wmf/__init__.py @@ -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 ' +__docformat__ = 'restructuredtext en' + + + diff --git a/src/calibre/utils/wmf/wmf.c b/src/calibre/utils/wmf/wmf.c new file mode 100644 index 0000000000..1f8e8a27f3 --- /dev/null +++ b/src/calibre/utils/wmf/wmf.c @@ -0,0 +1,200 @@ +#define UNICODE +#define PY_SSIZE_T_CLEAN +#include + +#include +#include + +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"); + + +} +