Sync to ldolse heuristics branch.

This commit is contained in:
John Schember 2011-01-17 20:18:02 -05:00
commit 152fac4782
48 changed files with 1775 additions and 1044 deletions

View File

@ -0,0 +1,39 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
'''
Available fields:
{title} Title of the book
{series} Series name
{series_index} Number of the book in the series
{rating} Rating
{rating_parens} Rating, in parentheses
{pubyear} Year the book was published
{pubyear_parens} Year the book was published, in parentheses
'''
# Books by Author
by_authors_normal_title_template = '{title} {pubyear_parens}'
by_authors_series_title_template = '[{series_index}] {title} {pubyear_parens}'
# Books by Title
by_titles_normal_title_template = '{title}'
by_titles_series_title_template = '{title} ({series} [{series_index}])'
# Books by Series
by_series_title_template = '[{series_index}] {title} {pubyear_parens}'
# Books by Genre
by_genres_normal_title_template = '{title} {pubyear_parens}'
by_genres_series_title_template = '{series_index}. {title} {pubyear_parens}'
# Recently Added
by_recently_added_normal_title_template = '{title}'
by_recently_added_series_title_template = '{title} ({series} [{series_index}])'
# By Month added
by_month_added_normal_title_template = '{title} {pubyear_parens}'
by_month_added_series_title_template = '[{series_index}] {title} {pubyear_parens}'

View File

@ -28,7 +28,7 @@ class DilbertBig(BasicNewsRecipe):
,'publisher' : publisher ,'publisher' : publisher
} }
feeds = [(u'Dilbert', u'http://feeds.dilbert.com/DilbertDailyStrip' )] feeds = [(u'Dilbert', u'http://feed.dilbert.com/dilbert/daily_strip' )]
def get_article_url(self, article): def get_article_url(self, article):
return article.get('feedburner_origlink', None) return article.get('feedburner_origlink', None)

View File

@ -9,7 +9,7 @@ from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulSoup from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.ebooks.BeautifulSoup import Tag, NavigableString from calibre.ebooks.BeautifulSoup import Tag, NavigableString
import mechanize, string, urllib, time, re import string, time, re
class Economist(BasicNewsRecipe): class Economist(BasicNewsRecipe):
@ -18,19 +18,19 @@ class Economist(BasicNewsRecipe):
__author__ = "Kovid Goyal" __author__ = "Kovid Goyal"
INDEX = 'http://www.economist.com/printedition' INDEX = 'http://www.economist.com/printedition'
description = ('Global news and current affairs from a European perspective.' description = 'Global news and current affairs from a European perspective.'
' Needs a subscription from ')+INDEX
oldest_article = 7.0 oldest_article = 7.0
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg' cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']), remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
dict(attrs={'class':['dblClkTrk', 'ec-article-info']})] dict(attrs={'class':['dblClkTrk', 'ec-article-info']})]
keep_only_tags = [dict(id='ec-article-body')] keep_only_tags = [dict(id='ec-article-body')]
needs_subscription = True needs_subscription = False
no_stylesheets = True no_stylesheets = True
preprocess_regexps = [(re.compile('</html>.*', re.DOTALL), preprocess_regexps = [(re.compile('</html>.*', re.DOTALL),
lambda x:'</html>')] lambda x:'</html>')]
'''
def get_browser(self): def get_browser(self):
br = BasicNewsRecipe.get_browser() br = BasicNewsRecipe.get_browser()
br.open('http://www.economist.com') br.open('http://www.economist.com')
@ -50,6 +50,7 @@ class Economist(BasicNewsRecipe):
})) }))
br.open(req).read() br.open(req).read()
return br return br
'''
def parse_index(self): def parse_index(self):
try: try:

View File

@ -7,12 +7,12 @@ from lxml import html
class Economist(BasicNewsRecipe): class Economist(BasicNewsRecipe):
title = 'The Economist (free)' title = 'The Economist (RSS)'
language = 'en' language = 'en'
__author__ = "Kovid Goyal" __author__ = "Kovid Goyal"
description = ('Global news and current affairs from a European perspective.' description = ('Global news and current affairs from a European perspective.'
' Much slower than the subscription based version.') ' Much slower than the print edition based version.')
oldest_article = 7.0 oldest_article = 7.0
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg' cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'

View File

@ -1,4 +1,5 @@
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import Tag
import re import re
class NatureNews(BasicNewsRecipe): class NatureNews(BasicNewsRecipe):
@ -10,17 +11,76 @@ class NatureNews(BasicNewsRecipe):
max_articles_per_feed = 50 max_articles_per_feed = 50
no_stylesheets = True no_stylesheets = True
remove_tags_before = dict(name='h1', attrs={'class':'heading entry-title'}) keep_only_tags = [dict(name='div', attrs={'id':'content'})]
remove_tags_after = dict(name='h2', attrs={'id':'comments'}) # remove_tags_before = dict(name='h1', attrs={'class':'heading entry-title'})
# remove_tags_after = dict(name='h2', attrs={'id':'comments'})
remove_tags = [ remove_tags = [
dict(name='h2', attrs={'id':'comments'}), dict(name='h2', attrs={'id':'comments'}),
dict(attrs={'alt':'Advertisement'}), dict(attrs={'alt':'Advertisement'}),
dict(name='div', attrs={'class':'ad'}), dict(name='div', attrs={'class':'ad'}),
dict(attrs={'class':'Z3988'}),
dict(attrs={'class':['formatpublished','type-of-article','cleardiv','disclaimer','buttons','comments xoxo']}),
dict(name='a', attrs={'href':'#comments'}),
dict(name='h2',attrs={'class':'subheading plusicon icon-add-comment'})
] ]
preprocess_regexps = [ preprocess_regexps = [
(re.compile(r'<p>ADVERTISEMENT</p>', re.DOTALL|re.IGNORECASE), lambda match: ''), (re.compile(r'<p>ADVERTISEMENT</p>', re.DOTALL|re.IGNORECASE), lambda match: ''),
] ]
extra_css = '''
.author { text-align: right; font-size: small; line-height:1em; margin-top:0px; margin-left:0; margin-right:0; margin-bottom: 0; }
.imagedescription { font-size: small; font-style:italic; line-height:1em; margin-top:5px; margin-left:0; margin-right:0; margin-bottom: 0; }
.imagecredit { font-size: x-small; font-style: normal; font-weight: bold}
'''
feeds = [('Nature News', 'http://feeds.nature.com/news/rss/most_recent')] feeds = [('Nature News', 'http://feeds.nature.com/news/rss/most_recent')]
def preprocess_html(self,soup):
# The author name is slightly buried - dig it up
author = soup.find('p', {'class':'byline'})
if author:
# Find out the author's name
authornamediv = author.find('span',{'class':'author fn'})
authornamelink = authornamediv.find('a')
if authornamelink:
authorname = authornamelink.contents[0]
else:
authorname = authornamediv.contents[0]
# Stick the author's name in the byline tag
tag = Tag(soup,'div')
tag['class'] = 'author'
tag.insert(0,authorname.strip())
author.replaceWith(tag)
# Change the intro from a p to a div
intro = soup.find('p',{'class':'intro'})
if intro:
tag = Tag(soup,'div')
tag['class'] = 'intro'
tag.insert(0,intro.contents[0])
intro.replaceWith(tag)
# Change span class=imagedescription to div
descr = soup.find('span',{'class':'imagedescription'})
if descr:
tag = Tag(soup,'div')
tag['class'] = 'imagedescription'
tag.insert(0,descr.renderContents())
descr.replaceWith(tag)
# The references are in a list, let's make them simpler
reflistcont = soup.find('ul',{'id':'article-refrences'})
if reflistcont:
reflist = reflistcont.li.renderContents()
tag = Tag(soup,'div')
tag['class'] = 'article-references'
tag.insert(0,reflist)
reflistcont.replaceWith(tag)
# Within the id=content div, we need to remove all the stuff after the end of the class=entry-content
entrycontent = soup.find('div',{'class':'entry-content'})
for nextSibling in entrycontent.findNextSiblings():
nextSibling.extract()
return soup

View File

@ -0,0 +1,182 @@
import re, time
from calibre import strftime
from calibre.web.feeds.recipes import BasicNewsRecipe
class IHNed(BasicNewsRecipe):
stahnout_vsechny = False
#True = stahuje vsechny z homepage
#False = stahuje pouze dnesni clanky (ze dne, kdy je skript spusten)
title = 'iHNed'
__author__ = 'Karel Bílek'
language = 'cs'
description = 'Zprávy z iHNed.cz'
timefmt = ' [%a, %d %b, %Y]'
needs_subscription = False
remove_tags = [dict(attrs={'class':['borderbottom', 'web', 'foot', 'reklama', 'd-elm d-rellinks', 'd-elm']}),
dict(style=['text-align: center;']),
dict(id=['r-bfull']),
dict(name=['script', 'noscript', 'style'])]
encoding = 'windows-1250'
no_stylesheets = True
remove_tags_before = dict(attrs={'class':'d-nadtit'})
remove_tags_after = dict(attrs={'class':'like'})
conversion_options = {
'linearize_tables' : True,
}
def preprocess_html(self, soup):
def makeurl(wat):
return "http://ihned.cz"+wat;
for h1 in soup.findAll('h1'):
a = h1.find('a')
if a:
string = a.string
if string:
soup.a.replaceWith(string)
for a in soup.findAll('a', href=True) :
cil = str(a['href'])
if cil.startswith("/") or cil.startswith("index"):
a['href'] = makeurl(cil)
return soup
def parse_index(self):
def makeurl(wat):
if wat.startswith("/") or wat.startswith("index"):
return "http://ihned.cz"+wat;
else:
return wat
articles = {} #vysledek, asi
key = None #soucasna sekce
ans = [] #vsechny sekce
articles["Hlavní"] = []
ans.append("Hlavní")
was = {}
def parse_subpage(url, name):
articles[name] = []
ans.append(name)
soup = self.index_to_soup(url)
otvirak = soup.find(True, attrs={'class':['otv']})
if otvirak:
#the code is copypasted here because I don't know python. simple as that.
a = otvirak.find('a', href=True)
title = self.tag_to_string(a, use_alt=True).strip()
txt = otvirak.find(True, attrs={'class':['txt']})
description = ''
if txt:
match = re.match(r'<div class="txt">\s*([^<]*)\s*<a', str(txt), re.L)
if match:
description = match.group(1)
pubdate = strftime('%d. %m.')
if not title in was:
articles[name].append(
dict(title=title, url=makeurl(a['href']), date=pubdate,
description=description,
content=''))
otv234 = soup.find(True, attrs={'class':['otv234', 'col2a']})
if otv234:
for ow in otv234.findAll(True, attrs={'class':['ow']}):
a = ow.find('a', href=True)
title = self.tag_to_string(a, use_alt=True).strip()
description=''
prx = ow.find(True, attrs={'class':['prx']});
if prx:
description = str(prx.string)
nfo = ow.find(True, attrs={'class':['nfo']});
pubdate = ''
if nfo:
dtime = time.localtime();
day = dtime[2]
month = dtime[1]
pubdate = strftime('%d. %m.')
match = re.search(r'([0-9]*)\.([0-9]*)\.', str(nfo))
if self.stahnout_vsechny or (int(day) == int(match.group(1)) and int(month) == int(match.group(2))):
if not title in was:
articles[name].append(
dict(title=title, url=makeurl(a['href']), date=pubdate,
description=description,
content=''))
soup = self.index_to_soup('http://ihned.cz/')
otvirak = soup.find(True, attrs={'class':['otv']})
if otvirak:
a = otvirak.find('a', href=True)
title = self.tag_to_string(a, use_alt=True).strip()
txt = otvirak.find(True, attrs={'class':['txt']})
description = ''
if txt:
match = re.match(r'<div class="txt">\s*([^<]*)\s*<a', str(txt), re.L)
if match:
description = match.group(1)
pubdate = strftime('%d. %m.')
feed = "Hlavní"
articles[feed].append(
dict(title=title, url=(a['href']), date=pubdate,
description=description,
content=''))
was[title]=1
otvirak2345 = soup.find(True, attrs={'class':['otv2345']})
if otvirak2345:
for otv2 in otvirak2345.findAll(True, attrs={'class':['otv2-5']}):
a = otv2.find('a', attrs={'class':['tit2']}, href=True)
title = self.tag_to_string(a, use_alt=True).strip()
description=''
span = otv2.find('span');
if span:
match = re.match(r'<span>\s*([^<]*)\s*<a', str(span), re.L)
if match:
description = match.group(1)
feed = "Hlavní"
pubdate = strftime('%d. %m.')
articles[feed].append(
dict(title=title, url=(a['href']), date=pubdate,
description=description,
content=''))
was[title]=1
parse_subpage("http://komentare.ihned.cz/", "Komentáře")
parse_subpage("http://domaci.ihned.cz", "Domácí")
parse_subpage("http://ekonomika.ihned.cz", "Ekonomika")
parse_subpage("http://zahranicni.ihned.cz/", "Zahraničí");
parse_subpage("http://finweb.ihned.cz/", "Finance");
parse_subpage("http://digiweb.ihned.cz/", "DigiWeb");
parse_subpage("http://kultura.ihned.cz/", "Kultura")
parse_subpage("http://sport.ihned.cz/", "Sport");
#seradi kategorie
ans = self.sort_index_by(ans, {'Hlavni':1, 'Domácí':2, 'Ekonomika':5, 'Zahraničí':3, 'Finance':6, 'DigiWeb':7, 'Kultura':8, 'Sport':9, 'Komentáře':4})
#vrati, ale pouze, kdyz je v kategoriich...
ans = [(key, articles[key]) for key in ans if articles.has_key(key)]
return ans

View File

@ -0,0 +1,16 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1295262156(BasicNewsRecipe):
title = u'kath.net'
__author__ = 'Bobus'
oldest_article = 7
max_articles_per_feed = 100
feeds = [(u'kath.net', u'http://www.kath.net/2005/xml/index.xml')]
def print_version(self, url):
return url+"&print=yes"
extra_css = 'td.textb {font-size: medium;}'

View File

@ -27,6 +27,9 @@ class NikkeiNet_sub_economy(BasicNewsRecipe):
{'class':"JSID_basePageMove JSID_baseAsyncSubmit cmn-form_area JSID_optForm_utoken"}, {'class':"JSID_basePageMove JSID_baseAsyncSubmit cmn-form_area JSID_optForm_utoken"},
{'class':"cmn-article_keyword cmn-clearfix"}, {'class':"cmn-article_keyword cmn-clearfix"},
{'class':"cmn-print_headline cmn-clearfix"}, {'class':"cmn-print_headline cmn-clearfix"},
{'class':"cmn-article_list"},
dict(id="ABOUT-NIKKEI"),
{'class':"cmn-sub_market"},
] ]
remove_tags_after = {'class':"cmn-pr_list"} remove_tags_after = {'class':"cmn-pr_list"}

View File

@ -586,105 +586,125 @@ class NYTimes(BasicNewsRecipe):
return self.strip_anchors(soup) return self.strip_anchors(soup)
def postprocess_html(self,soup, True): def postprocess_html(self,soup, True):
try:
if self.one_picture_per_article:
# Remove all images after first
largeImg = soup.find(True, {'class':'articleSpanImage'})
inlineImgs = soup.findAll(True, {'class':'inlineImage module'})
if largeImg:
for inlineImg in inlineImgs:
inlineImg.extract()
else:
if inlineImgs:
firstImg = inlineImgs[0]
for inlineImg in inlineImgs[1:]:
inlineImg.extract()
# Move firstImg before article body
cgFirst = soup.find(True, {'class':re.compile('columnGroup *first')})
if cgFirst:
# Strip all sibling NavigableStrings: noise
navstrings = cgFirst.findAll(text=True, recursive=False)
[ns.extract() for ns in navstrings]
headline_found = False
tag = cgFirst.find(True)
insertLoc = 0
while True:
insertLoc += 1
if hasattr(tag,'class') and tag['class'] == 'articleHeadline':
headline_found = True
break
tag = tag.nextSibling
if not tag:
headline_found = False
break
if headline_found:
cgFirst.insert(insertLoc,firstImg)
else:
self.log(">>> No class:'columnGroup first' found <<<")
except:
self.log("ERROR: One picture per article in postprocess_html")
if self.one_picture_per_article: try:
# Remove all images after first # Change captions to italic
largeImg = soup.find(True, {'class':'articleSpanImage'}) for caption in soup.findAll(True, {'class':'caption'}) :
inlineImgs = soup.findAll(True, {'class':'inlineImage module'}) if caption and len(caption) > 0:
if largeImg: cTag = Tag(soup, "p", [("class", "caption")])
for inlineImg in inlineImgs: c = self.fixChars(self.tag_to_string(caption,use_alt=False)).strip()
inlineImg.extract() mp_off = c.find("More Photos")
else: if mp_off >= 0:
if inlineImgs: c = c[:mp_off]
firstImg = inlineImgs[0] cTag.insert(0, c)
for inlineImg in inlineImgs[1:]: caption.replaceWith(cTag)
inlineImg.extract() except:
# Move firstImg before article body self.log("ERROR: Problem in change captions to italic")
cgFirst = soup.find(True, {'class':re.compile('columnGroup *first')})
if cgFirst:
# Strip all sibling NavigableStrings: noise
navstrings = cgFirst.findAll(text=True, recursive=False)
[ns.extract() for ns in navstrings]
headline_found = False
tag = cgFirst.find(True)
insertLoc = 0
while True:
insertLoc += 1
if hasattr(tag,'class') and tag['class'] == 'articleHeadline':
headline_found = True
break
tag = tag.nextSibling
if not tag:
headline_found = False
break
if headline_found:
cgFirst.insert(insertLoc,firstImg)
else:
self.log(">>> No class:'columnGroup first' found <<<")
# Change captions to italic try:
for caption in soup.findAll(True, {'class':'caption'}) : # Change <nyt_headline> to <h2>
if caption and caption.contents[0]: h1 = soup.find('h1')
cTag = Tag(soup, "p", [("class", "caption")]) if h1:
c = self.fixChars(self.tag_to_string(caption,use_alt=False)).strip() headline = h1.find("nyt_headline")
mp_off = c.find("More Photos") if headline:
if mp_off >= 0: tag = Tag(soup, "h2")
c = c[:mp_off] tag['class'] = "headline"
cTag.insert(0, c) tag.insert(0, self.fixChars(headline.contents[0]))
caption.replaceWith(cTag) h1.replaceWith(tag)
else:
# Blog entry - replace headline, remove <hr> tags
headline = soup.find('title')
if headline:
tag = Tag(soup, "h2")
tag['class'] = "headline"
tag.insert(0, self.fixChars(headline.contents[0]))
soup.insert(0, tag)
hrs = soup.findAll('hr')
for hr in hrs:
hr.extract()
except:
self.log("ERROR: Problem in Change <nyt_headline> to <h2>")
# Change <nyt_headline> to <h2> try:
h1 = soup.find('h1') # Change <h1> to <h3> - used in editorial blogs
if h1: masthead = soup.find("h1")
headline = h1.find("nyt_headline") if masthead:
if headline: # Nuke the href
tag = Tag(soup, "h2") if masthead.a:
tag['class'] = "headline" del(masthead.a['href'])
tag.insert(0, self.fixChars(headline.contents[0])) tag = Tag(soup, "h3")
h1.replaceWith(tag) tag.insert(0, self.fixChars(masthead.contents[0]))
else: masthead.replaceWith(tag)
# Blog entry - replace headline, remove <hr> tags except:
headline = soup.find('title') self.log("ERROR: Problem in Change <h1> to <h3> - used in editorial blogs")
if headline:
tag = Tag(soup, "h2")
tag['class'] = "headline"
tag.insert(0, self.fixChars(headline.contents[0]))
soup.insert(0, tag)
hrs = soup.findAll('hr')
for hr in hrs:
hr.extract()
# Change <h1> to <h3> - used in editorial blogs try:
masthead = soup.find("h1") # Change <span class="bold"> to <b>
if masthead: for subhead in soup.findAll(True, {'class':'bold'}) :
# Nuke the href if subhead.contents:
if masthead.a: bTag = Tag(soup, "b")
del(masthead.a['href']) bTag.insert(0, subhead.contents[0])
tag = Tag(soup, "h3") subhead.replaceWith(bTag)
tag.insert(0, self.fixChars(masthead.contents[0])) except:
masthead.replaceWith(tag) self.log("ERROR: Problem in Change <h1> to <h3> - used in editorial blogs")
# Change <span class="bold"> to <b> try:
for subhead in soup.findAll(True, {'class':'bold'}) : divTag = soup.find('div',attrs={'id':'articleBody'})
if subhead.contents: if divTag:
bTag = Tag(soup, "b") divTag['class'] = divTag['id']
bTag.insert(0, subhead.contents[0]) except:
subhead.replaceWith(bTag) self.log("ERROR: Problem in soup.find(div,attrs={id:articleBody})")
divTag = soup.find('div',attrs={'id':'articleBody'}) try:
if divTag: # Add class="authorId" to <div> so we can format with CSS
divTag['class'] = divTag['id'] divTag = soup.find('div',attrs={'id':'authorId'})
if divTag and divTag.contents[0]:
tag = Tag(soup, "p")
tag['class'] = "authorId"
tag.insert(0, self.fixChars(self.tag_to_string(divTag.contents[0],
use_alt=False)))
divTag.replaceWith(tag)
except:
self.log("ERROR: Problem in Add class=authorId to <div> so we can format with CSS")
# Add class="authorId" to <div> so we can format with CSS return soup
divTag = soup.find('div',attrs={'id':'authorId'})
if divTag and divTag.contents[0]:
tag = Tag(soup, "p")
tag['class'] = "authorId"
tag.insert(0, self.fixChars(self.tag_to_string(divTag.contents[0],
use_alt=False)))
divTag.replaceWith(tag)
return soup
def populate_article_metadata(self, article, soup, first): def populate_article_metadata(self, article, soup, first):
shortparagraph = "" shortparagraph = ""

