Integrate conversion of comics into the GUI

This commit is contained in:
Kovid Goyal 2008-08-02 22:31:59 -07:00
parent 313bc4d51b
commit acc94d3736
13 changed files with 706 additions and 71 deletions

View File

@ -19,6 +19,8 @@ EXTRAS = ('/usr/lib/python2.5/site-packages/PIL', os.path.expanduser('~/
SQLITE = '/usr/lib/libsqlite3.so.0' SQLITE = '/usr/lib/libsqlite3.so.0'
DBUS = '/usr/lib/libdbus-1.so.3' DBUS = '/usr/lib/libdbus-1.so.3'
LIBMNG = '/usr/lib/libmng.so.1' LIBMNG = '/usr/lib/libmng.so.1'
LIBZ = '/lib/libz.so.1'
LIBUSB = '/lib/libusb.so'
CALIBRESRC = os.path.join(CALIBREPREFIX, 'src') CALIBRESRC = os.path.join(CALIBREPREFIX, 'src')
@ -117,7 +119,7 @@ binaries += [('clit', CLIT, 'BINARY'), ('pdftohtml', PDFTOHTML, 'BINARY'),
('libunrar.so', LIBUNRAR, 'BINARY')] ('libunrar.so', LIBUNRAR, 'BINARY')]
print 'Adding external libraries...' print 'Adding external libraries...'
binaries += [ (os.path.basename(x), x, 'BINARY') for x in (SQLITE, DBUS, LIBMNG)] binaries += [ (os.path.basename(x), x, 'BINARY') for x in (SQLITE, DBUS, LIBMNG, LIBZ, LIBUSB)]
qt = [] qt = []

View File

@ -26,6 +26,7 @@ terminal_controller = TerminalController(sys.stdout)
iswindows = 'win32' in sys.platform.lower() or 'win64' in sys.platform.lower() iswindows = 'win32' in sys.platform.lower() or 'win64' in sys.platform.lower()
isosx = 'darwin' in sys.platform.lower() isosx = 'darwin' in sys.platform.lower()
islinux = not(iswindows or isosx) islinux = not(iswindows or isosx)
isfrozen = hasattr(sys, 'frozen')
try: try:
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, '')
@ -351,11 +352,11 @@ def get_proxies(self):
def browser(honor_time=False): def browser(honor_time=False):
http_proxy = get_proxies().get('http', None)
opener = mechanize.Browser() opener = mechanize.Browser()
opener.set_handle_refresh(True, honor_time=honor_time) opener.set_handle_refresh(True, honor_time=honor_time)
opener.set_handle_robots(False) opener.set_handle_robots(False)
opener.addheaders = [('User-agent', 'Mozilla/5.0 (X11; U; i686 Linux; en_US; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.4')] opener.addheaders = [('User-agent', 'Mozilla/5.0 (X11; U; i686 Linux; en_US; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.4')]
http_proxy = get_proxies().get('http', None)
if http_proxy: if http_proxy:
opener.set_proxies({'http':http_proxy}) opener.set_proxies({'http':http_proxy})
return opener return opener

View File

@ -8,7 +8,7 @@ from ctypes import cdll, POINTER, byref, pointer, Structure, \
c_ubyte, c_ushort, c_int, c_char, c_void_p, c_byte, c_uint c_ubyte, c_ushort, c_int, c_char, c_void_p, c_byte, c_uint
from errno import EBUSY, ENOMEM from errno import EBUSY, ENOMEM
from calibre import iswindows, isosx, load_library from calibre import iswindows, isosx, load_library, isfrozen
_libusb_name = 'libusb' _libusb_name = 'libusb'
PATH_MAX = 511 if iswindows else 1024 if isosx else 4096 PATH_MAX = 511 if iswindows else 1024 if isosx else 4096

View File

@ -17,4 +17,4 @@ class UnknownFormatError(Exception):
BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'xhtm', BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'xhtm',
'html', 'xhtml', 'epub', 'pdf', 'prc', 'mobi', 'azw', 'html', 'xhtml', 'epub', 'pdf', 'prc', 'mobi', 'azw',
'epub', 'fb2', 'djvu', 'lrx'] 'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz']

View File

