merge from trunk

This commit is contained in:
Lee 2011-05-05 11:21:11 +08:00
commit 83ea916791
29 changed files with 506 additions and 483 deletions

View File

@ -12,7 +12,6 @@ class AdvancedUserRecipe1301860159(BasicNewsRecipe):
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
language = 'en_EN'
remove_javascript = True remove_javascript = True
keep_only_tags = [dict(name='div', attrs={'class':'modSectionTd2'})] keep_only_tags = [dict(name='div', attrs={'class':'modSectionTd2'})]
remove_tags = [dict(name='a'),dict(name='hr')] remove_tags = [dict(name='a'),dict(name='hr')]

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = u'2010, Tomasz Dlugosz <tomek3d@gmail.com>' __copyright__ = u'2010-2011, Tomasz Dlugosz <tomek3d@gmail.com>'
''' '''
frazpc.pl frazpc.pl
''' '''
@ -19,17 +19,20 @@ class FrazPC(BasicNewsRecipe):
use_embedded_content = False use_embedded_content = False
no_stylesheets = True no_stylesheets = True
feeds = [(u'Aktualno\u015bci', u'http://www.frazpc.pl/feed'), (u'Recenzje', u'http://www.frazpc.pl/kat/recenzje-2/feed') ] feeds = [
(u'Aktualno\u015bci', u'http://www.frazpc.pl/feed/aktualnosci'),
keep_only_tags = [dict(name='div', attrs={'id':'FRAZ_CONTENT'})] (u'Artyku\u0142y', u'http://www.frazpc.pl/feed/artykuly')
remove_tags = [dict(name='p', attrs={'class':'gray tagsP fs11'})]
preprocess_regexps = [
(re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
[(r'<div id="post-[0-9]*"', lambda match: '<div id="FRAZ_CONTENT"'),
(r'href="/f/news/', lambda match: 'href="http://www.frazpc.pl/f/news/'),
(r' &nbsp; <a href="http://www.frazpc.pl/[^>]*?">(Skomentuj|Komentarz(e)?\([0-9]*\))</a>&nbsp; \|', lambda match: '')]
] ]
keep_only_tags = [dict(name='div', attrs={'class':'article'})]
remove_tags = [
dict(name='div', attrs={'class':'title-wrapper'}),
dict(name='p', attrs={'class':'tags'}),
dict(name='p', attrs={'class':'article-links'}),
dict(name='div', attrs={'class':'comments_box'})
]
preprocess_regexps = [(re.compile(r'\| <a href="#comments">Komentarze \([0-9]*\)</a>'), lambda match: '')]
remove_attributes = [ 'width', 'height' ] remove_attributes = [ 'width', 'height' ]

View File

@ -18,7 +18,7 @@ class TelepolisNews(BasicNewsRecipe):
recursion = 0 recursion = 0
no_stylesheets = True no_stylesheets = True
encoding = "utf-8" encoding = "utf-8"
language = 'de_AT' language = 'de'
use_embedded_content =False use_embedded_content =False
remove_empty_feeds = True remove_empty_feeds = True

View File

@ -7,13 +7,11 @@ usatoday.com
''' '''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, NavigableString, Tag
import re
class USAToday(BasicNewsRecipe): class USAToday(BasicNewsRecipe):
title = 'USA Today' title = 'USA Today'
__author__ = 'GRiker' __author__ = 'Kovid Goyal'
oldest_article = 1 oldest_article = 1
timefmt = '' timefmt = ''
max_articles_per_feed = 20 max_articles_per_feed = 20
@ -31,7 +29,6 @@ class USAToday(BasicNewsRecipe):
margin-bottom: 0em; \ margin-bottom: 0em; \
font-size: smaller;}\n \ font-size: smaller;}\n \
.articleBody {text-align: left;}\n ' .articleBody {text-align: left;}\n '
conversion_options = { 'linearize_tables' : True }
#simultaneous_downloads = 1 #simultaneous_downloads = 1
feeds = [ feeds = [
('Top Headlines', 'http://rssfeeds.usatoday.com/usatoday-NewsTopStories'), ('Top Headlines', 'http://rssfeeds.usatoday.com/usatoday-NewsTopStories'),
@ -47,63 +44,26 @@ class USAToday(BasicNewsRecipe):
('Most Popular', 'http://rssfeeds.usatoday.com/Usatoday-MostViewedArticles'), ('Most Popular', 'http://rssfeeds.usatoday.com/Usatoday-MostViewedArticles'),
('Offbeat News', 'http://rssfeeds.usatoday.com/UsatodaycomOffbeat-TopStories'), ('Offbeat News', 'http://rssfeeds.usatoday.com/UsatodaycomOffbeat-TopStories'),
] ]
keep_only_tags = [dict(attrs={'class':[ keep_only_tags = [dict(attrs={'class':'story'})]
'byLine', remove_tags = [
'inside-copy', dict(attrs={'class':[
'inside-head', 'share',
'inside-head2', 'reprints',
'item', 'inline-h3',
'item-block', 'info-extras',
'photo-container', 'ppy-outer',
]}), 'ppy-caption',
dict(id=[ 'comments',
'applyMainStoryPhoto', 'jump',
'permalink', 'pagetools',
])] 'post-attributes',
'tags',
'bottom-tools',
'sponsoredlinks',
]}),
dict(id=['pluck']),
]
remove_tags = [dict(attrs={'class':[
'comments',
'jump',
'pagetools',
'post-attributes',
'tags',
]}),
dict(id=[])]
#feeds = [('Most Popular', 'http://rssfeeds.usatoday.com/Usatoday-MostViewedArticles')]
def dump_hex(self, src, length=16):
''' Diagnostic '''
FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)])
N=0; result=''
while src:
s,src = src[:length],src[length:]
hexa = ' '.join(["%02X"%ord(x) for x in s])
s = s.translate(FILTER)
result += "%04X %-*s %s\n" % (N, length*3, hexa, s)
N+=length
print result
def fixChars(self,string):
# Replace lsquo (\x91)
fixed = re.sub("\x91","&#8216;",string)
# Replace rsquo (\x92)
fixed = re.sub("\x92","&#8217;",fixed)
# Replace ldquo (\x93)
fixed = re.sub("\x93","&#8220;",fixed)
# Replace rdquo (\x94)
fixed = re.sub("\x94","&#8221;",fixed)
# Replace ndash (\x96)
fixed = re.sub("\x96","&#8211;",fixed)
# Replace mdash (\x97)
fixed = re.sub("\x97","&#8212;",fixed)
return fixed
def get_masthead_url(self): def get_masthead_url(self):
masthead = 'http://i.usatoday.net/mobile/_common/_images/565x73_usat_mobile.gif' masthead = 'http://i.usatoday.net/mobile/_common/_images/565x73_usat_mobile.gif'
@ -115,321 +75,4 @@ class USAToday(BasicNewsRecipe):
masthead = None masthead = None
return masthead return masthead
def massageNCXText(self, description):
# Kindle TOC descriptions won't render certain characters
if description:
massaged = unicode(BeautifulStoneSoup(description, convertEntities=BeautifulStoneSoup.HTML_ENTITIES))
# Replace '&' with '&#38;'
massaged = re.sub("&","&#38;", massaged)
return self.fixChars(massaged)
else:
return description
def parse_feeds(self, *args, **kwargs):
parsed_feeds = BasicNewsRecipe.parse_feeds(self, *args, **kwargs)
# Count articles for progress dialog
article_count = 0
for feed in parsed_feeds:
article_count += len(feed)
self.log( "Queued %d articles" % article_count)
return parsed_feeds
def preprocess_html(self, soup):
soup = self.strip_anchors(soup)
return soup
def postprocess_html(self, soup, first_fetch):
# Remove navLinks <div class="inside-copy" style="padding-bottom:3px">
navLinks = soup.find(True,{'style':'padding-bottom:3px'})
if navLinks:
navLinks.extract()
# Remove <div class="inside-copy" style="margin-bottom:10px">
gibberish = soup.find(True,{'style':'margin-bottom:10px'})
if gibberish:
gibberish.extract()
# Change <inside-head> to <h2>
headline = soup.find(True, {'class':['inside-head','inside-head2']})
if not headline:
headline = soup.find('h3')
if headline:
tag = Tag(soup, "h2")
tag['class'] = "headline"
tag.insert(0, headline.contents[0])
headline.replaceWith(tag)
else:
print "unable to find headline:\n%s\n" % soup
# Change byLine to byline, change commas to middot
# Kindle renders commas in byline as '&'
byline = soup.find(True, {'class':'byLine'})
if byline:
byline['class'] = 'byline'
# Replace comma with middot
byline.contents[0].replaceWith(re.sub(","," &middot;", byline.renderContents()))
jumpout_punc_list = [':','?']
# Remove the inline jumpouts in <div class="inside-copy">
paras = soup.findAll(True, {'class':'inside-copy'})
for para in paras:
if re.match("<b>[\w\W]+ ",para.renderContents()):
p = para.find('b')
for punc in jumpout_punc_list:
punc_offset = p.contents[0].find(punc)
if punc_offset == -1:
continue
if punc_offset > 1:
if p.contents[0][:punc_offset] == p.contents[0][:punc_offset].upper():
#print "extracting \n%s\n" % para.prettify()
para.extract()
# Reset class for remaining
paras = soup.findAll(True, {'class':'inside-copy'})
for para in paras:
para['class'] = 'articleBody'
# Remove inline jumpouts in <p>
paras = soup.findAll(['p'])
for p in paras:
if hasattr(p,'contents') and len(p.contents):
for punc in jumpout_punc_list:
punc_offset = p.contents[0].find(punc)
if punc_offset == -1:
continue
if punc_offset > 2 and hasattr(p,'a') and len(p.contents):
#print "evaluating %s\n" % p.contents[0][:punc_offset+1]
if p.contents[0][:punc_offset] == p.contents[0][:punc_offset].upper():
#print "extracting \n%s\n" % p.prettify()
p.extract()
# Capture the first img, insert after headline
imgs = soup.findAll('img')
print "postprocess_html(): %d images" % len(imgs)
if imgs:
divTag = Tag(soup, 'div')
divTag['class'] = 'image'
body = soup.find('body')
img = imgs[0]
#print "img: \n%s\n" % img.prettify()
# Table for photo and credit
tableTag = Tag(soup,'table')
# Photo
trimgTag = Tag(soup, 'tr')
tdimgTag = Tag(soup, 'td')
tdimgTag.insert(0,img)
trimgTag.insert(0,tdimgTag)
tableTag.insert(0,trimgTag)
# Credit
trcreditTag = Tag(soup, 'tr')
tdcreditTag = Tag(soup, 'td')
tdcreditTag['class'] = 'credit'
credit = soup.find('td',{'class':'photoCredit'})
if credit:
tdcreditTag.insert(0,NavigableString(credit.renderContents()))
else:
credit = img['credit']
if credit:
tdcreditTag.insert(0,NavigableString(credit))
else:
tdcreditTag.insert(0,NavigableString(''))
trcreditTag.insert(0,tdcreditTag)
tableTag.insert(1,trcreditTag)
dtc = 0
divTag.insert(dtc,tableTag)
dtc += 1
if False:
# Add the caption in the table
tableCaptionTag = Tag(soup,'caption')
tableCaptionTag.insert(0,soup.find('td',{'class':'photoCredit'}).renderContents())
tableTag.insert(1,tableCaptionTag)
divTag.insert(dtc,tableTag)
dtc += 1
body.insert(1,divTag)
else:
# Add the caption below the table
#print "Looking for caption in this soup:\n%s" % img.prettify()
captionTag = Tag(soup,'p')
captionTag['class'] = 'caption'
if hasattr(img,'alt') and img['alt']:
captionTag.insert(0,NavigableString('<blockquote>%s</blockquote>' % img['alt']))
divTag.insert(dtc, captionTag)
dtc += 1
else:
try:
captionTag.insert(0,NavigableString('<blockquote>%s</blockquote>' % img['cutline']))
divTag.insert(dtc, captionTag)
dtc += 1
except:
pass
hrTag = Tag(soup, 'hr')
divTag.insert(dtc, hrTag)
dtc += 1
# Delete <div id="applyMainStoryPhoto"
photoJunk = soup.find('div',{'id':'applyMainStoryPhoto'})
if photoJunk:
photoJunk.extract()
# Insert img after headline
tag = body.find(True)
insertLoc = 0
headline_found = False
while True:
# Scan the top-level tags
insertLoc += 1
if hasattr(tag,'class') and tag['class'] == 'headline':
headline_found = True
body.insert(insertLoc,divTag)
break
tag = tag.nextSibling
if not tag:
break
if not headline_found:
# Monolithic <div> - restructure
tag = body.find(True)
while True:
insertLoc += 1
try:
if hasattr(tag,'class') and tag['class'] == 'headline':
headline_found = True
tag.insert(insertLoc,divTag)
break
except:
pass
tag = tag.next
if not tag:
break
# Yank out headline, img and caption
headline = body.find('h2','headline')
img = body.find('div','image')
caption = body.find('p''class')
# body(0) is calibre_navbar
# body(1) is <div class="item">
btc = 1
headline.extract()
body.insert(1, headline)
btc += 1
if img:
img.extract()
body.insert(btc, img)
btc += 1
if caption:
caption.extract()
body.insert(btc, caption)
btc += 1
if len(imgs) > 1:
if True:
[img.extract() for img in imgs[1:]]
else:
# Format the remaining images
# This doesn't work yet
for img in imgs[1:]:
print "img:\n%s\n" % img.prettify()
divTag = Tag(soup, 'div')
divTag['class'] = 'image'
# Table for photo and credit
tableTag = Tag(soup,'table')
# Photo
trimgTag = Tag(soup, 'tr')
tdimgTag = Tag(soup, 'td')
tdimgTag.insert(0,img)
trimgTag.insert(0,tdimgTag)
tableTag.insert(0,trimgTag)
# Credit
trcreditTag = Tag(soup, 'tr')
tdcreditTag = Tag(soup, 'td')
tdcreditTag['class'] = 'credit'
try:
tdcreditTag.insert(0,NavigableString(img['credit']))
except:
tdcreditTag.insert(0,NavigableString(''))
trcreditTag.insert(0,tdcreditTag)
tableTag.insert(1,trcreditTag)
divTag.insert(0,tableTag)
soup.img.replaceWith(divTag)
return soup
def postprocess_book(self, oeb, opts, log) :
def extract_byline(href) :
# <meta name="byline" content=
soup = BeautifulSoup(str(oeb.manifest.hrefs[href]))
byline = soup.find('div',attrs={'class':'byline'})
if byline:
byline['class'] = 'byline'
# Replace comma with middot
byline.contents[0].replaceWith(re.sub(u",", u" &middot;",
byline.renderContents(encoding=None)))
return byline.renderContents(encoding=None)
else :
paras = soup.findAll(text=True)
for para in paras:
if para.startswith("Copyright"):
return para[len('Copyright xxxx '):para.find('.')]
return None
def extract_description(href) :
soup = BeautifulSoup(str(oeb.manifest.hrefs[href]))
description = soup.find('meta',attrs={'name':'description'})
if description :
return self.massageNCXText(description['content'])
else:
# Take first paragraph of article
articleBody = soup.find('div',attrs={'id':['articleBody','item']})
if articleBody:
paras = articleBody.findAll('p')
for p in paras:
if p.renderContents() > '' :
return self.massageNCXText(self.tag_to_string(p,use_alt=False))
else:
print "Didn't find <div id='articleBody'> in this soup:\n%s" % soup.prettify()
return None
# Method entry point here
# Single section toc looks different than multi-section tocs
if oeb.toc.depth() == 2 :
for article in oeb.toc :
if article.author is None :
article.author = extract_byline(article.href)
if article.description is None :
article.description = extract_description(article.href)
elif oeb.toc.depth() == 3 :
for section in oeb.toc :
for article in section :
article.author = extract_byline(article.href)
'''
if article.author is None :
article.author = self.massageNCXText(extract_byline(article.href))
else:
article.author = self.massageNCXText(article.author)
'''
if article.description is None :
article.description = extract_description(article.href)
def strip_anchors(self,soup):
paras = soup.findAll(True)
for para in paras:
aTags = para.findAll('a')
for a in aTags:
if a.img is None:
a.replaceWith(a.renderContents().decode('cp1252','replace'))
return soup

