Sync to pluginize

This commit is contained in:
John Schember 2009-04-16 18:27:04 -04:00
commit e773e0e13e
43 changed files with 1015 additions and 711 deletions

View File

@ -571,9 +571,6 @@ Condition 08195201-0797-932C-4B51-E5EF9D1D41BD -active Yes -parent 710F2507-2557
Condition 2E18F4AE-F1BB-5C62-2900-73A576A49261 -active Yes -parent 710F2507-2557-652D-EA55-440D710EFDFA -title {String Is Condition} -component StringIsCondition -TreeObject::id 2E18F4AE-F1BB-5C62-2900-73A576A49261 Condition 2E18F4AE-F1BB-5C62-2900-73A576A49261 -active Yes -parent 710F2507-2557-652D-EA55-440D710EFDFA -title {String Is Condition} -component StringIsCondition -TreeObject::id 2E18F4AE-F1BB-5C62-2900-73A576A49261
InstallComponent 21B897C4-24BE-70D1-58EA-DE78EFA60719 -setup Install -type action -conditions 76FA3CA2-1F09-75C5-C6CF-72719A8EC4A5 -title {Message Box} -component MessageBox -command insert -active Yes -parent 8A7FD0C2-F053-8764-F204-4BAE71E05708 InstallComponent 21B897C4-24BE-70D1-58EA-DE78EFA60719 -setup Install -type action -conditions 76FA3CA2-1F09-75C5-C6CF-72719A8EC4A5 -title {Message Box} -component MessageBox -command insert -active Yes -parent 8A7FD0C2-F053-8764-F204-4BAE71E05708
Condition 76FA3CA2-1F09-75C5-C6CF-72719A8EC4A5 -active Yes -parent 21B897C4-24BE-70D1-58EA-DE78EFA60719 -title {String Is Condition} -component StringIsCondition -TreeObject::id 76FA3CA2-1F09-75C5-C6CF-72719A8EC4A5 Condition 76FA3CA2-1F09-75C5-C6CF-72719A8EC4A5 -active Yes -parent 21B897C4-24BE-70D1-58EA-DE78EFA60719 -title {String Is Condition} -component StringIsCondition -TreeObject::id 76FA3CA2-1F09-75C5-C6CF-72719A8EC4A5
InstallComponent 5D20DD8D-064A-9922-29E1-A7FABEF3666A -setup Install -type action -conditions {E5D227F7-E549-EFA9-1781-EFA6C5EEEC5C A8856922-E6C1-160B-E55C-5C1806A89136} -title {Launch Application Checkbutton} -component AddWidget -command insert -active Yes -parent 8A7FD0C2-F053-8764-F204-4BAE71E05708
Condition E5D227F7-E549-EFA9-1781-EFA6C5EEEC5C -active Yes -parent 5D20DD8D-064A-9922-29E1-A7FABEF3666A -title {File Exists Condition} -component FileExistsCondition -TreeObject::id E5D227F7-E549-EFA9-1781-EFA6C5EEEC5C
Condition A8856922-E6C1-160B-E55C-5C1806A89136 -active Yes -parent 5D20DD8D-064A-9922-29E1-A7FABEF3666A -title {String Is Condition} -component StringIsCondition -TreeObject::id A8856922-E6C1-160B-E55C-5C1806A89136
InstallComponent 940F7FED-7D20-7264-3BF9-ED78205A76B3 -setup Install -type action -conditions {96440B8B-C6D0-FCCA-6D3C-7ECE1C304CC0 FBA33088-C809-DD6B-D337-EADBF1CEE966} -title {Desktop Shortcut Checkbutton} -component AddWidget -command insert -active Yes -parent 8A7FD0C2-F053-8764-F204-4BAE71E05708 InstallComponent 940F7FED-7D20-7264-3BF9-ED78205A76B3 -setup Install -type action -conditions {96440B8B-C6D0-FCCA-6D3C-7ECE1C304CC0 FBA33088-C809-DD6B-D337-EADBF1CEE966} -title {Desktop Shortcut Checkbutton} -component AddWidget -command insert -active Yes -parent 8A7FD0C2-F053-8764-F204-4BAE71E05708
Condition 96440B8B-C6D0-FCCA-6D3C-7ECE1C304CC0 -active Yes -parent 940F7FED-7D20-7264-3BF9-ED78205A76B3 -title {File Exists Condition} -component FileExistsCondition -TreeObject::id 96440B8B-C6D0-FCCA-6D3C-7ECE1C304CC0 Condition 96440B8B-C6D0-FCCA-6D3C-7ECE1C304CC0 -active Yes -parent 940F7FED-7D20-7264-3BF9-ED78205A76B3 -title {File Exists Condition} -component FileExistsCondition -TreeObject::id 96440B8B-C6D0-FCCA-6D3C-7ECE1C304CC0
Condition FBA33088-C809-DD6B-D337-EADBF1CEE966 -active Yes -parent 940F7FED-7D20-7264-3BF9-ED78205A76B3 -title {String Is Condition} -component StringIsCondition -TreeObject::id FBA33088-C809-DD6B-D337-EADBF1CEE966 Condition FBA33088-C809-DD6B-D337-EADBF1CEE966 -active Yes -parent 940F7FED-7D20-7264-3BF9-ED78205A76B3 -title {String Is Condition} -component StringIsCondition -TreeObject::id FBA33088-C809-DD6B-D337-EADBF1CEE966
@ -630,7 +627,7 @@ Condition 03FA7EEF-F626-B69A-09C6-0AA7A54EE9E7 -active Yes -parent E32519F3-A540
InstallComponent D86BBA5C-4903-33BA-59F8-4266A3D45896 -setup Install -type action -conditions {C4C0A903-CF2A-D25A-27AB-A64219FB7E70 5EC7056B-6F90-311E-2C6F-76E96164CFFD} -title {Install Quick Launch Shortcut} -component InstallWindowsShortcut -command insert -active Yes -parent 28BAE662-E103-4E3F-D298-C8FBA36361FC InstallComponent D86BBA5C-4903-33BA-59F8-4266A3D45896 -setup Install -type action -conditions {C4C0A903-CF2A-D25A-27AB-A64219FB7E70 5EC7056B-6F90-311E-2C6F-76E96164CFFD} -title {Install Quick Launch Shortcut} -component InstallWindowsShortcut -command insert -active Yes -parent 28BAE662-E103-4E3F-D298-C8FBA36361FC
Condition C4C0A903-CF2A-D25A-27AB-A64219FB7E70 -active Yes -parent D86BBA5C-4903-33BA-59F8-4266A3D45896 -title {String Is Condition} -component StringIsCondition -TreeObject::id C4C0A903-CF2A-D25A-27AB-A64219FB7E70 Condition C4C0A903-CF2A-D25A-27AB-A64219FB7E70 -active Yes -parent D86BBA5C-4903-33BA-59F8-4266A3D45896 -title {String Is Condition} -component StringIsCondition -TreeObject::id C4C0A903-CF2A-D25A-27AB-A64219FB7E70
Condition 5EC7056B-6F90-311E-2C6F-76E96164CFFD -active Yes -parent D86BBA5C-4903-33BA-59F8-4266A3D45896 -title {File Exists Condition} -component FileExistsCondition -TreeObject::id 5EC7056B-6F90-311E-2C6F-76E96164CFFD Condition 5EC7056B-6F90-311E-2C6F-76E96164CFFD -active Yes -parent D86BBA5C-4903-33BA-59F8-4266A3D45896 -title {File Exists Condition} -component FileExistsCondition -TreeObject::id 5EC7056B-6F90-311E-2C6F-76E96164CFFD
InstallComponent 2A230259-3A6F-8669-8B8B-23C3E7C1BFC2 -setup Install -type action -conditions {4E5FC4FE-5D37-B216-CFFE-E046A2D6321E E560F3A1-208D-2B4F-2C87-E08595F8E1CD 9C1E4BD9-066D-ABCE-28D0-9E194B9F8475} -title {Launch Application} -component ExecuteExternalProgram -command insert -active Yes -parent 28BAE662-E103-4E3F-D298-C8FBA36361FC InstallComponent 2A230259-3A6F-8669-8B8B-23C3E7C1BFC2 -setup Install -type action -conditions {4E5FC4FE-5D37-B216-CFFE-E046A2D6321E E560F3A1-208D-2B4F-2C87-E08595F8E1CD 9C1E4BD9-066D-ABCE-28D0-9E194B9F8475} -title {Launch Application} -component ExecuteExternalProgram -command insert -active No -parent 28BAE662-E103-4E3F-D298-C8FBA36361FC
Condition 4E5FC4FE-5D37-B216-CFFE-E046A2D6321E -active Yes -parent 2A230259-3A6F-8669-8B8B-23C3E7C1BFC2 -title {String Is Condition} -component StringIsCondition -TreeObject::id 4E5FC4FE-5D37-B216-CFFE-E046A2D6321E Condition 4E5FC4FE-5D37-B216-CFFE-E046A2D6321E -active Yes -parent 2A230259-3A6F-8669-8B8B-23C3E7C1BFC2 -title {String Is Condition} -component StringIsCondition -TreeObject::id 4E5FC4FE-5D37-B216-CFFE-E046A2D6321E
Condition E560F3A1-208D-2B4F-2C87-E08595F8E1CD -active Yes -parent 2A230259-3A6F-8669-8B8B-23C3E7C1BFC2 -title {String Is Condition} -component StringIsCondition -TreeObject::id E560F3A1-208D-2B4F-2C87-E08595F8E1CD Condition E560F3A1-208D-2B4F-2C87-E08595F8E1CD -active Yes -parent 2A230259-3A6F-8669-8B8B-23C3E7C1BFC2 -title {String Is Condition} -component StringIsCondition -TreeObject::id E560F3A1-208D-2B4F-2C87-E08595F8E1CD
Condition 9C1E4BD9-066D-ABCE-28D0-9E194B9F8475 -active Yes -parent 2A230259-3A6F-8669-8B8B-23C3E7C1BFC2 -title {File Exists Condition} -component FileExistsCondition -TreeObject::id 9C1E4BD9-066D-ABCE-28D0-9E194B9F8475 Condition 9C1E4BD9-066D-ABCE-28D0-9E194B9F8475 -active Yes -parent 2A230259-3A6F-8669-8B8B-23C3E7C1BFC2 -title {File Exists Condition} -component FileExistsCondition -TreeObject::id 9C1E4BD9-066D-ABCE-28D0-9E194B9F8475
@ -802,6 +799,9 @@ CreateQuickLaunchShortcut
28FDA3F4-B799-901F-8A27-AA04F0C022AB,Title,subst 28FDA3F4-B799-901F-8A27-AA04F0C022AB,Title,subst
1 1
2A230259-3A6F-8669-8B8B-23C3E7C1BFC2,Active
No
2A230259-3A6F-8669-8B8B-23C3E7C1BFC2,Conditions 2A230259-3A6F-8669-8B8B-23C3E7C1BFC2,Conditions
{3 conditions} {3 conditions}
@ -976,27 +976,6 @@ disabled
5C66451D-6042-DBDE-0D8C-31156EE244AD,Widget 5C66451D-6042-DBDE-0D8C-31156EE244AD,Widget
{Back Button;Next Button} {Back Button;Next Button}
5D20DD8D-064A-9922-29E1-A7FABEF3666A,Background
white
5D20DD8D-064A-9922-29E1-A7FABEF3666A,Conditions
{2 conditions}
5D20DD8D-064A-9922-29E1-A7FABEF3666A,Text,subst
1
5D20DD8D-064A-9922-29E1-A7FABEF3666A,Type
checkbutton
5D20DD8D-064A-9922-29E1-A7FABEF3666A,VirtualText
LaunchApplication
5D20DD8D-064A-9922-29E1-A7FABEF3666A,X
185
5D20DD8D-064A-9922-29E1-A7FABEF3666A,Y
130
5EC7056B-6F90-311E-2C6F-76E96164CFFD,CheckCondition 5EC7056B-6F90-311E-2C6F-76E96164CFFD,CheckCondition
{Before Action is Executed} {Before Action is Executed}
@ -1408,15 +1387,6 @@ disabled
A75C97CC-01AC-C12A-D663-A54E3257F11B,Widget A75C97CC-01AC-C12A-D663-A54E3257F11B,Widget
{Back Button;Next Button} {Back Button;Next Button}
A8856922-E6C1-160B-E55C-5C1806A89136,CheckCondition
{Before Action is Executed}
A8856922-E6C1-160B-E55C-5C1806A89136,Operator
false
A8856922-E6C1-160B-E55C-5C1806A89136,String
<%InstallStopped%>
AAEC34E6-7F02-18F2-30BB-744738192A3B,Conditions AAEC34E6-7F02-18F2-30BB-744738192A3B,Conditions
{2 conditions} {2 conditions}
@ -1730,12 +1700,6 @@ disabled
E5CBB018-A89D-3145-CFF5-CFC3B62BEA97,Widget E5CBB018-A89D-3145-CFF5-CFC3B62BEA97,Widget
{NextButton; CancelButton} {NextButton; CancelButton}
E5D227F7-E549-EFA9-1781-EFA6C5EEEC5C,CheckCondition
{Before Action is Executed}
E5D227F7-E549-EFA9-1781-EFA6C5EEEC5C,Filename
<%ProgramExecutable%>
E611105F-DC85-9E20-4F7B-E63C54E5DF06,Message,subst E611105F-DC85-9E20-4F7B-E63C54E5DF06,Message,subst
1 1
@ -2340,9 +2304,6 @@ Please make sure that calibre is not running, as this will cause the install to
48E8A9D6-B57E-C506-680D-898C65DD2A1B,Title 48E8A9D6-B57E-C506-680D-898C65DD2A1B,Title
<%InstallApplicationText%> <%InstallApplicationText%>
5D20DD8D-064A-9922-29E1-A7FABEF3666A,Text
<%LaunchApplicationText%>
64B8D0F3-4B11-DA22-D6E7-7248872D5FA7,Message 64B8D0F3-4B11-DA22-D6E7-7248872D5FA7,Message
<%UninstallStartupText%> <%UninstallStartupText%>
@ -2356,7 +2317,7 @@ Please make sure that calibre is not running, as this will cause the install to
{<%AppName%> Installation complete} {<%AppName%> Installation complete}
8A7FD0C2-F053-8764-F204-4BAE71E05708,Message 8A7FD0C2-F053-8764-F204-4BAE71E05708,Message
{Installation of <%AppName%> was successful. Click Finish to quit the installer.} {Installation of <%AppName%> was successful. Click Finish to quit the installer. <%AppName%> can be launched from the start menu.}
940F7FED-7D20-7264-3BF9-ED78205A76B3,Text 940F7FED-7D20-7264-3BF9-ED78205A76B3,Text
<%CreateDesktopShortcutText%> <%CreateDesktopShortcutText%>

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = 'calibre' __appname__ = 'calibre'
__version__ = '0.5.6' __version__ = '0.5.7'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
''' '''
Various run time constants. Various run time constants.

View File

@ -143,7 +143,7 @@ class OutputProfile(Plugin):
# ADE dies an agonizing, long drawn out death if HTML files have more # ADE dies an agonizing, long drawn out death if HTML files have more
# bytes than this. # bytes than this.
flow_size = sys.maxint flow_size = -1
# ADE runs screaming when it sees these characters # ADE runs screaming when it sees these characters
remove_special_chars = re.compile(u'[\u200b\u00ad]') remove_special_chars = re.compile(u'[\u200b\u00ad]')
# ADE falls to the ground in a dead faint when it sees an <object> # ADE falls to the ground in a dead faint when it sees an <object>

View File

@ -2,7 +2,7 @@ from __future__ import with_statement
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, shutil, traceback, functools, sys import os, shutil, traceback, functools, sys, re
from calibre.customize import Plugin, FileTypePlugin, MetadataReaderPlugin, \ from calibre.customize import Plugin, FileTypePlugin, MetadataReaderPlugin, \
MetadataWriterPlugin MetadataWriterPlugin
@ -55,7 +55,14 @@ def load_plugin(path_to_zip_file):
for name in zf.namelist(): for name in zf.namelist():
if name.lower().endswith('plugin.py'): if name.lower().endswith('plugin.py'):
locals = {} locals = {}
exec zf.read(name) in locals raw = zf.read(name)
match = re.search(r'coding[:=]\s*([-\w.]+)', raw[:300])
encoding = 'utf-8'
if match is not None:
encoding = match.group(1)
raw = raw.decode(encoding)
raw = re.sub('\r\n', '\n', raw)
exec raw in locals
for x in locals.values(): for x in locals.values():
if isinstance(x, type) and issubclass(x, Plugin): if isinstance(x, type) and issubclass(x, Plugin):
if x.minimum_calibre_version > version or \ if x.minimum_calibre_version > version or \

View File