@ -10,8 +10,9 @@ Based on ideas from comiclrf created by FangornUK.
import os, sys, traceback, shutil import os, sys, traceback, shutil
from uuid import uuid4 from uuid import uuid4
from calibre import extract, OptionParser, detect_ncpus, terminal_controller, \ from calibre import extract, detect_ncpus, terminal_controller, \
__appname__, __version__ __appname__, __version__
from calibre.utils.config import Config, StringConfig
from calibre.ptempfile import PersistentTemporaryDirectory from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.threadpool import ThreadPool, WorkRequest from calibre.utils.threadpool import ThreadPool, WorkRequest
from calibre.utils.terminfo import ProgressBar from calibre.utils.terminfo import ProgressBar
@ -21,7 +22,7 @@ try:
NewMagickWand, NewPixelWand, \ NewMagickWand, NewPixelWand, \
MagickSetImageBorderColor, \ MagickSetImageBorderColor, \
MagickReadImage, MagickRotateImage, \ MagickReadImage, MagickRotateImage, \
MagickTrimImage, \ MagickTrimImage, PixelSetColor,\
MagickNormalizeImage, MagickGetImageWidth, \ MagickNormalizeImage, MagickGetImageWidth, \
MagickGetImageHeight, \ MagickGetImageHeight, \
MagickResizeImage, MagickSetImageType, \ MagickResizeImage, MagickSetImageType, \
@ -133,7 +134,7 @@ class PageProcessor(list):
pw = NewPixelWand() pw = NewPixelWand()
if pw < 0: if pw < 0:
raise RuntimeError('Cannot create wand.') raise RuntimeError('Cannot create wand.')
#flag = PixelSetColor(pw, 'white') PixelSetColor(pw, 'white')
MagickSetImageBorderColor(wand, pw) MagickSetImageBorderColor(wand, pw)
@ -145,7 +146,7 @@ class PageProcessor(list):
MagickSetImagePage(wand, 0,0,0,0) #Clear page after trim, like a "+repage" MagickSetImagePage(wand, 0,0,0,0) #Clear page after trim, like a "+repage"
# Do the Photoshop "Auto Levels" equivalent # Do the Photoshop "Auto Levels" equivalent
if self.opts.normalize: if not self.opts.dont_normalize:
MagickNormalizeImage(wand) MagickNormalizeImage(wand)
sizex = MagickGetImageWidth(wand) sizex = MagickGetImageWidth(wand)
@ -173,7 +174,7 @@ class PageProcessor(list):
else: else:
MagickResizeImage(wand, SCRWIDTH, SCRHEIGHT, CatromFilter, 1.0) MagickResizeImage(wand, SCRWIDTH, SCRHEIGHT, CatromFilter, 1.0)
if self.opts.sharpen: if not self.opts.dont_sharpen:
MagickSharpenImage(wand, 0.0, 1.0) MagickSharpenImage(wand, 0.0, 1.0)
MagickSetImageType(wand, GrayscaleType) MagickSetImageType(wand, GrayscaleType)
@ -225,34 +226,46 @@ def process_pages(pages, opts, update):
finally: finally:
finalize() finalize()
def config(defaults=None):
desc = _('Options to control the conversion of comics (CBR, CBZ) files into ebooks')
if defaults is None:
c = Config('comic', desc)
else:
c = StringConfig(defaults, desc)
c.add_opt('title', ['-t', '--title'],
help=_('Title for generated ebook. Default is to use the filename.'))
c.add_opt('author', ['-a', '--author'],
help=_('Set the author in the metadata of the generated ebook. Default is %default'),
default=_('Unknown'))
c.add_opt('output', ['-o', '--output'],
help=_('Path to output LRF file. By default a file is created in the current directory.'))
c.add_opt('colors', ['-c', '--colors'], type='int', default=64,
help=_('Number of colors for Grayscale image conversion. Default: %default'))
c.add_opt('dont_normalize', ['-n', '--disable-normalize'], default=False,
help=_('Disable normalize (improve contrast) color range for pictures. Default: False'))
c.add_opt('keep_aspect_ratio', ['-r', '--keep-aspect-ratio'], default=False,
help=_('Maintain picture aspect ratio. Default is to fill the screen.'))
c.add_opt('dont_sharpen', ['-s', '--disable-sharpen'], default=False,
help=_('Disable sharpening.'))
c.add_opt('landscape', ['-l', '--landscape'], default=False,
help=_("Don't split landscape images into two portrait images"))
c.add_opt('no_sort', ['--no-sort'], default=False,
help=_("Don't sort the files found in the comic alphabetically by name. Instead use the order they were added to the comic."))
c.add_opt('profile', ['-p', '--profile'], default='prs500', choices=PROFILES.keys(),
help=_('Choose a profile for the device you are generating this LRF for. The default is the SONY PRS-500 with a screen size of 584x754 pixels. Choices are %s')%PROFILES.keys())
c.add_opt('verbose', ['--verbose'], default=0, action='count',
help=_('Be verbose, useful for debugging. Can be specified multiple times for greater verbosity.'))
c.add_opt('no_progress_bar', ['--no-progress-bar'], default=False,
help=_("Don't show progress bar."))
return c
def option_parser(): def option_parser():
parser = OptionParser(_('''\ c = config()
return c.option_parser(usage=_('''\
%prog [options] comic.cb[z|r] %prog [options] comic.cb[z|r]
Convert a comic in a CBZ or CBR file to an LRF ebook. Convert a comic in a CBZ or CBR file to an LRF ebook.
''')) '''))
parser.add_option('-t', '--title', help=_('Title for generated ebook. Default is to use the filename.'), default=None)
parser.add_option('-a', '--author', help=_('Set the author in the metadata of the generated ebook. Default is %default'), default=_('Unknown'))
parser.add_option('-o', '--output', help=_('Path to output LRF file. By default a file is created in the current directory.'), default=None)
parser.add_option('-c', '--colors', type='int', default=64,
help=_('Number of colors for Grayscale image conversion. Default: %default'))
parser.add_option('-n', '--disable-normalize', dest='normalize', default=True, action='store_false',
help=_('Disable normalize (improve contrast) color range for pictures. Default: False'))
parser.add_option('-r', '--keep-aspect-ratio', action='store_true', default=False,
help=_('Maintain picture aspect ratio. Default is to fill the screen.'))
parser.add_option('-s', '--disable-sharpen', default=True, action='store_false', dest='sharpen',
help=_('Disable sharpening.'))
parser.add_option('-l', '--landscape', default=False, action='store_true',
help=_("Don't split landscape images into two portrait images"))
parser.add_option('--no-sort', default=False, action='store_true',
help=_("Don't sort the files found in the comic alphabetically by name. Instead use the order they were added to the comic."))
parser.add_option('-p', '--profile', default='prs500', dest='profile', type='choice',
choices=PROFILES.keys(), help=_('Choose a profile for the device you are generating this LRF for. The default is the SONY PRS-500 with a screen size of 584x754 pixels. Choices are %s')%PROFILES.keys())
parser.add_option('--verbose', default=False, action='store_true',
help=_('Be verbose, useful for debugging'))
parser.add_option('--no-progress-bar', default=False, action='store_true',
help=_("Don't show progress bar."))
return parser
def create_lrf(pages, profile, opts, thumbnail=None): def create_lrf(pages, profile, opts, thumbnail=None):
width, height = PROFILES[profile] width, height = PROFILES[profile]
@ -277,22 +290,8 @@ def create_lrf(pages, profile, opts, thumbnail=None):
book.renderLrf(open(opts.output, 'wb')) book.renderLrf(open(opts.output, 'wb'))
def do_convert(path_to_file, opts, notification=lambda m, p: p):
source = path_to_file
def main(args=sys.argv, notification=None):
parser = option_parser()
opts, args = parser.parse_args(args)
if len(args) < 2:
parser.print_help()
print '\nYou must specify a file to convert'
return 1
if not callable(notification):
pb = ProgressBar(terminal_controller, _('Rendering comic pages...'),
no_progress_bar=opts.no_progress_bar)
notification = pb.update
source = os.path.abspath(args[1])
if not opts.title: if not opts.title:
opts.title = os.path.splitext(os.path.basename(source)) opts.title = os.path.splitext(os.path.basename(source))
if not opts.output: if not opts.output:
@ -315,6 +314,24 @@ def main(args=sys.argv, notification=None):
create_lrf(pages, opts.profile, opts, thumbnail=thumbnail) create_lrf(pages, opts.profile, opts, thumbnail=thumbnail)
shutil.rmtree(tdir) shutil.rmtree(tdir)
shutil.rmtree(tdir2) shutil.rmtree(tdir2)
def main(args=sys.argv, notification=None):
parser = option_parser()
opts, args = parser.parse_args(args)
if len(args) < 2:
parser.print_help()
print '\nYou must specify a file to convert'
return 1
if not callable(notification):
pb = ProgressBar(terminal_controller, _('Rendering comic pages...'),
no_progress_bar=opts.no_progress_bar)
notification = pb.update
source = os.path.abspath(args[1])
do_convert(source, opts, notification)
print _('Output written to'), opts.output
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -0,0 +1,85 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
''''''
from PyQt4.QtGui import QDialog
from calibre.gui2.dialogs.comicconf_ui import Ui_Dialog
from calibre.gui2 import qstring_to_unicode
from calibre.ebooks.lrf.comic.convert_from import config
def set_conversion_defaults(window):
d = ComicConf(window)
d.exec_()
def get_bulk_conversion_options(window):
c = config(None)
with open(c.config_file_path, 'rb') as f:
d = ComicConf(window, config_defaults=f.read())
if d.exec_() == QDialog.Accepted:
return d.config.parse()
def get_conversion_options(window, defaults, title, author):
if defaults is None:
c = config(None)
with open(c.config_file_path, 'rb') as f:
defaults = f.read()
defaults += '\ntitle=%s\nauthor=%s'%(repr(title), repr(author))
d = ComicConf(window, config_defaults=defaults, generic=False)
if d.exec_() == QDialog.Accepted:
return d.config.parse(), d.config.src
return None, None
class ComicConf(QDialog, Ui_Dialog):
def __init__(self, window, config_defaults=None, generic=True,
title=_('Set defaults for conversion of comics (CBR/CBZ files)')):
QDialog.__init__(self, window)
Ui_Dialog.__init__(self)
self.setupUi(self)
self.setWindowTitle(title)
self.config = config(config_defaults)
opts = self.config.parse()
if generic:
for i in ('title', 'author'):
getattr(self, 'opt_'+i).setVisible(False)
getattr(self, i+'_label').setVisible(False)
else:
title = opts.title
if not title:
title = _('Unknown')
self.setWindowTitle(_('Set options for converting %s')%title)
author = opts.author
self.opt_title.setText(title)
self.opt_author.setText(author)
self.opt_colors.setValue(opts.colors)
self.opt_profile.addItem(opts.profile)
self.opt_dont_normalize.setChecked(opts.dont_normalize)
self.opt_keep_aspect_ratio.setChecked(opts.keep_aspect_ratio)
self.opt_dont_sharpen.setChecked(opts.dont_sharpen)
self.opt_landscape.setChecked(opts.landscape)
self.opt_no_sort.setChecked(opts.no_sort)
for opt in self.config.option_set.preferences:
g = getattr(self, 'opt_'+opt.name, False)
if opt.help and g:
g.setToolTip(opt.help)
def accept(self):
for opt in self.config.option_set.preferences:
g = getattr(self, 'opt_'+opt.name, False)
if not g or not g.isVisible(): continue
if hasattr(g, 'isChecked'):
val = bool(g.isChecked())
elif hasattr(g, 'value'):
val = g.value()
elif hasattr(g, 'itemText'):
val = qstring_to_unicode(g.itemText(g.currentIndex()))
elif hasattr(g, 'text'):
val = qstring_to_unicode(g.text())
else:
raise Exception('Bad coding')
self.config.set(opt.name, val)
return QDialog.accept(self)