View File

@ -607,6 +607,7 @@ class StoreBase(Plugin): # {{{
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
author = 'John Schember' author = 'John Schember'
type = _('Store') type = _('Store')
minimum_calibre_version = (0, 7, 59)
actual_plugin = None actual_plugin = None

View File

@ -613,6 +613,7 @@ from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, \
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
from calibre.devices.kobo.driver import KOBO from calibre.devices.kobo.driver import KOBO
from calibre.devices.bambook.driver import BAMBOOK from calibre.devices.bambook.driver import BAMBOOK
from calibre.devices.boeye.driver import BOEYE_BEX, BOEYE_BDX
from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX
from calibre.ebooks.epub.fix.unmanifested import Unmanifested from calibre.ebooks.epub.fix.unmanifested import Unmanifested
@ -743,6 +744,8 @@ plugins += [
EEEREADER, EEEREADER,
NEXTBOOK, NEXTBOOK,
ITUNES, ITUNES,
BOEYE_BEX,
BOEYE_BDX,
USER_DEFINED, USER_DEFINED,
] ]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \

View File

View File

@ -0,0 +1,56 @@
__license__ = 'GPL v3'
__copyright__ = '2011, Ken <ken at szboeye.com>'
__docformat__ = 'restructuredtext en'
'''
Device driver for BOEYE serial readers
'''
from calibre.devices.usbms.driver import USBMS
class BOEYE_BEX(USBMS):
name = 'BOEYE BEX reader driver'
gui_name = 'BOEYE BEX'
description = _('Communicate with BOEYE BEX Serial eBook readers.')
author = 'szboeye'
supported_platforms = ['windows', 'osx', 'linux']
FORMATS = ['epub', 'mobi', 'fb2', 'lit', 'prc', 'pdf', 'rtf', 'txt', 'djvu', 'doc', 'chm', 'html', 'zip', 'pdb']
VENDOR_ID = [0x0085]
PRODUCT_ID = [0x600]
VENDOR_NAME = 'LINUX'
WINDOWS_MAIN_MEM = 'FILE-STOR_GADGET'
OSX_MAIN_MEM = 'Linux File-Stor Gadget Media'
MAIN_MEMORY_VOLUME_LABEL = 'BOEYE BEX Storage Card'
EBOOK_DIR_MAIN = 'Documents'
SUPPORTS_SUB_DIRS = True
class BOEYE_BDX(USBMS):
name = 'BOEYE BDX reader driver'
gui_name = 'BOEYE BDX'
description = _('Communicate with BOEYE BDX serial eBook readers.')
author = 'szboeye'
supported_platforms = ['windows', 'osx', 'linux']
FORMATS = ['epub', 'mobi', 'fb2', 'lit', 'prc', 'pdf', 'rtf', 'txt', 'djvu', 'doc', 'chm', 'html', 'zip', 'pdb']
VENDOR_ID = [0x0085]
PRODUCT_ID = [0x800]
VENDOR_NAME = 'LINUX'
WINDOWS_MAIN_MEM = 'FILE-STOR_GADGET'
WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET'
OSX_MAIN_MEM = 'Linux File-Stor Gadget Media'
OSX_CARD_A_MEM = 'Linux File-Stor Gadget Media'
MAIN_MEMORY_VOLUME_LABEL = 'BOEYE BDX Internal Memory'
STORAGE_CARD_VOLUME_LABEL = 'BOEYE BDX Storage Card'
EBOOK_DIR_MAIN = 'Documents'
EBOOK_DIR_CARD_A = 'Documents'
SUPPORTS_SUB_DIRS = True

