Merge from trunk

This commit is contained in:
Charles Haley 2011-01-21 09:24:43 +00:00
commit e7df9e742c
26 changed files with 1942 additions and 159 deletions

View File

@ -1,6 +1,6 @@
__license__ = 'GPL v3'
__copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
__copyright__ = '2008-2011, Darko Miletic <darko.miletic at gmail.com>'
'''
blic.rs
'''
@ -21,21 +21,53 @@ class Blic(BasicNewsRecipe):
masthead_url = 'http://www.blic.rs/resources/images/header/header_back.png'
language = 'sr'
publication_type = 'newspaper'
extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: Georgia, serif1, serif} .article_description{font-family: Arial, sans1, sans-serif} .img_full{float: none} img{margin-bottom: 0.8em} '
extra_css = """
@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
body{font-family: Georgia, serif1, serif}
.articledescription,#nadnaslov,.article_info{font-family: Arial, sans1, sans-serif}
.img_full{float: none}
#nadnaslov{font-size: small}
#article_lead{font-size: 1.5em}
h1{color: red}
.potpis{font-size: x-small; color: gray}
.article_info{font-size: small}
img{margin-bottom: 0.8em; margin-top: 0.8em; display: block}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher': publisher
, 'language' : language
, 'linearize_tables' : True
}
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
remove_tags_before = dict(name='div', attrs={'id':'article_info'})
remove_tags = [dict(name=['object','link'])]
remove_attributes = ['width','height']
remove_tags = [dict(name=['object','link','meta','base','object','embed'])]
remove_attributes = ['width','height','m_id','m_ext','mlg_id','poll_id','v_id']
feeds = [(u'Danasnje Vesti', u'http://www.blic.rs/rss/danasnje-vesti')]
feeds = [
(u'Politika' , u'http://www.blic.rs/rss/Vesti/Politika')
,(u'Tema Dana' , u'http://www.blic.rs/rss/Vesti/Tema-Dana')
,(u'Svet' , u'http://www.blic.rs/rss/Vesti/Svet')
,(u'Drustvo' , u'http://www.blic.rs/rss/Vesti/Drustvo')
,(u'Ekonomija' , u'http://www.blic.rs/rss/Vesti/Ekonomija')
,(u'Hronika' , u'http://www.blic.rs/rss/Vesti/Hronika')
,(u'Beograd' , u'http://www.blic.rs/rss/Vesti/Beograd')
,(u'Srbija' , u'http://www.blic.rs/rss/Vesti/Srbija')
,(u'Vojvodina' , u'http://www.blic.rs/rss/Vesti/Vojvodina')
,(u'Republika Srpska' , u'http://www.blic.rs/rss/Vesti/Republika-Srpska')
,(u'Reportaza' , u'http://www.blic.rs/rss/Vesti/Reportaza')
,(u'Dodatak' , u'http://www.blic.rs/rss/Vesti/Dodatak')
,(u'Zabava' , u'http://www.blic.rs/rss/Zabava')
,(u'Kultura' , u'http://www.blic.rs/rss/Kultura')
,(u'Slobodno Vreme' , u'http://www.blic.rs/rss/Slobodno-vreme')
,(u'IT' , u'http://www.blic.rs/rss/IT')
,(u'Komentar' , u'http://www.blic.rs/rss/Komentar')
,(u'Intervju' , u'http://www.blic.rs/rss/Intervju')
]
def print_version(self, url):
@ -44,4 +76,4 @@ class Blic(BasicNewsRecipe):
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return self.adeify_images(soup)
return soup

View File

@ -0,0 +1,64 @@
__license__ = 'GPL v3'
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
'''
gulfnews.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class GulfNews(BasicNewsRecipe):
title = 'Gulf News'
__author__ = 'Darko Miletic'
description = 'News from United Arab Emirrates, persian gulf and rest of the world'
publisher = 'Al Nisr Publishing LLC'
category = 'news, politics, UAE, world'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'utf8'
use_embedded_content = False
language = 'en'
remove_empty_feeds = True
publication_type = 'newsportal'
masthead_url = 'http://gulfnews.com/media/img/gulf_news_logo.jpg'
extra_css = """
body{font-family: Arial,Helvetica,sans-serif }
img{margin-bottom: 0.4em; display:block}
h1{font-family: Georgia, 'Times New Roman', Times, serif}
ol,ul{list-style: none}
.synopsis{font-size: small}
.details{font-size: x-small}
.image{font-size: xx-small}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags = [
dict(name=['meta','link','object','embed'])
,dict(attrs={'class':['quickLinks','ratings']})
,dict(attrs={'id':'imageSelector'})
]
remove_attributes=['lang']
keep_only_tags=[
dict(name='h1')
,dict(attrs={'class':['synopsis','details','image','article']})
]
feeds = [
(u'UAE News' , u'http://gulfnews.com/cmlink/1.446094')
,(u'Business' , u'http://gulfnews.com/cmlink/1.446098')
,(u'Entertainment' , u'http://gulfnews.com/cmlink/1.446095')
,(u'Sport' , u'http://gulfnews.com/cmlink/1.446096')
,(u'Life' , u'http://gulfnews.com/cmlink/1.446097')
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -3,12 +3,17 @@ from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1274742400(BasicNewsRecipe):
title = u'Las Vegas Review Journal'
__author__ = 'Joel'
__author__ = 'Kovid Goyal'
language = 'en'
oldest_article = 7
max_articles_per_feed = 100
keep_only_tags = [dict(id='content-main')]
remove_tags = [dict(id=['right-col-content', 'trending-topics']),
{'class':['ppy-outer']}
]
no_stylesheets = True
feeds = [
(u'News', u'http://www.lvrj.com/news.rss'),

View File

@ -241,7 +241,7 @@ def get_parsed_proxy(typ='http', debug=True):
return ans
def browser(honor_time=True, max_time=2, mobile_browser=False):
def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None):
'''
Create a mechanize browser for web scraping. The browser handles cookies,
refresh requests and ignores robots.txt. Also uses proxy if avaialable.
@ -253,8 +253,10 @@ def browser(honor_time=True, max_time=2, mobile_browser=False):
opener = Browser()
opener.set_handle_refresh(True, max_time=max_time, honor_time=honor_time)
opener.set_handle_robots(False)
opener.addheaders = [('User-agent', ' Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016' if mobile_browser else \
'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13')]
if user_agent is None:
user_agent = ' Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016' if mobile_browser else \
'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13'
opener.addheaders = [('User-agent', user_agent)]
http_proxy = get_proxies().get('http', None)
if http_proxy:
opener.set_proxies({'http':http_proxy})

View File

@ -21,7 +21,7 @@ class ANDROID(USBMS):
# HTC
0x0bb4 : { 0x0c02 : [0x100, 0x0227, 0x0226], 0x0c01 : [0x100, 0x0227], 0x0ff9
: [0x0100, 0x0227, 0x0226], 0x0c87: [0x0100, 0x0227, 0x0226],
0xc92 : [0x100], 0xc97: [0x226]},
0xc92 : [0x100], 0xc97: [0x226], 0xc99 : [0x0100]},
# Eken
0x040d : { 0x8510 : [0x0001], 0x0851 : [0x1] },
@ -54,7 +54,7 @@ class ANDROID(USBMS):
0x1004 : { 0x61cc : [0x100] },
# Archos
0x0e79 : { 0x1420 : [0x0216]},
0x0e79 : { 0x1419: [0x0216], 0x1420 : [0x0216]},
}
EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books']
@ -70,10 +70,10 @@ class ANDROID(USBMS):
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID']
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S']
'A70S', 'A101IT']
OSX_MAIN_MEM = 'Android Device Main Memory'

View File

@ -106,7 +106,7 @@ class PDNOVEL(USBMS):
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE'
THUMBNAIL_HEIGHT = 130
EBOOK_DIR_MAIN = 'eBooks'
EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'eBooks'
SUPPORTS_SUB_DIRS = False
DELETE_EXTS = ['.jpg', '.jpeg', '.png']

View File

@ -201,7 +201,7 @@ class Dehyphenator(object):
lookupword = self.removesuffixes.sub('', dehyphenated)
else:
lookupword = dehyphenated
if len(firsthalf) > 3 and self.prefixes.match(firsthalf) is None:
if len(firsthalf) > 4 and self.prefixes.match(firsthalf) is None:
lookupword = self.removeprefix.sub('', lookupword)
if self.verbose > 2:
self.log("lookup word is: "+str(lookupword)+", orig is: " + str(hyphenated))
@ -224,6 +224,10 @@ class Dehyphenator(object):
return firsthalf+u'\u2014'+wraptags+secondhalf
else:
if self.format == 'individual_words' and len(firsthalf) + len(secondhalf) <= 6:
if self.verbose > 2:
self.log("too short, returned hyphenated word: " + str(hyphenated))
return hyphenated
if len(firsthalf) <= 2 and len(secondhalf) <= 2:
if self.verbose > 2:
self.log("too short, returned hyphenated word: " + str(hyphenated))

View File

@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
Fetch cover from LibraryThing.com based on ISBN number.
'''
import sys, socket, os, re
import sys, socket, os, re, random
from lxml import html
import mechanize
@ -16,13 +16,26 @@ from calibre.ebooks.chardet import strip_encoding_declarations
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
def get_ua():
choices = [
'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11'
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)'
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)'
'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1)'
'Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16'
'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.153.1 Safari/525.19'
'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11'
]
return choices[random.randint(0, len(choices)-1)]
class HeadRequest(mechanize.Request):
def get_method(self):
return 'HEAD'
def check_for_cover(isbn, timeout=5.):
br = browser()
br = browser(user_agent=get_ua())
br.set_handle_redirect(False)
try:
br.open_novisit(HeadRequest(OPENLIBRARY%isbn), timeout=timeout)
@ -51,7 +64,7 @@ def login(br, username, password, force=True):
def cover_from_isbn(isbn, timeout=5., username=None, password=None):
src = None
br = browser()
br = browser(user_agent=get_ua())
try:
return br.open(OPENLIBRARY%isbn, timeout=timeout).read(), 'jpg'
except:
@ -100,7 +113,7 @@ def get_social_metadata(title, authors, publisher, isbn, username=None,
from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(title, authors)
if isbn:
br = browser()
br = browser(user_agent=get_ua())
if username and password:
try:
login(br, username, password, force=False)

View File

@ -71,21 +71,41 @@ class TXTInput(InputFormatPlugin):
txt = txt.decode(ienc, 'replace')
txt = _ent_pat.sub(xml_entity_to_unicode, txt)
# Normalize line endings
txt = normalize_line_endings(txt)
if options.formatting_type == 'auto':
options.formatting_type = detect_formatting_type(txt)
if options.formatting_type == 'heuristic':
setattr(options, 'enable_heuristics', True)
setattr(options, 'markup_chapter_headings', True)
setattr(options, 'italicize_common_cases', True)
setattr(options, 'fix_indents', True)
setattr(options, 'preserve_spaces', True)
setattr(options, 'delete_blank_paragraphs', True)
setattr(options, 'format_scene_breaks', True)
setattr(options, 'dehyphenate', True)
# Determine the paragraph type of the document.
if options.paragraph_type == 'auto':
options.paragraph_type = detect_paragraph_type(txt)
if options.paragraph_type == 'unknown':
log.debug('Could not reliably determine paragraph type using block')
options.paragraph_type = 'block'
else:
log.debug('Auto detected paragraph type as %s' % options.paragraph_type)
# Preserve spaces will replace multiple spaces to a space
# followed by the &nbsp; entity.
if options.preserve_spaces:
txt = preserve_spaces(txt)
# Normalize line endings
txt = normalize_line_endings(txt)
# Get length for hyphen removal and punctuation unwrap
docanalysis = DocAnalysis('txt', txt)
length = docanalysis.line_length(.5)
if options.formatting_type == 'auto':
options.formatting_type = detect_formatting_type(txt)
if options.formatting_type == 'markdown':
log.debug('Running text though markdown conversion...')
try:
@ -96,16 +116,8 @@ class TXTInput(InputFormatPlugin):
elif options.formatting_type == 'textile':
log.debug('Running text though textile conversion...')
html = convert_textile(txt)
else:
# Determine the paragraph type of the document.
if options.paragraph_type == 'auto':
options.paragraph_type = detect_paragraph_type(txt)
if options.paragraph_type == 'unknown':
log.debug('Could not reliably determine paragraph type using block')
options.paragraph_type = 'block'
else:
log.debug('Auto detected paragraph type as %s' % options.paragraph_type)
else:
# Dehyphenate
dehyphenator = Dehyphenator(options.verbose, log=self.log)
txt = dehyphenator(txt,'txt', length)
@ -129,15 +141,6 @@ class TXTInput(InputFormatPlugin):
flow_size = getattr(options, 'flow_size', 0)
html = convert_basic(txt, epub_split_size_kb=flow_size)
if options.formatting_type == 'heuristic':
setattr(options, 'enable_heuristics', True)
setattr(options, 'markup_chapter_headings', True)
setattr(options, 'italicize_common_cases', True)
setattr(options, 'fix_indents', True)
setattr(options, 'delete_blank_paragraphs', True)
setattr(options, 'format_scene_breaks', True)
setattr(options, 'dehyphenate', True)
from calibre.customize.ui import plugin_for_input_format
html_input = plugin_for_input_format('html')
for opt in html_input.options:

View File

@ -175,9 +175,9 @@ def detect_formatting_type(txt):
# Block quote.
textile_count += len(re.findall(r'(?mu)^bq\.', txt))
# Images
textile_count += len(re.findall(r'\![^\s]+(:[^\s]+)*', txt))
textile_count += len(re.findall(r'\![^\s]+(?=.*?/)(:[^\s]+)*', txt))
# Links
textile_count += len(re.findall(r'"(\(.+?\))*[^\(]+?(\(.+?\))*":[^\s]+', txt))
textile_count += len(re.findall(r'"(?=".*?\()(\(.+?\))*[^\(]+?(\(.+?\))*":[^\s]+', txt))
if markdown_count > 5 or textile_count > 5:
if markdown_count > textile_count:

View File

@ -8,11 +8,12 @@ __docformat__ = 'restructuredtext en'
import os
from functools import partial
from PyQt4.Qt import QInputDialog, QPixmap, QMenu
from PyQt4.Qt import QPixmap, QMenu
from calibre.gui2 import error_dialog, choose_files, \
choose_dir, warning_dialog, info_dialog
from calibre.gui2.dialogs.add_empty_book import AddEmptyBookDialog
from calibre.gui2.widgets import IMAGE_EXTENSIONS
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.utils.filenames import ascii_filename
@ -42,7 +43,7 @@ class AddAction(InterfaceAction):
'ebook file is a different book)'), self.add_recursive_multiple)
self.add_menu.addSeparator()
self.add_menu.addAction(_('Add Empty book. (Book entry with no '
'formats)'), self.add_empty)
'formats)'), self.add_empty, _('Shift+Ctrl+E'))
self.add_menu.addAction(_('Add from ISBN'), self.add_from_isbn)
self.qaction.setMenu(self.add_menu)
self.qaction.triggered.connect(self.add_books)
@ -83,12 +84,21 @@ class AddAction(InterfaceAction):
Add an empty book item to the library. This does not import any formats
from a book file.
'''
num, ok = QInputDialog.getInt(self.gui, _('How many empty books?'),
_('How many empty books should be added?'), 1, 1, 100)
if ok:
author = None
index = self.gui.library_view.currentIndex()
if index.isValid():
raw = index.model().db.authors(index.row())
if raw:
authors = [a.strip().replace('|', ',') for a in raw.split(',')]
if authors:
author = authors[0]
dlg = AddEmptyBookDialog(self.gui, self.gui.library_view.model().db, author)
if dlg.exec_() == dlg.Accepted:
num = dlg.qty_to_add
from calibre.ebooks.metadata import MetaInformation
for x in xrange(num):
self.gui.library_view.model().db.import_book(MetaInformation(None), [])
mi = MetaInformation(_('Unknown'), dlg.selected_authors)
self.gui.library_view.model().db.import_book(mi, [])
self.gui.library_view.model().books_added(num)
def add_isbns(self, books, add_tags=[]):

View File

@ -32,7 +32,7 @@ class LibraryUsageStats(object): # {{{
locs = list(self.stats.keys())
locs.sort(cmp=lambda x, y: cmp(self.stats[x], self.stats[y]),
reverse=True)
for key in locs[15:]:
for key in locs[25:]:
self.stats.pop(key)
gprefs.set('library_usage_stats', self.stats)

View File

@ -35,7 +35,7 @@
</size>
</property>
<property name="toolTip">
<string>Sections to include in catalog. All catalogs include 'Books by Author'.</string>
<string>Sections to include in catalog.</string>
</property>
<property name="title">
<string>Included sections</string>
@ -79,13 +79,13 @@
<item row="0" column="0">
<widget class="QCheckBox" name="generate_authors">
<property name="enabled">
<bool>false</bool>
<bool>true</bool>
</property>
<property name="text">
<string>Books by Author</string>
</property>
<property name="checked">
<bool>true</bool>
<bool>false</bool>
</property>
</widget>
</item>

View File

@ -379,7 +379,8 @@ def populate_metadata_page(layout, db, book_id, bulk=False, two_column=False, pa
w = bulk_widgets[type](db, col, parent)
else:
w = widgets[type](db, col, parent)
w.initialize(book_id)
if book_id is not None:
w.initialize(book_id)
return w
x = db.custom_column_num_map
cols = list(x)

View File

@ -0,0 +1,85 @@
#!/usr/bin/env python
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__license__ = 'GPL v3'
from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \
QApplication, QSpinBox, QToolButton, QIcon
from calibre.ebooks.metadata import authors_to_string, string_to_authors
from calibre.gui2.widgets import CompleteComboBox
from calibre.utils.icu import sort_key
class AddEmptyBookDialog(QDialog):
def __init__(self, parent, db, author):
QDialog.__init__(self, parent)
self.db = db
self.setWindowTitle(_('How many empty books?'))
self._layout = QGridLayout(self)
self.setLayout(self._layout)
self.qty_label = QLabel(_('How many empty books should be added?'))
self._layout.addWidget(self.qty_label, 0, 0, 1, 2)
self.qty_spinbox = QSpinBox(self)
self.qty_spinbox.setRange(1, 10000)
self.qty_spinbox.setValue(1)
self._layout.addWidget(self.qty_spinbox, 1, 0, 1, 2)
self.author_label = QLabel(_('Set the author of the new books to:'))
self._layout.addWidget(self.author_label, 2, 0, 1, 2)
self.authors_combo = CompleteComboBox(self)
self.authors_combo.setSizeAdjustPolicy(
self.authors_combo.AdjustToMinimumContentsLengthWithIcon)
self.authors_combo.setEditable(True)
self._layout.addWidget(self.authors_combo, 3, 0, 1, 1)
self.initialize_authors(db, author)
self.clear_button = QToolButton(self)
self.clear_button.setIcon(QIcon(I('trash.png')))
self.clear_button.setToolTip(_('Reset author to Unknown'))
self.clear_button.clicked.connect(self.reset_author)
self._layout.addWidget(self.clear_button, 3, 1, 1, 1)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
self._layout.addWidget(button_box)
self.resize(self.sizeHint())
def reset_author(self, *args):
self.authors_combo.setEditText(_('Unknown'))
def initialize_authors(self, db, author):
all_authors = db.all_authors()
all_authors.sort(key=lambda x : sort_key(x[1]))
for i in all_authors:
id, name = i
name = [name.strip().replace('|', ',') for n in name.split(',')]
self.authors_combo.addItem(authors_to_string(name))
au = author
if not au:
au = _('Unknown')
self.authors_combo.setEditText(au.replace('|', ','))
self.authors_combo.set_separator('&')
self.authors_combo.set_space_before_sep(True)
self.authors_combo.update_items_cache(db.all_author_names())
@property
def qty_to_add(self):
return self.qty_spinbox.value()
@property
def selected_authors(self):
return string_to_authors(unicode(self.authors_combo.text()))
if __name__ == '__main__':
app = QApplication([])
d = AddEmptyBookDialog()
d.exec_()

View File

@ -775,7 +775,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.original_tags = unicode(self.tags.text())
else:
self.tags.setText(self.original_tags)
d = TagEditor(self, self.db, self.row)
d = TagEditor(self, self.db, self.id)
d.exec_()
if d.result() == QDialog.Accepted:
tag_string = ', '.join(d.tags)

View File

@ -10,13 +10,13 @@ from calibre.utils.icu import sort_key
class TagEditor(QDialog, Ui_TagEditor):
def __init__(self, window, db, index=None):
def __init__(self, window, db, id_=None):
QDialog.__init__(self, window)
Ui_TagEditor.__init__(self)
self.setupUi(self)
self.db = db
self.index = index
self.index = db.row(id_)
if self.index is not None:
tags = self.db.tags(self.index)
else:

View File

@ -356,6 +356,13 @@ class %(classname)s(%(base_class)s):
self.populate_options(AutomaticNewsRecipe)
self.source_code.setText('')
def reject(self):
if question_dialog(self, _('Are you sure?'),
_('You will lose any unsaved changes. To save your'
' changes, click the Add/Update recipe button.'
' Continue?'), show_copy_button=False):
ResizableDialog.reject(self)
if __name__ == '__main__':
from calibre.gui2 import is_ok_to_use_qt
is_ok_to_use_qt()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,474 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from functools import partial
from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \
QSizePolicy
from calibre.ebooks.metadata import authors_to_string, string_to_authors
from calibre.gui2 import ResizableDialog, error_dialog, gprefs
from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \
AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \
RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \
BuddyLabel, DateEdit, PubdateEdit
from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.utils.config import tweaks
class MetadataSingleDialog(ResizableDialog):
view_format = pyqtSignal(object)
def __init__(self, db, parent=None):
self.db = db
self.changed = set([])
ResizableDialog.__init__(self, parent)
def setupUi(self, *args): # {{{
self.resize(990, 650)
self.button_box = QDialogButtonBox(
QDialogButtonBox.Ok|QDialogButtonBox.Cancel, Qt.Horizontal,
self)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'),
self)
self.next_button.clicked.connect(partial(self.do_one, delta=1))
self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'),
self)
self.button_box.addButton(self.prev_button, self.button_box.ActionRole)
self.button_box.addButton(self.next_button, self.button_box.ActionRole)
self.prev_button.clicked.connect(partial(self.do_one, delta=-1))
self.scroll_area = QScrollArea(self)
self.scroll_area.setFrameShape(QScrollArea.NoFrame)
self.scroll_area.setWidgetResizable(True)
self.central_widget = QTabWidget(self)
self.scroll_area.setWidget(self.central_widget)
self.l = QVBoxLayout(self)
self.setLayout(self.l)
self.l.setMargin(0)
self.l.addWidget(self.scroll_area)
self.l.addWidget(self.button_box)
self.setWindowIcon(QIcon(I('edit_input.png')))
self.setWindowTitle(_('Edit Meta Information'))
self.create_basic_metadata_widgets()
if len(self.db.custom_column_label_map) == 0:
self.central_widget.tabBar().setVisible(False)
else:
self.create_custom_metadata_widgets()
self.do_layout()
geom = gprefs.get('metasingle_window_geometry3', None)
if geom is not None:
self.restoreGeometry(bytes(geom))
# }}}
def create_basic_metadata_widgets(self): # {{{
self.basic_metadata_widgets = []
self.title = TitleEdit(self)
self.title.textChanged.connect(self.update_window_title)
self.deduce_title_sort_button = QToolButton(self)
self.deduce_title_sort_button.setToolTip(
_('Automatically create the title sort entry based on the current '
'title entry.\nUsing this button to create title sort will '
'change title sort from red to green.'))
self.deduce_title_sort_button.setWhatsThis(
self.deduce_title_sort_button.toolTip())
self.title_sort = TitleSortEdit(self, self.title,
self.deduce_title_sort_button)
self.basic_metadata_widgets.extend([self.title, self.title_sort])
self.authors = AuthorsEdit(self)
self.deduce_author_sort_button = QToolButton(self)
self.deduce_author_sort_button.setToolTip(_(
'Automatically create the author sort entry based on the current'
' author entry.\n'
'Using this button to create author sort will change author sort from'
' red to green.'))
self.author_sort = AuthorSortEdit(self, self.authors,
self.deduce_author_sort_button, db)
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
self.swap_title_author_button = QToolButton(self)
self.swap_title_author_button.setIcon(QIcon(I('swap.png')))
self.swap_title_author_button.setToolTip(_(
'Swap the author and title'))
self.swap_title_author_button.clicked.connect(self.swap_title_author)
self.series = SeriesEdit(self)
self.remove_unused_series_button = QToolButton(self)
self.remove_unused_series_button.setToolTip(
_('Remove unused series (Series that have no books)') )
self.remove_unused_series_button.clicked.connect(self.remove_unused_series)
self.series_index = SeriesIndexEdit(self, self.series)
self.basic_metadata_widgets.extend([self.series, self.series_index])
self.formats_manager = FormatsManager(self)
self.basic_metadata_widgets.append(self.formats_manager)
self.formats_manager.metadata_from_format_button.clicked.connect(
self.metadata_from_format)
self.formats_manager.cover_from_format_button.clicked.connect(
self.cover_from_format)
self.cover = Cover(self)
self.basic_metadata_widgets.append(self.cover)
self.comments = CommentsEdit(self)
self.basic_metadata_widgets.append(self.comments)
self.rating = RatingEdit(self)
self.basic_metadata_widgets.append(self.rating)
self.tags = TagsEdit(self)
self.tags_editor_button = QToolButton(self)
self.tags_editor_button.setToolTip(_('Open Tag Editor'))
self.tags_editor_button.setIcon(QIcon(I('chapters.png')))
self.tags_editor_button.clicked.connect(self.tags_editor)
self.basic_metadata_widgets.append(self.tags)
self.isbn = ISBNEdit(self)
self.basic_metadata_widgets.append(self.isbn)
self.publisher = PublisherEdit(self)
self.basic_metadata_widgets.append(self.publisher)
self.timestamp = DateEdit(self)
self.pubdate = PubdateEdit(self)
self.basic_metadata_widgets.extend([self.timestamp, self.pubdate])
self.fetch_metadata_button = QPushButton(
_('&Fetch metadata from server'), self)
self.fetch_metadata_button.clicked.connect(self.fetch_metadata)
font = self.fmb_font = QFont()
font.setBold(True)
self.fetch_metadata_button.setFont(font)
# }}}
def create_custom_metadata_widgets(self): # {{{
self.custom_metadata_widgets_parent = w = QWidget(self)
layout = QGridLayout()
w.setLayout(layout)
self.custom_metadata_widgets, self.__cc_spacers = \
populate_metadata_page(layout, self.db, None, parent=w, bulk=False,
two_column=tweaks['metadata_single_use_2_cols_for_custom_fields'])
self.__custom_col_layouts = [layout]
ans = self.custom_metadata_widgets
for i in range(len(ans)-1):
if len(ans[i+1].widgets) == 2:
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[1])
else:
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[0])
for c in range(2, len(ans[i].widgets), 2):
w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1])
# }}}
def do_layout(self): # {{{
self.central_widget.clear()
self.tabs = []
self.labels = []
self.tabs.append(QWidget(self))
self.central_widget.addTab(self.tabs[0], _("&Basic metadata"))
self.tabs[0].l = l = QVBoxLayout()
self.tabs[0].tl = tl = QGridLayout()
self.tabs[0].setLayout(l)
w = getattr(self, 'custom_metadata_widgets_parent', None)
if w is not None:
self.tabs.append(w)
self.central_widget.addTab(w, _('&Custom metadata'))
l.addLayout(tl)
l.addItem(QSpacerItem(10, 15, QSizePolicy.Expanding,
QSizePolicy.Fixed))
sto = QWidget.setTabOrder
sto(self.button_box, self.fetch_metadata_button)
sto(self.fetch_metadata_button, self.title)
def create_row(row, one, two, three, col=1, icon='forward.png'):
ql = BuddyLabel(one)
tl.addWidget(ql, row, col+0, 1, 1)
self.labels.append(ql)
tl.addWidget(one, row, col+1, 1, 1)
if two is not None:
tl.addWidget(two, row, col+2, 1, 1)
two.setIcon(QIcon(I(icon)))
ql = BuddyLabel(three)
tl.addWidget(ql, row, col+3, 1, 1)
self.labels.append(ql)
tl.addWidget(three, row, col+4, 1, 1)
sto(one, two)
sto(two, three)
tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
create_row(0, self.title, self.deduce_title_sort_button, self.title_sort)
sto(self.title_sort, self.authors)
create_row(1, self.authors, self.deduce_author_sort_button, self.author_sort)
sto(self.author_sort, self.series)
create_row(2, self.series, self.remove_unused_series_button,
self.series_index, icon='trash.png')
sto(self.series_index, self.swap_title_author_button)
tl.addWidget(self.formats_manager, 0, 6, 3, 1)
self.splitter = QSplitter(Qt.Horizontal, self)
self.splitter.addWidget(self.cover)
l.addWidget(self.splitter)
self.tabs[0].gb = gb = QGroupBox(_('Change cover'), self)
gb.l = l = QGridLayout()
gb.setLayout(l)
sto(self.swap_title_author_button, self.cover.buttons[0])
for i, b in enumerate(self.cover.buttons[:3]):
l.addWidget(b, 0, i, 1, 1)
sto(b, self.cover.buttons[i+1])
gb.hl = QHBoxLayout()
for b in self.cover.buttons[3:]:
gb.hl.addWidget(b)
sto(self.cover.buttons[-2], self.cover.buttons[-1])
l.addLayout(gb.hl, 1, 0, 1, 3)
self.tabs[0].middle = w = QWidget(self)
w.l = l = QGridLayout()
w.setLayout(w.l)
l.setMargin(0)
self.splitter.addWidget(w)
def create_row2(row, widget, button=None):
row += 1
ql = BuddyLabel(widget)
l.addWidget(ql, row, 0, 1, 1)
l.addWidget(widget, row, 1, 1, 2 if button is None else 1)
if button is not None:
l.addWidget(button, row, 2, 1, 1)
if button is not None:
sto(widget, button)
l.addWidget(gb, 0, 0, 1, 3)
self.tabs[0].spc_one = QSpacerItem(10, 10, QSizePolicy.Expanding,
QSizePolicy.Expanding)
l.addItem(self.tabs[0].spc_one, 1, 0, 1, 3)
sto(self.cover.buttons[-1], self.rating)
create_row2(1, self.rating)
sto(self.rating, self.tags)
create_row2(2, self.tags, self.tags_editor_button)
sto(self.tags_editor_button, self.isbn)
create_row2(3, self.isbn)
sto(self.isbn, self.timestamp)
create_row2(4, self.timestamp, self.timestamp.clear_button)
sto(self.timestamp.clear_button, self.pubdate)
create_row2(5, self.pubdate, self.pubdate.clear_button)
sto(self.pubdate.clear_button, self.publisher)
create_row2(6, self.publisher)
self.tabs[0].spc_two = QSpacerItem(10, 10, QSizePolicy.Expanding,
QSizePolicy.Expanding)
l.addItem(self.tabs[0].spc_two, 8, 0, 1, 3)
l.addWidget(self.fetch_metadata_button, 9, 0, 1, 3)
self.tabs[0].gb2 = gb = QGroupBox(_('Co&mments'), self)
gb.l = l = QVBoxLayout()
gb.setLayout(l)
l.addWidget(self.comments)
self.splitter.addWidget(gb)
# }}}
def __call__(self, id_):
self.book_id = id_
for widget in self.basic_metadata_widgets:
widget.initialize(self.db, id_)
for widget in self.custom_metadata_widgets:
widget.initialize(id_)
# Commented out as it doesn't play nice with Next, Prev buttons
#self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
def update_window_title(self, *args):
title = self.title.current_val
if len(title) > 50:
title = title[:50] + u'\u2026'
self.setWindowTitle(_('Edit Meta Information') + ' - ' +
title)
def swap_title_author(self, *args):
title = self.title.current_val
self.title.current_val = authors_to_string(self.authors.current_val)
self.authors.current_val = string_to_authors(title)
self.title_sort.auto_generate()
self.author_sort.auto_generate()
def remove_unused_series(self, *args):
self.db.remove_unused_series()
idx = self.series.current_val
self.series.clear()
self.series.initialize(self.db, self.book_id)
if idx:
for i in range(self.series.count()):
if unicode(self.series.itemText(i)) == idx:
self.series.setCurrentIndex(i)
break
def tags_editor(self, *args):
self.tags.edit(self.db, self.book_id)
def metadata_from_format(self, *args):
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
self.book_id)
if mi is not None:
self.update_from_mi(mi)
def cover_from_format(self, *args):
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
self.book_id)
if mi is None:
return
cdata = None
if mi.cover and os.access(mi.cover, os.R_OK):
cdata = open(mi.cover).read()
elif mi.cover_data[1] is not None:
cdata = mi.cover_data[1]
if cdata is None:
error_dialog(self, _('Could not read cover'),
_('Could not read cover from %s format')%ext).exec_()
return
orig = self.cover.current_val
self.cover.current_val = cdata
if self.cover.current_val is None:
self.cover.current_val = orig
return error_dialog(self, _('Could not read cover'),
_('The cover in the %s format is invalid')%ext,
show=True)
return
def update_from_mi(self, mi):
if not mi.is_null('title'):
self.title.current_val = mi.title
if not mi.is_null('authors'):
self.authors.current_val = mi.authors
if not mi.is_null('author_sort'):
self.author_sort.current_val = mi.author_sort
if not mi.is_null('rating'):
try:
self.rating.current_val = mi.rating
except:
pass
if not mi.is_null('publisher'):
self.publisher.current_val = mi.publisher
if not mi.is_null('tags'):
self.tags.current_val = mi.tags
if not mi.is_null('isbn'):
self.isbn.current_val = mi.isbn
if not mi.is_null('pubdate'):
self.pubdate.current_val = mi.pubdate
if not mi.is_null('series') and mi.series.strip():
self.series.current_val = mi.series
if mi.series_index is not None:
self.series_index.current_val = float(mi.series_index)
if mi.comments and mi.comments.strip():
self.comments.current_val = mi.comments
def fetch_metadata(self, *args):
pass # TODO: fetch metadata
def apply_changes(self):
self.changed.add(self.book_id)
for widget in self.basic_metadata_widgets:
try:
if not widget.commit(self.db, self.book_id):
return False
except IOError, err:
if err.errno == 13: # Permission denied
import traceback
fname = err.filename if err.filename else 'file'
error_dialog(self, _('Permission denied'),
_('Could not open %s. Is it being used by another'
' program?')%fname, det_msg=traceback.format_exc(),
show=True)
return False
raise
for widget in getattr(self, 'custom_metadata_widgets', []):
widget.commit(self.book_id)
self.db.commit()
return True
def accept(self):
self.save_state()
if not self.apply_changes():
return
ResizableDialog.accept(self)
def reject(self):
self.save_state()
ResizableDialog.reject(self)
def save_state(self):
gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry())
def start(self, row_list, current_row, view_slot=None):
self.row_list = row_list
self.current_row = current_row
if view_slot is not None:
self.view_format.connect(view_slot)
self.do_one()
ret = self.exec_()
self.break_cycles()
return ret
def do_one(self, delta=0):
self.current_row += delta
prev = next_ = None
if self.current_row > 0:
prev = self.db.title(self.row_list[self.current_row-1])
if self.current_row < len(self.row_list) - 1:
next_ = self.db.title(self.row_list[self.current_row+1])
if next_ is not None:
tip = _('Save changes and edit the metadata of %s')%next_
self.next_button.setToolTip(tip)
self.next_button.setVisible(next_ is not None)
if prev is not None:
tip = _('Save changes and edit the metadata of %s')%prev
self.prev_button.setToolTip(tip)
self.prev_button.setVisible(prev is not None)
self(self.db.id(self.row_list[self.current_row]))
def break_cycles(self):
# Break any reference cycles that could prevent python
# from garbage collecting this dialog
def disconnect(signal):
try:
signal.disconnect()
except:
pass # Fails if view format was never connected
disconnect(self.view_format)
for b in ('next_button', 'prev_button'):
x = getattr(self, b, None)
if x is not None:
disconnect(x.clicked)
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None):
d = MetadataSingleDialog(db, parent)
d.start(row_list, current_row, view_slot=view_slot)
return d.changed
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
from calibre.library import db
db = db()
row_list = list(range(len(db.data)))
edit_metadata(db, row_list, 0)

View File

@ -449,7 +449,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def set_window_title(self):
self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name())
self.setWindowTitle(__appname__ + u' - || %s ||'%self.iactions['Choose Library'].library_name())
def location_selected(self, location):
'''

