diff --git a/Changelog.yaml b/Changelog.yaml
index 7f7afc117a..bcf58ae03d 100644
--- a/Changelog.yaml
+++ b/Changelog.yaml
@@ -11,7 +11,7 @@
- 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 o disables the animation completely."
+ "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."
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 750af9efa7..a420cd7d44 100644
--- a/resources/default_tweaks.py
+++ b/resources/default_tweaks.py
@@ -41,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
diff --git a/resources/images/devices/bambook.png b/resources/images/devices/bambook.png
new file mode 100644
index 0000000000..def3f6d594
Binary files /dev/null and b/resources/images/devices/bambook.png differ
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/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/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/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/customize/builtins.py b/src/calibre/customize/builtins.py
index 793c1fa0de..93dda884cc 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -474,12 +474,14 @@ 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, \
+ TREKSTOR
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
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
LibraryThing
@@ -579,7 +581,7 @@ plugins += [
ELONEX,
TECLAST_K3,
NEWSMY,
- PICO,
+ PICO, SUNSTECH_EB700,
IPAPYRUS,
SOVOS,
EDGE,
@@ -600,6 +602,9 @@ plugins += [
VELOCITYMICRO,
PDNOVEL_KOBO,
LUMIREAD,
+ ALURATEK_COLOR,
+ BAMBOOK,
+ TREKSTOR,
ITUNES,
]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py
index 0a3945304a..54c4259678 100644
--- a/src/calibre/customize/profiles.py
+++ b/src/calibre/customize/profiles.py
@@ -696,8 +696,9 @@ class BambookOutput(OutputProfile):
short_name = 'bambook'
description = _('This profile is intended for the Sanda Bambook.')
- # Screen size is a best guess
- screen_size = (600, 800)
+ # Screen size is for full screen display
+ screen_size = (580, 780)
+ # Comic size is for normal display
comic_screen_size = (540, 700)
dpi = 168.451
fbase = 12
diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py
index 8b30631528..492b00617d 100644
--- a/src/calibre/devices/android/driver.py
+++ b/src/calibre/devices/android/driver.py
@@ -24,11 +24,11 @@ class ANDROID(USBMS):
0xc92 : [0x100], 0xc97: [0x226]},
# Eken
- 0x040d : { 0x8510 : [0x0001] },
+ 0x040d : { 0x8510 : [0x0001], 0x0851 : [0x1] },
# Motorola
0x22b8 : { 0x41d9 : [0x216], 0x2d67 : [0x100], 0x41db : [0x216],
- 0x4285 : [0x216]},
+ 0x4285 : [0x216], 0x42a3 : [0x216] },
# Sony Ericsson
0xfce : { 0xd12e : [0x0100]},
@@ -49,8 +49,9 @@ class ANDROID(USBMS):
# Dell
0x413c : { 0xb007 : [0x0100, 0x0224]},
- # Eken?
- 0x040d : { 0x0851 : [0x0001]},
+ # LG
+ 0x1004 : { 0x61cc : [0x100] },
+
}
EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books']
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to '
@@ -59,13 +60,13 @@ class ANDROID(USBMS):
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN)
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
- 'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX']
+ 'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE']
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
- 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810']
+ 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
- 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID']
+ 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD']
OSX_MAIN_MEM = 'HTC Android Phone Media'
diff --git a/src/calibre/devices/bambook/__init__.py b/src/calibre/devices/bambook/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/calibre/devices/bambook/driver.py b/src/calibre/devices/bambook/driver.py
new file mode 100644
index 0000000000..930c67a159
--- /dev/null
+++ b/src/calibre/devices/bambook/driver.py
@@ -0,0 +1,477 @@
+# -*- coding: utf-8 -*-
+
+__license__ = 'GPL v3'
+__copyright__ = '2010, Li Fanxi '
+__docformat__ = 'restructuredtext en'
+
+'''
+Device driver for Sanda's Bambook
+'''
+
+import time, os, hashlib
+from itertools import cycle
+from calibre.devices.interface import DevicePlugin
+from calibre.devices.usbms.deviceconfig import DeviceConfig
+from calibre.devices.bambook.libbambookcore import Bambook, text_encoding, CONN_CONNECTED, is_bambook_lib_ready
+from calibre.devices.usbms.books import Book, BookList
+from calibre.ebooks.metadata.book.json_codec import JsonCodec
+from calibre.ptempfile import TemporaryDirectory, TemporaryFile
+from calibre.constants import __appname__, __version__
+from calibre.devices.errors import OpenFeedback
+
+class BAMBOOK(DeviceConfig, DevicePlugin):
+ name = 'Bambook Device Interface'
+ description = _('Communicate with the Sanda Bambook eBook reader.')
+ author = _('Li Fanxi')
+ supported_platforms = ['windows', 'linux', 'osx']
+ log_packets = False
+
+ booklist_class = BookList
+ book_class = Book
+
+ FORMATS = [ "snb" ]
+ VENDOR_ID = 0x230b
+ PRODUCT_ID = 0x0001
+ BCD = None
+ CAN_SET_METADATA = False
+ THUMBNAIL_HEIGHT = 155
+
+ icon = I("devices/bambook.png")
+# OPEN_FEEDBACK_MESSAGE = _(
+# 'Connecting to Bambook device, please wait ...')
+ BACKLOADING_ERROR_MESSAGE = _(
+ 'Unable to add book to library directly from Bambook. '
+ 'Please save the book to disk and add the file to library from disk.')
+
+ METADATA_CACHE = '.calibre.bambook'
+ METADATA_FILE_GUID = 'calibremetadata.snb'
+
+ bambook = None
+
+ def reset(self, key='-1', log_packets=False, report_progress=None,
+ detected_device=None) :
+ self.open()
+
+ def open(self):
+ # Make sure the Bambook library is ready
+ if not is_bambook_lib_ready():
+ raise OpenFeedback(_("Unable to connect to Bambook, you need to install Bambook library first."))
+ # Disconnect first if connected
+ self.eject()
+ # Connect
+ self.bambook = Bambook()
+ self.bambook.Connect()
+ if self.bambook.GetState() != CONN_CONNECTED:
+ self.bambook = None
+ raise Exception(_("Unable to connect to Bambook."))
+
+ def eject(self):
+ if self.bambook:
+ self.bambook.Disconnect()
+ self.bambook = None
+
+ def post_yank_cleanup(self):
+ self.eject()
+
+ def set_progress_reporter(self, report_progress):
+ '''
+ :param report_progress: Function that is called with a % progress
+ (number between 0 and 100) for various tasks
+ If it is called with -1 that means that the
+ task does not have any progress information
+
+ '''
+ self.report_progress = report_progress
+
+ def get_device_information(self, end_session=True):
+ """
+ Ask device for device information. See L{DeviceInfoQuery}.
+
+ :return: (device name, device version, software version on device, mime type)
+
+ """
+ if self.bambook:
+ deviceInfo = self.bambook.GetDeviceInfo()
+ return (_("Bambook"), "SD928", deviceInfo.firmwareVersion, "MimeType")
+
+ def card_prefix(self, end_session=True):
+ '''
+ Return a 2 element list of the prefix to paths on the cards.
+ If no card is present None is set for the card's prefix.
+ E.G.
+ ('/place', '/place2')
+ (None, 'place2')
+ ('place', None)
+ (None, None)
+ '''
+ return (None, None)
+
+ def total_space(self, end_session=True):
+ """
+ Get total space available on the mountpoints:
+ 1. Main memory
+ 2. Memory Card A
+ 3. Memory Card B
+
+ :return: A 3 element list with total space in bytes of (1, 2, 3). If a
+ particular device doesn't have any of these locations it should return 0.
+
+ """
+ deviceInfo = self.bambook.GetDeviceInfo()
+ return (deviceInfo.deviceVolume * 1024, 0, 0)
+
+ def free_space(self, end_session=True):
+ """
+ Get free space available on the mountpoints:
+ 1. Main memory
+ 2. Card A
+ 3. Card B
+
+ :return: A 3 element list with free space in bytes of (1, 2, 3). If a
+ particular device doesn't have any of these locations it should return -1.
+
+ """
+ deviceInfo = self.bambook.GetDeviceInfo()
+ return (deviceInfo.spareVolume * 1024, -1, -1)
+
+
+ def books(self, oncard=None, end_session=True):
+ """
+ Return a list of ebooks on the device.
+
+ :param oncard: If 'carda' or 'cardb' return a list of ebooks on the
+ specific storage card, otherwise return list of ebooks
+ in main memory of device. If a card is specified and no
+ books are on the card return empty list.
+
+ :return: A BookList.
+
+ """
+ # Bambook has no memroy card
+ if oncard:
+ return self.booklist_class(None, None, None)
+
+ # Get metadata cache
+ prefix = ''
+ booklist = self.booklist_class(oncard, prefix, self.settings)
+ need_sync = self.parse_metadata_cache(booklist)
+
+ # Get book list from device
+ devicebooks = self.bambook.GetBookList()
+ books = []
+ for book in devicebooks:
+ if book.bookGuid == self.METADATA_FILE_GUID:
+ continue
+ b = self.book_class('', book.bookGuid)
+ b.title = book.bookName.decode(text_encoding)
+ b.authors = [ book.bookAuthor.decode(text_encoding) ]
+ b.size = 0
+ b.datatime = time.gmtime()
+ b.lpath = book.bookGuid
+ b.thumbnail = None
+ b.tags = None
+ b.comments = book.bookAbstract.decode(text_encoding)
+ books.append(b)
+
+ # make a dict cache of paths so the lookup in the loop below is faster.
+ bl_cache = {}
+ for idx, b in enumerate(booklist):
+ bl_cache[b.lpath] = idx
+
+ def update_booklist(book, prefix):
+ changed = False
+ try:
+ idx = bl_cache.get(book.lpath, None)
+ if idx is not None:
+ bl_cache[book.lpath] = None
+ if self.update_metadata_item(book, booklist[idx]):
+ changed = True
+ else:
+ if booklist.add_book(book,
+ replace_metadata=False):
+ changed = True
+ except: # Probably a filename encoding error
+ import traceback
+ traceback.print_exc()
+ return changed
+
+ # Check each book on device whether it has a correspondig item
+ # in metadata cache. If not, add it to cache.
+ for i, book in enumerate(books):
+ self.report_progress(i/float(len(books)), _('Getting list of books on device...'))
+ changed = update_booklist(book, prefix)
+ if changed:
+ need_sync = True
+
+ # Remove books that are no longer in the Bambook. Cache contains
+ # indices into the booklist if book not in filesystem, None otherwise
+ # Do the operation in reverse order so indices remain valid
+ for idx in sorted(bl_cache.itervalues(), reverse=True):
+ if idx is not None:
+ need_sync = True
+ del booklist[idx]
+
+ if need_sync:
+ self.sync_booklists((booklist, None, None))
+
+ self.report_progress(1.0, _('Getting list of books on device...'))
+ return booklist
+
+ def upload_books(self, files, names, on_card=None, end_session=True,
+ metadata=None):
+ '''
+ Upload a list of books to the device. If a file already
+ exists on the device, it should be replaced.
+ This method should raise a :class:`FreeSpaceError` if there is not enough
+ free space on the device. The text of the FreeSpaceError must contain the
+ word "card" if ``on_card`` is not None otherwise it must contain the word "memory".
+
+ :param files: A list of paths and/or file-like objects. If they are paths and
+ the paths point to temporary files, they may have an additional
+ attribute, original_file_path pointing to the originals. They may have
+ another optional attribute, deleted_after_upload which if True means
+ that the file pointed to by original_file_path will be deleted after
+ being uploaded to the device.
+ :param names: A list of file names that the books should have
+ once uploaded to the device. len(names) == len(files)
+ :param metadata: If not None, it is a list of :class:`Metadata` objects.
+ The idea is to use the metadata to determine where on the device to
+ put the book. len(metadata) == len(files). Apart from the regular
+ cover (path to cover), there may also be a thumbnail attribute, which should
+ be used in preference. The thumbnail attribute is of the form
+ (width, height, cover_data as jpeg).
+
+ :return: A list of 3-element tuples. The list is meant to be passed
+ to :meth:`add_books_to_metadata`.
+ '''
+ self.report_progress(0, _('Transferring books to device...'))
+ paths = []
+ if self.bambook:
+ for (i, f) in enumerate(files):
+ self.report_progress((i+1) / float(len(files)), _('Transferring books to device...'))
+ if not hasattr(f, 'read'):
+ if self.bambook.VerifySNB(f):
+ guid = self.bambook.SendFile(f, self.get_guid(metadata[i].uuid))
+ if guid:
+ paths.append(guid)
+ else:
+ print "Send fail"
+ else:
+ print "book invalid"
+ ret = zip(paths, cycle([on_card]))
+ self.report_progress(1.0, _('Transferring books to device...'))
+ return ret
+
+ def add_books_to_metadata(self, locations, metadata, booklists):
+ metadata = iter(metadata)
+ for i, location in enumerate(locations):
+ self.report_progress((i+1) / float(len(locations)), _('Adding books to device metadata listing...'))
+ info = metadata.next()
+
+ # Extract the correct prefix from the pathname. To do this correctly,
+ # we must ensure that both the prefix and the path are normalized
+ # so that the comparison will work. Book's __init__ will fix up
+ # lpath, so we don't need to worry about that here.
+
+ book = self.book_class('', location[0], other=info)
+ if book.size is None:
+ book.size = 0
+ b = booklists[0].add_book(book, replace_metadata=True)
+ if b:
+ b._new_book = True
+ self.report_progress(1.0, _('Adding books to device metadata listing...'))
+
+ def delete_books(self, paths, end_session=True):
+ '''
+ Delete books at paths on device.
+ '''
+ if self.bambook:
+ for i, path in enumerate(paths):
+ self.report_progress((i+1) / float(len(paths)), _('Removing books from device...'))
+ self.bambook.DeleteFile(path)
+ self.report_progress(1.0, _('Removing books from device...'))
+
+ def remove_books_from_metadata(self, paths, booklists):
+ '''
+ Remove books from the metadata list. This function must not communicate
+ with the device.
+
+ :param paths: paths to books on the device.
+ :param booklists: A tuple containing the result of calls to
+ (:meth:`books(oncard=None)`,
+ :meth:`books(oncard='carda')`,
+ :meth`books(oncard='cardb')`).
+
+ '''
+ for i, path in enumerate(paths):
+ self.report_progress((i+1) / float(len(paths)), _('Removing books from device metadata listing...'))
+ for bl in booklists:
+ for book in bl:
+ if book.lpath == path:
+ bl.remove_book(book)
+ self.report_progress(1.0, _('Removing books from device metadata listing...'))
+
+ def sync_booklists(self, booklists, end_session=True):
+ '''
+ Update metadata on device.
+
+ :param booklists: A tuple containing the result of calls to
+ (:meth:`books(oncard=None)`,
+ :meth:`books(oncard='carda')`,
+ :meth`books(oncard='cardb')`).
+
+ '''
+ if not self.bambook:
+ return
+
+ json_codec = JsonCodec()
+
+ # Create stub virtual book for sync info
+ with TemporaryDirectory() as tdir:
+ snbcdir = os.path.join(tdir, 'snbc')
+ snbfdir = os.path.join(tdir, 'snbf')
+ os.mkdir(snbcdir)
+ os.mkdir(snbfdir)
+
+ f = open(os.path.join(snbfdir, 'book.snbf'), 'wb')
+ f.write('''
+
+ calibre同步信息
+ calibre
+ ZH-CN
+
+ calibre
+ ''' + __appname__ + ' ' + __version__ + '''
+
+
+
+
+
+''')
+ f.close()
+ f = open(os.path.join(snbfdir, 'toc.snbf'), 'wb')
+ f.write('''
+
+ 0
+
+
+
+
+''');
+ f.close()
+ 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):
+ print "Upload failed"
+ else:
+ print "Package failed"
+
+ # Clear the _new_book indication, as we are supposed to be done with
+ # adding books at this point
+ for blist in booklists:
+ if blist is not None:
+ for book in blist:
+ book._new_book = False
+
+ self.report_progress(1.0, _('Sending metadata to device...'))
+
+ def get_file(self, path, outfile, end_session=True):
+ '''
+ Read the file at ``path`` on the device and write it to outfile.
+
+ :param outfile: file object like ``sys.stdout`` or the result of an
+ :func:`open` call.
+
+ '''
+ if self.bambook:
+ with TemporaryDirectory() as tdir:
+ if self.bambook.GetFile(path, tdir):
+ filepath = os.path.join(tdir, path)
+ f = file(filepath, 'rb')
+ outfile.write(f.read())
+ f.close()
+ else:
+ print "Unable to get file from Bambook:", path
+
+ @classmethod
+ def config_widget(cls):
+ '''
+ Should return a QWidget. The QWidget contains the settings for the device interface
+ '''
+ from calibre.gui2.device_drivers.configwidget import ConfigWidget
+ cw = ConfigWidget(cls.settings(), cls.FORMATS, cls.SUPPORTS_SUB_DIRS,
+ cls.MUST_READ_METADATA, cls.SUPPORTS_USE_AUTHOR_SORT,
+ cls.EXTRA_CUSTOMIZATION_MESSAGE)
+ # Turn off the Save template
+ cw.opt_save_template.setVisible(False)
+ cw.label.setVisible(False)
+ # Repurpose the metadata checkbox
+ cw.opt_read_metadata.setVisible(False)
+ # Repurpose the use_subdirs checkbox
+ cw.opt_use_subdirs.setVisible(False)
+ return cw
+
+
+ # @classmethod
+ # def save_settings(cls, settings_widget):
+ # '''
+ # Should save settings to disk. Takes the widget created in
+ # :meth:`config_widget` and saves all settings to disk.
+ # '''
+ # raise NotImplementedError()
+
+ # @classmethod
+ # def settings(cls):
+ # '''
+ # Should return an opts object. The opts object should have at least one attribute
+ # `format_map` which is an ordered list of formats for the device.
+ # '''
+ # raise NotImplementedError()
+
+ def parse_metadata_cache(self, bl):
+ need_sync = True
+ if not self.bambook:
+ return need_sync
+
+ # Get the metadata virtual book from Bambook
+ 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,
+ cache_name):
+ json_codec = JsonCodec()
+ if os.access(cache_name, os.R_OK):
+ try:
+ with open(cache_name, 'rb') as f:
+ json_codec.decode_from_file(f, bl, self.book_class, '')
+ need_sync = False
+ except:
+ import traceback
+ traceback.print_exc()
+ bl = []
+ return need_sync
+
+ @classmethod
+ 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
+ # changed.
+ changed = True
+ # if book.bookName.decode(text_encoding) != blb.title:
+ # changed = True
+ # if book.bookAuthor.decode(text_encoding) != blb.authors[0]:
+ # changed = True
+ # if book.bookAbstract.decode(text_encoding) != blb.comments:
+ # changed = True
+ return changed
+
+ @staticmethod
+ def get_guid(uuid):
+ guid = hashlib.md5(uuid).hexdigest()[0:15] + ".snb"
+ return guid
diff --git a/src/calibre/devices/bambook/libbambookcore.py b/src/calibre/devices/bambook/libbambookcore.py
new file mode 100644
index 0000000000..a11c5e9e87
--- /dev/null
+++ b/src/calibre/devices/bambook/libbambookcore.py
@@ -0,0 +1,530 @@
+# -*- coding: utf-8 -*-
+
+__license__ = 'GPL v3'
+__copyright__ = '2010, Li Fanxi '
+__docformat__ = 'restructuredtext en'
+
+'''
+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
+ if iswindows:
+ _lib_name = 'BambookCore'
+ if hasattr(sys, 'frozen') and iswindows:
+ lp = os.path.join(os.path.dirname(sys.executable), 'DLLs', 'BambookCore.dll')
+ lib_handle = cdll.LoadLibrary(lp)
+ elif hasattr(sys, 'frozen_path'):
+ lp = os.path.join(sys.frozen_path, 'lib', 'libBambookCore.so')
+ lib_handle = cdll.LoadLibrary(lp)
+ else:
+ lib_handle = load_library(_lib_name, cdll)
+except:
+ lib_handle = None
+
+if iswindows:
+ text_encoding = 'mbcs'
+elif islinux:
+ text_encoding = 'utf-8'
+elif isosx:
+ text_encoding = 'utf-8'
+
+def is_bambook_lib_ready():
+ return lib_handle != None
+
+# Constant
+DEFAULT_BAMBOOK_IP = '192.168.250.2'
+BAMBOOK_SDK_VERSION = 0x00090000
+BR_SUCC = 0 # 操作成功
+BR_FAIL = 1001 # 操作失败
+BR_NOT_IMPL = 1002 # 该功能还未实现
+BR_DISCONNECTED = 1003 # 与设备的连接已断开
+BR_PARAM_ERROR = 1004 # 调用函数传入的参数错误
+BR_TIMEOUT = 1005 # 操作或通讯超时
+BR_INVALID_HANDLE = 1006 # 传入的句柄无效
+BR_INVALID_FILE = 1007 # 传入的文件不存在或格式无效
+BR_INVALID_DIR = 1008 # 传入的目录不存在
+BR_BUSY = 1010 # 设备忙,另一个操作还未完成
+BR_EOF = 1011 # 文件或操作已结束
+BR_IO_ERROR = 1012 # 文件读写失败
+BR_FILE_NOT_INSIDE = 1013 # 指定的文件不在包里
+
+# 当前连接状态
+CONN_CONNECTED = 0 # 已连接
+CONN_DISCONNECTED = 1 # 未连接或连接已断开
+CONN_CONNECTING = 2 # 正在连接
+CONN_WAIT_FOR_AUTH = 3 # 已连接,正在等待身份验证(暂未实现)
+
+#传输状态
+TRANS_STATUS_TRANS = 0 #正在传输
+TRANS_STATUS_DONE = 1 #传输完成
+TRANS_STATUS_ERR = 2 #传输出错
+
+# Key Enums
+BBKeyNum0 = 0
+BBKeyNum1 = 1
+BBKeyNum2 = 2
+BBKeyNum3 = 3
+BBKeyNum4 = 4
+BBKeyNum5 = 5
+BBKeyNum6 = 6
+BBKeyNum7 = 7
+BBKeyNum8 = 8
+BBKeyNum9 = 9
+BBKeyStar = 10
+BBKeyCross = 11
+BBKeyUp = 12
+BBKeyDown = 13
+BBKeyLeft = 14
+BBKeyRight = 15
+BBKeyPageUp = 16
+BBKeyPageDown = 17
+BBKeyOK = 18
+BBKeyESC = 19
+BBKeyBookshelf = 20
+BBKeyStore = 21
+BBKeyTTS = 22
+BBKeyMenu = 23
+BBKeyInteract =24
+
+class DeviceInfo(ctypes.Structure):
+ _fields_ = [ ("cbSize", ctypes.c_int),
+ ("sn", ctypes.c_char * 20),
+ ("firmwareVersion", ctypes.c_char * 20),
+ ("deviceVolume", ctypes.c_int),
+ ("spareVolume", ctypes.c_int),
+ ]
+ def __init__(self):
+ self.cbSize = ctypes.sizeof(self)
+
+class PrivBookInfo(ctypes.Structure):
+ _fields_ = [ ("cbSize", ctypes.c_int),
+ ("bookGuid", ctypes.c_char * 20),
+ ("bookName", ctypes.c_char * 80),
+ ("bookAuthor", ctypes.c_char * 40),
+ ("bookAbstract", ctypes.c_char * 256),
+ ]
+ def Clone(self):
+ bookInfo = PrivBookInfo()
+ bookInfo.cbSize = self.cbSize
+ bookInfo.bookGuid = self.bookGuid
+ bookInfo.bookName = self.bookName
+ bookInfo.bookAuthor = self.bookAuthor
+ bookInfo.bookAbstract = self.bookAbstract
+ return bookInfo
+
+ def __init__(self):
+ self.cbSize = ctypes.sizeof(self)
+
+# extern "C"_declspec(dllexport) BB_RESULT BambookConnect(const char* lpszIP, int timeOut, BB_HANDLE* hConn);
+def BambookConnect(ip = DEFAULT_BAMBOOK_IP, timeout = 0):
+ if isinstance(ip, unicode):
+ ip = ip.encode('ascii')
+ handle = ctypes.c_void_p(0)
+ if lib_handle == None:
+ raise Exception(_('Bambook SDK has not been installed.'))
+ ret = lib_handle.BambookConnect(ip, timeout, ctypes.byref(handle))
+ if ret == BR_SUCC:
+ return handle
+ else:
+ return None
+
+# extern "C" _declspec(dllexport) BB_RESULT BambookGetConnectStatus(BB_HANDLE hConn, int* status);
+def BambookGetConnectStatus(handle):
+ status = ctypes.c_int(0)
+ ret = lib_handle.BambookGetConnectStatus(handle, ctypes.byref(status))
+ if ret == BR_SUCC:
+ return status.value
+ else:
+ return None
+
+# extern "C" _declspec(dllexport) BB_RESULT BambookDisconnect(BB_HANDLE hConn);
+def BambookDisconnect(handle):
+ ret = lib_handle.BambookDisconnect(handle)
+ if ret == BR_SUCC:
+ return True
+ else:
+ return False
+
+# extern "C" const char * BambookGetErrorString(BB_RESULT nCode)
+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():
+ version = ctypes.c_int(0)
+ lib_handle.BambookGetSDKVersion(ctypes.byref(version))
+ return version.value
+
+# extern "C" BB_RESULT BambookGetDeviceInfo(BB_HANDLE hConn, DeviceInfo* pInfo);
+def BambookGetDeviceInfo(handle):
+ deviceInfo = DeviceInfo()
+ ret = lib_handle.BambookGetDeviceInfo(handle, ctypes.byref(deviceInfo))
+ if ret == BR_SUCC:
+ return deviceInfo
+ else:
+ return None
+
+
+# extern "C" BB_RESULT BambookKeyPress(BB_HANDLE hConn, BambookKey key);
+def BambookKeyPress(handle, key):
+ ret = lib_handle.BambookKeyPress(handle, key)
+ if ret == BR_SUCC:
+ return True
+ else:
+ return False
+
+# extern "C" BB_RESULT BambookGetFirstPrivBookInfo(BB_HANDLE hConn, PrivBookInfo * pInfo);
+def BambookGetFirstPrivBookInfo(handle, bookInfo):
+ bookInfo.contents.cbSize = ctypes.sizeof(bookInfo.contents)
+ ret = lib_handle.BambookGetFirstPrivBookInfo(handle, bookInfo)
+ if ret == BR_SUCC:
+ return True
+ else:
+ return False
+
+# extern "C" BB_RESULT BambookGetNextPrivBookInfo(BB_HANDLE hConn, PrivBookInfo * pInfo);
+def BambookGetNextPrivBookInfo(handle, bookInfo):
+ bookInfo.contents.cbSize = ctypes.sizeof(bookInfo.contents)
+ ret = lib_handle.BambookGetNextPrivBookInfo(handle, bookInfo)
+ if ret == BR_SUCC:
+ return True
+ elif ret == BR_EOF:
+ return False
+ else:
+ return False
+
+# extern "C" BB_RESULT BambookDeletePrivBook(BB_HANDLE hConn, const char * lpszBookID);
+def BambookDeletePrivBook(handle, guid):
+ if isinstance(guid, unicode):
+ guid = guid.encode('ascii')
+ ret = lib_handle.BambookDeletePrivBook(handle, guid)
+ if ret == BR_SUCC:
+ return True
+ else:
+ return False
+
+class JobQueue:
+ jobs = {}
+ maxID = 0
+ lock = Lock()
+ def __init__(self):
+ self.maxID = 0
+
+ def NewJob(self):
+ self.lock.acquire()
+ self.maxID = self.maxID + 1
+ maxid = self.maxID
+ self.lock.release()
+ event = Event()
+ self.jobs[maxid] = (event, TRANS_STATUS_TRANS)
+ return maxid
+
+ def FinishJob(self, jobID, status):
+ self.jobs[jobID] = (self.jobs[jobID][0], status)
+ self.jobs[jobID][0].set()
+
+ def WaitJob(self, jobID):
+ self.jobs[jobID][0].wait()
+ return (self.jobs[jobID][1] == TRANS_STATUS_DONE)
+
+ def DeleteJob(self, jobID):
+ del self.jobs[jobID]
+
+job = JobQueue()
+
+def BambookTransferCallback(status, progress, userData):
+ if status == TRANS_STATUS_DONE and progress == 100:
+ job.FinishJob(userData, status)
+ elif status == TRANS_STATUS_ERR:
+ job.FinishJob(userData, status)
+
+TransCallback = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int, ctypes.c_int)
+bambookTransferCallback = TransCallback(BambookTransferCallback)
+
+# extern "C" BB_RESULT BambookAddPrivBook(BB_HANDLE hConn, const char * pszSnbFile,
+# TransCallback pCallbackFunc, intptr_t userData);
+def BambookAddPrivBook(handle, filename, callback, userData):
+ if isinstance(filename, unicode):
+ filename = filename.encode('ascii')
+ ret = lib_handle.BambookAddPrivBook(handle, filename, callback, userData)
+ if ret == BR_SUCC:
+ return True
+ else:
+ return False
+
+# extern "C" BB_RESULT BambookReplacePrivBook(BB_HANDLE hConn, const char *
+# pszSnbFile, const char * lpszBookID, TransCallback pCallbackFunc, intptr_t userData);
+def BambookReplacePrivBook(handle, filename, bookID, callback, userData):
+ if isinstance(filename, unicode):
+ filename = filename.encode('ascii')
+ if isinstance(bookID, unicode):
+ bookID = bookID.encode('ascii')
+ ret = lib_handle.BambookReplacePrivBook(handle, filename, bookID, callback, userData)
+ if ret == BR_SUCC:
+ 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):
+ if isinstance(filename, unicode):
+ filename = filename.encode('ascii')
+ if isinstance(bookID, unicode):
+ bookID = bookID.encode('ascii')
+ ret = lib_handle.BambookFetchPrivBook(handle, bookID, filename, bambookTransferCallback, userData)
+ if ret == BR_SUCC:
+ return True
+ else:
+ return False
+
+# extern "C" BB_RESULT BambookVerifySnbFile(const char * snbName)
+def BambookVerifySnbFile(filename):
+ if isinstance(filename, unicode):
+ filename = filename.encode('ascii')
+ if lib_handle.BambookVerifySnbFile(filename) == BR_SUCC:
+ return True
+ else:
+ return False
+
+# BB_RESULT BambookPackSnbFromDir ( const char * snbName,, const char * rootDir );
+def BambookPackSnbFromDir(snbFileName, rootDir):
+ if isinstance(snbFileName, unicode):
+ snbFileName = snbFileName.encode('ascii')
+ if isinstance(rootDir, unicode):
+ rootDir = rootDir.encode('ascii')
+ ret = lib_handle.BambookPackSnbFromDir(snbFileName, rootDir)
+ if ret == BR_SUCC:
+ return True
+ else:
+ return False
+
+# BB_RESULT BambookUnpackFileFromSnb ( const char * snbName,, const char * relativePath, const char * outfname );
+def BambookUnpackFileFromSnb(snbFileName, relPath, outFileName):
+ if isinstance(snbFileName, unicode):
+ snbFileName = snbFileName.encode('ascii')
+ if isinstance(relPath, unicode):
+ relPath = relPath.encode('ascii')
+ if isinstance(outFileName, unicode):
+ outFileName = outFileName.encode('ascii')
+ ret = lib_handle.BambookUnpackFileFromSnb(snbFileName, relPath, outFileName)
+ if ret == BR_SUCC:
+ return True
+ else:
+ return False
+
+class Bambook:
+ def __init__(self):
+ self.handle = None
+
+ def Connect(self, ip = DEFAULT_BAMBOOK_IP, timeout = 10000):
+ self.handle = BambookConnect(ip, timeout)
+ if self.handle and self.handle != 0:
+ return True
+ else:
+ return False
+
+ def Disconnect(self):
+ if self.handle:
+ return BambookDisconnect(self.handle)
+ return False
+
+ def GetState(self):
+ if self.handle:
+ return BambookGetConnectStatus(self.handle)
+ return CONN_DISCONNECTED
+
+ def GetDeviceInfo(self):
+ if self.handle:
+ return BambookGetDeviceInfo(self.handle)
+ return None
+
+ def SendFile(self, fileName, guid = None):
+ if self.handle:
+ taskID = job.NewJob()
+ if guid:
+ if BambookReplacePrivBook(self.handle, fileName, guid,
+ bambookTransferCallback, taskID):
+ if(job.WaitJob(taskID)):
+ job.DeleteJob(taskID)
+ return guid
+ else:
+ job.DeleteJob(taskID)
+ return None
+ else:
+ job.DeleteJob(taskID)
+ return None
+ else:
+ guid = hashlib.md5(str(uuid.uuid4())).hexdigest()[0:15] + ".snb"
+ if BambookReplacePrivBook(self.handle, fileName, guid,
+ bambookTransferCallback, taskID):
+ if job.WaitJob(taskID):
+ job.DeleteJob(taskID)
+ return guid
+ else:
+ job.DeleteJob(taskID)
+ return None
+ else:
+ job.DeleteJob(taskID)
+ return None
+ return False
+
+ def GetFile(self, guid, fileName):
+ if self.handle:
+ taskID = job.NewJob()
+ ret = BambookFetchPrivBook(self.handle, guid, fileName, bambookTransferCallback, taskID)
+ if ret:
+ ret = job.WaitJob(taskID)
+ job.DeleteJob(taskID)
+ return ret
+ else:
+ job.DeleteJob(taskID)
+ return False
+ return False
+
+ def DeleteFile(self, guid):
+ if self.handle:
+ ret = BambookDeletePrivBook(self.handle, guid)
+ return ret
+ return False
+
+ def GetBookList(self):
+ if self.handle:
+ 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()
+
+ @staticmethod
+ def VerifySNB(fileName):
+ return BambookVerifySnbFile(fileName);
+
+ @staticmethod
+ def ExtractSNBContent(fileName, relPath, path):
+ return BambookUnpackFileFromSnb(fileName, relPath, path)
+
+ @staticmethod
+ def ExtractSNB(fileName, path):
+ ret = BambookUnpackFileFromSnb(fileName, 'snbf/book.snbf', path + '/snbf/book.snbf')
+ if not ret:
+ return False
+ 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"
+
+def failed():
+ print "> Failed"
+
+if __name__ == "__main__":
+
+ print "Bambook SDK Unit Test"
+ bb = Bambook()
+
+ print "Disconnect State"
+ if bb.GetState() == CONN_DISCONNECTED:
+ passed()
+ else:
+ failed()
+
+ print "Get SDK Version"
+ if bb.GetSDKVersion() == BAMBOOK_SDK_VERSION:
+ passed()
+ else:
+ failed()
+
+ print "Verify good SNB File"
+ if bb.VerifySNB(u'/tmp/f8268e6c1f4e78c.snb'):
+ passed()
+ else:
+ failed()
+
+ print "Verify bad SNB File"
+ if not bb.VerifySNB('./libwrapper.py'):
+ 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()
+ else:
+ failed()
+
+ print "Connect to Bambook"
+ if bb.Connect('192.168.250.2', 10000) and bb.GetState() == CONN_CONNECTED:
+ passed()
+ else:
+ failed()
+
+ print "Get Bambook Info"
+ devInfo = bb.GetDeviceInfo()
+ if devInfo:
+# print "Info Size: ", devInfo.cbSize
+# print "SN: ", devInfo.sn
+# print "Firmware: ", devInfo.firmwareVersion
+# print "Capacity: ", devInfo.deviceVolume
+# print "Free: ", devInfo.spareVolume
+ if devInfo.cbSize == 52 and devInfo.deviceVolume == 1714232:
+ passed()
+ else:
+ failed()
+
+ print "Send file"
+ if bb.SendFile('/tmp/tmp.snb'):
+ passed()
+ else:
+ failed()
+
+ print "Get book list"
+ books = bb.GetBookList()
+ if len(books) > 10:
+ passed()
+ else:
+ failed()
+
+ print "Get book"
+ if bb.GetFile('f8268e6c1f4e78c.snb', '/tmp') and bb.VerifySNB('/tmp/f8268e6c1f4e78c.snb'):
+ passed()
+ else:
+ failed()
+
+ print "Disconnect"
+ if bb.Disconnect():
+ passed()
+ else:
+ failed()
diff --git a/src/calibre/devices/eb600/driver.py b/src/calibre/devices/eb600/driver.py
index bc8b87533c..246b753fa8 100644
--- a/src/calibre/devices/eb600/driver.py
+++ b/src/calibre/devices/eb600/driver.py
@@ -266,3 +266,12 @@ class POCKETBOOK701(USBMS):
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/misc.py b/src/calibre/devices/misc.py
index e27aee4393..a895948316 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,43 @@ 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'
+
+class TREKSTOR(USBMS):
+
+ name = 'Trekstor E-book player device interface'
+ gui_name = 'Trekstor'
+ description = _('Communicate with the Trekstor')
+ author = 'Kovid Goyal'
+ supported_platforms = ['windows', 'osx', 'linux']
+
+ # Ordered list of supported formats
+ FORMATS = ['epub', 'txt', 'pdf']
+
+ VENDOR_ID = [0x1e68]
+ PRODUCT_ID = [0x0041]
+ BCD = [0x0002]
+
+ EBOOK_DIR_MAIN = 'Ebooks'
+
+ VENDOR_NAME = 'TREKSTOR'
+ WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_PLAYER_7'
+
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/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/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/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/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/ebooks/snb/input.py b/src/calibre/ebooks/snb/input.py
index 052db6d059..659ca79619 100755
--- a/src/calibre/ebooks/snb/input.py
+++ b/src/calibre/ebooks/snb/input.py
@@ -62,7 +62,7 @@ class SNBInput(InputFormatPlugin):
oeb.uid = oeb.metadata.identifier[0]
break
- with TemporaryDirectory('_chm2oeb', keep=True) as tdir:
+ with TemporaryDirectory('_snb2oeb', keep=True) as tdir:
log.debug('Process TOC ...')
toc = snbFile.GetFileStream('snbf/toc.snbf')
oeb.container = DirContainer(tdir, log)
@@ -74,17 +74,18 @@ class SNBInput(InputFormatPlugin):
chapterSrc = ch.get('src')
fname = 'ch_%d.htm' % i
data = snbFile.GetFileStream('snbc/' + chapterSrc)
- if data != None:
- snbc = etree.fromstring(data)
- outputFile = open(os.path.join(tdir, fname), 'wb')
- lines = []
- for line in snbc.find('.//body'):
- if line.tag == 'text':
- lines.append(u'
' % html_encode(line.text))
+ outputFile.write((HTML_TEMPLATE % (chapterName, u'\n'.join(lines))).encode('utf-8', 'replace'))
+ outputFile.close()
oeb.toc.add(ch.text, fname)
id, href = oeb.manifest.generate(id='html',
href=ascii_filename(fname))
diff --git a/src/calibre/ebooks/snb/output.py b/src/calibre/ebooks/snb/output.py
index fa8442391b..07a0460c57 100644
--- a/src/calibre/ebooks/snb/output.py
+++ b/src/calibre/ebooks/snb/output.py
@@ -35,14 +35,17 @@ class SNBOutput(OutputFormatPlugin):
recommended_value=False, level=OptionRecommendation.LOW,
help=_('Specify whether or not to insert an empty line between '
'two paragraphs.')),
- OptionRecommendation(name='snb_indent_first_line',
- recommended_value=True, level=OptionRecommendation.LOW,
+ OptionRecommendation(name='snb_dont_indent_first_line',
+ recommended_value=False, level=OptionRecommendation.LOW,
help=_('Specify whether or not to insert two space characters '
'to indent the first line of each paragraph.')),
OptionRecommendation(name='snb_hide_chapter_name',
recommended_value=False, level=OptionRecommendation.LOW,
help=_('Specify whether or not to hide the chapter title for each '
'chapter. Useful for image-only output (eg. comics).')),
+ OptionRecommendation(name='snb_full_screen',
+ recommended_value=False, level=OptionRecommendation.LOW,
+ help=_('Resize all the images for full screen view. ')),
])
def convert(self, oeb_book, output_path, input_plugin, opts, log):
@@ -228,7 +231,10 @@ class SNBOutput(OutputFormatPlugin):
img.load(imageData)
(x,y) = img.size
if self.opts:
- SCREEN_X, SCREEN_Y = self.opts.output_profile.comic_screen_size
+ if self.opts.snb_full_screen:
+ SCREEN_X, SCREEN_Y = self.opts.output_profile.screen_size
+ else:
+ SCREEN_X, SCREEN_Y = self.opts.output_profile.comic_screen_size
else:
SCREEN_X = 540
SCREEN_Y = 700
diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py
index 7c16eb5d90..078e7ebe76 100644
--- a/src/calibre/ebooks/snb/snbml.py
+++ b/src/calibre/ebooks/snb/snbml.py
@@ -121,7 +121,7 @@ class SNBMLizer(object):
subitem = line[len(CALIBRE_SNB_BM_TAG):]
bodyTree = trees[subitem].find(".//body")
else:
- if self.opts and self.opts.snb_indent_first_line:
+ if self.opts and not self.opts.snb_dont_indent_first_line:
prefix = u'\u3000\u3000'
else:
prefix = u''
diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index a68372f650..1c99d9d9d5 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -83,7 +83,7 @@ def _config():
c.add_opt('LRF_ebook_viewer_options', default=None,
help=_('Options for the LRF ebook viewer'))
c.add_opt('internally_viewed_formats', default=['LRF', 'EPUB', 'LIT',
- 'MOBI', 'PRC', 'HTML', 'FB2', 'PDB', 'RB'],
+ 'MOBI', 'PRC', 'HTML', 'FB2', 'PDB', 'RB', 'SNB'],
help=_('Formats that are viewed using the internal viewer'))
c.add_opt('column_map', default=ALL_COLUMNS,
help=_('Columns to be displayed in the book list'))
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('
'+_('The selected books will be '
'permanently deleted '
diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py
index b7394b9dd1..a3caa82e4b 100644
--- a/src/calibre/gui2/book_details.py
+++ b/src/calibre/gui2/book_details.py
@@ -249,7 +249,7 @@ class BookInfo(QWebView):
left_pane = u'
'%body.text)
elems += [html.tostring(x, encoding=unicode) for x in body if
- x.tag != 'script']
+ x.tag not in ('script', 'style')]
+
if len(elems) > 1:
ans = u'