diff --git a/Changelog.yaml b/Changelog.yaml
index 12f1b570bc..bcf58ae03d 100644
--- a/Changelog.yaml
+++ b/Changelog.yaml
@@ -4,6 +4,126 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
+- version: 0.7.34
+ date: 2010-12-17
+
+ new features:
+ - title: "Page turn animations in the e-book viewer"
+ type: major
+ description: >
+ "Now when you use the Page Down/Page Up keys or the next/previous page buttons in the viewer, page turning will be animated. The duration of the animation can be controlled in the viewer preferences. Setting it to 0 disables the animation completely."
+
+ - title: "Conversion pipeline: Add an option to set the minimum line height of all elemnts as a percentage of the computed font size. By default, calibre now sets the line height to 120% of the computed font size."
+
+ - title: "Large speedup in startup times and post metadata edit wait for large libraries"
+
+ - title: "Allow changing the font used in the calibre interface via Preferences->Look and feel"
+
+ - title: "Allow editing of the title sort value for a book via the edit metadata dialog"
+
+ - title: "Disable the cover cache. This means that if you are running calibre on an underpowered machine, you might notice some slow down in the cover browser. On the other hand, calibre's memory consumption is reduced."
+
+ - title: "You can now restart calibre in debug mode by clicking the arrow next to the Preferences button. In debug mode, after you quit calibre, a diagnostic log will popup"
+ tickets: [7359]
+
+ - title: "When creating a new calibre library add an option to copy the custom column, saved searches, etc from the current library."
+ tickets: [7643]
+
+ - title: "Add more tweaks to control how the next available series number is calculated."
+ tickets: [7892]
+
+ - title: "Add a tweak to control layout of the custom metadata tab in the edit metadata dialog"
+
+ - title: "Apple driver: Set series number as track number on windows when sending books to iTunes"
+
+ - title: "Drivers for PocketBook 701 and Samsung E65"
+
+ - title: "E-book viewer: Add option to have the mouse wheel flip pages"
+
+ - title: "Add a load_resources method to the InterfaceAction and Plugin classes to facilitate loading of resources from plugin ZIP files"
+
+ - title: "E-book viewer: Add option to not remember position in book when quitting."
+ tickets: [7699]
+
+ - title: "When sorting the book list, keep the current book visible after the sort completes."
+ tickets: [7504]
+
+ - title: "EPUB Output: Add an option to flatten the EPUB file structure, specially for FBReaderJ."
+ tickets: [7788]
+
+ - title: "EPUB Output: Ensure all files inside the generated EPUB have unique filenames, to support broken EPUB readers like Stanza, Aldiko, FBReader and Sigil"
+
+ - title: "FB2 Output: Add support for some 2.1 style tags."
+
+ - title: "Bulk metadata edit: Add options to delete cover/generate default cover."
+ tickets: [7885]
+
+ - title: "Fix a regression in 0.7.33 that broke updating covers in ebook files when saving to disk."
+ tickets: [7886]
+
+ - title: "Don't refresh the Tag browser if it is hidden. Speeds up metadata editing with large libraries, if you hide teh Tag Browser."
+
+ - title: "MOBI Output: Add option to ignore margins in input document"
+ tickets: [7877]
+
+ - title: "Kobo driver: Add support for 1.8.x firmware"
+
+ bug fixes:
+ - title: "Fix various memory leaks introduced in the last couple of releases"
+
+ - title: "EPUB metadata: When rendering first page as the cover, handle embedded svg correctly."
+ tickets: [7909]
+
+ - title: "Disable multiple library support when the CALIBRE_OVERRIDE_DATABASE_PATH env var is set"
+
+ - title: "Content server: Fix bug that could cause saved search based restrictions to not exclude all books"
+ tickets: [7876]
+
+ - title: "Topaz metadata: Read metadata correctly from Topaz files that have MOBI file extensions"
+
+ - title: "MOBI Input: Handle the (rare) MOBI files that do not specify per paragraph text indents correctly."
+ tickets: [7869]
+
+ - title: "MOBI metadata reader: Handle invalid PRC files with spurious image_offset headers"
+
+ - title: "Fix drag/drop of new cover to book detail panel does not update cover browser"
+ tickets: [7890]
+
+ - title: "Do not open the book details dialog when double click on the scrollbars in the book details panel"
+ tickets: [7826]
+
+ - title: "Templates: Fix {tags} not working when no tags are present"
+ tickets: [7888]
+
+ - title: "HTML metadata: Fix regression that broke parsing of some meta tags"
+ tickets: [7851]
+
+ - title: "Preferences: Add tooltips to buddy labels as well."
+ tickets: [7873]
+
+ - title: "Content server: Fix handling of root URL when using --url-prefix"
+
+ - title: "Ensure that the default encoding used by python is never ASCII (needed when running a non frozen version of calibre on linux)"
+
+ improved recipes:
+ - Astronomy Picture of the day
+ - New Scientist
+ - Radikal
+ - Times of India
+ - Economic Times
+ - Zeit Online
+ - Dilbert
+
+ new recipes:
+ - title: "Various Japanes news sources, National Geographic and paper.li"
+ author: "Hiroshi Miura"
+
+ - title: "Science based medicine"
+ author: "BuzzKill"
+
+ - title: "Kompiutierra"
+ author: "Vadim Dyadkin"
+
- version: 0.7.33
date: 2010-12-10
diff --git a/imgsrc/edit_copy.svg b/imgsrc/edit-copy.svg
similarity index 100%
rename from imgsrc/edit_copy.svg
rename to imgsrc/edit-copy.svg
diff --git a/imgsrc/edit-cut.svg b/imgsrc/edit-cut.svg
new file mode 100644
index 0000000000..f078b52e04
--- /dev/null
+++ b/imgsrc/edit-cut.svg
@@ -0,0 +1,831 @@
+
+
+
diff --git a/imgsrc/edit-paste.svg b/imgsrc/edit-paste.svg
new file mode 100644
index 0000000000..d22a8bb7dd
--- /dev/null
+++ b/imgsrc/edit-paste.svg
@@ -0,0 +1,3302 @@
+
+
+
\ No newline at end of file
diff --git a/imgsrc/swap.svg b/imgsrc/swap.svg
deleted file mode 100644
index aa62316b34..0000000000
--- a/imgsrc/swap.svg
+++ /dev/null
@@ -1,722 +0,0 @@
-
-
-
diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py
index 081112444a..a420cd7d44 100644
--- a/resources/default_tweaks.py
+++ b/resources/default_tweaks.py
@@ -12,13 +12,24 @@ defaults.
# The algorithm used to assign a new book in an existing series a series number.
+# New series numbers assigned using this tweak are always integer values, except
+# if a constant non-integer is specified.
# Possible values are:
-# next - Next available number
+# next - First available integer larger than the largest existing number
+# first_free - First available integer larger than 0
+# next_free - First available integer larger than the smallest existing number
+# last_free - First available integer smaller than the largest existing number
+# Return largest existing + 1 if no free number is found
# const - Assign the number 1 always
+# a number - Assign that number always. The number is not in quotes. Note that
+# 0.0 can be used here.
+# Examples:
+# series_index_auto_increment = 'next'
+# series_index_auto_increment = 'next_free'
+# series_index_auto_increment = 16.5
series_index_auto_increment = 'next'
-
# The algorithm used to copy author to author_sort
# Possible values are:
# invert: use "fn ln" -> "ln, fn" (the original algorithm)
@@ -30,6 +41,20 @@ series_index_auto_increment = 'next'
# selecting 'manage authors', and pressing 'Recalculate all author sort values'.
author_sort_copy_method = 'invert'
+# Set which author field to display in the tags pane (the list of authors,
+# series, publishers etc on the left hand side). The choices are author and
+# author_sort. This tweak affects only what is displayed under the authors
+# category in the tags pane and content server. Please note that if you set this
+# to author_sort, it is very possible to see duplicate names in the list because
+# although it is guaranteed that author names are unique, there is no such
+# guarantee for author_sort values. Showing duplicates won't break anything, but
+# it could lead to some confusion. When using 'author_sort', the tooltip will
+# show the author's name.
+# Examples:
+# categories_use_field_for_author_name = 'author'
+# categories_use_field_for_author_name = 'author_sort'
+categories_use_field_for_author_name = 'author'
+
# Set whether boolean custom columns are two- or three-valued.
# Two-values for true booleans
@@ -235,3 +260,9 @@ doubleclick_on_library_view = 'open_viewer'
# Example: locale_for_sorting = 'fr' -- sort using French rules.
# Example: locale_for_sorting = 'nb' -- sort using Norwegian rules.
locale_for_sorting = ''
+
+
+# Set whether to use one or two columns for custom metadata when editing
+# metadata one book at a time. If True, then the fields are laid out using two
+# columns. If False, one column is used.
+metadata_single_use_2_cols_for_custom_fields = True
\ No newline at end of file
diff --git a/resources/images/edit_copy.png b/resources/images/edit-copy.png
similarity index 100%
rename from resources/images/edit_copy.png
rename to resources/images/edit-copy.png
diff --git a/resources/images/edit-cut.png b/resources/images/edit-cut.png
new file mode 100644
index 0000000000..b995283754
Binary files /dev/null and b/resources/images/edit-cut.png differ
diff --git a/resources/images/edit-paste.png b/resources/images/edit-paste.png
new file mode 100644
index 0000000000..b790efec25
Binary files /dev/null and b/resources/images/edit-paste.png differ
diff --git a/resources/images/edit-redo.png b/resources/images/edit-redo.png
new file mode 100644
index 0000000000..8de333fe8c
Binary files /dev/null and b/resources/images/edit-redo.png differ
diff --git a/resources/images/edit-select-all.png b/resources/images/edit-select-all.png
new file mode 100644
index 0000000000..4393bc9dc7
Binary files /dev/null and b/resources/images/edit-select-all.png differ
diff --git a/resources/images/edit-undo.png b/resources/images/edit-undo.png
new file mode 100644
index 0000000000..f6d7e8ba56
Binary files /dev/null and b/resources/images/edit-undo.png differ
diff --git a/resources/images/format-fill-color.png b/resources/images/format-fill-color.png
new file mode 100644
index 0000000000..946bead5da
Binary files /dev/null and b/resources/images/format-fill-color.png differ
diff --git a/resources/images/format-indent-less.png b/resources/images/format-indent-less.png
new file mode 100644
index 0000000000..8662c34871
Binary files /dev/null and b/resources/images/format-indent-less.png differ
diff --git a/resources/images/format-indent-more.png b/resources/images/format-indent-more.png
new file mode 100644
index 0000000000..e1244ef47c
Binary files /dev/null and b/resources/images/format-indent-more.png differ
diff --git a/resources/images/format-justify-center.png b/resources/images/format-justify-center.png
new file mode 100644
index 0000000000..505160a1bb
Binary files /dev/null and b/resources/images/format-justify-center.png differ
diff --git a/resources/images/format-justify-fill.png b/resources/images/format-justify-fill.png
new file mode 100644
index 0000000000..ee34b8272f
Binary files /dev/null and b/resources/images/format-justify-fill.png differ
diff --git a/resources/images/format-justify-left.png b/resources/images/format-justify-left.png
new file mode 100644
index 0000000000..f5af823a82
Binary files /dev/null and b/resources/images/format-justify-left.png differ
diff --git a/resources/images/format-justify-right.png b/resources/images/format-justify-right.png
new file mode 100644
index 0000000000..9a3d8d6ee1
Binary files /dev/null and b/resources/images/format-justify-right.png differ
diff --git a/resources/images/format-list-ordered.png b/resources/images/format-list-ordered.png
new file mode 100644
index 0000000000..c7da85da3f
Binary files /dev/null and b/resources/images/format-list-ordered.png differ
diff --git a/resources/images/format-list-unordered.png b/resources/images/format-list-unordered.png
new file mode 100644
index 0000000000..c959989958
Binary files /dev/null and b/resources/images/format-list-unordered.png differ
diff --git a/resources/images/format-text-color.png b/resources/images/format-text-color.png
new file mode 100644
index 0000000000..2ec27d559d
Binary files /dev/null and b/resources/images/format-text-color.png differ
diff --git a/resources/images/format-text-heading.png b/resources/images/format-text-heading.png
new file mode 100644
index 0000000000..970acb7d60
Binary files /dev/null and b/resources/images/format-text-heading.png differ
diff --git a/resources/images/format-text-subscript.png b/resources/images/format-text-subscript.png
new file mode 100644
index 0000000000..7da8882aea
Binary files /dev/null and b/resources/images/format-text-subscript.png differ
diff --git a/resources/images/format-text-superscript.png b/resources/images/format-text-superscript.png
new file mode 100644
index 0000000000..9dc31ab783
Binary files /dev/null and b/resources/images/format-text-superscript.png differ
diff --git a/resources/images/insert-link.png b/resources/images/insert-link.png
new file mode 100644
index 0000000000..b9e335d320
Binary files /dev/null and b/resources/images/insert-link.png differ
diff --git a/resources/images/swap.png b/resources/images/swap.png
index e5aeb60e22..7f8d40ca1d 100644
Binary files a/resources/images/swap.png and b/resources/images/swap.png differ
diff --git a/resources/recipes/apod.recipe b/resources/recipes/apod.recipe
index 01f4ebf391..7bb3161954 100644
--- a/resources/recipes/apod.recipe
+++ b/resources/recipes/apod.recipe
@@ -11,6 +11,7 @@ class APOD(BasicNewsRecipe):
remove_javascript = True
recursions = 0
oldest_article = 14
+ remove_attributes = ['onmouseover', 'onmouseout']
feeds = [
(u'Astronomy Picture of the Day', u'http://apod.nasa.gov/apod.rss')
diff --git a/resources/recipes/bwmagazine.recipe b/resources/recipes/bwmagazine.recipe
index 26dbc459d3..9a1f10a680 100644
--- a/resources/recipes/bwmagazine.recipe
+++ b/resources/recipes/bwmagazine.recipe
@@ -1,64 +1,102 @@
-
__license__ = 'GPL v3'
-__copyright__ = '2009, Darko Miletic '
+__copyright__ = '2008 Kovid Goyal kovid@kovidgoyal.net, 2010 Darko Miletic '
'''
-http://www.businessweek.com/magazine/news/articles/business_news.htm
+www.businessweek.com
'''
-from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe
-class BWmagazine(BasicNewsRecipe):
- title = 'BusinessWeek Magazine'
- __author__ = 'Darko Miletic'
- description = 'Stay up to date with BusinessWeek magazine articles. Read news on international business, personal finances & the economy in the BusinessWeek online magazine.'
+class BusinessWeek(BasicNewsRecipe):
+ title = 'Business Week'
+ __author__ = 'Kovid Goyal and Darko Miletic'
+ description = 'Read the latest international business news & stock market news. Get updated company profiles, financial advice, global economy and technology news.'
publisher = 'Bloomberg L.P.'
- category = 'news, International Business News, current news in international business,international business articles, personal business, business week magazine, business week magazine articles, business week magazine online, business week online magazine'
- oldest_article = 10
- max_articles_per_feed = 100
+ category = 'Business, business news, stock market, stock market news, financial advice, company profiles, financial advice, global economy, technology news'
+ oldest_article = 7
+ max_articles_per_feed = 200
no_stylesheets = True
- encoding = 'utf-8'
+ encoding = 'utf8'
use_embedded_content = False
language = 'en'
- INDEX = 'http://www.businessweek.com/magazine/news/articles/business_news.htm'
+ remove_empty_feeds = True
+ publication_type = 'magazine'
cover_url = 'http://images.businessweek.com/mz/covers/current_120x160.jpg'
-
+ masthead_url = 'http://assets.businessweek.com/images/bw-logo.png'
+ extra_css = """
+ body{font-family: Helvetica,Arial,sans-serif }
+ img{margin-bottom: 0.4em; display:block}
+ .tagline{color: gray; font-style: italic}
+ .photoCredit{font-size: small; color: gray}
+ """
conversion_options = {
- 'comment' : description
- , 'tags' : category
- , 'publisher' : publisher
- , 'language' : language
+ 'comment' : description
+ , 'tags' : category
+ , 'publisher' : publisher
+ , 'language' : language
}
+ remove_tags = [
+ dict(attrs={'class':'inStory'})
+ ,dict(name=['meta','link','iframe','base','embed','object','table','th','tr','td'])
+ ,dict(attrs={'id':['inset','videoDisplay']})
+ ]
+ keep_only_tags = [dict(name='div', attrs={'id':['story-body','storyBody']})]
+ remove_attributes = ['lang']
+ match_regexps = [r'http://www.businessweek.com/.*_page_[1-9].*']
- def parse_index(self):
- articles = []
- soup = self.index_to_soup(self.INDEX)
- ditem = soup.find('div',attrs={'id':'column2'})
- if ditem:
- for item in ditem.findAll('h3'):
- title_prefix = ''
- description = ''
- feed_link = item.find('a')
- if feed_link and feed_link.has_key('href'):
- url = 'http://www.businessweek.com/magazine/' + feed_link['href'].partition('../../')[2]
- title = title_prefix + self.tag_to_string(feed_link)
- date = strftime(self.timefmt)
- articles.append({
- 'title' :title
- ,'date' :date
- ,'url' :url
- ,'description':description
- })
- return [(soup.head.title.string, articles)]
- keep_only_tags = dict(name='div', attrs={'id':'storyBody'})
+ feeds = [
+ (u'Top Stories', u'http://www.businessweek.com/topStories/rss/topStories.rss'),
+ (u'Top News' , u'http://www.businessweek.com/rss/bwdaily.rss' ),
+ (u'Asia', u'http://www.businessweek.com/rss/asia.rss'),
+ (u'Autos', u'http://www.businessweek.com/rss/autos/index.rss'),
+ (u'Classic Cars', u'http://rss.businessweek.com/bw_rss/classiccars'),
+ (u'Hybrids', u'http://rss.businessweek.com/bw_rss/hybrids'),
+ (u'Europe', u'http://www.businessweek.com/rss/europe.rss'),
+ (u'Auto Reviews', u'http://rss.businessweek.com/bw_rss/autoreviews'),
+ (u'Innovation & Design', u'http://www.businessweek.com/rss/innovate.rss'),
+ (u'Architecture', u'http://www.businessweek.com/rss/architecture.rss'),
+ (u'Brand Equity', u'http://www.businessweek.com/rss/brandequity.rss'),
+ (u'Auto Design', u'http://www.businessweek.com/rss/carbuff.rss'),
+ (u'Game Room', u'http://rss.businessweek.com/bw_rss/gameroom'),
+ (u'Technology', u'http://www.businessweek.com/rss/technology.rss'),
+ (u'Investing', u'http://rss.businessweek.com/bw_rss/investor'),
+ (u'Small Business', u'http://www.businessweek.com/rss/smallbiz.rss'),
+ (u'Careers', u'http://rss.businessweek.com/bw_rss/careers'),
+ (u'B-Schools', u'http://www.businessweek.com/rss/bschools.rss'),
+ (u'Magazine Selections', u'http://www.businessweek.com/rss/magazine.rss'),
+ (u'CEO Guide to Tech', u'http://www.businessweek.com/rss/ceo_guide_tech.rss'),
+ ]
+
+ def get_article_url(self, article):
+ url = article.get('guid', None)
+ if 'podcasts' in url:
+ return None
+ if 'surveys' in url:
+ return None
+ if 'images' in url:
+ return None
+ if 'feedroom' in url:
+ return None
+ if '/magazine/toc/' in url:
+ return None
+ rurl, sep, rest = url.rpartition('?')
+ if rurl:
+ return rurl
+ return rest
def print_version(self, url):
- rurl = url.rpartition('?')[0]
- if rurl == '':
- rurl = url
- return rurl.replace('.com/magazine/','.com/print/magazine/')
-
+ if '/news/' in url or '/blog/ in url':
+ return url
+ rurl = url.replace('http://www.businessweek.com/','http://www.businessweek.com/print/')
+ return rurl.replace('/investing/','/investor/')
+ 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/cnd.recipe b/resources/recipes/cnd.recipe
new file mode 100644
index 0000000000..0e8206d07a
--- /dev/null
+++ b/resources/recipes/cnd.recipe
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+
+__license__ = 'GPL v3'
+__copyright__ = '2010, Derek Liang '
+'''
+cnd.org
+'''
+import re
+
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class TheCND(BasicNewsRecipe):
+
+ title = 'CND'
+ __author__ = 'Derek Liang'
+ description = ''
+ INDEX = 'http://cnd.org'
+ language = 'zh'
+ conversion_options = {'linearize_tables':True}
+
+ remove_tags_before = dict(name='div', id='articleHead')
+ remove_tags_after = dict(id='copyright')
+ remove_tags = [dict(name='table', attrs={'align':'right'}), dict(name='img', attrs={'src':'http://my.cnd.org/images/logo.gif'}), dict(name='hr', attrs={}), dict(name='small', attrs={})]
+ no_stylesheets = True
+
+ preprocess_regexps = [(re.compile(r'', re.DOTALL), lambda m: '')]
+
+ def print_version(self, url):
+ if url.find('news/article.php') >= 0:
+ return re.sub("^[^=]*", "http://my.cnd.org/modules/news/print.php?storyid", url)
+ else:
+ return re.sub("^[^=]*", "http://my.cnd.org/modules/wfsection/print.php?articleid", url)
+
+ def parse_index(self):
+ soup = self.index_to_soup(self.INDEX)
+
+ feeds = []
+ articles = {}
+
+ for a in soup.findAll('a', attrs={'target':'_cnd'}):
+ url = a['href']
+ if url.find('article.php') < 0 :
+ continue
+ if url.startswith('/'):
+ url = 'http://cnd.org'+url
+ title = self.tag_to_string(a)
+ self.log('\tFound article: ', title, 'at', url)
+ date = a.nextSibling
+ if (date is not None) and len(date)>2:
+ if not articles.has_key(date):
+ articles[date] = []
+ articles[date].append({'title':title, 'url':url, 'description': '', 'date':''})
+ self.log('\t\tAppend to : ', date)
+
+ self.log('log articles', articles)
+ mostCurrent = sorted(articles).pop()
+ self.title = 'CND ' + mostCurrent
+
+ feeds.append((self.title, articles[mostCurrent]))
+
+ return feeds
+
+ def populate_article_metadata(self, article, soup, first):
+ header = soup.find('h3')
+ self.log('header: ' + self.tag_to_string(header))
+ pass
+
diff --git a/resources/recipes/ecotrend.recipe b/resources/recipes/ecotrend.recipe
new file mode 100644
index 0000000000..679f190e96
--- /dev/null
+++ b/resources/recipes/ecotrend.recipe
@@ -0,0 +1,42 @@
+__license__ = 'GPL v3'
+__copyright__ = '2010, Darko Miletic '
+'''
+globaleconomicanalysis.blogspot.com
+'''
+
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class GlobalEconomicAnalysis(BasicNewsRecipe):
+ title = "Mish's Global Economic Trend Analysis"
+ __author__ = 'Darko Miletic'
+ description = 'Thoughts on the global economy, housing, gold, silver, interest rates, oil, energy, China, commodities, the dollar, Euro, Renminbi, Yen, inflation, deflation, stagflation, precious metals, emerging markets, and policy decisions that affect the global markets.'
+ publisher = 'Mike Shedlock'
+ category = 'news, politics, economy, banking'
+ oldest_article = 7
+ max_articles_per_feed = 200
+ no_stylesheets = True
+ encoding = 'utf8'
+ use_embedded_content = True
+ language = 'en'
+ remove_empty_feeds = True
+ publication_type = 'blog'
+ masthead_url = 'http://www.pagina12.com.ar/commons/imgs/logo-home.gif'
+ extra_css = """
+ body{font-family: Arial,Helvetica,sans-serif }
+ img{margin-bottom: 0.4em; display:block}
+ """
+
+ conversion_options = {
+ 'comment' : description
+ , 'tags' : category
+ , 'publisher' : publisher
+ , 'language' : language
+ }
+
+ remove_tags = [
+ dict(name=['meta','link','iframe','object','embed'])
+ ,dict(attrs={'class':'blogger-post-footer'})
+ ]
+ remove_attributes=['border']
+
+ feeds = [(u'Articles', u'http://feeds2.feedburner.com/MishsGlobalEconomicTrendAnalysis')]
diff --git a/resources/recipes/gva_be.recipe b/resources/recipes/gva_be.recipe
index 34c4122394..f42bd23417 100644
--- a/resources/recipes/gva_be.recipe
+++ b/resources/recipes/gva_be.recipe
@@ -40,13 +40,12 @@ class GazetvanAntwerpen(BasicNewsRecipe):
remove_tags_after = dict(name='span', attrs={'class':'author'})
feeds = [
- (u'Overzicht & Blikvanger', u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/overview/overzicht' )
+ (u'Binnenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/binnenland' )
+ ,(u'Buitenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/buitenland' )
,(u'Stad & Regio' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/stadenregio' )
,(u'Economie' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/economie' )
- ,(u'Binnenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/binnenland' )
- ,(u'Buitenland' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/buitenland' )
,(u'Media & Cultur' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/mediaencultuur')
- ,(u'Wetenschap' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/mediaencultuur')
+ ,(u'Wetenschap' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/wetenschap' )
,(u'Sport' , u'http://www.gva.be/syndicationservices/artfeedservice.svc/rss/mostrecent/sport' )
]
diff --git a/resources/recipes/johm.recipe b/resources/recipes/johm.recipe
index ee162b27c2..0f5625b806 100644
--- a/resources/recipes/johm.recipe
+++ b/resources/recipes/johm.recipe
@@ -1,88 +1,72 @@
-# -*- coding: utf-8 -*-
-
+import re
from calibre.web.feeds.recipes import BasicNewsRecipe
class JournalofHospitalMedicine(BasicNewsRecipe):
title = 'Journal of Hospital Medicine'
- __author__ = 'Krittika Goyal'
+ __author__ = 'Kovid Goyal'
description = 'Medical news'
timefmt = ' [%d %b, %Y]'
needs_subscription = True
language = 'en'
no_stylesheets = True
- #remove_tags_before = dict(name='div', attrs={'align':'center'})
- #remove_tags_after = dict(name='ol', attrs={'compact':'COMPACT'})
- remove_tags = [
- dict(name='iframe'),
- dict(name='div', attrs={'class':'subContent'}),
- dict(name='div', attrs={'id':['contentFrame']}),
- #dict(name='form', attrs={'onsubmit':"return verifySearch(this.w,'Keyword, citation, or author')"}),
- #dict(name='table', attrs={'align':'RIGHT'}),
- ]
-
+ keep_only_tags = [dict(id=['articleTitle', 'articleMeta', 'fulltext'])]
+ remove_tags = [dict(attrs={'class':'licensedContent'})]
# TO LOGIN
def get_browser(self):
br = BasicNewsRecipe.get_browser()
br.open('http://www3.interscience.wiley.com/cgi-bin/home')
- br.select_form(name='siteLogin')
- br['LoginName'] = self.username
- br['Password'] = self.password
+ br.select_form(nr=0)
+ br['j_username'] = self.username
+ br['j_password'] = self.password
response = br.submit()
raw = response.read()
- if 'userName = ""' in raw:
+ if '
LOGGED IN
' not in raw:
raise Exception('Login failed. Check your username and password')
return br
#TO GET ARTICLE TOC
def johm_get_index(self):
- return self.index_to_soup('http://www3.interscience.wiley.com/journal/111081937/home')
+ return self.index_to_soup('http://onlinelibrary.wiley.com/journal/10.1002/(ISSN)1553-5606/currentissue')
# To parse artice toc
def parse_index(self):
- parse_soup = self.johm_get_index()
+ soup = self.johm_get_index()
+ toc = soup.find(id='issueTocGroups')
+ feeds = []
+ for group in toc.findAll('li', id=re.compile(r'group\d+')):
+ gtitle = group.find(attrs={'class':'subSectionHeading'})
+ if gtitle is None:
+ continue
+ gtitle = self.tag_to_string(gtitle)
+ arts = group.find(attrs={'class':'articles'})
+ if arts is None:
+ continue
+ self.log('Found section:', gtitle)
+ articles = []
+ for art in arts.findAll(attrs={'class':lambda x: x and 'tocArticle'
+ in x}):
+ a = art.find('a', href=True)
+ if a is None:
+ continue
+ url = a.get('href')
+ if url.startswith('/'):
+ url = 'http://onlinelibrary.wiley.com' + url
+ url = url.replace('/abstract', '/full')
+ title = self.tag_to_string(a)
+ a.extract()
+ pm = art.find(attrs={'class':'productMenu'})
+ if pm is not None:
+ pm.extract()
+ desc = self.tag_to_string(art)
+ self.log('\tFound article:', title, 'at', url)
+ articles.append({'title':title, 'url':url, 'description':desc,
+ 'date':''})
+ if articles:
+ feeds.append((gtitle, articles))
- div = parse_soup.find(id='contentCell')
-
- current_section = None
- current_articles = []
- feeds = []
- for x in div.findAll(True):
- if x.name == 'h4':
- # Section heading found
- if current_articles and current_section:
- feeds.append((current_section, current_articles))
- current_section = self.tag_to_string(x)
- current_articles = []
- self.log('\tFound section:', current_section)
- if current_section is not None and x.name == 'strong':
- title = self.tag_to_string(x)
- p = x.parent.parent.find('a', href=lambda x: x and '/HTMLSTART' in x)
- if p is None:
- continue
- url = p.get('href', False)
- if not url or not title:
- continue
- if url.startswith('/'):
- url = 'http://www3.interscience.wiley.com'+url
- url = url.replace('/HTMLSTART', '/main.html,ftx_abs')
- self.log('\t\tFound article:', title)
- self.log('\t\t\t', url)
- #if url.startswith('/'):
- #url = 'http://online.wsj.com'+url
- current_articles.append({'title': title, 'url':url,
- 'description':'', 'date':''})
-
- if current_articles and current_section:
- feeds.append((current_section, current_articles))
-
- return feeds
-
- def preprocess_html(self, soup):
- for img in soup.findAll('img', src=True):
- img['src'] = img['src'].replace('tfig', 'nfig')
- return soup
+ return feeds
diff --git a/resources/recipes/lanacion.recipe b/resources/recipes/lanacion.recipe
index 19f6c1c897..050cb2e79c 100644
--- a/resources/recipes/lanacion.recipe
+++ b/resources/recipes/lanacion.recipe
@@ -78,4 +78,6 @@ class Lanacion(BasicNewsRecipe):
]
def preprocess_html(self, soup):
+ for item in soup.findAll(style=True):
+ del item['style']
return self.adeify_images(soup)
diff --git a/resources/recipes/le_monde.recipe b/resources/recipes/le_monde.recipe
index 18be6ca711..c14b8eeeff 100644
--- a/resources/recipes/le_monde.recipe
+++ b/resources/recipes/le_monde.recipe
@@ -4,7 +4,7 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
class LeMonde(BasicNewsRecipe):
title = 'Le Monde'
__author__ = 'veezh'
- description = 'Actualités'
+ description = u'Actualit\xe9s'
oldest_article = 1
max_articles_per_feed = 100
no_stylesheets = True
diff --git a/resources/recipes/nejm.recipe b/resources/recipes/nejm.recipe
index c860413926..bc12fbcedf 100644
--- a/resources/recipes/nejm.recipe
+++ b/resources/recipes/nejm.recipe
@@ -4,23 +4,14 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
class NYTimes(BasicNewsRecipe):
title = 'New England Journal of Medicine'
- __author__ = 'Krittika Goyal'
+ __author__ = 'Kovid Goyal'
description = 'Medical news'
timefmt = ' [%d %b, %Y]'
needs_subscription = True
language = 'en'
no_stylesheets = True
- remove_tags_before = dict(name='div', attrs={'align':'center'})
- remove_tags_after = dict(name='ol', attrs={'compact':'COMPACT'})
- remove_tags = [
- dict(name='iframe'),
- #dict(name='div', attrs={'class':'related-articles'}),
- dict(name='div', attrs={'id':['sidebar']}),
- #dict(name='form', attrs={'onsubmit':"return verifySearch(this.w,'Keyword, citation, or author')"}),
- dict(name='table', attrs={'align':'RIGHT'}),
- ]
-
+ keep_only_tags = dict(id='content')
#TO LOGIN
@@ -38,61 +29,50 @@ class NYTimes(BasicNewsRecipe):
#TO GET ARTICLE TOC
def nejm_get_index(self):
- return self.index_to_soup('http://content.nejm.org/current.dtl')
+ return self.index_to_soup('http://content.nejm.org/current.dtl')
# To parse artice toc
def parse_index(self):
- parse_soup = self.nejm_get_index()
+ parse_soup = self.nejm_get_index()
- div = parse_soup.find(id='centerTOC')
+ feeds = []
- current_section = None
- current_articles = []
- feeds = []
- for x in div.findAll(True):
- if x.name == 'img' and '/toc/' in x.get('src', '') and 'uarrow.gif' not in x.get('src', ''):
- # Section heading found
- if current_articles and current_section and 'Week in the' not in current_section:
- feeds.append((current_section, current_articles))
- current_section = x.get('alt')
- current_articles = []
- self.log('\tFound section:', current_section)
- if current_section is not None and x.name == 'strong':
- title = self.tag_to_string(x)
- a = x.parent.find('a', href=lambda x: x and '/full/' in x)
- if a is None:
- continue
- url = a.get('href', False)
- if not url or not title:
- continue
- if url.startswith('/'):
- url = 'http://content.nejm.org'+url
- self.log('\t\tFound article:', title)
- self.log('\t\t\t', url)
- if url.startswith('/'):
- url = 'http://online.wsj.com'+url
- current_articles.append({'title': title, 'url':url,
- 'description':'', 'date':''})
-
- if current_articles and current_section:
- feeds.append((current_section, current_articles))
-
- return feeds
-
- def preprocess_html(self, soup):
- for a in soup.findAll(text=lambda x: x and '[in this window]' in x):
- a = a.findParent('a')
- url = a.get('href', None)
- if not url:
+ div = parse_soup.find(attrs={'class':'tocContent'})
+ for group in div.findAll(attrs={'class':'articleGrouping'}):
+ feed_title = group.find(attrs={'class':'articleType'})
+ if feed_title is None:
continue
- if url.startswith('/'):
- url = 'http://content.nejm.org'+url
- isoup = self.index_to_soup(url)
- img = isoup.find('img', src=lambda x: x and
- x.startswith('/content/'))
- if img is not None:
- img.extract()
- table = a.findParent('table')
- table.replaceWith(img)
- return soup
+ feed_title = self.tag_to_string(feed_title)
+ articles = []
+ self.log('Found section:', feed_title)
+ for art in group.findAll(attrs={'class':lambda x: x and 'articleEntry'
+ in x}):
+ link = art.find(attrs={'class':lambda x:x and 'articleLink' in
+ x})
+ if link is None:
+ continue
+ a = link.find('a', href=True)
+ if a is None:
+ continue
+ url = a.get('href')
+ if url.startswith('/'):
+ url = 'http://www.nejm.org'+url
+ title = self.tag_to_string(a)
+ self.log.info('\tFound article:', title, 'at', url)
+ article = {'title':title, 'url':url, 'date':''}
+ au = art.find(attrs={'class':'articleAuthors'})
+ if au is not None:
+ article['author'] = self.tag_to_string(au)
+ desc = art.find(attrs={'class':'hover_text'})
+ if desc is not None:
+ desc = self.tag_to_string(desc)
+ if 'author' in article:
+ desc = ' by ' + article['author'] + ' ' +desc
+ article['description'] = desc
+ articles.append(article)
+ if articles:
+ feeds.append((feed_title, articles))
+
+ return feeds
+
diff --git a/resources/recipes/new_scientist.recipe b/resources/recipes/new_scientist.recipe
index 02bbbe4d42..434c41f525 100644
--- a/resources/recipes/new_scientist.recipe
+++ b/resources/recipes/new_scientist.recipe
@@ -5,6 +5,7 @@ newscientist.com
'''
import re
+import urllib
from calibre.web.feeds.news import BasicNewsRecipe
class NewScientist(BasicNewsRecipe):
@@ -24,7 +25,7 @@ class NewScientist(BasicNewsRecipe):
needs_subscription = 'optional'
extra_css = """
body{font-family: Arial,sans-serif}
- img{margin-bottom: 0.8em}
+ img{margin-bottom: 0.8em; display: block}
.quotebx{font-size: x-large; font-weight: bold; margin-right: 2em; margin-left: 2em}
"""
@@ -41,12 +42,14 @@ class NewScientist(BasicNewsRecipe):
def get_browser(self):
br = BasicNewsRecipe.get_browser()
br.open('http://www.newscientist.com/')
- if self.username is not None and self.password is not None:
- br.open('https://www.newscientist.com/user/login?redirectURL=')
- br.select_form(nr=2)
- br['loginId' ] = self.username
- br['password'] = self.password
- br.submit()
+ if self.username is not None and self.password is not None:
+ br.open('https://www.newscientist.com/user/login')
+ data = urllib.urlencode({ 'source':'form'
+ ,'redirectURL':''
+ ,'loginId':self.username
+ ,'password':self.password
+ })
+ br.open('https://www.newscientist.com/user/login',data)
return br
remove_tags = [
@@ -55,21 +58,22 @@ class NewScientist(BasicNewsRecipe):
,dict(name='p' , attrs={'class':['marker','infotext' ]})
,dict(name='meta' , attrs={'name' :'description' })
,dict(name='a' , attrs={'rel' :'tag' })
+ ,dict(name='ul' , attrs={'class':'markerlist' })
,dict(name=['link','base','meta','iframe','object','embed'])
]
remove_tags_after = dict(attrs={'class':['nbpcopy','comments']})
- remove_attributes = ['height','width','lang']
+ remove_attributes = ['height','width','lang','onclick']
feeds = [
- (u'Latest Headlines' , u'http://feeds.newscientist.com/science-news' )
- ,(u'Magazine' , u'http://www.newscientist.com/feed/magazine' )
- ,(u'Health' , u'http://www.newscientist.com/feed/view?id=2&type=channel' )
- ,(u'Life' , u'http://www.newscientist.com/feed/view?id=3&type=channel' )
- ,(u'Space' , u'http://www.newscientist.com/feed/view?id=6&type=channel' )
- ,(u'Physics and Mathematics' , u'http://www.newscientist.com/feed/view?id=4&type=channel' )
- ,(u'Environment' , u'http://www.newscientist.com/feed/view?id=1&type=channel' )
- ,(u'Science in Society' , u'http://www.newscientist.com/feed/view?id=5&type=channel' )
- ,(u'Tech' , u'http://www.newscientist.com/feed/view?id=7&type=channel' )
+ (u'Latest Headlines' , u'http://feeds.newscientist.com/science-news' )
+ ,(u'Magazine' , u'http://feeds.newscientist.com/magazine' )
+ ,(u'Health' , u'http://feeds.newscientist.com/health' )
+ ,(u'Life' , u'http://feeds.newscientist.com/life' )
+ ,(u'Space' , u'http://feeds.newscientist.com/space' )
+ ,(u'Physics and Mathematics' , u'http://feeds.newscientist.com/physics-math' )
+ ,(u'Environment' , u'http://feeds.newscientist.com/environment' )
+ ,(u'Science in Society' , u'http://feeds.newscientist.com/science-in-society' )
+ ,(u'Tech' , u'http://feeds.newscientist.com/tech' )
]
def get_article_url(self, article):
@@ -79,11 +83,21 @@ class NewScientist(BasicNewsRecipe):
return url + '?full=true&print=true'
def preprocess_html(self, soup):
+ if soup.html.has_key('id'):
+ del soup.html['id']
+ for item in soup.findAll(style=True):
+ del item['style']
for item in soup.findAll(['quote','quotetext']):
item.name='p'
+ for item in soup.findAll(['xref','figref']):
+ tstr = item.string
+ item.replaceWith(tstr)
for tg in soup.findAll('a'):
if tg.string == 'Home':
tg.parent.extract()
- return self.adeify_images(soup)
- return self.adeify_images(soup)
+ else:
+ if tg.string is not None:
+ tstr = tg.string
+ tg.replaceWith(tstr)
+ return soup
diff --git a/resources/recipes/nrc-nl-epub.recipe b/resources/recipes/nrc-nl-epub.recipe
new file mode 100644
index 0000000000..da9b9195ce
--- /dev/null
+++ b/resources/recipes/nrc-nl-epub.recipe
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#Based on Lars Jacob's Taz Digiabo recipe
+
+__license__ = 'GPL v3'
+__copyright__ = '2010, veezh'
+
+'''
+www.nrc.nl
+'''
+import os, urllib2, zipfile
+import time
+from calibre.web.feeds.news import BasicNewsRecipe
+from calibre.ptempfile import PersistentTemporaryFile
+
+
+class NRCHandelsblad(BasicNewsRecipe):
+
+ title = u'NRC Handelsblad'
+ description = u'De EPUB-versie van NRC'
+ language = 'nl'
+ lang = 'nl-NL'
+
+ __author__ = 'veezh'
+
+ conversion_options = {
+ 'no_default_epub_cover' : True
+ }
+
+ def build_index(self):
+ today = time.strftime("%Y%m%d")
+ domain = "http://digitaleeditie.nrc.nl"
+
+ url = domain + "/digitaleeditie/helekrant/epub/nrc_" + today + ".epub"
+# print url
+
+ try:
+ f = urllib2.urlopen(url)
+ except urllib2.HTTPError:
+ self.report_progress(0,_('Kan niet inloggen om editie te downloaden'))
+ raise ValueError('Krant van vandaag nog niet beschikbaar')
+
+ tmp = PersistentTemporaryFile(suffix='.epub')
+ self.report_progress(0,_('downloading epub'))
+ tmp.write(f.read())
+ tmp.close()
+
+ zfile = zipfile.ZipFile(tmp.name, 'r')
+ self.report_progress(0,_('extracting epub'))
+
+ zfile.extractall(self.output_dir)
+
+ tmp.close()
+ index = os.path.join(self.output_dir, 'content.opf')
+
+ self.report_progress(1,_('epub downloaded and extracted'))
+
+ return index
diff --git a/resources/recipes/radikal_tr.recipe b/resources/recipes/radikal_tr.recipe
index 2d71c238dd..18021f1bb4 100644
--- a/resources/recipes/radikal_tr.recipe
+++ b/resources/recipes/radikal_tr.recipe
@@ -13,14 +13,16 @@ class Radikal_tr(BasicNewsRecipe):
description = 'News from Turkey'
publisher = 'radikal'
category = 'news, politics, Turkey'
- oldest_article = 2
+ oldest_article = 7
max_articles_per_feed = 150
no_stylesheets = True
encoding = 'cp1254'
use_embedded_content = False
masthead_url = 'http://www.radikal.com.tr/D/i/1/V2/radikal_logo.jpg'
language = 'tr'
- extra_css = ' @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} .article_description,body{font-family: Arial,Verdana,Helvetica,sans1,sans-serif } '
+ extra_css = """ @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
+ .article_description,body{font-family: Arial,Verdana,Helvetica,sans1,sans-serif}
+ """
conversion_options = {
'comment' : description
@@ -34,7 +36,13 @@ class Radikal_tr(BasicNewsRecipe):
remove_tags_after = dict(attrs={'id':'haberDetayYazi'})
- feeds = [(u'Yazarlar', u'http://www.radikal.com.tr/d/rss/RssYazarlar.xml')]
+ feeds = [
+ (u'Yazarlar' , u'http://www.radikal.com.tr/d/rss/RssYazarlar.xml')
+ ,(u'Turkiye' , u'http://www.radikal.com.tr/d/rss/Rss_97.xml' )
+ ,(u'Politika' , u'http://www.radikal.com.tr/d/rss/Rss_98.xml' )
+ ,(u'Dis Haberler', u'http://www.radikal.com.tr/d/rss/Rss_100.xml' )
+ ,(u'Ekonomi' , u'http://www.radikal.com.tr/d/rss/Rss_101.xml' )
+ ]
def print_version(self, url):
articleid = url.rpartition('ArticleID=')[2]
diff --git a/resources/recipes/wenxuecity-znjy.recipe b/resources/recipes/wenxuecity-znjy.recipe
new file mode 100644
index 0000000000..ecce80222e
--- /dev/null
+++ b/resources/recipes/wenxuecity-znjy.recipe
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+
+__license__ = 'GPL v3'
+__copyright__ = '2010, Derek Liang '
+'''
+wenxuecity.com
+'''
+import re
+
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class TheCND(BasicNewsRecipe):
+
+ title = 'wenxuecity - znjy'
+ __author__ = 'Derek Liang'
+ description = ''
+ INDEX = 'http://bbs.wenxuecity.com/znjy/?elite=1'
+ language = 'zh'
+ conversion_options = {'linearize_tables':True}
+
+ remove_tags_before = dict(name='div', id='message')
+ remove_tags_after = dict(name='div', id='message')
+ remove_tags = [dict(name='div', id='postmeta'), dict(name='div', id='footer')]
+ no_stylesheets = True
+
+ preprocess_regexps = [(re.compile(r'', re.DOTALL), lambda m: '')]
+
+ def print_version(self, url):
+ return url + '?print'
+
+ def parse_index(self):
+ soup = self.index_to_soup(self.INDEX)
+
+ feeds = []
+ articles = {}
+
+ for a in soup.findAll('a', attrs={'class':'post'}):
+ url = a['href']
+ if url.startswith('/'):
+ url = 'http://bbs.wenxuecity.com'+url
+ title = self.tag_to_string(a)
+ self.log('\tFound article: ', title, ' at:', url)
+ dateReg = re.search( '(\d\d?)/(\d\d?)/(\d\d)', self.tag_to_string(a.parent) )
+ date = '%(y)s/%(m)02d/%(d)02d' % {'y' : dateReg.group(3), 'm' : int(dateReg.group(1)), 'd' : int(dateReg.group(2)) }
+ if not articles.has_key(date):
+ articles[date] = []
+ articles[date].append({'title':title, 'url':url, 'description': '', 'date':''})
+ self.log('\t\tAppend to : ', date)
+
+ self.log('log articles', articles)
+ mostCurrent = sorted(articles).pop()
+ self.title = '文学城 - 子女教育 - ' + mostCurrent
+
+ feeds.append((self.title, articles[mostCurrent]))
+
+ return feeds
+
+ def populate_article_metadata(self, article, soup, first):
+ header = soup.find('h3')
+ self.log('header: ' + self.tag_to_string(header))
+ pass
+
diff --git a/resources/recipes/wsj.recipe b/resources/recipes/wsj.recipe
index 88e07bcea3..4ce315200c 100644
--- a/resources/recipes/wsj.recipe
+++ b/resources/recipes/wsj.recipe
@@ -46,7 +46,7 @@ class WallStreetJournal(BasicNewsRecipe):
br = BasicNewsRecipe.get_browser()
if self.username is not None and self.password is not None:
br.open('http://commerce.wsj.com/auth/login')
- br.select_form(nr=0)
+ br.select_form(nr=1)
br['user'] = self.username
br['password'] = self.password
res = br.submit()
diff --git a/setup/installer/linux/freeze2.py b/setup/installer/linux/freeze2.py
index df2c1d6480..bd8463b1a7 100644
--- a/setup/installer/linux/freeze2.py
+++ b/setup/installer/linux/freeze2.py
@@ -318,7 +318,11 @@ class LinuxFreeze(Command):
import codecs
def set_default_encoding():
- locale.setlocale(locale.LC_ALL, '')
+ try:
+ locale.setlocale(locale.LC_ALL, '')
+ except:
+ print 'WARNING: Failed to set default libc locale, using en_US.UTF-8'
+ locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
enc = locale.getdefaultlocale()[1]
if not enc:
enc = locale.nl_langinfo(locale.CODESET)
diff --git a/setup/installer/windows/notes.rst b/setup/installer/windows/notes.rst
index af4c871dac..b9aef39657 100644
--- a/setup/installer/windows/notes.rst
+++ b/setup/installer/windows/notes.rst
@@ -36,6 +36,16 @@ Install BeautifulSoup 3.0.x manually into site-packages (3.1.x parses broken HTM
Install pywin32 and edit win32com\__init__.py setting _frozen = True and
__gen_path__ to a temp dir (otherwise it tries to set it to a dir in the install tree which leads to permission errors)
+Note that you should use::
+
+ import tempfile
+ __gen_path__ = os.path.join(
+ tempfile.gettempdir(), "gen_py",
+ "%d.%d" % (sys.version_info[0], sys.version_info[1]))
+
+Use gettempdir instead of the win32 api method as gettempdir returns a temp dir that is guaranteed to actually work.
+
+
Also edit win32com\client\gencache.py and change the except IOError on line 57 to catch all exceptions.
SQLite
diff --git a/src/calibre/constants.py b/src/calibre/constants.py
index 9cc67b39e4..7f4d085b6a 100644
--- a/src/calibre/constants.py
+++ b/src/calibre/constants.py
@@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
-__version__ = '0.7.33'
+__version__ = '0.7.34'
__author__ = "Kovid Goyal "
import re
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index d3ccb93c49..274ef1f2f7 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -457,7 +457,8 @@ from calibre.devices.blackberry.driver import BLACKBERRY
from calibre.devices.cybook.driver import CYBOOK, ORIZON
from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \
POCKETBOOK360, GER2, ITALICA, ECLICTO, DBOOK, INVESBOOK, \
- BOOQ, ELONEX, POCKETBOOK301, MENTOR, POCKETBOOK602
+ BOOQ, ELONEX, POCKETBOOK301, MENTOR, POCKETBOOK602, \
+ POCKETBOOK701
from calibre.devices.iliad.driver import ILIAD
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
from calibre.devices.jetbook.driver import JETBOOK, MIBUK, JETBOOK_MINI
@@ -473,10 +474,10 @@ from calibre.devices.binatone.driver import README
from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
from calibre.devices.edge.driver import EDGE
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \
- SOVOS, PICO
+ SOVOS, PICO, SUNSTECH_EB700
from calibre.devices.sne.driver import SNE
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \
- GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD
+ GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD, ALURATEK_COLOR
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
from calibre.devices.kobo.driver import KOBO
from calibre.devices.bambook.driver import BAMBOOK
@@ -546,9 +547,7 @@ plugins += [
JETBOOK_MINI,
MIBUK,
SHINEBOOK,
- POCKETBOOK360,
- POCKETBOOK301,
- POCKETBOOK602,
+ POCKETBOOK360, POCKETBOOK301, POCKETBOOK602, POCKETBOOK701,
KINDLE,
KINDLE2,
KINDLE_DX,
@@ -581,7 +580,7 @@ plugins += [
ELONEX,
TECLAST_K3,
NEWSMY,
- PICO,
+ PICO, SUNSTECH_EB700,
IPAPYRUS,
SOVOS,
EDGE,
@@ -602,8 +601,9 @@ plugins += [
VELOCITYMICRO,
PDNOVEL_KOBO,
LUMIREAD,
- ITUNES,
+ ALURATEK_COLOR,
BAMBOOK,
+ ITUNES,
]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
x.__name__.endswith('MetadataReader')]
diff --git a/src/calibre/debug.py b/src/calibre/debug.py
index 8cc125b118..e1c3e1809e 100644
--- a/src/calibre/debug.py
+++ b/src/calibre/debug.py
@@ -23,6 +23,12 @@ Run an embedded python interpreter.
help='Debug the specified device driver.')
parser.add_option('-g', '--gui', default=False, action='store_true',
help='Run the GUI',)
+ parser.add_option('--gui-debug', default=None,
+ help='Run the GUI with a debug console, logging to the'
+ ' specified path',)
+ parser.add_option('--show-gui-debug', default=None,
+ help='Display the specified log file.',)
+
parser.add_option('-w', '--viewer', default=False, action='store_true',
help='Run the ebook viewer',)
parser.add_option('--paths', default=False, action='store_true',
@@ -135,7 +141,28 @@ def add_simple_plugin(path_to_plugin):
os.chdir(odir)
shutil.rmtree(tdir)
-
+def run_debug_gui(logpath):
+ import time, platform
+ time.sleep(3) # Give previous GUI time to shutdown fully and release locks
+ from calibre.constants import __appname__, __version__, isosx
+ print __appname__, _('Debug log')
+ print __appname__, __version__
+ print platform.platform()
+ print platform.system()
+ print platform.system_alias(platform.system(), platform.release(),
+ platform.version())
+ print 'Python', platform.python_version()
+ try:
+ if iswindows:
+ print 'Windows:', platform.win32_ver()
+ elif isosx:
+ print 'OSX:', platform.mac_ver()
+ else:
+ print 'Linux:', platform.linux_distribution()
+ except:
+ pass
+ from calibre.gui2.main import main
+ main(['__CALIBRE_GUI_DEBUG__', logpath])
def main(args=sys.argv):
from calibre.constants import debug
@@ -154,6 +181,20 @@ def main(args=sys.argv):
if opts.gui:
from calibre.gui2.main import main
main(['calibre'])
+ elif opts.gui_debug is not None:
+ run_debug_gui(opts.gui_debug)
+ elif opts.show_gui_debug:
+ import time, re
+ time.sleep(1)
+ from calibre.gui2 import open_local_file
+ if iswindows:
+ with open(opts.show_gui_debug, 'r+b') as f:
+ raw = f.read()
+ raw = re.sub('(?
@@ -362,7 +362,7 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
cache_name = os.path.join(snbcdir, self.METADATA_CACHE)
with open(cache_name, 'wb') as f:
json_codec.encode_to_file(f, booklists[0])
-
+
with TemporaryFile('.snb') as f:
if self.bambook.PackageSNB(f, tdir):
if not self.bambook.SendFile(f, self.METADATA_FILE_GUID):
@@ -441,8 +441,8 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
with TemporaryDirectory() as tdir:
if self.bambook.GetFile(self.METADATA_FILE_GUID, tdir):
cache_name = os.path.join(tdir, self.METADATA_CACHE)
- if self.bambook.ExtractSNBContent(os.path.join(tdir, self.METADATA_FILE_GUID),
- 'snbc/' + self.METADATA_CACHE,
+ if self.bambook.ExtractSNBContent(os.path.join(tdir, self.METADATA_FILE_GUID),
+ 'snbc/' + self.METADATA_CACHE,
cache_name):
json_codec = JsonCodec()
if os.access(cache_name, os.R_OK):
@@ -460,7 +460,7 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
def update_metadata_item(cls, book, blb):
# Currently, we do not have enough information
# from Bambook SDK to judge whether a book has
- # been changed, we assume all books has been
+ # been changed, we assume all books has been
# changed.
changed = True
# if book.bookName.decode(text_encoding) != blb.title:
diff --git a/src/calibre/devices/bambook/libbambookcore.py b/src/calibre/devices/bambook/libbambookcore.py
index 48d220ad14..a11c5e9e87 100644
--- a/src/calibre/devices/bambook/libbambookcore.py
+++ b/src/calibre/devices/bambook/libbambookcore.py
@@ -5,14 +5,14 @@ __copyright__ = '2010, Li Fanxi '
__docformat__ = 'restructuredtext en'
'''
-Sanda library wrapper
+Sanda library wrapper
'''
import ctypes, uuid, hashlib, os, sys
from threading import Event, Lock
from calibre.constants import iswindows, islinux, isosx
from calibre import load_library
-
+
try:
_lib_name = 'libBambookCore'
cdll = ctypes.cdll
@@ -71,7 +71,7 @@ TRANS_STATUS_ERR = 2 #传输出错
BBKeyNum0 = 0
BBKeyNum1 = 1
BBKeyNum2 = 2
-BBKeyNum3 = 3
+BBKeyNum3 = 3
BBKeyNum4 = 4
BBKeyNum5 = 5
BBKeyNum6 = 6
@@ -85,7 +85,7 @@ BBKeyDown = 13
BBKeyLeft = 14
BBKeyRight = 15
BBKeyPageUp = 16
-BBKeyPageDown = 17
+BBKeyPageDown = 17
BBKeyOK = 18
BBKeyESC = 19
BBKeyBookshelf = 20
@@ -158,7 +158,7 @@ def BambookGetErrorString(code):
func = lib_handle.BambookGetErrorString
func.restype = ctypes.c_char_p
return func(code)
-
+
# extern "C" BB_RESULT BambookGetSDKVersion(uint32_t * version);
def BambookGetSDKVersion():
@@ -275,7 +275,7 @@ def BambookReplacePrivBook(handle, filename, bookID, callback, userData):
return True
else:
return False
-
+
# extern "C" BB_RESULT BambookFetchPrivBook(BB_HANDLE hConn, const char *
# lpszBookID, const char * lpszFilePath, TransCallback pCallbackFunc, intptr_t userData);
def BambookFetchPrivBook(handle, bookID, filename, callback, userData):
@@ -339,7 +339,7 @@ class Bambook:
if self.handle:
return BambookDisconnect(self.handle)
return False
-
+
def GetState(self):
if self.handle:
return BambookGetConnectStatus(self.handle)
@@ -354,7 +354,7 @@ class Bambook:
if self.handle:
taskID = job.NewJob()
if guid:
- if BambookReplacePrivBook(self.handle, fileName, guid,
+ if BambookReplacePrivBook(self.handle, fileName, guid,
bambookTransferCallback, taskID):
if(job.WaitJob(taskID)):
job.DeleteJob(taskID)
@@ -391,7 +391,7 @@ class Bambook:
else:
job.DeleteJob(taskID)
return False
- return False
+ return False
def DeleteFile(self, guid):
if self.handle:
@@ -404,13 +404,13 @@ class Bambook:
books = []
bookInfo = PrivBookInfo()
bi = ctypes.pointer(bookInfo)
-
+
ret = BambookGetFirstPrivBookInfo(self.handle, bi)
while ret:
books.append(bi.contents.Clone())
ret = BambookGetNextPrivBookInfo(self.handle, bi)
return books
-
+
@staticmethod
def GetSDKVersion():
return BambookGetSDKVersion()
@@ -431,13 +431,13 @@ class Bambook:
ret = BambookUnpackFileFromSnb(fileName, 'snbf/toc.snbf', path + '/snbf/toc.snbf')
if not ret:
return False
-
+
return True
@staticmethod
def PackageSNB(fileName, path):
return BambookPackSnbFromDir(fileName, path)
-
+
def passed():
print "> Pass"
@@ -460,7 +460,7 @@ if __name__ == "__main__":
passed()
else:
failed()
-
+
print "Verify good SNB File"
if bb.VerifySNB(u'/tmp/f8268e6c1f4e78c.snb'):
passed()
@@ -472,13 +472,13 @@ if __name__ == "__main__":
passed()
else:
failed()
-
+
print "Extract SNB File"
if bb.ExtractSNB('./test.snb', '/tmp/test'):
passed()
else:
failed()
-
+
print "Packet SNB File"
if bb.PackageSNB('/tmp/tmp.snb', '/tmp/test') and bb.VerifySNB('/tmp/tmp.snb'):
passed()
@@ -509,7 +509,7 @@ if __name__ == "__main__":
passed()
else:
failed()
-
+
print "Get book list"
books = bb.GetBookList()
if len(books) > 10:
diff --git a/src/calibre/devices/eb600/driver.py b/src/calibre/devices/eb600/driver.py
index 54d73d9c1d..246b753fa8 100644
--- a/src/calibre/devices/eb600/driver.py
+++ b/src/calibre/devices/eb600/driver.py
@@ -246,3 +246,32 @@ class POCKETBOOK602(USBMS):
VENDOR_NAME = ''
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB902']
+class POCKETBOOK701(USBMS):
+
+ name = 'PocketBook 701 Device Interface'
+ description = _('Communicate with the PocketBook 701')
+ author = _('Kovid Goyal')
+
+ supported_platforms = ['windows', 'osx', 'linux']
+ FORMATS = ['epub', 'fb2', 'prc', 'mobi', 'pdf', 'djvu', 'rtf', 'chm',
+ 'doc', 'tcr', 'txt']
+
+ EBOOK_DIR_MAIN = 'books'
+ SUPPORTS_SUB_DIRS = True
+
+ VENDOR_ID = [0x18d1]
+ PRODUCT_ID = [0xa004]
+ BCD = [0x0224]
+
+ VENDOR_NAME = 'ANDROID'
+ WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE'
+
+ def windows_sort_drives(self, drives):
+ if len(drives) < 2: return drives
+ main = drives.get('main', None)
+ carda = drives.get('carda', None)
+ if main and carda:
+ drives['main'] = carda
+ drives['carda'] = main
+ return drives
+
diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py
index b76ee4b1c3..51ceb94a99 100644
--- a/src/calibre/devices/kobo/driver.py
+++ b/src/calibre/devices/kobo/driver.py
@@ -96,7 +96,7 @@ class KOBO(USBMS):
for idx,b in enumerate(bl):
bl_cache[b.lpath] = idx
- def update_booklist(prefix, path, title, authors, mime, date, ContentType, ImageID, readstatus):
+ def update_booklist(prefix, path, title, authors, mime, date, ContentType, ImageID, readstatus, MimeType):
changed = False
# if path_to_ext(path) in self.FORMATS:
try:
@@ -124,7 +124,7 @@ class KOBO(USBMS):
#print "Image name Normalized: " + imagename
if imagename is not None:
bl[idx].thumbnail = ImageWrapper(imagename)
- if (ContentType != '6'and self.has_kepubs == False) or (self.has_kepubs == True):
+ if (ContentType != '6' and MimeType != 'Shortcover'):
if self.update_metadata_item(bl[idx]):
# print 'update_metadata_item returned true'
changed = True
@@ -132,7 +132,7 @@ class KOBO(USBMS):
playlist_map[lpath] not in bl[idx].device_collections:
bl[idx].device_collections.append(playlist_map[lpath])
else:
- if ContentType == '6' and self.has_kepubs == False:
+ if ContentType == '6' and MimeType == 'Shortcover':
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576)
else:
try:
@@ -177,15 +177,15 @@ class KOBO(USBMS):
for i, row in enumerate(cursor):
# self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
- path = self.path_from_contentid(row[3], row[5], oncard)
+ path = self.path_from_contentid(row[3], row[5], row[4], oncard)
mime = mime_type_ext(path_to_ext(path)) if path.find('kepub') == -1 else 'application/epub+zip'
# debug_print("mime:", mime)
if oncard != 'carda' and oncard != 'cardb' and not row[3].startswith("file:///mnt/sd/"):
- changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7])
+ changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4])
# print "shortbook: " + path
elif oncard == 'carda' and row[3].startswith("file:///mnt/sd/"):
- changed = update_booklist(self._card_a_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7])
+ changed = update_booklist(self._card_a_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4])
if changed:
need_sync = True
@@ -363,7 +363,8 @@ class KOBO(USBMS):
def contentid_from_path(self, path, ContentType):
if ContentType == 6:
- if self.has_kepubs == False:
+ extension = os.path.splitext(path)[1]
+ if extension == '.kobo':
ContentID = os.path.splitext(path)[0]
# Remove the prefix on the file. it could be either
ContentID = ContentID.replace(self._main_prefix, '')
@@ -411,7 +412,7 @@ class KOBO(USBMS):
ContentType = 999 # Yet another hack: to get around Kobo changing how ContentID is stored
return ContentType
- def path_from_contentid(self, ContentID, ContentType, oncard):
+ def path_from_contentid(self, ContentID, ContentType, MimeType, oncard):
path = ContentID
if oncard == 'cardb':
@@ -420,13 +421,13 @@ class KOBO(USBMS):
path = path.replace("file:///mnt/sd/", self._card_a_prefix)
# print "SD Card: " + path
else:
- if ContentType == "6" and self.has_kepubs == False:
+ if ContentType == "6" and MimeType == 'Shortcover':
# This is a hack as the kobo files do not exist
# but the path is required to make a unique id
# for calibre's reference
path = self._main_prefix + path + '.kobo'
# print "Path: " + path
- elif (ContentType == "6" or ContentType == "10") and self.has_kepubs == True:
+ elif (ContentType == "6" or ContentType == "10") and MimeType == 'application/x-kobo-epub+zip':
path = self._main_prefix + '.kobo/kepub/' + path
# print "Internal: " + path
else:
diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py
index e27aee4393..1989fb7c61 100644
--- a/src/calibre/devices/misc.py
+++ b/src/calibre/devices/misc.py
@@ -62,9 +62,9 @@ class SWEEX(USBMS):
# Ordered list of supported formats
FORMATS = ['epub', 'prc', 'fb2', 'html', 'rtf', 'chm', 'pdf', 'txt']
- VENDOR_ID = [0x0525]
- PRODUCT_ID = [0xa4a5]
- BCD = [0x0319]
+ VENDOR_ID = [0x0525, 0x177f]
+ PRODUCT_ID = [0xa4a5, 0x300]
+ BCD = [0x0319, 0x110]
VENDOR_NAME = 'SWEEX'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOKREADER'
@@ -104,7 +104,7 @@ class PDNOVEL(USBMS):
VENDOR_NAME = 'ANDROID'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE'
- THUMBNAIL_HEIGHT = 144
+ THUMBNAIL_HEIGHT = 130
EBOOK_DIR_MAIN = 'eBooks'
SUPPORTS_SUB_DIRS = False
@@ -204,3 +204,23 @@ class LUMIREAD(USBMS):
with open(cfilepath+'.jpg', 'wb') as f:
f.write(metadata.thumbnail[-1])
+class ALURATEK_COLOR(USBMS):
+
+ name = 'Aluratek Color Device Interface'
+ gui_name = 'Aluratek Color'
+ description = _('Communicate with the Aluratek Color')
+ author = 'Kovid Goyal'
+ supported_platforms = ['windows', 'osx', 'linux']
+
+ # Ordered list of supported formats
+ FORMATS = ['epub', 'fb2', 'txt', 'pdf']
+
+ VENDOR_ID = [0x1f3a]
+ PRODUCT_ID = [0x1000]
+ BCD = [0x0002]
+
+ EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'books'
+
+ VENDOR_NAME = 'USB_2.0'
+ WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB_FLASH_DRIVER'
+
diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py
index 44ecd5cfd0..6652d581d4 100644
--- a/src/calibre/devices/prs505/driver.py
+++ b/src/calibre/devices/prs505/driver.py
@@ -58,9 +58,16 @@ class PRS505(USBMS):
SUPPORTS_USE_AUTHOR_SORT = True
EBOOK_DIR_MAIN = 'database/media/books'
+ ALL_BY_TITLE = _('All by title')
+ ALL_BY_AUTHOR = _('All by author')
+
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of metadata fields '
'to turn into collections on the device. Possibilities include: ')+\
- 'series, tags, authors'
+ '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'])
plugboard = None
@@ -151,7 +158,7 @@ class PRS505(USBMS):
blists[i] = booklists[i]
opts = self.settings()
if opts.extra_customization:
- collections = [x.lower().strip() for x in
+ collections = [x.strip() for x in
opts.extra_customization.split(',')]
else:
collections = []
@@ -179,6 +186,8 @@ 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
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/prs505/sony_cache.py b/src/calibre/devices/prs505/sony_cache.py
index f271329fc8..841f6bc346 100644
--- a/src/calibre/devices/prs505/sony_cache.py
+++ b/src/calibre/devices/prs505/sony_cache.py
@@ -410,6 +410,9 @@ class XMLCache(object):
newmi = book.deepcopy_metadata()
newmi.template_to_attribute(book, plugboard)
newmi.set('_new_book', getattr(book, '_new_book', False))
+ book.set('_pb_title_sort',
+ newmi.get('title_sort', newmi.get('title', None)))
+ book.set('_pb_author_sort', newmi.get('author_sort', ''))
else:
newmi = book
(gtz_count, ltz_count, use_tz_var) = \
diff --git a/src/calibre/devices/sne/driver.py b/src/calibre/devices/sne/driver.py
index 0ccac13245..bb8d34c59c 100644
--- a/src/calibre/devices/sne/driver.py
+++ b/src/calibre/devices/sne/driver.py
@@ -23,16 +23,16 @@ class SNE(USBMS):
FORMATS = ['epub', 'pdf', 'txt']
VENDOR_ID = [0x04e8]
- PRODUCT_ID = [0x2051, 0x2053]
+ PRODUCT_ID = [0x2051, 0x2053, 0x2054]
BCD = [0x0323]
VENDOR_NAME = 'SAMSUNG'
- WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'SNE-60'
+ WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['SNE-60', 'E65']
MAIN_MEMORY_VOLUME_LABEL = 'SNE Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'SNE Storage Card'
- EBOOK_DIR_MAIN = 'Books'
+ EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'Books'
SUPPORTS_SUB_DIRS = True
diff --git a/src/calibre/devices/teclast/driver.py b/src/calibre/devices/teclast/driver.py
index b9ec554cee..f406448ad2 100644
--- a/src/calibre/devices/teclast/driver.py
+++ b/src/calibre/devices/teclast/driver.py
@@ -72,3 +72,13 @@ class SOVOS(TECLAST_K3):
VENDOR_NAME = 'RK28XX'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB-MSC'
+class SUNSTECH_EB700(TECLAST_K3):
+ name = 'Sunstech EB700 device interface'
+ gui_name = 'EB700'
+ description = _('Communicate with the Sunstech EB700 reader.')
+
+ FORMATS = ['epub', 'fb2', 'pdf', 'pdb', 'txt']
+
+ VENDOR_NAME = 'SUNEB700'
+ WINDOWS_MAIN_MEM = 'USB-MSC'
+
diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index 3372f5c8a5..ba005c4e6d 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -132,9 +132,24 @@ class CollectionsBookList(BookList):
use_renaming_rules = prefs['manage_device_metadata'] == 'on_connect'
collections = {}
- # This map of sets is used to avoid linear searches when testing for
- # book equality
+
+ # get the special collection names
+ all_by_author = ''
+ all_by_title = ''
+ ca = []
+ for c in collection_attributes:
+ if c.startswith('aba:') and c[4:]:
+ all_by_author = c[4:].strip()
+ elif c.startswith('abt:') and c[4:]:
+ all_by_title = c[4:].strip()
+ else:
+ ca.append(c.lower())
+ collection_attributes = ca
+
for book in self:
+ tsval = book.get('_pb_title_sort',
+ book.get('title_sort', book.get('title', 'zzzz')))
+ asval = book.get('_pb_author_sort', book.get('author_sort', ''))
# Make sure we can identify this book via the lpath
lpath = getattr(book, 'lpath', None)
if lpath is None:
@@ -211,22 +226,29 @@ class CollectionsBookList(BookList):
collections[cat_name] = {}
if use_renaming_rules and sort_attr:
sort_val = book.get(sort_attr, None)
- collections[cat_name][lpath] = \
- (book, sort_val, book.get('title_sort', 'zzzz'))
+ collections[cat_name][lpath] = (book, sort_val, tsval)
elif is_series:
if doing_dc:
collections[cat_name][lpath] = \
- (book, book.get('series_index', sys.maxint),
- book.get('title_sort', 'zzzz'))
+ (book, book.get('series_index', sys.maxint), tsval)
else:
collections[cat_name][lpath] = \
- (book, book.get(attr+'_index', sys.maxint),
- book.get('title_sort', 'zzzz'))
+ (book, book.get(attr+'_index', sys.maxint), tsval)
else:
if lpath not in collections[cat_name]:
- collections[cat_name][lpath] = \
- (book, book.get('title_sort', 'zzzz'),
- book.get('title_sort', 'zzzz'))
+ collections[cat_name][lpath] = (book, tsval, tsval)
+
+ # All books by author
+ if all_by_author:
+ if all_by_author not in collections:
+ collections[all_by_author] = {}
+ collections[all_by_author][lpath] = (book, asval, tsval)
+ # All books by title
+ if all_by_title:
+ if all_by_title not in collections:
+ collections[all_by_title] = {}
+ collections[all_by_title][lpath] = (book, tsval, asval)
+
# Sort collections
result = {}
diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py
index af2948cf82..2c095d6f7b 100644
--- a/src/calibre/devices/usbms/device.py
+++ b/src/calibre/devices/usbms/device.py
@@ -605,8 +605,9 @@ class Device(DeviceConfig, DevicePlugin):
main, carda, cardb = self.find_device_nodes()
if main is None:
- raise DeviceError(_('Unable to detect the %s disk drive. Your '
- ' kernel is probably exporting a deprecated version of SYSFS.')
+ raise DeviceError(_('Unable to detect the %s disk drive. Either '
+ 'the device has already been ejected, or your '
+ 'kernel is exporting a deprecated version of SYSFS.')
%self.__class__.__name__)
self._linux_mount_map = {}
diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py
index 9bdf937dd1..da4d1178eb 100644
--- a/src/calibre/ebooks/__init__.py
+++ b/src/calibre/ebooks/__init__.py
@@ -22,6 +22,9 @@ class UnknownFormatError(Exception):
class DRMError(ValueError):
pass
+class ParserError(ValueError):
+ pass
+
BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'xhtm',
'html', 'xhtml', 'pdf', 'pdb', 'pdr', 'prc', 'mobi', 'azw', 'doc',
'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz', 'cbc', 'oebzip',
@@ -39,6 +42,10 @@ class HTMLRenderer(object):
try:
if not ok:
raise RuntimeError('Rendering of HTML failed.')
+ de = self.page.mainFrame().documentElement()
+ pe = de.findFirst('parsererror')
+ if not pe.isNull():
+ raise ParserError(pe.toPlainText())
image = QImage(self.page.viewportSize(), QImage.Format_ARGB32)
image.setDotsPerMeterX(96*(100/2.54))
image.setDotsPerMeterY(96*(100/2.54))
@@ -104,7 +111,7 @@ def render_html_svg_workaround(path_to_html, log, width=590, height=750):
return data
-def render_html(path_to_html, width=590, height=750):
+def render_html(path_to_html, width=590, height=750, as_xhtml=True):
from PyQt4.QtWebKit import QWebPage
from PyQt4.Qt import QEventLoop, QPalette, Qt, SIGNAL, QUrl, QSize
from calibre.gui2 import is_ok_to_use_qt
@@ -122,11 +129,18 @@ def render_html(path_to_html, width=590, height=750):
renderer = HTMLRenderer(page, loop)
page.connect(page, SIGNAL('loadFinished(bool)'), renderer,
Qt.QueuedConnection)
- page.mainFrame().load(QUrl.fromLocalFile(path_to_html))
+ if as_xhtml:
+ page.mainFrame().setContent(open(path_to_html, 'rb').read(),
+ 'application/xhtml+xml', QUrl.fromLocalFile(path_to_html))
+ else:
+ page.mainFrame().load(QUrl.fromLocalFile(path_to_html))
loop.exec_()
renderer.loop = renderer.page = None
del page
del loop
+ if isinstance(renderer.exception, ParserError) and as_xhtml:
+ return render_html(path_to_html, width=width, height=height,
+ as_xhtml=False)
return renderer
def check_ebook_format(stream, current_guess):
diff --git a/src/calibre/ebooks/fb2/fb2ml.py b/src/calibre/ebooks/fb2/fb2ml.py
index 5efc360f1f..90c88c3cd0 100644
--- a/src/calibre/ebooks/fb2/fb2ml.py
+++ b/src/calibre/ebooks/fb2/fb2ml.py
@@ -27,13 +27,10 @@ class FB2MLizer(object):
'''
Todo: * Include more FB2 specific tags in the conversion.
* Handle a tags.
- * Figure out some way to turn oeb_book.toc items into
-
to allow for readers to generate toc from the document.
'''
def __init__(self, log):
self.log = log
- self.image_hrefs = {}
self.reset_state()
def reset_state(self):
@@ -43,17 +40,25 @@ class FB2MLizer(object):
# in different directories. FB2 images are all in a flat layout so we rename all images
# into a sequential numbering system to ensure there are no collisions between image names.
self.image_hrefs = {}
+ # Mapping of toc items and their
+ self.toc = {}
+ # Used to see whether a new needs to be opened
+ self.section_level = 0
def extract_content(self, oeb_book, opts):
self.log.info('Converting XHTML to FB2 markup...')
self.oeb_book = oeb_book
self.opts = opts
+ self.reset_state()
+
+ # Used for adding s and s to allow readers
+ # to generate toc from the document.
+ if self.opts.sectionize == 'toc':
+ self.create_flat_toc(self.oeb_book.toc, 1)
return self.fb2mlize_spine()
def fb2mlize_spine(self):
- self.reset_state()
-
output = [self.fb2_header()]
output.append(self.get_text())
output.append(self.fb2mlize_images())
@@ -66,13 +71,19 @@ class FB2MLizer(object):
return u'' + output
def clean_text(self, text):
- text = re.sub(r'(?miu)\s*', '', text)
- text = re.sub(r'(?miu)\s+', '', text)
- text = re.sub(r'(?miu)
', '\n\n', text)
-
text = re.sub(r'(?miu)
\s*
', '', text)
- text = re.sub(r'(?miu)\s+
', '', text)
- text = re.sub(r'(?miu)
', '
\n\n
', text)
+ text = re.sub(r'(?miu)\s*
', '', text)
+ text = re.sub(r'(?miu)\s*
', '
\n\n
', text)
+
+ text = re.sub(r'(?miu)
\s*', '', text)
+ text = re.sub(r'(?miu)\s+', '', text)
+
+ text = re.sub(r'(?miu)\s*', '', text)
+ text = re.sub(r'(?miu)\s*', '\n', text)
+ text = re.sub(r'(?miu)\s*', '\n\n', text)
+ text = re.sub(r'(?miu)\s*', '\n', text)
+ text = re.sub(r'(?miu)\s*', '\n', text)
+ text = re.sub(r'(?miu)', '\n\n', text)
if self.opts.insert_blank_line:
text = re.sub(r'(?miu)', '', text)
@@ -144,12 +155,34 @@ class FB2MLizer(object):
def get_text(self):
text = ['']
+
+ # Create main section if there are no others to create
+ if self.opts.sectionize == 'nothing':
+ text.append('')
+ self.section_level += 1
+
for item in self.oeb_book.spine:
self.log.debug('Converting %s to FictionBook2 XML' % item.href)
stylizer = Stylizer(item.data, item.href, self.oeb_book, self.opts, self.opts.output_profile)
- text.append('')
+
+ # Start a if we must sectionize each file or if the TOC references this page
+ page_section_open = False
+ if self.opts.sectionize == 'files' or self.toc.get(item.href) == 'page':
+ text.append('')
+ page_section_open = True
+ self.section_level += 1
+
text += self.dump_text(item.data.find(XHTML('body')), stylizer, item)
+
+ if page_section_open:
+ text.append('')
+ self.section_level -= 1
+
+ # Close any open sections
+ while self.section_level > 0:
text.append('')
+ self.section_level -= 1
+
return ''.join(text) + ''
def fb2mlize_images(self):
@@ -184,6 +217,17 @@ class FB2MLizer(object):
'%s.' % (item.href, e))
return ''.join(images)
+ def create_flat_toc(self, nodes, level):
+ for item in nodes:
+ href, mid, id = item.href.partition('#')
+ if not id:
+ self.toc[href] = 'page'
+ else:
+ if not self.toc.get(href, None):
+ self.toc[href] = {}
+ self.toc[href][id] = level
+ self.create_flat_toc(item.nodes, level + 1)
+
def ensure_p(self):
if self.in_p:
return [], []
@@ -254,10 +298,38 @@ class FB2MLizer(object):
# First tag in tree
tag = barename(elem_tree.tag)
+ # Convert TOC entries to s and add s
+ if self.opts.sectionize == 'toc':
+ # A section cannot be a child of any other element than another section,
+ # so leave the tag alone if there are parents
+ if not tag_stack:
+ # There are two reasons to start a new section here: the TOC pointed to
+ # this page (then we use the first non- on the page as a ), or
+ # the TOC pointed to a specific element
+ newlevel = 0
+ toc_entry = self.toc.get(page.href, None)
+ if toc_entry == 'page':
+ if tag != 'body' and hasattr(elem_tree, 'text') and elem_tree.text:
+ newlevel = 1
+ self.toc[page.href] = None
+ elif toc_entry and elem_tree.attrib.get('id', None):
+ newlevel = toc_entry.get(elem_tree.attrib.get('id', None), None)
+
+ # Start a new section if necessary
+ if newlevel:
+ if not (newlevel > self.section_level):
+ fb2_out.append('')
+ self.section_level -= 1
+ fb2_out.append('')
+ self.section_level += 1
+ fb2_out.append('')
+ tags.append('title')
+ if self.section_level == 0:
+ # If none of the prior processing made a section, make one now to be FB2 spec compliant
+ fb2_out.append('')
+ self.section_level += 1
+
# Process the XHTML tag if it needs to be converted to an FB2 tag.
- if tag == 'h1' and self.opts.h1_to_title or tag == 'h2' and self.opts.h2_to_title or tag == 'h3' and self.opts.h3_to_title:
- fb2_out.append('')
- tags.append('title')
if tag == 'img':
if elem_tree.attrib.get('src', None):
# Only write the image tag if it is in the manifest.
diff --git a/src/calibre/ebooks/fb2/output.py b/src/calibre/ebooks/fb2/output.py
index 33714c6e6e..e8b50d6f77 100644
--- a/src/calibre/ebooks/fb2/output.py
+++ b/src/calibre/ebooks/fb2/output.py
@@ -16,15 +16,15 @@ class FB2Output(OutputFormatPlugin):
file_type = 'fb2'
options = set([
- OptionRecommendation(name='h1_to_title',
- recommended_value=False, level=OptionRecommendation.LOW,
- help=_('Wrap all h1 tags with fb2 title elements.')),
- OptionRecommendation(name='h2_to_title',
- recommended_value=False, level=OptionRecommendation.LOW,
- help=_('Wrap all h2 tags with fb2 title elements.')),
- OptionRecommendation(name='h3_to_title',
- recommended_value=False, level=OptionRecommendation.LOW,
- help=_('Wrap all h3 tags with fb2 title elements.')),
+ OptionRecommendation(name='sectionize',
+ recommended_value='files', level=OptionRecommendation.LOW,
+ choices=['toc', 'files', 'nothing'],
+ help=_('Specify the sectionization of elements. '
+ 'A value of "nothing" turns the book into a single section. '
+ 'A value of "files" turns each file into a separate section; use this if your device is having trouble. '
+ 'A value of "Table of Contents" turns the entries in the Table of Contents into titles and creates sections; '
+ 'if it fails, adjust the "Structure Detection" and/or "Table of Contents" settings '
+ '(turn on "Force use of auto-generated Table of Contents).')),
])
def convert(self, oeb_book, output_path, input_plugin, opts, log):
diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py
index 01e5190640..02401b25e6 100644
--- a/src/calibre/ebooks/metadata/__init__.py
+++ b/src/calibre/ebooks/metadata/__init__.py
@@ -55,8 +55,12 @@ except:
_ignore_starts = u'\'"'+u''.join(unichr(x) for x in range(0x2018, 0x201e)+[0x2032, 0x2033])
-def title_sort(title):
+def title_sort(title, order=None):
+ if order is None:
+ order = tweaks['title_series_sorting']
title = title.strip()
+ if order == 'strictly_alphabetic':
+ return title
if title and title[0] in _ignore_starts:
title = title[1:]
match = _title_pat.search(title)
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index f0844e3711..22752ca09e 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -463,6 +463,8 @@ class Metadata(object):
other_lang = getattr(other, 'language', None)
if other_lang and other_lang.lower() != 'und':
self.language = other_lang
+ if not getattr(self, 'series', None):
+ self.series_index = None
def format_series_index(self, val=None):
from calibre.ebooks.metadata import fmt_sidx
diff --git a/src/calibre/ebooks/metadata/pdf.py b/src/calibre/ebooks/metadata/pdf.py
index 2d1935539e..20a4c4659e 100644
--- a/src/calibre/ebooks/metadata/pdf.py
+++ b/src/calibre/ebooks/metadata/pdf.py
@@ -17,6 +17,7 @@ pdfreflow, pdfreflow_error = plugins['pdfreflow']
def get_metadata(stream, cover=True):
if pdfreflow is None:
raise RuntimeError(pdfreflow_error)
+ stream.seek(0)
raw = stream.read()
#isbn = _isbn_pat.search(raw)
#if isbn is not None:
diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py
index 0f364b8030..c015868992 100644
--- a/src/calibre/ebooks/oeb/base.py
+++ b/src/calibre/ebooks/oeb/base.py
@@ -11,12 +11,11 @@ import os, re, uuid, logging
from mimetypes import types_map
from collections import defaultdict
from itertools import count
-from urlparse import urldefrag, urlparse, urlunparse
+from urlparse import urldefrag, urlparse, urlunparse, urljoin
from urllib import unquote as urlunquote
-from urlparse import urljoin
from lxml import etree, html
-from cssutils import CSSParser
+from cssutils import CSSParser, parseString, parseStyle, replaceUrls
from cssutils.css import CSSRule
import calibre
@@ -88,11 +87,11 @@ def XLINK(name):
def CALIBRE(name):
return '{%s}%s' % (CALIBRE_NS, name)
-_css_url_re = re.compile(r'url\((.*?)\)', re.I)
+_css_url_re = re.compile(r'url\s*\((.*?)\)', re.I)
_css_import_re = re.compile(r'@import "(.*?)"')
_archive_re = re.compile(r'[^ ]+')
-def iterlinks(root):
+def iterlinks(root, find_links_in_css=True):
'''
Iterate over all links in a OEB Document.
@@ -134,6 +133,8 @@ def iterlinks(root):
yield (el, attr, attribs[attr], 0)
+ if not find_links_in_css:
+ continue
if tag == XHTML('style') and el.text:
for match in _css_url_re.finditer(el.text):
yield (el, None, match.group(1), match.start(1))
@@ -180,7 +181,7 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False):
'''
if resolve_base_href:
resolve_base_href(root)
- for el, attrib, link, pos in iterlinks(root):
+ for el, attrib, link, pos in iterlinks(root, find_links_in_css=False):
new_link = link_repl_func(link.strip())
if new_link == link:
continue
@@ -203,6 +204,40 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False):
new = cur[:pos] + new_link + cur[pos+len(link):]
el.attrib[attrib] = new
+ def set_property(v):
+ if v.CSS_PRIMITIVE_VALUE == v.cssValueType and \
+ v.CSS_URI == v.primitiveType:
+ v.setStringValue(v.CSS_URI,
+ link_repl_func(v.getStringValue()))
+
+ for el in root.iter():
+ try:
+ tag = el.tag
+ except UnicodeDecodeError:
+ continue
+
+ if tag == XHTML('style') and el.text and \
+ (_css_url_re.search(el.text) is not None or '@import' in
+ el.text):
+ stylesheet = parseString(el.text)
+ replaceUrls(stylesheet, link_repl_func)
+ el.text = '\n'+stylesheet.cssText + '\n'
+
+ if 'style' in el.attrib:
+ text = el.attrib['style']
+ if _css_url_re.search(text) is not None:
+ stext = parseStyle(text)
+ for p in stext.getProperties(all=True):
+ v = p.cssValue
+ if v.CSS_VALUE_LIST == v.cssValueType:
+ for item in v:
+ set_property(item)
+ elif v.CSS_PRIMITIVE_VALUE == v.cssValueType:
+ set_property(v)
+ el.attrib['style'] = stext.cssText.replace('\n', ' ').replace('\r',
+ ' ')
+
+
EPUB_MIME = types_map['.epub']
XHTML_MIME = types_map['.xhtml']
@@ -622,7 +657,10 @@ class Metadata(object):
attrib[key] = prefixname(value, nsrmap)
if namespace(self.term) == DC11_NS:
elem = element(parent, self.term, attrib=attrib)
- elem.text = self.value
+ try:
+ elem.text = self.value
+ except:
+ elem.text = repr(self.value)
else:
elem = element(parent, OPF('meta'), attrib=attrib)
elem.attrib['name'] = prefixname(self.term, nsrmap)
diff --git a/src/calibre/ebooks/oeb/iterator.py b/src/calibre/ebooks/oeb/iterator.py
index 10180541a1..6820709b3e 100644
--- a/src/calibre/ebooks/oeb/iterator.py
+++ b/src/calibre/ebooks/oeb/iterator.py
@@ -257,7 +257,6 @@ class EbookIterator(object):
s.max_page = s.start_page + s.pages - 1
self.toc = self.opf.toc
- self.find_embedded_fonts()
self.read_bookmarks()
return self
diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py
index 616cd3b800..40b82514c1 100644
--- a/src/calibre/ebooks/oeb/stylizer.py
+++ b/src/calibre/ebooks/oeb/stylizer.py
@@ -205,7 +205,10 @@ class Stylizer(object):
NameError, # thrown on OS X instead of SelectorSyntaxError
SelectorSyntaxError):
continue
- matches = selector(tree)
+ try:
+ matches = selector(tree)
+ except etree.XPathEvalError:
+ continue
if not matches:
ntext = capital_sel_pat.sub(lambda m: m.group().lower(), text)
diff --git a/src/calibre/ebooks/oeb/transforms/filenames.py b/src/calibre/ebooks/oeb/transforms/filenames.py
index 46f9fc5539..bad75b9a6f 100644
--- a/src/calibre/ebooks/oeb/transforms/filenames.py
+++ b/src/calibre/ebooks/oeb/transforms/filenames.py
@@ -6,7 +6,7 @@ __copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
import posixpath
-from urlparse import urldefrag
+from urlparse import urldefrag, urlparse
from lxml import etree
import cssutils
@@ -67,6 +67,10 @@ class RenameFiles(object): # {{{
def url_replacer(self, orig_url):
url = urlnormalize(orig_url)
+ parts = urlparse(url)
+ if parts.scheme:
+ # Only rewrite local URLs
+ return orig_url
path, frag = urldefrag(url)
if self.renamed_items_map:
orig_item = self.renamed_items_map.get(self.current_item.href, self.current_item)
diff --git a/src/calibre/ebooks/pml/pmlconverter.py b/src/calibre/ebooks/pml/pmlconverter.py
index b0fc15197a..10e5871d31 100644
--- a/src/calibre/ebooks/pml/pmlconverter.py
+++ b/src/calibre/ebooks/pml/pmlconverter.py
@@ -72,8 +72,8 @@ class PML_HTMLizer(object):
'ra': ('', ''),
'c': ('
', '
'),
'r': ('
', '
'),
- 't': ('
', '
'),
- 'T': ('
', '
'),
+ 't': ('
', '
'),
+ 'T': ('
', '
'),
'i': ('', ''),
'u': ('', ''),
'd': ('', ''),
diff --git a/src/calibre/ebooks/rtf/input.py b/src/calibre/ebooks/rtf/input.py
index 57903a6711..8c7561f68c 100644
--- a/src/calibre/ebooks/rtf/input.py
+++ b/src/calibre/ebooks/rtf/input.py
@@ -245,7 +245,7 @@ class RTFInput(InputFormatPlugin):
from calibre.ebooks.metadata.meta import get_metadata
from calibre.ebooks.metadata.opf2 import OPFCreator
from calibre.ebooks.rtf2xml.ParseRtf import RtfInvalidCodeException
- self.options = options
+ self.opts = options
self.log = log
self.log('Converting RTF to XML...')
#Name of the preprocesssed RTF file
@@ -290,12 +290,12 @@ class RTFInput(InputFormatPlugin):
res = transform.tostring(result)
res = res[:100].replace('xmlns:html', 'xmlns') + res[100:]
# Replace newlines inserted by the 'empty_paragraphs' option in rtf2xml with html blank lines
- if not getattr(self.options, 'remove_paragraph_spacing', False):
+ if not getattr(self.opts, 'remove_paragraph_spacing', False):
res = re.sub('\s*', '', res)
res = re.sub('(?<=\n)\n{2}',
u'
\u00a0
\n'.encode('utf-8'), res)
- if self.options.preprocess_html:
- preprocessor = PreProcessor(self.options, log=getattr(self, 'log', None))
+ if self.opts.preprocess_html:
+ preprocessor = PreProcessor(self.opts, log=getattr(self, 'log', None))
res = preprocessor(res)
f.write(res)
self.write_inline_css(inline_class, border_styles)
diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index 9d20f38b99..1c99d9d9d5 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -540,6 +540,7 @@ def choose_dir(window, name, title, default_dir='~'):
parent=window, name=name, mode=QFileDialog.Directory,
default_dir=default_dir)
dir = fd.get_files()
+ fd.setParent(None)
if dir:
return dir[0]
@@ -560,6 +561,7 @@ def choose_files(window, name, title,
fd = FileDialog(title=title, name=name, filters=filters,
parent=window, add_all_files_filter=all_files, mode=mode,
)
+ fd.setParent(None)
if fd.accepted:
return fd.get_files()
return None
@@ -570,6 +572,7 @@ def choose_images(window, name, title, select_only_single_file=True):
filters=[('Images', ['png', 'gif', 'jpeg', 'jpg', 'svg'])],
parent=window, add_all_files_filter=False, mode=mode,
)
+ fd.setParent(None)
if fd.accepted:
return fd.get_files()
return None
diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py
index 38c28661b7..9917c542ae 100644
--- a/src/calibre/gui2/actions/add.py
+++ b/src/calibre/gui2/actions/add.py
@@ -243,7 +243,9 @@ class AddAction(InterfaceAction):
if hasattr(self._adder, 'cleanup'):
self._adder.cleanup()
- self._adder = None
+ self._adder.setParent(None)
+ del self._adder
+ self._adder = None
def _add_from_device_adder(self, paths=[], names=[], infos=[],
on_card=None, model=None):
diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py
index e789ae62e6..6f4e883b1a 100644
--- a/src/calibre/gui2/actions/choose_library.py
+++ b/src/calibre/gui2/actions/choose_library.py
@@ -138,6 +138,10 @@ class CheckIntegrity(QProgressDialog):
'You should check them manually. This can '
'happen if you manipulate the files in the '
'library folder directly.'), det_msg=det_msg, show=True)
+ else:
+ info_dialog(self, _('No errors found'),
+ _('The integrity check completed with no uncorrectable errors found.'),
+ show=True)
self.reset()
# }}}
@@ -162,6 +166,7 @@ class ChooseLibraryAction(InterfaceAction):
self.choose_menu = QMenu(self.gui)
self.qaction.setMenu(self.choose_menu)
+
if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
self.choose_menu.addAction(self.action_choose)
@@ -172,6 +177,11 @@ class ChooseLibraryAction(InterfaceAction):
self.delete_menu = QMenu(_('Delete library'))
self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu)
+ ac = self.create_action(spec=(_('Pick a random book'), 'catalog.png',
+ None, None), attr='action_pick_random')
+ ac.triggered.connect(self.pick_random)
+ self.choose_menu.addAction(ac)
+
self.rename_separator = self.choose_menu.addSeparator()
self.switch_actions = []
@@ -209,6 +219,12 @@ class ChooseLibraryAction(InterfaceAction):
self.maintenance_menu.addAction(ac)
self.choose_menu.addMenu(self.maintenance_menu)
+ def pick_random(self, *args):
+ import random
+ pick = random.randint(0, self.gui.library_view.model().rowCount(None))
+ self.gui.library_view.set_current_row(pick)
+ self.gui.library_view.scroll_to_row(pick)
+
def library_name(self):
db = self.gui.library_view.model().db
path = db.library_path
diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py
index a0f49a7e9a..27973b5f5b 100644
--- a/src/calibre/gui2/actions/delete.py
+++ b/src/calibre/gui2/actions/delete.py
@@ -12,6 +12,7 @@ from PyQt4.Qt import QMenu, QObject, QTimer
from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.delete_matching_from_device import DeleteMatchingFromDeviceDialog
from calibre.gui2.dialogs.confirm_delete import confirm
+from calibre.gui2.dialogs.confirm_delete_location import confirm_location
from calibre.gui2.actions import InterfaceAction
single_shot = partial(QTimer.singleShot, 10)
@@ -96,10 +97,15 @@ class DeleteAction(InterfaceAction):
for action in list(self.delete_menu.actions())[1:]:
action.setEnabled(enabled)
- def _get_selected_formats(self, msg):
+ def _get_selected_formats(self, msg, ids):
from calibre.gui2.dialogs.select_formats import SelectFormats
- fmts = self.gui.library_view.model().db.all_formats()
- d = SelectFormats([x.lower() for x in fmts], msg, parent=self.gui)
+ fmts = set([])
+ db = self.gui.library_view.model().db
+ for x in ids:
+ fmts_ = db.formats(x, index_is_id=True, verify_formats=False)
+ if fmts_:
+ fmts.update(frozenset([x.lower() for x in fmts_.split(',')]))
+ d = SelectFormats(list(sorted(fmts)), msg, parent=self.gui)
if d.exec_() != d.Accepted:
return None
return d.selected_formats
@@ -117,7 +123,7 @@ class DeleteAction(InterfaceAction):
if not ids:
return
fmts = self._get_selected_formats(
- _('Choose formats to be deleted'))
+ _('Choose formats to be deleted'), ids)
if not fmts:
return
for id in ids:
@@ -135,7 +141,7 @@ class DeleteAction(InterfaceAction):
if not ids:
return
fmts = self._get_selected_formats(
- '
'+_('Choose formats not to be deleted'))
+ '
'+_('Choose formats not to be deleted'), ids)
if fmts is None:
return
for id in ids:
@@ -223,7 +229,31 @@ class DeleteAction(InterfaceAction):
rows = view.selectionModel().selectedRows()
if not rows or len(rows) == 0:
return
+ # Library view is visible.
if self.gui.stack.currentIndex() == 0:
+ # Ask the user if they want to delete the book from the library or device if it is in both.
+ if self.gui.device_manager.is_device_connected:
+ on_device = False
+ on_device_ids = self._get_selected_ids()
+ for id in on_device_ids:
+ res = self.gui.book_on_device(id)
+ if res[0] or res[1] or res[2]:
+ on_device = True
+ if on_device:
+ break
+ if on_device:
+ loc = confirm_location('
' + _('Some of the selected books are on the attached device. '
+ 'Where do you want the selected files deleted from?'),
+ self.gui)
+ if not loc:
+ return
+ elif loc == 'dev':
+ self.remove_matching_books_from_device()
+ return
+ elif loc == 'both':
+ self.remove_matching_books_from_device()
+ # The following will run if the selected books are not on a connected device.
+ # The user has selected to delete from the library or the device and library.
if not confirm('
'+_('The selected books will be '
'permanently deleted and the files '
'removed from your calibre library. Are you sure?')
@@ -239,7 +269,7 @@ class DeleteAction(InterfaceAction):
else:
self.__md = MultiDeleter(self.gui, rows,
partial(self.library_ids_deleted, current_row=row))
-
+ # Device view is visible.
else:
if not confirm('