View File

@ -7,10 +7,12 @@ __copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os import os
import posixpath
from calibre import walk from calibre import guess_type, walk
from calibre.customize.conversion import InputFormatPlugin from calibre.customize.conversion import InputFormatPlugin
from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.metadata.opf2 import OPF
from calibre.utils.zipfile import ZipFile from calibre.utils.zipfile import ZipFile
class HTMLZInput(InputFormatPlugin): class HTMLZInput(InputFormatPlugin):
@ -27,7 +29,7 @@ class HTMLZInput(InputFormatPlugin):
# Extract content from zip archive. # Extract content from zip archive.
zf = ZipFile(stream) zf = ZipFile(stream)
zf.extractall('.') zf.extractall()
for x in walk('.'): for x in walk('.'):
if os.path.splitext(x)[1].lower() in ('.html', '.xhtml', '.htm'): if os.path.splitext(x)[1].lower() in ('.html', '.xhtml', '.htm'):
@ -70,5 +72,24 @@ class HTMLZInput(InputFormatPlugin):
from calibre.ebooks.oeb.transforms.metadata import meta_info_to_oeb_metadata from calibre.ebooks.oeb.transforms.metadata import meta_info_to_oeb_metadata
mi = get_file_type_metadata(stream, file_ext) mi = get_file_type_metadata(stream, file_ext)
meta_info_to_oeb_metadata(mi, oeb.metadata, log) meta_info_to_oeb_metadata(mi, oeb.metadata, log)
# Get the cover path from the OPF.
cover_href = None
opf = None
for x in walk('.'):
if os.path.splitext(x)[1].lower() in ('.opf'):
opf = x
break
if opf:
opf = OPF(opf)
cover_href = posixpath.relpath(opf.cover, os.path.dirname(stream.name))
# Set the cover.
if cover_href:
cdata = None
with open(cover_href, 'rb') as cf:
cdata = cf.read()
id, href = oeb.manifest.generate('cover', cover_href)
oeb.manifest.add(id, href, guess_type(cover_href)[0], data=cdata)
oeb.guide.add('cover', 'Cover', href)
return oeb return oeb