View File

@ -586,105 +586,125 @@ class NYTimes(BasicNewsRecipe):
return self.strip_anchors(soup) return self.strip_anchors(soup)
def postprocess_html(self,soup, True): def postprocess_html(self,soup, True):
try:
if self.one_picture_per_article:
# Remove all images after first
largeImg = soup.find(True, {'class':'articleSpanImage'})
inlineImgs = soup.findAll(True, {'class':'inlineImage module'})
if largeImg:
for inlineImg in inlineImgs:
inlineImg.extract()
else:
if inlineImgs:
firstImg = inlineImgs[0]
for inlineImg in inlineImgs[1:]:
inlineImg.extract()
# Move firstImg before article body
cgFirst = soup.find(True, {'class':re.compile('columnGroup *first')})
if cgFirst:
# Strip all sibling NavigableStrings: noise
navstrings = cgFirst.findAll(text=True, recursive=False)
[ns.extract() for ns in navstrings]
headline_found = False
tag = cgFirst.find(True)
insertLoc = 0
while True:
insertLoc += 1
if hasattr(tag,'class') and tag['class'] == 'articleHeadline':
headline_found = True
break
tag = tag.nextSibling
if not tag:
headline_found = False
break
if headline_found:
cgFirst.insert(insertLoc,firstImg)
else:
self.log(">>> No class:'columnGroup first' found <<<")
except:
self.log("ERROR: One picture per article in postprocess_html")
if self.one_picture_per_article: try:
# Remove all images after first # Change captions to italic
largeImg = soup.find(True, {'class':'articleSpanImage'}) for caption in soup.findAll(True, {'class':'caption'}) :
inlineImgs = soup.findAll(True, {'class':'inlineImage module'}) if caption and len(caption) > 0:
if largeImg: cTag = Tag(soup, "p", [("class", "caption")])
for inlineImg in inlineImgs: c = self.fixChars(self.tag_to_string(caption,use_alt=False)).strip()
inlineImg.extract() mp_off = c.find("More Photos")
else: if mp_off >= 0:
if inlineImgs: c = c[:mp_off]
firstImg = inlineImgs[0] cTag.insert(0, c)
for inlineImg in inlineImgs[1:]: caption.replaceWith(cTag)
inlineImg.extract() except:
# Move firstImg before article body self.log("ERROR: Problem in change captions to italic")
cgFirst = soup.find(True, {'class':re.compile('columnGroup *first')})
if cgFirst:
# Strip all sibling NavigableStrings: noise
navstrings = cgFirst.findAll(text=True, recursive=False)
[ns.extract() for ns in navstrings]
headline_found = False
tag = cgFirst.find(True)
insertLoc = 0
while True:
insertLoc += 1
if hasattr(tag,'class') and tag['class'] == 'articleHeadline':
headline_found = True
break
tag = tag.nextSibling
if not tag:
headline_found = False
break
if headline_found:
cgFirst.insert(insertLoc,firstImg)
else:
self.log(">>> No class:'columnGroup first' found <<<")
# Change captions to italic try:
for caption in soup.findAll(True, {'class':'caption'}) : # Change <nyt_headline> to <h2>
if caption and caption.contents[0]: h1 = soup.find('h1')
cTag = Tag(soup, "p", [("class", "caption")]) if h1:
c = self.fixChars(self.tag_to_string(caption,use_alt=False)).strip() headline = h1.find("nyt_headline")
mp_off = c.find("More Photos") if headline:
if mp_off >= 0: tag = Tag(soup, "h2")
c = c[:mp_off] tag['class'] = "headline"
cTag.insert(0, c) tag.insert(0, self.fixChars(headline.contents[0]))
caption.replaceWith(cTag) h1.replaceWith(tag)
else:
# Blog entry - replace headline, remove <hr> tags
headline = soup.find('title')
if headline:
tag = Tag(soup, "h2")
tag['class'] = "headline"
tag.insert(0, self.fixChars(headline.contents[0]))
soup.insert(0, tag)
hrs = soup.findAll('hr')
for hr in hrs:
hr.extract()
except:
self.log("ERROR: Problem in Change <nyt_headline> to <h2>")
# Change <nyt_headline> to <h2> try:
h1 = soup.find('h1') # Change <h1> to <h3> - used in editorial blogs
if h1: masthead = soup.find("h1")
headline = h1.find("nyt_headline") if masthead:
if headline: # Nuke the href
tag = Tag(soup, "h2") if masthead.a:
tag['class'] = "headline" del(masthead.a['href'])
tag.insert(0, self.fixChars(headline.contents[0])) tag = Tag(soup, "h3")
h1.replaceWith(tag) tag.insert(0, self.fixChars(masthead.contents[0]))
else: masthead.replaceWith(tag)
# Blog entry - replace headline, remove <hr> tags except:
headline = soup.find('title') self.log("ERROR: Problem in Change <h1> to <h3> - used in editorial blogs")
if headline:
tag = Tag(soup, "h2")
tag['class'] = "headline"
tag.insert(0, self.fixChars(headline.contents[0]))
soup.insert(0, tag)
hrs = soup.findAll('hr')
for hr in hrs:
hr.extract()
# Change <h1> to <h3> - used in editorial blogs try:
masthead = soup.find("h1") # Change <span class="bold"> to <b>
if masthead: for subhead in soup.findAll(True, {'class':'bold'}) :
# Nuke the href if subhead.contents:
if masthead.a: bTag = Tag(soup, "b")
del(masthead.a['href']) bTag.insert(0, subhead.contents[0])
tag = Tag(soup, "h3") subhead.replaceWith(bTag)
tag.insert(0, self.fixChars(masthead.contents[0])) except:
masthead.replaceWith(tag) self.log("ERROR: Problem in Change <h1> to <h3> - used in editorial blogs")
# Change <span class="bold"> to <b> try:
for subhead in soup.findAll(True, {'class':'bold'}) : divTag = soup.find('div',attrs={'id':'articleBody'})
if subhead.contents: if divTag:
bTag = Tag(soup, "b") divTag['class'] = divTag['id']
bTag.insert(0, subhead.contents[0]) except:
subhead.replaceWith(bTag) self.log("ERROR: Problem in soup.find(div,attrs={id:articleBody})")
divTag = soup.find('div',attrs={'id':'articleBody'}) try:
if divTag: # Add class="authorId" to <div> so we can format with CSS
divTag['class'] = divTag['id'] divTag = soup.find('div',attrs={'id':'authorId'})
if divTag and divTag.contents[0]:
tag = Tag(soup, "p")
tag['class'] = "authorId"
tag.insert(0, self.fixChars(self.tag_to_string(divTag.contents[0],
use_alt=False)))
divTag.replaceWith(tag)
except:
self.log("ERROR: Problem in Add class=authorId to <div> so we can format with CSS")
# Add class="authorId" to <div> so we can format with CSS return soup
divTag = soup.find('div',attrs={'id':'authorId'})
if divTag and divTag.contents[0]:
tag = Tag(soup, "p")
tag['class'] = "authorId"
tag.insert(0, self.fixChars(self.tag_to_string(divTag.contents[0],
use_alt=False)))
divTag.replaceWith(tag)
return soup
def populate_article_metadata(self, article, soup, first): def populate_article_metadata(self, article, soup, first):
shortparagraph = "" shortparagraph = ""
try: try:

View File

@ -33,6 +33,6 @@ class SNE(USBMS):
STORAGE_CARD_VOLUME_LABEL = 'SNE Storage Card' STORAGE_CARD_VOLUME_LABEL = 'SNE Storage Card'
EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'Books' EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'Books'
SUPPORTS_SUB_DIRS = False SUPPORTS_SUB_DIRS = True

View File

@ -104,13 +104,17 @@ class FB2Input(InputFormatPlugin):
entries = [(f, guess_type(f)[0]) for f in os.listdir('.')] entries = [(f, guess_type(f)[0]) for f in os.listdir('.')]
opf.create_manifest(entries) opf.create_manifest(entries)
opf.create_spine(['index.xhtml']) opf.create_spine(['index.xhtml'])
if mi.cover_data and mi.cover_data[1]:
for img in doc.xpath('//f:coverpage/f:image', namespaces=NAMESPACES): with open('fb2_cover_calibre_mi.jpg', 'wb') as f:
href = img.get('{%s}href'%XLINK_NS, img.get('href', None)) f.write(mi.cover_data[1])
if href is not None: opf.guide.set_cover(os.path.abspath('fb2_cover_calibre_mi.jpg'))
if href.startswith('#'): else:
href = href[1:] for img in doc.xpath('//f:coverpage/f:image', namespaces=NAMESPACES):
opf.guide.set_cover(os.path.abspath(href)) href = img.get('{%s}href'%XLINK_NS, img.get('href', None))
if href is not None:
if href.startswith('#'):
href = href[1:]
opf.guide.set_cover(os.path.abspath(href))
opf.render(open('metadata.opf', 'wb')) opf.render(open('metadata.opf', 'wb'))
return os.path.join(os.getcwd(), 'metadata.opf') return os.path.join(os.getcwd(), 'metadata.opf')

View File

@ -542,7 +542,17 @@ class MobiReader(object):
elif tag.tag == 'img': elif tag.tag == 'img':
tag.set('height', height) tag.set('height', height)
else: else:
styles.append('margin-top: %s' % self.ensure_unit(height)) if tag.tag == 'div' and not tag.text and \
(not tag.tail or not tag.tail.strip()) and \
not len(list(tag.iterdescendants())):
# Paragraph spacer
# Insert nbsp so that the element is never
# discarded by a renderer
tag.text = u'\u00a0' # nbsp
styles.append('height: %s' %
self.ensure_unit(height))
else:
styles.append('margin-top: %s' % self.ensure_unit(height))
if attrib.has_key('width'): if attrib.has_key('width'):
width = attrib.pop('width').strip() width = attrib.pop('width').strip()
if width and re.search(r'\d+', width): if width and re.search(r'\d+', width):

View File

@ -251,7 +251,7 @@ class Serializer(object):
tag = prefixname(elem.tag, nsrmap) tag = prefixname(elem.tag, nsrmap)
# Previous layers take care of @name # Previous layers take care of @name
id = elem.attrib.pop('id', None) id = elem.attrib.pop('id', None)
if id is not None: if id:
href = '#'.join((item.href, id)) href = '#'.join((item.href, id))
offset = self.anchor_offset or buffer.tell() offset = self.anchor_offset or buffer.tell()
self.id_offsets[urlnormalize(href)] = offset self.id_offsets[urlnormalize(href)] = offset

View File

@ -227,7 +227,7 @@ class EbookIterator(object):
self.log.warn('Missing spine item:', repr(spath)) self.log.warn('Missing spine item:', repr(spath))
cover = self.opf.cover cover = self.opf.cover
if self.ebook_ext in ('lit', 'mobi', 'prc', 'opf') and cover: if self.ebook_ext in ('lit', 'mobi', 'prc', 'opf', 'fb2') and cover:
cfile = os.path.join(self.base, 'calibre_iterator_cover.html') cfile = os.path.join(self.base, 'calibre_iterator_cover.html')
chtml = (TITLEPAGE%os.path.relpath(cover, self.base).replace(os.sep, chtml = (TITLEPAGE%os.path.relpath(cover, self.base).replace(os.sep,
'/')).encode('utf-8') '/')).encode('utf-8')

View File

@ -34,18 +34,15 @@ class PML_HTMLizer(object):
'ra', 'ra',
'c', 'c',
'r', 'r',
't',
's', 's',
'l', 'l',
'k', 'k',
'T',
'FN', 'FN',
'SB', 'SB',
] ]
STATES_VALUE_REQ = [ STATES_VALUE_REQ = [
'a', 'a',
'T',
'FN', 'FN',
'SB', 'SB',
] ]
@ -96,8 +93,6 @@ class PML_HTMLizer(object):
'Sb': 'sb', 'Sb': 'sb',
'c': 'c', 'c': 'c',
'r': 'r', 'r': 'r',
't': 't',
'T': 'T',
'i': 'i', 'i': 'i',
'I': 'i', 'I': 'i',
'u': 'u', 'u': 'u',
@ -133,8 +128,6 @@ class PML_HTMLizer(object):
DIV_STATES = [ DIV_STATES = [
'c', 'c',
'r', 'r',
't',
'T',
'FN', 'FN',
'SB', 'SB',
] ]
@ -255,8 +248,6 @@ class PML_HTMLizer(object):
for key, val in self.state.items(): for key, val in self.state.items():
if val[0]: if val[0]:
if key == 'T':
self.state['T'][0] = False
if key in self.DIV_STATES: if key in self.DIV_STATES:
div.append(key) div.append(key)
elif key in self.SPAN_STATES: elif key in self.SPAN_STATES:
@ -506,6 +497,9 @@ class PML_HTMLizer(object):
self.toc = TOC() self.toc = TOC()
self.file_name = file_name self.file_name = file_name
indent_state = {'t': False, 'T': False}
adv_indent_val = ''
for s in self.STATES: for s in self.STATES:
self.state[s] = [False, '']; self.state[s] = [False, ''];
@ -515,6 +509,8 @@ class PML_HTMLizer(object):
parsed = [] parsed = []
empty = True empty = True
basic_indent = indent_state['t']
adv_indent = indent_state['T']
# Must use StringIO, cStringIO does not support unicode # Must use StringIO, cStringIO does not support unicode
line = StringIO.StringIO(line) line = StringIO.StringIO(line)
@ -527,7 +523,7 @@ class PML_HTMLizer(object):
if c == '\\': if c == '\\':
c = line.read(1) c = line.read(1)
if c in 'qcrtTiIuobBlk': if c in 'qcriIuobBlk':
text = self.process_code(c, line) text = self.process_code(c, line)
elif c in 'FS': elif c in 'FS':
l = line.read(1) l = line.read(1)
@ -574,6 +570,15 @@ class PML_HTMLizer(object):
elif c == 'w': elif c == 'w':
empty = False empty = False
text = '<hr width="%s" />' % self.code_value(line) text = '<hr width="%s" />' % self.code_value(line)
elif c == 't':
indent_state[c] = not indent_state[c]
if indent_state[c]:
basic_indent = True
elif c == 'T':
indent_state[c] = not indent_state[c]
if indent_state[c]:
adv_indent = True
adv_indent_val = self.code_value(line)
elif c == '-': elif c == '-':
empty = False empty = False
text = '&shy;' text = '&shy;'
@ -590,6 +595,16 @@ class PML_HTMLizer(object):
if not empty: if not empty:
text = self.end_line() text = self.end_line()
parsed.append(text) parsed.append(text)
if basic_indent:
parsed.insert(0, self.STATES_TAGS['t'][0])
parsed.append(self.STATES_TAGS['t'][1])
elif adv_indent:
parsed.insert(0, self.STATES_TAGS['T'][0] % adv_indent_val)
parsed.append(self.STATES_TAGS['T'][1])
indent_state['T'] = False
adv_indent_val = ''
output.append(u''.join(parsed)) output.append(u''.join(parsed))
line.close() line.close()

View File

@ -85,7 +85,7 @@ def _config():
c.add_opt('LRF_ebook_viewer_options', default=None, c.add_opt('LRF_ebook_viewer_options', default=None,
help=_('Options for the LRF ebook viewer')) help=_('Options for the LRF ebook viewer'))
c.add_opt('internally_viewed_formats', default=['LRF', 'EPUB', 'LIT', c.add_opt('internally_viewed_formats', default=['LRF', 'EPUB', 'LIT',
'MOBI', 'PRC', 'HTML', 'FB2', 'PDB', 'RB', 'SNB'], 'MOBI', 'PRC', 'AZW', 'HTML', 'FB2', 'PDB', 'RB', 'SNB'],
help=_('Formats that are viewed using the internal viewer')) help=_('Formats that are viewed using the internal viewer'))
c.add_opt('column_map', default=ALL_COLUMNS, c.add_opt('column_map', default=ALL_COLUMNS,
help=_('Columns to be displayed in the book list')) help=_('Columns to be displayed in the book list'))

View File

@ -91,13 +91,14 @@ class AddAction(InterfaceAction):
self.gui.library_view.model().db.import_book(MetaInformation(None), []) self.gui.library_view.model().db.import_book(MetaInformation(None), [])
self.gui.library_view.model().books_added(num) self.gui.library_view.model().books_added(num)
def add_isbns(self, books): def add_isbns(self, books, add_tags=[]):
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
ids = set([]) ids = set([])
db = self.gui.library_view.model().db
for x in books: for x in books:
mi = MetaInformation(None) mi = MetaInformation(None)
mi.isbn = x['isbn'] mi.isbn = x['isbn']
db = self.gui.library_view.model().db
if x['path'] is not None: if x['path'] is not None:
ids.add(db.import_book(mi, [x['path']])) ids.add(db.import_book(mi, [x['path']]))
else: else:
@ -109,6 +110,8 @@ class AddAction(InterfaceAction):
self.gui.iactions['Edit Metadata'].do_download_metadata(ids) self.gui.iactions['Edit Metadata'].do_download_metadata(ids)
finally: finally:
config['overwrite_author_title_metadata'] = orig config['overwrite_author_title_metadata'] = orig
if add_tags and ids:
db.bulk_modify_tags(ids, add=add_tags)
def files_dropped(self, paths): def files_dropped(self, paths):
@ -166,7 +169,7 @@ class AddAction(InterfaceAction):
from calibre.gui2.dialogs.add_from_isbn import AddFromISBN from calibre.gui2.dialogs.add_from_isbn import AddFromISBN
d = AddFromISBN(self.gui) d = AddFromISBN(self.gui)
if d.exec_() == d.Accepted: if d.exec_() == d.Accepted:
self.add_isbns(d.books) self.add_isbns(d.books, add_tags=d.set_tags)
def add_books(self, *args): def add_books(self, *args):
''' '''

