mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
merge from trunk
This commit is contained in:
commit
c5b93d597a
@ -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
|
||||
|
@ -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})
|
||||
|
@ -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] },
|
||||
|
@ -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']
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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 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:
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
984
src/calibre/gui2/metadata/basic_widgets.py
Normal file
984
src/calibre/gui2/metadata/basic_widgets.py
Normal file
@ -0,0 +1,984 @@
|
||||
#!/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 textwrap, re, os
|
||||
|
||||
from PyQt4.Qt import Qt, QDateEdit, QDate, \
|
||||
QIcon, QToolButton, QWidget, QLabel, QGridLayout, \
|
||||
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \
|
||||
QPushButton, QSpinBox, QMessageBox, QLineEdit
|
||||
|
||||
from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
|
||||
EnComboBox, FormatList, ImageView, CompleteLineEdit
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.config import tweaks
|
||||
from calibre.ebooks.metadata import title_sort, authors_to_string, \
|
||||
string_to_authors, check_isbn
|
||||
from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \
|
||||
choose_files, error_dialog, choose_images, question_dialog
|
||||
from calibre.utils.date import local_tz, qt_to_dt
|
||||
from calibre import strftime
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.customize.ui import run_plugins_on_import
|
||||
from calibre.utils.date import utcfromtimestamp
|
||||
from calibre.gui2.comments_editor import Editor
|
||||
from calibre.library.comments import comments_to_html
|
||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
||||
|
||||
'''
|
||||
The interface common to all widgets used to set basic metadata
|
||||
class BasicMetadataWidget(object):
|
||||
|
||||
LABEL = "label text"
|
||||
|
||||
def initialize(self, db, id_):
|
||||
pass
|
||||
|
||||
def commit(self, db, id_):
|
||||
return True
|
||||
|
||||
@dynamic_property
|
||||
def current_val(self):
|
||||
# Present in most but not all basic metadata widgets
|
||||
def fget(self):
|
||||
return None
|
||||
def fset(self, val):
|
||||
pass
|
||||
return property(fget=fget, fset=fset)
|
||||
'''
|
||||
|
||||
# Title {{{
|
||||
class TitleEdit(EnLineEdit):
|
||||
|
||||
TITLE_ATTR = 'title'
|
||||
COMMIT = True
|
||||
TOOLTIP = _('Change the title of this book')
|
||||
LABEL = _('&Title:')
|
||||
|
||||
def __init__(self, parent):
|
||||
self.dialog = parent
|
||||
EnLineEdit.__init__(self, parent)
|
||||
self.setToolTip(self.TOOLTIP)
|
||||
self.setWhatsThis(self.TOOLTIP)
|
||||
|
||||
def get_default(self):
|
||||
return _('Unknown')
|
||||
|
||||
def initialize(self, db, id_):
|
||||
title = getattr(db, self.TITLE_ATTR)(id_, index_is_id=True)
|
||||
self.current_val = title
|
||||
self.original_val = self.current_val
|
||||
|
||||
def commit(self, db, id_):
|
||||
title = self.current_val
|
||||
if self.COMMIT:
|
||||
getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False)
|
||||
else:
|
||||
getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False,
|
||||
commit=False)
|
||||
return True
|
||||
|
||||
@dynamic_property
|
||||
def current_val(self):
|
||||
|
||||
def fget(self):
|
||||
title = unicode(self.text()).strip()
|
||||
if not title:
|
||||
title = self.get_default()
|
||||
return title
|
||||
|
||||
def fset(self, val):
|
||||
if hasattr(val, 'strip'):
|
||||
val = val.strip()
|
||||
if not val:
|
||||
val = self.get_default()
|
||||
self.setText(val)
|
||||
self.setCursorPosition(0)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
class TitleSortEdit(TitleEdit):
|
||||
|
||||
TITLE_ATTR = 'title_sort'
|
||||
COMMIT = False
|
||||
TOOLTIP = _('Specify how this book should be sorted when by title.'
|
||||
' For example, The Exorcist might be sorted as Exorcist, The.')
|
||||
LABEL = _('Title &sort:')
|
||||
|
||||
def __init__(self, parent, title_edit, autogen_button):
|
||||
TitleEdit.__init__(self, parent)
|
||||
self.title_edit = title_edit
|
||||
|
||||
base = self.TOOLTIP
|
||||
ok_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+
|
||||
_(' The green color indicates that the current '
|
||||
'title sort matches the current title'))
|
||||
bad_tooltip = '<p>'+textwrap.fill(base + '<br><br>'+
|
||||
_(' The red color warns that the current '
|
||||
'title sort does not match the current title. '
|
||||
'No action is required if this is what you want.'))
|
||||
self.tooltips = (ok_tooltip, bad_tooltip)
|
||||
|
||||
self.title_edit.textChanged.connect(self.update_state)
|
||||
self.textChanged.connect(self.update_state)
|
||||
|
||||
autogen_button.clicked.connect(self.auto_generate)
|
||||
self.update_state()
|
||||
|
||||
def update_state(self, *args):
|
||||
ts = title_sort(self.title_edit.current_val)
|
||||
normal = ts == self.current_val
|
||||
if normal:
|
||||
col = 'rgb(0, 255, 0, 20%)'
|
||||
else:
|
||||
col = 'rgb(255, 0, 0, 20%)'
|
||||
self.setStyleSheet('QLineEdit { color: black; '
|
||||
'background-color: %s; }'%col)
|
||||
tt = self.tooltips[0 if normal else 1]
|
||||
self.setToolTip(tt)
|
||||
self.setWhatsThis(tt)
|
||||
|
||||
def auto_generate(self, *args):
|
||||
self.current_val = title_sort(self.title_edit.current_val)
|
||||
|
||||
# }}}
|
||||
|
||||
# Authors {{{
|
||||
class AuthorsEdit(CompleteComboBox):
|
||||
|
||||
TOOLTIP = ''
|
||||
LABEL = _('&Author(s):')
|
||||
|
||||
def __init__(self, parent):
|
||||
self.dialog = parent
|
||||
CompleteComboBox.__init__(self, parent)
|
||||
self.setToolTip(self.TOOLTIP)
|
||||
self.setWhatsThis(self.TOOLTIP)
|
||||
self.setEditable(True)
|
||||
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
|
||||
|
||||
def get_default(self):
|
||||
return _('Unknown')
|
||||
|
||||
def initialize(self, db, id_):
|
||||
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.addItem(authors_to_string(name))
|
||||
|
||||
self.set_separator('&')
|
||||
self.set_space_before_sep(True)
|
||||
self.update_items_cache(db.all_author_names())
|
||||
|
||||
au = db.authors(id_, index_is_id=True)
|
||||
if not au:
|
||||
au = _('Unknown')
|
||||
self.current_val = [a.strip().replace('|', ',') for a in au.split(',')]
|
||||
self.original_val = self.current_val
|
||||
|
||||
def commit(self, db, id_):
|
||||
authors = self.current_val
|
||||
db.set_authors(id_, authors, notify=False)
|
||||
return True
|
||||
|
||||
@dynamic_property
|
||||
def current_val(self):
|
||||
|
||||
def fget(self):
|
||||
au = unicode(self.text()).strip()
|
||||
if not au:
|
||||
au = self.get_default()
|
||||
return string_to_authors(au)
|
||||
|
||||
def fset(self, val):
|
||||
if not val:
|
||||
val = [self.get_default()]
|
||||
self.setEditText(' & '.join([x.strip() for x in val]))
|
||||
self.lineEdit().setCursorPosition(0)
|
||||
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
class AuthorSortEdit(EnLineEdit):
|
||||
|
||||
TOOLTIP = _('Specify how the author(s) of this book should be sorted. '
|
||||
'For example Charles Dickens should be sorted as Dickens, '
|
||||
'Charles.\nIf the box is colored green, then text matches '
|
||||
'the individual author\'s sort strings. If it is colored '
|
||||
'red, then the authors and this text do not match.')
|
||||
LABEL = _('Author s&ort:')
|
||||
|
||||
def __init__(self, parent, authors_edit, autogen_button, db):
|
||||
EnLineEdit.__init__(self, parent)
|
||||
self.authors_edit = authors_edit
|
||||
self.db = db
|
||||
|
||||
base = self.TOOLTIP
|
||||
ok_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+
|
||||
_(' The green color indicates that the current '
|
||||
'author sort matches the current author'))
|
||||
bad_tooltip = '<p>'+textwrap.fill(base + '<br><br>'+
|
||||
_(' The red color indicates that the current '
|
||||
'author sort does not match the current author. '
|
||||
'No action is required if this is what you want.'))
|
||||
self.tooltips = (ok_tooltip, bad_tooltip)
|
||||
|
||||
self.authors_edit.editTextChanged.connect(self.update_state)
|
||||
self.textChanged.connect(self.update_state)
|
||||
|
||||
autogen_button.clicked.connect(self.auto_generate)
|
||||
self.update_state()
|
||||
|
||||
@dynamic_property
|
||||
def current_val(self):
|
||||
|
||||
def fget(self):
|
||||
return unicode(self.text()).strip()
|
||||
|
||||
def fset(self, val):
|
||||
if not val:
|
||||
val = ''
|
||||
self.setText(val.strip())
|
||||
self.setCursorPosition(0)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def update_state(self, *args):
|
||||
au = unicode(self.authors_edit.text())
|
||||
au = re.sub(r'\s+et al\.$', '', au)
|
||||
au = self.db.author_sort_from_authors(string_to_authors(au))
|
||||
|
||||
normal = au == self.current_val
|
||||
if normal:
|
||||
col = 'rgb(0, 255, 0, 20%)'
|
||||
else:
|
||||
col = 'rgb(255, 0, 0, 20%)'
|
||||
self.setStyleSheet('QLineEdit { color: black; '
|
||||
'background-color: %s; }'%col)
|
||||
tt = self.tooltips[0 if normal else 1]
|
||||
self.setToolTip(tt)
|
||||
self.setWhatsThis(tt)
|
||||
|
||||
def auto_generate(self, *args):
|
||||
au = unicode(self.authors_edit.text())
|
||||
au = re.sub(r'\s+et al\.$', '', au)
|
||||
authors = string_to_authors(au)
|
||||
self.current_val = self.db.author_sort_from_authors(authors)
|
||||
|
||||
def initialize(self, db, id_):
|
||||
self.current_val = db.author_sort(id_, index_is_id=True)
|
||||
|
||||
def commit(self, db, id_):
|
||||
aus = self.current_val
|
||||
db.set_author_sort(id_, aus, notify=False, commit=False)
|
||||
return True
|
||||
|
||||
# }}}
|
||||
|
||||
# Series {{{
|
||||
class SeriesEdit(EnComboBox):
|
||||
|
||||
TOOLTIP = _('List of known series. You can add new series.')
|
||||
LABEL = _('&Series:')
|
||||
|
||||
def __init__(self, parent):
|
||||
EnComboBox.__init__(self, parent)
|
||||
self.dialog = parent
|
||||
self.setSizeAdjustPolicy(
|
||||
self.AdjustToMinimumContentsLengthWithIcon)
|
||||
self.setToolTip(self.TOOLTIP)
|
||||
self.setWhatsThis(self.TOOLTIP)
|
||||
self.setEditable(True)
|
||||
|
||||
@dynamic_property
|
||||
def current_val(self):
|
||||
|
||||
def fget(self):
|
||||
return unicode(self.currentText()).strip()
|
||||
|
||||
def fset(self, val):
|
||||
if not val:
|
||||
val = ''
|
||||
self.setEditText(val.strip())
|
||||
self.setCursorPosition(0)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def initialize(self, db, id_):
|
||||
all_series = db.all_series()
|
||||
all_series.sort(key=lambda x : sort_key(x[1]))
|
||||
series_id = db.series_id(id_, index_is_id=True)
|
||||
idx, c = None, 0
|
||||
for i in all_series:
|
||||
id, name = i
|
||||
if id == series_id:
|
||||
idx = c
|
||||
self.addItem(name)
|
||||
c += 1
|
||||
|
||||
self.lineEdit().setText('')
|
||||
if idx is not None:
|
||||
self.setCurrentIndex(idx)
|
||||
self.original_val = self.current_val
|
||||
|
||||
def commit(self, db, id_):
|
||||
series = self.current_val
|
||||
db.set_series(id_, series, notify=False, commit=True)
|
||||
return True
|
||||
|
||||
class SeriesIndexEdit(QDoubleSpinBox):
|
||||
|
||||
TOOLTIP = ''
|
||||
LABEL = _('&Number:')
|
||||
|
||||
def __init__(self, parent, series_edit):
|
||||
QDoubleSpinBox.__init__(self, parent)
|
||||
self.dialog = parent
|
||||
self.db = self.original_series_name = None
|
||||
self.setMaximum(1000000)
|
||||
self.series_edit = series_edit
|
||||
series_edit.currentIndexChanged.connect(self.enable)
|
||||
series_edit.editTextChanged.connect(self.enable)
|
||||
series_edit.lineEdit().editingFinished.connect(self.increment)
|
||||
self.enable()
|
||||
|
||||
def enable(self, *args):
|
||||
self.setEnabled(bool(self.series_edit.current_val))
|
||||
|
||||
@dynamic_property
|
||||
def current_val(self):
|
||||
|
||||
def fget(self):
|
||||
return self.value()
|
||||
|
||||
def fset(self, val):
|
||||
if val is None:
|
||||
val = 1.0
|
||||
val = float(val)
|
||||
self.setValue(val)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def initialize(self, db, id_):
|
||||
self.db = db
|
||||
if self.series_edit.current_val:
|
||||
val = db.series_index(id_, index_is_id=True)
|
||||
else:
|
||||
val = 1.0
|
||||
self.current_val = val
|
||||
self.original_val = self.current_val
|
||||
self.original_series_name = self.series_edit.original_val
|
||||
|
||||
def commit(self, db, id_):
|
||||
db.set_series_index(id_, self.current_val, notify=False, commit=False)
|
||||
return True
|
||||
|
||||
def increment(self):
|
||||
if self.db is not None:
|
||||
try:
|
||||
series = self.series_edit.current_val
|
||||
if series and series != self.original_series_name:
|
||||
ns = 1.0
|
||||
if tweaks['series_index_auto_increment'] != 'const':
|
||||
ns = self.db.get_next_series_num_for(series)
|
||||
self.current_val = ns
|
||||
self.original_series_name = series
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
class BuddyLabel(QLabel): # {{{
|
||||
|
||||
def __init__(self, buddy):
|
||||
QLabel.__init__(self, buddy.LABEL)
|
||||
self.setBuddy(buddy)
|
||||
self.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
|
||||
# }}}
|
||||
|
||||
class Format(QListWidgetItem): # {{{
|
||||
|
||||
def __init__(self, parent, ext, size, path=None, timestamp=None):
|
||||
self.path = path
|
||||
self.ext = ext
|
||||
self.size = float(size)/(1024*1024)
|
||||
text = '%s (%.2f MB)'%(self.ext.upper(), self.size)
|
||||
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
|
||||
text, parent, QListWidgetItem.UserType)
|
||||
if timestamp is not None:
|
||||
ts = timestamp.astimezone(local_tz)
|
||||
t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple())
|
||||
text = _('Last modified: %s')%t
|
||||
self.setToolTip(text)
|
||||
self.setStatusTip(text)
|
||||
|
||||
# }}}
|
||||
|
||||
class FormatsManager(QWidget): # {{{
|
||||
|
||||
def __init__(self, parent):
|
||||
QWidget.__init__(self, parent)
|
||||
self.dialog = parent
|
||||
self.changed = False
|
||||
|
||||
self.l = l = QGridLayout()
|
||||
self.setLayout(l)
|
||||
self.cover_from_format_button = QToolButton(self)
|
||||
self.cover_from_format_button.setToolTip(
|
||||
_('Set the cover for the book from the selected format'))
|
||||
self.cover_from_format_button.setIcon(QIcon(I('book.png')))
|
||||
self.cover_from_format_button.setIconSize(QSize(32, 32))
|
||||
|
||||
self.metadata_from_format_button = QToolButton(self)
|
||||
self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png')))
|
||||
self.metadata_from_format_button.setIconSize(QSize(32, 32))
|
||||
# TODO: Implement the *_from_format buttons
|
||||
|
||||
self.add_format_button = QToolButton(self)
|
||||
self.add_format_button.setIcon(QIcon(I('add_book.png')))
|
||||
self.add_format_button.setIconSize(QSize(32, 32))
|
||||
self.add_format_button.clicked.connect(self.add_format)
|
||||
|
||||
self.remove_format_button = QToolButton(self)
|
||||
self.remove_format_button.setIcon(QIcon(I('trash.png')))
|
||||
self.remove_format_button.setIconSize(QSize(32, 32))
|
||||
self.remove_format_button.clicked.connect(self.remove_format)
|
||||
|
||||
self.formats = FormatList(self)
|
||||
self.formats.setAcceptDrops(True)
|
||||
self.formats.formats_dropped.connect(self.formats_dropped)
|
||||
self.formats.delete_format.connect(self.remove_format)
|
||||
self.formats.itemDoubleClicked.connect(self.show_format)
|
||||
self.formats.setDragDropMode(self.formats.DropOnly)
|
||||
self.formats.setIconSize(QSize(32, 32))
|
||||
self.formats.setMaximumWidth(200)
|
||||
|
||||
l.addWidget(self.cover_from_format_button, 0, 0, 1, 1)
|
||||
l.addWidget(self.metadata_from_format_button, 2, 0, 1, 1)
|
||||
l.addWidget(self.add_format_button, 0, 2, 1, 1)
|
||||
l.addWidget(self.remove_format_button, 2, 2, 1, 1)
|
||||
l.addWidget(self.formats, 0, 1, 3, 1)
|
||||
|
||||
|
||||
|
||||
def initialize(self, db, id_):
|
||||
self.changed = False
|
||||
exts = db.formats(id_, index_is_id=True)
|
||||
if exts:
|
||||
exts = exts.split(',')
|
||||
for ext in exts:
|
||||
if not ext:
|
||||
ext = ''
|
||||
size = db.sizeof_format(id_, ext, index_is_id=True)
|
||||
timestamp = db.format_last_modified(id_, ext)
|
||||
if size is None:
|
||||
continue
|
||||
Format(self.formats, ext, size, timestamp=timestamp)
|
||||
|
||||
def commit(self, db, id_):
|
||||
if not self.changed:
|
||||
return True
|
||||
old_extensions, new_extensions, paths = set(), set(), {}
|
||||
for row in range(self.formats.count()):
|
||||
fmt = self.formats.item(row)
|
||||
ext, path = fmt.ext.lower(), fmt.path
|
||||
if 'unknown' in ext.lower():
|
||||
ext = None
|
||||
if path:
|
||||
new_extensions.add(ext)
|
||||
paths[ext] = path
|
||||
else:
|
||||
old_extensions.add(ext)
|
||||
for ext in new_extensions:
|
||||
db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False,
|
||||
index_is_id=True)
|
||||
db_extensions = set([f.lower() for f in db.formats(id_,
|
||||
index_is_id=True).split(',')])
|
||||
extensions = new_extensions.union(old_extensions)
|
||||
for ext in db_extensions:
|
||||
if ext not in extensions:
|
||||
db.remove_format(id_, ext, notify=False, index_is_id=True)
|
||||
|
||||
self.changed = False
|
||||
return True
|
||||
|
||||
def add_format(self, *args):
|
||||
files = choose_files(self, 'add formats dialog',
|
||||
_("Choose formats for ") +
|
||||
self.dialog.title.current_val,
|
||||
[(_('Books'), BOOK_EXTENSIONS)])
|
||||
self._add_formats(files)
|
||||
|
||||
def _add_formats(self, paths):
|
||||
added = False
|
||||
if not paths:
|
||||
return added
|
||||
bad_perms = []
|
||||
for _file in paths:
|
||||
_file = os.path.abspath(_file)
|
||||
if not os.access(_file, os.R_OK):
|
||||
bad_perms.append(_file)
|
||||
continue
|
||||
|
||||
nfile = run_plugins_on_import(_file)
|
||||
if nfile is not None:
|
||||
_file = nfile
|
||||
stat = os.stat(_file)
|
||||
size = stat.st_size
|
||||
ext = os.path.splitext(_file)[1].lower().replace('.', '')
|
||||
timestamp = utcfromtimestamp(stat.st_mtime)
|
||||
for row in range(self.formats.count()):
|
||||
fmt = self.formats.item(row)
|
||||
if fmt.ext.lower() == ext:
|
||||
self.formats.takeItem(row)
|
||||
break
|
||||
Format(self.formats, ext, size, path=_file, timestamp=timestamp)
|
||||
self.changed = True
|
||||
added = True
|
||||
if bad_perms:
|
||||
error_dialog(self, _('No permission'),
|
||||
_('You do not have '
|
||||
'permission to read the following files:'),
|
||||
det_msg='\n'.join(bad_perms), show=True)
|
||||
|
||||
return added
|
||||
|
||||
def formats_dropped(self, event, paths):
|
||||
if self._add_formats(paths):
|
||||
event.accept()
|
||||
|
||||
def remove_format(self, *args):
|
||||
rows = self.formats.selectionModel().selectedRows(0)
|
||||
for row in rows:
|
||||
self.formats.takeItem(row.row())
|
||||
self.changed = True
|
||||
|
||||
def show_format(self, item, *args):
|
||||
fmt = item.ext
|
||||
self.dialog.view_format.emit(fmt)
|
||||
|
||||
# }}}
|
||||
|
||||
class Cover(ImageView): # {{{
|
||||
|
||||
def __init__(self, parent):
|
||||
ImageView.__init__(self, parent)
|
||||
self.dialog = parent
|
||||
self._cdata = None
|
||||
self.cover_changed.connect(self.set_pixmap_from_data)
|
||||
|
||||
self.select_cover_button = QPushButton(QIcon(I('document_open.png')),
|
||||
_('&Browse'), parent)
|
||||
self.trim_cover_button = QPushButton(QIcon(I('trim.png')),
|
||||
_('T&rim'), parent)
|
||||
self.remove_cover_button = QPushButton(QIcon(I('trash.png')),
|
||||
_('&Remove'), parent)
|
||||
|
||||
self.select_cover_button.clicked.connect(self.select_cover)
|
||||
self.remove_cover_button.clicked.connect(self.remove_cover)
|
||||
self.trim_cover_button.clicked.connect(self.trim_cover)
|
||||
|
||||
self.download_cover_button = QPushButton(_('Download co&ver'), parent)
|
||||
self.generate_cover_button = QPushButton(_('&Generate cover'), parent)
|
||||
|
||||
self.download_cover_button.clicked.connect(self.download_cover)
|
||||
self.generate_cover_button.clicked.connect(self.generate_cover)
|
||||
|
||||
self.buttons = [self.select_cover_button, self.remove_cover_button,
|
||||
self.trim_cover_button, self.download_cover_button,
|
||||
self.generate_cover_button]
|
||||
|
||||
def select_cover(self, *args):
|
||||
files = choose_images(self, 'change cover dialog',
|
||||
_('Choose cover for ') +
|
||||
self.dialog.title.current_val)
|
||||
if not files:
|
||||
return
|
||||
_file = files[0]
|
||||
if _file:
|
||||
_file = os.path.abspath(_file)
|
||||
if not os.access(_file, os.R_OK):
|
||||
d = error_dialog(self, _('Cannot read'),
|
||||
_('You do not have permission to read the file: ') + _file)
|
||||
d.exec_()
|
||||
return
|
||||
cf, cover = None, None
|
||||
try:
|
||||
cf = open(_file, "rb")
|
||||
cover = cf.read()
|
||||
except IOError, e:
|
||||
d = error_dialog(self, _('Error reading file'),
|
||||
_("<p>There was an error reading from file: <br /><b>")
|
||||
+ _file + "</b></p><br />"+str(e))
|
||||
d.exec_()
|
||||
if cover:
|
||||
orig = self.current_val
|
||||
self.current_val = cover
|
||||
if self.current_val is None:
|
||||
self.current_val = orig
|
||||
error_dialog(self,
|
||||
_("Not a valid picture"),
|
||||
_file + _(" is not a valid picture"), show=True)
|
||||
|
||||
def remove_cover(self, *args):
|
||||
self.current_val = None
|
||||
|
||||
def trim_cover(self, *args):
|
||||
from calibre.utils.magick import Image
|
||||
cdata = self.current_val
|
||||
if not cdata:
|
||||
return
|
||||
im = Image()
|
||||
im.load(cdata)
|
||||
im.trim(10)
|
||||
cdata = im.export('png')
|
||||
self.current_val = cdata
|
||||
|
||||
def download_cover(self, *args):
|
||||
pass # TODO: Implement this
|
||||
|
||||
def generate_cover(self, *args):
|
||||
from calibre.ebooks import calibre_cover
|
||||
from calibre.ebooks.metadata import fmt_sidx
|
||||
from calibre.gui2 import config
|
||||
title = self.dialog.title.current_val
|
||||
author = authors_to_string(self.dialog.authors.current_val)
|
||||
if not title or not author:
|
||||
return error_dialog(self, _('Specify title and author'),
|
||||
_('You must specify a title and author before generating '
|
||||
'a cover'), show=True)
|
||||
series = self.dialog.series.current_val
|
||||
series_string = None
|
||||
if series:
|
||||
series_string = _('Book %s of %s')%(
|
||||
fmt_sidx(self.dialog.series_index.current_val,
|
||||
use_roman=config['use_roman_numerals_for_series_number']), series)
|
||||
self.current_val = calibre_cover(title, author,
|
||||
series_string=series_string)
|
||||
|
||||
def set_pixmap_from_data(self, data):
|
||||
if not data:
|
||||
self.current_val = None
|
||||
return
|
||||
orig = self.current_val
|
||||
self.current_val = data
|
||||
if self.current_val is None:
|
||||
error_dialog(self, _('Invalid cover'),
|
||||
_('Could not change cover as the image is invalid.'),
|
||||
show=True)
|
||||
self.current_val = orig
|
||||
|
||||
def initialize(self, db, id_):
|
||||
self._cdata = None
|
||||
self.current_val = db.cover(id_, index_is_id=True)
|
||||
self.original_val = self.current_val
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
return self.current_val != self.original_val
|
||||
|
||||
@dynamic_property
|
||||
def current_val(self):
|
||||
def fget(self):
|
||||
return self._cdata
|
||||
def fset(self, cdata):
|
||||
self._cdata = None
|
||||
pm = QPixmap()
|
||||
if cdata:
|
||||
pm.loadFromData(cdata)
|
||||
if pm.isNull():
|
||||
pm = QPixmap(I('default_cover.png'))
|
||||
else:
|
||||
self._cdata = cdata
|
||||
self.setPixmap(pm)
|
||||
tt = _('This book has no cover')
|
||||
if self._cdata:
|
||||
tt = _('Cover size: %dx%d pixels') % \
|
||||
(pm.width(), pm.height())
|
||||
self.setToolTip(tt)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def commit(self, db, id_):
|
||||
if self.changed:
|
||||
if self.current_val:
|
||||
db.set_cover(id_, self.current_val, notify=False, commit=False)
|
||||
else:
|
||||
db.remove_cover(id_, notify=False, commit=False)
|
||||
return True
|
||||
|
||||
# }}}
|
||||
|
||||
class CommentsEdit(Editor): # {{{
|
||||
|
||||
@dynamic_property
|
||||
def current_val(self):
|
||||
def fget(self):
|
||||
return self.html
|
||||
def fset(self, val):
|
||||
if not val or not val.strip():
|
||||
val = ''
|
||||
else:
|
||||
val = comments_to_html(val)
|
||||
self.html = val
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def initialize(self, db, id_):
|
||||
self.current_val = db.comments(id_, index_is_id=True)
|
||||
self.original_val = self.current_val
|
||||
|
||||
def commit(self, db, id_):
|
||||
db.set_comment(id_, self.current_val, notify=False, commit=False)
|
||||
return True
|
||||
# }}}
|
||||
|
||||
class RatingEdit(QSpinBox): # {{{
|
||||
LABEL = _('&Rating:')
|
||||
TOOLTIP = _('Rating of this book. 0-5 stars')
|
||||
|
||||
def __init__(self, parent):
|
||||
QSpinBox.__init__(self, parent)
|
||||
self.setToolTip(self.TOOLTIP)
|
||||
self.setWhatsThis(self.TOOLTIP)
|
||||
self.setMaximum(5)
|
||||
self.setSuffix(' ' + _('stars'))
|
||||
|
||||
@dynamic_property
|
||||
def current_val(self):
|
||||
def fget(self):
|
||||
return self.value()
|
||||
def fset(self, val):
|
||||
if val is None:
|
||||
val = 0
|
||||
val = int(val)
|
||||
if val < 0:
|
||||
val = 0
|
||||
if val > 5:
|
||||
val = 5
|
||||
self.setValue(val)
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def initialize(self, db, id_):
|
||||
val = db.rating(id_, index_is_id=True)
|
||||
if val > 0:
|
||||
val = int(val/2.)
|
||||
else:
|
||||
val = 0
|
||||
self.current_val = val
|
||||
self.original_val = self.current_val
|
||||
|
||||
def commit(self, db, id_):
|
||||
db.set_rating(id_, 2*self.current_val, notify=False, commit=False)
|
||||
return True
|
||||
|
||||
# }}}
|
||||
|
||||
class TagsEdit(CompleteLineEdit): # {{{
|
||||
LABEL = _('Ta&gs:')
|
||||
TOOLTIP = '<p>'+_('Tags categorize the book. This is particularly '
|
||||
'useful while searching. <br><br>They can be any words'
|
||||
'or phrases, separated by commas.')
|
||||
|
||||
def __init__(self, parent):
|
||||
CompleteLineEdit.__init__(self, parent)
|
||||
self.setToolTip(self.TOOLTIP)
|
||||
self.setWhatsThis(self.TOOLTIP)
|
||||
|
||||
@dynamic_property
|
||||
def current_val(self):
|
||||
def fget(self):
|
||||
return [x.strip() for x in unicode(self.text()).split(',')]
|
||||
def fset(self, val):
|
||||
if not val:
|
||||
val = []
|
||||
self.setText(', '.join([x.strip() for x in val]))
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def initialize(self, db, id_):
|
||||
tags = db.tags(id_, index_is_id=True)
|
||||
tags = tags.split(',') if tags else []
|
||||
self.current_val = tags
|
||||
self.update_items_cache(db.all_tags())
|
||||
self.original_val = self.current_val
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
return self.current_val != self.original_val
|
||||
|
||||
def edit(self, db, id_):
|
||||
if self.changed:
|
||||
if question_dialog(self, _('Tags changed'),
|
||||
_('You have changed the tags. In order to use the tags'
|
||||
' editor, you must either discard or apply these '
|
||||
'changes'), show_copy_button=False,
|
||||
buttons=QMessageBox.Apply|QMessageBox.Discard,
|
||||
yes_button=QMessageBox.Apply):
|
||||
self.commit(db, id_)
|
||||
db.commit()
|
||||
self.original_val = self.current_val
|
||||
else:
|
||||
self.current_val = self.original_val
|
||||
d = TagEditor(self, db, id_)
|
||||
if d.exec_() == TagEditor.Accepted:
|
||||
self.current_val = d.tags
|
||||
self.update_items_cache(db.all_tags())
|
||||
|
||||
|
||||
def commit(self, db, id_):
|
||||
db.set_tags(id_, self.current_val, notify=False, commit=False)
|
||||
return True
|
||||
|
||||
# }}}
|
||||
|
||||
class ISBNEdit(QLineEdit): # {{{
|
||||
LABEL = _('IS&BN:')
|
||||
|
||||
def __init__(self, parent):
|
||||
QLineEdit.__init__(self, parent)
|
||||
self.pat = re.compile(r'[^0-9a-zA-Z]')
|
||||
self.textChanged.connect(self.validate)
|
||||
|
||||
@dynamic_property
|
||||
def current_val(self):
|
||||
def fget(self):
|
||||
return self.pat.sub('', unicode(self.text()).strip())
|
||||
def fset(self, val):
|
||||
if not val:
|
||||
val = ''
|
||||
self.setText(val.strip())
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def initialize(self, db, id_):
|
||||
self.current_val = db.isbn(id_, index_is_id=True)
|
||||
self.original_val = self.current_val
|
||||
|
||||
def commit(self, db, id_):
|
||||
db.set_isbn(id_, self.current_val, notify=False, commit=False)
|
||||
return True
|
||||
|
||||
def validate(self, *args):
|
||||
isbn = self.current_val
|
||||
tt = _('This ISBN number is valid')
|
||||
if not isbn:
|
||||
col = 'rgba(0,255,0,0%)'
|
||||
elif check_isbn(isbn) is not None:
|
||||
col = 'rgba(0,255,0,20%)'
|
||||
else:
|
||||
col = 'rgba(255,0,0,20%)'
|
||||
tt = _('This ISBN number is invalid')
|
||||
self.setToolTip(tt)
|
||||
self.setStyleSheet('QLineEdit { background-color: %s }'%col)
|
||||
|
||||
# }}}
|
||||
|
||||
class PublisherEdit(EnComboBox): # {{{
|
||||
LABEL = _('&Publisher:')
|
||||
|
||||
def __init__(self, parent):
|
||||
EnComboBox.__init__(self, parent)
|
||||
self.setSizeAdjustPolicy(
|
||||
self.AdjustToMinimumContentsLengthWithIcon)
|
||||
|
||||
@dynamic_property
|
||||
def current_val(self):
|
||||
|
||||
def fget(self):
|
||||
return unicode(self.currentText()).strip()
|
||||
|
||||
def fset(self, val):
|
||||
if not val:
|
||||
val = ''
|
||||
self.setEditText(val.strip())
|
||||
self.setCursorPosition(0)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def initialize(self, db, id_):
|
||||
all_publishers = db.all_publishers()
|
||||
all_publishers.sort(key=lambda x : sort_key(x[1]))
|
||||
publisher_id = db.publisher_id(id_, index_is_id=True)
|
||||
idx, c = None, 0
|
||||
for i in all_publishers:
|
||||
id, name = i
|
||||
if id == publisher_id:
|
||||
idx = c
|
||||
self.addItem(name)
|
||||
c += 1
|
||||
|
||||
self.setEditText('')
|
||||
if idx is not None:
|
||||
self.setCurrentIndex(idx)
|
||||
|
||||
def commit(self, db, id_):
|
||||
db.set_publisher(id_, self.current_val, notify=False, commit=False)
|
||||
return True
|
||||
|
||||
# }}}
|
||||
|
||||
class DateEdit(QDateEdit): # {{{
|
||||
|
||||
TOOLTIP = ''
|
||||
LABEL = _('&Date:')
|
||||
FMT = 'd MMM yyyy'
|
||||
ATTR = 'timestamp'
|
||||
|
||||
def __init__(self, parent):
|
||||
QDateEdit.__init__(self, parent)
|
||||
self.setToolTip(self.TOOLTIP)
|
||||
self.setWhatsThis(self.TOOLTIP)
|
||||
fmt = self.FMT
|
||||
if fmt is None:
|
||||
fmt = tweaks['gui_pubdate_display_format']
|
||||
if fmt is None:
|
||||
fmt = 'MMM yyyy'
|
||||
self.setDisplayFormat(fmt)
|
||||
self.setCalendarPopup(True)
|
||||
self.setMinimumDate(UNDEFINED_QDATE)
|
||||
self.setSpecialValueText(_('Undefined'))
|
||||
self.clear_button = QToolButton(parent)
|
||||
self.clear_button.setIcon(QIcon(I('trash.png')))
|
||||
self.clear_button.setToolTip(_('Clear date'))
|
||||
self.clear_button.clicked.connect(self.reset_date)
|
||||
|
||||
def reset_date(self, *args):
|
||||
self.current_val = None
|
||||
|
||||
@dynamic_property
|
||||
def current_val(self):
|
||||
def fget(self):
|
||||
return qt_to_dt(self.date())
|
||||
def fset(self, val):
|
||||
if val is None:
|
||||
val = UNDEFINED_DATE
|
||||
self.setDate(QDate(val.year, val.month, val.day))
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def initialize(self, db, id_):
|
||||
self.current_val = getattr(db, self.ATTR)(id_, index_is_id=True)
|
||||
self.original_val = self.current_val
|
||||
|
||||
def commit(self, db, id_):
|
||||
if self.changed:
|
||||
getattr(db, 'set_'+self.ATTR)(id_, self.current_val, commit=False,
|
||||
notify=False)
|
||||
return True
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
o, c = self.original_val, self.current_val
|
||||
return o.year != c.year or o.month != c.month or o.day != c.day
|
||||
|
||||
class PubdateEdit(DateEdit):
|
||||
LABEL = _('Publishe&d:')
|
||||
FMT = None
|
||||
ATTR = 'pubdate'
|
||||
|
||||
# }}}
|
256
src/calibre/gui2/metadata/single.py
Normal file
256
src/calibre/gui2/metadata/single.py
Normal file
@ -0,0 +1,256 @@
|
||||
#!/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'
|
||||
|
||||
|
||||
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
|
||||
from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \
|
||||
AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \
|
||||
RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \
|
||||
BuddyLabel, DateEdit, PubdateEdit
|
||||
|
||||
class MetadataSingleDialog(ResizableDialog):
|
||||
|
||||
view_format = pyqtSignal(object)
|
||||
|
||||
def __init__(self, db, parent=None):
|
||||
self.db = db
|
||||
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.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()
|
||||
|
||||
self.do_layout()
|
||||
# }}}
|
||||
|
||||
def create_basic_metadata_widgets(self): # {{{
|
||||
self.basic_metadata_widgets = []
|
||||
|
||||
self.title = TitleEdit(self)
|
||||
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.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 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)
|
||||
l.addLayout(tl)
|
||||
|
||||
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)
|
||||
|
||||
tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
|
||||
|
||||
create_row(0, self.title, self.deduce_title_sort_button, self.title_sort)
|
||||
create_row(1, self.authors, self.deduce_author_sort_button, self.author_sort)
|
||||
create_row(2, self.series, self.remove_unused_series_button,
|
||||
self.series_index, icon='trash.png')
|
||||
|
||||
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)
|
||||
for i, b in enumerate(self.cover.buttons[:3]):
|
||||
l.addWidget(b, 0, i, 1, 1)
|
||||
gb.hl = QHBoxLayout()
|
||||
for b in self.cover.buttons[3:]:
|
||||
gb.hl.addWidget(b)
|
||||
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)
|
||||
|
||||
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)
|
||||
create_row2(1, self.rating)
|
||||
create_row2(2, self.tags, self.tags_editor_button)
|
||||
create_row2(3, self.isbn)
|
||||
create_row2(4, self.timestamp, self.timestamp.clear_button)
|
||||
create_row2(5, self.pubdate, self.pubdate.clear_button)
|
||||
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(_('&Comments'), self)
|
||||
gb.l = l = QVBoxLayout()
|
||||
gb.setLayout(l)
|
||||
l.addWidget(self.comments)
|
||||
self.splitter.addWidget(gb)
|
||||
|
||||
# }}}
|
||||
|
||||
def __call__(self, id_, has_next=False, has_previous=False):
|
||||
# TODO: Next and previous buttons
|
||||
self.book_id = id_
|
||||
for widget in self.basic_metadata_widgets:
|
||||
widget.initialize(self.db, id_)
|
||||
|
||||
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 fetch_metadata(self, *args):
|
||||
pass # TODO: fetch metadata
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from PyQt4.Qt import QApplication
|
||||
app = QApplication([])
|
||||
from calibre.library import db
|
||||
db = db()
|
||||
d = MetadataSingleDialog(db)
|
||||
d(db.data[0][0])
|
||||
d.exec_()
|
||||
|
@ -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):
|
||||
'''
|
||||
|
@ -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))
|
||||
# }}}
|
||||
|
||||
|
@ -580,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=':',
|
||||
@ -1370,7 +1370,9 @@ class EPUB_MOBI(CatalogPlugin):
|
||||
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 Genres found to catalog.\nCheck 'Excluded genres'\nin E-book options.\n")
|
||||
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)
|
||||
@ -2792,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}]}, ...]
|
||||
@ -3105,13 +3109,15 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
|
||||
navPointTag.insert(1, contentTag)
|
||||
elif self.opts.generate_genres:
|
||||
contentTag = Tag(soup, 'content')
|
||||
contentTag['src'] = "content/ByGenres.html"
|
||||
#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')
|
||||
@ -3125,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):
|
||||
@ -3911,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):
|
||||
@ -4055,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
|
||||
@ -4079,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
|
||||
|
||||
@ -4969,7 +4971,13 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
|
||||
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"]
|
||||
build_log.append(u" Sections: %s" % ', '.join(sections_list))
|
||||
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"
|
||||
@ -5017,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 = []
|
||||
|
@ -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 search works by using a python regular expression. All matched text is simply removed from
|
||||
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:
|
||||
|
||||
|
@ -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?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
Loading…
x
Reference in New Issue
Block a user