View File

@ -7,11 +7,13 @@ __copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os import os
from cStringIO import StringIO
from lxml import etree from lxml import etree
from calibre.customize.conversion import OutputFormatPlugin, \ from calibre.customize.conversion import OutputFormatPlugin, \
OptionRecommendation OptionRecommendation
from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory
from calibre.utils.zipfile import ZipFile from calibre.utils.zipfile import ZipFile
@ -79,10 +81,31 @@ class HTMLZOutput(OutputFormatPlugin):
fname = os.path.join(tdir, 'images', images[item.href]) fname = os.path.join(tdir, 'images', images[item.href])
with open(fname, 'wb') as img: with open(fname, 'wb') as img:
img.write(data) img.write(data)
# Cover
cover_path = None
try:
cover_data = None
if oeb_book.metadata.cover:
term = oeb_book.metadata.cover[0].term
cover_data = oeb_book.guide[term].item.data
if cover_data:
from calibre.utils.magick.draw import save_cover_data_to
cover_path = os.path.join(tdir, 'cover.jpg')
with open(cover_path, 'w') as cf:
cf.write('')
save_cover_data_to(cover_data, cover_path)
except:
import traceback
traceback.print_exc()
# Metadata # Metadata
with open(os.path.join(tdir, 'metadata.opf'), 'wb') as mdataf: with open(os.path.join(tdir, 'metadata.opf'), 'wb') as mdataf:
mdataf.write(etree.tostring(oeb_book.metadata.to_opf1())) opf = OPF(StringIO(etree.tostring(oeb_book.metadata.to_opf1())))
mi = opf.to_book_metadata()
if cover_path:
mi.cover = 'cover.jpg'
mdataf.write(metadata_to_opf(mi))
htmlz = ZipFile(output_path, 'w') htmlz = ZipFile(output_path, 'w')
htmlz.add_dir(tdir) htmlz.add_dir(tdir)

View File

@ -13,7 +13,7 @@ import posixpath
from cStringIO import StringIO from cStringIO import StringIO
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf2 import OPF from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.zipfile import ZipFile, safe_replace from calibre.utils.zipfile import ZipFile, safe_replace
@ -31,9 +31,9 @@ def get_metadata(stream, extract_cover=True):
opf = OPF(opf_stream) opf = OPF(opf_stream)
mi = opf.to_book_metadata() mi = opf.to_book_metadata()
if extract_cover: if extract_cover:
cover_name = opf.raster_cover cover_href = posixpath.relpath(opf.cover, os.path.dirname(stream.name))
if cover_name: if cover_href:
mi.cover_data = ('jpg', zf.read(cover_name)) mi.cover_data = ('jpg', zf.read(cover_href))
except: except:
return mi return mi
return mi return mi
@ -59,17 +59,20 @@ def set_metadata(stream, mi):
except: except:
pass pass
if new_cdata: if new_cdata:
raster_cover = opf.raster_cover cover = opf.cover
if not raster_cover: if not cover:
raster_cover = 'cover.jpg' cover = 'cover.jpg'
cpath = posixpath.join(posixpath.dirname(opf_path), raster_cover) cpath = posixpath.join(posixpath.dirname(opf_path), cover)
new_cover = _write_new_cover(new_cdata, cpath) new_cover = _write_new_cover(new_cdata, cpath)
replacements[cpath] = open(new_cover.name, 'rb') replacements[cpath] = open(new_cover.name, 'rb')
mi.cover = cover
# Update the metadata. # Update the metadata.
opf.smart_update(mi, replace_metadata=True) old_mi = opf.to_book_metadata()
old_mi.smart_update(mi)
opf.smart_update(metadata_to_opf(old_mi))
newopf = StringIO(opf.render()) newopf = StringIO(opf.render())
safe_replace(stream, opf_path, newopf, extra_replacements=replacements) safe_replace(stream, opf_path, newopf, extra_replacements=replacements, add_missing=True)
# Cleanup temporary files. # Cleanup temporary files.
try: try:

View File

@ -966,7 +966,9 @@ class OPF(object): # {{{
cover_id = covers[0].get('content') cover_id = covers[0].get('content')
for item in self.itermanifest(): for item in self.itermanifest():
if item.get('id', None) == cover_id: if item.get('id', None) == cover_id:
return item.get('href', None) mt = item.get('media-type', '')
if 'xml' not in mt:
return item.get('href', None)
@dynamic_property @dynamic_property
def cover(self): def cover(self):

View File

@ -7,6 +7,8 @@ __docformat__ = 'restructuredtext en'
Convert an ODT file into a Open Ebook Convert an ODT file into a Open Ebook
''' '''
import os import os
from lxml import etree
from odf.odf2xhtml import ODF2XHTML from odf.odf2xhtml import ODF2XHTML
from calibre import CurrentDir, walk from calibre import CurrentDir, walk
@ -23,7 +25,51 @@ class Extract(ODF2XHTML):
with open(name, 'wb') as f: with open(name, 'wb') as f:
f.write(data) f.write(data)
def __call__(self, stream, odir): def filter_css(self, html, log):
root = etree.fromstring(html)
style = root.xpath('//*[local-name() = "style" and @type="text/css"]')
if style:
style = style[0]
css = style.text
if css:
style.text, sel_map = self.do_filter_css(css)
for x in root.xpath('//*[@class]'):
extra = []
orig = x.get('class')
for cls in orig.split():
extra.extend(sel_map.get(cls, []))
if extra:
x.set('class', orig + ' ' + ' '.join(extra))
html = etree.tostring(root, encoding='utf-8',
xml_declaration=True)
return html
def do_filter_css(self, css):
from cssutils import parseString
from cssutils.css import CSSRule
sheet = parseString(css)
rules = list(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE))
sel_map = {}
count = 0
for r in rules:
# Check if we have only class selectors for this rule
nc = [x for x in r.selectorList if not
x.selectorText.startswith('.')]
if len(r.selectorList) > 1 and not nc:
# Replace all the class selectors with a single class selector
# This will be added to the class attribute of all elements
# that have one of these selectors.
replace_name = 'c_odt%d'%count
count += 1
for sel in r.selectorList:
s = sel.selectorText[1:]
if s not in sel_map:
sel_map[s] = []
sel_map[s].append(replace_name)
r.selectorText = '.'+replace_name
return sheet.cssText, sel_map
def __call__(self, stream, odir, log):
from calibre.utils.zipfile import ZipFile from calibre.utils.zipfile import ZipFile
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.ebooks.metadata.opf2 import OPFCreator
@ -32,13 +78,17 @@ class Extract(ODF2XHTML):
if not os.path.exists(odir): if not os.path.exists(odir):
os.makedirs(odir) os.makedirs(odir)
with CurrentDir(odir): with CurrentDir(odir):
print 'Extracting ODT file...' log('Extracting ODT file...')
html = self.odf2xhtml(stream) html = self.odf2xhtml(stream)
# A blanket img specification like this causes problems # A blanket img specification like this causes problems
# with EPUB output as the contaiing element often has # with EPUB output as the containing element often has
# an absolute height and width set that is larger than # an absolute height and width set that is larger than
# the available screen real estate # the available screen real estate
html = html.replace('img { width: 100%; height: 100%; }', '') html = html.replace('img { width: 100%; height: 100%; }', '')
try:
html = self.filter_css(html, log)
except:
log.exception('Failed to filter CSS, conversion may be slow')
with open('index.xhtml', 'wb') as f: with open('index.xhtml', 'wb') as f:
f.write(html.encode('utf-8')) f.write(html.encode('utf-8'))
zf = ZipFile(stream, 'r') zf = ZipFile(stream, 'r')
@ -67,7 +117,7 @@ class ODTInput(InputFormatPlugin):
def convert(self, stream, options, file_ext, log, def convert(self, stream, options, file_ext, log,
accelerators): accelerators):
return Extract()(stream, '.') return Extract()(stream, '.', log)
def postprocess_book(self, oeb, opts, log): def postprocess_book(self, oeb, opts, log):
# Fix <p><div> constructs as the asinine epubchecker complains # Fix <p><div> constructs as the asinine epubchecker complains