@ -31,6 +31,11 @@ Run an embedded python interpreter.
parser.add_option('--migrate', action='store_true', default=False, parser.add_option('--migrate', action='store_true', default=False,
help='Migrate old database. Needs two arguments. Path ' help='Migrate old database. Needs two arguments. Path '
'to library1.db and path to new library folder.') 'to library1.db and path to new library folder.')
parser.add_option('--add-simple-plugin', default=None,
help='Add a simple plugin (i.e. a plugin that consists of only a '
'.py file), by specifying the path to the py file containing the '
'plugin code.')
return parser return parser
def update_zipfile(zipfile, mod, path): def update_zipfile(zipfile, mod, path):
@ -115,6 +120,22 @@ def debug_device_driver():
print 'Total space:', d.total_space() print 'Total space:', d.total_space()
break break
def add_simple_plugin(path_to_plugin):
import tempfile, zipfile, shutil
tdir = tempfile.mkdtemp()
open(os.path.join(tdir, 'custom_plugin.py'),
'wb').write(open(path_to_plugin, 'rb').read())
odir = os.getcwd()
os.chdir(tdir)
zf = zipfile.ZipFile('plugin.zip', 'w')
zf.write('custom_plugin.py')
zf.close()
from calibre.customize.ui import main
main(['calibre-customize', '-a', 'plugin.zip'])
os.chdir(odir)
shutil.rmtree(tdir)
def main(args=sys.argv): def main(args=sys.argv):
opts, args = option_parser().parse_args(args) opts, args = option_parser().parse_args(args)
@ -137,6 +158,8 @@ def main(args=sys.argv):
print 'You must specify the path to library1.db and the path to the new library folder' print 'You must specify the path to library1.db and the path to the new library folder'
return 1 return 1
migrate(args[1], args[2]) migrate(args[1], args[2])
elif opts.add_simple_plugin is not None:
add_simple_plugin(opts.add_simple_plugin)
else: else:
from IPython.Shell import IPShellEmbed from IPython.Shell import IPShellEmbed
ipshell = IPShellEmbed() ipshell = IPShellEmbed()

View File

@ -209,7 +209,7 @@ class Device(_Device):
time.sleep(6) time.sleep(6)
drives = {} drives = {}
wmi = __import__('wmi', globals(), locals(), [], -1) wmi = __import__('wmi', globals(), locals(), [], -1)
c = wmi.WMI() c = wmi.WMI(find_classes=False)
for drive in c.Win32_DiskDrive(): for drive in c.Win32_DiskDrive():
if self.windows_match_device(str(drive.PNPDeviceID), self.WINDOWS_MAIN_MEM): if self.windows_match_device(str(drive.PNPDeviceID), self.WINDOWS_MAIN_MEM):
drives['main'] = self.windows_get_drive_prefix(drive) drives['main'] = self.windows_get_drive_prefix(drive)

View File

@ -94,7 +94,8 @@ OptionRecommendation(name='font_size_mapping',
OptionRecommendation(name='line_height', OptionRecommendation(name='line_height',
recommended_value=None, level=OptionRecommendation.LOW, recommended_value=None, level=OptionRecommendation.LOW,
help=_('The line height in pts. Controls spacing between consecutive ' help=_('The line height in pts. Controls spacing between consecutive '
'lines of text. By default ??' 'lines of text. By default no line height manipulation is '
'performed.'
) )
), ),
@ -102,12 +103,25 @@ OptionRecommendation(name='linearize_tables',
recommended_value=False, level=OptionRecommendation.LOW, recommended_value=False, level=OptionRecommendation.LOW,
help=_('Some badly designed documents use tables to control the ' help=_('Some badly designed documents use tables to control the '
'layout of text on the page. When converted these documents ' 'layout of text on the page. When converted these documents '
'often have text that runs of the page and other artifacts. ' 'often have text that runs off the page and other artifacts. '
'This option will extract the content from the tables and ' 'This option will extract the content from the tables and '
'present it in a linear fashion.' 'present it in a linear fashion.'
) )
), ),
OptionRecommendation(name='dont_split_on_page_breaks',
recommended_value=False, level=OptionRecommendation.LOW,
help=_('Turn off splitting at page breaks. Normally, input '
'files are automatically split at every page break into '
'two files. This gives an output ebook that can be '
'parsed faster and with less resources. However, '
'splitting is slow and if your source file contains a '
'very large number of page breaks, you should turn off '
'splitting on page breaks.'
)
),
OptionRecommendation(name='read_metadata_from_opf', OptionRecommendation(name='read_metadata_from_opf',
recommended_value=None, level=OptionRecommendation.LOW, recommended_value=None, level=OptionRecommendation.LOW,
short_switch='m', short_switch='m',
@ -330,6 +344,17 @@ OptionRecommendation(name='language',
untable=self.opts.linearize_tables) untable=self.opts.linearize_tables)
flattener(self.oeb, self.opts) flattener(self.oeb, self.opts)
if self.opts.linearize_tables:
from calibre.ebooks.oeb.transforms.linearize_tables import LinearizeTables
LinearizeTables()(self.oeb, self.opts)
from calibre.ebooks.oeb.transforms.split import Split
pbx = accelerators.get('pagebreaks', None)
split = Split(not self.opts.dont_split_on_page_breaks,
max_flow_size=self.opts.output_profile.flow_size,
page_breaks_xpath=pbx)
split(self.oeb, self.opts)
from calibre.ebooks.oeb.transforms.trimmanifest import ManifestTrimmer from calibre.ebooks.oeb.transforms.trimmanifest import ManifestTrimmer
self.log.info('Cleaning up manifest...') self.log.info('Cleaning up manifest...')

View File

@ -25,7 +25,7 @@ def get_metadata(stream):
for item in litfile.manifest.values(): for item in litfile.manifest.values():
if item.path in candidates: if item.path in candidates:
try: try:
covers.append((litfile.get_file('/data/'+item.internal), covers.append((litfile.get_file('/data/'+item.internal),
ctype)) ctype))
except: except:
pass pass
@ -33,7 +33,7 @@ def get_metadata(stream):
covers.sort(cmp=lambda x, y:cmp(len(x[0]), len(y[0])), reverse=True) covers.sort(cmp=lambda x, y:cmp(len(x[0]), len(y[0])), reverse=True)
idx = 0 idx = 0
if len(covers) > 1: if len(covers) > 1:
if covers[1][1] == covers[1][0]+'-standard': if covers[1][1] == covers[0][1]+'-standard':
idx = 1 idx = 1
mi.cover_data = ('jpg', covers[idx][0]) mi.cover_data = ('jpg', covers[idx][0])
return mi return mi

View File

@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, re, collections import os, re, collections
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.ebooks.metadata.opf2 import OPF from calibre.ebooks.metadata.opf2 import OPF
from calibre.customize.ui import get_file_type_metadata, set_file_type_metadata from calibre.customize.ui import get_file_type_metadata, set_file_type_metadata
@ -37,18 +37,18 @@ def metadata_from_formats(formats):
mi2 = opf_metadata(opf) mi2 = opf_metadata(opf)
if mi2 is not None and mi2.title: if mi2 is not None and mi2.title:
return mi2 return mi2
for path, ext in zip(formats, extensions): for path, ext in zip(formats, extensions):
with open(path, 'rb') as stream: with open(path, 'rb') as stream:
try: try:
newmi = get_metadata(stream, stream_type=ext, newmi = get_metadata(stream, stream_type=ext,
use_libprs_metadata=True) use_libprs_metadata=True)
mi.smart_update(newmi) mi.smart_update(newmi)
except: except:
continue continue
if getattr(mi, 'application_id', None) is not None: if getattr(mi, 'application_id', None) is not None:
return mi return mi
if not mi.title: if not mi.title:
mi.title = _('Unknown') mi.title = _('Unknown')
if not mi.authors: if not mi.authors:
@ -64,20 +64,20 @@ def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False):
stream_type = 'mobi' stream_type = 'mobi'
if stream_type in ('odt', 'ods', 'odp', 'odg', 'odf'): if stream_type in ('odt', 'ods', 'odp', 'odg', 'odf'):
stream_type = 'odt' stream_type = 'odt'
opf = None opf = None
if hasattr(stream, 'name'): if hasattr(stream, 'name'):
c = os.path.splitext(stream.name)[0]+'.opf' c = os.path.splitext(stream.name)[0]+'.opf'
if os.access(c, os.R_OK): if os.access(c, os.R_OK):
opf = opf_metadata(os.path.abspath(c)) opf = opf_metadata(os.path.abspath(c))
if use_libprs_metadata and getattr(opf, 'application_id', None) is not None: if use_libprs_metadata and getattr(opf, 'application_id', None) is not None:
return opf return opf
mi = MetaInformation(None, None) mi = MetaInformation(None, None)
if prefs['read_file_metadata']: if prefs['read_file_metadata']:
mi = get_file_type_metadata(stream, stream_type) mi = get_file_type_metadata(stream, stream_type)
name = os.path.basename(getattr(stream, 'name', '')) name = os.path.basename(getattr(stream, 'name', ''))
base = metadata_from_filename(name) base = metadata_from_filename(name)
if base.title == os.path.splitext(name)[0] and base.authors is None: if base.title == os.path.splitext(name)[0] and base.authors is None:
@ -98,17 +98,17 @@ def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False):
base.smart_update(mi) base.smart_update(mi)
if opf is not None: if opf is not None:
base.smart_update(opf) base.smart_update(opf)
return base return base
def set_metadata(stream, mi, stream_type='lrf'): def set_metadata(stream, mi, stream_type='lrf'):
if stream_type: if stream_type:
stream_type = stream_type.lower() stream_type = stream_type.lower()
set_file_type_metadata(stream, mi, stream_type) set_file_type_metadata(stream, mi, stream_type)
def metadata_from_filename(name, pat=None): def metadata_from_filename(name, pat=None):
name = os.path.splitext(name)[0] name = name.rpartition('.')[0]
mi = MetaInformation(None, None) mi = MetaInformation(None, None)
if pat is None: if pat is None:
pat = re.compile(prefs.get('filename_pattern')) pat = re.compile(prefs.get('filename_pattern'))
@ -161,7 +161,7 @@ def opf_metadata(opfpath):
mi = MetaInformation(opf) mi = MetaInformation(opf)
if hasattr(opf, 'cover') and opf.cover: if hasattr(opf, 'cover') and opf.cover:
cpath = os.path.join(os.path.dirname(opfpath), opf.cover) cpath = os.path.join(os.path.dirname(opfpath), opf.cover)
if os.access(cpath, os.R_OK): if os.access(cpath, os.R_OK):
fmt = cpath.rpartition('.')[-1] fmt = cpath.rpartition('.')[-1]
data = open(cpath, 'rb').read() data = open(cpath, 'rb').read()
mi.cover_data = (fmt, data) mi.cover_data = (fmt, data)

View File