View File

@ -5,11 +5,11 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, shutil import re, os, shutil
from PyQt4.Qt import QModelIndex from PyQt4.Qt import QModelIndex
from calibre.gui2 import error_dialog, choose_dir from calibre.gui2 import choose_dir, error_dialog, warning_dialog
from calibre.gui2.tools import generate_catalog from calibre.gui2.tools import generate_catalog
from calibre.utils.config import dynamic from calibre.utils.config import dynamic
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
@ -55,10 +55,18 @@ class GenerateCatalogAction(InterfaceAction):
def catalog_generated(self, job): def catalog_generated(self, job):
if job.result: if job.result:
# Error during catalog generation # Problems during catalog generation
return error_dialog(self.gui, _('Catalog generation terminated'), # jobs.results is a list - the first entry is the intended title for the dialog
job.result, # Subsequent strings are error messages
show=True) dialog_title = job.result.pop(0)
if re.match('warning:', job.result[0].lower()):
job.result.append("Catalog generation complete.")
warning_dialog(self.gui, dialog_title, '\n'.join(job.result), show=True)
else:
job.result.append("Catalog generation terminated.")
error_dialog(self.gui, dialog_title,'\n'.join(job.result),show=True)
return
if job.failed: if job.failed:
return self.gui.job_exception(job) return self.gui.job_exception(job)
id = self.gui.library_view.model().add_catalog(job.catalog_file_path, job.catalog_title) id = self.gui.library_view.model().add_catalog(job.catalog_file_path, job.catalog_title)

View File

@ -68,6 +68,9 @@ class MetadataWidget(Widget, Ui_Form):
def initialize_metadata_options(self): def initialize_metadata_options(self):
self.initialize_combos() self.initialize_combos()
self.author.editTextChanged.connect(self.deduce_author_sort) self.author.editTextChanged.connect(self.deduce_author_sort)
self.author.set_separator('&')
self.author.set_space_before_sep(True)
self.author.update_items_cache(self.db.all_author_names())
mi = self.db.get_metadata(self.book_id, index_is_id=True) mi = self.db.get_metadata(self.book_id, index_is_id=True)
self.title.setText(mi.title) self.title.setText(mi.title)
@ -75,7 +78,7 @@ class MetadataWidget(Widget, Ui_Form):
self.publisher.setCurrentIndex(self.publisher.findText(mi.publisher)) self.publisher.setCurrentIndex(self.publisher.findText(mi.publisher))
self.author_sort.setText(mi.author_sort if mi.author_sort else '') self.author_sort.setText(mi.author_sort if mi.author_sort else '')
self.tags.setText(', '.join(mi.tags if mi.tags else [])) self.tags.setText(', '.join(mi.tags if mi.tags else []))
self.tags.update_tags_cache(self.db.all_tags()) self.tags.update_items_cache(self.db.all_tags())
self.comment.setPlainText(mi.comments if mi.comments else '') self.comment.setPlainText(mi.comments if mi.comments else '')
if mi.series: if mi.series:
self.series.setCurrentIndex(self.series.findText(mi.series)) self.series.setCurrentIndex(self.series.findText(mi.series))

View File

@ -190,7 +190,7 @@
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="4" column="1">
<widget class="TagsLineEdit" name="tags"> <widget class="CompleteLineEdit" name="tags">
<property name="toolTip"> <property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string> <string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property> </property>
@ -255,7 +255,7 @@
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="EnComboBox" name="author"> <widget class="CompleteComboBox" name="author">
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -310,7 +310,12 @@
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>TagsLineEdit</class> <class>CompleteComboBox</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>CompleteLineEdit</class>
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>

View File

@ -14,7 +14,7 @@ from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
QPushButton QPushButton
from calibre.utils.date import qt_to_dt, now from calibre.utils.date import qt_to_dt, now
from calibre.gui2.widgets import TagsLineEdit, EnComboBox from calibre.gui2.widgets import CompleteLineEdit, EnComboBox
from calibre.gui2.comments_editor import Editor as CommentsEditor from calibre.gui2.comments_editor import Editor as CommentsEditor
from calibre.gui2 import UNDEFINED_QDATE, error_dialog from calibre.gui2 import UNDEFINED_QDATE, error_dialog
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
@ -212,7 +212,7 @@ class Text(Base):
values = self.all_values = list(self.db.all_custom(num=self.col_id)) values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key) values.sort(key=sort_key)
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
w = TagsLineEdit(parent, values) w = CompleteLineEdit(parent, values)
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
else: else:
w = EnComboBox(parent) w = EnComboBox(parent)
@ -226,7 +226,7 @@ class Text(Base):
val = self.normalize_db_val(val) val = self.normalize_db_val(val)
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
self.setter(val) self.setter(val)
self.widgets[1].update_tags_cache(self.all_values) self.widgets[1].update_items_cache(self.all_values)
else: else:
idx = None idx = None
for i, c in enumerate(self.all_values): for i, c in enumerate(self.all_values):
@ -656,7 +656,7 @@ class RemoveTags(QWidget):
layout.setSpacing(5) layout.setSpacing(5)
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
self.tags_box = TagsLineEdit(parent, values) self.tags_box = CompleteLineEdit(parent, values)
layout.addWidget(self.tags_box, stretch = 1) layout.addWidget(self.tags_box, stretch = 1)
# self.tags_box.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) # self.tags_box.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
@ -678,7 +678,7 @@ class BulkText(BulkBase):
values = self.all_values = list(self.db.all_custom(num=self.col_id)) values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key) values.sort(key=sort_key)
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
w = TagsLineEdit(parent, values) w = CompleteLineEdit(parent, values)
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.widgets = [QLabel('&'+self.col_metadata['name']+': ' + self.widgets = [QLabel('&'+self.col_metadata['name']+': ' +
_('tags to add'), parent), w] _('tags to add'), parent), w]
@ -697,7 +697,7 @@ class BulkText(BulkBase):
def initialize(self, book_ids): def initialize(self, book_ids):
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
self.widgets[1].update_tags_cache(self.all_values) self.widgets[1].update_items_cache(self.all_values)
else: else:
val = self.get_initial_value(book_ids) val = self.get_initial_value(book_ids)
self.initial_val = val = self.normalize_db_val(val) self.initial_val = val = self.normalize_db_val(val)

View File

@ -1018,7 +1018,8 @@ class DeviceMixin(object): # {{{
ids = [self.library_view.model().id(r) \ ids = [self.library_view.model().id(r) \
for r in self.library_view.selectionModel().selectedRows()] \ for r in self.library_view.selectionModel().selectedRows()] \
if send_ids is None else send_ids if send_ids is None else send_ids
if not self.device_manager or not ids or len(ids) == 0: if not self.device_manager or not ids or len(ids) == 0 or \
not self.device_manager.is_device_connected:
return return
settings = self.device_manager.device.settings() settings = self.device_manager.device.settings()

View File

@ -12,6 +12,7 @@ from PyQt4.Qt import QDialog, QApplication
from calibre.gui2.dialogs.add_from_isbn_ui import Ui_Dialog from calibre.gui2.dialogs.add_from_isbn_ui import Ui_Dialog
from calibre.ebooks.metadata import check_isbn from calibre.ebooks.metadata import check_isbn
from calibre.constants import iswindows from calibre.constants import iswindows
from calibre.gui2 import gprefs
class AddFromISBN(QDialog, Ui_Dialog): class AddFromISBN(QDialog, Ui_Dialog):
@ -25,7 +26,9 @@ class AddFromISBN(QDialog, Ui_Dialog):
self.isbns = [] self.isbns = []
self.books = [] self.books = []
self.set_tags = []
self.paste_button.clicked.connect(self.paste) self.paste_button.clicked.connect(self.paste)
self.add_tags.setText(', '.join(gprefs.get('add from ISBN tags', [])))
def paste(self, *args): def paste(self, *args):
app = QApplication.instance() app = QApplication.instance()
@ -37,6 +40,10 @@ class AddFromISBN(QDialog, Ui_Dialog):
self.isbn_box.setPlainText(new) self.isbn_box.setPlainText(new)
def accept(self, *args): def accept(self, *args):
tags = unicode(self.add_tags.text()).strip().split(',')
tags = list(filter(None, [x.strip() for x in tags]))
gprefs['add from ISBN tags'] = tags
self.set_tags = tags
for line in unicode(self.isbn_box.toPlainText()).strip().splitlines(): for line in unicode(self.isbn_box.toPlainText()).strip().splitlines():
line = line.strip() line = line.strip()
if not line: if not line:

View File

@ -18,8 +18,19 @@
<normaloff>:/images/add_book.png</normaloff>:/images/add_book.png</iconset> <normaloff>:/images/add_book.png</normaloff>:/images/add_book.png</iconset>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="0" column="0"> <item row="0" column="0" rowspan="2">
<widget class="QPlainTextEdit" name="isbn_box"/> <layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QPlainTextEdit" name="isbn_box"/>
</item>
<item>
<widget class="QPushButton" name="paste_button">
<property name="text">
<string>&amp;Paste from clipboard</string>
</property>
</widget>
</item>
</layout>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
@ -34,6 +45,36 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Tags to set on created book entries:</string>
</property>
<property name="buddy">
<cstring>add_tags</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="add_tags"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="2" column="0" colspan="2"> <item row="2" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation"> <property name="orientation">
@ -44,13 +85,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0">
<widget class="QPushButton" name="paste_button">
<property name="text">
<string>&amp;Paste from clipboard</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources> <resources>

View File