View File

@ -36,7 +36,7 @@ def meta_info_to_oeb_metadata(mi, m, log, override_input_metadata=False):
m.clear('description') m.clear('description')
m.add('description', mi.comments) m.add('description', mi.comments)
elif override_input_metadata: elif override_input_metadata:
m.clear('description') m.clear('description')
if not mi.is_null('publisher'): if not mi.is_null('publisher'):
m.clear('publisher') m.clear('publisher')
m.add('publisher', mi.publisher) m.add('publisher', mi.publisher)

View File

@ -15,7 +15,6 @@ import cStringIO
from lxml import etree from lxml import etree
from calibre.ebooks.metadata import authors_to_string from calibre.ebooks.metadata import authors_to_string
from calibre.utils.filenames import ascii_text
from calibre.utils.magick.draw import save_cover_data_to, identify_data from calibre.utils.magick.draw import save_cover_data_to, identify_data
TAGS = { TAGS = {
@ -79,8 +78,7 @@ def txt2rtf(text):
elif val <= 127: elif val <= 127:
buf.write(x) buf.write(x)
else: else:
repl = ascii_text(x) c = r'\u{0:d}?'.format(val)
c = r'\uc{2}\u{0:d}{1}'.format(val, repl, len(repl))
buf.write(c) buf.write(c)
return buf.getvalue() return buf.getvalue()

View File

@ -34,7 +34,7 @@ if isosx:
) )
gprefs.defaults['action-layout-toolbar'] = ( gprefs.defaults['action-layout-toolbar'] = (
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None, 'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk', 'Choose Library', 'Donate', None, 'Fetch News', 'Store', 'Save To Disk',
'Connect Share', None, 'Remove Books', 'Connect Share', None, 'Remove Books',
) )
gprefs.defaults['action-layout-toolbar-device'] = ( gprefs.defaults['action-layout-toolbar-device'] = (
@ -48,7 +48,7 @@ else:
gprefs.defaults['action-layout-menubar-device'] = () gprefs.defaults['action-layout-menubar-device'] = ()
gprefs.defaults['action-layout-toolbar'] = ( gprefs.defaults['action-layout-toolbar'] = (
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None, 'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk', 'Choose Library', 'Donate', None, 'Fetch News', 'Store', 'Save To Disk',
'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences', 'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences',
) )
gprefs.defaults['action-layout-toolbar-device'] = ( gprefs.defaults['action-layout-toolbar-device'] = (

View File

@ -246,7 +246,7 @@ class ChooseLibraryAction(InterfaceAction):
def delete_requested(self, name, location): def delete_requested(self, name, location):
loc = location.replace('/', os.sep) loc = location.replace('/', os.sep)
if not question_dialog(self.gui, _('Are you sure?'), '<p>'+ if not question_dialog(self.gui, _('Are you sure?'), '<p>'+
_('All files from %s will be ' _('<b style="color: red">All files</b> from <br><br><b>%s</b><br><br> will be '
'<b>permanently deleted</b>. Are you sure?') % loc, '<b>permanently deleted</b>. Are you sure?') % loc,
show_copy_button=False): show_copy_button=False):
return return

View File

@ -10,7 +10,7 @@ from PyQt4.Qt import QIcon, QMenu, Qt
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
from calibre.gui2.preferences.main import Preferences from calibre.gui2.preferences.main import Preferences
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.constants import DEBUG from calibre.constants import DEBUG, isosx
class PreferencesAction(InterfaceAction): class PreferencesAction(InterfaceAction):
@ -19,7 +19,8 @@ class PreferencesAction(InterfaceAction):
def genesis(self): def genesis(self):
pm = QMenu() pm = QMenu()
pm.addAction(QIcon(I('config.png')), _('Preferences'), self.do_config) acname = _('Change calibre behavior') if isosx else _('Preferences')
pm.addAction(QIcon(I('config.png')), acname, self.do_config)
pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'), pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'),
self.gui.run_wizard) self.gui.run_wizard)
if not DEBUG: if not DEBUG:

View File

@ -7,16 +7,16 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, shutil import os, shutil
from contextlib import closing
from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
from PyQt4.Qt import QDialog from PyQt4.Qt import QDialog
from calibre.constants import isosx from calibre.constants import isosx
from calibre.gui2 import open_local_file from calibre.gui2 import open_local_file, error_dialog
from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog
from calibre.libunzip import extract as zipextract from calibre.libunzip import extract as zipextract
from calibre.ptempfile import PersistentTemporaryDirectory from calibre.ptempfile import (PersistentTemporaryDirectory,
PersistentTemporaryFile)
class TweakEpub(QDialog, Ui_Dialog): class TweakEpub(QDialog, Ui_Dialog):
''' '''
@ -37,11 +37,15 @@ class TweakEpub(QDialog, Ui_Dialog):
self.cancel_button.clicked.connect(self.reject) self.cancel_button.clicked.connect(self.reject)
self.explode_button.clicked.connect(self.explode) self.explode_button.clicked.connect(self.explode)
self.rebuild_button.clicked.connect(self.rebuild) self.rebuild_button.clicked.connect(self.rebuild)
self.preview_button.clicked.connect(self.preview)
# Position update dialog overlaying top left of app window # Position update dialog overlaying top left of app window
parent_loc = parent.pos() parent_loc = parent.pos()
self.move(parent_loc.x(),parent_loc.y()) self.move(parent_loc.x(),parent_loc.y())
self.gui = parent
self._preview_files = []
def cleanup(self): def cleanup(self):
if isosx: if isosx:
try: try:
@ -55,6 +59,11 @@ class TweakEpub(QDialog, Ui_Dialog):
# Delete directory containing exploded ePub # Delete directory containing exploded ePub
if self._exploded is not None: if self._exploded is not None:
shutil.rmtree(self._exploded, ignore_errors=True) shutil.rmtree(self._exploded, ignore_errors=True)
for x in self._preview_files:
try:
os.remove(x)
except:
pass
def display_exploded(self): def display_exploded(self):
''' '''
@ -71,9 +80,8 @@ class TweakEpub(QDialog, Ui_Dialog):
self.rebuild_button.setEnabled(True) self.rebuild_button.setEnabled(True)
self.explode_button.setEnabled(False) self.explode_button.setEnabled(False)
def rebuild(self, *args): def do_rebuild(self, src):
self._output = os.path.join(self._exploded, 'rebuilt.epub') with ZipFile(src, 'w', compression=ZIP_DEFLATED) as zf:
with closing(ZipFile(self._output, 'w', compression=ZIP_DEFLATED)) as zf:
# Write mimetype # Write mimetype
zf.write(os.path.join(self._exploded,'mimetype'), 'mimetype', compress_type=ZIP_STORED) zf.write(os.path.join(self._exploded,'mimetype'), 'mimetype', compress_type=ZIP_STORED)
# Write everything else # Write everything else
@ -86,5 +94,23 @@ class TweakEpub(QDialog, Ui_Dialog):
zfn = os.path.relpath(absfn, zfn = os.path.relpath(absfn,
self._exploded).replace(os.sep, '/') self._exploded).replace(os.sep, '/')
zf.write(absfn, zfn) zf.write(absfn, zfn)
def preview(self):
if not self._exploded:
return error_dialog(self, _('Cannot preview'),
_('You must first explode the epub before previewing.'),
show=True)
tf = PersistentTemporaryFile('.epub')
tf.close()
self._preview_files.append(tf.name)
self.do_rebuild(tf.name)
self.gui.iactions['View']._view_file(tf.name)
def rebuild(self, *args):
self._output = os.path.join(self._exploded, 'rebuilt.epub')
self.do_rebuild(self._output)
return QDialog.accept(self) return QDialog.accept(self)

View File

@ -23,6 +23,16 @@
<bool>false</bool> <bool>false</bool>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;p&gt;Explode the ePub to display contents in a file browser window. To tweak individual files, right-click, then 'Open with...' your editor of choice. When tweaks are complete, close the file browser window &lt;b&gt;and the editor windows you used to edit files in the epub&lt;/b&gt;.&lt;/p&gt;&lt;p&gt;Rebuild the ePub, updating your calibre library.&lt;/p&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QPushButton" name="explode_button"> <widget class="QPushButton" name="explode_button">
<property name="statusTip"> <property name="statusTip">
@ -37,23 +47,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0">
<widget class="QPushButton" name="rebuild_button">
<property name="enabled">
<bool>false</bool>
</property>
<property name="statusTip">
<string>Rebuild ePub from exploded contents</string>
</property>
<property name="text">
<string>&amp;Rebuild ePub</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/exec.png</normaloff>:/images/exec.png</iconset>
</property>
</widget>
</item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QPushButton" name="cancel_button"> <widget class="QPushButton" name="cancel_button">
<property name="statusTip"> <property name="statusTip">
@ -68,13 +61,31 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0"> <item row="3" column="1">
<widget class="QLabel" name="label"> <widget class="QPushButton" name="rebuild_button">
<property name="text"> <property name="enabled">
<string>&lt;p&gt;Explode the ePub to display contents in a file browser window. To tweak individual files, right-click, then 'Open with...' your editor of choice. When tweaks are complete, close the file browser window &lt;b&gt;and the editor windows you used to edit files in the epub&lt;/b&gt;.&lt;/p&gt;&lt;p&gt;Rebuild the ePub, updating your calibre library.&lt;/p&gt;</string> <bool>false</bool>
</property> </property>
<property name="wordWrap"> <property name="statusTip">
<bool>true</bool> <string>Rebuild ePub from exploded contents</string>
</property>
<property name="text">
<string>&amp;Rebuild ePub</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/exec.png</normaloff>:/images/exec.png</iconset>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="preview_button">
<property name="text">
<string>&amp;Preview ePub</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/view.png</normaloff>:/images/view.png</iconset>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -44,18 +44,19 @@ class LocationManager(QObject): # {{{
receiver = partial(self._location_selected, name) receiver = partial(self._location_selected, name)
ac.triggered.connect(receiver) ac.triggered.connect(receiver)
self.tooltips[name] = tooltip self.tooltips[name] = tooltip
m = QMenu(parent)
self._mem.append(m)
a = m.addAction(icon, tooltip)
a.triggered.connect(receiver)
if name != 'library': if name != 'library':
m = QMenu(parent)
self._mem.append(m)
a = m.addAction(icon, tooltip)
a.triggered.connect(receiver)
self._mem.append(a) self._mem.append(a)
a = m.addAction(QIcon(I('eject.png')), _('Eject this device')) a = m.addAction(QIcon(I('eject.png')), _('Eject this device'))
a.triggered.connect(self._eject_requested) a.triggered.connect(self._eject_requested)
ac.setMenu(m)
self._mem.append(a) self._mem.append(a)
else: else:
ac.setToolTip(tooltip) ac.setToolTip(tooltip)
ac.setMenu(m)
ac.calibre_name = name ac.calibre_name = name
return ac return ac
@ -71,7 +72,12 @@ class LocationManager(QObject): # {{{
def set_switch_actions(self, quick_actions, rename_actions, delete_actions, def set_switch_actions(self, quick_actions, rename_actions, delete_actions,
switch_actions, choose_action): switch_actions, choose_action):
self.switch_menu = QMenu() self.switch_menu = self.library_action.menu()
if self.switch_menu:
self.switch_menu.addSeparator()
else:
self.switch_menu = QMenu()
self.switch_menu.addAction(choose_action) self.switch_menu.addAction(choose_action)
self.cs_menus = [] self.cs_menus = []
for t, acs in [(_('Quick switch'), quick_actions), for t, acs in [(_('Quick switch'), quick_actions),
@ -85,7 +91,9 @@ class LocationManager(QObject): # {{{
self.switch_menu.addSeparator() self.switch_menu.addSeparator()
for ac in switch_actions: for ac in switch_actions:
self.switch_menu.addAction(ac) self.switch_menu.addAction(ac)
self.library_action.setMenu(self.switch_menu)
if self.switch_menu != self.library_action.menu():
self.library_action.setMenu(self.switch_menu)
def _location_selected(self, location, *args): def _location_selected(self, location, *args):
if location != self.current_location and hasattr(self, if location != self.current_location and hasattr(self,

View File

@ -198,7 +198,7 @@ class MetadataSingleDialogBase(ResizableDialog):
ans = self.custom_metadata_widgets ans = self.custom_metadata_widgets
for i in range(len(ans)-1): for i in range(len(ans)-1):
if before is not None and i == 0: if before is not None and i == 0:
pass# Do something pass
if len(ans[i+1].widgets) == 2: if len(ans[i+1].widgets) == 2:
sto(ans[i].widgets[-1], ans[i+1].widgets[1]) sto(ans[i].widgets[-1], ans[i+1].widgets[1])
else: else:
@ -206,7 +206,7 @@ class MetadataSingleDialogBase(ResizableDialog):
for c in range(2, len(ans[i].widgets), 2): for c in range(2, len(ans[i].widgets), 2):
sto(ans[i].widgets[c-1], ans[i].widgets[c+1]) sto(ans[i].widgets[c-1], ans[i].widgets[c+1])
if after is not None: if after is not None:
pass # Do something pass
# }}} # }}}
def do_view_format(self, path, fmt): def do_view_format(self, path, fmt):
@ -728,7 +728,135 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
# }}} # }}}
editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1} class MetadataSingleDialogAlt2(MetadataSingleDialogBase): # {{{
cc_two_column = False
one_line_comments_toolbar = True
def do_layout(self):
self.central_widget.clear()
self.labels = []
sto = QWidget.setTabOrder
self.central_widget.tabBar().setVisible(False)
tab0 = QWidget(self)
self.central_widget.addTab(tab0, _("&Metadata"))
l = QGridLayout()
tab0.setLayout(l)
# Basic metadata in col 0
tl = QGridLayout()
gb = QGroupBox(_('Basic metadata'), tab0)
l.addWidget(gb, 0, 0, 1, 1)
gb.setLayout(tl)
self.button_box.addButton(self.fetch_metadata_button,
QDialogButtonBox.ActionRole)
self.config_metadata_button.setToolButtonStyle(Qt.ToolButtonTextOnly)
self.config_metadata_button.setText(_('Configure metadata downloading'))
self.button_box.addButton(self.config_metadata_button,
QDialogButtonBox.ActionRole)
sto(self.button_box, self.title)
def create_row(row, widget, tab_to, button=None, icon=None, span=1):
ql = BuddyLabel(widget)
tl.addWidget(ql, row, 1, 1, 1)
tl.addWidget(widget, row, 2, 1, 1)
if button is not None:
tl.addWidget(button, row, 3, span, 1)
if icon is not None:
button.setIcon(QIcon(I(icon)))
if tab_to is not None:
if button is not None:
sto(widget, button)
sto(button, tab_to)
else:
sto(widget, tab_to)
tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
create_row(0, self.title, self.title_sort,
button=self.deduce_title_sort_button, span=2,
icon='auto_author_sort.png')
create_row(1, self.title_sort, self.authors)
create_row(2, self.authors, self.author_sort,
button=self.deduce_author_sort_button,
span=2, icon='auto_author_sort.png')
create_row(3, self.author_sort, self.series)
create_row(4, self.series, self.series_index,
button=self.remove_unused_series_button, icon='trash.png')
create_row(5, self.series_index, self.tags)
create_row(6, self.tags, self.rating, button=self.tags_editor_button)
create_row(7, self.rating, self.pubdate)
create_row(8, self.pubdate, self.publisher,
button=self.pubdate.clear_button, icon='trash.png')
create_row(9, self.publisher, self.timestamp)
create_row(10, self.timestamp, self.identifiers,
button=self.timestamp.clear_button, icon='trash.png')
create_row(11, self.identifiers, self.comments,
button=self.clear_identifiers_button, icon='trash.png')
tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding),
12, 1, 1 ,1)
# Custom metadata in col 1
w = getattr(self, 'custom_metadata_widgets_parent', None)
if w is not None:
gb = QGroupBox(_('Custom metadata'), tab0)
gbl = QVBoxLayout()
gb.setLayout(gbl)
sr = QScrollArea(gb)
sr.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
sr.setWidgetResizable(True)
sr.setBackgroundRole(QPalette.Base)
sr.setFrameStyle(QFrame.NoFrame)
sr.setWidget(w)
gbl.addWidget(sr)
l.addWidget(gb, 0, 1, 1, 1)
sp = QSizePolicy()
sp.setVerticalStretch(10)
sp.setHorizontalPolicy(QSizePolicy.Fixed)
sp.setVerticalPolicy(QSizePolicy.Expanding)
gb.setSizePolicy(sp)
self.set_custom_metadata_tab_order()
# comments span col 0 & 1
w = QGroupBox(_('Comments'), tab0)
sp = QSizePolicy()
sp.setVerticalStretch(10)
sp.setHorizontalPolicy(QSizePolicy.Expanding)
sp.setVerticalPolicy(QSizePolicy.Expanding)
w.setSizePolicy(sp)
lb = QHBoxLayout()
w.setLayout(lb)
lb.addWidget(self.comments)
l.addWidget(w, 1, 0, 1, 2)
# Cover & formats in col 3
gb = QGroupBox(_('Cover'), tab0)
lb = QGridLayout()
gb.setLayout(lb)
lb.addWidget(self.cover, 0, 0, 1, 3, alignment=Qt.AlignCenter)
sto(self.clear_identifiers_button, self.cover.buttons[0])
for i, b in enumerate(self.cover.buttons[:3]):
lb.addWidget(b, 1, i, 1, 1)
sto(b, self.cover.buttons[i+1])
hl = QHBoxLayout()
for b in self.cover.buttons[3:]:
hl.addWidget(b)
sto(self.cover.buttons[-2], self.cover.buttons[-1])
lb.addLayout(hl, 2, 0, 1, 3)
l.addWidget(gb, 0, 2, 1, 1)
l.addWidget(self.formats_manager, 1, 2, 1, 1)
sto(self.cover.buttons[-1], self.formats_manager)
self.formats_manager.formats.setMaximumWidth(10000)
self.formats_manager.formats.setIconSize(QSize(32, 32))
# }}}
editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1,
'alt2': MetadataSingleDialogAlt2}
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None, def edit_metadata(db, row_list, current_row, parent=None, view_slot=None,
set_current_callback=None): set_current_callback=None):