@ -1,9 +1,8 @@
'''Read meta information from PDF files'''
from __future__ import with_statement from __future__ import with_statement
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''Read meta information from PDF files'''
import sys, os, StringIO import sys, os, StringIO
@ -31,7 +30,7 @@ def get_metadata(stream, extract_cover=True):
except: except:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
try: try:
info = PdfFileReader(stream).getDocumentInfo() info = PdfFileReader(stream).getDocumentInfo()
if info.title: if info.title:
@ -52,23 +51,18 @@ def get_metadata(stream, extract_cover=True):
def set_metadata(stream, mi): def set_metadata(stream, mi):
stream.seek(0) stream.seek(0)
# Use a StringIO object for the pdf because we will want to over # Use a StringIO object for the pdf because we will want to over
# write it later and if we are working on the stream directly it # write it later and if we are working on the stream directly it
# could cause some issues. # could cause some issues.
raw = StringIO.StringIO(stream.read()) raw = StringIO.StringIO(stream.read())
orig_pdf = PdfFileReader(raw) orig_pdf = PdfFileReader(raw)
title = mi.title if mi.title else orig_pdf.documentInfo.title title = mi.title if mi.title else orig_pdf.documentInfo.title
author = authors_to_string(mi.authors) if mi.authors else orig_pdf.documentInfo.author author = authors_to_string(mi.authors) if mi.authors else orig_pdf.documentInfo.author
out_pdf = PdfFileWriter(title=title, author=author) out_pdf = PdfFileWriter(title=title, author=author)
for page in orig_pdf.pages: for page in orig_pdf.pages:
out_pdf.addPage(page) out_pdf.addPage(page)
out_str = StringIO.StringIO() out_str = StringIO.StringIO()
out_pdf.write(out_str) out_pdf.write(out_str)
stream.seek(0) stream.seek(0)
stream.truncate() stream.truncate()
out_str.seek(0) out_str.seek(0)
@ -77,31 +71,31 @@ def set_metadata(stream, mi):
def get_cover(stream): def get_cover(stream):
data = StringIO.StringIO() data = StringIO.StringIO()
try: try:
pdf = PdfFileReader(stream) pdf = PdfFileReader(stream)
output = PdfFileWriter() output = PdfFileWriter()
if len(pdf.pages) >= 1: if len(pdf.pages) >= 1:
output.addPage(pdf.getPage(0)) output.addPage(pdf.getPage(0))
with TemporaryDirectory('_pdfmeta') as tdir: with TemporaryDirectory('_pdfmeta') as tdir:
cover_path = os.path.join(tdir, 'cover.pdf') cover_path = os.path.join(tdir, 'cover.pdf')
outputStream = file(cover_path, "wb") outputStream = file(cover_path, "wb")
output.write(outputStream) output.write(outputStream)
outputStream.close() outputStream.close()
wand = NewMagickWand() wand = NewMagickWand()
MagickReadImage(wand, cover_path) MagickReadImage(wand, cover_path)
MagickSetImageFormat(wand, 'JPEG') MagickSetImageFormat(wand, 'JPEG')
MagickWriteImage(wand, '%s.jpg' % cover_path) MagickWriteImage(wand, '%s.jpg' % cover_path)
img = Image.open('%s.jpg' % cover_path) img = Image.open('%s.jpg' % cover_path)
img.save(data, 'JPEG') img.save(data, 'JPEG')
except: except:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return data.getvalue() return data.getvalue()

View File

@ -29,5 +29,5 @@ class MOBIInput(InputFormatPlugin):
with open(f, 'wb') as q: with open(f, 'wb') as q:
q.write(html.tostring(root, encoding='utf-8', method='xml', q.write(html.tostring(root, encoding='utf-8', method='xml',
include_meta_content_type=False)) include_meta_content_type=False))
accelerators['pagebreaks'] = {f: '//*[@class="mbp_pagebreak"]'} accelerators['pagebreaks'] = '//h:div[@class="mbp_pagebreak"]'
return mr.created_opf_path return mr.created_opf_path

View File

@ -160,35 +160,31 @@ class BookHeader(object):
class MetadataHeader(BookHeader): class MetadataHeader(BookHeader):
def __init__(self, stream, log): def __init__(self, stream, log):
self.stream = stream self.stream = stream
self.ident = self.identity() self.ident = self.identity()
self.num_sections = self.section_count() self.num_sections = self.section_count()
if self.num_sections >= 2: if self.num_sections >= 2:
header = self.header() header = self.header()
BookHeader.__init__(self, header, self.ident, None, log) BookHeader.__init__(self, header, self.ident, None, log)
else: else:
self.exth = None self.exth = None
def identity(self): def identity(self):
self.stream.seek(60) self.stream.seek(60)
ident = self.stream.read(8).upper() ident = self.stream.read(8).upper()
if ident not in ['BOOKMOBI', 'TEXTREAD']: if ident not in ['BOOKMOBI', 'TEXTREAD']:
raise MobiError('Unknown book type: %s' % ident) raise MobiError('Unknown book type: %s' % ident)
return ident return ident
def section_count(self): def section_count(self):
self.stream.seek(76) self.stream.seek(76)
return struct.unpack('>H', self.stream.read(2))[0] return struct.unpack('>H', self.stream.read(2))[0]
def section_offset(self, number): def section_offset(self, number):
self.stream.seek(78+number*8) self.stream.seek(78+number*8)
return struct.unpack('>LBBBB', self.stream.read(8))[0] return struct.unpack('>LBBBB', self.stream.read(8))[0]
def header(self): def header(self):
section_headers = [] section_headers = []
# First section with the metadata # First section with the metadata
section_headers.append(self.section_offset(0)) section_headers.append(self.section_offset(0))
# Second section used to get the lengh of the first # Second section used to get the lengh of the first
@ -196,20 +192,16 @@ class MetadataHeader(BookHeader):
end_off = section_headers[1] end_off = section_headers[1]
off = section_headers[0] off = section_headers[0]
self.stream.seek(off) self.stream.seek(off)
return self.stream.read(end_off - off) return self.stream.read(end_off - off)
def section_data(self, number): def section_data(self, number):
start = self.section_offset(number) start = self.section_offset(number)
if number == self.num_sections -1: if number == self.num_sections -1:
end = os.stat(self.stream.name).st_size end = os.stat(self.stream.name).st_size
else: else:
end = self.section_offset(number + 1) end = self.section_offset(number + 1)
self.stream.seek(start) self.stream.seek(start)
return self.stream.read(end - start) return self.stream.read(end - start)
@ -651,7 +643,7 @@ class MobiReader(object):
def get_metadata(stream): def get_metadata(stream):
from calibre.utils.logging import Log from calibre.utils.logging import Log
log = Log() log = Log()
mi = MetaInformation(os.path.basename(stream.name), [_('Unknown')]) mi = MetaInformation(os.path.basename(stream.name), [_('Unknown')])
try: try:
mh = MetadataHeader(stream, log) mh = MetadataHeader(stream, log)
@ -666,7 +658,6 @@ def get_metadata(stream):
mr.extract_content(tdir, parse_cache) mr.extract_content(tdir, parse_cache)
if mr.embedded_mi is not None: if mr.embedded_mi is not None:
mi = mr.embedded_mi mi = mr.embedded_mi
if hasattr(mh.exth, 'cover_offset'): if hasattr(mh.exth, 'cover_offset'):
cover_index = mh.first_image_index + mh.exth.cover_offset cover_index = mh.first_image_index + mh.exth.cover_offset
data = mh.section_data(int(cover_index)) data = mh.section_data(int(cover_index))
@ -679,5 +670,4 @@ def get_metadata(stream):
mi.cover_data = ('jpg', obuf.getvalue()) mi.cover_data = ('jpg', obuf.getvalue())
except: except:
log.exception() log.exception()
return mi return mi

View File

@ -218,7 +218,7 @@ class Serializer(object):
for elem in item.data.find(XHTML('body')): for elem in item.data.find(XHTML('body')):
self.serialize_elem(elem, item) self.serialize_elem(elem, item)
#buffer.write('</mbp:section>') #buffer.write('</mbp:section>')
buffer.write('</mbp:pagebreak>') buffer.write('<mbp:pagebreak/>')
def serialize_elem(self, elem, item, nsrmap=NSRMAP): def serialize_elem(self, elem, item, nsrmap=NSRMAP):
buffer = self.buffer buffer = self.buffer

View File

@ -272,11 +272,26 @@ def XPath(expr):
def xpath(elem, expr): def xpath(elem, expr):
return elem.xpath(expr, namespaces=XPNSMAP) return elem.xpath(expr, namespaces=XPNSMAP)
def xml2str(root, pretty_print=False): def _prepare_xml_for_serialization(root):
return etree.tostring(root, encoding='utf-8', xml_declaration=True, root.set('xmlns', XHTML_NS)
root.set('{%s}xlink'%XHTML_NS, XLINK_NS)
for x in root.iter():
if hasattr(x.tag, 'rpartition') and x.tag.rpartition('}')[-1].lower() == 'svg':
x.set('xmlns', SVG_NS)
def xml2str(root, pretty_print=False, strip_comments=False):
_prepare_xml_for_serialization(root)
ans = etree.tostring(root, encoding='utf-8', xml_declaration=True,
pretty_print=pretty_print) pretty_print=pretty_print)
if strip_comments:
ans = re.compile(r'<!--.*?-->', re.DOTALL).sub('', ans)
return ans
def xml2unicode(root, pretty_print=False): def xml2unicode(root, pretty_print=False):
_prepare_xml_for_serialization(root)
return etree.tostring(root, pretty_print=pretty_print) return etree.tostring(root, pretty_print=pretty_print)
ASCII_CHARS = set(chr(x) for x in xrange(128)) ASCII_CHARS = set(chr(x) for x in xrange(128))
@ -826,6 +841,11 @@ class Manifest(object):
return xml2str(data, pretty_print=self.oeb.pretty_print) return xml2str(data, pretty_print=self.oeb.pretty_print)
if isinstance(data, unicode): if isinstance(data, unicode):
return data.encode('utf-8') return data.encode('utf-8')
if hasattr(data, 'cssText'):
data = data.cssText
if isinstance(data, unicode):
data = data.encode('utf-8')
return data
return str(data) return str(data)
def __unicode__(self): def __unicode__(self):
@ -834,6 +854,8 @@ class Manifest(object):
return xml2unicode(data, pretty_print=self.oeb.pretty_print) return xml2unicode(data, pretty_print=self.oeb.pretty_print)
if isinstance(data, unicode): if isinstance(data, unicode):
return data return data
if hasattr(data, 'cssText'):
return data.cssText
return unicode(data) return unicode(data)
def __eq__(self, other): def __eq__(self, other):
@ -1044,6 +1066,12 @@ class Spine(object):
self.items[i].spine_position = i self.items[i].spine_position = i
item.spine_position = None item.spine_position = None
def index(self, item):
for i, x in enumerate(self):
if item == x:
return i
return -1
def __iter__(self): def __iter__(self):
for item in self.items: for item in self.items:
yield item yield item

View File

@ -163,7 +163,6 @@ class EbookIterator(object):
s.pages = p s.pages = p
start = 1 start = 1
for s in self.spine: for s in self.spine:
s.start_page = start s.start_page = start
start += s.pages start += s.pages

View File

@ -22,7 +22,6 @@ class OEBOutput(OutputFormatPlugin):
if not os.path.exists(output_path): if not os.path.exists(output_path):
os.makedirs(output_path) os.makedirs(output_path)
from calibre.ebooks.oeb.base import OPF_MIME, NCX_MIME, PAGE_MAP_MIME from calibre.ebooks.oeb.base import OPF_MIME, NCX_MIME, PAGE_MAP_MIME
from calibre.ebooks.html import tostring as html_tostring
with CurrentDir(output_path): with CurrentDir(output_path):
results = oeb_book.to_opf2(page_map=True) results = oeb_book.to_opf2(page_map=True)
for key in (OPF_MIME, NCX_MIME, PAGE_MAP_MIME): for key in (OPF_MIME, NCX_MIME, PAGE_MAP_MIME):
@ -38,16 +37,7 @@ class OEBOutput(OutputFormatPlugin):
dir = os.path.dirname(path) dir = os.path.dirname(path)
if not os.path.exists(dir): if not os.path.exists(dir):
os.makedirs(dir) os.makedirs(dir)
raw = item.data
if not isinstance(raw, basestring):
if hasattr(raw, 'cssText'):
raw = raw.cssText
else:
raw = html_tostring(raw,
pretty_print=opts.pretty_print)
if isinstance(raw, unicode):
raw = raw.encode('utf-8')
with open(path, 'wb') as f: with open(path, 'wb') as f:
f.write(raw) f.write(str(item))

View File

@ -0,0 +1,21 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.ebooks.oeb.base import OEB_DOCS, XPNSMAP
class LinearizeTables(object):
def linearize(self, root):
for x in root.xpath('//h:table|//h:td|//h:tr|//h:th',
namespaces=XPNSMAP):
x.tag = 'div'
def __call__(self, oeb, context):
for x in oeb.manifest.items:
if x.media_type in OEB_DOCS:
self.linearize(x.data)

View File

@ -4,21 +4,25 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
''' '''
Split the flows in an epub file to conform to size limitations. Splitting of the XHTML flows. Splitting can happen on page boundaries or can be
forces at "likely" locations to conform to size limitations. This transform
assumes a prior call to the flatcss transform.
''' '''
import os, math, functools, collections, re, copy, sys import os, math, functools, collections, re, copy
from lxml.etree import XPath as _XPath from lxml.etree import XPath as _XPath
from lxml import etree, html from lxml import etree, html
from lxml.cssselect import CSSSelector from lxml.cssselect import CSSSelector
from calibre.ebooks.metadata.opf2 import OPF from calibre.ebooks.oeb.base import OEB_STYLES, XPNSMAP, urldefrag, \
rewrite_links
from calibre.ebooks.epub import tostring, rules from calibre.ebooks.epub import tostring, rules
from calibre import CurrentDir
XPath = functools.partial(_XPath, namespaces={'re':'http://exslt.org/regular-expressions'}) NAMESPACES = dict(XPNSMAP)
content = functools.partial(os.path.join, 'content') NAMESPACES['re'] = 'http://exslt.org/regular-expressions'
XPath = functools.partial(_XPath, namespaces=NAMESPACES)
SPLIT_ATTR = 'cs' SPLIT_ATTR = 'cs'
SPLIT_POINT_ATTR = 'csp' SPLIT_POINT_ATTR = 'csp'
@ -27,149 +31,166 @@ class SplitError(ValueError):
def __init__(self, path, root): def __init__(self, path, root):
size = len(tostring(root))/1024. size = len(tostring(root))/1024.
ValueError.__init__(self, _('Could not find reasonable point at which to split: %s Sub-tree size: %d KB')% ValueError.__init__(self,
(os.path.basename(path), size)) _('Could not find reasonable point at which to split: '
'%s Sub-tree size: %d KB')%
(path, size))
class Split(object):
def __init__(self, split_on_page_breaks=True, page_breaks_xpath=None,
max_flow_size=0):
self.split_on_page_breaks = split_on_page_breaks
self.page_breaks_xpath = page_breaks_xpath
self.max_flow_size = max_flow_size
if self.page_breaks_xpath is not None:
self.page_breaks_xpath = XPath(self.page_breaks_xpath)
def __call__(self, oeb, context):
self.oeb = oeb
self.log = oeb.log
self.map = {}
self.page_break_selectors = None
for item in self.oeb.manifest.items:
if etree.iselement(item.data):
self.split_item(item)
self.fix_links()
def split_item(self, item):
if self.split_on_page_breaks:
if self.page_breaks_xpath is None:
page_breaks, page_break_ids = self.find_page_breaks(item)
else:
page_breaks, page_break_ids = self.page_breaks_xpath(item.data)
splitter = FlowSplitter(item, page_breaks, page_break_ids,
self.max_flow_size, self.oeb)
if splitter.was_split:
self.map[item.href] = dict(splitter.anchor_map)
def find_page_breaks(self, item):
if self.page_break_selectors is None:
self.page_break_selectors = set([])
stylesheets = [x.data for x in self.oeb.manifest if x.media_type in
OEB_STYLES]
page_break_selectors = set([])
for rule in rules(stylesheets):
before = getattr(rule.style.getPropertyCSSValue(
'page-break-before'), 'cssText', '').strip().lower()
after = getattr(rule.style.getPropertyCSSValue(
'page-break-after'), 'cssText', '').strip().lower()
try:
if before and before != 'avoid':
page_break_selectors.add((CSSSelector(rule.selectorText),
True))
except:
pass
try:
if after and after != 'avoid':
page_break_selectors.add((CSSSelector(rule.selectorText),
False))
except:
pass
page_breaks = set([])
for selector, before in page_break_selectors:
for elem in selector(item.data):
elem.pb_before = before
page_breaks.add(elem)
for i, elem in enumerate(item.data.iter()):
elem.pb_order = i
page_breaks = list(page_breaks)
page_breaks.sort(cmp=lambda x,y : cmp(x.pb_order, y.pb_order))
page_break_ids, page_breaks_ = [], []
for i, x in enumerate(page_breaks):
x.set('id', x.get('id', 'calibre_pb_%d'%i))
id = x.get('id')
page_breaks_.append((XPath('//*[@id="%s"]'%id), x.pb_before))
page_break_ids.append(id)
return page_breaks_, page_break_ids
def fix_links(self, opf):
'''
Fix references to the split files in other content files.
'''
for item in self.oeb.manifest:
if etree.iselement(item.data):
self.current_item = item
rewrite_links(item.data, self.rewrite_links)
def rewrite_links(self, url):
href, frag = urldefrag(url)
href = self.current_item.abshref(href)
if href in self.map:
anchor_map = self.map[href]
nhref = anchor_map[frag if frag else None]
if frag:
nhref = '#'.joinn(href, frag)
return nhref
return url
class Splitter(object): class FlowSplitter(object):
def __init__(self, path, opts, stylesheet_map, opf): def __init__(self, item, page_breaks, page_break_ids, max_flow_size, oeb):
self.setup_cli_handler(opts.verbose) self.item = item
self.path = path self.oeb = oeb
self.always_remove = not opts.preserve_tag_structure or \ self.log = oeb.log
os.stat(content(path)).st_size > 5*opts.profile.flow_size self.page_breaks = page_breaks
self.base = (os.path.splitext(path)[0].replace('%', '%%') + '_split_%d.html') self.page_break_ids = page_break_ids
self.opts = opts self.max_flow_size = max_flow_size
self.orig_size = os.stat(content(path)).st_size self.base = item.abshref(item.href)
self.log_info('\tSplitting %s (%d KB)', path, self.orig_size/1024.)
root = html.fromstring(open(content(path)).read())
self.page_breaks, self.trees = [], [] base, ext = os.path.splitext(self.base)
self.split_size = 0 self.base = base.replace('%', '%%')+'_split_%d'+ext
# Split on page breaks self.trees = [self.item.data]
self.splitting_on_page_breaks = True self.splitting_on_page_breaks = True
if not opts.dont_split_on_page_breaks: if self.page_breaks:
self.log_info('\tSplitting on page breaks...') self.split_on_page_breaks(self.item.data)
if self.path in stylesheet_map:
self.find_page_breaks(stylesheet_map[self.path], root)
self.split_on_page_breaks(root.getroottree())
trees = list(self.trees)
else:
self.trees = [root.getroottree()]
trees = list(self.trees)
# Split any remaining over-sized trees
self.splitting_on_page_breaks = False self.splitting_on_page_breaks = False
if self.opts.profile.flow_size < sys.maxint:
if self.max_flow_size > 0:
lt_found = False lt_found = False
self.log_info('\tLooking for large trees...') self.log('\tLooking for large trees...')
for i, tree in enumerate(list(trees)): trees = list(self.trees)
for i, tree in enumerate(list(self.trees)):
self.trees = [] self.trees = []
size = len(tostring(tree.getroot())) size = len(tostring(tree.getroot()))
if size > self.opts.profile.flow_size: if size > self.opts.profile.flow_size:
lt_found = True lt_found = True
try: self.split_to_size(tree)
self.split_to_size(tree)
except (SplitError, RuntimeError): # Splitting fails
if not self.always_remove:
self.always_remove = True
self.split_to_size(tree)
else:
raise
trees[i:i+1] = list(self.trees) trees[i:i+1] = list(self.trees)
if not lt_found: if not lt_found:
self.log_info('\tNo large trees found') self.log_info('\tNo large trees found')
self.trees = trees
self.trees = trees
self.was_split = len(self.trees) > 1 self.was_split = len(self.trees) > 1
if self.was_split: self.commit()
self.commit()
self.log_info('\t\tSplit into %d parts.', len(self.trees))
if self.opts.verbose:
for f in self.files:
self.log_info('\t\t\t%s - %d KB', f, os.stat(content(f)).st_size/1024.)
self.fix_opf(opf)
self.trees = None def split_on_page_breaks(self, orig_tree):
ordered_ids = []
for elem in orig_tree.xpath('//*[@id]'):
id = elem.get('id')
if id in self.page_break_ids:
ordered_ids.append(self.page_breaks[self.page_break_ids.index(id)])
self.trees = []
def split_text(self, text, root, size): tree = orig_tree
self.log_debug('\t\t\tSplitting text of length: %d'%len(text)) for pattern, before in ordered_ids:
rest = text.replace('\r', '') self.log.debug('\t\tSplitting on page-break')
parts = re.split('\n\n', rest) elem = pattern(tree)
self.log_debug('\t\t\t\tFound %d parts'%len(parts)) if elem:
if max(map(len, parts)) > size: before, after = self.do_split(tree, elem[0], before)
raise SplitError('Cannot split as file contains a <pre> tag with a very large paragraph', root) self.trees.append(before)
ans = [] tree = after
buf = '' self.trees.append(tree)
for part in parts: self.trees = [t for t in self.trees if not self.is_page_empty(t.getroot())]
if len(buf) + len(part) < size:
buf += '\n\n'+part
else:
ans.append(buf)
buf = part
return ans
def split_to_size(self, tree):
self.log_debug('\t\tSplitting...')
root = tree.getroot()
# Split large <pre> tags
for pre in list(root.xpath('//pre')):
text = u''.join(pre.xpath('descendant::text()'))
pre.text = text
for child in list(pre.iterchildren()):
pre.remove(child)
if len(pre.text) > self.opts.profile.flow_size*0.5:
frags = self.split_text(pre.text, root, int(0.2*self.opts.profile.flow_size))
new_pres = []
for frag in frags:
pre2 = copy.copy(pre)
pre2.text = frag
pre2.tail = u''
new_pres.append(pre2)
new_pres[-1].tail = pre.tail
p = pre.getparent()
i = p.index(pre)
p[i:i+1] = new_pres
split_point, before = self.find_split_point(root)
if split_point is None or self.split_size > 6*self.orig_size:
if not self.always_remove:
self.log_warn(_('\t\tToo much markup. Re-splitting without '
'structure preservation. This may cause '
'incorrect rendering.'))
raise SplitError(self.path, root)
for t in self.do_split(tree, split_point, before):
r = t.getroot()
if self.is_page_empty(r):
continue
size = len(tostring(r))
if size <= self.opts.profile.flow_size:
self.trees.append(t)
#print tostring(t.getroot(), pretty_print=True)
self.log_debug('\t\t\tCommitted sub-tree #%d (%d KB)',
len(self.trees), size/1024.)
self.split_size += size
else:
self.split_to_size(t)
def is_page_empty(self, root):
body = root.find('body')
if body is None:
return False
txt = re.sub(r'\s+', '', html.tostring(body, method='text', encoding=unicode))
if len(txt) > 4:
#if len(txt) < 100:
# print 1111111, html.tostring(body, method='html', encoding=unicode)
return False
for img in root.xpath('//img'):
if img.get('style', '') != 'display:none':
return False
return True
def do_split(self, tree, split_point, before): def do_split(self, tree, split_point, before):
''' '''
@ -190,7 +211,7 @@ class Splitter(object):
split_point2 = root2.xpath(path)[0] split_point2 = root2.xpath(path)[0]
def nix_element(elem, top=True): def nix_element(elem, top=True):
if self.always_remove: if True:
parent = elem.getparent() parent = elem.getparent()
index = parent.index(elem) index = parent.index(elem)
if top: if top:
@ -198,7 +219,6 @@ class Splitter(object):
else: else:
index = parent.index(elem) index = parent.index(elem)
parent[index:index+1] = list(elem.iterchildren()) parent[index:index+1] = list(elem.iterchildren())
else: else:
elem.text = u'' elem.text = u''
elem.tail = u'' elem.tail = u''
@ -241,67 +261,76 @@ class Splitter(object):
return tree, tree2 return tree, tree2
def is_page_empty(self, root):
body = root.find('body')
if body is None:
return False
txt = re.sub(r'\s+', '', html.tostring(body, method='text', encoding=unicode))
if len(txt) > 4:
return False
for img in root.xpath('//img'):
if img.get('style', '') != 'display:none':
return False
return True
def split_on_page_breaks(self, orig_tree): def split_text(self, text, root, size):
ordered_ids = [] self.log.debug('\t\t\tSplitting text of length: %d'%len(text))
for elem in orig_tree.xpath('//*[@id]'): rest = text.replace('\r', '')
id = elem.get('id') parts = re.split('\n\n', rest)
if id in self.page_break_ids: self.log.debug('\t\t\t\tFound %d parts'%len(parts))
ordered_ids.append(self.page_breaks[self.page_break_ids.index(id)]) if max(map(len, parts)) > size:
raise SplitError('Cannot split as file contains a <pre> tag '
self.trees = [] 'with a very large paragraph', root)
tree = orig_tree ans = []
for pattern, before in ordered_ids: buf = ''
self.log_info('\t\tSplitting on page-break') for part in parts:
elem = pattern(tree) if len(buf) + len(part) < size:
if elem: buf += '\n\n'+part
before, after = self.do_split(tree, elem[0], before) else:
self.trees.append(before) ans.append(buf)
tree = after buf = part
self.trees.append(tree) return ans
self.trees = [t for t in self.trees if not self.is_page_empty(t.getroot())]
def split_to_size(self, tree):
self.log.debug('\t\tSplitting...')
root = tree.getroot()
# Split large <pre> tags
for pre in list(root.xpath('//pre')):
text = u''.join(pre.xpath('descendant::text()'))
pre.text = text
for child in list(pre.iterchildren()):
pre.remove(child)
if len(pre.text) > self.max_flow_size*0.5:
frags = self.split_text(pre.text, root, int(0.2*self.max_flow_size))
new_pres = []
for frag in frags:
pre2 = copy.copy(pre)
pre2.text = frag
pre2.tail = u''
new_pres.append(pre2)
new_pres[-1].tail = pre.tail
p = pre.getparent()
i = p.index(pre)
p[i:i+1] = new_pres
def find_page_breaks(self, stylesheets, root): split_point, before = self.find_split_point(root)
''' if split_point is None:
Find all elements that have either page-break-before or page-break-after set. raise SplitError(self.item.href, root)
Populates `self.page_breaks` with id based XPath selectors (for elements that don't
have ids, an id is created).
'''
page_break_selectors = set([])
for rule in rules(stylesheets):
before = getattr(rule.style.getPropertyCSSValue('page-break-before'), 'cssText', '').strip().lower()
after = getattr(rule.style.getPropertyCSSValue('page-break-after'), 'cssText', '').strip().lower()
try:
if before and before != 'avoid':
page_break_selectors.add((CSSSelector(rule.selectorText), True))
except:
pass
try:
if after and after != 'avoid':
page_break_selectors.add((CSSSelector(rule.selectorText), False))
except:
pass
page_breaks = set([])
for selector, before in page_break_selectors:
for elem in selector(root):
elem.pb_before = before
page_breaks.add(elem)
for i, elem in enumerate(root.iter()):
elem.pb_order = i
page_breaks = list(page_breaks)
page_breaks.sort(cmp=lambda x,y : cmp(x.pb_order, y.pb_order))
self.page_break_ids = []
for i, x in enumerate(page_breaks):
x.set('id', x.get('id', 'calibre_pb_%d'%i))
id = x.get('id')
self.page_breaks.append((XPath('//*[@id="%s"]'%id), x.pb_before))
self.page_break_ids.append(id)
for t in self.do_split(tree, split_point, before):
r = t.getroot()
if self.is_page_empty(r):
continue
size = len(tostring(r))
if size <= self.max_flow_size:
self.trees.append(t)
#print tostring(t.getroot(), pretty_print=True)
self.log.debug('\t\t\tCommitted sub-tree #%d (%d KB)',
len(self.trees), size/1024.)
self.split_size += size
else:
self.split_to_size(t)
def find_split_point(self, root): def find_split_point(self, root):
''' '''
@ -336,8 +365,7 @@ class Splitter(object):
'//br', '//br',
'//li', '//li',
): ):
elems = root.xpath(path, elems = root.xpath(path, namespaces=NAMESPACES)
namespaces={'re':'http://exslt.org/regular-expressions'})
elem = pick_elem(elems) elem = pick_elem(elems)
if elem is not None: if elem is not None:
try: try:
@ -355,6 +383,8 @@ class Splitter(object):
all anchors in the original tree. Internal links are re-directed. The all anchors in the original tree. Internal links are re-directed. The
original file is deleted and the split files are saved. original file is deleted and the split files are saved.
''' '''
if not self.was_split:
return
self.anchor_map = collections.defaultdict(lambda :self.base%0) self.anchor_map = collections.defaultdict(lambda :self.base%0)
self.files = [] self.files = []
@ -368,134 +398,46 @@ class Splitter(object):
elem.attrib.pop(SPLIT_ATTR, None) elem.attrib.pop(SPLIT_ATTR, None)
elem.attrib.pop(SPLIT_POINT_ATTR, '0') elem.attrib.pop(SPLIT_POINT_ATTR, '0')
for current, tree in zip(self.files, self.trees): spine_pos = self.item.spine_pos
for a in tree.getroot().xpath('//a[@href]'): for current, tree in zip(map(reversed, (self.files, self.trees))):
for a in tree.getroot().xpath('//h:a[@href]', namespaces=NAMESPACES):
href = a.get('href').strip() href = a.get('href').strip()
if href.startswith('#'): if href.startswith('#'):
anchor = href[1:] anchor = href[1:]
file = self.anchor_map[anchor] file = self.anchor_map[anchor]
if file != current: if file != current:
a.set('href', file+href) a.set('href', file+href)
open(content(current), 'wb').\
write(tostring(tree.getroot(), pretty_print=self.opts.pretty_print))
os.remove(content(self.path)) new_id = self.oeb.manifest.generate(id=self.item.id)[0]
new_item = self.oeb.manifest.add(new_id, current,
self.item.media_type, data=tree.getroot())
self.oeb.spine.insert(spine_pos, new_item, self.item.linear)
if self.oeb.guide:
for ref in self.oeb.guide:
href, frag = urldefrag(ref.href)
if href == self.item.href:
nhref = self.anchor_map[frag if frag else None]
if frag:
nhref = '#'.join(nhref, frag)
ref.href = nhref
def fix_toc_entry(toc):
if toc.href:
href, frag = urldefrag(toc.href)
if href == self.item.href:
nhref = self.anchor_map[frag if frag else None]
if frag:
nhref = '#'.join(nhref, frag)
toc.href = nhref
for x in toc:
fix_toc_entry(x)
def fix_opf(self, opf): if self.oeb.toc:
''' fix_toc_entry(self.oeb.toc)
Fix references to the split file in the OPF.
'''
items = [item for item in opf.itermanifest() if item.get('href') == 'content/'+self.path]
new_items = [('content/'+f, None) for f in self.files]
id_map = {}
for item in items:
id_map[item.get('id')] = opf.replace_manifest_item(item, new_items)
for id in id_map.keys(): self.oeb.manifest.remove(self.item)
opf.replace_spine_items_by_idref(id, id_map[id])
for ref in opf.iterguide():
href = ref.get('href', '')
if href.startswith('content/'+self.path):
href = href.split('#')
frag = None
if len(href) > 1:
frag = href[1]
if frag not in self.anchor_map:
self.log_warning('\t\tUnable to re-map OPF link', href)
continue
new_file = self.anchor_map[frag]
ref.set('href', 'content/'+new_file+('' if frag is None else ('#'+frag)))
def fix_content_links(html_files, changes, opts):
split_files = [f.path for f in changes]
anchor_maps = [f.anchor_map for f in changes]
files = list(html_files)
for j, f in enumerate(split_files):
try:
i = files.index(f)
files[i:i+1] = changes[j].files
except ValueError:
continue
for htmlfile in files:
changed = False
root = html.fromstring(open(content(htmlfile), 'rb').read())
for a in root.xpath('//a[@href]'):
href = a.get('href')
if not href.startswith('#'):
href = href.split('#')
anchor = href[1] if len(href) > 1 else None
href = href[0]
if href in split_files:
try:
newf = anchor_maps[split_files.index(href)][anchor]
except:
print '\t\tUnable to remap HTML link:', href, anchor
continue
frag = ('#'+anchor) if anchor else ''
a.set('href', newf+frag)
changed = True
if changed:
open(content(htmlfile), 'wb').write(tostring(root, pretty_print=opts.pretty_print))
def fix_ncx(path, changes):
split_files = [f.path for f in changes]
anchor_maps = [f.anchor_map for f in changes]
tree = etree.parse(path)
changed = False
for content in tree.getroot().xpath('//x:content[@src]',
namespaces={'x':"http://www.daisy.org/z3986/2005/ncx/"}):
href = content.get('src')
if not href.startswith('#'):
href = href.split('#')
anchor = href[1] if len(href) > 1 else None
href = href[0].split('/')[-1]
if href in split_files:
try:
newf = anchor_maps[split_files.index(href)][anchor]
except:
print 'Unable to remap NCX link:', href, anchor
frag = ('#'+anchor) if anchor else ''
content.set('src', 'content/'+newf+frag)
changed = True
if changed:
open(path, 'wb').write(etree.tostring(tree.getroot(), encoding='UTF-8', xml_declaration=True))
def find_html_files(opf):
'''
Find all HTML files referenced by `opf`.
'''
html_files = []
for item in opf.itermanifest():
if 'html' in item.get('media-type', '').lower():
f = item.get('href').split('/')[-1]
f2 = f.replace('&', '%26')
if not os.path.exists(content(f)) and os.path.exists(content(f2)):
f = f2
item.set('href', item.get('href').replace('&', '%26'))
if os.path.exists(content(f)):
html_files.append(f)
return html_files
def split(pathtoopf, opts, stylesheet_map):
pathtoopf = os.path.abspath(pathtoopf)
opf = OPF(open(pathtoopf, 'rb'), os.path.dirname(pathtoopf))
with CurrentDir(os.path.dirname(pathtoopf)):
html_files = find_html_files(opf)
changes = [Splitter(f, opts, stylesheet_map, opf) for f in html_files]
changes = [c for c in changes if c.was_split]
fix_content_links(html_files, changes, opts)
for item in opf.itermanifest():
if item.get('media-type', '') == 'application/x-dtbncx+xml':
fix_ncx(item.get('href'), changes)
break
open(pathtoopf, 'wb').write(opf.render())

View File

@ -67,6 +67,10 @@ def _config():
c.add_opt('default_send_to_device_action', default=None, c.add_opt('default_send_to_device_action', default=None,
help=_('Default action to perform when send to device button is ' help=_('Default action to perform when send to device button is '
'clicked')) 'clicked'))
c.add_opt('show_donate_button', default=True,
help='Show donation button')
c.add_opt('asked_library_thing_password', default=False,
help='Asked library thing password at least once.')
return ConfigProxy(c) return ConfigProxy(c)
config = _config() config = _config()

View File

@ -12,6 +12,7 @@ from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \
from calibre.customize.ui import available_input_formats, available_output_formats from calibre.customize.ui import available_input_formats, available_output_formats
from calibre.devices import devices from calibre.devices import devices
from calibre.constants import iswindows
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.parallel import Job from calibre.parallel import Job
from calibre.devices.scanner import DeviceScanner from calibre.devices.scanner import DeviceScanner
@ -71,7 +72,14 @@ class DeviceManager(Thread):
if connected and not device[1]: if connected and not device[1]:
try: try:
dev = device[0]() dev = device[0]()
dev.open() if iswindows:
import pythoncom
pythoncom.CoInitialize()
try:
dev.open()
finally:
if iswindows:
pythoncom.CoUninitialize()
self.device = dev self.device = dev
self.device_class = dev.__class__ self.device_class = dev.__class__
self.connected_slot(True) self.connected_slot(True)
@ -670,7 +678,9 @@ class DeviceGUI(object):
bad = '\n'.join('<li>%s</li>'%(i,) for i in bad) bad = '\n'.join('<li>%s</li>'%(i,) for i in bad)
d = warning_dialog(self, _('No suitable formats'), d = warning_dialog(self, _('No suitable formats'),
_('Could not upload the following books to the device, ' _('Could not upload the following books to the device, '
'as no suitable formats were found:<br><ul>%s</ul>')%(bad,)) 'as no suitable formats were found. Try changing the output '
'format in the upper right corner next to the red heart and '
're-converting. <br><ul>%s</ul>')%(bad,))
d.exec_() d.exec_()
def upload_booklists(self): def upload_booklists(self):

View File

@ -176,19 +176,19 @@ class Config(ResizableDialog, Ui_Dialog):
def get_metadata(self): def get_metadata(self):
title, authors = self.get_title_and_authors() title, authors = self.get_title_and_authors()
mi = MetaInformation(title, authors) mi = MetaInformation(title, authors)
publisher = unicode(self.publisher.text()) publisher = unicode(self.publisher.text()).strip()
if publisher: if publisher:
mi.publisher = publisher mi.publisher = publisher
author_sort = unicode(self.author_sort.text()) author_sort = unicode(self.author_sort.text()).strip()
if author_sort: if author_sort:
mi.author_sort = author_sort mi.author_sort = author_sort
comments = unicode(self.comment.toPlainText()) comments = unicode(self.comment.toPlainText()).strip()
if comments: if comments:
mi.comments = comments mi.comments = comments
mi.series_index = int(self.series_index.value()) mi.series_index = int(self.series_index.value())
if self.series.currentIndex() > -1: if self.series.currentIndex() > -1:
mi.series = unicode(self.series.currentText()) mi.series = unicode(self.series.currentText()).strip()
tags = [t.strip() for t in unicode(self.tags.text()).split(',')] tags = [t.strip() for t in unicode(self.tags.text()).strip().split(',')]
if tags: if tags:
mi.tags = tags mi.tags = tags
@ -267,6 +267,7 @@ class Config(ResizableDialog, Ui_Dialog):
).exec_() ).exec_()
return return
mi = self.get_metadata() mi = self.get_metadata()
self.user_mi = mi
self.read_settings() self.read_settings()
self.cover_file = None self.cover_file = None
if self.row is not None: if self.row is not None:

View File

@ -25,24 +25,48 @@ from calibre import islinux
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.customize.ui import run_plugins_on_import from calibre.customize.ui import run_plugins_on_import
from calibre.gui2 import config as gui_conf
class CoverFetcher(QThread): class CoverFetcher(QThread):
def __init__(self, username, password, isbn, timeout): def __init__(self, username, password, isbn, timeout, title, author):
self.username = username self.username = username.strip() if username else username
self.password = password self.password = password.strip() if password else password
self.timeout = timeout self.timeout = timeout
self.isbn = isbn self.isbn = isbn
self.title = title
self.needs_isbn = False
self.author = author
QThread.__init__(self) QThread.__init__(self)
self.exception = self.traceback = self.cover_data = None self.exception = self.traceback = self.cover_data = None
def run(self): def run(self):
try: try:
login(self.username, self.password, force=False) if not self.isbn:
from calibre.ebooks.metadata.fetch import search
if not self.title:
self.needs_isbn = True
return
au = self.author if self.author else None
key = prefs['isbndb_com_key']
if not key:
key = None
results = search(title=self.title, author=au,
isbndb_key=key)[0]
results = sorted([x.isbn for x in results if x.isbn],
cmp=lambda x,y:cmp(len(x),len(y)), reverse=True)
if not results:
self.needs_isbn = True
return
self.isbn = results[0]
if self.username and self.password:
login(self.username, self.password, force=False)
self.cover_data = cover_from_isbn(self.isbn, timeout=self.timeout)[0] self.cover_data = cover_from_isbn(self.isbn, timeout=self.timeout)[0]
except Exception, e: except Exception, e:
self.exception = e self.exception = e
self.traceback = traceback.format_exc() self.traceback = traceback.format_exc()
print self.traceback
@ -64,6 +88,8 @@ class AuthorCompleter(QCompleter):
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
COVER_FETCH_TIMEOUT = 240 # seconds
def do_reset_cover(self, *args): def do_reset_cover(self, *args):
pix = QPixmap(':/images/book.svg') pix = QPixmap(':/images/book.svg')
self.cover.setPixmap(pix) self.cover.setPixmap(pix)
@ -345,36 +371,39 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def lt_password_dialog(self): def lt_password_dialog(self):
return PasswordDialog(self, 'LibraryThing account', return PasswordDialog(self, 'LibraryThing account',
_('<p>Enter your username and password for <b>LibraryThing.com</b>. <br/>If you do not have one, you can <a href=\'http://www.librarything.com\'>register</a> for free!.</p>')) _('<p>Enter your username and password for '
'<b>LibraryThing.com</b>. This is <b>optional</b>. It will '
'make fetching of covers faster and more reliable.<br/>If '
'you do not have an account, you can '
'<a href=\'http://www.librarything.com\'>register</a> for '
'free.</p>'))
def change_password(self): def change_password(self):
d = self.lt_password_dialog() d = self.lt_password_dialog()
d.exec_() d.exec_()
def fetch_cover(self): def fetch_cover(self):
isbn = qstring_to_unicode(self.isbn.text()) isbn = unicode(self.isbn.text()).strip()
if isbn: d = self.lt_password_dialog()
d = self.lt_password_dialog() if not gui_conf['asked_library_thing_password'] and \
if not d.username() or not d.password(): (not d.username() or not d.password()):
d.exec_() d.exec_()
if d.result() != PasswordDialog.Accepted: gui_conf['asked_library_thing_password'] = True
return self.fetch_cover_button.setEnabled(False)
self.fetch_cover_button.setEnabled(False) self.setCursor(Qt.WaitCursor)
self.setCursor(Qt.WaitCursor) title, author = map(unicode, (self.title.text(), self.authors.text()))
self.cover_fetcher = CoverFetcher(d.username(), d.password(), isbn, self.cover_fetcher = CoverFetcher(d.username(), d.password(), isbn,
self.timeout) self.timeout, title, author)
self.cover_fetcher.start() self.cover_fetcher.start()
self._hangcheck = QTimer(self) self._hangcheck = QTimer(self)
self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck) self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck)
self.cf_start_time = time.time() self.cf_start_time = time.time()
self.pi.start(_('Downloading cover...')) self.pi.start(_('Downloading cover...'))
self._hangcheck.start(100) self._hangcheck.start(100)
else:
error_dialog(self, _('Cannot fetch cover'),
_('You must specify the ISBN identifier for this book.')).exec_()
def hangcheck(self): def hangcheck(self):
if not (self.cover_fetcher.isFinished() or time.time()-self.cf_start_time > 150): if not self.cover_fetcher.isFinished() and \
time.time()-self.cf_start_time < self.COVER_FETCH_TIMEOUT:
return return
self._hangcheck.stop() self._hangcheck.stop()
@ -385,6 +414,11 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
_('<b>Could not fetch cover.</b><br/>')+ _('<b>Could not fetch cover.</b><br/>')+
_('The download timed out.')).exec_() _('The download timed out.')).exec_()
return return
if self.cover_fetcher.needs_isbn:
error_dialog(self, _('Cannot fetch cover'),
_('Could not find cover for this book. Try '
'specifying the ISBN first.')).exec_()
return
if self.cover_fetcher.exception is not None: if self.cover_fetcher.exception is not None:
err = self.cover_fetcher.exception err = self.cover_fetcher.exception
error_dialog(self, _('Cannot fetch cover'), error_dialog(self, _('Cannot fetch cover'),

View File

@ -1,7 +1,8 @@
<ui version="4.0" > <?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class> <class>Dialog</class>
<widget class="QDialog" name="Dialog" > <widget class="QDialog" name="Dialog">
<property name="geometry" > <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
@ -9,66 +10,70 @@
<height>209</height> <height>209</height>
</rect> </rect>
</property> </property>
<property name="windowTitle" > <property name="windowTitle">
<string>Password needed</string> <string>Password needed</string>
</property> </property>
<property name="windowIcon" > <property name="windowIcon">
<iconset resource="../images.qrc" >:/images/mimetypes/unknown.svg</iconset> <iconset resource="../images.qrc">
<normaloff>:/images/mimetypes/unknown.svg</normaloff>:/images/mimetypes/unknown.svg</iconset>
</property> </property>
<layout class="QGridLayout" > <layout class="QGridLayout">
<item row="0" column="1" > <item row="0" column="1">
<widget class="QLabel" name="msg" > <widget class="QLabel" name="msg">
<property name="text" > <property name="text">
<string>TextLabel</string> <string>TextLabel</string>
</property> </property>
<property name="openExternalLinks" > <property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0" > <item row="1" column="0">
<widget class="QLabel" name="label" > <widget class="QLabel" name="label">
<property name="text" > <property name="text">
<string>&amp;Username:</string> <string>&amp;Username:</string>
</property> </property>
<property name="buddy" > <property name="buddy">
<cstring>gui_username</cstring> <cstring>gui_username</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1" > <item row="1" column="1">
<widget class="QLineEdit" name="gui_username" /> <widget class="QLineEdit" name="gui_username"/>
</item> </item>
<item row="2" column="0" > <item row="2" column="0">
<widget class="QLabel" name="label_2" > <widget class="QLabel" name="label_2">
<property name="text" > <property name="text">
<string>&amp;Password:</string> <string>&amp;Password:</string>
</property> </property>
<property name="buddy" > <property name="buddy">
<cstring>gui_password</cstring> <cstring>gui_password</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1" > <item row="2" column="1">
<widget class="QLineEdit" name="gui_password" > <widget class="QLineEdit" name="gui_password">
<property name="echoMode" > <property name="echoMode">
<enum>QLineEdit::Password</enum> <enum>QLineEdit::Password</enum>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1" > <item row="4" column="1">
<widget class="QDialogButtonBox" name="buttonBox" > <widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation" > <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
<property name="standardButtons" > <property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok</set> <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1" > <item row="3" column="1">
<widget class="QCheckBox" name="show_password" > <widget class="QCheckBox" name="show_password">
<property name="text" > <property name="text">
<string>&amp;Show password</string> <string>&amp;Show password</string>
</property> </property>
</widget> </widget>
@ -76,7 +81,7 @@
</layout> </layout>
</widget> </widget>
<resources> <resources>
<include location="../images.qrc" /> <include location="../images.qrc"/>
</resources> </resources>
<connections> <connections>
<connection> <connection>
@ -85,11 +90,11 @@
<receiver>Dialog</receiver> <receiver>Dialog</receiver>
<slot>accept()</slot> <slot>accept()</slot>
<hints> <hints>
<hint type="sourcelabel" > <hint type="sourcelabel">
<x>248</x> <x>248</x>
<y>254</y> <y>254</y>
</hint> </hint>
<hint type="destinationlabel" > <hint type="destinationlabel">
<x>157</x> <x>157</x>
<y>274</y> <y>274</y>
</hint> </hint>
@ -101,11 +106,11 @@
<receiver>Dialog</receiver> <receiver>Dialog</receiver>
<slot>reject()</slot> <slot>reject()</slot>
<hints> <hints>
<hint type="sourcelabel" > <hint type="sourcelabel">
<x>316</x> <x>316</x>
<y>260</y> <y>260</y>
</hint> </hint>
<hint type="destinationlabel" > <hint type="destinationlabel">
<x>286</x> <x>286</x>
<y>274</y> <y>274</y>
</hint> </hint>

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

View File

@ -108,6 +108,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.donate_action = self.system_tray_menu.addAction( self.donate_action = self.system_tray_menu.addAction(
QIcon(':/images/donate.svg'), _('&Donate to support calibre')) QIcon(':/images/donate.svg'), _('&Donate to support calibre'))
self.donate_button.setDefaultAction(self.donate_action) self.donate_button.setDefaultAction(self.donate_action)
if not config['show_donate_button']:
self.donate_button.setVisible(False)
self.addAction(self.quit_action) self.addAction(self.quit_action)
self.action_restart = QAction(_('&Restart'), self) self.action_restart = QAction(_('&Restart'), self)
self.addAction(self.action_restart) self.addAction(self.action_restart)

View File

@ -25,7 +25,7 @@ from calibre.ebooks.lrf.comic.convert_from import config as comicconfig
# Ordered list of source formats. Items closer to the beginning are # Ordered list of source formats. Items closer to the beginning are
# preferred for conversion over those toward the end. # preferred for conversion over those toward the end.
PREFERRED_SOURCE_FORMATS = ['epub', 'lit', 'mobi', 'prc', 'azw', 'fb2', 'odt', 'rtf', PREFERRED_SOURCE_FORMATS = ['epub', 'lit', 'mobi', 'prc', 'azw', 'fb2', 'odt', 'rtf',
'txt', 'pdf', 'oebzip', 'htm', 'html'] 'txt', 'pdf', 'oebzip', 'htm', 'html']
def get_dialog(fmt): def get_dialog(fmt):
@ -43,20 +43,20 @@ def get_config(fmt):
def auto_convert(fmt, parent, db, rows): def auto_convert(fmt, parent, db, rows):
changed = False changed = False
jobs = [] jobs = []
total = len(rows) total = len(rows)
if total == 0: if total == 0:
return None, None, None return None, None, None
parent.status_bar.showMessage(_('Starting auto conversion of %d books')%total, 2000) parent.status_bar.showMessage(_('Starting auto conversion of %d books')%total, 2000)
i = 0 i = 0
bad_rows = [] bad_rows = []
for i, row in enumerate(rows): for i, row in enumerate(rows):
row_id = db.id(row) row_id = db.id(row)
temp_files = [] temp_files = []
data = None data = None
in_formats = [f.lower() for f in db.formats(row).split(',')] in_formats = [f.lower() for f in db.formats(row).split(',')]
in_formats = list(set(in_formats).intersection(available_input_formats())) in_formats = list(set(in_formats).intersection(available_input_formats()))
@ -88,10 +88,10 @@ def auto_convert(fmt, parent, db, rows):
for row in bad_rows: for row in bad_rows:
title = db.title(row) title = db.title(row)
res.append('<li>%s</li>'%title) res.append('<li>%s</li>'%title)
msg = _('<p>Could not convert %d of %d books, because no suitable source format was found.<ul>%s</ul>')%(len(res), total, '\n'.join(res)) msg = _('<p>Could not convert %d of %d books, because no suitable source format was found.<ul>%s</ul>')%(len(res), total, '\n'.join(res))
warning_dialog(parent, _('Could not convert some books'), msg).exec_() warning_dialog(parent, _('Could not convert some books'), msg).exec_()
return jobs, changed, bad_rows return jobs, changed, bad_rows
def convert_single(fmt, parent, db, comics, others): def convert_single(fmt, parent, db, comics, others):
@ -120,10 +120,10 @@ def convert_single(fmt, parent, db, comics, others):
temp_files.append(d.cover_file) temp_files.append(d.cover_file)
opts.cover = d.cover_file.name opts.cover = d.cover_file.name
temp_files.extend([d.opf_file, pt, of]) temp_files.extend([d.opf_file, pt, of])
jobs.append(('any2'+fmt, args, _('Convert book: ')+d.mi.title, jobs.append(('any2'+fmt, args, _('Convert book: ')+d.mi.title,
fmt.upper(), row_id, temp_files)) fmt.upper(), row_id, temp_files))
changed = True changed = True
for row, row_id in zip(comics, comics_ids): for row, row_id in zip(comics, comics_ids):
mi = db.get_metadata(row) mi = db.get_metadata(row)
title = author = _('Unknown') title = author = _('Unknown')
@ -140,7 +140,7 @@ def convert_single(fmt, parent, db, comics, others):
try: try:
data = db.format(row, _fmt.upper()) data = db.format(row, _fmt.upper())
if data is not None: if data is not None:
break break
except: except:
continue continue
pt = PersistentTemporaryFile('.'+_fmt) pt = PersistentTemporaryFile('.'+_fmt)
@ -152,12 +152,12 @@ def convert_single(fmt, parent, db, comics, others):
opts.verbose = 2 opts.verbose = 2
args = [pt.name, opts] args = [pt.name, opts]
changed = True changed = True
jobs.append(('comic2'+fmt, args, _('Convert comic: ')+opts.title, jobs.append(('comic2'+fmt, args, _('Convert comic: ')+opts.title,
fmt.upper(), row_id, [pt, of])) fmt.upper(), row_id, [pt, of]))
return jobs, changed return jobs, changed
def convert_single_lrf(parent, db, comics, others): def convert_single_lrf(parent, db, comics, others):
changed = False changed = False
@ -182,10 +182,10 @@ def convert_single_lrf(parent, db, comics, others):
if d.cover_file: if d.cover_file:
temp_files.append(d.cover_file) temp_files.append(d.cover_file)
temp_files.extend([pt, of]) temp_files.extend([pt, of])
jobs.append(('any2lrf', [cmdline], _('Convert book: ')+d.title(), jobs.append(('any2lrf', [cmdline], _('Convert book: ')+d.title(),
'LRF', row_id, temp_files)) 'LRF', row_id, temp_files))
changed = True changed = True
for row, row_id in zip(comics, comics_ids): for row, row_id in zip(comics, comics_ids):
mi = db.get_metadata(row) mi = db.get_metadata(row)
title = author = _('Unknown') title = author = _('Unknown')
@ -202,7 +202,7 @@ def convert_single_lrf(parent, db, comics, others):
try: try:
data = db.format(row, fmt.upper()) data = db.format(row, fmt.upper())
if data is not None: if data is not None:
break break
except: except:
continue continue
if data is None: if data is None:
@ -216,19 +216,20 @@ def convert_single_lrf(parent, db, comics, others):
opts.verbose = 1 opts.verbose = 1
args = [pt.name, opts] args = [pt.name, opts]
changed = True changed = True
jobs.append(('comic2lrf', args, _('Convert comic: ')+opts.title, jobs.append(('comic2lrf', args, _('Convert comic: ')+opts.title,
'LRF', row_id, [pt, of])) 'LRF', row_id, [pt, of]))
return jobs, changed return jobs, changed
def convert_bulk(fmt, parent, db, comics, others): def convert_bulk(fmt, parent, db, comics, others):
if others: if others:
d = get_dialog(fmt)(parent, db) d = get_dialog(fmt)(parent, db)
if d.exec_() != QDialog.Accepted: if d.exec_() != QDialog.Accepted:
others = [] others, user_mi = [], None
else: else:
opts = d.opts opts = d.opts
opts.verbose = 2 opts.verbose = 2
user_mi = d.user_mi
if comics: if comics:
comic_opts = ComicConf.get_bulk_conversion_options(parent) comic_opts = ComicConf.get_bulk_conversion_options(parent)
if not comic_opts: if not comic_opts:
@ -239,7 +240,7 @@ def convert_bulk(fmt, parent, db, comics, others):
if total == 0: if total == 0:
return return
parent.status_bar.showMessage(_('Starting Bulk conversion of %d books')%total, 2000) parent.status_bar.showMessage(_('Starting Bulk conversion of %d books')%total, 2000)
for i, row in enumerate(others+comics): for i, row in enumerate(others+comics):
row_id = db.id(row) row_id = db.id(row)
if row in others: if row in others:
@ -256,6 +257,11 @@ def convert_bulk(fmt, parent, db, comics, others):
continue continue
options = opts.copy() options = opts.copy()
mi = db.get_metadata(row) mi = db.get_metadata(row)
if user_mi is not None:
if user_mi.series_index == 1:
user_mi.series_index = None
mi.smart_update(user_mi)
db.set_metadata(db.id(row), mi)
opf = OPFCreator(os.getcwdu(), mi) opf = OPFCreator(os.getcwdu(), mi)
opf_file = PersistentTemporaryFile('.opf') opf_file = PersistentTemporaryFile('.opf')
opf.render(opf_file) opf.render(opf_file)
@ -291,10 +297,10 @@ def convert_bulk(fmt, parent, db, comics, others):
try: try:
data = db.format(row, _fmt.upper()) data = db.format(row, _fmt.upper())
if data is not None: if data is not None:
break break
except: except:
continue continue
pt = PersistentTemporaryFile('.'+_fmt.lower()) pt = PersistentTemporaryFile('.'+_fmt.lower())
pt.write(data) pt.write(data)
pt.close() pt.close()
@ -304,17 +310,17 @@ def convert_bulk(fmt, parent, db, comics, others):
options.verbose = 1 options.verbose = 1
args = [pt.name, options] args = [pt.name, options]
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title)) desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
jobs.append(('comic2'+fmt, args, desc, fmt.upper(), row_id, [pt, of])) jobs.append(('comic2'+fmt, args, desc, fmt.upper(), row_id, [pt, of]))
if bad_rows: if bad_rows:
res = [] res = []
for row in bad_rows: for row in bad_rows:
title = db.title(row) title = db.title(row)
res.append('<li>%s</li>'%title) res.append('<li>%s</li>'%title)
msg = _('<p>Could not convert %d of %d books, because no suitable source format was found.<ul>%s</ul>')%(len(res), total, '\n'.join(res)) msg = _('<p>Could not convert %d of %d books, because no suitable source format was found.<ul>%s</ul>')%(len(res), total, '\n'.join(res))
warning_dialog(parent, _('Could not convert some books'), msg).exec_() warning_dialog(parent, _('Could not convert some books'), msg).exec_()
return jobs, False return jobs, False
@ -333,7 +339,7 @@ def convert_bulk_lrf(parent, db, comics, others):
if total == 0: if total == 0:
return return
parent.status_bar.showMessage(_('Starting Bulk conversion of %d books')%total, 2000) parent.status_bar.showMessage(_('Starting Bulk conversion of %d books')%total, 2000)
for i, row in enumerate(others+comics): for i, row in enumerate(others+comics):
row_id = db.id(row) row_id = db.id(row)
if row in others: if row in others:
@ -388,10 +394,10 @@ def convert_bulk_lrf(parent, db, comics, others):
try: try:
data = db.format(row, fmt.upper()) data = db.format(row, fmt.upper())
if data is not None: if data is not None:
break break
except: except:
continue continue
pt = PersistentTemporaryFile('.'+fmt.lower()) pt = PersistentTemporaryFile('.'+fmt.lower())
pt.write(data) pt.write(data)
pt.close() pt.close()
@ -401,17 +407,17 @@ def convert_bulk_lrf(parent, db, comics, others):
options.verbose = 1 options.verbose = 1
args = [pt.name, options] args = [pt.name, options]
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title)) desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
jobs.append(('comic2lrf', args, desc, 'LRF', row_id, [pt, of])) jobs.append(('comic2lrf', args, desc, 'LRF', row_id, [pt, of]))
if bad_rows: if bad_rows:
res = [] res = []
for row in bad_rows: for row in bad_rows:
title = db.title(row) title = db.title(row)
res.append('<li>%s</li>'%title) res.append('<li>%s</li>'%title)
msg = _('<p>Could not convert %d of %d books, because no suitable source format was found.<ul>%s</ul>')%(len(res), total, '\n'.join(res)) msg = _('<p>Could not convert %d of %d books, because no suitable source format was found.<ul>%s</ul>')%(len(res), total, '\n'.join(res))
warning_dialog(parent, _('Could not convert some books'), msg).exec_() warning_dialog(parent, _('Could not convert some books'), msg).exec_()
return jobs, False return jobs, False
def set_conversion_defaults_lrf(comic, parent, db): def set_conversion_defaults_lrf(comic, parent, db):
@ -438,7 +444,7 @@ def _fetch_news(data, fmt):
args.extend(['--password', data['password']]) args.extend(['--password', data['password']])
args.append(data['script'] if data['script'] else data['title']) args.append(data['script'] if data['script'] else data['title'])
return 'feeds2'+fmt.lower(), [args], _('Fetch news from ')+data['title'], fmt.upper(), [pt] return 'feeds2'+fmt.lower(), [args], _('Fetch news from ')+data['title'], fmt.upper(), [pt]
def fetch_scheduled_recipe(recipe, script): def fetch_scheduled_recipe(recipe, script):
from calibre.gui2.dialogs.scheduler import config from calibre.gui2.dialogs.scheduler import config
@ -453,7 +459,7 @@ def fetch_scheduled_recipe(recipe, script):
args.extend(['--username', x[0], '--password', x[1]]) args.extend(['--username', x[0], '--password', x[1]])
args.append(script) args.append(script)
return 'feeds2'+fmt, [args], _('Fetch news from ')+recipe.title, fmt.upper(), [pt] return 'feeds2'+fmt, [args], _('Fetch news from ')+recipe.title, fmt.upper(), [pt]
def auto_convert_ebook(*args): def auto_convert_ebook(*args):
return auto_convert(*args) return auto_convert(*args)
@ -463,14 +469,14 @@ def convert_single_ebook(*args):
return convert_single_lrf(*args) return convert_single_lrf(*args)
elif fmt in ('epub', 'mobi'): elif fmt in ('epub', 'mobi'):
return convert_single(fmt, *args) return convert_single(fmt, *args)
def convert_bulk_ebooks(*args): def convert_bulk_ebooks(*args):
fmt = prefs['output_format'].lower() fmt = prefs['output_format'].lower()
if fmt == 'lrf': if fmt == 'lrf':
return convert_bulk_lrf(*args) return convert_bulk_lrf(*args)
elif fmt in ('epub', 'mobi'): elif fmt in ('epub', 'mobi'):
return convert_bulk(fmt, *args) return convert_bulk(fmt, *args)
def set_conversion_defaults(comic, parent, db): def set_conversion_defaults(comic, parent, db):
fmt = prefs['output_format'].lower() fmt = prefs['output_format'].lower()
if fmt == 'lrf': if fmt == 'lrf':

View File

@ -7,19 +7,19 @@ from calibre.utils.config import Config, StringConfig
def server_config(defaults=None): def server_config(defaults=None):
desc=_('Settings to control the calibre content server') desc=_('Settings to control the calibre content server')
c = Config('server', desc) if defaults is None else StringConfig(defaults, desc) c = Config('server', desc) if defaults is None else StringConfig(defaults, desc)
c.add_opt('port', ['-p', '--port'], default=8080, c.add_opt('port', ['-p', '--port'], default=8080,
help=_('The port on which to listen. Default is %default')) help=_('The port on which to listen. Default is %default'))
c.add_opt('timeout', ['-t', '--timeout'], default=120, c.add_opt('timeout', ['-t', '--timeout'], default=120,
help=_('The server timeout in seconds. Default is %default')) help=_('The server timeout in seconds. Default is %default'))
c.add_opt('thread_pool', ['--thread-pool'], default=30, c.add_opt('thread_pool', ['--thread-pool'], default=30,
help=_('The max number of worker threads to use. Default is %default')) help=_('The max number of worker threads to use. Default is %default'))
c.add_opt('password', ['--password'], default=None, c.add_opt('password', ['--password'], default=None,
help=_('Set a password to restrict access. By default access is unrestricted.')) help=_('Set a password to restrict access. By default access is unrestricted.'))
c.add_opt('username', ['--username'], default='calibre', c.add_opt('username', ['--username'], default='calibre',
help=_('Username for access. By default, it is: %default')) help=_('Username for access. By default, it is: %default'))
c.add_opt('develop', ['--develop'], default=False, c.add_opt('develop', ['--develop'], default=False,
help='Development mode. Server automatically restarts on file changes and serves code files (html, css, js) from the file system instead of calibre\'s resource system.') help='Development mode. Server automatically restarts on file changes and serves code files (html, css, js) from the file system instead of calibre\'s resource system.')
c.add_opt('max_cover', ['--max-cover'], default='600x800', c.add_opt('max_cover', ['--max-cover'], default='600x800',
help=_('The maximum size for displayed covers. Default is %default.')) help=_('The maximum size for displayed covers. Default is %default.'))
return c return c

View File

@ -30,31 +30,31 @@ build_time = datetime.strptime(build_time, '%d %m %Y %H%M%S')
server_resources['jquery.js'] = jquery server_resources['jquery.js'] = jquery
def expose(func): def expose(func):
def do(self, *args, **kwargs): def do(self, *args, **kwargs):
dict.update(cherrypy.response.headers, {'Server':self.server_name}) dict.update(cherrypy.response.headers, {'Server':self.server_name})
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
return cherrypy.expose(do) return cherrypy.expose(do)
log_access_file = os.path.join(config_dir, 'server_access_log.txt') log_access_file = os.path.join(config_dir, 'server_access_log.txt')
log_error_file = os.path.join(config_dir, 'server_error_log.txt') log_error_file = os.path.join(config_dir, 'server_error_log.txt')
class LibraryServer(object): class LibraryServer(object):
server_name = __appname__ + '/' + __version__ server_name = __appname__ + '/' + __version__
BOOK = textwrap.dedent('''\ BOOK = textwrap.dedent('''\
<book xmlns:py="http://genshi.edgewall.org/" <book xmlns:py="http://genshi.edgewall.org/"
id="${r[0]}" id="${r[0]}"
title="${r[1]}" title="${r[1]}"
sort="${r[11]}" sort="${r[11]}"
author_sort="${r[12]}" author_sort="${r[12]}"
authors="${authors}" authors="${authors}"
rating="${r[4]}" rating="${r[4]}"
timestamp="${r[5].strftime('%Y/%m/%d %H:%M:%S')}" timestamp="${r[5].strftime('%Y/%m/%d %H:%M:%S')}"
size="${r[6]}" size="${r[6]}"
isbn="${r[14] if r[14] else ''}" isbn="${r[14] if r[14] else ''}"
formats="${r[13] if r[13] else ''}" formats="${r[13] if r[13] else ''}"
series = "${r[9] if r[9] else ''}" series = "${r[9] if r[9] else ''}"
@ -63,7 +63,7 @@ class LibraryServer(object):
publisher="${r[3] if r[3] else ''}">${r[8] if r[8] else ''} publisher="${r[3] if r[3] else ''}">${r[8] if r[8] else ''}
</book> </book>
''') ''')
LIBRARY = MarkupTemplate(textwrap.dedent('''\ LIBRARY = MarkupTemplate(textwrap.dedent('''\
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<library xmlns:py="http://genshi.edgewall.org/" start="$start" num="${len(books)}" total="$total" updated="${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}"> <library xmlns:py="http://genshi.edgewall.org/" start="$start" num="${len(books)}" total="$total" updated="${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}">
@ -72,7 +72,7 @@ class LibraryServer(object):
</py:for> </py:for>
</library> </library>
''')) '''))
STANZA_ENTRY=MarkupTemplate(textwrap.dedent('''\ STANZA_ENTRY=MarkupTemplate(textwrap.dedent('''\
<entry xmlns:py="http://genshi.edgewall.org/"> <entry xmlns:py="http://genshi.edgewall.org/">
<title>${record[FM['title']]}</title> <title>${record[FM['title']]}</title>
@ -87,7 +87,7 @@ class LibraryServer(object):
</content> </content>
</entry> </entry>
''')) '''))
STANZA = MarkupTemplate(textwrap.dedent('''\ STANZA = MarkupTemplate(textwrap.dedent('''\
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:py="http://genshi.edgewall.org/"> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:py="http://genshi.edgewall.org/">
@ -107,7 +107,7 @@ class LibraryServer(object):
</feed> </feed>
''')) '''))
def __init__(self, db, opts, embedded=False, show_tracebacks=True): def __init__(self, db, opts, embedded=False, show_tracebacks=True):
self.db = db self.db = db
for item in self.db: for item in self.db:
@ -116,7 +116,7 @@ class LibraryServer(object):
self.opts = opts self.opts = opts
self.max_cover_width, self.max_cover_height = \ self.max_cover_width, self.max_cover_height = \
map(int, self.opts.max_cover.split('x')) map(int, self.opts.max_cover.split('x'))
cherrypy.config.update({ cherrypy.config.update({
'log.screen' : opts.develop, 'log.screen' : opts.develop,
'engine.autoreload_on' : opts.develop, 'engine.autoreload_on' : opts.develop,
@ -141,10 +141,10 @@ class LibraryServer(object):
'tools.digest_auth.realm' : (_('Password to access your calibre library. Username is ') + opts.username.strip()).encode('ascii', 'replace'), 'tools.digest_auth.realm' : (_('Password to access your calibre library. Username is ') + opts.username.strip()).encode('ascii', 'replace'),
'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()}, 'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()},
} }
self.is_running = False self.is_running = False
self.exception = None self.exception = None
def setup_loggers(self): def setup_loggers(self):
access_file = log_access_file access_file = log_access_file
error_file = log_error_file error_file = log_error_file
@ -152,20 +152,20 @@ class LibraryServer(object):
maxBytes = getattr(log, "rot_maxBytes", 10000000) maxBytes = getattr(log, "rot_maxBytes", 10000000)
backupCount = getattr(log, "rot_backupCount", 1000) backupCount = getattr(log, "rot_backupCount", 1000)
# Make a new RotatingFileHandler for the error log. # Make a new RotatingFileHandler for the error log.
h = RotatingFileHandler(error_file, 'a', maxBytes, backupCount) h = RotatingFileHandler(error_file, 'a', maxBytes, backupCount)
h.setLevel(logging.DEBUG) h.setLevel(logging.DEBUG)
h.setFormatter(cherrypy._cplogging.logfmt) h.setFormatter(cherrypy._cplogging.logfmt)
log.error_log.addHandler(h) log.error_log.addHandler(h)
# Make a new RotatingFileHandler for the access log. # Make a new RotatingFileHandler for the access log.
h = RotatingFileHandler(access_file, 'a', maxBytes, backupCount) h = RotatingFileHandler(access_file, 'a', maxBytes, backupCount)
h.setLevel(logging.DEBUG) h.setLevel(logging.DEBUG)
h.setFormatter(cherrypy._cplogging.logfmt) h.setFormatter(cherrypy._cplogging.logfmt)
log.access_log.addHandler(h) log.access_log.addHandler(h)
def start(self): def start(self):
self.is_running = False self.is_running = False
self.setup_loggers() self.setup_loggers()
@ -173,7 +173,7 @@ class LibraryServer(object):
try: try:
cherrypy.engine.start() cherrypy.engine.start()
self.is_running = True self.is_running = True
publish_zeroconf('Books in calibre', '_stanza._tcp', publish_zeroconf('Books in calibre', '_stanza._tcp',
self.opts.port, {'path':'/stanza'}) self.opts.port, {'path':'/stanza'})
cherrypy.engine.block() cherrypy.engine.block()
except Exception, e: except Exception, e:
@ -181,10 +181,10 @@ class LibraryServer(object):
finally: finally:
self.is_running = False self.is_running = False
stop_zeroconf() stop_zeroconf()
def exit(self): def exit(self):
cherrypy.engine.exit() cherrypy.engine.exit()
def get_cover(self, id, thumbnail=False): def get_cover(self, id, thumbnail=False):
cover = self.db.cover(id, index_is_id=True, as_file=False) cover = self.db.cover(id, index_is_id=True, as_file=False)
if cover is None: if cover is None:
@ -196,14 +196,14 @@ class LibraryServer(object):
try: try:
if QApplication.instance() is None: if QApplication.instance() is None:
QApplication([]) QApplication([])
im = QImage() im = QImage()
im.loadFromData(cover) im.loadFromData(cover)
if im.isNull(): if im.isNull():
raise cherrypy.HTTPError(404, 'No valid cover found') raise cherrypy.HTTPError(404, 'No valid cover found')
width, height = im.width(), im.height() width, height = im.width(), im.height()
scaled, width, height = fit_image(width, height, scaled, width, height = fit_image(width, height,
60 if thumbnail else self.max_cover_width, 60 if thumbnail else self.max_cover_width,
80 if thumbnail else self.max_cover_height) 80 if thumbnail else self.max_cover_height)
if not scaled: if not scaled:
return cover return cover
@ -217,7 +217,7 @@ class LibraryServer(object):
import traceback import traceback
traceback.print_exc() traceback.print_exc()
raise cherrypy.HTTPError(404, 'Failed to generate cover: %s'%err) raise cherrypy.HTTPError(404, 'Failed to generate cover: %s'%err)
def get_format(self, id, format): def get_format(self, id, format):
format = format.upper() format = format.upper()
fmt = self.db.format(id, format, index_is_id=True, as_file=True, mode='rb') fmt = self.db.format(id, format, index_is_id=True, as_file=True, mode='rb')
@ -232,7 +232,7 @@ class LibraryServer(object):
updated = datetime.utcfromtimestamp(os.stat(path).st_mtime) updated = datetime.utcfromtimestamp(os.stat(path).st_mtime)
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
return fmt.read() return fmt.read()
def sort(self, items, field, order): def sort(self, items, field, order):
field = field.lower().strip() field = field.lower().strip()
if field == 'author': if field == 'author':
@ -243,10 +243,23 @@ class LibraryServer(object):
raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field) raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field)
cmpf = cmp if field in ('rating', 'size', 'timestamp') else \ cmpf = cmp if field in ('rating', 'size', 'timestamp') else \
lambda x, y: cmp(x.lower() if x else '', y.lower() if y else '') lambda x, y: cmp(x.lower() if x else '', y.lower() if y else '')
field = FIELD_MAP[field] if field == 'series':
getter = operator.itemgetter(field) items.sort(cmp=self.seriescmp, reverse=not order)
items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order) else:
field = FIELD_MAP[field]
getter = operator.itemgetter(field)
items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order)
def seriescmp(self, x, y):
si = FIELD_MAP['series']
try:
ans = cmp(x[si].lower(), y[si].lower())
except AttributeError: # Some entries may be None
ans = cmp(x[si], y[si])
if ans != 0: return ans
return cmp(x[FIELD_MAP['series_index']], y[FIELD_MAP['series_index']])
def last_modified(self, updated): def last_modified(self, updated):
lm = updated.strftime('day, %d month %Y %H:%M:%S GMT') lm = updated.strftime('day, %d month %Y %H:%M:%S GMT')
day ={0:'Sun', 1:'Mon', 2:'Tue', 3:'Wed', 4:'Thu', 5:'Fri', 6:'Sat'} day ={0:'Sun', 1:'Mon', 2:'Tue', 3:'Wed', 4:'Thu', 5:'Fri', 6:'Sat'}
@ -254,8 +267,8 @@ class LibraryServer(object):
month = {1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May', 6:'Jun', 7:'Jul', month = {1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May', 6:'Jun', 7:'Jul',
8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'} 8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'}
return lm.replace('month', month[updated.month]) return lm.replace('month', month[updated.month])
@expose @expose
def stanza(self): def stanza(self):
' Feeds to read calibre books on a ipod with stanza.' ' Feeds to read calibre books on a ipod with stanza.'
@ -264,7 +277,7 @@ class LibraryServer(object):
r = record[FIELD_MAP['formats']] r = record[FIELD_MAP['formats']]
r = r.upper() if r else '' r = r.upper() if r else ''
if 'EPUB' in r or 'PDB' in r: if 'EPUB' in r or 'PDB' in r:
authors = ' & '.join([i.replace('|', ',') for i in authors = ' & '.join([i.replace('|', ',') for i in
record[FIELD_MAP['authors']].split(',')]) record[FIELD_MAP['authors']].split(',')])
extra = [] extra = []
rating = record[FIELD_MAP['rating']] rating = record[FIELD_MAP['rating']]
@ -276,7 +289,7 @@ class LibraryServer(object):
extra.append('TAGS: %s<br />'%', '.join(tags.split(','))) extra.append('TAGS: %s<br />'%', '.join(tags.split(',')))
series = record[FIELD_MAP['series']] series = record[FIELD_MAP['series']]
if series: if series:
extra.append('SERIES: %s [%d]<br />'%(series, extra.append('SERIES: %s [%d]<br />'%(series,
record[FIELD_MAP['series_index']])) record[FIELD_MAP['series_index']]))
fmt = 'epub' if 'EPUB' in r else 'pdb' fmt = 'epub' if 'EPUB' in r else 'pdb'
mimetype = guess_type('dummy.'+fmt)[0] mimetype = guess_type('dummy.'+fmt)[0]
@ -288,24 +301,24 @@ class LibraryServer(object):
mimetype=mimetype, mimetype=mimetype,
fmt=fmt, fmt=fmt,
).render('xml').decode('utf8')) ).render('xml').decode('utf8'))
updated = self.db.last_modified() updated = self.db.last_modified()
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
cherrypy.response.headers['Content-Type'] = 'text/xml' cherrypy.response.headers['Content-Type'] = 'text/xml'
return self.STANZA.generate(subtitle='', data=books, FM=FIELD_MAP, return self.STANZA.generate(subtitle='', data=books, FM=FIELD_MAP,
updated=updated, id='urn:calibre:main').render('xml') updated=updated, id='urn:calibre:main').render('xml')
@expose @expose
def library(self, start='0', num='50', sort=None, search=None, def library(self, start='0', num='50', sort=None, search=None,
_=None, order='ascending'): _=None, order='ascending'):
''' '''
Serves metadata from the calibre database as XML. Serves metadata from the calibre database as XML.
:param sort: Sort results by ``sort``. Can be one of `title,author,rating`. :param sort: Sort results by ``sort``. Can be one of `title,author,rating`.
:param search: Filter results by ``search`` query. See :class:`SearchQueryParser` for query syntax :param search: Filter results by ``search`` query. See :class:`SearchQueryParser` for query syntax
:param start,num: Return the slice `[start:start+num]` of the sorted and filtered results :param start,num: Return the slice `[start:start+num]` of the sorted and filtered results
:param _: Firefox seems to sometimes send this when using XMLHttpRequest with no caching :param _: Firefox seems to sometimes send this when using XMLHttpRequest with no caching
''' '''
try: try:
start = int(start) start = int(start)
@ -321,19 +334,19 @@ class LibraryServer(object):
items = [r for r in iter(self.db) if r[0] in ids] items = [r for r in iter(self.db) if r[0] in ids]
if sort is not None: if sort is not None:
self.sort(items, sort, order) self.sort(items, sort, order)
book, books = MarkupTemplate(self.BOOK), [] book, books = MarkupTemplate(self.BOOK), []
for record in items[start:start+num]: for record in items[start:start+num]:
aus = record[2] if record[2] else _('Unknown') aus = record[2] if record[2] else _('Unknown')
authors = '|'.join([i.replace('|', ',') for i in aus.split(',')]) authors = '|'.join([i.replace('|', ',') for i in aus.split(',')])
books.append(book.generate(r=record, authors=authors).render('xml').decode('utf-8')) books.append(book.generate(r=record, authors=authors).render('xml').decode('utf-8'))
updated = self.db.last_modified() updated = self.db.last_modified()
cherrypy.response.headers['Content-Type'] = 'text/xml' cherrypy.response.headers['Content-Type'] = 'text/xml'
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
return self.LIBRARY.generate(books=books, start=start, updated=updated, return self.LIBRARY.generate(books=books, start=start, updated=updated,
total=len(ids)).render('xml') total=len(ids)).render('xml')
@expose @expose
def index(self, **kwargs): def index(self, **kwargs):
'The / URL' 'The / URL'
@ -341,8 +354,8 @@ class LibraryServer(object):
if stanza == 919: if stanza == 919:
return self.static('index.html') return self.static('index.html')
return self.stanza() return self.stanza()
@expose @expose
def get(self, what, id): def get(self, what, id):
'Serves files, covers, thumbnails from the calibre database' 'Serves files, covers, thumbnails from the calibre database'
@ -361,7 +374,7 @@ class LibraryServer(object):
if what == 'cover': if what == 'cover':
return self.get_cover(id) return self.get_cover(id)
return self.get_format(id, what) return self.get_format(id, what)
@expose @expose
def static(self, name): def static(self, name):
'Serves static content' 'Serves static content'
@ -392,11 +405,11 @@ def start_threaded_server(db, opts):
server.thread.setDaemon(True) server.thread.setDaemon(True)
server.thread.start() server.thread.start()
return server return server
def stop_threaded_server(server): def stop_threaded_server(server):
server.exit() server.exit()
server.thread = None server.thread = None
def option_parser(): def option_parser():
return config().option_parser('%prog '+ _('[options]\n\nStart the calibre content server.')) return config().option_parser('%prog '+ _('[options]\n\nStart the calibre content server.'))

View File

@ -69,6 +69,7 @@ else:
DOWNLOAD_DIR = '/var/www/calibre.kovidgoyal.net/htdocs/downloads' DOWNLOAD_DIR = '/var/www/calibre.kovidgoyal.net/htdocs/downloads'
MOBILEREAD = 'https://dev.mobileread.com/dist/kovid/calibre/' MOBILEREAD = 'https://dev.mobileread.com/dist/kovid/calibre/'
#MOBILEREAD = 'http://calibre.kovidgoyal.net/downloads/'
class OS(dict): class OS(dict):
"""Dictionary with a default value for unknown keys.""" """Dictionary with a default value for unknown keys."""
@ -197,6 +198,8 @@ else:
import sys, os, shutil, tarfile, subprocess, tempfile, urllib2, re, stat import sys, os, shutil, tarfile, subprocess, tempfile, urllib2, re, stat
MOBILEREAD='https://dev.mobileread.com/dist/kovid/calibre/' MOBILEREAD='https://dev.mobileread.com/dist/kovid/calibre/'
#MOBILEREAD='http://calibre.kovidgoyal.net/downloads/'
class TerminalController: class TerminalController:
BOL = '' #: Move the cursor to the beginning of the line BOL = '' #: Move the cursor to the beginning of the line

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -128,6 +128,12 @@ class BasicNewsRecipe(object):
#: #:
extra_css = None extra_css = None
#: If True empty feeds are removed from the output.
#: This option has no effect if parse_index is overriden in
#: the sub class. It is meant only for recipes that return a list
#: of feeds using :member:`feeds` or :method:`get_feeds`.
remove_empty_feeds = False
#: List of regular expressions that determines which links to follow #: List of regular expressions that determines which links to follow
#: If empty, it is ignored. For example:: #: If empty, it is ignored. For example::
#: #:
@ -985,6 +991,11 @@ class BasicNewsRecipe(object):
self.log.exception(msg) self.log.exception(msg)
remove = [f for f in parsed_feeds if len(f) == 0 and
self.remove_empty_feeds]
for f in remove:
parsed_feeds.remove(f)
return parsed_feeds return parsed_feeds
@classmethod @classmethod

View File

@ -39,6 +39,7 @@ recipe_modules = ['recipe_' + r for r in (
'nacional_cro', '24sata', 'dnevni_avaz', 'glas_srpske', '24sata_rs', 'nacional_cro', '24sata', 'dnevni_avaz', 'glas_srpske', '24sata_rs',
'krstarica', 'krstarica_en', 'tanjug', 'laprensa_ni', 'azstarnet', 'krstarica', 'krstarica_en', 'tanjug', 'laprensa_ni', 'azstarnet',
'corriere_della_sera_it', 'corriere_della_sera_en', 'msdnmag_en', 'corriere_della_sera_it', 'corriere_della_sera_en', 'msdnmag_en',
'moneynews', 'der_standard', 'diepresse', 'nzz_ger', 'hna',
)] )]
import re, imp, inspect, time, os import re, imp, inspect, time, os

View File

@ -0,0 +1,42 @@
''' http://www.derstandard.at - Austrian Newspaper '''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class DerStandardRecipe(BasicNewsRecipe):
title = u'derStandard'
__author__ = 'Gerhard Aigner'
oldest_article = 1
max_articles_per_feed = 100
feeds = [(u'International', u'http://derstandard.at/?page=rss&ressort=internationalpolitik'),
(u'Inland', u'http://derstandard.at/?page=rss&ressort=innenpolitik'),
(u'Wirtschaft', u'http://derstandard.at/?page=rss&ressort=investor'),
(u'Web', u'http://derstandard.at/?page=rss&ressort=webstandard'),
(u'Sport', u'http://derstandard.at/?page=rss&ressort=sport'),
(u'Panorama', u'http://derstandard.at/?page=rss&ressort=panorama'),
(u'Etat', u'http://derstandard.at/?page=rss&ressort=etat'),
(u'Kultur', u'http://derstandard.at/?page=rss&ressort=kultur'),
(u'Wissenschaft', u'http://derstandard.at/?page=rss&ressort=wissenschaft'),
(u'Gesundheit', u'http://derstandard.at/?page=rss&ressort=gesundheit'),
(u'Bildung', u'http://derstandard.at/?page=rss&ressort=subildung')]
encoding = 'utf-8'
language = _('German')
recursions = 0
remove_tags = [dict(name='div'), dict(name='a'), dict(name='link'), dict(name='meta'),
dict(name='form',attrs={'name':'sitesearch'}), dict(name='hr')]
preprocess_regexps = [
(re.compile(r'\[[\d*]\]', re.DOTALL|re.IGNORECASE), lambda match: ''),
(re.compile(r'bgcolor="#\w{3,6}"', re.DOTALL|re.IGNORECASE), lambda match: '')
]
def print_version(self, url):
return url.replace('?id=', 'txt/?id=')
def get_article_url(self, article):
'''if the article links to a index page (ressort) or a picture gallery
(ansichtssache), don't add it'''
if (article.link.count('ressort') > 0 or article.title.lower().count('ansichtssache') > 0):
return None
return article.link