View File

@ -0,0 +1,166 @@
<ui version="4.0" >
<class>Dialog</class>
<widget class="QDialog" name="Dialog" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>646</width>
<height>468</height>
</rect>
</property>
<property name="windowTitle" >
<string>Dialog</string>
</property>
<property name="windowIcon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/convert.svg</normaloff>:/images/convert.svg</iconset>
</property>
<layout class="QGridLayout" name="gridLayout" >
<item row="0" column="0" >
<widget class="QLabel" name="title_label" >
<property name="text" >
<string>&amp;Title:</string>
</property>
<property name="buddy" >
<cstring>opt_title</cstring>
</property>
</widget>
</item>
<item row="0" column="1" >
<widget class="QLineEdit" name="opt_title" />
</item>
<item row="1" column="0" >
<widget class="QLabel" name="author_label" >
<property name="text" >
<string>&amp;Author(s):</string>
</property>
<property name="buddy" >
<cstring>opt_author</cstring>
</property>
</widget>
</item>
<item row="1" column="1" >
<widget class="QLineEdit" name="opt_author" />
</item>
<item row="2" column="0" >
<widget class="QLabel" name="label_3" >
<property name="text" >
<string>&amp;Number of Colors:</string>
</property>
<property name="buddy" >
<cstring>opt_colors</cstring>
</property>
</widget>
</item>
<item row="2" column="1" >
<widget class="QSpinBox" name="opt_colors" >
<property name="minimum" >
<number>8</number>
</property>
<property name="maximum" >
<number>3200000</number>
</property>
<property name="singleStep" >
<number>8</number>
</property>
</widget>
</item>
<item row="3" column="0" >
<widget class="QLabel" name="label_4" >
<property name="text" >
<string>&amp;Profile:</string>
</property>
<property name="buddy" >
<cstring>opt_profile</cstring>
</property>
</widget>
</item>
<item row="3" column="1" >
<widget class="QComboBox" name="opt_profile" />
</item>
<item row="4" column="0" >
<widget class="QCheckBox" name="opt_dont_normalize" >
<property name="text" >
<string>Disable &amp;normalize</string>
</property>
</widget>
</item>
<item row="5" column="0" >
<widget class="QCheckBox" name="opt_keep_aspect_ratio" >
<property name="text" >
<string>Keep &amp;aspect ratio</string>
</property>
</widget>
</item>
<item row="6" column="0" >
<widget class="QCheckBox" name="opt_dont_sharpen" >
<property name="text" >
<string>Disable &amp;Sharpening</string>
</property>
</widget>
</item>
<item row="7" column="0" >
<widget class="QCheckBox" name="opt_landscape" >
<property name="text" >
<string>&amp;Landscape</string>
</property>
</widget>
</item>
<item row="8" column="0" >
<widget class="QCheckBox" name="opt_no_sort" >
<property name="text" >
<string>Dont so&amp;rt</string>
</property>
</widget>
</item>
<item row="9" column="1" >
<widget class="QDialogButtonBox" name="buttonBox" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons" >
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../images.qrc" />
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel" >
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel" >
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel" >
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel" >
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -37,6 +37,7 @@ from calibre.gui2.dialogs.lrf_single import LRFSingleDialog, LRFBulkDialog
from calibre.gui2.dialogs.config import ConfigDialog from calibre.gui2.dialogs.config import ConfigDialog
from calibre.gui2.dialogs.search import SearchDialog from calibre.gui2.dialogs.search import SearchDialog
from calibre.gui2.dialogs.user_profiles import UserProfiles from calibre.gui2.dialogs.user_profiles import UserProfiles
import calibre.gui2.dialogs.comicconf as ComicConf
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.gui2.dialogs.book_info import BookInfo from calibre.gui2.dialogs.book_info import BookInfo
from calibre.ebooks.metadata.meta import set_metadata from calibre.ebooks.metadata.meta import set_metadata
@ -173,11 +174,13 @@ class Main(MainWindow, Ui_MainWindow):
cm.addAction(_('Convert individually')) cm.addAction(_('Convert individually'))
cm.addAction(_('Bulk convert')) cm.addAction(_('Bulk convert'))
cm.addSeparator() cm.addSeparator()
cm.addAction(_('Set conversion defaults')) cm.addAction(_('Set defaults for conversion to LRF'))
cm.addAction(_('Set defaults for conversion of comics'))
self.action_convert.setMenu(cm) self.action_convert.setMenu(cm)
QObject.connect(cm.actions()[0], SIGNAL('triggered(bool)'), self.convert_single) QObject.connect(cm.actions()[0], SIGNAL('triggered(bool)'), self.convert_single)
QObject.connect(cm.actions()[1], SIGNAL('triggered(bool)'), self.convert_bulk) QObject.connect(cm.actions()[1], SIGNAL('triggered(bool)'), self.convert_bulk)
QObject.connect(cm.actions()[3], SIGNAL('triggered(bool)'), self.set_conversion_defaults) QObject.connect(cm.actions()[3], SIGNAL('triggered(bool)'), self.set_conversion_defaults)
QObject.connect(cm.actions()[4], SIGNAL('triggered(bool)'), self.set_comic_conversion_defaults)
QObject.connect(self.action_convert, SIGNAL('triggered(bool)'), self.convert_single) QObject.connect(self.action_convert, SIGNAL('triggered(bool)'), self.convert_single)
self.convert_menu = cm self.convert_menu = cm
self.tool_bar.widgetForAction(self.action_news).setPopupMode(QToolButton.InstantPopup) self.tool_bar.widgetForAction(self.action_news).setPopupMode(QToolButton.InstantPopup)
@ -763,12 +766,25 @@ class Main(MainWindow, Ui_MainWindow):
############################### Convert #################################### ############################### Convert ####################################
def convert_bulk(self, checked): def get_books_for_conversion(self):
rows = self.library_view.selectionModel().selectedRows() rows = [r.row() for r in self.library_view.selectionModel().selectedRows()]
if not rows or len(rows) == 0: if not rows or len(rows) == 0:
d = error_dialog(self, _('Cannot convert'), _('No books selected')) d = error_dialog(self, _('Cannot convert'), _('No books selected'))
d.exec_() d.exec_()
return return [], []
comics, others = [], []
db = self.library_view.model().db
for r in rows:
formats = db.formats(r)
if not formats: continue
formats = formats.lower().split(',')
if 'cbr' in formats or 'cbz' in formats:
comics.append(r)
else:
others.append(r)
return comics, others
def convert_bulk_others(self, rows):
d = LRFBulkDialog(self) d = LRFBulkDialog(self)
d.exec_() d.exec_()
if d.result() != QDialog.Accepted: if d.result() != QDialog.Accepted:
@ -813,34 +829,67 @@ class Main(MainWindow, Ui_MainWindow):
cmdline.append(pt.name) cmdline.append(pt.name)
id = self.job_manager.run_conversion_job(self.book_converted, id = self.job_manager.run_conversion_job(self.book_converted,
'any2lrf', args=[cmdline], 'any2lrf', args=[cmdline],
job_description='Convert book %d of %d'%(i+1, len(rows))) job_description=_('Convert book %d of %d (%s)')%(i+1, len(rows), repr(mi.title)))
self.conversion_jobs[id] = (d.cover_file, pt, of, d.output_format, self.conversion_jobs[id] = (d.cover_file, pt, of, d.output_format,
self.library_view.model().db.id(row)) self.library_view.model().db.id(row))
res = [] res = []
for row in bad_rows: for row in bad_rows:
title = self.library_view.model().db.title(row) title = self.library_view.model().db.title(row)
res.append('<li>%s</li>'%title) res.append('<li>%s</li>'%title)
if res: if res:
msg = '<p>Could not convert %d of %d books, because no suitable source format was found.<ul>%s</ul>'%(len(res), len(rows), '\n'.join(res)) msg = _('<p>Could not convert %d of %d books, because no suitable source format was found.<ul>%s</ul>')%(len(res), len(rows), '\n'.join(res))
warning_dialog(self, 'Could not convert some books', msg).exec_() warning_dialog(self, _('Could not convert some books'), msg).exec_()
def convert_bulk(self, checked):
comics, others = self.get_books_for_conversion()
if others:
self.convert_bulk_others(others)
if comics:
opts = ComicConf.get_bulk_conversion_options(self)
if opts:
for i, row in enumerate(comics):
options = opts.copy()
mi = self.library_view.model().db.get_metadata(row)
if mi.title:
options.title = mi.title
if mi.authors:
opts.author = ','.join(mi.authors)
data = None
for fmt in ['cbz', 'cbr']:
try:
data = self.library_view.model().db.format(row, fmt.upper())
break
except:
continue
pt = PersistentTemporaryFile('.'+fmt.lower())
pt.write(data)
pt.close()
of = PersistentTemporaryFile('.lrf')
of.close()
setattr(options, 'output', of.name)
options.verbose = 1
args = [pt.name, options]
id = self.job_manager.run_conversion_job(self.book_converted,
'comic2lrf', args=args,
job_description=_('Convert comic %d of %d (%s)')%(i+1, len(comics), repr(options.title)))
self.conversion_jobs[id] = (None, pt, of, 'lrf',
self.library_view.model().db.id(row))
def set_conversion_defaults(self, checked): def set_conversion_defaults(self, checked):
d = LRFSingleDialog(self, None, None) d = LRFSingleDialog(self, None, None)
d.exec_() d.exec_()
def convert_single(self, checked): def set_comic_conversion_defaults(self, checked):
rows = self.library_view.selectionModel().selectedRows() ComicConf.set_conversion_defaults(self)
if not rows or len(rows) == 0:
d = error_dialog(self, _('Cannot convert'), _('No books selected'))
d.exec_()
def convert_single_others(self, rows):
changed = False changed = False
for row in [r.row() for r in rows]: for row in rows:
d = LRFSingleDialog(self, self.library_view.model().db, row) d = LRFSingleDialog(self, self.library_view.model().db, row)
if d.selected_format: if d.selected_format:
d.exec_() d.exec_()
@ -856,7 +905,7 @@ class Main(MainWindow, Ui_MainWindow):
cmdline.append(pt.name) cmdline.append(pt.name)
id = self.job_manager.run_conversion_job(self.book_converted, id = self.job_manager.run_conversion_job(self.book_converted,
'any2lrf', args=[cmdline], 'any2lrf', args=[cmdline],
job_description='Convert book:'+d.title()) job_description=_('Convert book: ')+d.title())
self.conversion_jobs[id] = (d.cover_file, pt, of, d.output_format, d.id) self.conversion_jobs[id] = (d.cover_file, pt, of, d.output_format, d.id)
@ -866,6 +915,48 @@ class Main(MainWindow, Ui_MainWindow):
self.library_view.model().research() self.library_view.model().research()
def convert_single(self, checked):
comics, others = self.get_books_for_conversion()
if others:
self.convert_single_others(others)
changed = False
db = self.library_view.model().db
for row in comics:
mi = db.get_metadata(row)
title = author = _('Unknown')
if mi.title:
title = mi.title
if mi.authors:
author = ','.join(mi.authors)
defaults = db.conversion_options(db.id(row), 'comic')
opts, defaults = ComicConf.get_conversion_options(self, defaults, title, author)
if defaults is not None:
db.set_conversion_options(db.id(row), 'comic', defaults)
if opts is None: continue
for fmt in ['cbz', 'cbr']:
try:
data = db.format(row, fmt.upper())
break
except:
continue
pt = PersistentTemporaryFile('.'+fmt)
pt.write(data)
pt.close()
of = PersistentTemporaryFile('.lrf')
of.close()
opts.output = of.name
opts.verbose = 1
args = [pt.name, opts]
changed = True
id = self.job_manager.run_conversion_job(self.book_converted,
'comic2lrf', args=args,
job_description=_('Convert comic: ')+opts.title)
self.conversion_jobs[id] = (None, pt, of, 'lrf',
self.library_view.model().db.id(row))
if changed:
self.library_view.model().resort(reset=False)
self.library_view.model().research()
def book_converted(self, id, description, result, exception, formatted_traceback, log): def book_converted(self, id, description, result, exception, formatted_traceback, log):
of, fmt, book_id = self.conversion_jobs.pop(id)[2:] of, fmt, book_id = self.conversion_jobs.pop(id)[2:]
if exception: if exception:

View File

@ -1092,7 +1092,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
self.set_tags(id, val.split(','), append=False) self.set_tags(id, val.split(','), append=False)
def set_conversion_options(self, id, format, options): def set_conversion_options(self, id, format, options):
data = sqlite.Binary(cPickle.dumps(options)) data = sqlite.Binary(cPickle.dumps(options, -1))
oid = self.conn.execute('SELECT id FROM conversion_options WHERE book=? AND format=?', (id, format.upper())).fetchone() oid = self.conn.execute('SELECT id FROM conversion_options WHERE book=? AND format=?', (id, format.upper())).fetchone()
if oid: if oid:
self.conn.execute('UPDATE conversion_options SET data=? WHERE id=?', (data, oid[0])) self.conn.execute('UPDATE conversion_options SET data=? WHERE id=?', (data, oid[0]))

View File

@ -1,8 +1,7 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import shutil
''' Post installation script for linux ''' ''' Post installation script for linux '''
import sys, os, re import sys, os, re, shutil
from subprocess import check_call, call from subprocess import check_call, call
from calibre import __version__, __appname__ from calibre import __version__, __appname__

View File

@ -215,11 +215,12 @@ cd $frozen_path
''' '''
def extract_tarball(tar, destdir): def extract_tarball(tar, destdir):
print 'Extracting application files...'
if hasattr(tar, 'read'): if hasattr(tar, 'read'):
try: try:
tarfile.open(fileobj=tar, mode='r').extractall(destdir) tarfile.open(fileobj=tar, mode='r').extractall(destdir)
except: # tarfile.py on Fedora 9 is buggy except: # tarfile.py on Fedora 9 is buggy
subprocess.check_call(['tar', 'xvjf', tar.name, '-C', destdir]) subprocess.check_call(['tar', 'xjf', tar.name, '-C', destdir])
else: else:
tarfile.open(tar, 'r').extractall(destdir) tarfile.open(tar, 'r').extractall(destdir)
@ -242,6 +243,7 @@ def do_postinstall(destdir):
try: try:
os.chdir(destdir) os.chdir(destdir)
os.environ['LD_LIBRARY_PATH'] = destdir+':'+os.environ.get('LD_LIBRARY_PATH', '') os.environ['LD_LIBRARY_PATH'] = destdir+':'+os.environ.get('LD_LIBRARY_PATH', '')
os.environ['PYTHONPATH'] = destdir
subprocess.call((os.path.join(destdir, 'calibre_postinstall'), '--save-manifest-to', t.name)) subprocess.call((os.path.join(destdir, 'calibre_postinstall'), '--save-manifest-to', t.name))
finally: finally:
os.chdir(cwd) os.chdir(cwd)