@ -15,15 +15,16 @@ from calibre.ebooks.metadata import string_to_authors, authors_to_string
from calibre.ebooks.metadata.book.base import composite_formatter from calibre.ebooks.metadata.book.base import composite_formatter
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.gui2 import error_dialog, ResizableDialog from calibre.gui2 import error_dialog, ResizableDialog, UNDEFINED_QDATE
from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.utils.config import dynamic from calibre.utils.config import dynamic
from calibre.utils.titlecase import titlecase from calibre.utils.titlecase import titlecase
from calibre.utils.icu import sort_key, capitalize from calibre.utils.icu import sort_key, capitalize
from calibre.utils.config import prefs from calibre.utils.config import prefs, tweaks
from calibre.utils.magick.draw import identify_data from calibre.utils.magick.draw import identify_data
from calibre.utils.date import qt_to_dt
def get_cover_data(path): def get_cover_data(path): # {{{
old = prefs['read_file_metadata'] old = prefs['read_file_metadata']
if not old: if not old:
prefs['read_file_metadata'] = True prefs['read_file_metadata'] = True
@ -46,8 +47,7 @@ def get_cover_data(path):
prefs['read_file_metadata'] = old prefs['read_file_metadata'] = old
return cdata, area return cdata, area
# }}}
class MyBlockingBusy(QDialog): # {{{ class MyBlockingBusy(QDialog): # {{{
@ -132,7 +132,8 @@ class MyBlockingBusy(QDialog): # {{{
remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, \ remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, \
do_autonumber, do_remove_format, remove_format, do_swap_ta, \ do_autonumber, do_remove_format, remove_format, do_swap_ta, \
do_remove_conv, do_auto_author, series, do_series_restart, \ do_remove_conv, do_auto_author, series, do_series_restart, \
series_start_value, do_title_case, cover_action, clear_series = self.args series_start_value, do_title_case, cover_action, clear_series, \
pubdate, adddate = self.args
# first loop: do author and title. These will commit at the end of each # first loop: do author and title. These will commit at the end of each
@ -209,6 +210,12 @@ class MyBlockingBusy(QDialog): # {{{
if clear_series: if clear_series:
self.db.set_series(id, '', notify=False, commit=False) self.db.set_series(id, '', notify=False, commit=False)
if pubdate is not None:
self.db.set_pubdate(id, pubdate, notify=False, commit=False)
if adddate is not None:
self.db.set_timestamp(id, adddate, notify=False, commit=False)
if do_series: if do_series:
if do_series_restart: if do_series_restart:
if self.series_start_value is None: if self.series_start_value is None:
@ -274,8 +281,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.changed = False self.changed = False
all_tags = self.db.all_tags() all_tags = self.db.all_tags()
self.tags.update_tags_cache(all_tags) self.tags.update_items_cache(all_tags)
self.remove_tags.update_tags_cache(all_tags) self.remove_tags.update_items_cache(all_tags)
self.initialize_combos() self.initialize_combos()
@ -288,6 +295,17 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.series.editTextChanged.connect(self.series_changed) self.series.editTextChanged.connect(self.series_changed)
self.tag_editor_button.clicked.connect(self.tag_editor) self.tag_editor_button.clicked.connect(self.tag_editor)
self.autonumber_series.stateChanged[int].connect(self.auto_number_changed) self.autonumber_series.stateChanged[int].connect(self.auto_number_changed)
self.pubdate.setMinimumDate(UNDEFINED_QDATE)
pubdate_format = tweaks['gui_pubdate_display_format']
if pubdate_format is not None:
self.pubdate.setDisplayFormat(pubdate_format)
self.pubdate.setSpecialValueText(_('Undefined'))
self.clear_pubdate_button.clicked.connect(self.clear_pubdate)
self.pubdate.dateChanged.connect(self.do_apply_pubdate)
self.adddate.setMinimumDate(UNDEFINED_QDATE)
self.adddate.setSpecialValueText(_('Undefined'))
self.clear_adddate_button.clicked.connect(self.clear_adddate)
self.adddate.dateChanged.connect(self.do_apply_adddate)
if len(self.db.custom_field_keys(include_composites=False)) == 0: if len(self.db.custom_field_keys(include_composites=False)) == 0:
self.central_widget.removeTab(1) self.central_widget.removeTab(1)
@ -304,6 +322,18 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.central_widget.setCurrentIndex(tab) self.central_widget.setCurrentIndex(tab)
self.exec_() self.exec_()
def do_apply_pubdate(self, *args):
self.apply_pubdate.setChecked(True)
def clear_pubdate(self, *args):
self.pubdate.setDate(UNDEFINED_QDATE)
def do_apply_adddate(self, *args):
self.apply_adddate.setChecked(True)
def clear_adddate(self, *args):
self.adddate.setDate(UNDEFINED_QDATE)
def button_clicked(self, which): def button_clicked(self, which):
if which == self.button_box.button(QDialogButtonBox.Apply): if which == self.button_box.button(QDialogButtonBox.Apply):
self.do_again = True self.do_again = True
@ -709,6 +739,10 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.authors.addItem(name) self.authors.addItem(name)
self.authors.setEditText('') self.authors.setEditText('')
self.authors.set_separator('&')
self.authors.set_space_before_sep(True)
self.authors.update_items_cache(self.db.all_author_names())
def initialize_series(self): def initialize_series(self):
all_series = self.db.all_series() all_series = self.db.all_series()
all_series.sort(key=lambda x : sort_key(x[1])) all_series.sort(key=lambda x : sort_key(x[1]))
@ -733,8 +767,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
if d.result() == QDialog.Accepted: if d.result() == QDialog.Accepted:
tag_string = ', '.join(d.tags) tag_string = ', '.join(d.tags)
self.tags.setText(tag_string) self.tags.setText(tag_string)
self.tags.update_tags_cache(self.db.all_tags()) self.tags.update_items_cache(self.db.all_tags())
self.remove_tags.update_tags_cache(self.db.all_tags()) self.remove_tags.update_items_cache(self.db.all_tags())
def auto_number_changed(self, state): def auto_number_changed(self, state):
if state: if state:
@ -783,6 +817,12 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
do_remove_conv = self.remove_conversion_settings.isChecked() do_remove_conv = self.remove_conversion_settings.isChecked()
do_auto_author = self.auto_author_sort.isChecked() do_auto_author = self.auto_author_sort.isChecked()
do_title_case = self.change_title_to_title_case.isChecked() do_title_case = self.change_title_to_title_case.isChecked()
pubdate = adddate = None
if self.apply_pubdate.isChecked():
pubdate = qt_to_dt(self.pubdate.date())
if self.apply_adddate.isChecked():
adddate = qt_to_dt(self.adddate.date())
cover_action = None cover_action = None
if self.cover_remove.isChecked(): if self.cover_remove.isChecked():
cover_action = 'remove' cover_action = 'remove'
@ -794,7 +834,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
args = (remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, args = (remove_all, remove, add, au, aus, do_aus, rating, pub, do_series,
do_autonumber, do_remove_format, remove_format, do_swap_ta, do_autonumber, do_remove_format, remove_format, do_swap_ta,
do_remove_conv, do_auto_author, series, do_series_restart, do_remove_conv, do_auto_author, series, do_series_restart,
series_start_value, do_title_case, cover_action, clear_series) series_start_value, do_title_case, cover_action, clear_series,
pubdate, adddate)
bb = MyBlockingBusy(_('Applying changes to %d books.\nPhase {0} {1}%%.') bb = MyBlockingBusy(_('Applying changes to %d books.\nPhase {0} {1}%%.')
%len(self.ids), args, self.db, self.ids, %len(self.ids), args, self.db, self.ids,

View File

@ -75,13 +75,31 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="0" column="1">
<widget class="QCheckBox" name="auto_author_sort"> <widget class="CompleteComboBox" name="authors">
<property name="text"> <property name="editable">
<string>A&amp;utomatically set author sort</string> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QCheckBox" name="auto_author_sort">
<property name="text">
<string>A&amp;utomatically set author sort</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="swap_title_and_author">
<property name="text">
<string>&amp;Swap title and author</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0"> <item row="2" column="0">
<widget class="QLabel" name="label_8"> <widget class="QLabel" name="label_8">
<property name="text"> <property name="text">
@ -95,7 +113,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1" colspan="2"> <item row="2" column="1">
<widget class="EnLineEdit" name="author_sort"> <widget class="EnLineEdit" name="author_sort">
<property name="toolTip"> <property name="toolTip">
<string>Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles.</string> <string>Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles.</string>
@ -115,7 +133,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1" colspan="2"> <item row="3" column="1">
<widget class="QSpinBox" name="rating"> <widget class="QSpinBox" name="rating">
<property name="toolTip"> <property name="toolTip">
<string>Rating of this book. 0-5 stars</string> <string>Rating of this book. 0-5 stars</string>
@ -156,7 +174,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1" colspan="2"> <item row="4" column="1">
<widget class="EnComboBox" name="publisher"> <widget class="EnComboBox" name="publisher">
<property name="editable"> <property name="editable">
<bool>true</bool> <bool>true</bool>
@ -177,7 +195,7 @@
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="5" column="1">
<widget class="TagsLineEdit" name="tags"> <widget class="CompleteLineEdit" name="tags">
<property name="toolTip"> <property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string> <string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property> </property>
@ -202,13 +220,16 @@
<property name="text"> <property name="text">
<string>&amp;Remove tags:</string> <string>&amp;Remove tags:</string>
</property> </property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy"> <property name="buddy">
<cstring>remove_tags</cstring> <cstring>remove_tags</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="6" column="1">
<widget class="TagsLineEdit" name="remove_tags"> <widget class="CompleteLineEdit" name="remove_tags">
<property name="toolTip"> <property name="toolTip">
<string>Comma separated list of tags to remove from the books. </string> <string>Comma separated list of tags to remove from the books. </string>
</property> </property>
@ -220,7 +241,7 @@
<string>Check this box to remove all tags from the books.</string> <string>Check this box to remove all tags from the books.</string>
</property> </property>
<property name="text"> <property name="text">
<string>Remove all</string> <string>Remove &amp;all</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -241,52 +262,44 @@
</widget> </widget>
</item> </item>
<item row="7" column="1"> <item row="7" column="1">
<layout class="QHBoxLayout" name="HLayout_34"> <widget class="EnComboBox" name="series">
<item> <property name="sizePolicy">
<widget class="EnComboBox" name="series"> <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<property name="toolTip"> <horstretch>0</horstretch>
<string>List of known series. You can add new series.</string> <verstretch>0</verstretch>
</property> </sizepolicy>
<property name="whatsThis"> </property>
<string>List of known series. You can add new series.</string> <property name="toolTip">
</property> <string>List of known series. You can add new series.</string>
<property name="editable"> </property>
<bool>true</bool> <property name="whatsThis">
</property> <string>List of known series. You can add new series.</string>
<property name="insertPolicy"> </property>
<enum>QComboBox::InsertAlphabetically</enum> <property name="editable">
</property> <bool>true</bool>
<property name="sizeAdjustPolicy"> </property>
<enum>QComboBox::AdjustToContents</enum> <property name="insertPolicy">
</property> <enum>QComboBox::InsertAlphabetically</enum>
</widget> </property>
</item> <property name="sizeAdjustPolicy">
<item> <enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
<widget class="QCheckBox" name="clear_series"> </property>
<property name="toolTip"> <property name="minimumContentsLength">
<string>If checked, the series will be cleared</string> <number>40</number>
</property> </property>
<property name="text"> </widget>
<string>Clear series</string>
</property>
</widget>
</item>
<item>
<spacer name="HSpacer_344">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item> </item>
<item row="8" column="1" colspan="2"> <item row="7" column="2">
<widget class="QCheckBox" name="clear_series">
<property name="toolTip">
<string>If checked, the series will be cleared</string>
</property>
<property name="text">
<string>&amp;Clear series</string>
</property>
</widget>
</item>
<item row="8" column="1">
<layout class="QHBoxLayout" name="HLayout_3"> <layout class="QHBoxLayout" name="HLayout_3">
<item> <item>
<widget class="QCheckBox" name="autonumber_series"> <widget class="QCheckBox" name="autonumber_series">
@ -297,7 +310,7 @@ you selected them. So if you selected Book A and then Book B,
Book A will have series number 1 and Book B series number 2.</string> Book A will have series number 1 and Book B series number 2.</string>
</property> </property>
<property name="text"> <property name="text">
<string>Automatically number books in this series</string> <string>&amp;Automatically number books in this series</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -312,7 +325,7 @@ for that series. Checking this box will tell calibre to start numbering
from the value in the box</string> from the value in the box</string>
</property> </property>
<property name="text"> <property name="text">
<string>Force numbers to start with </string> <string>&amp;Force numbers to start with:</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -332,22 +345,102 @@ from the value in the box</string>
</property> </property>
</widget> </widget>
</item> </item>
<item>
<spacer name="HSpacer_34">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
</layout> </layout>
</item> </item>
<item row="9" column="0"> <item row="9" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>&amp;Date:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>adddate</cstring>
</property>
</widget>
</item>
<item row="9" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QDateEdit" name="adddate">
<property name="displayFormat">
<string>d MMM yyyy</string>
</property>
<property name="calendarPopup">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="clear_adddate_button">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="9" column="2">
<widget class="QCheckBox" name="apply_adddate">
<property name="text">
<string>&amp;Apply date</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>&amp;Published:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>pubdate</cstring>
</property>
</widget>
</item>
<item row="10" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QDateEdit" name="pubdate">
<property name="displayFormat">
<string>MMM yyyy</string>
</property>
<property name="calendarPopup">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="clear_pubdate_button">
<property name="toolTip">
<string>Clear published date</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="10" column="2">
<widget class="QCheckBox" name="apply_pubdate">
<property name="text">
<string>&amp;Apply date</string>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="text"> <property name="text">
<string>Remove &amp;format:</string> <string>Remove &amp;format:</string>
@ -357,60 +450,73 @@ from the value in the box</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="9" column="1"> <item row="11" column="1">
<widget class="QComboBox" name="remove_format"/> <widget class="QComboBox" name="remove_format">
</item> <property name="maximumSize">
<item row="0" column="1"> <size>
<widget class="EnComboBox" name="authors"> <width>120</width>
<property name="editable"> <height>16777215</height>
<bool>true</bool> </size>
</property> </property>
</widget> </widget>
</item> </item>
<item row="11" column="0" colspan="2"> <item row="12" column="0">
<widget class="QCheckBox" name="swap_title_and_author"> <spacer name="verticalSpacer">
<property name="text">
<string>&amp;Swap title and author</string>
</property>
</widget>
</item>
<item row="12" column="0" colspan="2">
<widget class="QCheckBox" name="change_title_to_title_case">
<property name="toolTip">
<string>Force the title to be in title case. If both this and swap authors are checked,
title and author are swapped before the title case is set</string>
</property>
<property name="text">
<string>Change title to title case</string>
</property>
</widget>
</item>
<item row="10" column="0" colspan="2">
<widget class="QCheckBox" name="remove_conversion_settings">
<property name="toolTip">
<string>Remove stored conversion settings for the selected books.
Future conversion of these books will use the default settings.</string>
</property>
<property name="text">
<string>Remove &amp;stored conversion settings for the selected books</string>
</property>
</widget>
</item>
<item row="14" column="0" colspan="3">
<spacer name="verticalSpacer_2">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
</property> </property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>20</width> <width>20</width>
<height>40</height> <height>15</height>
</size> </size>
</property> </property>
</spacer> </spacer>
</item> </item>
<item row="13" column="0" colspan="3"> <item row="13" column="0" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QCheckBox" name="change_title_to_title_case">
<property name="toolTip">
<string>Force the title to be in title case. If both this and swap authors are checked,
title and author are swapped before the title case is set</string>
</property>
<property name="text">
<string>Change title to title &amp;case</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="remove_conversion_settings">
<property name="toolTip">
<string>Remove stored conversion settings for the selected books.
Future conversion of these books will use the default settings.</string>
</property>
<property name="text">
<string>Remove &amp;stored conversion settings for the selected books</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="14" column="0" colspan="3">
<widget class="QGroupBox" name="groupBox"> <widget class="QGroupBox" name="groupBox">
<property name="title"> <property name="title">
<string>Change &amp;cover</string> <string>Change &amp;cover</string>
@ -440,6 +546,19 @@ Future conversion of these books will use the default settings.</string>
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="15" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tab"> <widget class="QWidget" name="tab">
@ -881,7 +1000,12 @@ not multiple and the destination field is multiple</string>
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>TagsLineEdit</class> <class>CompleteComboBox</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>CompleteLineEdit</class>
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
@ -902,14 +1026,9 @@ not multiple and the destination field is multiple</string>
<tabstop>remove_tags</tabstop> <tabstop>remove_tags</tabstop>
<tabstop>remove_all_tags</tabstop> <tabstop>remove_all_tags</tabstop>
<tabstop>series</tabstop> <tabstop>series</tabstop>
<tabstop>clear_series</tabstop>
<tabstop>autonumber_series</tabstop> <tabstop>autonumber_series</tabstop>
<tabstop>series_numbering_restarts</tabstop> <tabstop>series_numbering_restarts</tabstop>
<tabstop>series_start_number</tabstop> <tabstop>series_start_number</tabstop>
<tabstop>remove_format</tabstop>
<tabstop>remove_conversion_settings</tabstop>
<tabstop>swap_title_and_author</tabstop>
<tabstop>change_title_to_title_case</tabstop>
<tabstop>button_box</tabstop> <tabstop>button_box</tabstop>
<tabstop>search_field</tabstop> <tabstop>search_field</tabstop>
<tabstop>search_mode</tabstop> <tabstop>search_mode</tabstop>

View File

@ -16,7 +16,7 @@ from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QDate, \
from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \ from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \
choose_files, choose_images, ResizableDialog, \ choose_files, choose_images, ResizableDialog, \
warning_dialog, question_dialog warning_dialog, question_dialog, UNDEFINED_QDATE
from calibre.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog from calibre.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog
from calibre.gui2.dialogs.fetch_metadata import FetchMetadata from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.gui2.dialogs.tag_editor import TagEditor
@ -491,11 +491,15 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.formats.setAcceptDrops(True) self.formats.setAcceptDrops(True)
self.cover_changed = False self.cover_changed = False
self.cpixmap = None self.cpixmap = None
self.pubdate.setMinimumDate(QDate(100,1,1)) self.pubdate.setMinimumDate(UNDEFINED_QDATE)
pubdate_format = tweaks['gui_pubdate_display_format'] pubdate_format = tweaks['gui_pubdate_display_format']
if pubdate_format is not None: if pubdate_format is not None:
self.pubdate.setDisplayFormat(pubdate_format) self.pubdate.setDisplayFormat(pubdate_format)
self.date.setMinimumDate(QDate(100,1,1)) self.date.setMinimumDate(UNDEFINED_QDATE)
self.pubdate.setSpecialValueText(_('Undefined'))
self.date.setSpecialValueText(_('Undefined'))
self.clear_pubdate_button.clicked.connect(self.clear_pubdate)
self.connect(self.cover, SIGNAL('cover_changed(PyQt_PyObject)'), self.cover_dropped) self.connect(self.cover, SIGNAL('cover_changed(PyQt_PyObject)'), self.cover_dropped)
QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \ QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \
@ -552,7 +556,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
tags = self.db.tags(row) tags = self.db.tags(row)
self.original_tags = ', '.join(tags.split(',')) if tags else '' self.original_tags = ', '.join(tags.split(',')) if tags else ''
self.tags.setText(self.original_tags) self.tags.setText(self.original_tags)
self.tags.update_tags_cache(self.db.all_tags()) self.tags.update_items_cache(self.db.all_tags())
rating = self.db.rating(row) rating = self.db.rating(row)
if rating > 0: if rating > 0:
self.rating.setValue(int(rating/2.)) self.rating.setValue(int(rating/2.))
@ -615,6 +619,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.show() self.show()
def clear_pubdate(self, *args):
self.pubdate.setDate(UNDEFINED_QDATE)
def create_custom_column_editors(self): def create_custom_column_editors(self):
w = self.central_widget.widget(1) w = self.central_widget.widget(1)
layout = w.layout() layout = w.layout()
@ -718,6 +725,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
au = ' & '.join([a.strip().replace('|', ',') for a in au.split(',')]) au = ' & '.join([a.strip().replace('|', ',') for a in au.split(',')])
self.authors.setEditText(au) self.authors.setEditText(au)
self.authors.set_separator('&')
self.authors.set_space_before_sep(True)
self.authors.update_items_cache(self.db.all_author_names())
def initialize_series(self): def initialize_series(self):
self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow) self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow)
all_series = self.db.all_series() all_series = self.db.all_series()
@ -769,7 +780,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if d.result() == QDialog.Accepted: if d.result() == QDialog.Accepted:
tag_string = ', '.join(d.tags) tag_string = ', '.join(d.tags)
self.tags.setText(tag_string) self.tags.setText(tag_string)
self.tags.update_tags_cache(self.db.all_tags()) self.tags.update_items_cache(self.db.all_tags())
def fetch_metadata(self): def fetch_metadata(self):

View File

@ -100,246 +100,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Title &amp;sort: </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>title_sort</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="EnLineEdit" name="title_sort">
<property name="toolTip">
<string>Specify how this book should be sorted when by title. For example, The Exorcist might be sorted as Exorcist, The.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Author(s): </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>authors</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="EnComboBox" name="authors">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Author S&amp;ort: </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>author_sort</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="EnLineEdit" name="author_sort">
<property name="toolTip">
<string>Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles.
If the box is colored green, then text matches the individual author's sort strings. If it is colored red, then the authors and this text do not match.</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>&amp;Rating:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>rating</cstring>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="QSpinBox" name="rating">
<property name="toolTip">
<string>Rating of this book. 0-5 stars</string>
</property>
<property name="whatsThis">
<string>Rating of this book. 0-5 stars</string>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::PlusMinus</enum>
</property>
<property name="suffix">
<string> stars</string>
</property>
<property name="maximum">
<number>5</number>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>&amp;Publisher: </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>publisher</cstring>
</property>
</widget>
</item>
<item row="5" column="1" colspan="2">
<widget class="EnComboBox" name="publisher">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Ta&amp;gs: </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>tags</cstring>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>&amp;Series:</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>series</cstring>
</property>
</widget>
</item>
<item row="7" column="1">
<layout class="QHBoxLayout" name="_3">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="EnComboBox" name="series">
<property name="toolTip">
<string>List of known series. You can add new series.</string>
</property>
<property name="whatsThis">
<string>List of known series. You can add new series.</string>
</property>
<property name="editable">
<bool>true</bool>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAlphabetically</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="8" column="1" colspan="2">
<widget class="QDoubleSpinBox" name="series_index">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>Book </string>
</property>
<property name="maximum">
<double>9999.989999999999782</double>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>IS&amp;BN:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>isbn</cstring>
</property>
</widget>
</item>
<item row="9" column="1" colspan="2">
<widget class="QLineEdit" name="isbn"/>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>&amp;Date:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>date</cstring>
</property>
</widget>
</item>
<item row="10" column="1" colspan="2">
<widget class="QDateEdit" name="date">
<property name="displayFormat">
<string>dd MMM yyyy</string>
</property>
<property name="calendarPopup">
<bool>true</bool>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Publishe&amp;d:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>pubdate</cstring>
</property>
</widget>
</item>
<item row="11" column="1" colspan="2">
<widget class="QDateEdit" name="pubdate">
<property name="displayFormat">
<string>MMM yyyy</string>
</property>
<property name="calendarPopup">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2" rowspan="4"> <item row="0" column="2" rowspan="4">
<layout class="QVBoxLayout" name="verticalLayout_7"> <layout class="QVBoxLayout" name="verticalLayout_7">
<item> <item>
@ -446,10 +206,136 @@ Using this button to create author sort will change author sort from red to gree
</item> </item>
</layout> </layout>
</item> </item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Title &amp;sort: </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>title_sort</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="EnLineEdit" name="title_sort">
<property name="toolTip">
<string>Specify how this book should be sorted when by title. For example, The Exorcist might be sorted as Exorcist, The.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Author(s): </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>authors</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="CompleteComboBox" name="authors">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Author S&amp;ort: </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>author_sort</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="EnLineEdit" name="author_sort">
<property name="toolTip">
<string>Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles.
If the box is colored green, then text matches the individual author's sort strings. If it is colored red, then the authors and this text do not match.</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>&amp;Rating:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>rating</cstring>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<widget class="QSpinBox" name="rating">
<property name="toolTip">
<string>Rating of this book. 0-5 stars</string>
</property>
<property name="whatsThis">
<string>Rating of this book. 0-5 stars</string>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::PlusMinus</enum>
</property>
<property name="suffix">
<string> stars</string>
</property>
<property name="maximum">
<number>5</number>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>&amp;Publisher: </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>publisher</cstring>
</property>
</widget>
</item>
<item row="5" column="1" colspan="2">
<widget class="EnComboBox" name="publisher">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Ta&amp;gs: </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>tags</cstring>
</property>
</widget>
</item>
<item row="6" column="1"> <item row="6" column="1">
<layout class="QHBoxLayout" name="_2"> <layout class="QHBoxLayout" name="_2">
<item> <item>
<widget class="TagsLineEdit" name="tags"> <widget class="CompleteLineEdit" name="tags">
<property name="toolTip"> <property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string> <string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property> </property>
@ -471,6 +357,45 @@ Using this button to create author sort will change author sort from red to gree
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>&amp;Series:</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>series</cstring>
</property>
</widget>
</item>
<item row="7" column="1">
<layout class="QHBoxLayout" name="_3">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="EnComboBox" name="series">
<property name="toolTip">
<string>List of known series. You can add new series.</string>
</property>
<property name="whatsThis">
<string>List of known series. You can add new series.</string>
</property>
<property name="editable">
<bool>true</bool>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAlphabetically</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="7" column="2"> <item row="7" column="2">
<widget class="QToolButton" name="remove_series_button"> <widget class="QToolButton" name="remove_series_button">
<property name="toolTip"> <property name="toolTip">
@ -485,6 +410,92 @@ Using this button to create author sort will change author sort from red to gree
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="1" colspan="2">
<widget class="QDoubleSpinBox" name="series_index">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>Book </string>
</property>
<property name="maximum">
<double>9999.989999999999782</double>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>IS&amp;BN:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>isbn</cstring>
</property>
</widget>
</item>
<item row="9" column="1" colspan="2">
<widget class="QLineEdit" name="isbn"/>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>&amp;Date:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>date</cstring>
</property>
</widget>
</item>
<item row="10" column="1" colspan="2">
<widget class="QDateEdit" name="date">
<property name="displayFormat">
<string>dd MMM yyyy</string>
</property>
<property name="calendarPopup">
<bool>true</bool>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Publishe&amp;d:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>pubdate</cstring>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QDateEdit" name="pubdate">
<property name="displayFormat">
<string>MMM yyyy</string>
</property>
<property name="calendarPopup">
<bool>true</bool>
</property>
</widget>
</item>
<item row="11" column="2">
<widget class="QToolButton" name="clear_pubdate_button">
<property name="toolTip">
<string>Clear published date</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -831,10 +842,15 @@ Using this button to create author sort will change author sort from red to gree
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>TagsLineEdit</class> <class>CompleteLineEdit</class>
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget>
<class>CompleteComboBox</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
<customwidget> <customwidget>
<class>FormatList</class> <class>FormatList</class>
<extends>QListWidget</extends> <extends>QListWidget</extends>

View File

@ -31,6 +31,9 @@ class SearchDialog(QDialog, Ui_Dialog):
self.authors_box.setEditText('') self.authors_box.setEditText('')
self.authors_box.completer().setCompletionMode(QCompleter.PopupCompletion) self.authors_box.completer().setCompletionMode(QCompleter.PopupCompletion)
self.authors_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive) self.authors_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive)
self.authors_box.set_separator('&')
self.authors_box.set_space_before_sep(True)
self.authors_box.update_items_cache(db.all_author_names())
all_series = db.all_series() all_series = db.all_series()
all_series.sort(key=lambda x : sort_key(x[1])) all_series.sort(key=lambda x : sort_key(x[1]))
@ -42,7 +45,7 @@ class SearchDialog(QDialog, Ui_Dialog):
self.series_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive) self.series_box.setAutoCompletionCaseSensitivity(Qt.CaseInsensitive)
all_tags = db.all_tags() all_tags = db.all_tags()
self.tags_box.update_tags_cache(all_tags) self.tags_box.update_items_cache(all_tags)
self.box_last_values = copy.deepcopy(box_values) self.box_last_values = copy.deepcopy(box_values)
if self.box_last_values: if self.box_last_values:

View File