View File

@ -0,0 +1,40 @@
import re
from calibre.web.feeds.news import BasicNewsRecipe
class DiePresseRecipe(BasicNewsRecipe):
title = u'diePresse'
oldest_article = 1
max_articles_per_feed = 100
recursions = 0
language = _('German')
__author__ = 'Gerhard Aigner'
preprocess_regexps = [
(re.compile(r'Textversion', re.DOTALL), lambda match: ''),
]
remove_tags = [dict(name='hr'),
dict(name='br'),
dict(name='small'),
dict(name='img'),
dict(name='div', attrs={'class':'textnavi'}),
dict(name='h1', attrs={'class':'titel'}),
dict(name='a', attrs={'class':'print'}),
dict(name='div', attrs={'class':'hline'})]
feeds = [(u'Politik', u'http://diepresse.com/rss/Politik'),
(u'Wirtschaft', u'http://diepresse.com/rss/Wirtschaft'),
(u'Europa', u'http://diepresse.com/rss/EU'),
(u'Panorama', u'http://diepresse.com/rss/Panorama'),
(u'Sport', u'http://diepresse.com/rss/Sport'),
(u'Kultur', u'http://diepresse.com/rss/Kultur'),
(u'Leben', u'http://diepresse.com/rss/Leben'),
(u'Tech', u'http://diepresse.com/rss/Tech'),
(u'Science', u'http://diepresse.com/rss/Science'),
(u'Bildung', u'http://diepresse.com/rss/Bildung'),
(u'Gesundheit', u'http://diepresse.com/rss/Gesundheit'),
(u'Recht', u'http://diepresse.com/rss/Recht'),
(u'Spectrum', u'http://diepresse.com/rss/Spectrum'),
(u'Meinung', u'http://diepresse.com/rss/Meinung')]
def print_version(self, url):
return url.replace('home','text/home')