View File

@ -61,7 +61,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('bools_are_tristate', db.prefs, restart_required=True) r('bools_are_tristate', db.prefs, restart_required=True)
r = self.register r = self.register
choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1')] choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1'),
(_('All on 1 tab'), 'alt2')]
r('edit_metadata_single_layout', gprefs, choices=choices) r('edit_metadata_single_layout', gprefs, choices=choices)
def initialize(self): def initialize(self):

View File

@ -190,7 +190,15 @@ class FieldsModel(QAbstractListModel): # {{{
return ans | Qt.ItemIsUserCheckable return ans | Qt.ItemIsUserCheckable
def restore_defaults(self): def restore_defaults(self):
self.overrides = dict([(f, self.state(f, True)) for f in self.fields]) self.overrides = dict([(f, self.state(f, Qt.Checked)) for f in self.fields])
self.reset()
def select_all(self):
self.overrides = dict([(f, Qt.Checked) for f in self.fields])
self.reset()
def clear_all(self):
self.overrides = dict([(f, Qt.Unchecked) for f in self.fields])
self.reset() self.reset()
def setData(self, index, val, role): def setData(self, index, val, role):
@ -273,6 +281,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.fields_view.setModel(self.fields_model) self.fields_view.setModel(self.fields_model)
self.fields_model.dataChanged.connect(self.changed_signal) self.fields_model.dataChanged.connect(self.changed_signal)
self.select_all_button.clicked.connect(self.fields_model.select_all)
self.clear_all_button.clicked.connect(self.fields_model.clear_all)
def configure_plugin(self): def configure_plugin(self):
for index in self.sources_view.selectionModel().selectedRows(): for index in self.sources_view.selectionModel().selectedRows():
plugin = self.sources_model.data(index, Qt.UserRole) plugin = self.sources_model.data(index, Qt.UserRole)