@ -265,7 +265,7 @@
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="EnComboBox" name="authors_box"> <widget class="CompleteComboBox" name="authors_box">
<property name="toolTip"> <property name="toolTip">
<string>Enter an author's name. Only one author can be used.</string> <string>Enter an author's name. Only one author can be used.</string>
</property> </property>
@ -279,7 +279,7 @@
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="4" column="1">
<widget class="TagsLineEdit" name="tags_box"> <widget class="CompleteLineEdit" name="tags_box">
<property name="toolTip"> <property name="toolTip">
<string>Enter tags separated by spaces</string> <string>Enter tags separated by spaces</string>
</property> </property>
@ -360,10 +360,15 @@
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget> <customwidget>
<class>TagsLineEdit</class> <class>CompleteLineEdit</class>
<extends>QLineEdit</extends> <extends>QLineEdit</extends>
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget>
<class>CompleteComboBox</class>
<extends>QComboBox</extends>
<header>widgets.h</header>
</customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>all</tabstop> <tabstop>all</tabstop>

View File

@ -3,8 +3,11 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__license__ = 'GPL v3' __license__ = 'GPL v3'
import json
from PyQt4.Qt import Qt, QDialog, QDialogButtonBox from PyQt4.Qt import Qt, QDialog, QDialogButtonBox
from calibre.gui2.dialogs.template_dialog_ui import Ui_TemplateDialog from calibre.gui2.dialogs.template_dialog_ui import Ui_TemplateDialog
from calibre.utils.formatter_functions import formatter_functions
class TemplateDialog(QDialog, Ui_TemplateDialog): class TemplateDialog(QDialog, Ui_TemplateDialog):
@ -17,9 +20,41 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint)) self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon) self.setWindowIcon(icon)
self.textbox.setTabStopWidth(10)
self.source_code.setTabStopWidth(10)
self.documentation.setReadOnly(True)
self.source_code.setReadOnly(True)
if text is not None: if text is not None:
self.textbox.setPlainText(text) self.textbox.setPlainText(text)
self.textbox.setTabStopWidth(50)
self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK')) self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK'))
self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel')) self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel'))
try:
with open(P('template-functions.json'), 'rb') as f:
self.builtin_source_dict = json.load(f, encoding='utf-8')
except:
self.builtin_source_dict = {}
self.funcs = formatter_functions.get_functions()
self.builtins = formatter_functions.get_builtins()
func_names = sorted(self.funcs)
self.function.clear()
self.function.addItem('')
self.function.addItems(func_names)
self.function.setCurrentIndex(0)
self.function.currentIndexChanged[str].connect(self.function_changed)
def function_changed(self, toWhat):
name = unicode(toWhat)
self.source_code.clear()
self.documentation.clear()
if name in self.funcs:
self.documentation.setPlainText(self.funcs[name].doc)
if name in self.builtins:
if name in self.builtin_source_dict:
self.source_code.setPlainText(self.builtin_source_dict[name])
else:
self.source_code.setPlainText(self.funcs[name].program_text)

View File

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>500</width> <width>588</width>
<height>235</height> <height>546</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy"> <property name="sizePolicy">
@ -19,21 +19,77 @@
<property name="windowTitle"> <property name="windowTitle">
<string>Edit Comments</string> <string>Edit Comments</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QPlainTextEdit" name="textbox"/> <widget class="QPlainTextEdit" name="textbox"/>
</item> </item>
<item> <item>
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
<property name="standardButtons"> <property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property> </property>
</widget> </widget>
</item> </item>
</layout> <item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Function &amp;name:</string>
</property>
<property name="buddy">
<cstring>function</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="function"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Documentation:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="buddy">
<cstring>documentation</cstring>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Python &amp;code:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="buddy">
<cstring>source_code</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPlainTextEdit" name="documentation">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>75</height>
</size>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPlainTextEdit" name="source_code"/>
</item>
</layout>
</item>
</layout>
</widget> </widget>
<resources/> <resources/>
<connections> <connections>

View File

@ -148,7 +148,6 @@ class StatusBar(QStatusBar): # {{{
self.get_version() + ' ' + _('created by Kovid Goyal') self.get_version() + ' ' + _('created by Kovid Goyal')
self.device_string = '' self.device_string = ''
self.update_label = QLabel('') self.update_label = QLabel('')
self.update_label.setOpenExternalLinks(True)
self.addPermanentWidget(self.update_label) self.addPermanentWidget(self.update_label)
self.update_label.setVisible(False) self.update_label.setVisible(False)
self._font = QFont() self._font = QFont()
@ -174,8 +173,9 @@ class StatusBar(QStatusBar): # {{{
self.clearMessage() self.clearMessage()
def new_version_available(self, ver, url): def new_version_available(self, ver, url):
msg = (u'<span style="color:red; font-weight: bold">%s: <a href="%s">%s<a></span>') % ( msg = (u'<span style="color:red; font-weight: bold">%s: <a'
_('Update found'), url, ver) ' href="update:%s">%s<a></span>') % (
_('Update found'), ver, ver)
self.update_label.setText(msg) self.update_label.setText(msg)
self.update_label.setCursor(Qt.PointingHandCursor) self.update_label.setCursor(Qt.PointingHandCursor)
self.update_label.setVisible(True) self.update_label.setVisible(True)
@ -240,6 +240,13 @@ class LayoutMixin(object): # {{{
self.status_bar.addPermanentWidget(button) self.status_bar.addPermanentWidget(button)
self.status_bar.addPermanentWidget(self.jobs_button) self.status_bar.addPermanentWidget(self.jobs_button)
self.setStatusBar(self.status_bar) self.setStatusBar(self.status_bar)
self.status_bar.update_label.linkActivated.connect(self.update_link_clicked)
def update_link_clicked(self, url):
url = unicode(url)
if url.startswith('update:'):
version = url.partition(':')[-1]
self.update_found(version, force=True)
def finalize_layout(self): def finalize_layout(self):
self.status_bar.initialize(self.system_tray_icon) self.status_bar.initialize(self.system_tray_icon)

View File

@ -16,7 +16,7 @@ from PyQt4.Qt import QColor, Qt, QModelIndex, QSize, \
QComboBox, QTextDocument QComboBox, QTextDocument
from calibre.gui2 import UNDEFINED_QDATE, error_dialog from calibre.gui2 import UNDEFINED_QDATE, error_dialog
from calibre.gui2.widgets import EnLineEdit, TagsLineEdit from calibre.gui2.widgets import EnLineEdit, CompleteLineEdit
from calibre.utils.date import now, format_date from calibre.utils.date import now, format_date
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre.utils.formatter import validation_formatter from calibre.utils.formatter import validation_formatter
@ -173,9 +173,9 @@ class TagsDelegate(QStyledItemDelegate): # {{{
if self.db: if self.db:
col = index.model().column_map[index.column()] col = index.model().column_map[index.column()]
if not index.model().is_custom_column(col): if not index.model().is_custom_column(col):
editor = TagsLineEdit(parent, self.db.all_tags()) editor = CompleteLineEdit(parent, self.db.all_tags())
else: else:
editor = TagsLineEdit(parent, editor = CompleteLineEdit(parent,
sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))), sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))),
key=sort_key)) key=sort_key))
return editor return editor
@ -184,6 +184,31 @@ class TagsDelegate(QStyledItemDelegate): # {{{
return editor return editor
# }}} # }}}
class CompleteDelegate(QStyledItemDelegate): # {{{
def __init__(self, parent, sep, items_func_name, space_before_sep=False):
QStyledItemDelegate.__init__(self, parent)
self.sep = sep
self.items_func_name = items_func_name
self.space_before_sep = space_before_sep
def set_database(self, db):
self.db = db
def createEditor(self, parent, option, index):
if self.db and hasattr(self.db, self.items_func_name):
col = index.model().column_map[index.column()]
if not index.model().is_custom_column(col):
editor = CompleteLineEdit(parent, getattr(self.db, self.items_func_name)(),
self.sep, self.space_before_sep)
else:
editor = CompleteLineEdit(parent,
sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))),
key=sort_key), self.sep, self.space_before_sep)
else:
editor = EnLineEdit(parent)
return editor
# }}}
class CcDateDelegate(QStyledItemDelegate): # {{{ class CcDateDelegate(QStyledItemDelegate): # {{{
''' '''
Delegate for custom columns dates. Because this delegate stores the Delegate for custom columns dates. Because this delegate stores the

View File

@ -13,7 +13,7 @@ from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \
QPoint, QPixmap, QUrl, QImage, QPainter, QColor, QRect QPoint, QPixmap, QUrl, QImage, QPainter, QColor, QRect
from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \ from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \
TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \ TextDelegate, DateDelegate, CompleteDelegate, CcTextDelegate, \
CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate, \ CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate, \
CcEnumDelegate CcEnumDelegate
from calibre.gui2.library.models import BooksModel, DeviceBooksModel from calibre.gui2.library.models import BooksModel, DeviceBooksModel
@ -76,8 +76,8 @@ class BooksView(QTableView): # {{{
self.rating_delegate = RatingDelegate(self) self.rating_delegate = RatingDelegate(self)
self.timestamp_delegate = DateDelegate(self) self.timestamp_delegate = DateDelegate(self)
self.pubdate_delegate = PubDateDelegate(self) self.pubdate_delegate = PubDateDelegate(self)
self.tags_delegate = TagsDelegate(self) self.tags_delegate = CompleteDelegate(self, ',', 'all_tags')
self.authors_delegate = TextDelegate(self) self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names', True)
self.series_delegate = TextDelegate(self) self.series_delegate = TextDelegate(self)
self.publisher_delegate = TextDelegate(self) self.publisher_delegate = TextDelegate(self)
self.text_delegate = TextDelegate(self) self.text_delegate = TextDelegate(self)
@ -410,8 +410,7 @@ class BooksView(QTableView): # {{{
self.save_state() self.save_state()
self._model.set_database(db) self._model.set_database(db)
self.tags_delegate.set_database(db) self.tags_delegate.set_database(db)
self.authors_delegate.set_auto_complete_function( self.authors_delegate.set_database(db)
lambda: [(x, y.replace('|', ',')) for (x, y) in db.all_authors()])
self.series_delegate.set_auto_complete_function(db.all_series) self.series_delegate.set_auto_complete_function(db.all_series)
self.publisher_delegate.set_auto_complete_function(db.all_publishers) self.publisher_delegate.set_auto_complete_function(db.all_publishers)

View File

@ -5,7 +5,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import traceback import json, traceback
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences import ConfigWidgetBase, test_widget
@ -73,6 +73,12 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.textBrowser.setHtml(help_text) self.textBrowser.setHtml(help_text)
def initialize(self): def initialize(self):
try:
with open(P('template-functions.json'), 'rb') as f:
self.builtin_source_dict = json.load(f, encoding='utf-8')
except:
self.builtin_source_dict = {}
self.funcs = formatter_functions.get_functions() self.funcs = formatter_functions.get_functions()
self.builtins = formatter_functions.get_builtins() self.builtins = formatter_functions.get_builtins()
@ -179,8 +185,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
func = self.funcs[txt] func = self.funcs[txt]
self.argument_count.setValue(func.arg_count) self.argument_count.setValue(func.arg_count)
self.documentation.setText(func.doc) self.documentation.setText(func.doc)
self.program.setPlainText(func.program_text)
if txt in self.builtins: if txt in self.builtins:
if hasattr(func, 'program_text'):
self.program.setPlainText(func.program_text)
elif txt in self.builtin_source_dict:
self.program.setPlainText(self.builtin_source_dict[txt])
else:
self.program.setPlainText(_('function source code not available'))
self.documentation.setReadOnly(True) self.documentation.setReadOnly(True)
self.argument_count.setReadOnly(True) self.argument_count.setReadOnly(True)
self.program.setReadOnly(True) self.program.setReadOnly(True)

View File

@ -52,8 +52,7 @@ class UpdateNotification(QDialog):
self.label = QLabel('<p>'+ self.label = QLabel('<p>'+
_('%s has been updated to version <b>%s</b>. ' _('%s has been updated to version <b>%s</b>. '
'See the <a href="http://calibre-ebook.com/whats-new' 'See the <a href="http://calibre-ebook.com/whats-new'
'">new features</a>. Visit the download pa' '">new features</a>.')%(__appname__, version))
'ge?')%(__appname__, version))
self.label.setOpenExternalLinks(True) self.label.setOpenExternalLinks(True)
self.label.setWordWrap(True) self.label.setWordWrap(True)
self.setWindowTitle(_('Update available!')) self.setWindowTitle(_('Update available!'))
@ -94,13 +93,13 @@ class UpdateMixin(object):
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
self.update_checker.start() self.update_checker.start()
def update_found(self, version): def update_found(self, version, force=False):
os = 'windows' if iswindows else 'osx' if isosx else 'linux' os = 'windows' if iswindows else 'osx' if isosx else 'linux'
url = 'http://calibre-ebook.com/download_%s'%os url = 'http://calibre-ebook.com/download_%s'%os
self.status_bar.new_version_available(version, url) self.status_bar.new_version_available(version, url)
if config.get('new_version_notification') and \ if force or (config.get('new_version_notification') and \
dynamic.get('update to version %s'%version, True): dynamic.get('update to version %s'%version, True)):
self._update_notification__ = UpdateNotification(version, self._update_notification__ = UpdateNotification(version,
parent=self) parent=self)
self._update_notification__.show() self._update_notification__.show()

View File

@ -400,46 +400,47 @@ class EnLineEdit(LineEditECM, QLineEdit):
pass pass
class TagsCompleter(QCompleter): class ItemsCompleter(QCompleter):
''' '''
A completer object that completes a list of tags. It is used in conjunction A completer object that completes a list of tags. It is used in conjunction
with a CompleterLineEdit. with a CompleterLineEdit.
''' '''
def __init__(self, parent, all_tags): def __init__(self, parent, all_items):
QCompleter.__init__(self, all_tags, parent) QCompleter.__init__(self, all_items, parent)
self.all_tags = set(all_tags) self.all_items = set(all_items)
def update(self, text_tags, completion_prefix): def update(self, text_items, completion_prefix):
tags = list(self.all_tags.difference(text_tags)) items = list(self.all_items.difference(text_items))
model = QStringListModel(tags, self) model = QStringListModel(items, self)
self.setModel(model) self.setModel(model)
self.setCompletionPrefix(completion_prefix) self.setCompletionPrefix(completion_prefix)
if completion_prefix.strip() != '': if completion_prefix.strip() != '':
self.complete() self.complete()
def update_tags_cache(self, tags): def update_items_cache(self, items):
self.all_tags = set(tags) self.all_items = set(items)
model = QStringListModel(tags, self) model = QStringListModel(items, self)
self.setModel(model) self.setModel(model)
class TagsLineEdit(EnLineEdit): class CompleteLineEdit(EnLineEdit):
''' '''
A QLineEdit that can complete parts of text separated by separator. A QLineEdit that can complete parts of text separated by separator.
''' '''
def __init__(self, parent=0, tags=[]): def __init__(self, parent=0, complete_items=[], sep=',', space_before_sep=False):
EnLineEdit.__init__(self, parent) EnLineEdit.__init__(self, parent)
self.separator = ',' self.separator = sep
self.space_before_sep = space_before_sep
self.connect(self, SIGNAL('textChanged(QString)'), self.text_changed) self.connect(self, SIGNAL('textChanged(QString)'), self.text_changed)
self.completer = TagsCompleter(self, tags) self.completer = ItemsCompleter(self, complete_items)
self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.completer.setCaseSensitivity(Qt.CaseInsensitive)
self.connect(self, self.connect(self,
@ -450,32 +451,43 @@ class TagsLineEdit(EnLineEdit):
self.completer.setWidget(self) self.completer.setWidget(self)
def update_tags_cache(self, tags): def update_items_cache(self, complete_items):
self.completer.update_tags_cache(tags) self.completer.update_items_cache(complete_items)
def set_separator(self, sep):
self.separator = sep
def set_space_before_sep(self, space_before):
self.space_before_sep = space_before
def text_changed(self, text): def text_changed(self, text):
all_text = unicode(text) all_text = unicode(text)
text = all_text[:self.cursorPosition()] text = all_text[:self.cursorPosition()]
prefix = text.split(',')[-1].strip() prefix = text.split(self.separator)[-1].strip()
text_tags = [] text_items = []
for t in all_text.split(self.separator): for t in all_text.split(self.separator):
t1 = unicode(t).strip() t1 = unicode(t).strip()
if t1 != '': if t1 != '':
text_tags.append(t) text_items.append(t)
text_tags = list(set(text_tags)) text_items = list(set(text_items))
self.emit(SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'), self.emit(SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'),
text_tags, prefix) text_items, prefix)
def complete_text(self, text): def complete_text(self, text):
cursor_pos = self.cursorPosition() cursor_pos = self.cursorPosition()
before_text = unicode(self.text())[:cursor_pos] before_text = unicode(self.text())[:cursor_pos]
after_text = unicode(self.text())[cursor_pos:] after_text = unicode(self.text())[cursor_pos:]
prefix_len = len(before_text.split(',')[-1].strip()) prefix_len = len(before_text.split(self.separator)[-1].strip())
self.setText('%s%s%s %s' % (before_text[:cursor_pos - prefix_len], if self.space_before_sep:
text, self.separator, after_text)) complete_text_pat = '%s%s %s %s'
self.setCursorPosition(cursor_pos - prefix_len + len(text) + 2) len_extra = 3
else:
complete_text_pat = '%s%s%s %s'
len_extra = 2
self.setText(complete_text_pat % (before_text[:cursor_pos - prefix_len], text, self.separator, after_text))
self.setCursorPosition(cursor_pos - prefix_len + len(text) + len_extra)
class EnComboBox(QComboBox): class EnComboBox(QComboBox):
@ -502,6 +514,22 @@ class EnComboBox(QComboBox):
idx = 0 idx = 0
self.setCurrentIndex(idx) self.setCurrentIndex(idx)
class CompleteComboBox(EnComboBox):
def __init__(self, *args):
EnComboBox.__init__(self, *args)
self.setLineEdit(CompleteLineEdit(self))
def update_items_cache(self, complete_items):
self.lineEdit().update_items_cache(complete_items)
def set_separator(self, sep):
self.lineEdit().set_separator(sep)
def set_space_before_sep(self, space_before):
self.lineEdit().set_space_before_sep(space_before)
class HistoryLineEdit(QComboBox): class HistoryLineEdit(QComboBox):
lost_focus = pyqtSignal() lost_focus = pyqtSignal()

View File