View File

@ -0,0 +1,40 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
Fetch Hessisch Niedersachsische Allgemeine.
'''
from calibre.web.feeds.news import BasicNewsRecipe
class hnaDe(BasicNewsRecipe):
title = 'HNA'
description = 'local news from Hessen/Germany'
__author__ = 'Oliver Niesner'
use_embedded_content = False
language = _('German')
use_embedded_content = False
timefmt = ' [%d %b %Y]'
max_articles_per_feed = 40
no_stylesheets = True
encoding = 'iso-8859-1'
remove_tags = [dict(id='topnav'),
dict(id='nav_main'),
dict(id='suchen'),
dict(id=''),
dict(name='span'),
dict(name='ul', attrs={'class':'linklist'}),
dict(name='a', attrs={'href':'#'}),
dict(name='p', attrs={'class':'breadcrumb'}),
dict(name='p', attrs={'class':'h5'})]
#remove_tags_after = [dict(name='div', attrs={'class':'rahmenbreaking'})]
remove_tags_after = [dict(name='a', attrs={'href':'#'})]
feeds = [ ('hna_soehre', 'http://feeds2.feedburner.com/hna/soehre'),
('hna_kassel', 'http://feeds2.feedburner.com/hna/kassel') ]

View File

@ -1,80 +1,78 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
''' '''
Fetch Linuxdevices. Fetch Linuxdevices.
''' '''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class Sueddeutsche(BasicNewsRecipe): class Sueddeutsche(BasicNewsRecipe):
title = u'Linuxdevices' title = u'Linuxdevices'
description = 'News about Linux driven Hardware' description = 'News about Linux driven Hardware'
__author__ = 'Oliver Niesner' __author__ = 'Oliver Niesner'
use_embedded_content = False use_embedded_content = False
timefmt = ' [%a, %d %b %Y]' timefmt = ' [%a %d %b %Y]'
language = _('English') max_articles_per_feed = 50
max_articles_per_feed = 50 no_stylesheets = True
no_stylesheets = True html2epub_options = 'linearize_tables = True\nbase_font_size2=14'
encoding = 'latin1' encoding = 'latin1'
remove_tags_after = [dict(id='nointelliTXT')]
filter_regexps = [r'ad\.doubleclick\.net'] remove_tags_after = [dict(id='nointelliTXT')]
filter_regexps = [r'ad\.doubleclick\.net']
remove_tags = [dict(name='div', attrs={'class':'bannerSuperBanner'}), remove_tags = [dict(name='div', attrs={'class':'bannerSuperBanner'}),
dict(name='div', attrs={'class':'bannerSky'}), dict(name='div', attrs={'class':'bannerSky'}),
dict(name='div', attrs={'class':'footerLinks'}), dict(name='div', attrs={'class':'footerLinks'}),
dict(name='div', attrs={'class':'seitenanfang'}), dict(name='div', attrs={'class':'seitenanfang'}),
dict(name='td', attrs={'class':'mar5'}), dict(name='td', attrs={'class':'mar5'}),
dict(name='td', attrs={'class':'mar5'}), dict(name='td', attrs={'class':'mar5'}),
dict(name='table', attrs={'class':'pageAktiv'}), dict(name='table', attrs={'class':'pageAktiv'}),
dict(name='table', attrs={'class':'xartable'}), dict(name='table', attrs={'class':'xartable'}),
dict(name='table', attrs={'class':'wpnavi'}), dict(name='table', attrs={'class':'wpnavi'}),
dict(name='table', attrs={'class':'bgcontent absatz'}), dict(name='table', attrs={'class':'bgcontent absatz'}),
dict(name='table', attrs={'class':'footer'}), dict(name='table', attrs={'class':'footer'}),
dict(name='table', attrs={'class':'artikelBox'}), dict(name='table', attrs={'class':'artikelBox'}),
dict(name='table', attrs={'class':'kommentare'}), dict(name='table', attrs={'class':'kommentare'}),
dict(name='table', attrs={'class':'pageBoxBot'}), dict(name='table', attrs={'class':'pageBoxBot'}),
#dict(name='table', attrs={'with':'100%'}), dict(name='td', attrs={'nowrap':'nowrap'}),
dict(name='td', attrs={'nowrap':'nowrap'}), dict(name='td', attrs={'valign':'middle'}),
dict(name='td', attrs={'valign':'middle'}), dict(name='td', attrs={'align':'left'}),
dict(name='td', attrs={'align':'left'}), dict(name='td', attrs={'align':'center'}),
dict(name='td', attrs={'align':'center'}), dict(name='td', attrs={'height':'5'}),
dict(name='td', attrs={'height':'5'}), dict(name='div', attrs={'class':'artikelBox navigatorBox'}),
dict(name='div', attrs={'class':'artikelBox navigatorBox'}), dict(name='div', attrs={'class':'similar-article-box'}),
dict(name='div', attrs={'class':'similar-article-box'}), dict(name='div', attrs={'class':'videoBigHack'}),
dict(name='div', attrs={'class':'videoBigHack'}), dict(name='td', attrs={'class':'artikelDruckenRight'}),
dict(name='td', attrs={'class':'artikelDruckenRight'}), dict(name='td', attrs={'class':'width="200"'}),
dict(name='td', attrs={'class':'width="200"'}), dict(name='a', attrs={'href':'/news'}),
dict(name='a', attrs={'href':'/news'}), dict(name='a', attrs={'href':'/'}),
dict(name='a', attrs={'href':'/'}), dict(name='a', attrs={'href':'/articles'}),
dict(name='a', attrs={'href':'/articles'}), dict(name='a', attrs={'href':'/cgi-bin/survey/survey.cgi'}),
dict(name='a', attrs={'href':'/cgi-bin/survey/survey.cgi'}), dict(name='a', attrs={'href':'/cgi-bin/board/UltraBoard.pl'}),
dict(name='a', attrs={'href':'/cgi-bin/board/UltraBoard.pl'}), dict(name='iframe'),
dict(name='iframe'), dict(name='form'),
dict(name='form'), dict(name='span', attrs={'class':'hidePrint'}),
#dict(name='tr', attrs={'td':'Click here to learn'}), dict(id='headerLBox'),
dict(name='span', attrs={'class':'hidePrint'}), dict(id='nointelliTXT'),
dict(id='headerLBox'), dict(id='rechteSpalte'),
dict(id='nointelliTXT'), dict(id='newsticker-list-small'),
dict(id='rechteSpalte'), dict(id='ntop5'),
dict(id='newsticker-list-small'), dict(id='ntop5send'),
dict(id='ntop5'), dict(id='ntop5commented'),
dict(id='ntop5send'), dict(id='nnav-bgheader'),
dict(id='ntop5commented'), dict(id='nnav-headerteaser'),
dict(id='nnav-bgheader'), dict(id='nnav-head'),
dict(id='nnav-headerteaser'), dict(id='nnav-top'),
dict(id='nnav-head'), dict(id='nnav-logodiv'),
dict(id='nnav-top'), dict(id='nnav-logo'),
dict(id='nnav-logodiv'), dict(id='nnav-oly'),
dict(id='nnav-logo'), dict(id='readcomment')]
dict(id='nnav-oly'),
dict(id='readcomment')]
feeds = [ (u'Linuxdevices', u'http://www.linuxdevices.com/backend/headlines.rss') ]
feeds = [ (u'Linuxdevices', u'http://www.linuxdevices.com/backend/headlines.rss') ]

View File

@ -0,0 +1,49 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
'''
moneynews.newsmax.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class MoneyNews(BasicNewsRecipe):
title = 'Moneynews.com'
__author__ = 'Darko Miletic'
description = 'Financial news worldwide'
publisher = 'moneynews.com'
category = 'news, finances, USA, business'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
encoding = 'cp1252'
html2lrf_options = [
'--comment', description
, '--category', category
, '--publisher', publisher
, '--ignore-tables'
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True'
feeds = [
(u'Street Talk' , u'http://moneynews.newsmax.com/xml/streettalk.xml' )
,(u'Finance News' , u'http://moneynews.newsmax.com/xml/FinanceNews.xml' )
,(u'Economy' , u'http://moneynews.newsmax.com/xml/economy.xml' )
,(u'Companies' , u'http://moneynews.newsmax.com/xml/companies.xml' )
,(u'Markets' , u'http://moneynews.newsmax.com/xml/Markets.xml' )
,(u'Investing & Analysis' , u'http://moneynews.newsmax.com/xml/investing.xml' )
]
keep_only_tags = [dict(name='table', attrs={'class':'copy'})]
remove_tags = [
dict(name='td' , attrs={'id':'article_fontsize'})
,dict(name='table', attrs={'id':'toolbox' })
,dict(name='tr' , attrs={'id':'noprint3' })
]

View File

@ -0,0 +1,66 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
'''
www.nzz.ch
'''
from calibre.web.feeds.recipes import BasicNewsRecipe
class Nzz(BasicNewsRecipe):
title = 'NZZ Online'
__author__ = 'Darko Miletic'
description = 'Laufend aktualisierte Nachrichten, Analysen und Hintergruende zu Politik, Wirtschaft, Kultur und Sport'
publisher = 'NZZ AG'
category = 'news, politics, nachrichten, Switzerland'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
encoding = 'utf-8'
use_embedded_content = False
lang = 'de-CH'
language = _('German')
html2lrf_options = [
'--comment', description
, '--category', category
, '--publisher', publisher
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\noverride_css=" p {text-indent: 0em; margin-top: 0em; margin-bottom: 0.5em} img {margin-top: 0em; margin-bottom: 0.4em}"'
keep_only_tags = [dict(name='div', attrs={'class':'article'})]
remove_tags = [
dict(name=['object','link','base','script'])
,dict(name='div',attrs={'class':['more','teaser','advXertXoriXals','legal']})
,dict(name='div',attrs={'id':['popup-src','readercomments','google-ad','advXertXoriXals']})
]
feeds = [
(u'Neuste Artikel', u'http://www.nzz.ch/feeds/recent/' )
,(u'International' , u'http://www.nzz.ch/nachrichten/international?rss=true')
,(u'Schweiz' , u'http://www.nzz.ch/nachrichten/schweiz?rss=true')
,(u'Wirtschaft' , u'http://www.nzz.ch/nachrichten/wirtschaft/aktuell?rss=true')
,(u'Finanzmaerkte' , u'http://www.nzz.ch/finanzen/nachrichten?rss=true')
,(u'Zuerich' , u'http://www.nzz.ch/nachrichten/zuerich?rss=true')
,(u'Sport' , u'http://www.nzz.ch/nachrichten/sport?rss=true')
,(u'Panorama' , u'http://www.nzz.ch/nachrichten/panorama?rss=true')
,(u'Kultur' , u'http://www.nzz.ch/nachrichten/kultur/aktuell?rss=true')
,(u'Wissenschaft' , u'http://www.nzz.ch/nachrichten/wissenschaft?rss=true')
,(u'Medien' , u'http://www.nzz.ch/nachrichten/medien?rss=true')
,(u'Reisen' , u'http://www.nzz.ch/magazin/reisen?rss=true')
]
def preprocess_html(self, soup):
soup.html['xml:lang'] = self.lang
soup.html['lang'] = self.lang
mtag = '<meta http-equiv="Content-Type" content="text/html; charset=' + self.encoding + '">'
soup.head.insert(0,mtag)
return soup
def print_version(self, url):
return url + '?printview=true'

View File

@ -8,26 +8,19 @@ Fetch tomshardware.
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class TomsHardwareDe(BasicNewsRecipe): class cdnet(BasicNewsRecipe):
title = 'Tom\'s Hardware German' title = 'tomshardware'
description = 'Computer news in german' description = 'computer news in german'
__author__ = 'Oliver Niesner' __author__ = 'Oliver Niesner'
use_embedded_content = False use_embedded_content = False
timefmt = ' [%d %b %Y]' timefmt = ' [%d %b %Y]'
max_articles_per_feed = 50 max_articles_per_feed = 50
language = _('German')
no_stylesheets = True no_stylesheets = True
language = _('German')
encoding = 'utf-8' encoding = 'utf-8'
#preprocess_regexps = \
# [(re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
# [
# (r'<84>', lambda match: ''),
# (r'<93>', lambda match: ''),
# ]
# ]
remove_tags = [dict(id='outside-advert'), remove_tags = [dict(id='outside-advert'),
dict(id='advertRightWhite'), dict(id='advertRightWhite'),
dict(id='header-advert'), dict(id='header-advert'),
@ -36,9 +29,15 @@ class TomsHardwareDe(BasicNewsRecipe):
dict(id='header-top'), dict(id='header-top'),
dict(id='header-tools'), dict(id='header-tools'),
dict(id='nbComment'), dict(id='nbComment'),
dict(id='commentTools'),
dict(id='internalSidebar'), dict(id='internalSidebar'),
dict(id='header-news-infos'), dict(id='header-news-infos'),
dict(id='header-news-tools'),
dict(id='breadcrumbs'), dict(id='breadcrumbs'),
dict(id='emailTools'),
dict(id='bookmarkTools'),
dict(id='printTools'),
dict(id='header-nextNews'),
dict(id=''), dict(id=''),
dict(name='div', attrs={'class':'pyjama'}), dict(name='div', attrs={'class':'pyjama'}),
dict(name='href', attrs={'class':'comment'}), dict(name='href', attrs={'class':'comment'}),
@ -47,8 +46,10 @@ class TomsHardwareDe(BasicNewsRecipe):
dict(name='div', attrs={'class':'greyBox clearfix'}), dict(name='div', attrs={'class':'greyBox clearfix'}),
dict(id='')] dict(id='')]
#remove_tags_before = [dict(id='header-news-title')] #remove_tags_before = [dict(id='header-news-title')]
remove_tags_after = [dict(name='div', attrs={'class':'news-elm'})] remove_tags_after = [dict(name='div', attrs={'class':'btmGreyTables'})]
#remove_tags_after = [dict(name='div', attrs={'class':'intelliTXT'})] #remove_tags_after = [dict(name='div', attrs={'class':'intelliTXT'})]
feeds = [ ('tomshardware', 'http://www.tomshardware.com/de/feeds/rss2/tom-s-hardware-de,12-1.xml') ] feeds = [ ('tomshardware', 'http://www.tomshardware.com/de/feeds/rss2/tom-s-hardware-de,12-1.xml') ]

View File

@ -11,20 +11,23 @@ from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class Vreme(BasicNewsRecipe): class Vreme(BasicNewsRecipe):
title = 'Vreme' title = 'Vreme'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = 'Politicki Nedeljnik Srbije' description = 'Politicki Nedeljnik Srbije'
publisher = 'Vreme d.o.o.' publisher = 'NP Vreme d.o.o.'
category = 'news, politics, Serbia' category = 'news, politics, Serbia'
no_stylesheets = True delay = 1
remove_javascript = True no_stylesheets = True
needs_subscription = True needs_subscription = True
INDEX = 'http://www.vreme.com' INDEX = 'http://www.vreme.com'
LOGIN = 'http://www.vreme.com/account/index.php' LOGIN = 'http://www.vreme.com/account/login.php?url=%2F'
remove_javascript = True remove_javascript = True
use_embedded_content = False use_embedded_content = False
language = _('Serbian') encoding = 'utf-8'
extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} body{text-align: justify; font-family: serif1, serif} .article_description{font-family: serif1, serif}' language = _('Serbian')
lang = 'sr-Latn-RS'
direction = 'ltr'
extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} body{text-align: justify; font-family: serif1, serif} .article_description{font-family: serif1, serif}'
html2lrf_options = [ html2lrf_options = [
'--comment' , description '--comment' , description
@ -52,20 +55,11 @@ class Vreme(BasicNewsRecipe):
articles = [] articles = []
soup = self.index_to_soup(self.INDEX) soup = self.index_to_soup(self.INDEX)
for item in soup.findAll('span', attrs={'class':'toc2'}): for item in soup.findAll(['h3','h4']):
description = '' description = ''
title_prefix = '' title_prefix = ''
descript_title_tag = item.findPreviousSibling('span', attrs={'class':'toc1'})
if descript_title_tag:
title_prefix = self.tag_to_string(descript_title_tag) + ' '
descript_tag = item.findNextSibling('span', attrs={'class':'toc3'})
if descript_tag:
description = self.tag_to_string(descript_tag)
feed_link = item.find('a') feed_link = item.find('a')
if feed_link and feed_link.has_key('href'): if feed_link and feed_link.has_key('href') and feed_link['href'].startswith('/cms/view.php'):
url = self.INDEX + feed_link['href'] url = self.INDEX + feed_link['href']
title = title_prefix + self.tag_to_string(feed_link) title = title_prefix + self.tag_to_string(feed_link)
date = strftime(self.timefmt) date = strftime(self.timefmt)
@ -93,14 +87,17 @@ class Vreme(BasicNewsRecipe):
del item['face'] del item['face']
for item in soup.findAll(size=True): for item in soup.findAll(size=True):
del item['size'] del item['size']
mtag = '<meta http-equiv="Content-Language" content="sr-Latn-RS"/>' soup.html['lang'] = self.lang
soup.head.insert(0,mtag) soup.html['dir' ] = self.direction
mtag = '<meta http-equiv="Content-Language" content="' + self.lang + '"/>'
mtag += '\n<meta http-equiv="Content-Type" content="text/html; charset=' + self.encoding + '"/>'
soup.head.insert(0,mtag)
return soup return soup
def get_cover_url(self): def get_cover_url(self):
cover_url = None cover_url = None
soup = self.index_to_soup(self.INDEX) soup = self.index_to_soup(self.INDEX)
cover_item = soup.find('img',attrs={'alt':'Naslovna strana broja'}) cover_item = soup.find('div',attrs={'id':'najava'})
if cover_item: if cover_item:
cover_url = self.INDEX + cover_item['src'] cover_url = self.INDEX + cover_item.img['src']
return cover_url return cover_url

View File

@ -530,6 +530,7 @@ class build_windows(VMInstaller):
self.run_windows_install_jammer(installer) self.run_windows_install_jammer(installer)
return os.path.basename(installer) return os.path.basename(installer)
@classmethod
def run_windows_install_jammer(self, installer): def run_windows_install_jammer(self, installer):
ibp = os.path.abspath('installer/windows') ibp = os.path.abspath('installer/windows')
sys.path.insert(0, ibp) sys.path.insert(0, ibp)