View File

@ -77,8 +77,8 @@
<property name="title"> <property name="title">
<string>Downloaded metadata fields</string> <string>Downloaded metadata fields</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QGridLayout" name="gridLayout_2">
<item> <item row="0" column="0" colspan="2">
<widget class="QListView" name="fields_view"> <widget class="QListView" name="fields_view">
<property name="toolTip"> <property name="toolTip">
<string>If you uncheck any fields, metadata for those fields will not be downloaded</string> <string>If you uncheck any fields, metadata for those fields will not be downloaded</string>
@ -88,6 +88,20 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0">
<widget class="QPushButton" name="select_all_button">
<property name="text">
<string>&amp;Select all</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="clear_all_button">
<property name="text">
<string>&amp;Clear all</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

View File

@ -47,7 +47,7 @@ class StorePlugin(object): # {{{
def __init__(self, gui, name): def __init__(self, gui, name):
from calibre.gui2 import JSONConfig from calibre.gui2 import JSONConfig
self.gui = gui self.gui = gui
self.name = name self.name = name
self.base_plugin = None self.base_plugin = None
@ -79,14 +79,14 @@ class StorePlugin(object): # {{{
return items as a generator. return items as a generator.
Don't be lazy with the search! Load as much data as possible in the Don't be lazy with the search! Load as much data as possible in the
:class:`calibre.gui2.store.search_result.SearchResult` object. :class:`calibre.gui2.store.search_result.SearchResult` object.
However, if data (such as cover_url) However, if data (such as cover_url)
isn't available because the store does not display cover images then it's okay to isn't available because the store does not display cover images then it's okay to
ignore it. ignore it.
At the very least a :class:`calibre.gui2.store.search_result.SearchResult` At the very least a :class:`calibre.gui2.store.search_result.SearchResult`
returned by this function must have the title, author and id. returned by this function must have the title, author and id.
If you have to parse multiple pages to get all of the data then implement If you have to parse multiple pages to get all of the data then implement
:meth:`get_deatils` for retrieving additional information. :meth:`get_deatils` for retrieving additional information.
@ -105,24 +105,24 @@ class StorePlugin(object): # {{{
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store. item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
''' '''
raise NotImplementedError() raise NotImplementedError()
def get_details(self, search_result, timeout=60): def get_details(self, search_result, timeout=60):
''' '''
Delayed search for information about specific search items. Delayed search for information about specific search items.
Typically, this will be used when certain information such as Typically, this will be used when certain information such as
formats, drm status, cover url are not part of the main search formats, drm status, cover url are not part of the main search
results and the information is on another web page. results and the information is on another web page.
Using this function allows for the main information (title, author) Using this function allows for the main information (title, author)
to be displayed in the search results while other information can to be displayed in the search results while other information can
take extra time to load. Splitting retrieving data that takes longer take extra time to load. Splitting retrieving data that takes longer
to load into a separate function will give the illusion of the search to load into a separate function will give the illusion of the search
being faster. being faster.
:param search_result: A search result that need details set. :param search_result: A search result that need details set.
:param timeout: The maximum amount of time in seconds to spend downloading details. :param timeout: The maximum amount of time in seconds to spend downloading details.
:return: True if the search_result was modified otherwise False :return: True if the search_result was modified otherwise False
''' '''
return False return False
@ -133,30 +133,30 @@ class StorePlugin(object): # {{{
is called to update the caches. It is recommended to call this function is called to update the caches. It is recommended to call this function
from :meth:`open`. Especially if :meth:`open` does anything other than from :meth:`open`. Especially if :meth:`open` does anything other than
open a web page. open a web page.
This function can be called at any time. It is up to the plugin to determine This function can be called at any time. It is up to the plugin to determine
if the cache really does need updating. Unless :param:`force` is True, then if the cache really does need updating. Unless :param:`force` is True, then
the plugin must update the cache. The only time force should be True is if the plugin must update the cache. The only time force should be True is if
this function is called by the plugin's configuration dialog. this function is called by the plugin's configuration dialog.
if :param:`suppress_progress` is False it is safe to assume that this function if :param:`suppress_progress` is False it is safe to assume that this function
is being called from the main GUI thread so it is safe and recommended to use is being called from the main GUI thread so it is safe and recommended to use
a QProgressDialog to display what is happening and allow the user to cancel a QProgressDialog to display what is happening and allow the user to cancel
the operation. if :param:`suppress_progress` is True then run the update the operation. if :param:`suppress_progress` is True then run the update
silently. In this case there is no guarantee what thread is calling this silently. In this case there is no guarantee what thread is calling this
function so no Qt related functionality that requires being run in the main function so no Qt related functionality that requires being run in the main
GUI thread should be run. E.G. Open a QProgressDialog. GUI thread should be run. E.G. Open a QProgressDialog.
:param parent: The parent object to be used by an GUI dialogs. :param parent: The parent object to be used by an GUI dialogs.
:param timeout: The maximum amount of time that should be spent in :param timeout: The maximum amount of time that should be spent in
any given network connection. any given network connection.
:param force: Force updating the cache even if the plugin has determined :param force: Force updating the cache even if the plugin has determined
it is not necessary. it is not necessary.
:param suppress_progress: Should a progress indicator be shown. :param suppress_progress: Should a progress indicator be shown.
:return: True if the cache was updated, False otherwise. :return: True if the cache was updated, False otherwise.
''' '''
return False return False