View File

@ -123,6 +123,8 @@ IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'gif', 'png', 'bmp']
class FormatList(QListWidget):
DROPABBLE_EXTENSIONS = BOOK_EXTENSIONS
formats_dropped = pyqtSignal(object, object)
delete_format = pyqtSignal()
@classmethod
def paths_from_event(cls, event):
@ -146,15 +148,14 @@ class FormatList(QListWidget):
def dropEvent(self, event):
paths = self.paths_from_event(event)
event.setDropAction(Qt.CopyAction)
self.emit(SIGNAL('formats_dropped(PyQt_PyObject,PyQt_PyObject)'),
event, paths)
self.formats_dropped.emit(event, paths)
def dragMoveEvent(self, event):
event.acceptProposedAction()
def keyPressEvent(self, event):
if event.key() == Qt.Key_Delete:
self.emit(SIGNAL('delete_format()'))
self.delete_format.emit()
else:
return QListWidget.keyPressEvent(self, event)
@ -162,6 +163,7 @@ class FormatList(QListWidget):
class ImageView(QWidget):
BORDER_WIDTH = 1
cover_changed = pyqtSignal(object)
def __init__(self, parent=None):
QWidget.__init__(self, parent)
@ -201,8 +203,7 @@ class ImageView(QWidget):
if not pmap.isNull():
self.setPixmap(pmap)
event.accept()
self.emit(SIGNAL('cover_changed(PyQt_PyObject)'), open(path,
'rb').read())
self.cover_changed.emit(open(path, 'rb').read())
break
def dragMoveEvent(self, event):
@ -271,7 +272,7 @@ class ImageView(QWidget):
pmap = cb.pixmap(cb.Selection)
if not pmap.isNull():
self.setPixmap(pmap)
self.emit(SIGNAL('cover_changed(PyQt_PyObject)'),
self.cover_changed.emit(
pixmap_to_data(pmap))
# }}}

View File

@ -29,7 +29,6 @@ FIELDS = ['all', 'author_sort', 'authors', 'comments',
'series_index', 'series', 'size', 'tags', 'timestamp', 'title',
'uuid']
#Allowed fields for template
TEMPLATE_ALLOWED_FIELDS = [ 'author_sort', 'authors', 'id', 'isbn', 'pubdate',
'publisher', 'series_index', 'series', 'tags', 'timestamp', 'title', 'uuid' ]
@ -581,7 +580,7 @@ class EPUB_MOBI(CatalogPlugin):
"pipeline to the specified "
"directory. Useful if you are unsure at which stage "
"of the conversion process a bug is occurring.\n"
"Default: '%default'None\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--exclude-book-marker',
default=':',
@ -605,43 +604,42 @@ class EPUB_MOBI(CatalogPlugin):
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--generate-authors',
default=True,
default=False,
dest='generate_authors',
action = 'store_true',
help=_("Include 'Authors' section in catalog."
"This switch is ignored - Books By Author section is always generated."
help=_("Include 'Authors' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--generate-descriptions',
default=True,
default=False,
dest='generate_descriptions',
action = 'store_true',
help=_("Include book descriptions in catalog.\n"
help=_("Include 'Descriptions' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--generate-genres',
default=True,
default=False,
dest='generate_genres',
action = 'store_true',
help=_("Include 'Genres' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--generate-titles',
default=True,
default=False,
dest='generate_titles',
action = 'store_true',
help=_("Include 'Titles' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--generate-series',
default=True,
default=False,
dest='generate_series',
action = 'store_true',
help=_("Include 'Series' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--generate-recently-added',
default=True,
default=False,
dest='generate_recently_added',
action = 'store_true',
help=_("Include 'Recently Added' section in catalog.\n"
@ -976,7 +974,7 @@ class EPUB_MOBI(CatalogPlugin):
self.__thumbWidth = 0
self.__thumbHeight = 0
self.__title = opts.catalog_title
self.__totalSteps = 8.0
self.__totalSteps = 6.0
self.__useSeriesPrefixInTitlesSection = False
self.__verbose = opts.verbose
@ -1014,17 +1012,21 @@ class EPUB_MOBI(CatalogPlugin):
(self.__archive_path, float(cached_thumb_width)))
# Tweak build steps based on optional sections: 1 call for HTML, 1 for NCX
incremental_jobs = 0
if self.opts.generate_authors:
incremental_jobs += 2
if self.opts.generate_titles:
self.__totalSteps += 2
incremental_jobs += 2
if self.opts.generate_recently_added:
self.__totalSteps += 2
incremental_jobs += 2
if self.generateRecentlyRead:
self.__totalSteps += 2
incremental_jobs += 2
if self.opts.generate_series:
self.__totalSteps += 2
incremental_jobs += 2
if self.opts.generate_descriptions:
# +1 thumbs
self.__totalSteps += 3
incremental_jobs += 3
self.__totalSteps += incremental_jobs
# Load section list templates
templates = []
@ -1358,13 +1360,23 @@ class EPUB_MOBI(CatalogPlugin):
if self.opts.generate_descriptions:
self.generateThumbnails()
self.generateHTMLDescriptions()
self.generateHTMLByAuthor()
if self.opts.generate_authors:
self.generateHTMLByAuthor()
if self.opts.generate_titles:
self.generateHTMLByTitle()
if self.opts.generate_series:
self.generateHTMLBySeries()
if self.opts.generate_genres:
self.generateHTMLByTags()
# If this is the only Section, and there are no genres, bail
if self.opts.section_list == ['Genres'] and not self.genres:
error_msg = _("No enabled genres found to catalog.\n")
if not self.opts.cli_environment:
error_msg += "Check 'Excluded genres'\nin E-book options.\n"
self.opts.log.error(error_msg)
self.error.append(_('No books available to catalog'))
self.error.append(error_msg)
return False
if self.opts.generate_recently_added:
self.generateHTMLByDateAdded()
if self.generateRecentlyRead:
@ -1372,7 +1384,8 @@ class EPUB_MOBI(CatalogPlugin):
self.generateOPF()
self.generateNCXHeader()
self.generateNCXByAuthor("Authors")
if self.opts.generate_authors:
self.generateNCXByAuthor("Authors")
if self.opts.generate_titles:
self.generateNCXByTitle("Titles")
if self.opts.generate_series:
@ -1508,7 +1521,6 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
for tag in exclude_tags:
search_terms.append("tag:=%s" % tag)
search_phrase = "not (%s)" % " or ".join(search_terms)
# If a list of ids are provided, don't use search_text
if self.opts.ids:
self.opts.search_text = search_phrase
@ -1879,7 +1891,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
# Link to author
emTag = Tag(soup, "em")
aTag = Tag(soup, "a")
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author']))
if self.opts.generate_authors:
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author']))
aTag.insert(0, NavigableString(book['author']))
emTag.insert(0,aTag)
pBookTag.insert(ptc, emTag)
@ -2149,7 +2162,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
pAuthorTag = Tag(soup, "p")
pAuthorTag['class'] = "author_index"
aTag = Tag(soup, "a")
aTag['name'] = "%s" % self.generateAuthorAnchor(current_author)
if self.opts.generate_authors:
aTag['name'] = "%s" % self.generateAuthorAnchor(current_author)
aTag.insert(0,NavigableString(current_author))
pAuthorTag.insert(0,aTag)
divTag.insert(dtc,pAuthorTag)
@ -2276,7 +2290,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
# Link to author
emTag = Tag(soup, "em")
aTag = Tag(soup, "a")
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author']))
if self.opts.generate_authors:
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author']))
aTag.insert(0, NavigableString(new_entry['author']))
emTag.insert(0,aTag)
pBookTag.insert(ptc, emTag)
@ -2425,7 +2440,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
# Link to author
emTag = Tag(soup, "em")
aTag = Tag(soup, "a")
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author']))
if self.opts.generate_authors:
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author']))
aTag.insert(0, NavigableString(new_entry['author']))
emTag.insert(0,aTag)
pBookTag.insert(ptc, emTag)
@ -2473,7 +2489,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
# Link to author
emTag = Tag(soup, "em")
aTag = Tag(soup, "a")
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author']))
if self.opts.generate_authors:
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author']))
aTag.insert(0, NavigableString(new_entry['author']))
emTag.insert(0,aTag)
pBookTag.insert(ptc, emTag)
@ -2692,7 +2709,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
# Link to author
aTag = Tag(soup, "a")
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor",
if self.opts.generate_authors:
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor",
self.generateAuthorAnchor(escape(' & '.join(book['authors']))))
aTag.insert(0, NavigableString(' &amp; '.join(book['authors'])))
pBookTag.insert(ptc, aTag)
@ -2776,14 +2794,16 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
genre_list.append(tag_list)
if self.opts.verbose:
self.opts.log.info(" Genre summary: %d active genre tags used in generating catalog with %d titles" %
if len(genre_list):
self.opts.log.info(" Genre summary: %d active genre tags used in generating catalog with %d titles" %
(len(genre_list), len(self.booksByTitle)))
for genre in genre_list:
for key in genre:
self.opts.log.info(" %s: %d %s" % (self.getFriendlyGenreTag(key),
len(genre[key]),
'titles' if len(genre[key]) > 1 else 'title'))
for genre in genre_list:
for key in genre:
self.opts.log.info(" %s: %d %s" % (self.getFriendlyGenreTag(key),
len(genre[key]),
'titles' if len(genre[key]) > 1 else 'title'))
# Write the results
# genre_list = [ {friendly_tag:[{book},{book}]}, {friendly_tag:[{book},{book}]}, ...]
@ -3074,10 +3094,36 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
textTag.insert(0, NavigableString(self.title))
navLabelTag.insert(0, textTag)
navPointTag.insert(0, navLabelTag)
contentTag = Tag(soup, 'content')
#contentTag['src'] = "content/book_%d.html" % int(self.booksByTitle[0]['id'])
contentTag['src'] = "content/ByAlphaAuthor.html"
navPointTag.insert(1, contentTag)
if self.opts.generate_authors:
contentTag = Tag(soup, 'content')
contentTag['src'] = "content/ByAlphaAuthor.html"
navPointTag.insert(1, contentTag)
elif self.opts.generate_titles:
contentTag = Tag(soup, 'content')
contentTag['src'] = "content/ByAlphaTitle.html"
navPointTag.insert(1, contentTag)
elif self.opts.generate_series:
contentTag = Tag(soup, 'content')
contentTag['src'] = "content/BySeries.html"
navPointTag.insert(1, contentTag)
elif self.opts.generate_genres:
contentTag = Tag(soup, 'content')
#contentTag['src'] = "content/ByGenres.html"
contentTag['src'] = "%s" % self.genres[0]['file']
navPointTag.insert(1, contentTag)
elif self.opts.generate_recently_added:
contentTag = Tag(soup, 'content')
contentTag['src'] = "content/ByDateAdded.html"
navPointTag.insert(1, contentTag)
else:
# Descriptions only
sort_descriptions_by = self.booksByAuthor if self.opts.sort_descriptions_by_author \
else self.booksByTitle
contentTag = Tag(soup, 'content')
contentTag['src'] = "content/book_%d.html" % int(sort_descriptions_by[0]['id'])
navPointTag.insert(1, contentTag)
cmiTag = Tag(soup, '%s' % 'calibre:meta-img')
cmiTag['name'] = "mastheadImage"
cmiTag['src'] = "images/mastheadImage.gif"
@ -3085,7 +3131,6 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
navMapTag.insert(0,navPointTag)
ncx.insert(0,navMapTag)
self.ncxSoup = soup
def generateNCXDescriptions(self, tocTitle):
@ -3871,7 +3916,6 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
# Add this section to the body
body.insert(btc, navPointTag)
btc += 1
self.ncxSoup = ncx_soup
def writeNCX(self):
@ -4015,12 +4059,34 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
# Remove the special marker tags from the database's tag list,
# return sorted list of normalized genre tags
def format_tag_list(tags, indent=5, line_break=70, header='Tag list'):
def next_tag(sorted_tags):
for (i, tag) in enumerate(sorted_tags):
if i < len(tags) - 1:
yield tag + ", "
else:
yield tag
ans = '%s%d %s:\n' % (' ' * indent, len(tags), header)
ans += ' ' * (indent + 1)
out_str = ''
sorted_tags = sorted(tags)
for tag in next_tag(sorted_tags):
out_str += tag
if len(out_str) >= line_break:
ans += out_str + '\n'
out_str = ' ' * (indent + 1)
return ans + out_str
normalized_tags = []
friendly_tags = []
excluded_tags = []
for tag in tags:
if tag[0] in self.markerTags:
if tag in self.markerTags:
excluded_tags.append(tag)
continue
if re.search(self.opts.exclude_genre, tag):
excluded_tags.append(tag)
continue
if tag == ' ':
continue
@ -4039,32 +4105,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
if genre_tags_dict[key] == normalized:
self.opts.log.warn(" %s" % key)
if self.verbose:
def next_tag(tags):
for (i, tag) in enumerate(tags):
if i < len(tags) - 1:
yield tag + ", "
else:
yield tag
self.opts.log.info(u' %d genre tags in database (excluding genres matching %s):' % \
(len(genre_tags_dict), self.opts.exclude_genre))
# Display friendly/normalized genres
# friendly => normalized
if False:
sorted_tags = ['%s => %s' % (key, genre_tags_dict[key]) for key in sorted(genre_tags_dict.keys())]
for tag in next_tag(sorted_tags):
self.opts.log(u' %s' % tag)
else:
sorted_tags = ['%s' % (key) for key in sorted(genre_tags_dict.keys())]
out_str = ''
line_break = 70
for tag in next_tag(sorted_tags):
out_str += tag
if len(out_str) >= line_break:
self.opts.log.info(' %s' % out_str)
out_str = ''
self.opts.log.info(' %s' % out_str)
self.opts.log.info('%s' % format_tag_list(genre_tags_dict, header="enabled genre tags in database"))
self.opts.log.info('%s' % format_tag_list(excluded_tags, header="excluded genre tags"))
return genre_tags_dict
@ -4140,7 +4182,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
pAuthorTag = Tag(soup, "p")
pAuthorTag['class'] = "author_index"
aTag = Tag(soup, "a")
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author']))
if self.opts.generate_authors:
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author']))
aTag.insert(0, book['author'])
pAuthorTag.insert(0,aTag)
divTag.insert(dtc,pAuthorTag)
@ -4371,7 +4414,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
# Insert the author link (always)
aTag = body.find('a', attrs={'class':'author'})
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor",
if self.opts.generate_authors:
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor",
self.generateAuthorAnchor(book['author']))
if publisher == ' ':
@ -4860,6 +4904,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
opts.basename = "Catalog"
opts.cli_environment = not hasattr(opts,'sync')
# Hard-wired to always sort descriptions by author, with series after non-series
opts.sort_descriptions_by_author = True
build_log = []
@ -4898,14 +4944,13 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
if opts_dict['ids']:
build_log.append(" book count: %d" % len(opts_dict['ids']))
'''
sections_list = []
if opts.generate_authors:
sections_list.append('Authors')
'''
sections_list = ['Authors']
if opts.generate_titles:
sections_list.append('Titles')
if opts.generate_series:
sections_list.append('Series')
if opts.generate_genres:
sections_list.append('Genres')
if opts.generate_recently_added:
@ -4913,7 +4958,27 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
if opts.generate_descriptions:
sections_list.append('Descriptions')
build_log.append(u" Sections: %s" % ', '.join(sections_list))
if not sections_list:
if opts.cli_environment:
opts.log.warn('*** No Section switches specified, enabling all Sections ***')
opts.generate_authors = True
opts.generate_titles = True
opts.generate_series = True
opts.generate_genres = True
opts.generate_recently_added = True
opts.generate_descriptions = True
sections_list = ['Authors','Titles','Series','Genres','Recently Added','Descriptions']
else:
opts.log.warn('\n*** No enabled Sections, terminating catalog generation ***')
return ["No Included Sections","No enabled Sections.\nCheck E-book options tab\n'Included sections'\n"]
if opts.fmt == 'mobi' and sections_list == ['Descriptions']:
warning = _("\n*** Adding 'By Authors' Section required for MOBI output ***")
opts.log.warn(warning)
sections_list.insert(0,'Authors')
opts.generate_authors = True
opts.log(u" Sections: %s" % ', '.join(sections_list))
opts.section_list = sections_list
# Limit thumb_width to 1.0" - 2.0"
try:
@ -4948,6 +5013,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
# Launch the Catalog builder
catalog = self.CatalogBuilder(db, opts, self, report_progress=notification)
if opts.verbose:
log.info(" Begin catalog source generation")
catalog.createDirectoryStructure()
@ -4959,7 +5025,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
if catalog_source_built:
log.info(" Completed catalog source generation\n")
else:
log.warn(" *** Errors during catalog generation, check log for details ***")
log.error(" *** Terminated catalog generation, check log for details ***")
if catalog_source_built:
recommendations = []

View File

@ -260,11 +260,11 @@ The Output profile also controls the screen size. This will cause, for example,
Heuristic Processing
---------------------
Heuristic Processing provides a variety of functions which can be used that try to detect and correct
Heuristic Processing provides a variety of functions which can be used to try and detect and correct
common problems in poorly formatted input documents. Use these functions if your input document suffers
from bad formatting. Because these functions rely on common patterns, be aware that in some cases an
from poor formatting. Because these functions rely on common patterns, be aware that in some cases an
option may lead to worse results, so use with care. As an example, several of these options will
remove all non-breaking-space entities.
remove all non-breaking-space entities, or may include false positive matches relating to the function.
:guilabel:`Enable heuristic processing`
This option activates |app|'s Heuristic Processing stage of the conversion pipeline.
@ -283,7 +283,7 @@ remove all non-breaking-space entities.
correction, then this value should be reduced to somewhere between 0.1 and 0.2.
:guilabel:`Detect and markup unformatted chapter headings and sub headings`
If your document does not have Chapter Markers and titles formatted differently from the rest of the text,
If your document does not have chapter headings and titles formatted differently from the rest of the text,
|app| can use this option to attempt detection them and surround them with heading tags. <h2> tags are used
for chapter headings; <h3> tags are used for any titles that are detected.
@ -331,21 +331,23 @@ remove all non-breaking-space entities.
Some documents use a convention of defining text indents using non-breaking space entities. When this option is enabled |app| will
attempt to detect this sort of formatting and convert them to a 3% text indent using css.
.. search-replace:
.. _search-replace:
Search & Replace
---------------------
These options are useful primarily for conversion of PDF documents. Often, the conversion leaves
behind page headers and footers in the text. These options use regular expressions to try and detect
the headers and footers and remove them. Remember that they operate on the intermediate XHTML produced
by the conversion pipeline. There is also a wizard to help you customize the regular expressions for
your document. These options can also be used for generic search and replace of any content by additionally
specifying a replacement expression.
These options are useful primarily for conversion of PDF documents or OCR conversions, though they can
also be used to fix many document specific problems. As an example, some conversions can leaves behind page
headers and footers in the text. These options use regular expressions to try and detect headers, footers,
or other arbitrary text and remove or replace them. Remember that they operate on the intermediate XHTML produced
by the conversion pipeline. There is a wizard to help you customize the regular expressions for
your document. Click the magic wand beside the expression box, and click the 'Test' button after composing
your search expression. Successful matches will be highlighted in Yellow.
The search works by using a python regular expression. All matched text is simply removed from
the document or replaced using the replacement pattern. You can learn more about regular expressions and
their syntax at :ref:`regexptutorial`.
the document or replaced using the replacement pattern. The replacement pattern is optional, if left blank
then text matching the search pattern will be deleted from the document. You can learn more about regular expressions
and their syntax at :ref:`regexptutorial`.
.. _structure-detection:

View File

@ -108,8 +108,8 @@ 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 `the calibre website <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.
* 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, the plugin icon next to it should be green when it 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 `the calibre bug tracker <http://bugs.calibre-ebook.com>`_.
How does |app| manage collections on my SONY reader?
@ -441,7 +441,7 @@ menu, choose "Validate fonts".
I downloaded the installer, but it is not working?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Downloading from the internet can sometimes result in a corrupted download. If the |app| installer you downloaded is not opening, try downloading it again. If re-downloading it does not work, download it from `an alternate location <http://sourceforge.net/projects/calibre/files/>`_. If the installer still doesn't work, then something on your computer is preventing it from running. Best place to ask for more help is in the `forums <http://www.mobileread.com/forums/usercp.php>`_.
Downloading from the internet can sometimes result in a corrupted download. If the |app| installer you downloaded is not opening, try downloading it again. If re-downloading it does not work, download it from `an alternate location <http://sourceforge.net/projects/calibre/files/>`_. If the installer still doesn't work, then something on your computer is preventing it from running. Try rebooting your computer and running a registry cleaner like `Wise registry cleaner <http://www.wisecleaner.com>`_. Best place to ask for more help is in the `forums <http://www.mobileread.com/forums/usercp.php>`_.
My antivirus program claims |app| is a virus/trojan?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -104,12 +104,12 @@ class cmd_commit(_cmd_commit):
def close_bug(self, bug, action, url, config):
print 'Closing bug #%s'% bug
nick = config.get_nickname()
#nick = config.get_nickname()
suffix = config.get_user_option('bug_close_comment')
if suffix is None:
suffix = 'The fix will be in the next release.'
action = action+'ed'
msg = '%s in branch %s. %s'%(action, nick, suffix)
msg = '%s in branch %s. %s'%(action, 'lp:calibre', suffix)
msg = msg.replace('Fixesed', 'Fixed')
server = xmlrpclib.ServerProxy(url)
server.ticket.update(int(bug), msg,