mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
c07699ce42
@ -8,7 +8,7 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
|||||||
|
|
||||||
class Lifehacker(BasicNewsRecipe):
|
class Lifehacker(BasicNewsRecipe):
|
||||||
title = 'Lifehacker'
|
title = 'Lifehacker'
|
||||||
__author__ = 'NA'
|
__author__ = 'Kovid Goyal'
|
||||||
description = "Computers make us more productive. Yeah, right. Lifehacker recommends the software downloads and web sites that actually save time. Don't live to geek; geek to live."
|
description = "Computers make us more productive. Yeah, right. Lifehacker recommends the software downloads and web sites that actually save time. Don't live to geek; geek to live."
|
||||||
publisher = 'lifehacker.com'
|
publisher = 'lifehacker.com'
|
||||||
category = 'news, IT, Internet, gadgets, tips and tricks, howto, diy'
|
category = 'news, IT, Internet, gadgets, tips and tricks, howto, diy'
|
||||||
@ -32,14 +32,20 @@ class Lifehacker(BasicNewsRecipe):
|
|||||||
, 'language' : language
|
, 'language' : language
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_attributes = ['width','height']
|
remove_attributes = ['width', 'height', 'style']
|
||||||
keep_only_tags = [dict(attrs={'class':'content permalink'})]
|
|
||||||
remove_tags_before = dict(name='h1')
|
remove_tags_before = dict(name='h1')
|
||||||
remove_tags = [dict(attrs={'class':'contactinfo'})]
|
keep_only_tags = [dict(id='container')]
|
||||||
remove_tags_after = dict(attrs={'class':'contactinfo'})
|
remove_tags_after = dict(attrs={'class':'post-body'})
|
||||||
|
remove_tags = [
|
||||||
|
dict(id="sharemenu"),
|
||||||
|
{'class': 'related'},
|
||||||
|
]
|
||||||
|
|
||||||
feeds = [(u'Articles', u'http://feeds.gawker.com/lifehacker/full')]
|
feeds = [(u'Articles', u'http://feeds.gawker.com/lifehacker/full')]
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
return self.adeify_images(soup)
|
return self.adeify_images(soup)
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
return url.replace('#!', '?_escaped_fragment_=')
|
||||||
|
|
||||||
|
@ -668,7 +668,7 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
#remove "Related content" bar
|
#remove "Related content" bar
|
||||||
runAroundsFound = soup.findAll('div',{'class':['articleInline runaroundLeft','articleInline doubleRule runaroundLeft','articleInline runaroundLeft firstArticleInline','articleInline runaroundLeft ']})
|
runAroundsFound = soup.findAll('div',{'class':['articleInline runaroundLeft','articleInline doubleRule runaroundLeft','articleInline runaroundLeft firstArticleInline','articleInline runaroundLeft ','articleInline runaroundLeft lastArticleInline']})
|
||||||
if runAroundsFound:
|
if runAroundsFound:
|
||||||
for runAround in runAroundsFound:
|
for runAround in runAroundsFound:
|
||||||
#find all section headers
|
#find all section headers
|
||||||
|
26
resources/recipes/workers_world.recipe
Normal file
26
resources/recipes/workers_world.recipe
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||||
|
|
||||||
|
class WorkersWorld(BasicNewsRecipe):
|
||||||
|
|
||||||
|
title = u'Workers World'
|
||||||
|
description = u'Socialist news and analysis'
|
||||||
|
__author__ = u'urslnx'
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
remove_javascript = True
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
encoding = 'utf8'
|
||||||
|
publisher = 'workers.org'
|
||||||
|
category = 'news, politics, USA, world'
|
||||||
|
language = 'en'
|
||||||
|
publication_type = 'newsportal'
|
||||||
|
extra_css = ' body{ font-family: Verdana,Arial,Helvetica,sans-serif; } h1{ font-size: x-large; text-align: left; margin-top:0.5em; margin-bottom:0.25em; } h2{ font-size: large; } p{ text-align: left; } .published{ font-size: small; } .byline{ font-size: small; } .copyright{ font-size: small; } '
|
||||||
|
remove_tags_before = dict(name='div', attrs={'id':'evernote'})
|
||||||
|
remove_tags_after = dict(name='div', attrs={'id':'footer'})
|
||||||
|
|
||||||
|
masthead_url='http://www.workers.org/graphics/wwlogo300.gif'
|
||||||
|
cover_url = 'http://www.workers.org/pdf/current.jpg'
|
||||||
|
feeds = [(u'Headlines', u'http://www.workers.org/rss/nonstandard_rss.xml'),
|
||||||
|
]
|
||||||
|
|
@ -90,6 +90,11 @@ class Plugin(object): # {{{
|
|||||||
an optional method validate() that takes no arguments and is called
|
an optional method validate() that takes no arguments and is called
|
||||||
immediately after the user clicks OK. Changes are applied if and only
|
immediately after the user clicks OK. Changes are applied if and only
|
||||||
if the method returns True.
|
if the method returns True.
|
||||||
|
|
||||||
|
If for some reason you cannot perform the configuration at this time,
|
||||||
|
return a tuple of two strings (message, details), these will be
|
||||||
|
displayed as a warning dialog to the user and the process will be
|
||||||
|
aborted.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@ -133,6 +138,12 @@ class Plugin(object): # {{{
|
|||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
config_widget = None
|
config_widget = None
|
||||||
|
|
||||||
|
if isinstance(config_widget, tuple):
|
||||||
|
from calibre.gui2 import warning_dialog
|
||||||
|
warning_dialog(parent, _('Cannot configure'), config_widget[0],
|
||||||
|
det_msg=config_widget[1], show=True)
|
||||||
|
return False
|
||||||
|
|
||||||
if config_widget is not None:
|
if config_widget is not None:
|
||||||
v.addWidget(config_widget)
|
v.addWidget(config_widget)
|
||||||
v.addWidget(button_box)
|
v.addWidget(button_box)
|
||||||
|
@ -511,14 +511,14 @@ from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
|
|||||||
from calibre.ebooks.metadata.douban import DoubanBooks
|
from calibre.ebooks.metadata.douban import DoubanBooks
|
||||||
from calibre.ebooks.metadata.nicebooks import NiceBooks, NiceBooksCovers
|
from calibre.ebooks.metadata.nicebooks import NiceBooks, NiceBooksCovers
|
||||||
from calibre.ebooks.metadata.covers import OpenLibraryCovers, \
|
from calibre.ebooks.metadata.covers import OpenLibraryCovers, \
|
||||||
LibraryThingCovers, DoubanCovers
|
AmazonCovers, DoubanCovers
|
||||||
from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX
|
from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX
|
||||||
from calibre.ebooks.epub.fix.unmanifested import Unmanifested
|
from calibre.ebooks.epub.fix.unmanifested import Unmanifested
|
||||||
from calibre.ebooks.epub.fix.epubcheck import Epubcheck
|
from calibre.ebooks.epub.fix.epubcheck import Epubcheck
|
||||||
|
|
||||||
plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon,
|
plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon,
|
||||||
KentDistrictLibrary, DoubanBooks, NiceBooks, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested,
|
KentDistrictLibrary, DoubanBooks, NiceBooks, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested,
|
||||||
Epubcheck, OpenLibraryCovers, LibraryThingCovers, DoubanCovers,
|
Epubcheck, OpenLibraryCovers, AmazonCovers, DoubanCovers,
|
||||||
NiceBooksCovers]
|
NiceBooksCovers]
|
||||||
plugins += [
|
plugins += [
|
||||||
ComicInput,
|
ComicInput,
|
||||||
|
@ -19,7 +19,7 @@ class ANDROID(USBMS):
|
|||||||
|
|
||||||
VENDOR_ID = {
|
VENDOR_ID = {
|
||||||
# HTC
|
# HTC
|
||||||
0x0bb4 : { 0x0c02 : [0x100, 0x0227, 0x0226],
|
0x0bb4 : { 0x0c02 : [0x100, 0x0227, 0x0226, 0x222],
|
||||||
0x0c01 : [0x100, 0x0227, 0x0226],
|
0x0c01 : [0x100, 0x0227, 0x0226],
|
||||||
0x0ff9 : [0x0100, 0x0227, 0x0226],
|
0x0ff9 : [0x0100, 0x0227, 0x0226],
|
||||||
0x0c87 : [0x0100, 0x0227, 0x0226],
|
0x0c87 : [0x0100, 0x0227, 0x0226],
|
||||||
|
@ -39,6 +39,7 @@ if iswindows:
|
|||||||
class DriverBase(DeviceConfig, DevicePlugin):
|
class DriverBase(DeviceConfig, DevicePlugin):
|
||||||
# Needed for config_widget to work
|
# Needed for config_widget to work
|
||||||
FORMATS = ['epub', 'pdf']
|
FORMATS = ['epub', 'pdf']
|
||||||
|
USER_CAN_ADD_NEW_FORMATS = False
|
||||||
SUPPORTS_SUB_DIRS = True # To enable second checkbox in customize widget
|
SUPPORTS_SUB_DIRS = True # To enable second checkbox in customize widget
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -32,6 +32,7 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
|
|||||||
ip = None
|
ip = None
|
||||||
|
|
||||||
FORMATS = [ "snb" ]
|
FORMATS = [ "snb" ]
|
||||||
|
USER_CAN_ADD_NEW_FORMATS = False
|
||||||
VENDOR_ID = 0x230b
|
VENDOR_ID = 0x230b
|
||||||
PRODUCT_ID = 0x0001
|
PRODUCT_ID = 0x0001
|
||||||
BCD = None
|
BCD = None
|
||||||
@ -421,7 +422,7 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
|
|||||||
from calibre.gui2.device_drivers.configwidget import ConfigWidget
|
from calibre.gui2.device_drivers.configwidget import ConfigWidget
|
||||||
cw = ConfigWidget(cls.settings(), cls.FORMATS, cls.SUPPORTS_SUB_DIRS,
|
cw = ConfigWidget(cls.settings(), cls.FORMATS, cls.SUPPORTS_SUB_DIRS,
|
||||||
cls.MUST_READ_METADATA, cls.SUPPORTS_USE_AUTHOR_SORT,
|
cls.MUST_READ_METADATA, cls.SUPPORTS_USE_AUTHOR_SORT,
|
||||||
cls.EXTRA_CUSTOMIZATION_MESSAGE)
|
cls.EXTRA_CUSTOMIZATION_MESSAGE, cls)
|
||||||
# Turn off the Save template
|
# Turn off the Save template
|
||||||
cw.opt_save_template.setVisible(False)
|
cw.opt_save_template.setVisible(False)
|
||||||
cw.label.setVisible(False)
|
cw.label.setVisible(False)
|
||||||
|
@ -93,11 +93,11 @@ class MIBUK(USBMS):
|
|||||||
|
|
||||||
VENDOR_ID = [0x0525]
|
VENDOR_ID = [0x0525]
|
||||||
PRODUCT_ID = [0xa4a5]
|
PRODUCT_ID = [0xa4a5]
|
||||||
BCD = [0x314]
|
BCD = [0x314, 0x319]
|
||||||
SUPPORTS_SUB_DIRS = True
|
SUPPORTS_SUB_DIRS = True
|
||||||
|
|
||||||
VENDOR_NAME = 'LINUX'
|
VENDOR_NAME = ['LINUX', 'FILE_BAC']
|
||||||
WINDOWS_MAIN_MEM = 'WOLDERMIBUK'
|
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['WOLDERMIBUK', 'KED_STORAGE_GADG']
|
||||||
|
|
||||||
class JETBOOK_MINI(USBMS):
|
class JETBOOK_MINI(USBMS):
|
||||||
|
|
||||||
|
@ -11,44 +11,42 @@ Generates and writes an APNX page mapping file.
|
|||||||
import struct
|
import struct
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from calibre.ebooks.mobi.reader import MobiReader
|
||||||
from calibre.ebooks.pdb.header import PdbHeaderReader
|
from calibre.ebooks.pdb.header import PdbHeaderReader
|
||||||
|
from calibre.utils.logging import default_log
|
||||||
|
|
||||||
class APNXBuilder(object):
|
class APNXBuilder(object):
|
||||||
'''
|
'''
|
||||||
2300 characters of uncompressed text per page. This is
|
Create an APNX file using a pseudo page mapping.
|
||||||
not meant to map 1 to 1 to a print book but to be a
|
|
||||||
close enough measure.
|
|
||||||
|
|
||||||
A test book was chosen and the characters were counted
|
|
||||||
on one page. This number was round to 2240 then 60
|
|
||||||
characters of markup were added to the total giving
|
|
||||||
2300.
|
|
||||||
|
|
||||||
Uncompressed text length is used because it's easily
|
|
||||||
accessible in MOBI files (part of the header). Also,
|
|
||||||
It's faster to work off of the length then to
|
|
||||||
decompress and parse the actual text.
|
|
||||||
|
|
||||||
A better but much more resource intensive and slower
|
|
||||||
method to calculate the page length would be to parse
|
|
||||||
the uncompressed text. For each paragraph we would
|
|
||||||
want to find how many lines it would occupy in a paper
|
|
||||||
back book. 70 characters per line and 32 lines per page.
|
|
||||||
So divide the number of characters (minus markup) in
|
|
||||||
each paragraph by 70. If there are less than 70
|
|
||||||
characters in the paragraph then it is 1 line. Then,
|
|
||||||
count every 32 lines and mark that location as a page.
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def write_apnx(self, mobi_file_path, apnx_path):
|
def write_apnx(self, mobi_file_path, apnx_path, accurate=True):
|
||||||
|
# Check that this is really a MOBI file.
|
||||||
with open(mobi_file_path, 'rb') as mf:
|
with open(mobi_file_path, 'rb') as mf:
|
||||||
phead = PdbHeaderReader(mf)
|
ident = PdbHeaderReader(mf).identity()
|
||||||
r0 = phead.section_data(0)
|
if ident != 'BOOKMOBI':
|
||||||
text_length = struct.unpack('>I', r0[4:8])[0]
|
raise Exception(_('Not a valid MOBI file. Reports identity of %s' % ident))
|
||||||
|
|
||||||
pages = self.get_pages(text_length)
|
# Get the pages depending on the chosen parser
|
||||||
|
pages = []
|
||||||
|
if accurate:
|
||||||
|
try:
|
||||||
|
pages = self.get_pages_accurate(mobi_file_path)
|
||||||
|
except:
|
||||||
|
# Fall back to the fast parser if we can't
|
||||||
|
# use the accurate one. Typically this is
|
||||||
|
# due to the file having DRM.
|
||||||
|
pages = self.get_pages_fast(mobi_file_path)
|
||||||
|
else:
|
||||||
|
pages = self.get_pages_fast(mobi_file_path)
|
||||||
|
|
||||||
|
if not pages:
|
||||||
|
raise Exception(_('Could not generate page mapping.'))
|
||||||
|
|
||||||
|
# Generate the APNX file from the page mapping.
|
||||||
apnx = self.generate_apnx(pages)
|
apnx = self.generate_apnx(pages)
|
||||||
|
|
||||||
|
# Write the APNX.
|
||||||
with open(apnx_path, 'wb') as apnxf:
|
with open(apnx_path, 'wb') as apnxf:
|
||||||
apnxf.write(apnx)
|
apnxf.write(apnx)
|
||||||
|
|
||||||
@ -73,18 +71,126 @@ class APNXBuilder(object):
|
|||||||
apnx += struct.pack('>H', 32)
|
apnx += struct.pack('>H', 32)
|
||||||
apnx += page_header
|
apnx += page_header
|
||||||
|
|
||||||
# write page values to apnx
|
# Write page values to APNX.
|
||||||
for page in pages:
|
for page in pages:
|
||||||
apnx += struct.pack('>L', page)
|
apnx += struct.pack('>I', page)
|
||||||
|
|
||||||
return apnx
|
return apnx
|
||||||
|
|
||||||
def get_pages(self, text_length):
|
def get_pages_fast(self, mobi_file_path):
|
||||||
|
'''
|
||||||
|
2300 characters of uncompressed text per page. This is
|
||||||
|
not meant to map 1 to 1 to a print book but to be a
|
||||||
|
close enough measure.
|
||||||
|
|
||||||
|
A test book was chosen and the characters were counted
|
||||||
|
on one page. This number was round to 2240 then 60
|
||||||
|
characters of markup were added to the total giving
|
||||||
|
2300.
|
||||||
|
|
||||||
|
Uncompressed text length is used because it's easily
|
||||||
|
accessible in MOBI files (part of the header). Also,
|
||||||
|
It's faster to work off of the length then to
|
||||||
|
decompress and parse the actual text.
|
||||||
|
'''
|
||||||
|
text_length = 0
|
||||||
pages = []
|
pages = []
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
|
with open(mobi_file_path, 'rb') as mf:
|
||||||
|
phead = PdbHeaderReader(mf)
|
||||||
|
r0 = phead.section_data(0)
|
||||||
|
text_length = struct.unpack('>I', r0[4:8])[0]
|
||||||
|
|
||||||
while count < text_length:
|
while count < text_length:
|
||||||
pages.append(count)
|
pages.append(count)
|
||||||
count += 2300
|
count += 2300
|
||||||
|
|
||||||
return pages
|
return pages
|
||||||
|
|
||||||
|
def get_pages_accurate(self, mobi_file_path):
|
||||||
|
'''
|
||||||
|
A more accurate but much more resource intensive and slower
|
||||||
|
method to calculate the page length.
|
||||||
|
|
||||||
|
Parses the uncompressed text. In an average paper back book
|
||||||
|
There are 32 lines per page and a maximum of 70 characters
|
||||||
|
per line.
|
||||||
|
|
||||||
|
Each paragraph starts a new line and every 70 characters
|
||||||
|
(minus markup) in a paragraph starts a new line. The
|
||||||
|
position after every 30 lines will be marked as a new
|
||||||
|
page.
|
||||||
|
|
||||||
|
This can be make more accurate by accounting for
|
||||||
|
<div class="mbp_pagebreak" /> as a new page marker.
|
||||||
|
And <br> elements as an empty line.
|
||||||
|
'''
|
||||||
|
pages = []
|
||||||
|
|
||||||
|
# Get the MOBI html.
|
||||||
|
mr = MobiReader(mobi_file_path, default_log)
|
||||||
|
if mr.book_header.encryption_type != 0:
|
||||||
|
# DRMed book
|
||||||
|
return self.get_pages_fast(mobi_file_path)
|
||||||
|
mr.extract_text()
|
||||||
|
|
||||||
|
# States
|
||||||
|
in_tag = False
|
||||||
|
in_p = False
|
||||||
|
check_p = False
|
||||||
|
closing = False
|
||||||
|
p_char_count = 0
|
||||||
|
|
||||||
|
# Get positions of every line
|
||||||
|
# A line is either a paragraph starting
|
||||||
|
# or every 70 characters in a paragraph.
|
||||||
|
lines = []
|
||||||
|
pos = -1
|
||||||
|
# We want this to be as fast as possible so we
|
||||||
|
# are going to do one pass across the text. re
|
||||||
|
# and string functions will parse the text each
|
||||||
|
# time they are called.
|
||||||
|
#
|
||||||
|
# We can can use .lower() here because we are
|
||||||
|
# not modifying the text. In this case the case
|
||||||
|
# doesn't matter just the absolute character and
|
||||||
|
# the position within the stream.
|
||||||
|
for c in mr.mobi_html.lower():
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
# Check if we are starting or stopping a p tag.
|
||||||
|
if check_p:
|
||||||
|
if c == '/':
|
||||||
|
closing = True
|
||||||
|
continue
|
||||||
|
elif c == 'p':
|
||||||
|
if closing:
|
||||||
|
in_p = False
|
||||||
|
else:
|
||||||
|
in_p = True
|
||||||
|
lines.append(pos - 2)
|
||||||
|
check_p = False
|
||||||
|
closing = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
if c == '<':
|
||||||
|
in_tag = True
|
||||||
|
check_p = True
|
||||||
|
continue
|
||||||
|
elif c == '>':
|
||||||
|
in_tag = False
|
||||||
|
check_p = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_p and not in_tag:
|
||||||
|
p_char_count += 1
|
||||||
|
if p_char_count == 70:
|
||||||
|
lines.append(pos)
|
||||||
|
p_char_count = 0
|
||||||
|
|
||||||
|
# Every 30 lines is a new page
|
||||||
|
for i in xrange(0, len(lines), 32):
|
||||||
|
pages.append(lines[i])
|
||||||
|
|
||||||
|
return pages
|
||||||
|
@ -176,6 +176,28 @@ class KINDLE2(KINDLE):
|
|||||||
PRODUCT_ID = [0x0002, 0x0004]
|
PRODUCT_ID = [0x0002, 0x0004]
|
||||||
BCD = [0x0100]
|
BCD = [0x0100]
|
||||||
|
|
||||||
|
EXTRA_CUSTOMIZATION_MESSAGE = [
|
||||||
|
_('Send page number information when sending books') +
|
||||||
|
':::' +
|
||||||
|
_('The Kindle 3 and newer versions can use page number information '
|
||||||
|
'in MOBI files. With this option, calibre will calculate and send'
|
||||||
|
' this information to the Kindle when uploading MOBI files by'
|
||||||
|
' USB. Note that the page numbers do not correspond to any paper'
|
||||||
|
' book.'),
|
||||||
|
_('Use slower but more accurate page number generation') +
|
||||||
|
':::' +
|
||||||
|
_('There are two ways to generate the page number information. Using the more accurate '
|
||||||
|
'generator will produce pages that correspond better to a printed book. '
|
||||||
|
'However, this method is slower and will slow down sending files '
|
||||||
|
'to the Kindle.'),
|
||||||
|
]
|
||||||
|
EXTRA_CUSTOMIZATION_DEFAULT = [
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
]
|
||||||
|
OPT_APNX = 0
|
||||||
|
OPT_APNX_ACCURATE = 1
|
||||||
|
|
||||||
def books(self, oncard=None, end_session=True):
|
def books(self, oncard=None, end_session=True):
|
||||||
bl = USBMS.books(self, oncard=oncard, end_session=end_session)
|
bl = USBMS.books(self, oncard=oncard, end_session=end_session)
|
||||||
# Read collections information
|
# Read collections information
|
||||||
@ -212,13 +234,17 @@ class KINDLE2(KINDLE):
|
|||||||
'''
|
'''
|
||||||
Hijacking this function to write the apnx file.
|
Hijacking this function to write the apnx file.
|
||||||
'''
|
'''
|
||||||
if not filepath.lower().endswith('.mobi'):
|
opts = self.settings()
|
||||||
|
if not opts.extra_customization[self.OPT_APNX]:
|
||||||
|
return
|
||||||
|
|
||||||
|
if os.path.splitext(filepath.lower())[1] not in ('.azw', '.mobi', '.prc'):
|
||||||
return
|
return
|
||||||
|
|
||||||
apnx_path = '%s.apnx' % os.path.join(path, filename)
|
apnx_path = '%s.apnx' % os.path.join(path, filename)
|
||||||
apnx_builder = APNXBuilder()
|
apnx_builder = APNXBuilder()
|
||||||
try:
|
try:
|
||||||
apnx_builder.write_apnx(filepath, apnx_path)
|
apnx_builder.write_apnx(filepath, apnx_path, accurate=opts.extra_customization[self.OPT_APNX_ACCURATE])
|
||||||
except:
|
except:
|
||||||
print 'Failed to generate APNX'
|
print 'Failed to generate APNX'
|
||||||
import traceback
|
import traceback
|
||||||
|
@ -98,7 +98,6 @@ class KOBO(USBMS):
|
|||||||
|
|
||||||
def update_booklist(prefix, path, title, authors, mime, date, ContentType, ImageID, readstatus, MimeType):
|
def update_booklist(prefix, path, title, authors, mime, date, ContentType, ImageID, readstatus, MimeType):
|
||||||
changed = False
|
changed = False
|
||||||
# if path_to_ext(path) in self.FORMATS:
|
|
||||||
try:
|
try:
|
||||||
lpath = path.partition(self.normalize_path(prefix))[2]
|
lpath = path.partition(self.normalize_path(prefix))[2]
|
||||||
if lpath.startswith(os.sep):
|
if lpath.startswith(os.sep):
|
||||||
|
@ -34,6 +34,10 @@ class DeviceConfig(object):
|
|||||||
#: If None the default is used
|
#: If None the default is used
|
||||||
SAVE_TEMPLATE = None
|
SAVE_TEMPLATE = None
|
||||||
|
|
||||||
|
#: If True the user can add new formats to the driver
|
||||||
|
USER_CAN_ADD_NEW_FORMATS = True
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _default_save_template(cls):
|
def _default_save_template(cls):
|
||||||
from calibre.library.save_to_disk import config
|
from calibre.library.save_to_disk import config
|
||||||
@ -73,7 +77,7 @@ class DeviceConfig(object):
|
|||||||
from calibre.gui2.device_drivers.configwidget import ConfigWidget
|
from calibre.gui2.device_drivers.configwidget import ConfigWidget
|
||||||
cw = ConfigWidget(cls.settings(), cls.FORMATS, cls.SUPPORTS_SUB_DIRS,
|
cw = ConfigWidget(cls.settings(), cls.FORMATS, cls.SUPPORTS_SUB_DIRS,
|
||||||
cls.MUST_READ_METADATA, cls.SUPPORTS_USE_AUTHOR_SORT,
|
cls.MUST_READ_METADATA, cls.SUPPORTS_USE_AUTHOR_SORT,
|
||||||
cls.EXTRA_CUSTOMIZATION_MESSAGE)
|
cls.EXTRA_CUSTOMIZATION_MESSAGE, cls)
|
||||||
return cw
|
return cw
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -93,9 +93,11 @@ class USBMS(CLI, Device):
|
|||||||
for idx,b in enumerate(bl):
|
for idx,b in enumerate(bl):
|
||||||
bl_cache[b.lpath] = idx
|
bl_cache[b.lpath] = idx
|
||||||
|
|
||||||
|
all_formats = set(self.settings().format_map) | set(self.FORMATS)
|
||||||
|
|
||||||
def update_booklist(filename, path, prefix):
|
def update_booklist(filename, path, prefix):
|
||||||
changed = False
|
changed = False
|
||||||
if path_to_ext(filename) in self.FORMATS:
|
if path_to_ext(filename) in all_formats:
|
||||||
try:
|
try:
|
||||||
lpath = os.path.join(path, filename).partition(self.normalize_path(prefix))[2]
|
lpath = os.path.join(path, filename).partition(self.normalize_path(prefix))[2]
|
||||||
if lpath.startswith(os.sep):
|
if lpath.startswith(os.sep):
|
||||||
|
@ -156,17 +156,17 @@ class HeuristicProcessor(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
ITALICIZE_STYLE_PATS = [
|
ITALICIZE_STYLE_PATS = [
|
||||||
r'(?msu)(?<=[\s>])_(?P<words>[^_]+)?_',
|
r'(?msu)(?<=[\s>])_(?P<words>[^_]+)_',
|
||||||
r'(?msu)(?<=[\s>])/(?P<words>[^/]+)?/',
|
r'(?msu)(?<=[\s>])/(?P<words>[^/]+)/',
|
||||||
r'(?msu)(?<=[\s>])~~(?P<words>[^~]+)?~~',
|
r'(?msu)(?<=[\s>])~~(?P<words>[^~]+)~~',
|
||||||
r'(?msu)(?<=[\s>])\*(?P<words>[^\*]+)?\*',
|
r'(?msu)(?<=[\s>])\*(?P<words>[^\*]+)\*',
|
||||||
r'(?msu)(?<=[\s>])~(?P<words>[^~]+)?~',
|
r'(?msu)(?<=[\s>])~(?P<words>[^~]+)~',
|
||||||
r'(?msu)(?<=[\s>])_/(?P<words>[^/_]+)?/_',
|
r'(?msu)(?<=[\s>])_/(?P<words>[^/_]+)/_',
|
||||||
r'(?msu)(?<=[\s>])_\*(?P<words>[^\*_]+)?\*_',
|
r'(?msu)(?<=[\s>])_\*(?P<words>[^\*_]+)\*_',
|
||||||
r'(?msu)(?<=[\s>])\*/(?P<words>[^/\*]+)?/\*',
|
r'(?msu)(?<=[\s>])\*/(?P<words>[^/\*]+)/\*',
|
||||||
r'(?msu)(?<=[\s>])_\*/(?P<words>[^\*_]+)?/\*_',
|
r'(?msu)(?<=[\s>])_\*/(?P<words>[^\*_]+)/\*_',
|
||||||
r'(?msu)(?<=[\s>])/:(?P<words>[^:/]+)?:/',
|
r'(?msu)(?<=[\s>])/:(?P<words>[^:/]+):/',
|
||||||
r'(?msu)(?<=[\s>])\|:(?P<words>[^:\|]+)?:\|',
|
r'(?msu)(?<=[\s>])\|:(?P<words>[^:\|]+):\|',
|
||||||
]
|
]
|
||||||
|
|
||||||
for word in ITALICIZE_WORDS:
|
for word in ITALICIZE_WORDS:
|
||||||
|
@ -271,6 +271,8 @@ def check_isbn13(isbn):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def check_isbn(isbn):
|
def check_isbn(isbn):
|
||||||
|
if not isbn:
|
||||||
|
return None
|
||||||
isbn = re.sub(r'[^0-9X]', '', isbn.upper())
|
isbn = re.sub(r'[^0-9X]', '', isbn.upper())
|
||||||
if len(isbn) == 10:
|
if len(isbn) == 10:
|
||||||
return check_isbn10(isbn)
|
return check_isbn10(isbn)
|
||||||
|
@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
Fetch metadata using Amazon AWS
|
Fetch metadata using Amazon AWS
|
||||||
'''
|
'''
|
||||||
import sys, re
|
import sys, re
|
||||||
|
from threading import RLock
|
||||||
|
|
||||||
from lxml import html
|
from lxml import html
|
||||||
from lxml.html import soupparser
|
from lxml.html import soupparser
|
||||||
@ -17,6 +18,10 @@ from calibre.ebooks.metadata.book.base import Metadata
|
|||||||
from calibre.ebooks.chardet import xml_to_unicode
|
from calibre.ebooks.chardet import xml_to_unicode
|
||||||
from calibre.library.comments import sanitize_comments_html
|
from calibre.library.comments import sanitize_comments_html
|
||||||
|
|
||||||
|
asin_cache = {}
|
||||||
|
cover_url_cache = {}
|
||||||
|
cache_lock = RLock()
|
||||||
|
|
||||||
def find_asin(br, isbn):
|
def find_asin(br, isbn):
|
||||||
q = 'http://www.amazon.com/s?field-keywords='+isbn
|
q = 'http://www.amazon.com/s?field-keywords='+isbn
|
||||||
raw = br.open_novisit(q).read()
|
raw = br.open_novisit(q).read()
|
||||||
@ -29,6 +34,12 @@ def find_asin(br, isbn):
|
|||||||
return revs[0]
|
return revs[0]
|
||||||
|
|
||||||
def to_asin(br, isbn):
|
def to_asin(br, isbn):
|
||||||
|
with cache_lock:
|
||||||
|
ans = asin_cache.get(isbn, None)
|
||||||
|
if ans:
|
||||||
|
return ans
|
||||||
|
if ans is False:
|
||||||
|
return None
|
||||||
if len(isbn) == 13:
|
if len(isbn) == 13:
|
||||||
try:
|
try:
|
||||||
asin = find_asin(br, isbn)
|
asin = find_asin(br, isbn)
|
||||||
@ -38,8 +49,11 @@ def to_asin(br, isbn):
|
|||||||
asin = None
|
asin = None
|
||||||
else:
|
else:
|
||||||
asin = isbn
|
asin = isbn
|
||||||
|
with cache_lock:
|
||||||
|
asin_cache[isbn] = ans if ans else False
|
||||||
return asin
|
return asin
|
||||||
|
|
||||||
|
|
||||||
def get_social_metadata(title, authors, publisher, isbn):
|
def get_social_metadata(title, authors, publisher, isbn):
|
||||||
mi = Metadata(title, authors)
|
mi = Metadata(title, authors)
|
||||||
if not isbn:
|
if not isbn:
|
||||||
@ -58,6 +72,68 @@ def get_social_metadata(title, authors, publisher, isbn):
|
|||||||
return mi
|
return mi
|
||||||
return mi
|
return mi
|
||||||
|
|
||||||
|
def get_cover_url(isbn, br):
|
||||||
|
isbn = check_isbn(isbn)
|
||||||
|
if not isbn:
|
||||||
|
return None
|
||||||
|
with cache_lock:
|
||||||
|
ans = cover_url_cache.get(isbn, None)
|
||||||
|
if ans:
|
||||||
|
return ans
|
||||||
|
if ans is False:
|
||||||
|
return None
|
||||||
|
asin = to_asin(br, isbn)
|
||||||
|
if asin:
|
||||||
|
ans = _get_cover_url(br, asin)
|
||||||
|
if ans:
|
||||||
|
with cache_lock:
|
||||||
|
cover_url_cache[isbn] = ans
|
||||||
|
return ans
|
||||||
|
from calibre.ebooks.metadata.xisbn import xisbn
|
||||||
|
for i in xisbn.get_associated_isbns(isbn):
|
||||||
|
asin = to_asin(br, i)
|
||||||
|
if asin:
|
||||||
|
ans = _get_cover_url(br, asin)
|
||||||
|
if ans:
|
||||||
|
with cache_lock:
|
||||||
|
cover_url_cache[isbn] = ans
|
||||||
|
cover_url_cache[i] = ans
|
||||||
|
return ans
|
||||||
|
with cache_lock:
|
||||||
|
cover_url_cache[isbn] = False
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_cover_url(br, asin):
|
||||||
|
q = 'http://amzn.com/'+asin
|
||||||
|
try:
|
||||||
|
raw = br.open_novisit(q).read()
|
||||||
|
except Exception, e:
|
||||||
|
if callable(getattr(e, 'getcode', None)) and \
|
||||||
|
e.getcode() == 404:
|
||||||
|
return None
|
||||||
|
raise
|
||||||
|
if '<title>404 - ' in raw:
|
||||||
|
return None
|
||||||
|
raw = xml_to_unicode(raw, strip_encoding_pats=True,
|
||||||
|
resolve_entities=True)[0]
|
||||||
|
try:
|
||||||
|
root = soupparser.fromstring(raw)
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
imgs = root.xpath('//img[@id="prodImage" and @src]')
|
||||||
|
if imgs:
|
||||||
|
src = imgs[0].get('src')
|
||||||
|
parts = src.split('/')
|
||||||
|
if len(parts) > 3:
|
||||||
|
bn = parts[-1]
|
||||||
|
sparts = bn.split('_')
|
||||||
|
if len(sparts) > 2:
|
||||||
|
bn = sparts[0] + sparts[-1]
|
||||||
|
return ('/'.join(parts[:-1]))+'/'+bn
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_metadata(br, asin, mi):
|
def get_metadata(br, asin, mi):
|
||||||
q = 'http://amzn.com/'+asin
|
q = 'http://amzn.com/'+asin
|
||||||
try:
|
try:
|
||||||
@ -111,18 +187,25 @@ def get_metadata(br, asin, mi):
|
|||||||
|
|
||||||
|
|
||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
# Test xisbn
|
import tempfile, os
|
||||||
print get_social_metadata('Learning Python', None, None, '8324616489')
|
tdir = tempfile.gettempdir()
|
||||||
print
|
br = browser()
|
||||||
|
for title, isbn in [
|
||||||
# Test sophisticated comment formatting
|
('Learning Python', '8324616489'), # Test xisbn
|
||||||
print get_social_metadata('Angels & Demons', None, None, '9781416580829')
|
('Angels & Demons', '9781416580829'), # Test sophisticated comment formatting
|
||||||
print
|
|
||||||
|
|
||||||
# Random tests
|
# Random tests
|
||||||
print get_social_metadata('Star Trek: Destiny: Mere Mortals', None, None, '9781416551720')
|
('Star Trek: Destiny: Mere Mortals', '9781416551720'),
|
||||||
print
|
('The Great Gatsby', '0743273567'),
|
||||||
print get_social_metadata('The Great Gatsby', None, None, '0743273567')
|
]:
|
||||||
|
cpath = os.path.join(tdir, title+'.jpg')
|
||||||
|
curl = get_cover_url(isbn, br)
|
||||||
|
if curl is None:
|
||||||
|
print 'No cover found for', title
|
||||||
|
else:
|
||||||
|
open(cpath, 'wb').write(br.open_novisit(curl).read())
|
||||||
|
print 'Cover for', title, 'saved to', cpath
|
||||||
|
|
||||||
|
print get_social_metadata(title, None, None, isbn)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import traceback, socket, re, sys
|
import traceback, socket, sys
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from threading import Thread, Event
|
from threading import Thread, Event
|
||||||
from Queue import Queue, Empty
|
from Queue import Queue, Empty
|
||||||
@ -15,7 +15,6 @@ import mechanize
|
|||||||
|
|
||||||
from calibre.customize import Plugin
|
from calibre.customize import Plugin
|
||||||
from calibre import browser, prints
|
from calibre import browser, prints
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
|
||||||
from calibre.constants import preferred_encoding, DEBUG
|
from calibre.constants import preferred_encoding, DEBUG
|
||||||
|
|
||||||
class CoverDownload(Plugin):
|
class CoverDownload(Plugin):
|
||||||
@ -112,72 +111,38 @@ class OpenLibraryCovers(CoverDownload): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class LibraryThingCovers(CoverDownload): # {{{
|
class AmazonCovers(CoverDownload): # {{{
|
||||||
|
|
||||||
name = 'librarything.com covers'
|
name = 'amazon.com covers'
|
||||||
description = _('Download covers from librarything.com')
|
description = _('Download covers from amazon.com')
|
||||||
author = 'Kovid Goyal'
|
author = 'Kovid Goyal'
|
||||||
|
|
||||||
LIBRARYTHING = 'http://www.librarything.com/isbn/'
|
|
||||||
|
|
||||||
def get_cover_url(self, isbn, br, timeout=5.):
|
|
||||||
|
|
||||||
try:
|
|
||||||
src = br.open_novisit('http://www.librarything.com/isbn/'+isbn,
|
|
||||||
timeout=timeout).read().decode('utf-8', 'replace')
|
|
||||||
except Exception, err:
|
|
||||||
if isinstance(getattr(err, 'args', [None])[0], socket.timeout):
|
|
||||||
err = Exception(_('LibraryThing.com timed out. Try again later.'))
|
|
||||||
raise err
|
|
||||||
else:
|
|
||||||
if '/wiki/index.php/HelpThing:Verify' in src:
|
|
||||||
raise Exception('LibraryThing is blocking calibre.')
|
|
||||||
s = BeautifulSoup(src)
|
|
||||||
url = s.find('td', attrs={'class':'left'})
|
|
||||||
if url is None:
|
|
||||||
if s.find('div', attrs={'class':'highloadwarning'}) is not None:
|
|
||||||
raise Exception(_('Could not fetch cover as server is experiencing high load. Please try again later.'))
|
|
||||||
raise Exception(_('ISBN: %s not found')%isbn)
|
|
||||||
url = url.find('img')
|
|
||||||
if url is None:
|
|
||||||
raise Exception(_('LibraryThing.com server error. Try again later.'))
|
|
||||||
url = re.sub(r'_S[XY]\d+', '', url['src'])
|
|
||||||
return url
|
|
||||||
|
|
||||||
def has_cover(self, mi, ans, timeout=5.):
|
def has_cover(self, mi, ans, timeout=5.):
|
||||||
if not mi.isbn or not self.site_customization:
|
if not mi.isbn:
|
||||||
return False
|
return False
|
||||||
from calibre.ebooks.metadata.library_thing import get_browser, login
|
from calibre.ebooks.metadata.amazon import get_cover_url
|
||||||
br = get_browser()
|
br = browser()
|
||||||
un, _, pw = self.site_customization.partition(':')
|
|
||||||
login(br, un, pw)
|
|
||||||
try:
|
try:
|
||||||
self.get_cover_url(mi.isbn, br, timeout=timeout)
|
get_cover_url(mi.isbn, br)
|
||||||
self.debug('cover for', mi.isbn, 'found')
|
self.debug('cover for', mi.isbn, 'found')
|
||||||
ans.set()
|
ans.set()
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
self.debug(e)
|
self.debug(e)
|
||||||
|
|
||||||
def get_covers(self, mi, result_queue, abort, timeout=5.):
|
def get_covers(self, mi, result_queue, abort, timeout=5.):
|
||||||
if not mi.isbn or not self.site_customization:
|
if not mi.isbn:
|
||||||
return
|
return
|
||||||
from calibre.ebooks.metadata.library_thing import get_browser, login
|
from calibre.ebooks.metadata.amazon import get_cover_url
|
||||||
br = get_browser()
|
br = browser()
|
||||||
un, _, pw = self.site_customization.partition(':')
|
|
||||||
login(br, un, pw)
|
|
||||||
try:
|
try:
|
||||||
url = self.get_cover_url(mi.isbn, br, timeout=timeout)
|
url = get_cover_url(mi.isbn, br)
|
||||||
cover_data = br.open_novisit(url).read()
|
cover_data = br.open_novisit(url).read()
|
||||||
result_queue.put((True, cover_data, 'jpg', self.name))
|
result_queue.put((True, cover_data, 'jpg', self.name))
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
result_queue.put((False, self.exception_to_string(e),
|
result_queue.put((False, self.exception_to_string(e),
|
||||||
traceback.format_exc(), self.name))
|
traceback.format_exc(), self.name))
|
||||||
|
|
||||||
def customization_help(self, gui=False):
|
|
||||||
ans = _('To use librarything.com you must sign up for a %sfree account%s '
|
|
||||||
'and enter your username and password separated by a : below.')
|
|
||||||
return '<p>'+ans%('<a href="http://www.librarything.com">', '</a>')
|
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def check_for_cover(mi, timeout=5.): # {{{
|
def check_for_cover(mi, timeout=5.): # {{{
|
||||||
|
@ -367,6 +367,9 @@ class MobiMLizer(object):
|
|||||||
istate.attrib['src'] = elem.attrib['src']
|
istate.attrib['src'] = elem.attrib['src']
|
||||||
istate.attrib['align'] = 'baseline'
|
istate.attrib['align'] = 'baseline'
|
||||||
cssdict = style.cssdict()
|
cssdict = style.cssdict()
|
||||||
|
valign = cssdict.get('vertical-align', None)
|
||||||
|
if valign in ('top', 'bottom', 'middle'):
|
||||||
|
istate.attrib['align'] = valign
|
||||||
for prop in ('width', 'height'):
|
for prop in ('width', 'height'):
|
||||||
if cssdict[prop] != 'auto':
|
if cssdict[prop] != 'auto':
|
||||||
value = style[prop]
|
value = style[prop]
|
||||||
@ -451,8 +454,11 @@ class MobiMLizer(object):
|
|||||||
text = COLLAPSE.sub(' ', elem.text)
|
text = COLLAPSE.sub(' ', elem.text)
|
||||||
valign = style['vertical-align']
|
valign = style['vertical-align']
|
||||||
not_baseline = valign in ('super', 'sub', 'text-top',
|
not_baseline = valign in ('super', 'sub', 'text-top',
|
||||||
'text-bottom')
|
'text-bottom') or (
|
||||||
vtag = 'sup' if valign in ('super', 'text-top') else 'sub'
|
isinstance(valign, (float, int)) and abs(valign) != 0)
|
||||||
|
issup = valign in ('super', 'text-top') or (
|
||||||
|
isinstance(valign, (float, int)) and valign > 0)
|
||||||
|
vtag = 'sup' if issup else 'sub'
|
||||||
if not_baseline and not ignore_valign and tag not in NOT_VTAGS and not isblock:
|
if not_baseline and not ignore_valign and tag not in NOT_VTAGS and not isblock:
|
||||||
nroot = etree.Element(XHTML('html'), nsmap=MOBI_NSMAP)
|
nroot = etree.Element(XHTML('html'), nsmap=MOBI_NSMAP)
|
||||||
vbstate = BlockState(etree.SubElement(nroot, XHTML('body')))
|
vbstate = BlockState(etree.SubElement(nroot, XHTML('body')))
|
||||||
|
@ -207,7 +207,14 @@ class CSSFlattener(object):
|
|||||||
font_size = self.sbase if self.sbase is not None else \
|
font_size = self.sbase if self.sbase is not None else \
|
||||||
self.context.source.fbase
|
self.context.source.fbase
|
||||||
if 'align' in node.attrib:
|
if 'align' in node.attrib:
|
||||||
|
if tag != 'img':
|
||||||
cssdict['text-align'] = node.attrib['align']
|
cssdict['text-align'] = node.attrib['align']
|
||||||
|
else:
|
||||||
|
val = node.attrib['align']
|
||||||
|
if val in ('middle', 'bottom', 'top'):
|
||||||
|
cssdict['vertical-align'] = val
|
||||||
|
elif val in ('left', 'right'):
|
||||||
|
cssdict['text-align'] = val
|
||||||
del node.attrib['align']
|
del node.attrib['align']
|
||||||
if node.tag == XHTML('font'):
|
if node.tag == XHTML('font'):
|
||||||
node.tag = XHTML('span')
|
node.tag = XHTML('span')
|
||||||
|
@ -4,10 +4,9 @@ __license__ = 'GPL 3'
|
|||||||
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import glob
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from calibre import _ent_pat, xml_entity_to_unicode
|
from calibre import _ent_pat, walk, xml_entity_to_unicode
|
||||||
from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation
|
from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation
|
||||||
from calibre.ebooks.conversion.preprocess import DocAnalysis, Dehyphenator
|
from calibre.ebooks.conversion.preprocess import DocAnalysis, Dehyphenator
|
||||||
from calibre.ebooks.chardet import detect
|
from calibre.ebooks.chardet import detect
|
||||||
@ -16,7 +15,6 @@ from calibre.ebooks.txt.processor import convert_basic, convert_markdown, \
|
|||||||
preserve_spaces, detect_paragraph_type, detect_formatting_type, \
|
preserve_spaces, detect_paragraph_type, detect_formatting_type, \
|
||||||
normalize_line_endings, convert_textile, remove_indents, block_to_single_line, \
|
normalize_line_endings, convert_textile, remove_indents, block_to_single_line, \
|
||||||
separate_hard_scene_breaks
|
separate_hard_scene_breaks
|
||||||
from calibre.ptempfile import TemporaryDirectory
|
|
||||||
from calibre.utils.zipfile import ZipFile
|
from calibre.utils.zipfile import ZipFile
|
||||||
|
|
||||||
class TXTInput(InputFormatPlugin):
|
class TXTInput(InputFormatPlugin):
|
||||||
@ -28,20 +26,23 @@ class TXTInput(InputFormatPlugin):
|
|||||||
|
|
||||||
options = set([
|
options = set([
|
||||||
OptionRecommendation(name='paragraph_type', recommended_value='auto',
|
OptionRecommendation(name='paragraph_type', recommended_value='auto',
|
||||||
choices=['auto', 'block', 'single', 'print', 'unformatted'],
|
choices=['auto', 'block', 'single', 'print', 'unformatted', 'off'],
|
||||||
help=_('Paragraph structure.\n'
|
help=_('Paragraph structure.\n'
|
||||||
'choices are [\'auto\', \'block\', \'single\', \'print\', \'unformatted\']\n'
|
'choices are [\'auto\', \'block\', \'single\', \'print\', \'unformatted\', \'off\']\n'
|
||||||
'* auto: Try to auto detect paragraph type.\n'
|
'* auto: Try to auto detect paragraph type.\n'
|
||||||
'* block: Treat a blank line as a paragraph break.\n'
|
'* block: Treat a blank line as a paragraph break.\n'
|
||||||
'* single: Assume every line is a paragraph.\n'
|
'* single: Assume every line is a paragraph.\n'
|
||||||
'* print: Assume every line starting with 2+ spaces or a tab '
|
'* print: Assume every line starting with 2+ spaces or a tab '
|
||||||
'starts a paragraph.'
|
'starts a paragraph.\n'
|
||||||
'* unformatted: Most lines have hard line breaks, few/no blank lines or indents.')),
|
'* unformatted: Most lines have hard line breaks, few/no blank lines or indents. '
|
||||||
|
'Tries to determine structure and reformat the differentiate elements.\n'
|
||||||
|
'* off: Don\'t modify the paragraph structure. This is useful when combined with '
|
||||||
|
'Markdown or Textile formatting to ensure no formatting is lost.')),
|
||||||
OptionRecommendation(name='formatting_type', recommended_value='auto',
|
OptionRecommendation(name='formatting_type', recommended_value='auto',
|
||||||
choices=['auto', 'none', 'heuristic', 'textile', 'markdown'],
|
choices=['auto', 'plain', 'heuristic', 'textile', 'markdown'],
|
||||||
help=_('Formatting used within the document.'
|
help=_('Formatting used within the document.'
|
||||||
'* auto: Automatically decide which formatting processor to use.\n'
|
'* auto: Automatically decide which formatting processor to use.\n'
|
||||||
'* none: Do not process the document formatting. Everything is a '
|
'* plain: Do not process the document formatting. Everything is a '
|
||||||
'paragraph and no styling is applied.\n'
|
'paragraph and no styling is applied.\n'
|
||||||
'* heuristic: Process using heuristics to determine formatting such '
|
'* heuristic: Process using heuristics to determine formatting such '
|
||||||
'as chapter headings and italic text.\n'
|
'as chapter headings and italic text.\n'
|
||||||
@ -64,18 +65,17 @@ class TXTInput(InputFormatPlugin):
|
|||||||
txt = ''
|
txt = ''
|
||||||
log.debug('Reading text from file...')
|
log.debug('Reading text from file...')
|
||||||
length = 0
|
length = 0
|
||||||
|
# [(u'path', mime),]
|
||||||
|
|
||||||
# Extract content from zip archive.
|
# Extract content from zip archive.
|
||||||
if file_ext == 'txtz':
|
if file_ext == 'txtz':
|
||||||
log.debug('De-compressing content to temporary directory...')
|
|
||||||
with TemporaryDirectory('_untxtz') as tdir:
|
|
||||||
zf = ZipFile(stream)
|
zf = ZipFile(stream)
|
||||||
zf.extractall(tdir)
|
zf.extractall('.')
|
||||||
|
|
||||||
txts = glob.glob(os.path.join(tdir, '*.txt'))
|
for x in walk('.'):
|
||||||
for t in txts:
|
if os.path.splitext(x)[1].lower() == '.txt':
|
||||||
with open(t, 'rb') as tf:
|
with open(x, 'rb') as tf:
|
||||||
txt += tf.read()
|
txt += tf.read() + '\n\n'
|
||||||
else:
|
else:
|
||||||
txt = stream.read()
|
txt = stream.read()
|
||||||
|
|
||||||
@ -134,7 +134,7 @@ class TXTInput(InputFormatPlugin):
|
|||||||
preprocessor = HeuristicProcessor(options, log=getattr(self, 'log', None))
|
preprocessor = HeuristicProcessor(options, log=getattr(self, 'log', None))
|
||||||
txt = preprocessor.punctuation_unwrap(length, txt, 'txt')
|
txt = preprocessor.punctuation_unwrap(length, txt, 'txt')
|
||||||
txt = separate_paragraphs_single_line(txt)
|
txt = separate_paragraphs_single_line(txt)
|
||||||
else:
|
elif options.paragraph_type == 'block':
|
||||||
txt = separate_hard_scene_breaks(txt)
|
txt = separate_hard_scene_breaks(txt)
|
||||||
txt = block_to_single_line(txt)
|
txt = block_to_single_line(txt)
|
||||||
|
|
||||||
@ -178,7 +178,7 @@ class TXTInput(InputFormatPlugin):
|
|||||||
setattr(options, opt.option.name, opt.recommended_value)
|
setattr(options, opt.option.name, opt.recommended_value)
|
||||||
options.input_encoding = 'utf-8'
|
options.input_encoding = 'utf-8'
|
||||||
base = os.getcwdu()
|
base = os.getcwdu()
|
||||||
if hasattr(stream, 'name'):
|
if file_ext != 'txtz' and hasattr(stream, 'name'):
|
||||||
base = os.path.dirname(stream.name)
|
base = os.path.dirname(stream.name)
|
||||||
fname = os.path.join(base, 'index.html')
|
fname = os.path.join(base, 'index.html')
|
||||||
c = 0
|
c = 0
|
||||||
@ -190,7 +190,7 @@ class TXTInput(InputFormatPlugin):
|
|||||||
htmlfile.write(html.encode('utf-8'))
|
htmlfile.write(html.encode('utf-8'))
|
||||||
odi = options.debug_pipeline
|
odi = options.debug_pipeline
|
||||||
options.debug_pipeline = None
|
options.debug_pipeline = None
|
||||||
# Generate oeb from htl conversion.
|
# Generate oeb from html conversion.
|
||||||
oeb = html_input.convert(open(htmlfile.name, 'rb'), options, 'html', log,
|
oeb = html_input.convert(open(htmlfile.name, 'rb'), options, 'html', log,
|
||||||
{})
|
{})
|
||||||
options.debug_pipeline = odi
|
options.debug_pipeline = odi
|
||||||
|
@ -126,7 +126,7 @@ def separate_hard_scene_breaks(txt):
|
|||||||
return '\n%s\n' % line
|
return '\n%s\n' % line
|
||||||
else:
|
else:
|
||||||
return line
|
return line
|
||||||
txt = re.sub(u'(?miu)^[ \t-=~\/]+$', lambda mo: sep_break(mo.group()), txt)
|
txt = re.sub(u'(?miu)^[ \t-=~\/_]+$', lambda mo: sep_break(mo.group()), txt)
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
def block_to_single_line(txt):
|
def block_to_single_line(txt):
|
||||||
|
@ -204,7 +204,8 @@ class AddAction(InterfaceAction):
|
|||||||
]
|
]
|
||||||
to_device = self.gui.stack.currentIndex() != 0
|
to_device = self.gui.stack.currentIndex() != 0
|
||||||
if to_device:
|
if to_device:
|
||||||
filters = [(_('Supported books'), self.gui.device_manager.device.FORMATS)]
|
fmts = self.gui.device_manager.device.settings().format_map
|
||||||
|
filters = [(_('Supported books'), fmts)]
|
||||||
|
|
||||||
books = choose_files(self.gui, 'add books dialog dir', 'Select books',
|
books = choose_files(self.gui, 'add books dialog dir', 'Select books',
|
||||||
filters=filters)
|
filters=filters)
|
||||||
|
@ -158,6 +158,8 @@ class MultiCompleteComboBox(EnComboBox):
|
|||||||
# item that matches case insensitively
|
# item that matches case insensitively
|
||||||
c = self.lineEdit().completer()
|
c = self.lineEdit().completer()
|
||||||
c.setCaseSensitivity(Qt.CaseSensitive)
|
c.setCaseSensitivity(Qt.CaseSensitive)
|
||||||
|
self.dummy_model = CompleteModel(self)
|
||||||
|
c.setModel(self.dummy_model)
|
||||||
|
|
||||||
def update_items_cache(self, complete_items):
|
def update_items_cache(self, complete_items):
|
||||||
self.lineEdit().update_items_cache(complete_items)
|
self.lineEdit().update_items_cache(complete_items)
|
||||||
|
@ -9,15 +9,16 @@ import textwrap
|
|||||||
from PyQt4.Qt import QWidget, QListWidgetItem, Qt, QVariant, SIGNAL, \
|
from PyQt4.Qt import QWidget, QListWidgetItem, Qt, QVariant, SIGNAL, \
|
||||||
QLabel, QLineEdit, QCheckBox
|
QLabel, QLineEdit, QCheckBox
|
||||||
|
|
||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog, question_dialog
|
||||||
from calibre.gui2.device_drivers.configwidget_ui import Ui_ConfigWidget
|
from calibre.gui2.device_drivers.configwidget_ui import Ui_ConfigWidget
|
||||||
from calibre.utils.formatter import validation_formatter
|
from calibre.utils.formatter import validation_formatter
|
||||||
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
|
|
||||||
class ConfigWidget(QWidget, Ui_ConfigWidget):
|
class ConfigWidget(QWidget, Ui_ConfigWidget):
|
||||||
|
|
||||||
def __init__(self, settings, all_formats, supports_subdirs,
|
def __init__(self, settings, all_formats, supports_subdirs,
|
||||||
must_read_metadata, supports_use_author_sort,
|
must_read_metadata, supports_use_author_sort,
|
||||||
extra_customization_message):
|
extra_customization_message, device):
|
||||||
|
|
||||||
QWidget.__init__(self)
|
QWidget.__init__(self)
|
||||||
Ui_ConfigWidget.__init__(self)
|
Ui_ConfigWidget.__init__(self)
|
||||||
@ -25,9 +26,15 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
|
|||||||
|
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
|
||||||
|
all_formats = set(all_formats)
|
||||||
|
self.calibre_known_formats = device.FORMATS
|
||||||
|
self.device_name = device.get_gui_name()
|
||||||
|
if device.USER_CAN_ADD_NEW_FORMATS:
|
||||||
|
all_formats = set(all_formats) | set(BOOK_EXTENSIONS)
|
||||||
|
|
||||||
format_map = settings.format_map
|
format_map = settings.format_map
|
||||||
disabled_formats = list(set(all_formats).difference(format_map))
|
disabled_formats = list(set(all_formats).difference(format_map))
|
||||||
for format in format_map + disabled_formats:
|
for format in format_map + list(sorted(disabled_formats)):
|
||||||
item = QListWidgetItem(format, self.columns)
|
item = QListWidgetItem(format, self.columns)
|
||||||
item.setData(Qt.UserRole, QVariant(format))
|
item.setData(Qt.UserRole, QVariant(format))
|
||||||
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
|
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
|
||||||
@ -110,6 +117,18 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
|
|||||||
return self.opt_use_author_sort.isChecked()
|
return self.opt_use_author_sort.isChecked()
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
formats = set(self.format_map())
|
||||||
|
extra = formats - set(self.calibre_known_formats)
|
||||||
|
if extra:
|
||||||
|
fmts = sorted([x.upper() for x in extra])
|
||||||
|
if not question_dialog(self, _('Unknown formats'),
|
||||||
|
_('You have enabled the <b>{0}</b> formats for'
|
||||||
|
' your {1}. The {1} may not support them.'
|
||||||
|
' If you send these formats to your {1} they '
|
||||||
|
'may not work. Are you sure?').format(
|
||||||
|
(', '.join(fmts)), self.device_name)):
|
||||||
|
return False
|
||||||
|
|
||||||
tmpl = unicode(self.opt_save_template.text())
|
tmpl = unicode(self.opt_save_template.text())
|
||||||
try:
|
try:
|
||||||
validation_formatter.validate(tmpl)
|
validation_formatter.validate(tmpl)
|
||||||
|
@ -912,6 +912,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
|||||||
|
|
||||||
def series_changed(self, *args):
|
def series_changed(self, *args):
|
||||||
self.write_series = True
|
self.write_series = True
|
||||||
|
self.autonumber_series.setEnabled(True)
|
||||||
|
|
||||||
def s_r_remove_query(self, *args):
|
def s_r_remove_query(self, *args):
|
||||||
if self.query_field.currentIndex() == 0:
|
if self.query_field.currentIndex() == 0:
|
||||||
|
@ -303,6 +303,9 @@
|
|||||||
<layout class="QHBoxLayout" name="HLayout_3">
|
<layout class="QHBoxLayout" name="HLayout_3">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="autonumber_series">
|
<widget class="QCheckBox" name="autonumber_series">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>If not checked, the series number for the books will be set to 1.
|
<string>If not checked, the series number for the books will be set to 1.
|
||||||
If checked, selected books will be automatically numbered, in the order
|
If checked, selected books will be automatically numbered, in the order
|
||||||
@ -1006,8 +1009,8 @@ not multiple and the destination field is multiple</string>
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>938</width>
|
<width>197</width>
|
||||||
<height>268</height>
|
<height>60</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="testgrid">
|
<layout class="QGridLayout" name="testgrid">
|
||||||
|
@ -26,12 +26,19 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
r('limit_search_columns_to', prefs, setting=CommaSeparatedList)
|
r('limit_search_columns_to', prefs, setting=CommaSeparatedList)
|
||||||
fl = gui.library_view.model().db.field_metadata.get_search_terms()
|
fl = gui.library_view.model().db.field_metadata.get_search_terms()
|
||||||
self.opt_limit_search_columns_to.update_items_cache(fl)
|
self.opt_limit_search_columns_to.update_items_cache(fl)
|
||||||
|
self.clear_history_button.clicked.connect(self.clear_histories)
|
||||||
|
|
||||||
def refresh_gui(self, gui):
|
def refresh_gui(self, gui):
|
||||||
gui.search.search_as_you_type(config['search_as_you_type'])
|
gui.search.search_as_you_type(config['search_as_you_type'])
|
||||||
gui.library_view.model().set_highlight_only(config['highlight_search_matches'])
|
gui.library_view.model().set_highlight_only(config['highlight_search_matches'])
|
||||||
gui.search.do_search()
|
gui.search.do_search()
|
||||||
|
|
||||||
|
def clear_histories(self, *args):
|
||||||
|
for key, val in config.defaults.iteritems():
|
||||||
|
if key.endswith('_search_history') and isinstance(val, list):
|
||||||
|
config[key] = []
|
||||||
|
self.gui.search.clear_history()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
test_widget('Interface', 'Search')
|
test_widget('Interface', 'Search')
|
||||||
|
@ -77,7 +77,7 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0">
|
<item row="4" column="0">
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
@ -90,13 +90,23 @@
|
|||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QPushButton" name="clear_history_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Clear search histories from all over calibre. Including the book list, e-book viewer, fetch news dialog, etc.</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Clear search &histories</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>MultiCompleteLineEdit</class>
|
<class>MultiCompleteLineEdit</class>
|
||||||
<extends>QLineEdit</extends>
|
<extends>QLineEdit</extends>
|
||||||
<header>calibre/gui2.complete.h</header>
|
<header>calibre/gui2/complete.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources/>
|
<resources/>
|
||||||
|
@ -114,6 +114,9 @@ class SearchBox2(QComboBox): # {{{
|
|||||||
def text(self):
|
def text(self):
|
||||||
return self.currentText()
|
return self.currentText()
|
||||||
|
|
||||||
|
def clear_history(self, *args):
|
||||||
|
QComboBox.clear(self)
|
||||||
|
|
||||||
def clear(self, emit_search=True):
|
def clear(self, emit_search=True):
|
||||||
self.normalize_state()
|
self.normalize_state()
|
||||||
self.setEditText('')
|
self.setEditText('')
|
||||||
|
@ -17,16 +17,16 @@ from calibre.gui2.viewer.bookmarkmanager import BookmarkManager
|
|||||||
from calibre.gui2.widgets import ProgressIndicator
|
from calibre.gui2.widgets import ProgressIndicator
|
||||||
from calibre.gui2.main_window import MainWindow
|
from calibre.gui2.main_window import MainWindow
|
||||||
from calibre.gui2 import Application, ORG_NAME, APP_UID, choose_files, \
|
from calibre.gui2 import Application, ORG_NAME, APP_UID, choose_files, \
|
||||||
info_dialog, error_dialog, open_url, available_height
|
info_dialog, error_dialog, open_url, available_height, gprefs
|
||||||
from calibre.ebooks.oeb.iterator import EbookIterator
|
from calibre.ebooks.oeb.iterator import EbookIterator
|
||||||
from calibre.ebooks import DRMError
|
from calibre.ebooks import DRMError
|
||||||
from calibre.constants import islinux, isfreebsd, isosx
|
from calibre.constants import islinux, isfreebsd, isosx, filesystem_encoding
|
||||||
from calibre.utils.config import Config, StringConfig, dynamic
|
from calibre.utils.config import Config, StringConfig, dynamic
|
||||||
from calibre.gui2.search_box import SearchBox2
|
from calibre.gui2.search_box import SearchBox2
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
from calibre.customize.ui import available_input_formats
|
from calibre.customize.ui import available_input_formats
|
||||||
from calibre.gui2.viewer.dictionary import Lookup
|
from calibre.gui2.viewer.dictionary import Lookup
|
||||||
from calibre import as_unicode
|
from calibre import as_unicode, force_unicode, isbytestring
|
||||||
|
|
||||||
class TOCItem(QStandardItem):
|
class TOCItem(QStandardItem):
|
||||||
|
|
||||||
@ -160,6 +160,12 @@ class HelpfulLineEdit(QLineEdit):
|
|||||||
self.setPalette(self.gray)
|
self.setPalette(self.gray)
|
||||||
self.setText(self.HELP_TEXT)
|
self.setText(self.HELP_TEXT)
|
||||||
|
|
||||||
|
class RecentAction(QAction):
|
||||||
|
|
||||||
|
def __init__(self, path, parent):
|
||||||
|
self.path = path
|
||||||
|
QAction.__init__(self, os.path.basename(path), parent)
|
||||||
|
|
||||||
class EbookViewer(MainWindow, Ui_EbookViewer):
|
class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||||
|
|
||||||
STATE_VERSION = 1
|
STATE_VERSION = 1
|
||||||
@ -284,8 +290,26 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
ca = self.view.copy_action
|
ca = self.view.copy_action
|
||||||
ca.setShortcut(QKeySequence.Copy)
|
ca.setShortcut(QKeySequence.Copy)
|
||||||
self.addAction(ca)
|
self.addAction(ca)
|
||||||
|
self.open_history_menu = QMenu()
|
||||||
|
self.build_recent_menu()
|
||||||
|
self.action_open_ebook.setMenu(self.open_history_menu)
|
||||||
|
self.open_history_menu.triggered[QAction].connect(self.open_recent)
|
||||||
|
w = self.tool_bar.widgetForAction(self.action_open_ebook)
|
||||||
|
w.setPopupMode(QToolButton.MenuButtonPopup)
|
||||||
|
|
||||||
self.restore_state()
|
self.restore_state()
|
||||||
|
|
||||||
|
def build_recent_menu(self):
|
||||||
|
m = self.open_history_menu
|
||||||
|
m.clear()
|
||||||
|
count = 0
|
||||||
|
for path in gprefs.get('viewer_open_history', []):
|
||||||
|
if count > 9:
|
||||||
|
break
|
||||||
|
if os.path.exists(path):
|
||||||
|
m.addAction(RecentAction(path, m))
|
||||||
|
count += 1
|
||||||
|
|
||||||
def closeEvent(self, e):
|
def closeEvent(self, e):
|
||||||
self.save_state()
|
self.save_state()
|
||||||
return MainWindow.closeEvent(self, e)
|
return MainWindow.closeEvent(self, e)
|
||||||
@ -425,6 +449,9 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
if files:
|
if files:
|
||||||
self.load_ebook(files[0])
|
self.load_ebook(files[0])
|
||||||
|
|
||||||
|
def open_recent(self, action):
|
||||||
|
self.load_ebook(action.path)
|
||||||
|
|
||||||
def font_size_larger(self, checked):
|
def font_size_larger(self, checked):
|
||||||
frac = self.view.magnify_fonts()
|
frac = self.view.magnify_fonts()
|
||||||
self.action_font_size_larger.setEnabled(self.view.multiplier() < 3)
|
self.action_font_size_larger.setEnabled(self.view.multiplier() < 3)
|
||||||
@ -647,6 +674,17 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
self.action_table_of_contents.setChecked(True)
|
self.action_table_of_contents.setChecked(True)
|
||||||
else:
|
else:
|
||||||
self.action_table_of_contents.setChecked(False)
|
self.action_table_of_contents.setChecked(False)
|
||||||
|
if isbytestring(pathtoebook):
|
||||||
|
pathtoebook = force_unicode(pathtoebook, filesystem_encoding)
|
||||||
|
vh = gprefs.get('viewer_open_history', [])
|
||||||
|
try:
|
||||||
|
vh.remove(pathtoebook)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
vh.insert(0, pathtoebook)
|
||||||
|
gprefs.set('viewer_open_history', vh[:50])
|
||||||
|
self.build_recent_menu()
|
||||||
|
|
||||||
self.action_table_of_contents.setDisabled(not self.iterator.toc)
|
self.action_table_of_contents.setDisabled(not self.iterator.toc)
|
||||||
self.current_book_has_toc = bool(self.iterator.toc)
|
self.current_book_has_toc = bool(self.iterator.toc)
|
||||||
self.current_title = title
|
self.current_title = title
|
||||||
|
Loading…
x
Reference in New Issue
Block a user