View File

@ -155,6 +155,7 @@ class SearchDialog(QDialog, Ui_Dialog):
self.config['results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount())] self.config['results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount())]
self.config['sort_col'] = self.results_view.model().sort_col self.config['sort_col'] = self.results_view.model().sort_col
self.config['sort_order'] = self.results_view.model().sort_order self.config['sort_order'] = self.results_view.model().sort_order
self.config['open_external'] = self.open_external.isChecked()
store_check = {} store_check = {}
for n in self.store_plugins: for n in self.store_plugins:
@ -179,6 +180,8 @@ class SearchDialog(QDialog, Ui_Dialog):
else: else:
self.resize_columns() self.resize_columns()
self.open_external.setChecked(self.config.get('open_external', False))
store_check = self.config.get('store_checked', None) store_check = self.config.get('store_checked', None)
if store_check: if store_check:
for n in store_check: for n in store_check:
@ -212,7 +215,7 @@ class SearchDialog(QDialog, Ui_Dialog):
def open_store(self, index): def open_store(self, index):
result = self.results_view.model().get_result(index) result = self.results_view.model().get_result(index)
self.store_plugins[result.store_name].open(self, result.detail_item) self.store_plugins[result.store_name].open(self, result.detail_item, self.open_external.isChecked())
def check_progress(self): def check_progress(self):
if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running(): if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running():

View File

@ -70,7 +70,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>215</width> <width>215</width>
<height>116</height> <height>93</height>
</rect> </rect>
</property> </property>
</widget> </widget>
@ -101,6 +101,16 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<widget class="QCheckBox" name="open_external">
<property name="toolTip">
<string>Open a selected book in the system's web browser</string>
</property>
<property name="text">
<string>Open in &amp;external browser</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QSplitter" name="splitter_2"> <widget class="QSplitter" name="splitter_2">

View File

@ -841,11 +841,19 @@ ol, ul { padding-left: 2em; }
self.styledict[name] = styles self.styledict[name] = styles
# Write the styles to HTML # Write the styles to HTML
self.writeout(self.default_styles) self.writeout(self.default_styles)
# Changed by Kovid to not write out endless copies of the same style
css_styles = {}
for name in self.stylestack: for name in self.stylestack:
styles = self.styledict.get(name) styles = self.styledict.get(name)
css2 = self.cs.convert_styles(styles) css2 = tuple(self.cs.convert_styles(styles).iteritems())
self.writeout("%s {\n" % name) if css2 in css_styles:
for style, val in css2.items(): css_styles[css2].append(name)
else:
css_styles[css2] = [name]
for css2, names in css_styles.iteritems():
self.writeout("%s {\n" % ', '.join(names))
for style, val in css2:
self.writeout("\t%s: %s;\n" % (style, val) ) self.writeout("\t%s: %s;\n" % (style, val) )
self.writeout("}\n") self.writeout("}\n")