GwR revisions

This commit is contained in:
GRiker 2010-06-03 06:42:03 -06:00
commit 75f243a470
37 changed files with 977 additions and 365 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

View File

@ -1,189 +1,76 @@
__license__ = 'GPL v3' import string
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import re
from calibre import strftime
from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class Newsweek(BasicNewsRecipe): class Newsweek(BasicNewsRecipe):
title = 'Newsweek' title = 'Newsweek'
__author__ = 'Kovid Goyal and Sujata Raman' __author__ = 'Kovid Goyal'
description = 'Weekly news and current affairs in the US' description = 'Weekly news and current affairs in the US'
language = 'en'
encoding = 'utf-8'
no_stylesheets = True no_stylesheets = True
extra_css = ''' BASE_URL = 'http://www.newsweek.com'
h1{font-family:Arial,Helvetica,sans-serif; font-size:large; color:#383733;} INDEX = BASE_URL+'/topics.html'
.deck{font-family:Georgia,sans-serif; color:#383733;}
.bylineDate{font-family:georgia ; color:#58544A; font-size:x-small;}
.authorInfo{font-family:arial,helvetica,sans-serif; color:#0066CC; font-size:x-small;}
.articleUpdated{font-family:arial,helvetica,sans-serif; color:#73726C; font-size:x-small;}
.issueDate{font-family:arial,helvetica,sans-serif; color:#73726C; font-size:x-small; font-style:italic;}
h5{font-family:arial,helvetica,sans-serif; color:#73726C; font-size:x-small;}
h6{font-family:arial,helvetica,sans-serif; color:#73726C; font-size:x-small;}
.story{font-family:georgia,sans-serif ;color:black;}
.photoCredit{color:#999999; font-family:Arial,Helvetica,sans-serif;font-size:x-small;}
.photoCaption{color:#0A0A09;font-family:Arial,Helvetica,sans-serif;font-size:x-small;}
.fwArticle{font-family:Arial,Helvetica,sans-serif;font-size:x-small;font-weight:bold;}
'''
encoding = 'utf-8' keep_only_tags = dict(name='article', attrs={'class':'article-text'})
language = 'en' remove_tags = [dict(attrs={'data-dartad':True})]
remove_attributes = ['property']
remove_tags = [ def postprocess_html(self, soup, first):
{'class':['fwArticle noHr','fwArticle','hdlBulletItem','head-content','navbar','link', 'ad', 'sponsorLinksArticle', 'mm-content', for tag in soup.findAll(name=['article', 'header']):
'inline-social-links-wrapper', 'email-article','ToolBox', tag.name = 'div'
'inline-promo-link', 'sponsorship', return soup
'inlineComponentRight',
'comments-and-social-links-wrapper', 'EmailArticleBlock']}, def newsweek_sections(self):
{'id' : ['footer', 'ticker-data', 'topTenVertical', soup = self.index_to_soup(self.INDEX)
'digg-top-five', 'mesothorax', 'nw-comments', 'my-take-landing', for a in soup.findAll('a', title='Primary tag', href=True):
'ToolBox', 'EmailMain']}, yield (string.capitalize(self.tag_to_string(a)),
{'class': re.compile('related-cloud')}, self.BASE_URL+a['href'])
dict(name='li', attrs={'id':['slug_bigbox']})
]
keep_only_tags = [{'class':['article HorizontalHeader', def newsweek_parse_section_page(self, soup):
'articlecontent','photoBox', 'article columnist first']}, ] for article in soup.findAll('article', about=True,
recursions = 1 attrs={'class':'stream-item'}):
match_regexps = [r'http://www.newsweek.com/id/\S+/page/\d+'] title = article.find(attrs={'property': 'dc:title'})
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')] if title is None: continue
title = self.tag_to_string(title)
def find_title(self, section): url = self.BASE_URL + article['about']
d = {'scope':'Scope', 'thetake':'The Take', 'features':'Features', desc = ''
None:'Departments', 'culture':'Culture'} author = article.find({'property':'dc:creator'})
ans = None if author:
a = section.find('a', attrs={'name':True}) desc = u'by %s. '%self.tag_to_string(author)
if a is not None: p = article.find(attrs={'property':'dc:abstract'})
ans = a['name'] if p is not None:
return d.get(ans, ans) for a in p.find('a'): a.extract()
desc += self.tag_to_string(p)
t = article.find('time', attrs={'property':'dc:created'})
def find_articles(self, section): date = ''
ans = [] if t is not None:
for x in section.findAll('h5'): date = u' [%s]'%self.tag_to_string(t)
title = ' '.join(x.findAll(text=True)).strip() self.log('\tFound article:', title, 'at', url)
a = x.find('a') self.log('\t\t', desc)
if not a: continue yield {'title':title, 'url':url, 'description':desc, 'date':date}
href = a['href']
ans.append({'title':title, 'url':href, 'description':'', 'date': strftime('%a, %d %b')})
if not ans:
for x in section.findAll('div', attrs={'class':'hdlItem'}):
a = x.find('a', href=True)
if not a : continue
title = ' '.join(a.findAll(text=True)).strip()
href = a['href']
if 'http://xtra.newsweek.com' in href: continue
ans.append({'title':title, 'url':href, 'description':'', 'date': strftime('%a, %d %b')})
#for x in ans:
# x['url'] += '/output/print'
return ans
def parse_index(self): def parse_index(self):
soup = self.get_current_issue() sections = []
if not soup: for section, shref in self.newsweek_sections():
raise RuntimeError('Unable to connect to newsweek.com. Try again later.') self.log('Processing section', section, shref)
sections = soup.findAll('div', attrs={'class':'featurewell'}) articles = []
titles = map(self.find_title, sections) soups = [self.index_to_soup(shref)]
articles = map(self.find_articles, sections) na = soups[0].find('a', rel='next')
ans = list(zip(titles, articles)) if na:
def fcmp(x, y): soups.append(self.index_to_soup(self.BASE_URL+na['href']))
tx, ty = x[0], y[0] for soup in soups:
if tx == "Features": return cmp(1, 2) articles.extend(self.newsweek_parse_section_page(soup))
if ty == "Features": return cmp(2, 1) if self.test and len(articles) > 1:
return cmp(tx, ty) break
return sorted(ans, cmp=fcmp) if articles:
sections.append((section, articles))
def ensure_html(self, soup): if self.test and len(sections) > 1:
root = soup.find(name=True) break
if root.name == 'html': return soup return sections
nsoup = BeautifulSoup('<html><head></head><body/></html>')
nroot = nsoup.find(name='body')
for x in soup.contents:
if getattr(x, 'name', False):
x.extract()
nroot.insert(len(nroot), x)
return nsoup
def postprocess_html(self, soup, first_fetch):
if not first_fetch:
h1 = soup.find(id='headline')
if h1:
h1.extract()
div = soup.find(attrs={'class':'articleInfo'})
if div:
div.extract()
divs = list(soup.findAll('div', 'pagination'))
if not divs:
return self.ensure_html(soup)
for div in divs[1:]: div.extract()
all_a = divs[0].findAll('a', href=True)
divs[0]['style']="display:none"
if len(all_a) > 1:
all_a[-1].extract()
test = re.compile(self.match_regexps[0])
for a in soup.findAll('a', href=test):
if a not in all_a:
del a['href']
return self.ensure_html(soup)
def get_current_issue(self):
soup = self.index_to_soup('http://www.newsweek.com')
div = soup.find('div', attrs={'class':re.compile('more-from-mag')})
if div is None: return None
a = div.find('a')
if a is not None:
href = a['href'].split('#')[0]
return self.index_to_soup(href)
def get_cover_url(self):
cover_url = None
soup = self.index_to_soup('http://www.newsweek.com')
link_item = soup.find('div',attrs={'class':'cover-image'})
if link_item and link_item.a and link_item.a.img:
cover_url = link_item.a.img['src']
return cover_url
def postprocess_book(self, oeb, opts, log) :
def extractByline(href) :
soup = BeautifulSoup(str(oeb.manifest.hrefs[href]))
byline = soup.find(True,attrs={'class':'authorInfo'})
byline = self.tag_to_string(byline) if byline is not None else ''
issueDate = soup.find(True,attrs={'class':'issueDate'})
issueDate = self.tag_to_string(issueDate) if issueDate is not None else ''
issueDate = re.sub(',','', issueDate)
if byline > '' and issueDate > '' :
return byline + ' | ' + issueDate
else :
return byline + issueDate
def extractDescription(href) :
soup = BeautifulSoup(str(oeb.manifest.hrefs[href]))
description = soup.find(True,attrs={'name':'description'})
if description is not None and description.has_key('content'):
description = description['content']
if description.startswith('Newsweek magazine online plus') :
description = soup.find(True, attrs={'class':'story'})
firstPara = soup.find('p')
description = self.tag_to_string(firstPara)
else :
description = soup.find(True, attrs={'class':'story'})
firstPara = soup.find('p')
description = self.tag_to_string(firstPara)
return description
for section in oeb.toc :
for article in section :
if article.author is None :
article.author = extractByline(article.href)
if article.description is None :
article.description = extractDescription(article.href)
return

View File

@ -391,10 +391,14 @@ class NYTimes(BasicNewsRecipe):
return ans return ans
def preprocess_html(self, soup): def preprocess_html(self, soup):
# Skip ad pages before actual article # Skip ad pages served before actual article
skip_tag = soup.find(True, {'name':'skip'}) skip_tag = soup.find(True, {'name':'skip'})
if skip_tag is not None: if skip_tag is not None:
soup = self.index_to_soup(skip_tag.parent['href']) self.log.error("Found forwarding link: %s" % skip_tag.parent['href'])
url = 'http://www.nytimes.com' + re.sub(r'\?.*', '', skip_tag.parent['href'])
url += '?pagewanted=all'
self.log.error("Skipping ad to article at '%s'" % url)
soup = self.index_to_soup(url)
return self.strip_anchors(soup) return self.strip_anchors(soup)
def postprocess_html(self,soup, True): def postprocess_html(self,soup, True):