View File

@ -48,6 +48,9 @@ PARALLEL_FUNCS = {
'render_table' : 'render_table' :
('calibre.ebooks.lrf.html.table_as_image', 'do_render', {}, None), ('calibre.ebooks.lrf.html.table_as_image', 'do_render', {}, None),
'comic2lrf' :
('calibre.ebooks.lrf.comic.convert_from', 'do_convert', {}, 'notification'),
} }

269
src/calibre/utils/config.py Normal file
View File

@ -0,0 +1,269 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Manage application-wide preferences.
'''
import os, re, cPickle
from copy import deepcopy
from PyQt4.QtCore import QString
from calibre import iswindows, isosx, OptionParser, ExclusiveFile, LockError
from collections import defaultdict
if iswindows:
from calibre import plugins
config_dir = plugins['winutil'][0].special_folder_path(plugins['winutil'][0].CSIDL_APPDATA)
if not os.access(config_dir, os.W_OK|os.X_OK):
config_dir = os.path.expanduser('~')
config_dir = os.path.join(config_dir, 'calibre')
elif isosx:
config_dir = os.path.expanduser('~/Library/Preferences/calibre')
else:
config_dir = os.path.expanduser('~/.config/calibre')
if not os.path.exists(config_dir):
os.makedirs(config_dir)
class Option(object):
def __init__(self, name, switches=[], help='', type=None, choices=None,
check=None, group=None, default=None, action=None, metavar=None):
if choices:
type = 'choice'
self.name = name
self.switches = switches
self.help = help.replace('%default', repr(default)) if help else None
self.type = type
self.choices = choices
self.check = check
self.group = group
self.default = default
self.action = action
self.metavar = metavar
def __eq__(self, other):
return self.name == getattr(other, 'name', None)
class OptionValues(object):
def copy(self):
return deepcopy(self)
class OptionSet(object):
OVERRIDE_PAT = re.compile(r'#{3,100} Override Options #{15}(.*?)#{3,100} End Override #{3,100}',
re.DOTALL|re.IGNORECASE)
def __init__(self, description=''):
self.description = description
self.preferences = []
self.group_list = []
self.groups = {}
self.set_buffer = {}
def has_option(self, name_or_option_object):
if name_or_option_object in self.preferences:
return True
for p in self.preferences:
if p.name == name_or_option_object:
return True
return False
def add_group(self, name, description=''):
if name in self.group_list:
raise ValueError('A group by the name %s already exists in this set'%name)
self.groups[name] = description
self.group_list.append(name)
def add_opt(self, name, switches=[], help=None, type=None, choices=None,
group=None, default=None, action=None, metavar=None):
'''
Add an option to this section.
:param name: The name of this option. Must be a valid Python identifier.
Must also be unique in this OptionSet and all its subsets.
:param switches: List of command line switches for this option
(as supplied to :module:`optparse`). If empty, this
option will not be added to the command line parser.
:param help: Help text.
:param type: Type checking of option values. Supported types are:
`None, 'choice', 'complex', 'float', 'int', 'long', 'string'`.
:param choices: List of strings or `None`.
:param group: Group this option belongs to. You must previously
have created this group with a call to :method:`add_group`.
:param default: The default value for this option.
:param action: The action to pass to optparse. Supported values are:
`None, 'count'`. For choices and boolean options,
action is automatically set correctly.
'''
pref = Option(name, switches=switches, help=help, type=type, choices=choices,
group=group, default=default, action=action, metavar=None)
if group is not None and group not in self.groups.keys():
raise ValueError('Group %s has not been added to this section'%group)
if pref in self.preferences:
raise ValueError('An option with the name %s already exists in this set.'%name)
self.preferences.append(pref)
def option_parser(self, user_defaults=None, usage='', gui_mode=False):
parser = OptionParser(usage, gui_mode=gui_mode)
groups = defaultdict(lambda : parser)
for group, desc in self.groups.items():
groups[group] = parser.add_group(group, desc)
for pref in self.preferences:
if not pref.switches:
continue
g = groups[pref.group]
action = pref.action
if action is None:
action = 'store'
if pref.default is True or pref.default is False:
action = 'store_' + ('false' if pref.default else 'true')
args = dict(
dest=pref.name,
help=pref.help,
metavar=pref.metavar,
type=pref.type,
choices=pref.choices,
default=getattr(user_defaults, pref.name, pref.default),
action=action,
)
g.add_option(*pref.switches, **args)
return parser
def get_override_section(self, src):
match = self.OVERRIDE_PAT.search(src)
if match:
return match.group()
return ''
def parse_string(self, src):
options = {'cPickle':cPickle}
if src is not None:
exec src in options
opts = OptionValues()
for pref in self.preferences:
setattr(opts, pref.name, options.get(pref.name, pref.default))
return opts
def render_group(self, name, desc, opts):
prefs = [pref for pref in self.preferences if pref.group == name]
lines = ['### Begin group: %s'%(name if name else 'DEFAULT')]
if desc:
lines += map(lambda x: '# '+x for x in desc.split('\n'))
lines.append(' ')
for pref in prefs:
lines.append('# '+pref.name.replace('_', ' '))
if pref.help:
lines += map(lambda x: '# ' + x, pref.help.split('\n'))
lines.append('%s = %s'%(pref.name,
self.serialize_opt(getattr(opts, pref.name, pref.default))))
lines.append(' ')
return '\n'.join(lines)
def serialize_opt(self, val):
if val is val is True or val is False or val is None or \
isinstance(val, (int, float, long, basestring)):
return repr(val)
if isinstance(val, QString):
return repr(unicode(val))
pickle = cPickle.dumps(val, -1)
return 'cPickle.loads(%s)'%repr(pickle)
def serialize(self, opts):
src = '# %s\n\n'%(self.description.replace('\n', '\n# '))
groups = [self.render_group(name, self.groups.get(name, ''), opts) \
for name in [None] + self.group_list]
return src + '\n\n'.join(groups)
class Config(object):
def __init__(self, basename, description=''):
self.config_file_path = os.path.join(config_dir, basename+'.py')
self.option_set = OptionSet(description=description)
self.add_opt = self.option_set.add_opt
self.add_group = self.option_set.add_group
def option_parser(self, usage='', gui_mode=False):
return self.option_set.option_parser(user_defaults=self.parse(),
usage=usage, gui_mode=gui_mode)
def parse(self):
try:
with ExclusiveFile(self.config_file_path) as f:
src = f.read()
except LockError:
raise IOError('Could not lock config file: %s'%self.config_file_path)
return self.option_set.parse_string(src)
def set(self, name, val):
if not self.option_set.has_option(name):
raise ValueError('The option %s is not defined.'%name)
try:
with ExclusiveFile(self.config_file_path) as f:
src = f.read()
opts = self.option_set.parse_string(src)
setattr(opts, name, val)
footer = self.option_set.get_override_section(src)
src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n'
f.seek(0)
f.truncate()
f.write(src)
except LockError:
raise IOError('Could not lock config file: %s'%self.config_file_path)
class StringConfig(object):
def __init__(self, src, description=''):
self.src = src
self.option_set = OptionSet(description=description)
self.add_opt = self.option_set.add_opt
self.option_parser = self.option_set.option_parser
def option_parser(self, usage='', gui_mode=False):
return self.option_set.option_parser(user_defaults=self.parse(),
usage=usage, gui_mode=gui_mode)
def parse(self):
return self.option_set.parse_string(self.src)
def set(self, name, val):
if not self.option_set.has_option(name):
raise ValueError('The option %s is not defined.'%name)
opts = self.option_set.parse_string(self.src)
setattr(opts, name, val)
footer = self.option_set.get_override_section(self.src)
self.src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n'
if __name__ == '__main__':
import subprocess
from PyQt4.Qt import QByteArray
c = Config('test', 'test config')
c.add_opt('one', ['-1', '--one'], help="This is option #1")
c.set('one', u'345')
c.add_opt('two', help="This is option #2")
c.set('two', 345)
c.add_opt('three', help="This is option #3")
c.set('three', QString(u'aflatoon'))
c.add_opt('four', help="This is option #4")
c.set('four', QByteArray('binary aflatoon'))
subprocess.call(['pygmentize', os.path.expanduser('~/.config/calibre/test.py')])
opts = c.parse()
for i in ('one', 'two', 'three', 'four'):
print i, repr(getattr(opts, i))