@ -546,9 +546,9 @@ class EPUB_MOBI(CatalogPlugin):
name = 'Catalog_EPUB_MOBI' name = 'Catalog_EPUB_MOBI'
description = 'EPUB/MOBI catalog generator' description = 'EPUB/MOBI catalog generator'
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
minimum_calibre_version = (0, 6, 34) minimum_calibre_version = (0, 7, 40)
author = 'Greg Riker' author = 'Greg Riker'
version = (0, 0, 1) version = (1, 0, 0)
file_types = set(['epub','mobi']) file_types = set(['epub','mobi'])
THUMB_SMALLEST = "1.0" THUMB_SMALLEST = "1.0"
@ -900,15 +900,7 @@ class EPUB_MOBI(CatalogPlugin):
''' '''
Generates catalog source files from calibre database Generates catalog source files from calibre database
Implementation notes Flow of control:
- 'Marker tags' in a book's metadata are used to flag special conditions:
(Defaults)
'~' : Do not catalog this book
'+' : Mark this book as read (check mark) in lists
'*' : Display trailing text as 'Note: <text>' in top frame next to cover
'[<source>] : Source of content (e.g., Amazon, Project Gutenberg). Do not create genre
- Program flow
gui2.actions.catalog:generate_catalog() gui2.actions.catalog:generate_catalog()
gui2.tools:generate_catalog() or library.cli:command_catalog() gui2.tools:generate_catalog() or library.cli:command_catalog()
called from gui2.convert.gui_conversion:gui_catalog() called from gui2.convert.gui_conversion:gui_catalog()
@ -953,7 +945,7 @@ class EPUB_MOBI(CatalogPlugin):
self.__creator = opts.creator self.__creator = opts.creator
self.__db = db self.__db = db
self.__descriptionClip = opts.descriptionClip self.__descriptionClip = opts.descriptionClip
self.__error = None self.__error = []
self.__generateForKindle = True if (self.opts.fmt == 'mobi' and \ self.__generateForKindle = True if (self.opts.fmt == 'mobi' and \
self.opts.output_profile and \ self.opts.output_profile and \
self.opts.output_profile.startswith("kindle")) else False self.opts.output_profile.startswith("kindle")) else False
@ -1033,6 +1025,22 @@ class EPUB_MOBI(CatalogPlugin):
# +1 thumbs # +1 thumbs
self.__totalSteps += 3 self.__totalSteps += 3
# Load section list templates
templates = ['by_authors_normal_title_template',
'by_authors_series_title_template',
'by_titles_normal_title_template',
'by_titles_series_title_template',
'by_series_title_template',
'by_genres_normal_title_template',
'by_genres_series_title_template',
'by_recently_added_normal_title_template',
'by_recently_added_series_title_template',
'by_month_added_normal_title_template',
'by_month_added_series_title_template']
execfile(P('catalog/section_list_templates.py'), locals())
for t in templates:
setattr(self,t,eval(t))
# Accessors # Accessors
if True: if True:
''' '''
@ -1352,6 +1360,7 @@ class EPUB_MOBI(CatalogPlugin):
return False return False
self.fetchBookmarks() self.fetchBookmarks()
if self.opts.generate_descriptions: if self.opts.generate_descriptions:
self.generateThumbnails()
self.generateHTMLDescriptions() self.generateHTMLDescriptions()
self.generateHTMLByAuthor() self.generateHTMLByAuthor()
if self.opts.generate_titles: if self.opts.generate_titles:
@ -1364,8 +1373,7 @@ class EPUB_MOBI(CatalogPlugin):
self.generateHTMLByDateAdded() self.generateHTMLByDateAdded()
if self.generateRecentlyRead: if self.generateRecentlyRead:
self.generateHTMLByDateRead() self.generateHTMLByDateRead()
if self.opts.generate_descriptions:
self.generateThumbnails()
self.generateOPF() self.generateOPF()
self.generateNCXHeader() self.generateNCXHeader()
self.generateNCXByAuthor("Authors") self.generateNCXByAuthor("Authors")
@ -1420,26 +1428,35 @@ class EPUB_MOBI(CatalogPlugin):
''' '''
self.updateProgressFullStep("Sorting database") self.updateProgressFullStep("Sorting database")
'''
# Sort titles case-insensitive, by author
self.booksByAuthor = sorted(self.booksByTitle,
key=lambda x:(x['author_sort'].upper(), x['author_sort'].upper()))
'''
self.booksByAuthor = list(self.booksByTitle) self.booksByAuthor = list(self.booksByTitle)
self.booksByAuthor.sort(self.author_compare)
if False and self.verbose: # Test for author_sort mismatches
self.opts.log.info("fetchBooksByAuthor(): %d books" % len(self.booksByAuthor)) self.booksByAuthor = sorted(self.booksByAuthor, key=self.booksByAuthorSorter_author)
self.opts.log.info(" %-30s %-20s %s" % ('title', 'series', 'series_index')) # Build the unique_authors set from existing data
for title in self.booksByAuthor: authors = [(record['author'], record['author_sort']) for record in self.booksByAuthor]
self.opts.log.info((u" %-30s %-20s%5s " % \ current_author = authors[0]
(title['title'][:30], for (i,author) in enumerate(authors):
title['series'][:20] if title['series'] else '', if author != current_author and i:
title['series_index'], # Exit if author matches previous, but author_sort doesn't match
)).encode('utf-8')) if author[0] == current_author[0]:
raise SystemExit error_msg = _('''
Inconsistent Author Sort values for Author '{0}' ('{1}' <> '{2}'), unable to build catalog.\n
Select all books by '{0}', apply correct Author Sort value in Edit Metadata dialog,
then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
self.opts.log.warn('\n*** Metadata error ***')
self.opts.log.warn(error_msg)
self.error.append('Metadata error')
self.error.append(error_msg)
return False
self.booksByAuthor = sorted(self.booksByAuthor, key=self.booksByAuthorSorter_author_sort)
# for book in self.booksByAuthor:
# print '{0:<10} {1:<5} {2:<20} {3:<20} {4:<20} {5:<20}'.format(book['series'], book['series_index'], book['title'],
# book['author'], book['authors'],book['author_sort'])
# print
# Build the unique_authors set from existing data # Build the unique_authors set from existing data
authors = [(record['author'], record['author_sort'].capitalize()) for record in self.booksByAuthor] authors = [(record['author'], record['author_sort'].capitalize()) for record in self.booksByAuthor]
@ -1456,19 +1473,6 @@ class EPUB_MOBI(CatalogPlugin):
# Note that current_author and author are tuples: (friendly, sort) # Note that current_author and author are tuples: (friendly, sort)
multiple_authors = True multiple_authors = True
if author != current_author and i:
# Warn, exit if friendly matches previous, but sort doesn't
if author[0] == current_author[0]:
error_msg = _('''
\n*** Metadata error ***
Inconsistent Author Sort values for Author '{0}', unable to continue building catalog.
Select all books by '{0}', apply correct Author Sort value in Edit Metadata dialog,
then rebuild the catalog.\n''').format(author[0])
self.opts.log.warn(error_msg)
self.error = error_msg
return False
# New author, save the previous author/sort/count # New author, save the previous author/sort/count
unique_authors.append((current_author[0], icu_title(current_author[1]), unique_authors.append((current_author[0], icu_title(current_author[1]),
books_by_current_author)) books_by_current_author))
@ -1496,16 +1500,8 @@ then rebuild the catalog.\n''').format(author[0])
return True return True
def fetchBooksByTitle(self): def fetchBooksByTitle(self):
self.updateProgressFullStep("Fetching database") self.updateProgressFullStep("Fetching database")
# Get the database as a dictionary
# Sort by title
# Search is a string like this:
# not tag:<exclude_tag> author:"Riker"
# So we need to merge opts.exclude_tag with opts.search_text
# not tag:"~" author:"Riker"
self.opts.sort_by = 'title' self.opts.sort_by = 'title'
# Merge opts.exclude_tags with opts.search_text # Merge opts.exclude_tags with opts.search_text
@ -1528,7 +1524,6 @@ then rebuild the catalog.\n''').format(author[0])
else: else:
self.opts.search_text = search_phrase self.opts.search_text = search_phrase
#print "fetchBooksByTitle(): opts.search_text: %s" % self.opts.search_text
# Fetch the database as a dictionary # Fetch the database as a dictionary
data = self.plugin.search_sort_db(self.db, self.opts) data = self.plugin.search_sort_db(self.db, self.opts)
data = self.processExclusions(data) data = self.processExclusions(data)
@ -1536,8 +1531,6 @@ then rebuild the catalog.\n''').format(author[0])
# Populate this_title{} from data[{},{}] # Populate this_title{} from data[{},{}]
titles = [] titles = []
for record in data: for record in data:
if False:
print "available record metadata:\n%s" % sorted(record.keys())
this_title = {} this_title = {}
this_title['id'] = record['id'] this_title['id'] = record['id']
@ -1547,7 +1540,6 @@ then rebuild the catalog.\n''').format(author[0])
if record['series']: if record['series']:
this_title['series'] = record['series'] this_title['series'] = record['series']
this_title['series_index'] = record['series_index'] this_title['series_index'] = record['series_index']
this_title['title'] = self.generateSeriesTitle(this_title)
else: else:
this_title['series'] = None this_title['series'] = None
this_title['series_index'] = 0.0 this_title['series_index'] = 0.0
@ -1572,7 +1564,12 @@ then rebuild the catalog.\n''').format(author[0])
this_title['publisher'] = re.sub('&', '&amp;', record['publisher']) this_title['publisher'] = re.sub('&', '&amp;', record['publisher'])
this_title['rating'] = record['rating'] if record['rating'] else 0 this_title['rating'] = record['rating'] if record['rating'] else 0
this_title['date'] = strftime(u'%B %Y', record['pubdate'].timetuple())
if re.match('0100-01-01',str(record['pubdate'].date())):
this_title['date'] = None
else:
this_title['date'] = strftime(u'%B %Y', record['pubdate'].timetuple())
this_title['timestamp'] = record['timestamp'] this_title['timestamp'] = record['timestamp']
if record['comments']: if record['comments']:
@ -1646,7 +1643,10 @@ then rebuild the catalog.\n''').format(author[0])
title['title_sort'][0:40])).decode('mac-roman')) title['title_sort'][0:40])).decode('mac-roman'))
return True return True
else: else:
self.error = _("No books found to catalog.\nCheck 'Excluded books' criteria in E-book options.") error_msg = _("No books found to catalog.\nCheck 'Excluded books' criteria in E-book options.\n")
self.opts.log.error('*** ' + error_msg + ' ***')
self.error.append(_('No books available to include in catalog'))
self.error.append(error_msg)
return False return False
def fetchBookmarks(self): def fetchBookmarks(self):
@ -1748,13 +1748,12 @@ then rebuild the catalog.\n''').format(author[0])
self.bookmarked_books = {} self.bookmarked_books = {}
def generateHTMLDescriptions(self): def generateHTMLDescriptions(self):
# Write each title to a separate HTML file in contentdir '''
Write each title to a separate HTML file in contentdir
'''
self.updateProgressFullStep("'Descriptions'") self.updateProgressFullStep("'Descriptions'")
for (title_num, title) in enumerate(self.booksByTitle): for (title_num, title) in enumerate(self.booksByTitle):
if False:
self.opts.log.info("%3s: %s - %s" % (title['id'], title['title'], title['author']))
self.updateProgressMicroStep("Description %d of %d" % \ self.updateProgressMicroStep("Description %d of %d" % \
(title_num, len(self.booksByTitle)), (title_num, len(self.booksByTitle)),
float(title_num*100/len(self.booksByTitle))/100) float(title_num*100/len(self.booksByTitle))/100)
@ -1768,8 +1767,9 @@ then rebuild the catalog.\n''').format(author[0])
outfile.close() outfile.close()
def generateHTMLByTitle(self): def generateHTMLByTitle(self):
# Write books by title A-Z to HTML file '''
Write books by title A-Z to HTML file
'''
self.updateProgressFullStep("'Titles'") self.updateProgressFullStep("'Titles'")
soup = self.generateHTMLEmptyHeader("Books By Alpha Title") soup = self.generateHTMLEmptyHeader("Books By Alpha Title")
@ -1807,22 +1807,11 @@ then rebuild the catalog.\n''').format(author[0])
current_letter = "" current_letter = ""
# Re-sort title list without leading series/series_index # Re-sort title list without leading series/series_index
# Incoming title <series> <series_index>: <title>
if not self.useSeriesPrefixInTitlesSection: if not self.useSeriesPrefixInTitlesSection:
nspt = deepcopy(self.booksByTitle) nspt = deepcopy(self.booksByTitle)
for book in nspt: nspt = sorted(nspt, key=lambda x:(x['title_sort'].upper(), x['title_sort'].upper()))
if book['series']:
tokens = book['title'].partition(':')
book['title'] = '%s (%s)' % (tokens[2].strip(), tokens[0])
book['title_sort'] = self.generateSortTitle(book['title'])
nspt = sorted(nspt,
key=lambda x:(x['title_sort'].upper(), x['title_sort'].upper()))
self.booksByTitle_noSeriesPrefix = nspt self.booksByTitle_noSeriesPrefix = nspt
if False and self.verbose:
self.opts.log.info("no_series_prefix_titles: %d books" % len(nspt))
self.opts.log.info(" %-40s %-40s" % ('title', 'title_sort'))
for title in nspt:
self.opts.log.info((u" %-40s %-40s" % (title['title'][0:40],
title['title_sort'][0:40])).encode('utf-8'))
# Loop through the books by title # Loop through the books by title
title_list = self.booksByTitle title_list = self.booksByTitle
@ -1878,7 +1867,14 @@ then rebuild the catalog.\n''').format(author[0])
aTag = Tag(soup, "a") aTag = Tag(soup, "a")
if self.opts.generate_descriptions: if self.opts.generate_descriptions:
aTag['href'] = "book_%d.html" % (int(float(book['id']))) aTag['href'] = "book_%d.html" % (int(float(book['id'])))
aTag.insert(0,escape(book['title']))
# Generate the title from the template
args = self.generateFormatArgs(book)
if book['series']:
formatted_title = self.by_titles_series_title_template.format(**args).rstrip()
else:
formatted_title = self.by_titles_normal_title_template.format(**args).rstrip()
aTag.insert(0,NavigableString(escape(formatted_title)))
pBookTag.insert(ptc, aTag) pBookTag.insert(ptc, aTag)
ptc += 1 ptc += 1
@ -1916,7 +1912,9 @@ then rebuild the catalog.\n''').format(author[0])
self.htmlFileList_1.append("content/ByAlphaTitle.html") self.htmlFileList_1.append("content/ByAlphaTitle.html")
def generateHTMLByAuthor(self): def generateHTMLByAuthor(self):
# Write books by author A-Z '''
Write books by author A-Z
'''
self.updateProgressFullStep("'Authors'") self.updateProgressFullStep("'Authors'")
friendly_name = "Authors" friendly_name = "Authors"
@ -1953,7 +1951,9 @@ then rebuild the catalog.\n''').format(author[0])
current_author = '' current_author = ''
current_letter = '' current_letter = ''
current_series = None current_series = None
#for book in sorted(self.booksByAuthor, key = self.booksByAuthorSorter_author_sort):
for book in self.booksByAuthor: for book in self.booksByAuthor:
book_count += 1 book_count += 1
if self.letter_or_symbol(book['author_sort'][0].upper()) != current_letter : if self.letter_or_symbol(book['author_sort'][0].upper()) != current_letter :
# Start a new letter with Index letter # Start a new letter with Index letter
@ -2067,14 +2067,18 @@ then rebuild the catalog.\n''').format(author[0])
aTag = Tag(soup, "a") aTag = Tag(soup, "a")
if self.opts.generate_descriptions: if self.opts.generate_descriptions:
aTag['href'] = "book_%d.html" % (int(float(book['id']))) aTag['href'] = "book_%d.html" % (int(float(book['id'])))
# Use series, series index if avail else title, + year of publication
# Generate the title from the template
args = self.generateFormatArgs(book)
if current_series: if current_series:
aTag.insert(0,'%s (%s)' % (escape(book['title'][len(book['series'])+1:]), #aTag.insert(0,'%s%s' % (escape(book['title'][len(book['series'])+1:]),pubyear))
book['date'].split()[1])) formatted_title = self.by_authors_series_title_template.format(**args).rstrip()
else: else:
aTag.insert(0,'%s (%s)' % (escape(book['title']), #aTag.insert(0,'%s%s' % (escape(book['title']), pubyear))
book['date'].split()[1])) formatted_title = self.by_authors_normal_title_template.format(**args).rstrip()
non_series_books += 1 non_series_books += 1
aTag.insert(0,NavigableString(escape(formatted_title)))
pBookTag.insert(ptc, aTag) pBookTag.insert(ptc, aTag)
ptc += 1 ptc += 1
@ -2111,7 +2115,6 @@ then rebuild the catalog.\n''').format(author[0])
# Add the divTag to the body # Add the divTag to the body
body.insert(btc, divTag) body.insert(btc, divTag)
# Write the generated file to contentdir # Write the generated file to contentdir
outfile_spec = "%s/ByAlphaAuthor.html" % (self.contentDir) outfile_spec = "%s/ByAlphaAuthor.html" % (self.contentDir)
outfile = open(outfile_spec, 'w') outfile = open(outfile_spec, 'w')
@ -2120,13 +2123,15 @@ then rebuild the catalog.\n''').format(author[0])
self.htmlFileList_1.append("content/ByAlphaAuthor.html") self.htmlFileList_1.append("content/ByAlphaAuthor.html")
def generateHTMLByDateAdded(self): def generateHTMLByDateAdded(self):
# Write books by reverse chronological order '''
Write books by reverse chronological order
'''
self.updateProgressFullStep("'Recently Added'") self.updateProgressFullStep("'Recently Added'")
def add_books_to_HTML_by_month(this_months_list, dtc): def add_books_to_HTML_by_month(this_months_list, dtc):
if len(this_months_list): if len(this_months_list):
this_months_list.sort(self.author_compare) #this_months_list = sorted(this_months_list, key=self.booksByAuthorSorter_author_sort)
# Create a new month anchor # Create a new month anchor
date_string = strftime(u'%B %Y', current_date.timetuple()) date_string = strftime(u'%B %Y', current_date.timetuple())
@ -2156,16 +2161,6 @@ then rebuild the catalog.\n''').format(author[0])
divTag.insert(dtc,pAuthorTag) divTag.insert(dtc,pAuthorTag)
dtc += 1 dtc += 1
'''
# Insert an <hr /> between non-series and series
if not current_series and non_series_books and new_entry['series']:
# Insert an <hr />
hrTag = Tag(soup,'hr')
hrTag['class'] = "series_divider"
divTag.insert(dtc,hrTag)
dtc += 1
'''
# Check for series # Check for series
if new_entry['series'] and new_entry['series'] != current_series: if new_entry['series'] and new_entry['series'] != current_series:
# Start a new series # Start a new series
@ -2213,11 +2208,15 @@ then rebuild the catalog.\n''').format(author[0])
aTag = Tag(soup, "a") aTag = Tag(soup, "a")
if self.opts.generate_descriptions: if self.opts.generate_descriptions:
aTag['href'] = "book_%d.html" % (int(float(new_entry['id']))) aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
# Generate the title from the template
args = self.generateFormatArgs(new_entry)
if current_series: if current_series:
aTag.insert(0,escape(new_entry['title'][len(new_entry['series'])+1:])) formatted_title = self.by_month_added_series_title_template.format(**args).rstrip()
else: else:
aTag.insert(0,escape(new_entry['title'])) formatted_title = self.by_month_added_normal_title_template.format(**args).rstrip()
non_series_books += 1 non_series_books += 1
aTag.insert(0,NavigableString(escape(formatted_title)))
pBookTag.insert(ptc, aTag) pBookTag.insert(ptc, aTag)
ptc += 1 ptc += 1
@ -2265,7 +2264,14 @@ then rebuild the catalog.\n''').format(author[0])
aTag = Tag(soup, "a") aTag = Tag(soup, "a")
if self.opts.generate_descriptions: if self.opts.generate_descriptions:
aTag['href'] = "book_%d.html" % (int(float(new_entry['id']))) aTag['href'] = "book_%d.html" % (int(float(new_entry['id'])))
aTag.insert(0,escape(new_entry['title']))
# Generate the title from the template
args = self.generateFormatArgs(new_entry)
if new_entry['series']:
formatted_title = self.by_recently_added_series_title_template.format(**args).rstrip()
else:
formatted_title = self.by_recently_added_normal_title_template.format(**args).rstrip()
aTag.insert(0,NavigableString(escape(formatted_title)))
pBookTag.insert(ptc, aTag) pBookTag.insert(ptc, aTag)
ptc += 1 ptc += 1
@ -2323,17 +2329,12 @@ then rebuild the catalog.\n''').format(author[0])
divTag = Tag(soup, "div") divTag = Tag(soup, "div")
dtc = 0 dtc = 0
# Add books by date range # >>> Books by date range <<<
if self.useSeriesPrefixInTitlesSection: if self.useSeriesPrefixInTitlesSection:
self.booksByDateRange = sorted(self.booksByTitle, self.booksByDateRange = sorted(self.booksByTitle,
key=lambda x:(x['timestamp'], x['timestamp']),reverse=True) key=lambda x:(x['timestamp'], x['timestamp']),reverse=True)
else: else:
nspt = deepcopy(self.booksByTitle) nspt = deepcopy(self.booksByTitle)
for book in nspt:
if book['series']:
tokens = book['title'].partition(':')
book['title'] = '%s (%s)' % (tokens[2].strip(), tokens[0])
book['title_sort'] = self.generateSortTitle(book['title'])
self.booksByDateRange = sorted(nspt, key=lambda x:(x['timestamp'], x['timestamp']),reverse=True) self.booksByDateRange = sorted(nspt, key=lambda x:(x['timestamp'], x['timestamp']),reverse=True)
date_range_list = [] date_range_list = []
@ -2356,15 +2357,6 @@ then rebuild the catalog.\n''').format(author[0])
dtc = add_books_to_HTML_by_date_range(date_range_list, date_range, dtc) dtc = add_books_to_HTML_by_date_range(date_range_list, date_range, dtc)
date_range_list = [book] date_range_list = [book]
'''
if books_added_in_date_range:
# Add an <hr> separating date ranges from months
hrTag = Tag(soup,'hr')
hrTag['class'] = "description_divider"
divTag.insert(dtc,hrTag)
dtc += 1
'''
# >>>> Books by month <<<< # >>>> Books by month <<<<
# Sort titles case-insensitive for by month using series prefix # Sort titles case-insensitive for by month using series prefix
self.booksByMonth = sorted(self.booksByTitle, self.booksByMonth = sorted(self.booksByTitle,
@ -2395,7 +2387,9 @@ then rebuild the catalog.\n''').format(author[0])
self.htmlFileList_2.append("content/ByDateAdded.html") self.htmlFileList_2.append("content/ByDateAdded.html")
def generateHTMLByDateRead(self): def generateHTMLByDateRead(self):
# Write books by active bookmarks '''
Write books by active bookmarks
'''
friendly_name = 'Recently Read' friendly_name = 'Recently Read'
self.updateProgressFullStep("'%s'" % friendly_name) self.updateProgressFullStep("'%s'" % friendly_name)
if not self.bookmarked_books: if not self.bookmarked_books:
@ -2533,32 +2527,6 @@ then rebuild the catalog.\n''').format(author[0])
self.booksByDateRead = sorted(bookmarked_books, self.booksByDateRead = sorted(bookmarked_books,
key=lambda x:(x['bookmark_timestamp'], x['bookmark_timestamp']),reverse=True) key=lambda x:(x['bookmark_timestamp'], x['bookmark_timestamp']),reverse=True)
'''
# >>>> Recently by date range <<<<
date_range_list = []
today_time = datetime.datetime.utcnow()
today_time.replace(hour=23, minute=59, second=59)
books_added_in_date_range = False
for (i, date) in enumerate(self.DATE_RANGE):
date_range_limit = self.DATE_RANGE[i]
if i:
date_range = '%d to %d days ago' % (self.DATE_RANGE[i-1], self.DATE_RANGE[i])
else:
date_range = 'Last %d days' % (self.DATE_RANGE[i])
for book in self.booksByDateRead:
bookmark_time = datetime.datetime.utcfromtimestamp(book['bookmark_timestamp'])
delta = today_time-bookmark_time
if delta.days <= date_range_limit:
date_range_list.append(book)
books_added_in_date_range = True
else:
break
dtc = add_books_to_HTML_by_date_range(date_range_list, date_range, dtc)
date_range_list = [book]
'''
# >>>> Recently read by day <<<< # >>>> Recently read by day <<<<
current_date = datetime.date.fromordinal(1) current_date = datetime.date.fromordinal(1)
todays_list = [] todays_list = []
@ -2713,10 +2681,15 @@ then rebuild the catalog.\n''').format(author[0])
# Use series, series index if avail else just title # Use series, series index if avail else just title
#aTag.insert(0,'%d. %s &middot; %s' % (book['series_index'],escape(book['title']), ' & '.join(book['authors']))) #aTag.insert(0,'%d. %s &middot; %s' % (book['series_index'],escape(book['title']), ' & '.join(book['authors'])))
# Link to book # Reassert 'date' since this is the result of a new search
aTag.insert(0,'%d. %s (%s)' % (book['series_index'], if re.match('0100-01-01',str(book['pubdate'].date())):
escape(book['title']), book['date'] = None
strftime(u'%Y', book['pubdate'].timetuple()))) else:
book['date'] = strftime(u'%B %Y', book['pubdate'].timetuple())
args = self.generateFormatArgs(book)
formatted_title = self.by_series_title_template.format(**args).rstrip()
aTag.insert(0,NavigableString(escape(formatted_title)))
pBookTag.insert(ptc, aTag) pBookTag.insert(ptc, aTag)
ptc += 1 ptc += 1
@ -2760,10 +2733,11 @@ then rebuild the catalog.\n''').format(author[0])
self.htmlFileList_1.append("content/BySeries.html") self.htmlFileList_1.append("content/BySeries.html")
def generateHTMLByTags(self): def generateHTMLByTags(self):
# Generate individual HTML files for each tag, e.g. Fiction, Nonfiction ... '''
# Note that special tags - ~+*[] - have already been filtered from books[] Generate individual HTML files for each tag, e.g. Fiction, Nonfiction ...
# There may be synonomous tags Note that special tags - have already been filtered from books[]
There may be synonomous tags
'''
self.updateProgressFullStep("'Genres'") self.updateProgressFullStep("'Genres'")
self.genre_tags_dict = self.filterDbTags(self.db.all_tags()) self.genre_tags_dict = self.filterDbTags(self.db.all_tags())
@ -2787,6 +2761,8 @@ then rebuild the catalog.\n''').format(author[0])
this_book['tags'] = book['tags'] this_book['tags'] = book['tags']
this_book['id'] = book['id'] this_book['id'] = book['id']
this_book['series'] = book['series'] this_book['series'] = book['series']
this_book['series_index'] = book['series_index']
this_book['date'] = book['date']
normalized_tag = self.genre_tags_dict[friendly_tag] normalized_tag = self.genre_tags_dict[friendly_tag]
genre_tag_list = [key for genre in genre_list for key in genre] genre_tag_list = [key for genre in genre_list for key in genre]
if normalized_tag in genre_tag_list: if normalized_tag in genre_tag_list:
@ -2843,13 +2819,7 @@ then rebuild the catalog.\n''').format(author[0])
unique_authors.append((current_author[0], current_author[1], books_by_current_author)) unique_authors.append((current_author[0], current_author[1], books_by_current_author))
else: else:
books_by_current_author += 1 books_by_current_author += 1
'''
# Extract the unique entries
unique_authors = []
for author in authors:
if not author in unique_authors:
unique_authors.append(author)
'''
# Write the genre book list as an article # Write the genre book list as an article
titles_spanned = self.generateHTMLByGenre(genre, True if index==0 else False, titles_spanned = self.generateHTMLByGenre(genre, True if index==0 else False,
genre_tag_set[genre], genre_tag_set[genre],
@ -2863,18 +2833,14 @@ then rebuild the catalog.\n''').format(author[0])
'books':genre_tag_set[genre], 'books':genre_tag_set[genre],
'titles_spanned':titles_spanned}) 'titles_spanned':titles_spanned})
if False and self.opts.verbose:
for genre in master_genre_list:
print "genre['tag']: %s" % genre['tag']
for book in genre['books']:
print book['title']
self.genres = master_genre_list self.genres = master_genre_list
def generateThumbnails(self): def generateThumbnails(self):
# Generate a thumbnail per cover. If a current thumbnail exists, skip '''
# If a cover doesn't exist, use default Generate a thumbnail per cover. If a current thumbnail exists, skip
# Return list of active thumbs If a cover doesn't exist, use default
Return list of active thumbs
'''
self.updateProgressFullStep("'Thumbnails'") self.updateProgressFullStep("'Thumbnails'")
thumbs = ['thumbnail_default.jpg'] thumbs = ['thumbnail_default.jpg']
image_dir = "%s/images" % self.catalogPath image_dir = "%s/images" % self.catalogPath
@ -2886,45 +2852,52 @@ then rebuild the catalog.\n''').format(author[0])
thumb_file = 'thumbnail_%d.jpg' % int(title['id']) thumb_file = 'thumbnail_%d.jpg' % int(title['id'])
thumb_generated = True thumb_generated = True
valid_cover = True
try: try:
self.generateThumbnail(title, image_dir, thumb_file) self.generateThumbnail(title, image_dir, thumb_file)
thumbs.append("thumbnail_%d.jpg" % int(title['id'])) thumbs.append("thumbnail_%d.jpg" % int(title['id']))
except: except:
if 'cover' in title and os.path.exists(title['cover']):
valid_cover = False
self.opts.log.warn(" *** Invalid cover file for '%s'***" %
(title['title']))
if not self.error:
self.error.append('Invalid cover files')
self.error.append("Warning: invalid cover file for '%s', default cover substituted.\n" % (title['title']))
thumb_generated = False thumb_generated = False
if not thumb_generated: if not thumb_generated:
# Use default cover self.opts.log.warn(" using default cover for '%s' (%d)" % (title['title'], title['id']))
if False and self.verbose: # Confirm thumb exists, default is current
self.opts.log.warn(" using default cover for '%s'" % \ default_thumb_fp = os.path.join(image_dir,"thumbnail_default.jpg")
(title['title'])) cover = os.path.join(self.catalogPath, "DefaultCover.png")
# Check to make sure default is current title['cover'] = cover
# Check to see if thumbnail exists
thumb_fp = "%s/thumbnail_default.jpg" % (image_dir)
cover = "%s/DefaultCover.png" % (self.catalogPath)
if not os.path.exists(cover): if not os.path.exists(cover):
shutil.copyfile(I('book.png'), cover) shutil.copyfile(I('book.png'), cover)
if os.path.isfile(thumb_fp): if os.path.isfile(default_thumb_fp):
# Check to see if default cover is newer than thumbnail # Check to see if default cover is newer than thumbnail
# os.path.getmtime() = modified time # os.path.getmtime() = modified time
# os.path.ctime() = creation time # os.path.ctime() = creation time
cover_timestamp = os.path.getmtime(cover) cover_timestamp = os.path.getmtime(cover)
thumb_timestamp = os.path.getmtime(thumb_fp) thumb_timestamp = os.path.getmtime(default_thumb_fp)
if thumb_timestamp < cover_timestamp: if thumb_timestamp < cover_timestamp:
if False and self.verbose: if False and self.verbose:
self.opts.log.warn("updating thumbnail_default for %s" % title['title']) self.opts.log.warn("updating thumbnail_default for %s" % title['title'])
#title['cover'] = "%s/DefaultCover.jpg" % self.catalogPath self.generateThumbnail(title, image_dir,
title['cover'] = cover "thumbnail_default.jpg" if valid_cover else thumb_file)
self.generateThumbnail(title, image_dir, "thumbnail_default.jpg")
else: else:
if False and self.verbose: if False and self.verbose:
self.opts.log.warn(" generating new thumbnail_default.jpg") self.opts.log.warn(" generating new thumbnail_default.jpg")
#title['cover'] = "%s/DefaultCover.jpg" % self.catalogPath self.generateThumbnail(title, image_dir,
title['cover'] = cover "thumbnail_default.jpg" if valid_cover else thumb_file)
self.generateThumbnail(title, image_dir, "thumbnail_default.jpg") # Clear the book's cover property
title['cover'] = None
# Write the thumb_width to the file validating cache contents
# Write thumb_width to the file, validating cache contents
# Allows detection of aborted catalog builds # Allows detection of aborted catalog builds
with ZipFile(self.__archive_path, mode='a') as zfw: with ZipFile(self.__archive_path, mode='a') as zfw:
zfw.writestr('thumb_width', self.opts.thumb_width) zfw.writestr('thumb_width', self.opts.thumb_width)
@ -3162,15 +3135,17 @@ then rebuild the catalog.\n''').format(author[0])
navLabelTag = Tag(ncx_soup, "navLabel") navLabelTag = Tag(ncx_soup, "navLabel")
textTag = Tag(ncx_soup, "text") textTag = Tag(ncx_soup, "text")
if book['series']: if book['series']:
tokens = list(book['title'].partition(':')) series_index = str(book['series_index'])
if series_index.endswith('.0'):
series_index = series_index[:-2]
if self.generateForKindle: if self.generateForKindle:
# Don't include Author for Kindle # Don't include Author for Kindle
textTag.insert(0, NavigableString(self.formatNCXText('%s (%s)' % \ textTag.insert(0, NavigableString(self.formatNCXText('%s (%s [%s])' %
(tokens[2].strip(), tokens[0]), dest='title'))) (book['title'], book['series'], series_index), dest='title')))
else: else:
# Include Author for non-Kindle # Include Author for non-Kindle
textTag.insert(0, NavigableString(self.formatNCXText('%s &middot; %s (%s)' % \ textTag.insert(0, NavigableString(self.formatNCXText('%s (%s [%s]) &middot; %s ' %
(tokens[2].strip(), book['author'], tokens[0]), dest='title'))) (book['title'], book['series'], series_index, book['author']), dest='title')))
else: else:
if self.generateForKindle: if self.generateForKindle:
# Don't include Author for Kindle # Don't include Author for Kindle
@ -3199,8 +3174,13 @@ then rebuild the catalog.\n''').format(author[0])
# Add the author tag # Add the author tag
cmTag = Tag(ncx_soup, '%s' % 'calibre:meta') cmTag = Tag(ncx_soup, '%s' % 'calibre:meta')
cmTag['name'] = "author" cmTag['name'] = "author"
navStr = '%s | %s' % (self.formatNCXText(book['author'], dest='author'),
book['date'].split()[1]) if book['date']:
navStr = '%s | %s' % (self.formatNCXText(book['author'], dest='author'),
book['date'].split()[1])
else:
navStr = '%s' % (self.formatNCXText(book['author'], dest='author'))
if 'tags' in book and len(book['tags']): if 'tags' in book and len(book['tags']):
navStr = self.formatNCXText(navStr + ' | ' + ' &middot; '.join(sorted(book['tags'])), dest='author') navStr = self.formatNCXText(navStr + ' | ' + ' &middot; '.join(sorted(book['tags'])), dest='author')
cmTag.insert(0, NavigableString(navStr)) cmTag.insert(0, NavigableString(navStr))
@ -3725,43 +3705,6 @@ then rebuild the catalog.\n''').format(author[0])
add_to_master_date_range_list(current_titles_list) add_to_master_date_range_list(current_titles_list)
current_titles_list = [book['title']] current_titles_list = [book['title']]
'''
# Add *article* entries for each populated date range
# master_date_range_list{}: [0]:titles list [1]:datestr
for books_by_date_range in master_date_range_list:
navPointByDateRangeTag = Tag(soup, 'navPoint')
navPointByDateRangeTag['class'] = "article"
navPointByDateRangeTag['id'] = "%s-ID" % books_by_date_range[1].replace(' ','')
navPointTag['playOrder'] = self.playOrder
self.playOrder += 1
navLabelTag = Tag(soup, 'navLabel')
textTag = Tag(soup, 'text')
textTag.insert(0, NavigableString(books_by_date_range[1]))
navLabelTag.insert(0, textTag)
navPointByDateRangeTag.insert(0,navLabelTag)
contentTag = Tag(soup, 'content')
contentTag['src'] = "%s#bdr_%s" % (HTML_file,
books_by_date_range[1].replace(' ',''))
navPointByDateRangeTag.insert(1,contentTag)
if self.generateForKindle:
cmTag = Tag(soup, '%s' % 'calibre:meta')
cmTag['name'] = "description"
cmTag.insert(0, NavigableString(books_by_date_range[0]))
navPointByDateRangeTag.insert(2, cmTag)
cmTag = Tag(soup, '%s' % 'calibre:meta')
cmTag['name'] = "author"
navStr = '%d titles' % books_by_date_range[2] if books_by_date_range[2] > 1 else \
'%d title' % books_by_date_range[2]
cmTag.insert(0, NavigableString(navStr))
navPointByDateRangeTag.insert(3, cmTag)
navPointTag.insert(nptc, navPointByDateRangeTag)
nptc += 1
'''
# Create an NCX article entry for each populated day # Create an NCX article entry for each populated day
# Loop over the booksByDate list, find start of each month, # Loop over the booksByDate list, find start of each month,
# add description_preview_count titles # add description_preview_count titles
@ -3944,7 +3887,8 @@ then rebuild the catalog.\n''').format(author[0])
outfile = open("%s/%s.ncx" % (self.catalogPath, self.basename), 'w') outfile = open("%s/%s.ncx" % (self.catalogPath, self.basename), 'w')
outfile.write(self.ncxSoup.prettify()) outfile.write(self.ncxSoup.prettify())
# Helpers
# ======================== Helpers ========================
def author_to_author_sort(self, author): def author_to_author_sort(self, author):
tokens = author.split() tokens = author.split()
tokens = tokens[-1:] + tokens[:-1] tokens = tokens[-1:] + tokens[:-1]
@ -3952,45 +3896,39 @@ then rebuild the catalog.\n''').format(author[0])
tokens[0] += ',' tokens[0] += ','
return ' '.join(tokens).capitalize() return ' '.join(tokens).capitalize()
def author_compare(self,x,y): def booksByAuthorSorter_author_sort(self, book):
# Return -1 if x<y '''
# Return 0 if x==y Sort non-series books before series books
# Return 1 if x>y '''
if not book['series']:
# Different authors - sort by author_sort key = '%s %s' % (book['author_sort'].capitalize(),
if x['author_sort'].capitalize() > y['author_sort'].capitalize(): book['title_sort'].capitalize())
return 1
elif x['author_sort'].capitalize() < y['author_sort'].capitalize():
return -1
else: else:
# Same author index = book['series_index']
if x['series'] != y['series']: integer = int(index)
# One title is a series, the other is not fraction = index-integer
if not x['series']: series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
# Sort regular titles < series titles key = '%s ~%s %s' % (book['author_sort'].capitalize(),
return -1 self.generateSortTitle(book['series']),
elif not y['series']: series_index)
return 1 return key
# Different series def booksByAuthorSorter_author(self, book):
if x['title_sort'].lstrip() > y['title_sort'].lstrip(): '''
return 1 Sort non-series books before series books
else: '''
return -1 if not book['series']:
else: key = '%s %s' % (self.author_to_author_sort(book['author']),
# Same series book['title_sort'].capitalize())
if x['series'] == y['series']: else:
if float(x['series_index']) > float(y['series_index']): index = book['series_index']
return 1 integer = int(index)
elif float(x['series_index']) < float(y['series_index']): fraction = index-integer
return -1 series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
else: key = '%s ~%s %s' % (self.author_to_author_sort(book['author']),
return 0 self.generateSortTitle(book['series']),
else: series_index)
if x['series'] > y['series']: return key
return 1
else:
return -1
def calculateThumbnailSize(self): def calculateThumbnailSize(self):
''' Calculate thumbnail dimensions based on device DPI. Scale Kindle by 50% ''' ''' Calculate thumbnail dimensions based on device DPI. Scale Kindle by 50% '''
@ -4155,6 +4093,20 @@ then rebuild the catalog.\n''').format(author[0])
# Strip white space to '' # Strip white space to ''
return re.sub("\W","", author) return re.sub("\W","", author)
def generateFormatArgs(self, book):
series_index = str(book['series_index'])
if series_index.endswith('.0'):
series_index = series_index[:-2]
args = dict(
title = book['title'],
series = book['series'],
series_index = series_index,
rating = self.generateRatingString(book),
rating_parens = '(%s)' % self.generateRatingString(book) if 'rating' in book else '',
pubyear = book['date'].split()[1] if book['date'] else '',
pubyear_parens = "(%s)" % book['date'].split()[1] if book['date'] else '')
return args
def generateHTMLByGenre(self, genre, section_head, books, outfile): def generateHTMLByGenre(self, genre, section_head, books, outfile):
# Write an HTML file of this genre's book list # Write an HTML file of this genre's book list
# Return a list with [(first_author, first_book), (last_author, last_book)] # Return a list with [(first_author, first_book), (last_author, last_book)]
@ -4201,16 +4153,6 @@ then rebuild the catalog.\n''').format(author[0])
divTag.insert(dtc,pAuthorTag) divTag.insert(dtc,pAuthorTag)
dtc += 1 dtc += 1
'''
# Insert an <hr /> between non-series and series
if not current_series and non_series_books and book['series']:
# Insert an <hr />
hrTag = Tag(soup,'hr')
hrTag['class'] = "series_divider"
divTag.insert(dtc,hrTag)
dtc += 1
'''
# Check for series # Check for series
if book['series'] and book['series'] != current_series: if book['series'] and book['series'] != current_series:
# Start a new series # Start a new series
@ -4235,17 +4177,6 @@ then rebuild the catalog.\n''').format(author[0])
pBookTag = Tag(soup, "p") pBookTag = Tag(soup, "p")
ptc = 0 ptc = 0
'''
# This if clause does not display MISSING_SYMBOL for wishlist items
# If this is the wishlist_tag genre, don't show missing symbols
# normalized_wishlist_tag = self.genre_tags_dict[self.opts.wishlist_tag]
if self.opts.wishlist_tag in book['tags'] and \
self.genre_tags_dict[self.opts.wishlist_tag] != genre:
pBookTag['class'] = "wishlist_item"
pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL))
ptc += 1
'''
# book with read|reading|unread symbol or wishlist item # book with read|reading|unread symbol or wishlist item
if self.opts.wishlist_tag in book.get('tags', []): if self.opts.wishlist_tag in book.get('tags', []):
pBookTag['class'] = "wishlist_item" pBookTag['class'] = "wishlist_item"
@ -4271,12 +4202,18 @@ then rebuild the catalog.\n''').format(author[0])
aTag = Tag(soup, "a") aTag = Tag(soup, "a")
if self.opts.generate_descriptions: if self.opts.generate_descriptions:
aTag['href'] = "book_%d.html" % (int(float(book['id']))) aTag['href'] = "book_%d.html" % (int(float(book['id'])))
# Use series, series index if avail else just title
# Generate the title from the template
args = self.generateFormatArgs(book)
if current_series: if current_series:
aTag.insert(0,escape(book['title'][len(book['series'])+1:])) #aTag.insert(0,escape(book['title'][len(book['series'])+1:]))
formatted_title = self.by_genres_series_title_template.format(**args).rstrip()
else: else:
aTag.insert(0,escape(book['title'])) #aTag.insert(0,escape(book['title']))
formatted_title = self.by_genres_normal_title_template.format(**args).rstrip()
non_series_books += 1 non_series_books += 1
aTag.insert(0,NavigableString(escape(formatted_title)))
pBookTag.insert(ptc, aTag) pBookTag.insert(ptc, aTag)
ptc += 1 ptc += 1
@ -4327,31 +4264,16 @@ then rebuild the catalog.\n''').format(author[0])
generated_html = substitute_entites(generated_html) generated_html = substitute_entites(generated_html)
return BeautifulSoup(generated_html) return BeautifulSoup(generated_html)
if False:
print "title metadata:\n%s" % ', '.join(sorted(book.keys()))
if False:
for item in sorted(book.keys()):
try:
print "%s: %s%s" % (item, book[item][:50], '...' if len(book[item])>50 else '')
except:
print "%s: %s" % (item, book[item])
# Generate the template arguments # Generate the template arguments
css = P('catalog/stylesheet.css', data=True).decode('utf-8') css = P('catalog/stylesheet.css', data=True).decode('utf-8')
title_str = escape(book['title']) title_str = title = escape(book['title'])
series = ''
# Title/series series_index = ''
if book['series']: if book['series']:
series_id, _, title = book['title'].partition(':')
title = escape(title.strip())
series = escape(book['series']) series = escape(book['series'])
series_index = str(book['series_index']) series_index = str(book['series_index'])
if series_index.endswith('.0'): if series_index.endswith('.0'):
series_index = series_index[:-2] series_index = series_index[:-2]
else:
title = escape(book['title'])
series = ''
series_index = ''
# Author, author_prefix (read|reading|none symbol or missing symbol) # Author, author_prefix (read|reading|none symbol or missing symbol)
author = book['author'] author = book['author']
@ -4392,12 +4314,14 @@ then rebuild the catalog.\n''').format(author[0])
# Date of publication # Date of publication
pubdate = book['date'] pubdate = book['date']
pubmonth, pubyear = pubdate.split(' ') pubmonth, pubyear = pubdate.split()
if pubyear == '101':
pubdate = pubmonth = pubyear = ''
# Thumb # Thumb
_soup = BeautifulSoup('<html>',selfClosingTags=['img']) _soup = BeautifulSoup('<html>',selfClosingTags=['img'])
thumb = Tag(_soup,"img") thumb = Tag(_soup,"img")
if 'cover' in book: if 'cover' in book and book['cover']:
thumb['src'] = "../images/thumbnail_%d.jpg" % int(book['id']) thumb['src'] = "../images/thumbnail_%d.jpg" % int(book['id'])
else: else:
thumb['src'] = "../images/thumbnail_default.jpg" thumb['src'] = "../images/thumbnail_default.jpg"
@ -4562,16 +4486,19 @@ then rebuild the catalog.\n''').format(author[0])
draw.text((left, top), text, fill=(0,0,0), font=font) draw.text((left, top), text, fill=(0,0,0), font=font)
img.save(open(out_path, 'wb'), 'GIF') img.save(open(out_path, 'wb'), 'GIF')
def generateSeriesTitle(self, title): def generateRatingString(self, book):
if float(title['series_index']) - int(title['series_index']): rating = ''
series_title = '%s %4.2f: %s' % (title['series'], try:
title['series_index'], if 'rating' in book:
title['title']) stars = int(book['rating']) / 2
else: if stars:
series_title = '%s %d: %s' % (title['series'], star_string = self.FULL_RATING_SYMBOL * stars
title['series_index'], empty_stars = self.EMPTY_RATING_SYMBOL * (5 - stars)
title['title']) rating = '%s%s' % (star_string,empty_stars)
return series_title except:
# Rating could be None
pass
return rating
def generateShortDescription(self, description, dest=None): def generateShortDescription(self, description, dest=None):
# Truncate the description, on word boundaries if necessary # Truncate the description, on word boundaries if necessary
@ -4610,9 +4537,11 @@ then rebuild the catalog.\n''').format(author[0])
raise RuntimeError raise RuntimeError
def generateSortTitle(self, title): def generateSortTitle(self, title):
# Generate a string suitable for sorting from the title '''
# Ignore leading stop words Generate a string suitable for sorting from the title
# Optionally convert leading numbers to strings Ignore leading stop words
Optionally convert leading numbers to strings
'''
from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata import title_sort
# Strip stop words # Strip stop words
@ -4910,13 +4839,6 @@ then rebuild the catalog.\n''').format(author[0])
self.progressInt = coarse_progress + fine_progress self.progressInt = coarse_progress + fine_progress
self.reporter(self.progressInt, self.progressString) self.reporter(self.progressInt, self.progressString)
class NotImplementedError:
def __init__(self, error):
self.error = error
def logerror(self):
self.opts.log.info('%s not implemented' % self.error)
def run(self, path_to_output, opts, db, notification=DummyReporter()): def run(self, path_to_output, opts, db, notification=DummyReporter()):
opts.log = log opts.log = log
opts.fmt = self.fmt = path_to_output.rpartition('.')[2] opts.fmt = self.fmt = path_to_output.rpartition('.')[2]
@ -4982,11 +4904,12 @@ then rebuild the catalog.\n''').format(author[0])
if opts_dict['ids']: if opts_dict['ids']:
build_log.append(" book count: %d" % len(opts_dict['ids'])) build_log.append(" book count: %d" % len(opts_dict['ids']))
sections_list = ['Authors']
''' '''
sections_list = []
if opts.generate_authors: if opts.generate_authors:
sections_list.append('Authors') sections_list.append('Authors')
''' '''
sections_list = ['Authors']
if opts.generate_titles: if opts.generate_titles:
sections_list.append('Titles') sections_list.append('Titles')
if opts.generate_genres: if opts.generate_genres:
@ -5042,7 +4965,7 @@ then rebuild the catalog.\n''').format(author[0])
if catalog_source_built: if catalog_source_built:
log.info(" Completed catalog source generation\n") log.info(" Completed catalog source generation\n")
else: else:
log.warn(" No database hits with supplied criteria") log.warn(" *** Errors during catalog generation, check log for details ***")
if catalog_source_built: if catalog_source_built:
recommendations = [] recommendations = []
@ -5072,8 +4995,6 @@ then rebuild the catalog.\n''').format(author[0])
abort_after_input_dump=False) abort_after_input_dump=False)
plumber.merge_ui_recommendations(recommendations) plumber.merge_ui_recommendations(recommendations)
plumber.run() plumber.run()
# returns to gui2.actions.catalog:catalog_generated()
return None # returns to gui2.actions.catalog:catalog_generated()
else: return catalog.error
# returns to gui2.actions.catalog:catalog_generated()
return catalog.error

View File

@ -693,14 +693,7 @@ def command_catalog(args, dbpath):
} }
with plugin: with plugin:
ret = plugin.run(args[1], opts, get_db(dbpath, opts)) return int(bool(plugin.run(args[1], opts, get_db(dbpath, opts))))
if ret is None:
ret = 0
else:
ret = 1
return ret
# end of GR additions
def parse_series_string(db, label, value): def parse_series_string(db, label, value):
val = unicode(value).strip() val = unicode(value).strip()

View File

@ -1060,6 +1060,10 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
return [ (i[0], i[1]) for i in \ return [ (i[0], i[1]) for i in \
self.conn.get('SELECT id, name FROM authors')] self.conn.get('SELECT id, name FROM authors')]
def all_author_names(self):
return filter(None, [i[0].strip().replace('|', ',') for i in self.conn.get(
'SELECT name FROM authors')])
def all_publishers(self): def all_publishers(self):
return [ (i[0], i[1]) for i in \ return [ (i[0], i[1]) for i in \
self.conn.get('SELECT id, name FROM publishers')] self.conn.get('SELECT id, name FROM publishers')]

View File

@ -100,7 +100,7 @@ class AumSortedConcatenate(object):
keys = self.ans.keys() keys = self.ans.keys()
l = len(keys) l = len(keys)
if l == 0: if l == 0:
return 'Unknown:::Unknown' return None
if l == 1: if l == 1:
return self.ans[keys[0]] return self.ans[keys[0]]
return ':#:'.join([self.ans[v] for v in sorted(keys)]) return ':#:'.join([self.ans[v] for v in sorted(keys)])

View File

@ -589,6 +589,7 @@ Some limitations of PDF input are:
* Extraction of vector images and tables from within the document is also not supported. * Extraction of vector images and tables from within the document is also not supported.
* Some PDFs use special glyphs to represent ll or ff or fi, etc. Conversion of these may or may not work depending on just how they are represented internally in the PDF. * Some PDFs use special glyphs to represent ll or ff or fi, etc. Conversion of these may or may not work depending on just how they are represented internally in the PDF.
* Some PDFs store their images upside down with a rotation instruction, |app| currently doesn't support that instruction, so the images will be rotated in the output as well. * Some PDFs store their images upside down with a rotation instruction, |app| currently doesn't support that instruction, so the images will be rotated in the output as well.
* Links and Tables of Contents are not supported
To re-iterate **PDF is a really, really bad** format to use as input. If you absolutely must use PDF, then be prepared for an To re-iterate **PDF is a really, really bad** format to use as input. If you absolutely must use PDF, then be prepared for an
output ranging anywhere from decent to unusable, depending on the input PDF. output ranging anywhere from decent to unusable, depending on the input PDF.

View File

@ -101,6 +101,17 @@ We just need some information from you:
Once you send us the output for a particular operating system, support for the device in that operating system Once you send us the output for a particular operating system, support for the device in that operating system
will appear in the next release of |app|. will appear in the next release of |app|.
My device is not being detected by |app|?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Follow these steps to find the problem:
* Make sure that you are connecting only a single device to your computer at a time. Do not have another |app| supported device like an iPhone/iPad etc. at the same time.
* Make sure you are running the latest version of |app|. The latest version can always be downloaded from `http://calibre-ebook.com/download`_.
* Ensure your operating system is seeing the device. That is, the device should be mounted as a disk that you can access using Windows explorer or whatever the file management program on your computer is
* In calibre, go to Preferences->Plugins->Device Interface plugin and make sure the plugin for your device is enabled.
* If all the above steps fail, go to Preferences->Miscellaneous and click debug device detection with your device attached and post the output as a ticket on `http://bugs.calibre-ebook.com`_.
How does |app| manage collections on my SONY reader? How does |app| manage collections on my SONY reader?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -450,6 +461,11 @@ How do I use purchased EPUB books with |app|?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Most purchased EPUB books have `DRM <http://wiki.mobileread.com/wiki/DRM>`_. This prevents |app| from opening them. You can still use |app| to store and transfer them to your e-book reader. First, you must authorize your reader on a windows machine with Adobe Digital Editions. Once this is done, EPUB books transferred with |app| will work fine on your reader. When you purchase an epub book from a website, you will get an ".acsm" file. This file should be opened with Adobe Digital Editions, which will then download the actual ".epub" e-book. The e-book file will be stored in the folder "My Digital Editions", from where you can add it to |app|. Most purchased EPUB books have `DRM <http://wiki.mobileread.com/wiki/DRM>`_. This prevents |app| from opening them. You can still use |app| to store and transfer them to your e-book reader. First, you must authorize your reader on a windows machine with Adobe Digital Editions. Once this is done, EPUB books transferred with |app| will work fine on your reader. When you purchase an epub book from a website, you will get an ".acsm" file. This file should be opened with Adobe Digital Editions, which will then download the actual ".epub" e-book. The e-book file will be stored in the folder "My Digital Editions", from where you can add it to |app|.
I am getting a "Permission Denied" error?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A permission denied error can occur because of many possible reasons, none of them having anything to do with |app|. You can get permission denied errors if you are using an SD card with write protect enabled. Or if you, or some program you used changed the file permissions of the files in question to read only. Or if there is a filesystem error on the device which caused your operating system to mount the filesystem in read only mode or mark a particular file as read only pending recovery. Or if the files have their owner set to a user other than you. You will need to fix the underlying cause of the permissions error before resuming to use |app|. Read the error message carefully, see what file it points to and fix the permissions on that file.
Can I have the comment metadata show up on my reader? Can I have the comment metadata show up on my reader?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -148,6 +148,9 @@ def format_date(dt, format, assume_utc=False, as_utc=False):
if len(mo.group(0)) == 2: return '%02d'%(dt.year % 100) if len(mo.group(0)) == 2: return '%02d'%(dt.year % 100)
return '%04d'%dt.year return '%04d'%dt.year
if dt == UNDEFINED_DATE:
return ''
format = re.sub('d{1,4}', format_day, format) format = re.sub('d{1,4}', format_day, format)
format = re.sub('M{1,4}', format_month, format) format = re.sub('M{1,4}', format_month, format)
return re.sub('yyyy|yy', format_year, format) return re.sub('yyyy|yy', format_year, format)

View File

@ -274,9 +274,9 @@ class TemplateFormatter(string.Formatter):
colon += 1 colon += 1
funcs = formatter_functions.get_functions() funcs = formatter_functions.get_functions()
if fmt[colon:p] in funcs: fname = fmt[colon:p]
field = fmt[colon:p] if fname in funcs:
func = funcs[field] func = funcs[fname]
if func.arg_count == 2: if func.arg_count == 2:
# only one arg expected. Don't bother to scan. Avoids need # only one arg expected. Don't bother to scan. Avoids need
# for escaping characters # for escaping characters
@ -292,6 +292,8 @@ class TemplateFormatter(string.Formatter):
else: else:
val = func.eval_(self, self.kwargs, self.book, self.locals, val = func.eval_(self, self.kwargs, self.book, self.locals,
val, *args).strip() val, *args).strip()
else:
return _('%s: unknown function')%fname
if val: if val:
val = self._do_format(val, dispfmt) val = self._do_format(val, dispfmt)
if not val: if not val:

View File

@ -839,7 +839,13 @@ class BasicNewsRecipe(Recipe):
fetcher.image_url_processor = self.image_url_processor fetcher.image_url_processor = self.image_url_processor
res, path, failures = fetcher.start_fetch(url), fetcher.downloaded_paths, fetcher.failed_links res, path, failures = fetcher.start_fetch(url), fetcher.downloaded_paths, fetcher.failed_links
if not res or not os.path.exists(res): if not res or not os.path.exists(res):
raise Exception(_('Could not fetch article. Run with -vv to see the reason')) msg = _('Could not fetch article.') + ' '
if self.debug:
msg += _('The debug traceback is available earlier in this log')
else:
msg += _('Run with -vv to see the reason')
raise Exception(msg)
return res, path, failures return res, path, failures
def fetch_article(self, url, dir, f, a, num_of_feeds): def fetch_article(self, url, dir, f, a, num_of_feeds):
@ -902,9 +908,6 @@ class BasicNewsRecipe(Recipe):
feeds = feeds[:2] feeds = feeds[:2]
self.has_single_feed = len(feeds) == 1 self.has_single_feed = len(feeds) == 1
if self.use_embedded_content is None:
self.use_embedded_content = feeds[0].has_embedded_content()
index = os.path.join(self.output_dir, 'index.html') index = os.path.join(self.output_dir, 'index.html')
html = self.feeds2index(feeds) html = self.feeds2index(feeds)
@ -939,7 +942,9 @@ class BasicNewsRecipe(Recipe):
url = None url = None
if not url: if not url:
continue continue
func, arg = (self.fetch_embedded_article, article) if self.use_embedded_content else \ func, arg = (self.fetch_embedded_article, article) \
if self.use_embedded_content or (self.use_embedded_content == None and feed.has_embedded_content()) \
else \
((self.fetch_obfuscated_article if self.articles_are_obfuscated \ ((self.fetch_obfuscated_article if self.articles_are_obfuscated \
else self.fetch_article), url) else self.fetch_article), url)
req = WorkRequest(func, (arg, art_dir, f, a, len(feed)), req = WorkRequest(func, (arg, art_dir, f, a, len(feed)),