View File

@ -280,18 +280,14 @@ class NYTimes(BasicNewsRecipe):
return ans return ans
def preprocess_html(self, soup): def preprocess_html(self, soup):
''' # Skip ad pages served before actual article
refresh = soup.find('meta', {'http-equiv':'refresh'})
if refresh is None:
return soup
content = refresh.get('content').partition('=')[2]
raw = self.browser.open('http://www.nytimes.com'+content).read()
return BeautifulSoup(raw.decode('cp1252', 'replace'))
'''
# Skip ad pages before actual article
skip_tag = soup.find(True, {'name':'skip'}) skip_tag = soup.find(True, {'name':'skip'})
if skip_tag is not None: if skip_tag is not None:
soup = self.index_to_soup(skip_tag.parent['href']) self.log.error("Found forwarding link: %s" % skip_tag.parent['href'])
url = 'http://www.nytimes.com' + re.sub(r'\?.*', '', skip_tag.parent['href'])
url += '?pagewanted=all'
self.log.error("Skipping ad to article at '%s'" % url)
soup = self.index_to_soup(url)
return self.strip_anchors(soup) return self.strip_anchors(soup)
def postprocess_html(self,soup, True): def postprocess_html(self,soup, True):

View File

@ -0,0 +1,66 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
sarajevo-x.com
'''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import Tag, NavigableString
class SarajevoX(BasicNewsRecipe):
title = 'Sarajevo-x.com'
__author__ = 'Darko Miletic'
description = 'Sarajevo-x.com - najposjeceniji bosanskohercegovacki internet portal'
publisher = 'InterSoft d.o.o.'
category = 'news, politics, Bosnia and Herzegovina,Sarajevo-x.com, internet, portal, vijesti, bosna i hercegovina, sarajevo'
oldest_article = 2
delay = 1
max_articles_per_feed = 100
no_stylesheets = True
encoding = 'cp1250'
use_embedded_content = False
language = 'bs'
extra_css = ' @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: Arial,Verdana,Helvetica,sans1,sans-serif} .article_description{font-family: sans1, sans-serif} div#fotka{display: block} img{margin-bottom: 0.5em} '
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [dict(name='div', attrs={'class':'content-bg'})]
remove_tags_after = dict(name='div',attrs={'class':'izvor'})
remove_tags = [dict(name=['object','link','base','table'])]
remove_attributes = ['height','width','alt','border']
feeds = [
(u'BIH' , u'http://www.sarajevo-x.com/rss/bih' )
,(u'Svijet' , u'http://www.sarajevo-x.com/rss/svijet' )
,(u'Biznis' , u'http://www.sarajevo-x.com/rss/biznis' )
,(u'Sport' , u'http://www.sarajevo-x.com/rss/sport' )
,(u'Showtime' , u'http://www.sarajevo-x.com/rss/showtime' )
,(u'Scitech' , u'http://www.sarajevo-x.com/rss/scitech' )
,(u'Lifestyle' , u'http://www.sarajevo-x.com/rss/lifestyle' )
,(u'Kultura' , u'http://www.sarajevo-x.com/rss/kultura' )
,(u'Zanimljivosti', u'http://www.sarajevo-x.com/rss/zanimljivosti')
]
def preprocess_html(self, soup):
dtag = soup.find('div',attrs={'id':'fotka'})
if dtag:
sp = soup.find('div',attrs={'id':'opisslike'})
img = soup.find('img')
if sp:
sp
else:
mtag = Tag(soup,'div',[("id","opisslike"),("class","opscitech")])
mopis = NavigableString("Opis")
mtag.insert(0,mopis)
img.append(mtag)
return soup

View File

@ -29,7 +29,7 @@ class Plugin(object):
''' '''
#: List of platforms this plugin works on #: List of platforms this plugin works on
#: For example: ``['windows', 'osx', 'linux'] #: For example: ``['windows', 'osx', 'linux']``
supported_platforms = [] supported_platforms = []
#: The name of this plugin. You must set it something other #: The name of this plugin. You must set it something other
@ -214,10 +214,8 @@ class MetadataReaderPlugin(Plugin):
Return metadata for the file represented by stream (a file like object Return metadata for the file represented by stream (a file like object
that supports reading). Raise an exception when there is an error that supports reading). Raise an exception when there is an error
with the input data. with the input data.
:param type: The type of file. Guaranteed to be one of the entries :param type: The type of file. Guaranteed to be one of the entries
in :attr:`file_types`. in :attr:`file_types`.
:return: A :class:`calibre.ebooks.metadata.MetaInformation` object :return: A :class:`calibre.ebooks.metadata.MetaInformation` object
''' '''
return None return None
@ -245,11 +243,9 @@ class MetadataWriterPlugin(Plugin):
Set metadata for the file represented by stream (a file like object Set metadata for the file represented by stream (a file like object
that supports reading). Raise an exception when there is an error that supports reading). Raise an exception when there is an error
with the input data. with the input data.
:param type: The type of file. Guaranteed to be one of the entries :param type: The type of file. Guaranteed to be one of the entries
in :attr:`file_types`. in :attr:`file_types`.
:param mi: A :class:`calibre.ebooks.metadata.MetaInformation` object :param mi: A :class:`calibre.ebooks.metadata.MetaInformation` object
''' '''
pass pass

View File

@ -240,6 +240,9 @@ class OutputProfile(Plugin):
# Device supports displaying a nested TOC # Device supports displaying a nested TOC
supports_nested_toc = True supports_nested_toc = True
# If True output should be optimized for a touchscreen interface
touchscreen = False
@classmethod @classmethod
def tags_to_string(cls, tags): def tags_to_string(cls, tags):
return escape(', '.join(tags)) return escape(', '.join(tags))

View File

@ -14,8 +14,14 @@ XMLDECL_RE = re.compile(r'^\s*<[?]xml.*?[?]>')
SVG_NS = 'http://www.w3.org/2000/svg' SVG_NS = 'http://www.w3.org/2000/svg'
XLINK_NS = 'http://www.w3.org/1999/xlink' XLINK_NS = 'http://www.w3.org/1999/xlink'
convert_entities = functools.partial(entity_to_unicode, exceptions=['quot', convert_entities = functools.partial(entity_to_unicode,
'apos', 'lt', 'gt', 'amp', '#60', '#62']) result_exceptions = {
u'<' : '&lt;',
u'>' : '&gt;',
u"'" : '&apos;',
u'"' : '&quot;',
u'&' : '&amp;',
})
_span_pat = re.compile('<span.*?</span>', re.DOTALL|re.IGNORECASE) _span_pat = re.compile('<span.*?</span>', re.DOTALL|re.IGNORECASE)
LIGATURES = { LIGATURES = {

View File

@ -416,9 +416,9 @@ class HTMLInput(InputFormatPlugin):
link = unquote(link).replace('/', os.sep) link = unquote(link).replace('/', os.sep)
if not link.strip(): if not link.strip():
return link_ return link_
if base and not os.path.isabs(link):
link = os.path.join(base, link)
try: try:
if base and not os.path.isabs(link):
link = os.path.join(base, link)
link = os.path.abspath(link) link = os.path.abspath(link)
except: except:
return link_ return link_

View File

@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import struct, array, zlib, cStringIO, collections, re import struct, array, zlib, cStringIO, collections, re
from calibre.ebooks.lrf import LRFParseError, PRS500_PROFILE from calibre.ebooks.lrf import LRFParseError, PRS500_PROFILE
from calibre import entity_to_unicode from calibre import entity_to_unicode, prepare_string_for_xml
from calibre.ebooks.lrf.tags import Tag from calibre.ebooks.lrf.tags import Tag
ruby_tags = { ruby_tags = {
@ -870,7 +870,7 @@ class Text(LRFStream):
open_containers = collections.deque() open_containers = collections.deque()
for c in self.content: for c in self.content:
if isinstance(c, basestring): if isinstance(c, basestring):
s += c s += prepare_string_for_xml(c)
elif c is None: elif c is None:
if open_containers: if open_containers:
p = open_containers.pop() p = open_containers.pop()

View File

@ -11,7 +11,7 @@ import re
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.chardet import xml_to_unicode
from calibre import entity_to_unicode
def get_metadata(stream): def get_metadata(stream):
src = stream.read() src = stream.read()
@ -43,6 +43,10 @@ def get_metadata_(src, encoding=None):
if match: if match:
author = match.group(2).replace(',', ';') author = match.group(2).replace(',', ';')
ent_pat = re.compile(r'&(\S+)?;')
title = ent_pat.sub(entity_to_unicode, title)
if author:
author = ent_pat.sub(entity_to_unicode, author)
mi = MetaInformation(title, [author] if author else None) mi = MetaInformation(title, [author] if author else None)
# Publisher # Publisher

View File

@ -787,7 +787,6 @@ class Manifest(object):
data = self.oeb.decode(data) data = self.oeb.decode(data)
data = self.oeb.html_preprocessor(data) data = self.oeb.html_preprocessor(data)
# Remove DOCTYPE declaration as it messes up parsing # Remove DOCTYPE declaration as it messes up parsing
# In particular, it causes tostring to insert xmlns # In particular, it causes tostring to insert xmlns
# declarations, which messes up the coercing logic # declarations, which messes up the coercing logic

View File

@ -136,6 +136,8 @@ class CoverManager(object):
href = g['cover'].href href = g['cover'].href
else: else:
href = self.default_cover() href = self.default_cover()
if href is None:
return
width, height = self.inspect_cover(href) width, height = self.inspect_cover(href)
if width is None or height is None: if width is None or height is None:
self.log.warning('Failed to read cover dimensions') self.log.warning('Failed to read cover dimensions')

View File

@ -97,7 +97,8 @@ def _config():
help=_('Overwrite author and title with new metadata')) help=_('Overwrite author and title with new metadata'))
c.add_opt('enforce_cpu_limit', default=True, c.add_opt('enforce_cpu_limit', default=True,
help=_('Limit max simultaneous jobs to number of CPUs')) help=_('Limit max simultaneous jobs to number of CPUs'))
c.add_opt('tag_browser_hidden_categories', default=set(),
help=_('tag browser categories not to display'))
return ConfigProxy(c) return ConfigProxy(c)
config = _config() config = _config()

View File

@ -14,6 +14,7 @@ from calibre.gui2.convert.regex_builder_ui import Ui_RegexBuilder
from calibre.gui2.convert.xexp_edit_ui import Ui_Form as Ui_Edit from calibre.gui2.convert.xexp_edit_ui import Ui_Form as Ui_Edit
from calibre.gui2 import error_dialog, choose_files from calibre.gui2 import error_dialog, choose_files
from calibre.ebooks.oeb.iterator import EbookIterator from calibre.ebooks.oeb.iterator import EbookIterator
from calibre.ebooks.conversion.preprocess import convert_entities
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
class RegexBuilder(QDialog, Ui_RegexBuilder): class RegexBuilder(QDialog, Ui_RegexBuilder):
@ -87,8 +88,10 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
self.iterator = EbookIterator(pathtoebook) self.iterator = EbookIterator(pathtoebook)
self.iterator.__enter__(only_input_plugin=True) self.iterator.__enter__(only_input_plugin=True)
text = [u''] text = [u'']
ent_pat = re.compile(r'&(\S+?);')
for path in self.iterator.spine: for path in self.iterator.spine:
html = open(path, 'rb').read().decode('utf-8', 'replace') html = open(path, 'rb').read().decode('utf-8', 'replace')
html = ent_pat.sub(convert_entities, html)
text.append(html) text.append(html)
self.preview.setPlainText('\n---\n'.join(text)) self.preview.setPlainText('\n---\n'.join(text))

View File

@ -1123,12 +1123,12 @@ class DeviceGUI(object):
if cache: if cache:
if id in cache['db_ids']: if id in cache['db_ids']:
loc[i] = True loc[i] = True
break continue
if mi.authors and \ if mi.authors and \
re.sub('(?u)\W|[_]', '', authors_to_string(mi.authors).lower()) \ re.sub('(?u)\W|[_]', '', authors_to_string(mi.authors).lower()) \
in cache['authors']: in cache['authors']:
loc[i] = True loc[i] = True
break continue
return loc return loc
def set_books_in_library(self, booklists, reset=False): def set_books_in_library(self, booklists, reset=False):

View File

@ -14,7 +14,7 @@ from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
from calibre.constants import iswindows, isosx from calibre.constants import iswindows, isosx
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
from calibre.gui2 import choose_dir, error_dialog, config, \ from calibre.gui2 import choose_dir, error_dialog, config, gprefs, \
ALL_COLUMNS, NONE, info_dialog, choose_files, \ ALL_COLUMNS, NONE, info_dialog, choose_files, \
warning_dialog, ResizableDialog, question_dialog warning_dialog, ResizableDialog, question_dialog
from calibre.utils.config import prefs from calibre.utils.config import prefs
@ -480,6 +480,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit']) self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit'])
self.device_detection_button.clicked.connect(self.debug_device_detection) self.device_detection_button.clicked.connect(self.debug_device_detection)
self.port.editingFinished.connect(self.check_port_value) self.port.editingFinished.connect(self.check_port_value)
self.show_splash_screen.setChecked(gprefs.get('show_splash_screen',
True))
def check_port_value(self, *args): def check_port_value(self, *args):
port = self.port.value() port = self.port.value()
@ -852,6 +854,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['get_social_metadata'] = self.opt_get_social_metadata.isChecked() config['get_social_metadata'] = self.opt_get_social_metadata.isChecked()
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked() config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked()
config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked()) config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())
gprefs['show_splash_screen'] = bool(self.show_splash_screen.isChecked())
fmts = [] fmts = []
for i in range(self.viewer.count()): for i in range(self.viewer.count()):
if self.viewer.item(i).checkState() == Qt.Checked: if self.viewer.item(i).checkState() == Qt.Checked:

View File

@ -331,8 +331,8 @@
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="page"> <widget class="QWidget" name="page">
<layout class="QVBoxLayout" name="verticalLayout_4"> <layout class="QGridLayout" name="gridLayout_8">
<item> <item row="0" column="0">
<widget class="QCheckBox" name="roman_numerals"> <widget class="QCheckBox" name="roman_numerals">
<property name="text"> <property name="text">
<string>Use &amp;Roman numerals for series number</string> <string>Use &amp;Roman numerals for series number</string>
@ -342,28 +342,35 @@
</property> </property>
</widget> </widget>
</item> </item>
<item> <item row="1" column="0">
<widget class="QCheckBox" name="systray_icon"> <widget class="QCheckBox" name="systray_icon">
<property name="text"> <property name="text">
<string>Enable system &amp;tray icon (needs restart)</string> <string>Enable system &amp;tray icon (needs restart)</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item row="1" column="1">
<widget class="QCheckBox" name="systray_notifications"> <widget class="QCheckBox" name="systray_notifications">
<property name="text"> <property name="text">
<string>Show &amp;notifications in system tray</string> <string>Show &amp;notifications in system tray</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="show_splash_screen">
<property name="text">
<string>Show &amp;splash screen at startup</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="separate_cover_flow"> <widget class="QCheckBox" name="separate_cover_flow">
<property name="text"> <property name="text">
<string>Show cover &amp;browser in a separate window (needs restart)</string> <string>Show cover &amp;browser in a separate window (needs restart)</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item row="4" column="0">
<widget class="QCheckBox" name="search_as_you_type"> <widget class="QCheckBox" name="search_as_you_type">
<property name="text"> <property name="text">
<string>Search as you type</string> <string>Search as you type</string>
@ -373,21 +380,21 @@
</property> </property>
</widget> </widget>
</item> </item>
<item> <item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="sync_news"> <widget class="QCheckBox" name="sync_news">
<property name="text"> <property name="text">
<string>Automatically send downloaded &amp;news to ebook reader</string> <string>Automatically send downloaded &amp;news to ebook reader</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="delete_news"> <widget class="QCheckBox" name="delete_news">
<property name="text"> <property name="text">
<string>&amp;Delete news from library when it is automatically sent to reader</string> <string>&amp;Delete news from library when it is automatically sent to reader</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item row="7" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QLabel" name="label_6"> <widget class="QLabel" name="label_6">
@ -404,7 +411,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item> <item row="8" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2"> <widget class="QGroupBox" name="groupBox_2">
<property name="title"> <property name="title">
<string>Toolbar</string> <string>Toolbar</string>
@ -452,7 +459,7 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item> <item row="9" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_7"> <layout class="QHBoxLayout" name="horizontalLayout_7">
<item> <item>
<widget class="QGroupBox" name="groupBox"> <widget class="QGroupBox" name="groupBox">
@ -527,12 +534,12 @@
</item> </item>
<item> <item>
<widget class="QToolButton" name="add_custcol_button"> <widget class="QToolButton" name="add_custcol_button">
<property name="text">
<string>...</string>
</property>
<property name="toolTip"> <property name="toolTip">
<string>Add a user-defined column</string> <string>Add a user-defined column</string>
</property> </property>
<property name="text">
<string>...</string>
</property>
<property name="icon"> <property name="icon">
<iconset resource="../../../../../resources/images.qrc"> <iconset resource="../../../../../resources/images.qrc">
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset> <normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>

View File

@ -0,0 +1,86 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor
from calibre.utils.config import prefs
from calibre.utils.search_query_parser import saved_searches
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.constants import islinux
class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
def __init__(self, window, initial_search=None):
QDialog.__init__(self, window)
Ui_SavedSearchEditor.__init__(self)
self.setupUi(self)
self.connect(self.add_search_button, SIGNAL('clicked()'), self.add_search)
self.connect(self.search_name_box, SIGNAL('currentIndexChanged(int)'),
self.current_index_changed)
self.connect(self.delete_search_button, SIGNAL('clicked()'), self.del_search)
self.current_search_name = None
self.searches = {}
self.searches_to_delete = []
for name in saved_searches.names():
self.searches[name] = saved_searches.lookup(name)
self.populate_search_list()
if initial_search is not None and initial_search in self.searches:
self.select_search(initial_search)
def populate_search_list(self):
self.search_name_box.clear()
for name in sorted(self.searches.keys()):
self.search_name_box.addItem(name)
def add_search(self):
search_name = unicode(self.input_box.text()).strip()
if search_name == '':
return False
if search_name not in self.searches:
self.searches[search_name] = ''
self.populate_search_list()
self.select_search(search_name)
else:
self.select_search(search_name)
return True
def del_search(self):
if self.current_search_name is not None:
if not confirm('<p>'+_('The current saved search will be '
'<b>permanently deleted</b>. Are you sure?')
+'</p>', 'saved_search_editor_delete', self):
return
del self.searches[self.current_search_name]
self.searches_to_delete.append(self.current_search_name)
self.current_search_name = None
self.search_name_box.removeItem(self.search_name_box.currentIndex())
def select_search(self, name):
self.search_name_box.setCurrentIndex(self.search_name_box.findText(name))
def current_index_changed(self, idx):
if self.current_search_name:
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
name = unicode(self.search_name_box.itemText(idx))
if name:
self.current_search_name = name
self.search_text.setPlainText(self.searches[name])
else:
self.current_search_name = None
self.search_text.setPlainText('')
def accept(self):
if self.current_search_name:
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
for name in self.searches_to_delete:
saved_searches.delete(name)
for name in self.searches:
saved_searches.add(name, self.searches[name])
QDialog.accept(self)

View File

@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SavedSearchEditor</class>
<widget class="QDialog" name="SavedSearchEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>548</width>
<height>148</height>
</rect>
</property>
<property name="windowTitle">
<string>Saved Search Editor</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>:/images/chapters.svg</normaloff>:/images/chapters.svg</iconset>
</property>
<layout class="QGridLayout">
<item row="2" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Saved Search: </string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>search_name_box</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="search_name_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>160</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>145</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Select a saved search to edit</string>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QToolButton" name="delete_search_button">
<property name="toolTip">
<string>Delete this selected saved search</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/minus.svg</normaloff>:/images/minus.svg</iconset>
</property>
</widget>
</item>
<item row="0" column="3">
<spacer>
<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 row="0" column="4">
<widget class="QLineEdit" name="input_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>60</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Enter a new saved search name.</string>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QToolButton" name="add_search_button">
<property name="toolTip">
<string>Add the new saved search</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QPlainTextEdit" name="search_text">
<property name="toolTip">
<string>Change the contents of the saved search</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../../calibre_datesearch/resources/images"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>SavedSearchEditor</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>SavedSearchEditor</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -24,13 +24,12 @@ class Item:
class TagCategories(QDialog, Ui_TagCategories): class TagCategories(QDialog, Ui_TagCategories):
category_labels_orig = ['', 'authors', 'series', 'publishers', 'tags'] category_labels_orig = ['', 'authors', 'series', 'publishers', 'tags']
def __init__(self, window, db, index=None): def __init__(self, window, db, on_category=None):
QDialog.__init__(self, window) QDialog.__init__(self, window)
Ui_TagCategories.__init__(self) Ui_TagCategories.__init__(self)
self.setupUi(self) self.setupUi(self)
self.db = db self.db = db
self.index = index
self.applied_items = [] self.applied_items = []
cc_icon = QIcon(I('column.svg')) cc_icon = QIcon(I('column.svg'))
@ -102,8 +101,10 @@ class TagCategories(QDialog, Ui_TagCategories):
self.connect(self.applied_items_box, SIGNAL('itemActivated(QListWidgetItem*)'), self.unapply_tags) self.connect(self.applied_items_box, SIGNAL('itemActivated(QListWidgetItem*)'), self.unapply_tags)
self.populate_category_list() self.populate_category_list()
return if on_category is not None:
self.select_category(0) l = self.category_box.findText(on_category)
if l >= 0:
self.category_box.setCurrentIndex(l)
def make_list_widget(self, item): def make_list_widget(self, item):
n = item.name if item.exists else item.name + _(' (not on any book)') n = item.name if item.exists else item.name + _(' (not on any book)')

View File

@ -11,7 +11,7 @@
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Tag Editor</string> <string>User Categories Editor</string>
</property> </property>
<property name="windowIcon"> <property name="windowIcon">
<iconset> <iconset>

View File

@ -0,0 +1,89 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QDialog, QListWidgetItem
from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor
from calibre.gui2 import question_dialog, error_dialog
class TagListEditor(QDialog, Ui_TagListEditor):
def tag_cmp(self, x, y):
return cmp(x.lower(), y.lower())
def __init__(self, window, db, tag_to_match):
QDialog.__init__(self, window)
Ui_TagListEditor.__init__(self)
self.setupUi(self)
self.to_rename = {}
self.to_delete = []
self.db = db
self.all_tags = {}
for k,v in db.get_tags_with_ids():
self.all_tags[v] = k
for tag in sorted(self.all_tags.keys(), cmp=self.tag_cmp):
item = QListWidgetItem(tag)
item.setData(Qt.UserRole, self.all_tags[tag])
self.available_tags.addItem(item)
items = self.available_tags.findItems(tag_to_match, Qt.MatchExactly)
if len(items) == 1:
self.available_tags.setCurrentItem(items[0])
self.connect(self.delete_button, SIGNAL('clicked()'), self.delete_tags)
self.connect(self.rename_button, SIGNAL('clicked()'), self.rename_tag)
self.connect(self.available_tags, SIGNAL('itemDoubleClicked(QListWidgetItem *)'), self._rename_tag)
self.connect(self.available_tags, SIGNAL('itemChanged(QListWidgetItem *)'), self.finish_editing)
def finish_editing(self, item):
if item.text() != self.item_before_editing.text():
if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys():
error_dialog(self, 'Tag already used',
'The tag %s is already used.'%(item.text())).exec_()
item.setText(self.item_before_editing.text())
return
id,ign = self.item_before_editing.data(Qt.UserRole).toInt()
self.to_rename[item.text()] = id
def rename_tag(self):
item = self.available_tags.currentItem()
self._rename_tag(item)
def _rename_tag(self, item):
if item is None:
error_dialog(self, 'No tag selected', 'You must select one tag from the list of Available tags.').exec_()
return
self.item_before_editing = item.clone()
item.setFlags (item.flags() | Qt.ItemIsEditable);
self.available_tags.editItem(item)
def delete_tags(self, item=None):
confirms, deletes = [], []
items = self.available_tags.selectedItems() if item is None else [item]
if not items:
error_dialog(self, 'No tags selected', 'You must select at least one tag from the list of Available tags.').exec_()
return
for item in items:
if self.db.is_tag_used(unicode(item.text())):
confirms.append(item)
else:
deletes.append(item)
if confirms:
ct = ', '.join([unicode(item.text()) for item in confirms])
if question_dialog(self, _('Are your sure?'),
'<p>'+_('The following tags are used by one or more books. '
'Are you certain you want to delete them?')+'<br>'+ct):
deletes += confirms
for item in deletes:
self.to_delete.append(item)
self.available_tags.takeItem(self.available_tags.row(item))
def accept(self):
for text in self.to_rename:
self.db.rename_tag(self.to_rename[text], unicode(text))
for item in self.to_delete:
self.db.delete_tag(unicode(item.text()))
QDialog.accept(self)

View File

@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TagListEditor</class>
<widget class="QDialog" name="TagListEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>397</width>
<height>335</height>
</rect>
</property>
<property name="windowTitle">
<string>Tag Editor</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>:/images/chapters.svg</normaloff>:/images/chapters.svg</iconset>
</property>
<layout class="QGridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout">
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Tags in use</string>
</property>
<property name="buddy">
<cstring>available_tags</cstring>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QToolButton" name="delete_button">
<property name="toolTip">
<string>Delete tag from database. This will unapply the tag from all books and then remove it from the database.</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/trash.svg</normaloff>:/images/trash.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="rename_button">
<property name="toolTip">
<string>Rename the tag everywhere it is used.</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/edit_input.svg</normaloff>:/images/edit_input.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListWidget" name="available_tags">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>TagListEditor</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>TagListEditor</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -5,13 +5,15 @@ import sys, os, time, socket, traceback
from functools import partial from functools import partial
from PyQt4.Qt import QCoreApplication, QIcon, QMessageBox, QObject, QTimer, \ from PyQt4.Qt import QCoreApplication, QIcon, QMessageBox, QObject, QTimer, \
QThread, pyqtSignal, Qt, QProgressDialog, QString QThread, pyqtSignal, Qt, QProgressDialog, QString, QPixmap, \
QSplashScreen, QApplication
from calibre import prints, plugins from calibre import prints, plugins
from calibre.constants import iswindows, __appname__, isosx, filesystem_encoding from calibre.constants import iswindows, __appname__, isosx, DEBUG, \
filesystem_encoding
from calibre.utils.ipc import ADDRESS, RC from calibre.utils.ipc import ADDRESS, RC
from calibre.gui2 import ORG_NAME, APP_UID, initialize_file_icon_provider, \ from calibre.gui2 import ORG_NAME, APP_UID, initialize_file_icon_provider, \
Application, choose_dir, error_dialog, question_dialog Application, choose_dir, error_dialog, question_dialog, gprefs
from calibre.gui2.main_window import option_parser as _option_parser from calibre.gui2.main_window import option_parser as _option_parser
from calibre.utils.config import prefs, dynamic from calibre.utils.config import prefs, dynamic
from calibre.library.database2 import LibraryDatabase2 from calibre.library.database2 import LibraryDatabase2
@ -113,15 +115,25 @@ class GuiRunner(QObject):
initialization''' initialization'''
def __init__(self, opts, args, actions, listener, app): def __init__(self, opts, args, actions, listener, app):
self.startup_time = time.time()
self.opts, self.args, self.listener, self.app = opts, args, listener, app self.opts, self.args, self.listener, self.app = opts, args, listener, app
self.actions = actions self.actions = actions
self.main = None self.main = None
QObject.__init__(self) QObject.__init__(self)
self.splash_screen = None
self.timer = QTimer.singleShot(1, self.initialize) self.timer = QTimer.singleShot(1, self.initialize)
if DEBUG:
prints('Starting up...')
def start_gui(self): def start_gui(self):
from calibre.gui2.ui import Main from calibre.gui2.ui import Main
main = Main(self.library_path, self.db, self.listener, self.opts, self.actions) main = Main(self.opts)
if self.splash_screen is not None:
self.splash_screen.showMessage(_('Initializing user interface...'))
self.splash_screen.finish(main)
main.initialize(self.library_path, self.db, self.listener, self.actions)
if DEBUG:
prints('Started up in', time.time() - self.startup_time)
add_filesystem_book = partial(main.add_filesystem_book, allow_device=False) add_filesystem_book = partial(main.add_filesystem_book, allow_device=False)
sys.excepthook = main.unhandled_exception sys.excepthook = main.unhandled_exception
if len(self.args) > 1: if len(self.args) > 1:
@ -142,7 +154,7 @@ class GuiRunner(QObject):
if db is None and tb is not None: if db is None and tb is not None:
# DB Repair failed # DB Repair failed
error_dialog(None, _('Repairing failed'), error_dialog(self.splash_screen, _('Repairing failed'),
_('The database repair failed. Starting with ' _('The database repair failed. Starting with '
'a new empty library.'), 'a new empty library.'),
det_msg=tb, show=True) det_msg=tb, show=True)
@ -159,7 +171,7 @@ class GuiRunner(QObject):
os.makedirs(x) os.makedirs(x)
except: except:
x = os.path.expanduser('~') x = os.path.expanduser('~')
candidate = choose_dir(None, 'choose calibre library', candidate = choose_dir(self.splash_screen, 'choose calibre library',
_('Choose a location for your new calibre e-book library'), _('Choose a location for your new calibre e-book library'),
default_dir=x) default_dir=x)
@ -170,7 +182,7 @@ class GuiRunner(QObject):
self.library_path = candidate self.library_path = candidate
db = LibraryDatabase2(candidate) db = LibraryDatabase2(candidate)
except: except:
error_dialog(None, _('Bad database location'), error_dialog(self.splash_screen, _('Bad database location'),
_('Bad database location %r. calibre will now quit.' _('Bad database location %r. calibre will now quit.'
)%self.library_path, )%self.library_path,
det_msg=traceback.format_exc(), show=True) det_msg=traceback.format_exc(), show=True)
@ -184,7 +196,7 @@ class GuiRunner(QObject):
try: try:
db = LibraryDatabase2(self.library_path) db = LibraryDatabase2(self.library_path)
except (sqlite.Error, DatabaseException): except (sqlite.Error, DatabaseException):
repair = question_dialog(None, _('Corrupted database'), repair = question_dialog(self.splash_screen, _('Corrupted database'),
_('Your calibre database appears to be corrupted. Do ' _('Your calibre database appears to be corrupted. Do '
'you want calibre to try and repair it automatically? ' 'you want calibre to try and repair it automatically? '
'If you say No, a new empty calibre library will be created.'), 'If you say No, a new empty calibre library will be created.'),
@ -203,14 +215,27 @@ class GuiRunner(QObject):
self.repair.start() self.repair.start()
return return
except: except:
error_dialog(None, _('Bad database location'), error_dialog(self.splash_screen, _('Bad database location'),
_('Bad database location %r. Will start with ' _('Bad database location %r. Will start with '
' a new, empty calibre library')%self.library_path, ' a new, empty calibre library')%self.library_path,
det_msg=traceback.format_exc(), show=True) det_msg=traceback.format_exc(), show=True)
self.initialize_db_stage2(db, None) self.initialize_db_stage2(db, None)
def show_splash_screen(self):
self.splash_pixmap = QPixmap()
self.splash_pixmap.load(I('library.png'))
self.splash_screen = QSplashScreen(self.splash_pixmap,
Qt.SplashScreen|Qt.WindowStaysOnTopHint)
self.splash_screen.showMessage(_('Starting %s: Loading books...') %
__appname__)
self.splash_screen.show()
QApplication.instance().processEvents()
def initialize(self, *args): def initialize(self, *args):
if gprefs.get('show_splash_screen', True):
self.show_splash_screen()
self.library_path = get_library_path() self.library_path = get_library_path()
if self.library_path is None: if self.library_path is None:
self.initialization_failed() self.initialization_failed()

View File

@ -8,10 +8,11 @@ Browsing book collection by tags.
''' '''
from itertools import izip from itertools import izip
from functools import partial
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
QFont, QSize, QIcon, QPoint, \ QFont, QSize, QIcon, QPoint, \
QAbstractItemModel, QVariant, QModelIndex QAbstractItemModel, QVariant, QModelIndex, QMenu
from calibre.gui2 import config, NONE from calibre.gui2 import config, NONE
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.library.field_metadata import TagsIcons from calibre.library.field_metadata import TagsIcons
@ -19,9 +20,12 @@ from calibre.utils.search_query_parser import saved_searches
class TagsView(QTreeView): # {{{ class TagsView(QTreeView): # {{{
need_refresh = pyqtSignal() need_refresh = pyqtSignal()
restriction_set = pyqtSignal(object) restriction_set = pyqtSignal(object)
tags_marked = pyqtSignal(object, object) tags_marked = pyqtSignal(object, object)
user_category_edit = pyqtSignal(object)
tag_list_edit = pyqtSignal(object)
saved_search_edit = pyqtSignal(object)
def __init__(self, *args): def __init__(self, *args):
QTreeView.__init__(self, *args) QTreeView.__init__(self, *args)
@ -31,13 +35,16 @@ class TagsView(QTreeView): # {{{
self.tag_match = None self.tag_match = None
def set_database(self, db, tag_match, popularity, restriction): def set_database(self, db, tag_match, popularity, restriction):
self._model = TagsModel(db, parent=self) self.hidden_categories = config['tag_browser_hidden_categories']
self._model = TagsModel(db, parent=self, hidden_categories=self.hidden_categories)
self.popularity = popularity self.popularity = popularity
self.restriction = restriction self.restriction = restriction
self.tag_match = tag_match self.tag_match = tag_match
self.db = db self.db = db
self.setModel(self._model) self.setModel(self._model)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.clicked.connect(self.toggle) self.clicked.connect(self.toggle)
self.customContextMenuRequested.connect(self.show_context_menu)
self.popularity.setChecked(config['sort_by_popularity']) self.popularity.setChecked(config['sort_by_popularity'])
self.popularity.stateChanged.connect(self.sort_changed) self.popularity.stateChanged.connect(self.sort_changed)
self.restriction.activated[str].connect(self.search_restriction_set) self.restriction.activated[str].connect(self.search_restriction_set)
@ -45,10 +52,6 @@ class TagsView(QTreeView): # {{{
db.add_listener(self.database_changed) db.add_listener(self.database_changed)
self.saved_searches_changed(recount=False) self.saved_searches_changed(recount=False)
def create_tag_category(self, name, tag_list):
self._model.create_tag_category(name, tag_list)
self.recount()
def database_changed(self, event, ids): def database_changed(self, event, ids):
self.need_refresh.emit() self.need_refresh.emit()
@ -72,12 +75,87 @@ class TagsView(QTreeView): # {{{
self.recount() # Must happen after the emission of the restriction_set signal self.recount() # Must happen after the emission of the restriction_set signal
self.tags_marked.emit(self._model.tokens(), self.match_all) self.tags_marked.emit(self._model.tokens(), self.match_all)
def mouseReleaseEvent(self, event):
# Swallow everything except leftButton so context menus work correctly
if event.button() == Qt.LeftButton:
QTreeView.mouseReleaseEvent(self, event)
def toggle(self, index): def toggle(self, index):
modifiers = int(QApplication.keyboardModifiers()) modifiers = int(QApplication.keyboardModifiers())
exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT) exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
if self._model.toggle(index, exclusive): if self._model.toggle(index, exclusive):
self.tags_marked.emit(self._model.tokens(), self.match_all) self.tags_marked.emit(self._model.tokens(), self.match_all)
def context_menu_handler(self, action=None, category=None):
if not action:
return
try:
if action == 'manage_tags':
self.tag_list_edit.emit(category)
return
if action == 'manage_categories':
self.user_category_edit.emit(category)
return
if action == 'manage_searches':
self.saved_search_edit.emit(category)
return
if action == 'hide':
self.hidden_categories.add(category)
elif action == 'show':
self.hidden_categories.discard(category)
elif action == 'defaults':
self.hidden_categories.clear()
config.set('tag_browser_hidden_categories', self.hidden_categories)
self.set_new_model()
except:
return
def show_context_menu(self, point):
index = self.indexAt(point)
if not index.isValid():
return False
item = index.internalPointer()
tag_name = ''
if item.type == TagTreeItem.TAG:
tag_name = item.tag.name
item = item.parent
if item.type == TagTreeItem.CATEGORY:
category = unicode(item.name.toString())
self.context_menu = QMenu(self)
self.context_menu.addAction(_('Hide %s') % category,
partial(self.context_menu_handler, action='hide', category=category))
if self.hidden_categories:
self.context_menu.addSeparator()
m = self.context_menu.addMenu(_('Show category'))
for col in self.hidden_categories:
m.addAction(col,
partial(self.context_menu_handler, action='show', category=col))
self.context_menu.addSeparator()
self.context_menu.addAction(_('Restore defaults'),
partial(self.context_menu_handler, action='defaults'))
self.context_menu.addSeparator()
self.context_menu.addAction(_('Manage Tags'),
partial(self.context_menu_handler, action='manage_tags',
category=tag_name))
if category in prefs['user_categories'].keys():
self.context_menu.addAction(_('Manage User Categories'),
partial(self.context_menu_handler, action='manage_categories',
category=category))
else:
self.context_menu.addAction(_('Manage User Categories'),
partial(self.context_menu_handler, action='manage_categories',
category=None))
self.context_menu.addAction(_('Manage Saved Searches'),
partial(self.context_menu_handler, action='manage_searches',
category=tag_name))
self.context_menu.popup(self.mapToGlobal(point))
return True
def clear(self): def clear(self):
self.model().clear_state() self.model().clear_state()
@ -110,13 +188,12 @@ class TagsView(QTreeView): # {{{
self.setCurrentIndex(idx) self.setCurrentIndex(idx)
self.scrollTo(idx, QTreeView.PositionAtCenter) self.scrollTo(idx, QTreeView.PositionAtCenter)
''' # If the number of user categories changed, if custom columns have come or
If the number of user categories changed, or if custom columns have come or gone, # gone, or if columns have been hidden or restored, we must rebuild the
we must rebuild the model. Reason: it is much easier to do that than to reconstruct # model. Reason: it is much easier than reconstructing the browser tree.
the browser tree.
'''
def set_new_model(self): def set_new_model(self):
self._model = TagsModel(self.db, parent=self) self._model = TagsModel(self.db, parent=self,
hidden_categories=self.hidden_categories)
self.setModel(self._model) self.setModel(self._model)
# }}} # }}}
@ -200,7 +277,7 @@ class TagTreeItem(object): # {{{
class TagsModel(QAbstractItemModel): # {{{ class TagsModel(QAbstractItemModel): # {{{
def __init__(self, db, parent=None): def __init__(self, db, parent=None, hidden_categories=None):
QAbstractItemModel.__init__(self, parent) QAbstractItemModel.__init__(self, parent)
# must do this here because 'QPixmap: Must construct a QApplication # must do this here because 'QPixmap: Must construct a QApplication
@ -220,6 +297,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))] self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
self.db = db self.db = db
self.hidden_categories = hidden_categories
self.search_restriction = '' self.search_restriction = ''
self.ignore_next_search = 0 self.ignore_next_search = 0
@ -237,6 +315,8 @@ class TagsModel(QAbstractItemModel): # {{{
data = self.get_node_tree(config['sort_by_popularity']) data = self.get_node_tree(config['sort_by_popularity'])
self.root_item = TagTreeItem() self.root_item = TagTreeItem()
for i, r in enumerate(self.row_map): for i, r in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue
if self.db.field_metadata[r]['kind'] != 'user': if self.db.field_metadata[r]['kind'] != 'user':
tt = _('The lookup/search name is "{0}"').format(r) tt = _('The lookup/search name is "{0}"').format(r)
else: else:
@ -271,12 +351,16 @@ class TagsModel(QAbstractItemModel): # {{{
def refresh(self): def refresh(self):
data = self.get_node_tree(config['sort_by_popularity']) # get category data data = self.get_node_tree(config['sort_by_popularity']) # get category data
row_index = -1
for i, r in enumerate(self.row_map): for i, r in enumerate(self.row_map):
category = self.root_item.children[i] if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue
row_index += 1
category = self.root_item.children[row_index]
names = [t.tag.name for t in category.children] names = [t.tag.name for t in category.children]
states = [t.tag.state for t in category.children] states = [t.tag.state for t in category.children]
state_map = dict(izip(names, states)) state_map = dict(izip(names, states))
category_index = self.index(i, 0, QModelIndex()) category_index = self.index(row_index, 0, QModelIndex())
if len(category.children) > 0: if len(category.children) > 0:
self.beginRemoveRows(category_index, 0, self.beginRemoveRows(category_index, 0,
len(category.children)-1) len(category.children)-1)
@ -401,16 +485,20 @@ class TagsModel(QAbstractItemModel): # {{{
def tokens(self): def tokens(self):
ans = [] ans = []
tags_seen = set() tags_seen = set()
row_index = -1
for i, key in enumerate(self.row_map): for i, key in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue
row_index += 1
if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category
continue continue
category_item = self.root_item.children[i] category_item = self.root_item.children[row_index]
for tag_item in category_item.children: for tag_item in category_item.children:
tag = tag_item.tag tag = tag_item.tag
if tag.state > 0: if tag.state > 0:
prefix = ' not ' if tag.state == 2 else '' prefix = ' not ' if tag.state == 2 else ''
category = key if key != 'news' else 'tag' category = key if key != 'news' else 'tag'
if tag.name[0] == u'\u2605': # char is a star. Assume rating if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
ans.append('%s%s:%s'%(prefix, category, len(tag.name))) ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
else: else:
if category == 'tags': if category == 'tags':

View File

@ -61,6 +61,8 @@ from calibre.library.database2 import LibraryDatabase2
from calibre.library.caches import CoverCache from calibre.library.caches import CoverCache
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.tag_categories import TagCategories from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
class SaveMenu(QMenu): class SaveMenu(QMenu):
@ -127,13 +129,18 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
pixmap_to_data(pixmap)) pixmap_to_data(pixmap))
self.last_time = datetime.datetime.now() self.last_time = datetime.datetime.now()
def __init__(self, library_path, db, listener, opts, actions, parent=None):
def __init__(self, opts, parent=None):
MainWindow.__init__(self, opts, parent)
self.opts = opts
def initialize(self, library_path, db, listener, actions):
opts = self.opts
self.last_time = datetime.datetime.now() self.last_time = datetime.datetime.now()
self.preferences_action, self.quit_action = actions self.preferences_action, self.quit_action = actions
self.library_path = library_path self.library_path = library_path
self.spare_servers = [] self.spare_servers = []
self.must_restart_before_config = False self.must_restart_before_config = False
MainWindow.__init__(self, opts, parent)
# Initialize fontconfig in a separate thread as this can be a lengthy # Initialize fontconfig in a separate thread as this can be a lengthy
# process if run for the first time on this machine # process if run for the first time on this machine
from calibre.utils.fonts import fontconfig from calibre.utils.fonts import fontconfig
@ -537,19 +544,23 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.cover_cache = CoverCache(self.library_path) self.cover_cache = CoverCache(self.library_path)
self.cover_cache.start() self.cover_cache.start()
self.library_view.model().cover_cache = self.cover_cache self.library_view.model().cover_cache = self.cover_cache
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_edit_categories) self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit)
self.tags_view.set_database(db, self.tag_match, self.popularity, self.search_restriction) self.tags_view.set_database(db, self.tag_match, self.popularity, self.search_restriction)
self.tags_view.tags_marked.connect(self.search.search_from_tags) self.tags_view.tags_marked.connect(self.search.search_from_tags)
for x in (self.saved_search.clear_to_help, self.mark_restriction_set): for x in (self.saved_search.clear_to_help, self.mark_restriction_set):
self.tags_view.restriction_set.connect(x) self.tags_view.restriction_set.connect(x)
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help) self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
self.search.search.connect(self.tags_view.model().reinit) self.search.search.connect(self.tags_view.model().reinit)
for x in (self.location_view.count_changed, self.tags_view.recount, for x in (self.location_view.count_changed, self.tags_view.recount,
self.restriction_count_changed): self.restriction_count_changed):
self.library_view.model().count_changed_signal.connect(x) self.library_view.model().count_changed_signal.connect(x)
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared) self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
self.connect(self.saved_search, SIGNAL('changed()'), self.tags_view.saved_searches_changed, Qt.QueuedConnection) self.connect(self.saved_search, SIGNAL('changed()'),
self.tags_view.saved_searches_changed, Qt.QueuedConnection)
if not gprefs.get('quick_start_guide_added', False): if not gprefs.get('quick_start_guide_added', False):
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember']) mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
@ -642,13 +653,28 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self._add_filesystem_book = Dispatcher(self.__add_filesystem_book) self._add_filesystem_book = Dispatcher(self.__add_filesystem_book)
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
def do_edit_categories(self): def do_user_categories_edit(self, on_category=None):
d = TagCategories(self, self.library_view.model().db) d = TagCategories(self, self.library_view.model().db, on_category)
d.exec_() d.exec_()
if d.result() == d.Accepted: if d.result() == d.Accepted:
self.tags_view.set_new_model() self.tags_view.set_new_model()
self.tags_view.recount() self.tags_view.recount()
def do_tags_list_edit(self, tag):
d = TagListEditor(self, self.library_view.model().db, tag)
d.exec_()
if d.result() == d.Accepted:
self.tags_view.set_new_model()
self.tags_view.recount()
self.library_view.model().refresh()
def do_saved_search_edit(self, search):
d = SavedSearchEditor(self, search)
d.exec_()
if d.result() == d.Accepted:
self.tags_view.saved_searches_changed(recount=True)
self.saved_search.clear_to_help()
def resizeEvent(self, ev): def resizeEvent(self, ev):
MainWindow.resizeEvent(self, ev) MainWindow.resizeEvent(self, ev)
self.search.setMaximumWidth(self.width()-150) self.search.setMaximumWidth(self.width()-150)

View File

@ -17,7 +17,7 @@ from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, now, UNDEFINED_DATE from calibre.utils.date import parse_date, now, UNDEFINED_DATE
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
from calibre.utils.pyparsing import ParseException from calibre.utils.pyparsing import ParseException
# from calibre.library.field_metadata import FieldMetadata from calibre.ebooks.metadata import title_sort
class CoverCache(QThread): class CoverCache(QThread):
@ -564,7 +564,8 @@ class ResultCache(SearchQueryParser):
def seriescmp(self, x, y): def seriescmp(self, x, y):
sidx = self.FIELD_MAP['series'] sidx = self.FIELD_MAP['series']
try: try:
ans = cmp(self._data[x][sidx].lower(), self._data[y][sidx].lower()) ans = cmp(title_sort(self._data[x][sidx].lower()),
title_sort(self._data[y][sidx].lower()))
except AttributeError: # Some entries may be None except AttributeError: # Some entries may be None
ans = cmp(self._data[x][sidx], self._data[y][sidx]) ans = cmp(self._data[x][sidx], self._data[y][sidx])
if ans != 0: return ans if ans != 0: return ans

View File

@ -648,6 +648,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute(st%dict(ltable='publishers', table='publishers', ltable_col='publisher')) self.conn.execute(st%dict(ltable='publishers', table='publishers', ltable_col='publisher'))
self.conn.execute(st%dict(ltable='tags', table='tags', ltable_col='tag')) self.conn.execute(st%dict(ltable='tags', table='tags', ltable_col='tag'))
self.conn.execute(st%dict(ltable='series', table='series', ltable_col='series')) self.conn.execute(st%dict(ltable='series', table='series', ltable_col='series'))
for id_, tag in self.conn.get('SELECT id, name FROM tags', all=True):
if not tag.strip():
self.conn.execute('DELETE FROM books_tags_link WHERE tag=?',
(id_,))
self.conn.execute('DELETE FROM tags WHERE id=?', (id_,))
self.clean_custom() self.clean_custom()
self.conn.commit() self.conn.commit()
@ -725,6 +730,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0], categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
icon=icon, tooltip = tooltip) icon=icon, tooltip = tooltip)
for r in data if item_not_zero_func(r)] for r in data if item_not_zero_func(r)]
if category == 'series':
categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name),
title_sort(y.name)))
# We delayed computing the standard formats category because it does not # We delayed computing the standard formats category because it does not
# use a view, but is computed dynamically # use a view, but is computed dynamically
@ -977,6 +985,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if notify: if notify:
self.notify('metadata', [id]) self.notify('metadata', [id])
# Convenience method for tags_list_editor
def get_tags_with_ids(self):
result = self.conn.get('SELECT * FROM tags')
if not result:
return {}
r = []
for k,v in result:
r.append((k,v))
return r
def rename_tag(self, id, new):
self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new, id))
self.conn.commit()
def get_tags(self, id): def get_tags(self, id):
result = self.conn.get( result = self.conn.get(
'SELECT name FROM tags WHERE id IN (SELECT tag FROM books_tags_link WHERE book=?)', 'SELECT name FROM tags WHERE id IN (SELECT tag FROM books_tags_link WHERE book=?)',

View File

@ -16,7 +16,7 @@ except ImportError:
from calibre import fit_image, guess_type from calibre import fit_image, guess_type
from calibre.utils.date import fromtimestamp from calibre.utils.date import fromtimestamp
from calibre.ebooks.metadata import title_sort
class ContentServer(object): class ContentServer(object):
@ -67,7 +67,7 @@ class ContentServer(object):
def seriescmp(self, x, y): def seriescmp(self, x, y):
si = self.db.FIELD_MAP['series'] si = self.db.FIELD_MAP['series']
try: try:
ans = cmp(x[si].lower(), y[si].lower()) ans = cmp(title_sort(x[si].lower()), title_sort(y[si].lower()))
except AttributeError: # Some entries may be None except AttributeError: # Some entries may be None
ans = cmp(x[si], y[si]) ans = cmp(x[si], y[si])
if ans != 0: return ans if ans != 0: return ans

View File

@ -453,7 +453,7 @@ as HTML and then convert the resulting HTML file with |app|. When saving as HTML
There is a Word macro package that can automate the conversion of Word documents using |app|. It also makes There is a Word macro package that can automate the conversion of Word documents using |app|. It also makes
generating the Table of Contents much simpler. It is called BookCreator and is available for free generating the Table of Contents much simpler. It is called BookCreator and is available for free
`here <http://www.mobileread.com/forums/showthread.php?t=28313>`_. at `mobileread <http://www.mobileread.com/forums/showthread.php?t=28313>`_.
Convert TXT documents Convert TXT documents
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
@ -493,7 +493,7 @@ TXT input supports a number of options to differentiate how paragraphs are detec
allows for basic formatting to be added to TXT documents, such as bold, italics, section headings, tables, allows for basic formatting to be added to TXT documents, such as bold, italics, section headings, tables,
lists, a Table of Contents, etc. Marking chapter headings with a leading # and setting the chapter XPath detection lists, a Table of Contents, etc. Marking chapter headings with a leading # and setting the chapter XPath detection
expression to "//h:h1" is the easiest way to have a proper table of contents generated from a TXT document. expression to "//h:h1" is the easiest way to have a proper table of contents generated from a TXT document.
You can learn more about the markdown syntax `here <http://daringfireball.net/projects/markdown/syntax>`_. You can learn more about the markdown syntax at `daringfireball <http://daringfireball.net/projects/markdown/syntax>`_.
Convert PDF documents Convert PDF documents
@ -540,7 +540,7 @@ EPUB advanced formatting demo
Various advanced formatting for EPUB files is demonstrated in this `demo file <http://calibre-ebook.com/downloads/demos/demo.epub>`_. Various advanced formatting for EPUB files is demonstrated in this `demo file <http://calibre-ebook.com/downloads/demos/demo.epub>`_.
The file was created from hand coded HTML using calibre and is meant to be used as a template for your own EPUB creation efforts. The file was created from hand coded HTML using calibre and is meant to be used as a template for your own EPUB creation efforts.
The source HTML it was created from is available `here <http://calibre-ebook.com/downloads/demos/demo.zip>`_. The settings used to create the The source HTML it was created from is available `demo.zip <http://calibre-ebook.com/downloads/demos/demo.zip>`_. The settings used to create the
EPUB from the ZIP file are:: EPUB from the ZIP file are::
ebook-convert demo.zip .epub -vv --authors "Kovid Goyal" --language en --level1-toc '//*[@class="title"]' --disable-font-rescaling --page-breaks-before / --no-default-epub-cover ebook-convert demo.zip .epub -vv --authors "Kovid Goyal" --language en --level1-toc '//*[@class="title"]' --disable-font-rescaling --page-breaks-before / --no-default-epub-cover

View File

@ -133,7 +133,7 @@ Can I use the collections feature of the SONY reader?
turned into a collection on the reader. Note that the PRS-500 does not support collections for books stored on the SD card. The PRS-505 does. turned into a collection on the reader. Note that the PRS-500 does not support collections for books stored on the SD card. The PRS-505 does.
How do I use |app| with my iPad/iPhone/iTouch? How do I use |app| with my iPad/iPhone/iTouch?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can access your calibre library on a iPad/iPhone/iTouch over the air using the calibre content server. You can access your calibre library on a iPad/iPhone/iTouch over the air using the calibre content server.

View File

@ -115,7 +115,7 @@ Pre/post processing of downloaded HTML
.. automethod:: BasicNewsRecipe.postprocess_html .. automethod:: BasicNewsRecipe.postprocess_html
.. automethod:: BasicNewsRecipe.populate_article_metadata
Convenience methods Convenience methods

View File

@ -51,6 +51,8 @@ class FontMetrics(object):
def get_font_metrics(image, d_wand, text): def get_font_metrics(image, d_wand, text):
if isinstance(text, unicode):
text = text.encode('utf-8')
ret = p.MagickQueryFontMetrics(image, d_wand, text) ret = p.MagickQueryFontMetrics(image, d_wand, text)
return FontMetrics(ret) return FontMetrics(ret)

View File

@ -146,7 +146,7 @@ class BasicNewsRecipe(Recipe):
#: If True empty feeds are removed from the output. #: If True empty feeds are removed from the output.
#: This option has no effect if parse_index is overriden in #: This option has no effect if parse_index is overriden in
#: the sub class. It is meant only for recipes that return a list #: the sub class. It is meant only for recipes that return a list
#: of feeds using :member:`feeds` or :method:`get_feeds`. #: of feeds using `feeds` or :method:`get_feeds`.
remove_empty_feeds = False remove_empty_feeds = False
#: List of regular expressions that determines which links to follow #: List of regular expressions that determines which links to follow
@ -256,7 +256,7 @@ class BasicNewsRecipe(Recipe):
#: The CSS that is used to style the templates, i.e., the navigation bars and #: The CSS that is used to style the templates, i.e., the navigation bars and
#: the Tables of Contents. Rather than overriding this variable, you should #: the Tables of Contents. Rather than overriding this variable, you should
#: use :member:`extra_css` in your recipe to customize look and feel. #: use `extra_css` in your recipe to customize look and feel.
template_css = u''' template_css = u'''
.article_date { .article_date {
color: gray; font-family: monospace; color: gray; font-family: monospace;
@ -506,7 +506,7 @@ class BasicNewsRecipe(Recipe):
def get_obfuscated_article(self, url): def get_obfuscated_article(self, url):
''' '''
If you set :member:`articles_are_obfuscated` this method is called with If you set `articles_are_obfuscated` this method is called with
every article URL. It should return the path to a file on the filesystem every article URL. It should return the path to a file on the filesystem
that contains the article HTML. That file is processed by the recursive that contains the article HTML. That file is processed by the recursive
HTML fetching engine, so it can contain links to pages/images on the web. HTML fetching engine, so it can contain links to pages/images on the web.
@ -517,20 +517,18 @@ class BasicNewsRecipe(Recipe):
''' '''
raise NotImplementedError raise NotImplementedError
def extract_author(self, soup): def populate_article_metadata(self, article, soup, first):
''' '''
Parse downloaded articles for author, add to OEBBook object. Called when each HTML page belonging to article is downloaded.
:param soup: Intended to be used to get article metadata like author/summary/etc.
from the parsed HTML (soup).
:param article: A object of class :class:`calibre.web.feeds.Article`.
If you change the sumamry, remember to also change the
text_summary
:param soup: Parsed HTML belonging to this article
:param first: True iff the parsed HTML is the first page of the article.
''' '''
return None pass
def extract_description(self, soup):
'''
Parse downloaded articles for description, add to OEBBook object.
:param soup:
'''
return None
def postprocess_book(self, oeb, opts, log): def postprocess_book(self, oeb, opts, log):
''' '''
@ -559,8 +557,8 @@ class BasicNewsRecipe(Recipe):
self.username = options.username self.username = options.username
self.password = options.password self.password = options.password
self.lrf = options.lrf self.lrf = options.lrf
self.output_profile = options.output_profile.name self.output_profile = options.output_profile
self.touchscreen = getattr(options.output_profile,'touchscreen',False) self.touchscreen = getattr(self.output_profile, 'touchscreen', False)
self.output_dir = os.path.abspath(self.output_dir) self.output_dir = os.path.abspath(self.output_dir)
if options.test: if options.test:
@ -655,7 +653,15 @@ class BasicNewsRecipe(Recipe):
for base in list(soup.findAll(['base', 'iframe'])): for base in list(soup.findAll(['base', 'iframe'])):
base.extract() base.extract()
return self.postprocess_html(soup, first_fetch) ans = self.postprocess_html(soup, first_fetch)
try:
article = self.feed_objects[f].articles[a]
except:
self.log.exception('Failed to get article object for postprocessing')
pass
else:
self.populate_article_metadata(article, ans, first_fetch)
return ans
def download(self): def download(self):
@ -879,6 +885,7 @@ class BasicNewsRecipe(Recipe):
if hasattr(feed, 'reverse'): if hasattr(feed, 'reverse'):
feed.reverse() feed.reverse()
self.feed_objects = feeds
for f, feed in enumerate(feeds): for f, feed in enumerate(feeds):
feed_dir = os.path.join(self.output_dir, 'feed_%d'%f) feed_dir = os.path.join(self.output_dir, 'feed_%d'%f)
if not os.path.isdir(feed_dir): if not os.path.isdir(feed_dir):
@ -927,41 +934,9 @@ class BasicNewsRecipe(Recipe):
#feeds.restore_duplicates() #feeds.restore_duplicates()
# GwR Populate any missing author/description fields in feed
for f, feed in enumerate(feeds): for f, feed in enumerate(feeds):
feed_dir = os.path.join(self.output_dir, 'feed_%d'%f)
for article in feed.articles:
if article.summary == '' or article.author == '':
file = os.path.join(self.output_dir,feed_dir, article.url)
if os.path.exists(file):
with open(file, 'rb') as fi:
src = fi.read().decode('utf-8')
soup = BeautifulSoup(src)
if article.author == '':
author = self.extract_author(soup)
if author and not isinstance(author, unicode):
author = author.decode('utf-8', 'replace')
article.author = author
if article.summary == '':
summary = article.summary = self.extract_description(soup)
if summary and not isinstance(summary, unicode):
summary = summary.decode('utf-8', 'replace')
if summary and '<' in summary:
try:
s = html.fragment_fromstring(summary, create_parent=True)
summary = html.tostring(s, method='text', encoding=unicode)
except:
print 'Failed to process article summary, deleting:'
print summary.encode('utf-8')
traceback.print_exc()
summary = u''
article.text_summary = summary
for f, feed in enumerate(feeds):
feed_dir = os.path.join(self.output_dir, 'feed_%d'%f)
html = self.feed2index(feed) html = self.feed2index(feed)
feed_dir = os.path.join(self.output_dir, 'feed_%d'%f)
with open(os.path.join(feed_dir, 'index.html'), 'wb') as fi: with open(os.path.join(feed_dir, 'index.html'), 'wb') as fi:
fi.write(html) fi.write(html)
self.create_opf(feeds) self.create_opf(feeds)
@ -1040,47 +1015,13 @@ class BasicNewsRecipe(Recipe):
Create a generic cover for recipes that dont have a cover Create a generic cover for recipes that dont have a cover
''' '''
try: try:
try: from calibre.utils.magick_draw import create_cover_page, TextLine
from PIL import Image, ImageDraw, ImageFont
Image, ImageDraw, ImageFont
except ImportError:
import Image, ImageDraw, ImageFont
font_path = P('fonts/liberation/LiberationSerif-Bold.ttf')
title = self.title if isinstance(self.title, unicode) else \ title = self.title if isinstance(self.title, unicode) else \
self.title.decode(preferred_encoding, 'replace') self.title.decode(preferred_encoding, 'replace')
date = strftime(self.timefmt) date = strftime(self.timefmt)
app = '['+__appname__ +' '+__version__+']' lines = [TextLine(title, 44), TextLine(date, 32)]
img_data = create_cover_page(lines, I('library.png'), output_format='jpg')
COVER_WIDTH, COVER_HEIGHT = 590, 750 cover_file.write(img_data)
img = Image.new('RGB', (COVER_WIDTH, COVER_HEIGHT), 'white')
draw = ImageDraw.Draw(img)
# Title
font = ImageFont.truetype(font_path, 44)
width, height = draw.textsize(title, font=font)
left = max(int((COVER_WIDTH - width)/2.), 0)
top = 15
draw.text((left, top), title, fill=(0,0,0), font=font)
bottom = top + height
# Date
font = ImageFont.truetype(font_path, 32)
width, height = draw.textsize(date, font=font)
left = max(int((COVER_WIDTH - width)/2.), 0)
draw.text((left, bottom+15), date, fill=(0,0,0), font=font)
# Vanity
font = ImageFont.truetype(font_path, 28)
width, height = draw.textsize(app, font=font)
left = max(int((COVER_WIDTH - width)/2.), 0)
top = COVER_HEIGHT - height - 15
draw.text((left, top), app, fill=(0,0,0), font=font)
# Logo
logo = Image.open(I('library.png'), 'r')
width, height = logo.size
left = max(int((COVER_WIDTH - width)/2.), 0)
top = max(int((COVER_HEIGHT - height)/2.), 0)
img.paste(logo, (left, top))
img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE)
img.convert('RGB').save(cover_file, 'JPEG')
cover_file.flush() cover_file.flush()
except: except:
self.log.exception('Failed to generate default cover') self.log.exception('Failed to generate default cover')
@ -1173,21 +1114,20 @@ class BasicNewsRecipe(Recipe):
pw.DestroyMagickWand(x) pw.DestroyMagickWand(x)
def create_opf(self, feeds, dir=None): def create_opf(self, feeds, dir=None):
if dir is None: if dir is None:
dir = self.output_dir dir = self.output_dir
mi = MetaInformation(self.short_title() + strftime(self.timefmt), [__appname__]) mi = MetaInformation(self.short_title() + strftime(self.timefmt), [__appname__])
mi.author_sort = __appname__
if self.output_profile == 'iPad':
mi = MetaInformation(self.short_title(), [strftime('%A, %d %B %Y')])
mi.author_sort = strftime('%Y-%m-%d')
mi.publisher = __appname__ mi.publisher = __appname__
mi.author_sort = __appname__
if self.output_profile.name == 'iPad':
date_as_author = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
mi.authors = [date_as_author]
mi.author_sort = strftime('%Y-%m-%d')
mi.publication_type = 'periodical:'+self.publication_type mi.publication_type = 'periodical:'+self.publication_type
mi.timestamp = nowf() mi.timestamp = nowf()
mi.comments = self.description mi.comments = self.description
if not isinstance(mi.comments, unicode): if not isinstance(mi.comments, unicode):
mi.comments = mi.comments.decode('utf-8', 'replace') mi.comments = mi.comments.decode('utf-8', 'replace')
mi.tags = ['News']
mi.pubdate = nowf() mi.pubdate = nowf()
opf_path = os.path.join(dir, 'index.opf') opf_path = os.path.join(dir, 'index.opf')
ncx_path = os.path.join(dir, 'index.ncx') ncx_path = os.path.join(dir, 'index.ncx')
@ -1230,7 +1170,7 @@ class BasicNewsRecipe(Recipe):
entries = ['index.html'] entries = ['index.html']
toc = TOC(base_path=dir) toc = TOC(base_path=dir)
self.play_order_counter = 1 self.play_order_counter = 0
self.play_order_map = {} self.play_order_map = {}
def feed_index(num, parent): def feed_index(num, parent):
@ -1342,7 +1282,6 @@ class BasicNewsRecipe(Recipe):
Create a list of articles from the list of feeds returned by :meth:`BasicNewsRecipe.get_feeds`. Create a list of articles from the list of feeds returned by :meth:`BasicNewsRecipe.get_feeds`.
Return a list of :class:`Feed` objects. Return a list of :class:`Feed` objects.
''' '''
print "\nweb.feeds.news:parse_feeds()\n"
feeds = self.get_feeds() feeds = self.get_feeds()
parsed_feeds = [] parsed_feeds = []
for obj in feeds: for obj in feeds:

View File

@ -120,6 +120,7 @@ class TouchscreenNavBarTemplate(Template):
href = '%s%s/%s/index.html'%(prefix, up, next) href = '%s%s/%s/index.html'%(prefix, up, next)
navbar.text = '| ' navbar.text = '| '
navbar.append(A('Next', href=href)) navbar.append(A('Next', href=href))
href = '%s../index.html#article_%d'%(prefix, art) href = '%s../index.html#article_%d'%(prefix, art)
navbar.iterchildren(reversed=True).next().tail = ' | ' navbar.iterchildren(reversed=True).next().tail = ' | '
navbar.append(A('Section Menu', href=href)) navbar.append(A('Section Menu', href=href))
@ -130,6 +131,7 @@ class TouchscreenNavBarTemplate(Template):
href = '%s../article_%d/index.html'%(prefix, art-1) href = '%s../article_%d/index.html'%(prefix, art-1)
navbar.iterchildren(reversed=True).next().tail = ' | ' navbar.iterchildren(reversed=True).next().tail = ' | '
navbar.append(A('Previous', href=href)) navbar.append(A('Previous', href=href))
navbar.iterchildren(reversed=True).next().tail = ' | ' navbar.iterchildren(reversed=True).next().tail = ' | '
if not bottom: if not bottom:
navbar.append(HR()) navbar.append(HR())
@ -165,8 +167,14 @@ class TouchscreenIndexTemplate(Template):
def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None): def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None):
if isinstance(datefmt, unicode): if isinstance(datefmt, unicode):
datefmt = datefmt.encode(preferred_encoding) datefmt = datefmt.encode(preferred_encoding)
date = strftime(datefmt) date = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
masthead_img = IMG(src=masthead,alt="masthead") masthead_p = etree.Element("p")
masthead_p.set("style","text-align:center")
masthead_img = etree.Element("img")
masthead_img.set("src",masthead)
masthead_img.set("alt","masthead")
masthead_p.append(masthead_img)
head = HEAD(TITLE(title)) head = HEAD(TITLE(title))
if style: if style:
head.append(STYLE(style, type='text/css')) head.append(STYLE(style, type='text/css'))
@ -178,11 +186,11 @@ class TouchscreenIndexTemplate(Template):
if feed: if feed:
tr = TR() tr = TR()
tr.append(TD( CLASS('toc_item'), A(feed.title, href='feed_%d/index.html'%i))) tr.append(TD( CLASS('toc_item'), A(feed.title, href='feed_%d/index.html'%i)))
tr.append(TD( CLASS('article_count'),'%d' % len(feed.articles))) tr.append(TD( CLASS('article_count'),'%3.3s' % len(feed.articles)))
toc.append(tr) toc.append(tr)
div = DIV( div = DIV(
PT(masthead_img,style='text-align:center'), masthead_p,
PT(date, style='text-align:center'), PT(date, style='text-align:center'),
toc, toc,
CLASS('calibre_rescale_100')) CLASS('calibre_rescale_100'))

View File

@ -329,7 +329,7 @@ class RecursiveFetcher(object):
try: try:
data = self.fetch_url(iurl) data = self.fetch_url(iurl)
if data == 'GIF89a\x01': if data == 'GIF89a\x01':
# Skip empty GIF files # Skip empty GIF files as PIL errors on them anyway
continue continue
except Exception: except Exception:
self.log.exception('Could not fetch image %s'% iurl) self.log.exception('Could not fetch image %s'% iurl)