mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge
This commit is contained in:
commit
03c61a48ef
BIN
resources/images/heuristics.png
Normal file
BIN
resources/images/heuristics.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.3 KiB |
@ -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
|
||||
|
@ -7,22 +7,29 @@ class DallasNews(BasicNewsRecipe):
|
||||
max_articles_per_feed = 25
|
||||
|
||||
no_stylesheets = True
|
||||
remove_tags_before = dict(name='h2', attrs={'class':'vitstoryheadline'})
|
||||
remove_tags_after = dict(name='div', attrs={'style':'width: 100%; clear: right'})
|
||||
remove_tags_after = dict(name='div', attrs={'id':'article_tools_bottom'})
|
||||
use_embedded_content = False
|
||||
remove_tags_before = dict(name='h1')
|
||||
keep_only_tags = {'class':lambda x: x and 'article' in x}
|
||||
remove_tags = [
|
||||
dict(name='iframe'),
|
||||
dict(name='div', attrs={'class':'biblockmore'}),
|
||||
dict(name='div', attrs={'style':'width: 100%; clear: right'}),
|
||||
dict(name='div', attrs={'id':'article_tools_bottom'}),
|
||||
#dict(name='ul', attrs={'class':'articleTools'}),
|
||||
{'class':['DMNSocialTools', 'article ', 'article first ', 'article premium']},
|
||||
]
|
||||
|
||||
feeds = [
|
||||
('Latest News', 'http://www.dallasnews.com/newskiosk/rss/dallasnewslatestnews.xml'),
|
||||
('Local News', 'http://www.dallasnews.com/newskiosk/rss/dallasnewslocalnews.xml'),
|
||||
('Nation and World', 'http://www.dallasnews.com/newskiosk/rss/dallasnewsnationworld.xml'),
|
||||
('Politics', 'http://www.dallasnews.com/newskiosk/rss/dallasnewsnationalpolitics.xml'),
|
||||
('Science', 'http://www.dallasnews.com/newskiosk/rss/dallasnewsscience.xml'),
|
||||
('Local News',
|
||||
'http://www.dallasnews.com/news/politics/local-politics/?rss'),
|
||||
('National Politics',
|
||||
'http://www.dallasnews.com/news/politics/national-politic/?rss'),
|
||||
('State Politics',
|
||||
'http://www.dallasnews.com/news/politics/state-politics/?rss'),
|
||||
('Religion',
|
||||
'http://www.dallasnews.com/news/religion/?rss'),
|
||||
('Crime',
|
||||
'http://www.dallasnews.com/news/crime/headlines/?rss'),
|
||||
('Celebrity News',
|
||||
'http://www.dallasnews.com/entertainment/celebrity-news/?rss&listname=TopStories'),
|
||||
('Nation',
|
||||
'http://www.dallasnews.com/news/nation-world/nation/?rss'),
|
||||
('World',
|
||||
'http://www.dallasnews.com/news/nation-world/world/?rss'),
|
||||
]
|
||||
|
||||
|
64
resources/recipes/gulfnews.recipe
Normal file
64
resources/recipes/gulfnews.recipe
Normal 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
|
@ -5,6 +5,7 @@ class AdvancedUserRecipe1293122276(BasicNewsRecipe):
|
||||
__author__ = 'Jack Mason'
|
||||
author = 'IBM Global Business Services'
|
||||
publisher = 'IBM'
|
||||
language = 'en'
|
||||
category = 'news, technology, IT, internet of things, analytics'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 30
|
||||
|
@ -6,6 +6,7 @@ class KANewsRecipe(BasicNewsRecipe):
|
||||
description = u'Nachrichten aus Karlsruhe, Deutschland und der Welt.'
|
||||
__author__ = 'tfeld'
|
||||
lang='de'
|
||||
language = 'de'
|
||||
no_stylesheets = True
|
||||
|
||||
oldest_article = 7
|
||||
|
@ -4,6 +4,7 @@ class AdvancedUserRecipe1295262156(BasicNewsRecipe):
|
||||
title = u'kath.net'
|
||||
__author__ = 'Bobus'
|
||||
oldest_article = 7
|
||||
language = 'en'
|
||||
max_articles_per_feed = 100
|
||||
|
||||
feeds = [(u'kath.net', u'http://www.kath.net/2005/xml/index.xml')]
|
||||
|
@ -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'),
|
||||
|
@ -20,8 +20,8 @@ class LaVanguardia(BasicNewsRecipe):
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
delay = 1
|
||||
encoding = 'cp1252'
|
||||
delay = 5
|
||||
# encoding = 'cp1252'
|
||||
language = 'es'
|
||||
|
||||
direction = 'ltr'
|
||||
@ -35,7 +35,7 @@ class LaVanguardia(BasicNewsRecipe):
|
||||
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
|
||||
|
||||
feeds = [
|
||||
(u'Ciudadanos' , u'http://feeds.feedburner.com/lavanguardia/ciudadanos' )
|
||||
(u'Portada' , u'http://feeds.feedburner.com/lavanguardia/home' )
|
||||
,(u'Cultura' , u'http://feeds.feedburner.com/lavanguardia/cultura' )
|
||||
,(u'Deportes' , u'http://feeds.feedburner.com/lavanguardia/deportes' )
|
||||
,(u'Economia' , u'http://feeds.feedburner.com/lavanguardia/economia' )
|
||||
@ -45,17 +45,17 @@ class LaVanguardia(BasicNewsRecipe):
|
||||
,(u'Internet y tecnologia', u'http://feeds.feedburner.com/lavanguardia/internet' )
|
||||
,(u'Motor' , u'http://feeds.feedburner.com/lavanguardia/motor' )
|
||||
,(u'Politica' , u'http://feeds.feedburner.com/lavanguardia/politica' )
|
||||
,(u'Sucessos' , u'http://feeds.feedburner.com/lavanguardia/sucesos' )
|
||||
,(u'Sucesos' , u'http://feeds.feedburner.com/lavanguardia/sucesos' )
|
||||
]
|
||||
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'class':'element1_3'})
|
||||
dict(name='div', attrs={'class':'detalle noticia'})
|
||||
]
|
||||
|
||||
remove_tags = [
|
||||
dict(name=['object','link','script'])
|
||||
,dict(name='div', attrs={'class':['colC','peu']})
|
||||
,dict(name='div', attrs={'class':['colC','peu','jstoolbar']})
|
||||
]
|
||||
|
||||
remove_tags_after = [dict(name='div', attrs={'class':'text'})]
|
||||
@ -67,4 +67,3 @@ class LaVanguardia(BasicNewsRecipe):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
return soup
|
||||
|
||||
|
@ -10,6 +10,7 @@ import re
|
||||
class NationalGeographicNews(BasicNewsRecipe):
|
||||
title = u'National Geographic News'
|
||||
oldest_article = 7
|
||||
language = 'en'
|
||||
max_articles_per_feed = 100
|
||||
remove_javascript = True
|
||||
no_stylesheets = True
|
||||
|
@ -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] },
|
||||
@ -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'
|
||||
|
||||
|
@ -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']
|
||||
|
||||
|
@ -98,6 +98,9 @@ class PRS505(USBMS):
|
||||
|
||||
THUMBNAIL_HEIGHT = 200
|
||||
|
||||
MAX_PATH_LEN = 201 # 250 - (max(len(CACHE_THUMBNAIL), len(MEDIA_THUMBNAIL)) +
|
||||
# len('main_thumbnail.jpg') + 1)
|
||||
|
||||
def windows_filter_pnp_id(self, pnp_id):
|
||||
return '_LAUNCHER' in pnp_id
|
||||
|
||||
@ -201,10 +204,13 @@ class PRS505(USBMS):
|
||||
self._card_b_prefix if idx == 2 \
|
||||
else self._main_prefix
|
||||
for book in bl:
|
||||
try:
|
||||
p = os.path.join(prefix, book.lpath)
|
||||
self._upload_cover(os.path.dirname(p),
|
||||
os.path.splitext(os.path.basename(p))[0],
|
||||
book, p)
|
||||
except:
|
||||
debug_print('FAILED to upload cover', p)
|
||||
else:
|
||||
debug_print('PRS505: NOT uploading covers in sync_booklists')
|
||||
|
||||
@ -229,7 +235,10 @@ class PRS505(USBMS):
|
||||
debug_print('PRS505: not uploading cover')
|
||||
return
|
||||
debug_print('PRS505: uploading cover')
|
||||
try:
|
||||
self._upload_cover(path, filename, metadata, filepath)
|
||||
except:
|
||||
debug_print('FAILED to upload cover', filepath)
|
||||
|
||||
def _upload_cover(self, path, filename, metadata, filepath):
|
||||
if metadata.thumbnail and metadata.thumbnail[-1]:
|
||||
|
@ -98,6 +98,9 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
# copy these back to the library
|
||||
BACKLOADING_ERROR_MESSAGE = None
|
||||
|
||||
#: The maximum length of paths created on the device
|
||||
MAX_PATH_LEN = 250
|
||||
|
||||
def reset(self, key='-1', log_packets=False, report_progress=None,
|
||||
detected_device=None):
|
||||
self._main_prefix = self._card_a_prefix = self._card_b_prefix = None
|
||||
@ -875,7 +878,7 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
|
||||
def create_upload_path(self, path, mdata, fname, create_dirs=True):
|
||||
path = os.path.abspath(path)
|
||||
extra_components = []
|
||||
maxlen = self.MAX_PATH_LEN
|
||||
|
||||
special_tag = None
|
||||
if mdata.tags:
|
||||
@ -902,7 +905,7 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
app_id = str(getattr(mdata, 'application_id', ''))
|
||||
# The db id will be in the created filename
|
||||
extra_components = get_components(template, mdata, fname,
|
||||
timefmt=opts.send_timefmt, length=250-len(app_id)-1)
|
||||
timefmt=opts.send_timefmt, length=maxlen-len(app_id)-1)
|
||||
if not extra_components:
|
||||
extra_components.append(sanitize(self.filename_callback(fname,
|
||||
mdata)))
|
||||
@ -937,12 +940,11 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
return ans
|
||||
|
||||
extra_components = list(map(remove_trailing_periods, extra_components))
|
||||
components = shorten_components_to(250 - len(path), extra_components)
|
||||
components = shorten_components_to(maxlen - len(path), extra_components)
|
||||
components = self.sanitize_path_components(components)
|
||||
filepath = os.path.join(path, *components)
|
||||
filedir = os.path.dirname(filepath)
|
||||
|
||||
|
||||
if create_dirs and not os.path.exists(filedir):
|
||||
os.makedirs(filedir)
|
||||
|
||||
|
@ -42,6 +42,12 @@ option.
|
||||
For full documentation of the conversion system see
|
||||
''') + 'http://calibre-ebook.com/user_manual/conversion.html'
|
||||
|
||||
HEURISTIC_OPTIONS = ['markup_chapter_headings',
|
||||
'italicize_common_cases', 'fix_indents',
|
||||
'html_unwrap_factor', 'unwrap_lines',
|
||||
'delete_blank_paragraphs', 'format_scene_breaks',
|
||||
'dehyphenate', 'renumber_headings']
|
||||
|
||||
def print_help(parser, log):
|
||||
help = parser.format_help().encode(preferred_encoding, 'replace')
|
||||
log(help)
|
||||
@ -83,6 +89,8 @@ def option_recommendation_to_cli_option(add_option, rec):
|
||||
if opt.long_switch == 'verbose':
|
||||
attrs['action'] = 'count'
|
||||
attrs.pop('type', '')
|
||||
if opt.name in HEURISTIC_OPTIONS and rec.recommended_value is True:
|
||||
switches = ['--disable-'+opt.long_switch]
|
||||
add_option(Option(*switches, **attrs))
|
||||
|
||||
def add_input_output_options(parser, plumber):
|
||||
@ -131,14 +139,11 @@ def add_pipeline_options(parser, plumber):
|
||||
),
|
||||
|
||||
'HEURISTIC PROCESSING' : (
|
||||
_('Modify the document text and structure using common patterns.'),
|
||||
[
|
||||
'enable_heuristics', 'markup_chapter_headings',
|
||||
'italicize_common_cases', 'fix_indents',
|
||||
'html_unwrap_factor', 'unwrap_lines',
|
||||
'delete_blank_paragraphs', 'format_scene_breaks',
|
||||
'dehyphenate', 'renumber_headings',
|
||||
]
|
||||
_('Modify the document text and structure using common'
|
||||
' patterns. Disabled by default. Use %s to enable. '
|
||||
' Individual actions can be disabled with the %s options.')
|
||||
% ('--enable-heuristics', '--disable-*'),
|
||||
['enable_heuristics'] + HEURISTIC_OPTIONS
|
||||
),
|
||||
|
||||
'SEARCH AND REPLACE' : (
|
||||
|
@ -72,7 +72,8 @@ class Plumber(object):
|
||||
]
|
||||
|
||||
def __init__(self, input, output, log, report_progress=DummyReporter(),
|
||||
dummy=False, merge_plugin_recs=True, abort_after_input_dump=False):
|
||||
dummy=False, merge_plugin_recs=True, abort_after_input_dump=False,
|
||||
override_input_metadata=False):
|
||||
'''
|
||||
:param input: Path to input file.
|
||||
:param output: Path to output file/directory
|
||||
@ -87,6 +88,7 @@ class Plumber(object):
|
||||
self.log = log
|
||||
self.ui_reporter = report_progress
|
||||
self.abort_after_input_dump = abort_after_input_dump
|
||||
self.override_input_metadata = override_input_metadata
|
||||
|
||||
# Pipeline options {{{
|
||||
# Initialize the conversion options that are independent of input and
|
||||
@ -490,19 +492,19 @@ OptionRecommendation(name='enable_heuristics',
|
||||
'heuristic processing to take place.')),
|
||||
|
||||
OptionRecommendation(name='markup_chapter_headings',
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
recommended_value=True, level=OptionRecommendation.LOW,
|
||||
help=_('Detect unformatted chapter headings and sub headings. Change '
|
||||
'them to h2 and h3 tags. This setting will not create a TOC, '
|
||||
'but can be used in conjunction with structure detection to create '
|
||||
'one.')),
|
||||
|
||||
OptionRecommendation(name='italicize_common_cases',
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
recommended_value=True, level=OptionRecommendation.LOW,
|
||||
help=_('Look for common words and patterns that denote '
|
||||
'italics and italicize them.')),
|
||||
|
||||
OptionRecommendation(name='fix_indents',
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
recommended_value=True, level=OptionRecommendation.LOW,
|
||||
help=_('Turn indentation created from multiple non-breaking space entities '
|
||||
'into CSS indents.')),
|
||||
|
||||
@ -515,28 +517,28 @@ OptionRecommendation(name='html_unwrap_factor',
|
||||
'be reduced')),
|
||||
|
||||
OptionRecommendation(name='unwrap_lines',
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
recommended_value=True, level=OptionRecommendation.LOW,
|
||||
help=_('Unwrap lines using punctuation and other formatting clues.')),
|
||||
|
||||
OptionRecommendation(name='delete_blank_paragraphs',
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
recommended_value=True, level=OptionRecommendation.LOW,
|
||||
help=_('Remove empty paragraphs from the document when they exist between '
|
||||
'every other paragraph')),
|
||||
|
||||
OptionRecommendation(name='format_scene_breaks',
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
recommended_value=True, level=OptionRecommendation.LOW,
|
||||
help=_('Left aligned scene break markers are center aligned. '
|
||||
'Replace soft scene breaks that use multiple blank lines with'
|
||||
'horizontal rules.')),
|
||||
|
||||
OptionRecommendation(name='dehyphenate',
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
recommended_value=True, level=OptionRecommendation.LOW,
|
||||
help=_('Analyze hyphenated words throughout the document. The '
|
||||
'document itself is used as a dictionary to determine whether hyphens '
|
||||
'should be retained or removed.')),
|
||||
|
||||
OptionRecommendation(name='renumber_headings',
|
||||
recommended_value=False, level=OptionRecommendation.LOW,
|
||||
recommended_value=True, level=OptionRecommendation.LOW,
|
||||
help=_('Looks for occurrences of sequential <h1> or <h2> tags. '
|
||||
'The tags are renumbered to prevent splitting in the middle '
|
||||
'of chapter headings.')),
|
||||
@ -924,7 +926,8 @@ OptionRecommendation(name='sr3_replace',
|
||||
self.opts.dest = self.opts.output_profile
|
||||
|
||||
from calibre.ebooks.oeb.transforms.metadata import MergeMetadata
|
||||
MergeMetadata()(self.oeb, self.user_metadata, self.opts)
|
||||
MergeMetadata()(self.oeb, self.user_metadata, self.opts,
|
||||
override_input_metadata=self.override_input_metadata)
|
||||
pr(0.2)
|
||||
self.flush()
|
||||
|
||||
|
@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import functools, re
|
||||
|
||||
from calibre import entity_to_unicode
|
||||
from calibre import entity_to_unicode, as_unicode
|
||||
|
||||
XMLDECL_RE = re.compile(r'^\s*<[?]xml.*?[?]>')
|
||||
SVG_NS = 'http://www.w3.org/2000/svg'
|
||||
@ -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))
|
||||
@ -459,11 +463,12 @@ class HTMLPreProcessor(object):
|
||||
try:
|
||||
search_re = re.compile(search_pattern)
|
||||
replace_txt = getattr(self.extra_opts, replace, '')
|
||||
if replace_txt == None:
|
||||
if not replace_txt:
|
||||
replace_txt = ''
|
||||
rules.insert(0, (search_re, replace_txt))
|
||||
except Exception as e:
|
||||
self.log.error('Failed to parse %s regexp because %s' % (search, e))
|
||||
self.log.error('Failed to parse %r regexp because %s' %
|
||||
(search, as_unicode(e)))
|
||||
|
||||
end_rules = []
|
||||
# delete soft hyphens - moved here so it's executed after header/footer removal
|
||||
|
@ -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)
|
||||
|
@ -10,7 +10,7 @@ import os
|
||||
from calibre.utils.date import isoformat, now
|
||||
from calibre import guess_type
|
||||
|
||||
def meta_info_to_oeb_metadata(mi, m, log):
|
||||
def meta_info_to_oeb_metadata(mi, m, log, override_input_metadata=False):
|
||||
from calibre.ebooks.oeb.base import OPF
|
||||
if not mi.is_null('title'):
|
||||
m.clear('title')
|
||||
@ -29,15 +29,23 @@ def meta_info_to_oeb_metadata(mi, m, log):
|
||||
if not mi.is_null('book_producer'):
|
||||
m.filter('contributor', lambda x : x.role.lower() == 'bkp')
|
||||
m.add('contributor', mi.book_producer, role='bkp')
|
||||
elif override_input_metadata:
|
||||
m.filter('contributor', lambda x : x.role.lower() == 'bkp')
|
||||
if not mi.is_null('comments'):
|
||||
m.clear('description')
|
||||
m.add('description', mi.comments)
|
||||
elif override_input_metadata:
|
||||
m.clear('description')
|
||||
if not mi.is_null('publisher'):
|
||||
m.clear('publisher')
|
||||
m.add('publisher', mi.publisher)
|
||||
elif override_input_metadata:
|
||||
m.clear('publisher')
|
||||
if not mi.is_null('series'):
|
||||
m.clear('series')
|
||||
m.add('series', mi.series)
|
||||
elif override_input_metadata:
|
||||
m.clear('series')
|
||||
if not mi.is_null('isbn'):
|
||||
has = False
|
||||
for x in m.identifier:
|
||||
@ -46,19 +54,27 @@ def meta_info_to_oeb_metadata(mi, m, log):
|
||||
has = True
|
||||
if not has:
|
||||
m.add('identifier', mi.isbn, scheme='ISBN')
|
||||
elif override_input_metadata:
|
||||
m.filter('identifier', lambda x: x.scheme.lower() == 'isbn')
|
||||
if not mi.is_null('language'):
|
||||
m.clear('language')
|
||||
m.add('language', mi.language)
|
||||
if not mi.is_null('series_index'):
|
||||
m.clear('series_index')
|
||||
m.add('series_index', mi.format_series_index())
|
||||
elif override_input_metadata:
|
||||
m.clear('series_index')
|
||||
if not mi.is_null('rating'):
|
||||
m.clear('rating')
|
||||
m.add('rating', '%.2f'%mi.rating)
|
||||
elif override_input_metadata:
|
||||
m.clear('rating')
|
||||
if not mi.is_null('tags'):
|
||||
m.clear('subject')
|
||||
for t in mi.tags:
|
||||
m.add('subject', t)
|
||||
elif override_input_metadata:
|
||||
m.clear('subject')
|
||||
if not mi.is_null('pubdate'):
|
||||
m.clear('date')
|
||||
m.add('date', isoformat(mi.pubdate))
|
||||
@ -68,9 +84,14 @@ def meta_info_to_oeb_metadata(mi, m, log):
|
||||
if not mi.is_null('rights'):
|
||||
m.clear('rights')
|
||||
m.add('rights', mi.rights)
|
||||
elif override_input_metadata:
|
||||
m.clear('rights')
|
||||
if not mi.is_null('publication_type'):
|
||||
m.clear('publication_type')
|
||||
m.add('publication_type', mi.publication_type)
|
||||
elif override_input_metadata:
|
||||
m.clear('publication_type')
|
||||
|
||||
if not m.timestamp:
|
||||
m.add('timestamp', isoformat(now()))
|
||||
|
||||
@ -78,11 +99,12 @@ def meta_info_to_oeb_metadata(mi, m, log):
|
||||
class MergeMetadata(object):
|
||||
'Merge in user metadata, including cover'
|
||||
|
||||
def __call__(self, oeb, mi, opts):
|
||||
def __call__(self, oeb, mi, opts, override_input_metadata=False):
|
||||
self.oeb, self.log = oeb, oeb.log
|
||||
m = self.oeb.metadata
|
||||
self.log('Merging user specified metadata...')
|
||||
meta_info_to_oeb_metadata(mi, m, oeb.log)
|
||||
meta_info_to_oeb_metadata(mi, m, oeb.log,
|
||||
override_input_metadata=override_input_metadata)
|
||||
cover_id = self.set_cover(mi, opts.prefer_metadata_cover)
|
||||
m.clear('cover')
|
||||
if cover_id is not None:
|
||||
|
@ -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:
|
||||
|
@ -176,9 +176,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:
|
||||
|
@ -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=[]):
|
||||
|
@ -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)
|
||||
|
||||
@ -384,7 +384,14 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
return
|
||||
|
||||
prefs['library_path'] = loc
|
||||
#from calibre.utils.mem import memory
|
||||
#import weakref, gc
|
||||
#ref = weakref.ref(self.gui.library_view.model().db)
|
||||
#before = memory()/1024**2
|
||||
self.gui.library_moved(loc)
|
||||
#print gc.get_referrers(ref)[0]
|
||||
#for i in xrange(3): gc.collect()
|
||||
#print 'leaked:', memory()/1024**2 - before
|
||||
|
||||
def qs_requested(self, idx, *args):
|
||||
self.switch_requested(self.qs_locations[idx])
|
||||
|
@ -144,6 +144,9 @@ class PluginWidget(QWidget,Ui_Form):
|
||||
# Hook changes to thumb_width
|
||||
self.thumb_width.valueChanged.connect(self.thumb_width_changed)
|
||||
|
||||
# Hook changes to Description section
|
||||
self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed)
|
||||
|
||||
def options(self):
|
||||
# Save/return the current options
|
||||
# exclude_genre stores literally
|
||||
@ -265,7 +268,7 @@ class PluginWidget(QWidget,Ui_Form):
|
||||
custom_fields = {}
|
||||
for custom_field in all_custom_fields:
|
||||
field_md = self.db.metadata_for_field(custom_field)
|
||||
if field_md['datatype'] in ['text','comments']:
|
||||
if field_md['datatype'] in ['text','comments','composite']:
|
||||
custom_fields[field_md['name']] = {'field':custom_field,
|
||||
'datatype':field_md['datatype']}
|
||||
# Blank field first
|
||||
@ -324,6 +327,28 @@ class PluginWidget(QWidget,Ui_Form):
|
||||
else:
|
||||
self.exclude_pattern.setEnabled(False)
|
||||
|
||||
def generate_descriptions_changed(self,new_state):
|
||||
'''
|
||||
Process changes to Descriptions section
|
||||
0: unchecked
|
||||
2: checked
|
||||
'''
|
||||
|
||||
return
|
||||
|
||||
if new_state == 0:
|
||||
# unchecked
|
||||
self.merge_source_field.setEnabled(False)
|
||||
self.merge_before.setEnabled(False)
|
||||
self.merge_after.setEnabled(False)
|
||||
self.include_hr.setEnabled(False)
|
||||
elif new_state == 2:
|
||||
# checked
|
||||
self.merge_source_field.setEnabled(True)
|
||||
self.merge_before.setEnabled(True)
|
||||
self.merge_after.setEnabled(True)
|
||||
self.include_hr.setEnabled(True)
|
||||
|
||||
def header_note_source_field_changed(self,new_index):
|
||||
'''
|
||||
Process changes in the header_note_source_field combo box
|
||||
|
@ -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>
|
||||
|
@ -94,7 +94,7 @@ class BulkConfig(Config):
|
||||
if not c: break
|
||||
self.stack.removeWidget(c)
|
||||
|
||||
widgets = [lf, hw, sr, ps, sd, toc]
|
||||
widgets = [lf, hw, ps, sd, toc, sr]
|
||||
if output_widget is not None:
|
||||
widgets.append(output_widget)
|
||||
for w in widgets:
|
||||
|
@ -12,17 +12,24 @@ from calibre.customize.ui import plugin_for_catalog_format
|
||||
from calibre.utils.logging import Log
|
||||
|
||||
def gui_convert(input, output, recommendations, notification=DummyReporter(),
|
||||
abort_after_input_dump=False, log=None):
|
||||
abort_after_input_dump=False, log=None, override_input_metadata=False):
|
||||
recommendations = list(recommendations)
|
||||
recommendations.append(('verbose', 2, OptionRecommendation.HIGH))
|
||||
if log is None:
|
||||
log = Log()
|
||||
plumber = Plumber(input, output, log, report_progress=notification,
|
||||
abort_after_input_dump=abort_after_input_dump)
|
||||
abort_after_input_dump=abort_after_input_dump,
|
||||
override_input_metadata=override_input_metadata)
|
||||
plumber.merge_ui_recommendations(recommendations)
|
||||
|
||||
plumber.run()
|
||||
|
||||
def gui_convert_override(input, output, recommendations, notification=DummyReporter(),
|
||||
abort_after_input_dump=False, log=None):
|
||||
gui_convert(input, output, recommendations, notification=notification,
|
||||
abort_after_input_dump=abort_after_input_dump, log=log,
|
||||
override_input_metadata=True)
|
||||
|
||||
def gui_catalog(fmt, title, dbspec, ids, out_file_name, sync, fmt_options, connected_device,
|
||||
notification=DummyReporter(), log=None):
|
||||
if log is None:
|
||||
|
@ -11,9 +11,10 @@ from calibre.gui2.convert import Widget
|
||||
|
||||
class HeuristicsWidget(Widget, Ui_Form):
|
||||
|
||||
TITLE = _('Heuristic Processing')
|
||||
TITLE = _('Heuristic\nProcessing')
|
||||
HELP = _('Modify the document text and structure using common patterns.')
|
||||
COMMIT_NAME = 'heuristics'
|
||||
ICON = I('heuristics.png')
|
||||
|
||||
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
|
||||
Widget.__init__(self, parent,
|
||||
@ -46,23 +47,8 @@ class HeuristicsWidget(Widget, Ui_Form):
|
||||
return True
|
||||
|
||||
def enable_heuristics(self, state):
|
||||
if state == Qt.Checked:
|
||||
state = True
|
||||
else:
|
||||
state = False
|
||||
self.opt_markup_chapter_headings.setEnabled(state)
|
||||
self.opt_italicize_common_cases.setEnabled(state)
|
||||
self.opt_fix_indents.setEnabled(state)
|
||||
self.opt_delete_blank_paragraphs.setEnabled(state)
|
||||
self.opt_format_scene_breaks.setEnabled(state)
|
||||
self.opt_dehyphenate.setEnabled(state)
|
||||
self.opt_renumber_headings.setEnabled(state)
|
||||
|
||||
self.opt_unwrap_lines.setEnabled(state)
|
||||
if state and self.opt_unwrap_lines.checkState() == Qt.Checked:
|
||||
self.opt_html_unwrap_factor.setEnabled(True)
|
||||
else:
|
||||
self.opt_html_unwrap_factor.setEnabled(False)
|
||||
state = state == Qt.Checked
|
||||
self.heuristic_options.setEnabled(state)
|
||||
|
||||
def enable_unwrap(self, state):
|
||||
if state == Qt.Checked:
|
||||
|
@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>938</width>
|
||||
<width>724</width>
|
||||
<height>470</height>
|
||||
</rect>
|
||||
</property>
|
||||
@ -15,36 +15,83 @@
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="opt_enable_heuristics">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>&Preprocess input file to possibly improve structure detection</string>
|
||||
<string><b>Heuristic processing</b> means that calibre will scan your book for common patterns and fix them. As the name implies, this involves guesswork, which means that it could end up worsening the result of a conversion, if calibre guesses wrong. Therefore, it is disabled by default. Often, if a conversion does not turn out as you expect, turning on heuristics can improve matters. Read more about the various heuristic processing options in the <a href="http://calibre-ebook.com/user_manual/conversion.html#heuristic-processing">User Manual</a>.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>15</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="opt_enable_heuristics">
|
||||
<property name="text">
|
||||
<string>Enable &heuristic processing</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="heuristic_options">
|
||||
<property name="title">
|
||||
<string>Heuristic Processing</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="opt_unwrap_lines">
|
||||
<property name="text">
|
||||
<string>Unwrap lines</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="huf_label">
|
||||
<property name="text">
|
||||
<string>Line &un-wrap factor during preprocess:</string>
|
||||
<string>Line &un-wrap factor :</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_html_unwrap_factor</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="opt_html_unwrap_factor">
|
||||
<property name="toolTip">
|
||||
<string/>
|
||||
@ -60,7 +107,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
@ -73,56 +120,58 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="4">
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="opt_markup_chapter_headings">
|
||||
<property name="text">
|
||||
<string>Detect and markup unformatted chapter headings and sub headings</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="4">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="opt_renumber_headings">
|
||||
<property name="text">
|
||||
<string>Renumber sequences of <h1> or <h2> tags to prevent splitting</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="opt_delete_blank_paragraphs">
|
||||
<property name="text">
|
||||
<string>Delete blank lines between paragraphs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="3">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="opt_format_scene_breaks">
|
||||
<property name="text">
|
||||
<string>Ensure scene breaks are consistently formatted</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0" colspan="2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="opt_dehyphenate">
|
||||
<property name="text">
|
||||
<string>Remove unnecessary hyphens</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0" colspan="2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="opt_italicize_common_cases">
|
||||
<property name="text">
|
||||
<string>Italicize common words and patterns</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0" colspan="2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="opt_fix_indents">
|
||||
<property name="text">
|
||||
<string>Replace entity indents with CSS indents</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0" colspan="2">
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
|
@ -47,6 +47,8 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
|
||||
return False
|
||||
else:
|
||||
self.regex.setStyleSheet('QLineEdit { color: black; background-color: white; }')
|
||||
self.preview.setExtraSelections([])
|
||||
return False
|
||||
return True
|
||||
|
||||
def do_test(self):
|
||||
|
@ -12,9 +12,10 @@ from calibre.gui2 import error_dialog
|
||||
|
||||
class SearchAndReplaceWidget(Widget, Ui_Form):
|
||||
|
||||
TITLE = _('Search &\nReplace')
|
||||
TITLE = _('Search\n&\nReplace')
|
||||
HELP = _('Modify the document text and structure using user defined patterns.')
|
||||
COMMIT_NAME = 'search_and_replace'
|
||||
ICON = I('search.png')
|
||||
|
||||
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
|
||||
Widget.__init__(self, parent,
|
||||
@ -24,13 +25,13 @@ class SearchAndReplaceWidget(Widget, Ui_Form):
|
||||
)
|
||||
self.db, self.book_id = db, book_id
|
||||
self.initialize_options(get_option, get_help, db, book_id)
|
||||
self.opt_sr1_search.set_msg(_('Search Regular Expression'))
|
||||
self.opt_sr1_search.set_msg(_('&Search Regular Expression'))
|
||||
self.opt_sr1_search.set_book_id(book_id)
|
||||
self.opt_sr1_search.set_db(db)
|
||||
self.opt_sr2_search.set_msg(_('Search Regular Expression'))
|
||||
self.opt_sr2_search.set_msg(_('&Search Regular Expression'))
|
||||
self.opt_sr2_search.set_book_id(book_id)
|
||||
self.opt_sr2_search.set_db(db)
|
||||
self.opt_sr3_search.set_msg(_('Search Regular Expression'))
|
||||
self.opt_sr3_search.set_msg(_('&Search Regular Expression'))
|
||||
self.opt_sr3_search.set_book_id(book_id)
|
||||
self.opt_sr3_search.set_db(db)
|
||||
|
||||
@ -49,6 +50,6 @@ class SearchAndReplaceWidget(Widget, Ui_Form):
|
||||
re.compile(pat)
|
||||
except Exception, err:
|
||||
error_dialog(self, _('Invalid regular expression'),
|
||||
_('Invalid regular expression: %s')%err).exec_()
|
||||
_('Invalid regular expression: %s')%err, show=True)
|
||||
return False
|
||||
return True
|
||||
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>198</width>
|
||||
<height>350</height>
|
||||
<width>468</width>
|
||||
<height>451</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
@ -23,7 +23,7 @@
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<item row="1" column="0">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
@ -32,7 +32,7 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>1.</string>
|
||||
<string>First expression</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<property name="sizeConstraint">
|
||||
@ -57,7 +57,10 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Replacement Text</string>
|
||||
<string>&Replacement Text</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_sr1_replace</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -74,7 +77,7 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<item row="2" column="0">
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
@ -83,7 +86,7 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>2.</string>
|
||||
<string>Second Expression</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="sizeConstraint">
|
||||
@ -108,7 +111,10 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Replacement Text</string>
|
||||
<string>&Replacement Text</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_sr2_replace</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -125,7 +131,7 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<item row="3" column="0">
|
||||
<widget class="QGroupBox" name="groupBox_3">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
@ -134,7 +140,7 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>3.</string>
|
||||
<string>Third expression</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<property name="sizeConstraint">
|
||||
@ -159,7 +165,10 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Replacement Text</string>
|
||||
<string>&Replacement Text</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_sr3_replace</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -176,6 +185,19 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string><p>Search and replace uses <i>regular expressions</i>. See the <a href="http://calibre-ebook.com/user_manual/regexp.html">regular expressions tutorial</a> to get started with regular expressions. Also clicking the wizard buttons below will allow you to test your regular expression against the current input document.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
|
@ -207,7 +207,7 @@ class Config(ResizableDialog, Ui_Dialog):
|
||||
if not c: break
|
||||
self.stack.removeWidget(c)
|
||||
|
||||
widgets = [self.mw, lf, hw, sr, ps, sd, toc]
|
||||
widgets = [self.mw, lf, hw, ps, sd, toc, sr]
|
||||
if input_widget is not None:
|
||||
widgets.append(input_widget)
|
||||
if output_widget is not None:
|
||||
|
@ -100,7 +100,7 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="spacing">
|
||||
<number>20</number>
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
@ -129,8 +129,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>805</width>
|
||||
<height>484</height>
|
||||
<width>810</width>
|
||||
<height>494</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
|
@ -40,10 +40,7 @@ class PluginWidget(Widget, Ui_Form):
|
||||
pass
|
||||
|
||||
def enable_markdown_format(self, state):
|
||||
if state == Qt.Checked:
|
||||
state = True
|
||||
else:
|
||||
state = False
|
||||
state = state == Qt.Checked
|
||||
self.opt_keep_links.setEnabled(state)
|
||||
self.opt_keep_image_references.setEnabled(state)
|
||||
|
@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>434</width>
|
||||
<width>430</width>
|
||||
<height>74</height>
|
||||
</rect>
|
||||
</property>
|
||||
@ -59,7 +59,7 @@
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/wizard.png</normaloff>:/images/wizard.png</iconset>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
|
@ -379,6 +379,7 @@ 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)
|
||||
if book_id is not None:
|
||||
w.initialize(book_id)
|
||||
return w
|
||||
x = db.custom_column_num_map
|
||||
@ -599,7 +600,7 @@ class BulkEnumeration(BulkBase, Enumeration):
|
||||
value = None
|
||||
ret_value = None
|
||||
dialog_shown = False
|
||||
for book_id in book_ids:
|
||||
for i,book_id in enumerate(book_ids):
|
||||
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
||||
if val and val not in self.col_metadata['display']['enum_values']:
|
||||
if not dialog_shown:
|
||||
@ -610,7 +611,7 @@ class BulkEnumeration(BulkBase, Enumeration):
|
||||
show=True, show_copy_button=False)
|
||||
dialog_shown = True
|
||||
ret_value = ' nochange '
|
||||
elif value is not None and value != val:
|
||||
elif (value is not None and value != val) or (val and i != 0):
|
||||
ret_value = ' nochange '
|
||||
value = val
|
||||
if ret_value is None:
|
||||
|
85
src/calibre/gui2/dialogs/add_empty_book.py
Normal file
85
src/calibre/gui2/dialogs/add_empty_book.py
Normal 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_()
|
@ -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:
|
||||
@ -79,6 +79,8 @@ class TagEditor(QDialog, Ui_TagEditor):
|
||||
|
||||
def apply_tags(self, item=None):
|
||||
items = self.available_tags.selectedItems() if item is None else [item]
|
||||
rows = [self.available_tags.row(i) for i in items]
|
||||
row = max(rows)
|
||||
for item in items:
|
||||
tag = unicode(item.text())
|
||||
self.tags.append(tag)
|
||||
@ -89,6 +91,12 @@ class TagEditor(QDialog, Ui_TagEditor):
|
||||
for tag in self.tags:
|
||||
self.applied_tags.addItem(tag)
|
||||
|
||||
if row >= self.available_tags.count():
|
||||
row = self.available_tags.count() - 1
|
||||
|
||||
if row > 2:
|
||||
item = self.available_tags.item(row)
|
||||
self.available_tags.scrollToItem(item)
|
||||
|
||||
|
||||
def unapply_tags(self, item=None):
|
||||
|
@ -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()
|
||||
|
1014
src/calibre/gui2/metadata/basic_widgets.py
Normal file
1014
src/calibre/gui2/metadata/basic_widgets.py
Normal file
File diff suppressed because it is too large
Load Diff
474
src/calibre/gui2/metadata/single.py
Normal file
474
src/calibre/gui2/metadata/single.py
Normal 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)
|
||||
|
@ -85,8 +85,8 @@ class CommonOptions(Base):
|
||||
|
||||
def load_conversion_widgets(self):
|
||||
self.conversion_widgets = [LookAndFeelWidget, HeuristicsWidget,
|
||||
SearchAndReplaceWidget, PageSetupWidget,
|
||||
StructureDetectionWidget, TOCWidget]
|
||||
PageSetupWidget,
|
||||
StructureDetectionWidget, TOCWidget, SearchAndReplaceWidget,]
|
||||
|
||||
class InputOptions(Base):
|
||||
|
||||
|
@ -75,7 +75,7 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, out_format
|
||||
temp_files.append(d.cover_file)
|
||||
args = [in_file, out_file.name, recs]
|
||||
temp_files.append(out_file)
|
||||
jobs.append(('gui_convert', args, desc, d.output_format.upper(), book_id, temp_files))
|
||||
jobs.append(('gui_convert_override', args, desc, d.output_format.upper(), book_id, temp_files))
|
||||
|
||||
changed = True
|
||||
d.break_cycles()
|
||||
@ -185,7 +185,7 @@ class QueueBulk(QProgressDialog):
|
||||
|
||||
args = [in_file, out_file.name, lrecs]
|
||||
temp_files.append(out_file)
|
||||
self.jobs.append(('gui_convert', args, desc, self.output_format.upper(), book_id, temp_files))
|
||||
self.jobs.append(('gui_convert_override', args, desc, self.output_format.upper(), book_id, temp_files))
|
||||
|
||||
self.changed = True
|
||||
self.setValue(self.i)
|
||||
|
@ -440,6 +440,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
olddb.break_cycles()
|
||||
if self.device_connected:
|
||||
self.set_books_in_library(self.booklists(), reset=True)
|
||||
self.refresh_ondevice()
|
||||
|
@ -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))
|
||||
# }}}
|
||||
|
||||
|
@ -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,6 +1360,7 @@ class EPUB_MOBI(CatalogPlugin):
|
||||
if self.opts.generate_descriptions:
|
||||
self.generateThumbnails()
|
||||
self.generateHTMLDescriptions()
|
||||
if self.opts.generate_authors:
|
||||
self.generateHTMLByAuthor()
|
||||
if self.opts.generate_titles:
|
||||
self.generateHTMLByTitle()
|
||||
@ -1365,6 +1368,15 @@ class EPUB_MOBI(CatalogPlugin):
|
||||
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,6 +1384,7 @@ class EPUB_MOBI(CatalogPlugin):
|
||||
|
||||
self.generateOPF()
|
||||
self.generateNCXHeader()
|
||||
if self.opts.generate_authors:
|
||||
self.generateNCXByAuthor("Authors")
|
||||
if self.opts.generate_titles:
|
||||
self.generateNCXByTitle("Titles")
|
||||
@ -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,6 +1891,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
|
||||
# Link to author
|
||||
emTag = Tag(soup, "em")
|
||||
aTag = Tag(soup, "a")
|
||||
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)
|
||||
@ -2149,6 +2162,7 @@ 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")
|
||||
if self.opts.generate_authors:
|
||||
aTag['name'] = "%s" % self.generateAuthorAnchor(current_author)
|
||||
aTag.insert(0,NavigableString(current_author))
|
||||
pAuthorTag.insert(0,aTag)
|
||||
@ -2276,6 +2290,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
|
||||
# Link to author
|
||||
emTag = Tag(soup, "em")
|
||||
aTag = Tag(soup, "a")
|
||||
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)
|
||||
@ -2425,6 +2440,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
|
||||
# Link to author
|
||||
emTag = Tag(soup, "em")
|
||||
aTag = Tag(soup, "a")
|
||||
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)
|
||||
@ -2473,6 +2489,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
|
||||
# Link to author
|
||||
emTag = Tag(soup, "em")
|
||||
aTag = Tag(soup, "a")
|
||||
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)
|
||||
@ -2692,6 +2709,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
|
||||
|
||||
# Link to author
|
||||
aTag = Tag(soup, "a")
|
||||
if self.opts.generate_authors:
|
||||
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor",
|
||||
self.generateAuthorAnchor(escape(' & '.join(book['authors']))))
|
||||
aTag.insert(0, NavigableString(' & '.join(book['authors'])))
|
||||
@ -2776,6 +2794,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
|
||||
genre_list.append(tag_list)
|
||||
|
||||
if self.opts.verbose:
|
||||
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)))
|
||||
|
||||
@ -2785,6 +2804,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
|
||||
len(genre[key]),
|
||||
'titles' if len(genre[key]) > 1 else 'title'))
|
||||
|
||||
|
||||
# Write the results
|
||||
# genre_list = [ {friendly_tag:[{book},{book}]}, {friendly_tag:[{book},{book}]}, ...]
|
||||
master_genre_list = []
|
||||
@ -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)
|
||||
|
||||
if self.opts.generate_authors:
|
||||
contentTag = Tag(soup, 'content')
|
||||
#contentTag['src'] = "content/book_%d.html" % int(self.booksByTitle[0]['id'])
|
||||
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,6 +4182,7 @@ 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")
|
||||
if self.opts.generate_authors:
|
||||
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author']))
|
||||
aTag.insert(0, book['author'])
|
||||
pAuthorTag.insert(0,aTag)
|
||||
@ -4371,6 +4414,7 @@ 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'})
|
||||
if self.opts.generate_authors:
|
||||
aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor",
|
||||
self.generateAuthorAnchor(book['author']))
|
||||
|
||||
@ -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 = []
|
||||
|
@ -361,6 +361,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
self.refresh()
|
||||
self.last_update_check = self.last_modified()
|
||||
|
||||
def break_cycles(self):
|
||||
self.data = self.field_metadata = self.prefs = self.listeners = None
|
||||
|
||||
def initialize_database(self):
|
||||
metadata_sqlite = open(P('metadata_sqlite.sql'), 'rb').read()
|
||||
|
@ -260,14 +260,14 @@ 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:`Preprocess input`
|
||||
This option activates various activates |app|'s Heuristic Processing stage of the conversion pipeline.
|
||||
:guilabel:`Enable heuristic processing`
|
||||
This option activates |app|'s Heuristic Processing stage of the conversion pipeline.
|
||||
This must be enabled in order for various sub-functions to be applied
|
||||
|
||||
:guilabel:`Unwrap lines`
|
||||
@ -283,22 +283,22 @@ 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,
|
||||
|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.
|
||||
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.
|
||||
|
||||
This function will not create a TOC, but in many cases it will cause |app|'s default chapter detection settings
|
||||
to correctly detect chapters and build a TOC. Adjust the Xpath under Structure Detection if a TOC is not automatically
|
||||
to correctly detect chapters and build a TOC. Adjust the XPath under Structure Detection if a TOC is not automatically
|
||||
created. If there are no other headings used in the document then setting "//h:h2" under Structure Detection would
|
||||
be the easiest way to create a TOC for the document.
|
||||
|
||||
The inserted headings are not formatted, to apply formatting use the 'extra_css' option under
|
||||
The inserted headings are not formatted, to apply formatting use the :guilabel:`Extra CSS` option under
|
||||
the Look and Feel conversion settings. For example, to center heading tags, use the following::
|
||||
|
||||
h2, h3 { text-align: center }
|
||||
|
||||
:guilabel:`Renumber sequences of <h1> or <h2> tags`
|
||||
Some publishers format chapter headings using multiple <h1> or <h2> tags sequentially.
|
||||
:guilabel:`Renumber sequences of <h1> or <h2> tags`
|
||||
Some publishers format chapter headings using multiple <h1> or <h2> tags sequentially.
|
||||
|app|'s default conversion settings will cause such titles to be split into two pieces. This option
|
||||
will re-number the heading tags to prevent splitting.
|
||||
|
||||
@ -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 http://docs.python.org/library/re.html.
|
||||
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:
|
||||
|
||||
|
@ -107,10 +107,10 @@ My device is not being detected by |app|?
|
||||
Follow these steps to find the problem:
|
||||
|
||||
* Make sure that you are connecting only a single device to your computer at a time. Do not have another |app| supported device like an iPhone/iPad etc. at the same time.
|
||||
* Make sure you are running the latest version of |app|. The latest version can always be downloaded from `http://calibre-ebook.com/download`_.
|
||||
* Ensure your operating system is seeing the device. That is, the device should be mounted as a disk that you can access using Windows explorer or whatever the file management program on your computer is
|
||||
* In calibre, go to Preferences->Plugins->Device Interface plugin and make sure the plugin for your device is enabled.
|
||||
* If all the above steps fail, go to Preferences->Miscellaneous and click debug device detection with your device attached and post the output as a ticket on `http://bugs.calibre-ebook.com`_.
|
||||
* 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, 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?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -21,7 +21,7 @@ This is, inevitably, going to be somewhat technical- after all, regular expressi
|
||||
Where in |app| can you use regular expressions?
|
||||
---------------------------------------------------
|
||||
|
||||
There are a few places |app| uses regular expressions. There's the header/footer removal in conversion options, metadata detection from filenames in the import settings and, since last version, there's the option to use regular expressions to search and replace in metadata of multiple books.
|
||||
There are a few places |app| uses regular expressions. There's the Search & Replace in conversion options, metadata detection from filenames in the import settings and Search & Replace when editing the metadata of books in bulk.
|
||||
|
||||
What on earth *is* a regular expression?
|
||||
------------------------------------------------
|
||||
@ -94,7 +94,7 @@ I think I'm beginning to understand these regular expressions now... how do I us
|
||||
Conversions
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
Let's begin with the conversion settings, which is really neat. In the structure detection part, you can input a regexp (short for regular expression) that describes the header or footer string that will be removed during the conversion. The neat part is the wizard. Click on the wizard staff and you get a preview of what |app| "sees" during the conversion process. Scroll down to the header or footer you want to remove, select and copy it, paste it into the regexp field on top of the window. If there are variable parts, like page numbers or so, use sets and quantifiers to cover those, and while you're at it, remember to escape special characters, if there are some. Hit the button labeled :guilabel:`Test` and |app| highlights the parts it would remove were you to use the regexp. Once you're satisfied, hit OK and convert. Be careful if your conversion source has tags like this example::
|
||||
Let's begin with the conversion settings, which is really neat. In the Search and Replace part, you can input a regexp (short for regular expression) that describes the string that will be replaced during the conversion. The neat part is the wizard. Click on the wizard staff and you get a preview of what |app| "sees" during the conversion process. Scroll down to the string you want to remove, select and copy it, paste it into the regexp field on top of the window. If there are variable parts, like page numbers or so, use sets and quantifiers to cover those, and while you're at it, remember to escape special characters, if there are some. Hit the button labeled :guilabel:`Test` and |app| highlights the parts it would replace were you to use the regexp. Once you're satisfied, hit OK and convert. Be careful if your conversion source has tags like this example::
|
||||
|
||||
Maybe, but the cops feel like you do, Anita. What's one more dead vampire?
|
||||
New laws don't change that. </p>
|
||||
@ -104,7 +104,7 @@ Let's begin with the conversion settings, which is really neat. In the structure
|
||||
<p class="calibre4"> It had only been two years since Addison v. Clark.
|
||||
The court case gave us a revised version of what life was
|
||||
|
||||
(shamelessly ripped out of `this thread <http://www.mobileread.com/forums/showthread.php?t=75594">`_). You'd have to remove some of the tags as well. In this example, I'd recommend beginning with the tag ``<b class="calibre2">``, now you have to end with the corresponding closing tag (opening tags are ``<tag>``, closing tags are ``</tag>``), which is simply the next ``</b>`` in this case. (Refer to a good HTML manual or ask in the forum if you are unclear on this point.) The opening tag can be described using ``<b.*?>``, the closing tag using ``</b>``, thus we could remove everything between those tags using ``<b.*?>.*?</b>``. But using this expression would be a bad idea, because it removes everything enclosed by <b>- tags (which, by the way, render the enclosed text in bold print), and it's a fair bet that we'll remove portions of the book in this way. Instead, include the beginning of the enclosed string as well, making the regular expression ``<b.*?>\s*Generated\s+by\s+ABC\s+Amber\s+LIT.*?</b>`` The ``\s`` with quantifiers are included here instead of explicitly using the spaces as seen in the string to catch any variations of the string that might occur. Remember to check what |app| will remove to make sure you don't remove any portions you want to keep if you test a new expression. If you only check one occurrence, you might miss a mismatch somewhere else in the text. Also note that should you accidentally remove more or fewer tags than you actually wanted to, |app| tries to repair the damaged code after doing the header/footer removal.
|
||||
(shamelessly ripped out of `this thread <http://www.mobileread.com/forums/showthread.php?t=75594">`_). You'd have to remove some of the tags as well. In this example, I'd recommend beginning with the tag ``<b class="calibre2">``, now you have to end with the corresponding closing tag (opening tags are ``<tag>``, closing tags are ``</tag>``), which is simply the next ``</b>`` in this case. (Refer to a good HTML manual or ask in the forum if you are unclear on this point.) The opening tag can be described using ``<b.*?>``, the closing tag using ``</b>``, thus we could remove everything between those tags using ``<b.*?>.*?</b>``. But using this expression would be a bad idea, because it removes everything enclosed by <b>- tags (which, by the way, render the enclosed text in bold print), and it's a fair bet that we'll remove portions of the book in this way. Instead, include the beginning of the enclosed string as well, making the regular expression ``<b.*?>\s*Generated\s+by\s+ABC\s+Amber\s+LIT.*?</b>`` The ``\s`` with quantifiers are included here instead of explicitly using the spaces as seen in the string to catch any variations of the string that might occur. Remember to check what |app| will remove to make sure you don't remove any portions you want to keep if you test a new expression. If you only check one occurrence, you might miss a mismatch somewhere else in the text. Also note that should you accidentally remove more or fewer tags than you actually wanted to, |app| tries to repair the damaged code after doing the removal.
|
||||
|
||||
Adding books
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
@ -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,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -42,30 +42,44 @@ def supports_long_names(path):
|
||||
else:
|
||||
return True
|
||||
|
||||
def shorten_components_to(length, components):
|
||||
def shorten_component(s, by_what):
|
||||
l = len(s)
|
||||
if l < by_what:
|
||||
return s
|
||||
l = (l - by_what)//2
|
||||
if l <= 0:
|
||||
return s
|
||||
return s[:l] + s[-l:]
|
||||
|
||||
def shorten_components_to(length, components, more_to_take=0):
|
||||
filepath = os.sep.join(components)
|
||||
extra = len(filepath) - length
|
||||
extra = len(filepath) - (length - more_to_take)
|
||||
if extra < 1:
|
||||
return components
|
||||
delta = int(ceil(extra/float(len(components))))
|
||||
ans = []
|
||||
deltas = []
|
||||
for x in components:
|
||||
pct = len(x)/float(len(filepath))
|
||||
deltas.append(int(ceil(pct*extra)))
|
||||
ans = []
|
||||
|
||||
for i, x in enumerate(components):
|
||||
delta = deltas[i]
|
||||
if delta > len(x):
|
||||
r = x[0] if x is components[-1] else ''
|
||||
else:
|
||||
if x is components[-1]:
|
||||
b, e = os.path.splitext(x)
|
||||
if e == '.': e = ''
|
||||
r = b[:-delta]+e
|
||||
r = shorten_component(b, delta)+e
|
||||
if r.startswith('.'): r = x[0]+r
|
||||
else:
|
||||
r = x[:-delta]
|
||||
r = shorten_component(x, delta)
|
||||
r = r.strip()
|
||||
if not r:
|
||||
r = x.strip()[0] if x.strip() else 'x'
|
||||
ans.append(r)
|
||||
if len(os.sep.join(ans)) > length:
|
||||
return shorten_components_to(length, ans)
|
||||
return shorten_components_to(length, components, more_to_take+2)
|
||||
return ans
|
||||
|
||||
def find_executable_in_path(name, path=None):
|
||||
|
@ -75,7 +75,7 @@ class FormatterFunction(object):
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
info = ': '.join(traceback.format_exception(exc_type, exc_value,
|
||||
exc_traceback)[-2:]).replace('\n', '')
|
||||
return _('Exception ' + info)
|
||||
return _('Exception ') + info
|
||||
|
||||
all_builtin_functions = []
|
||||
class BuiltinFormatterFunction(FormatterFunction):
|
||||
|
@ -28,6 +28,9 @@ PARALLEL_FUNCS = {
|
||||
'gui_convert' :
|
||||
('calibre.gui2.convert.gui_conversion', 'gui_convert', 'notification'),
|
||||
|
||||
'gui_convert_override' :
|
||||
('calibre.gui2.convert.gui_conversion', 'gui_convert_override', 'notification'),
|
||||
|
||||
'gui_catalog' :
|
||||
('calibre.gui2.convert.gui_conversion', 'gui_catalog', 'notification'),
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user