mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-11-21 22:13:04 -05:00
395 lines
15 KiB
Python
395 lines
15 KiB
Python
__license__ = 'GPL v3'
|
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|
|
|
import sys, os, time, socket, traceback
|
|
from functools import partial
|
|
|
|
from PyQt4.Qt import (QCoreApplication, QIcon, QObject, QTimer,
|
|
QPixmap, QSplashScreen, QApplication)
|
|
|
|
from calibre import prints, plugins, force_unicode
|
|
from calibre.constants import (iswindows, __appname__, isosx, DEBUG,
|
|
filesystem_encoding)
|
|
from calibre.utils.ipc import ADDRESS, RC
|
|
from calibre.gui2 import (ORG_NAME, APP_UID, initialize_file_icon_provider,
|
|
Application, choose_dir, error_dialog, question_dialog, gprefs)
|
|
from calibre.gui2.main_window import option_parser as _option_parser
|
|
from calibre.utils.config import prefs, dynamic
|
|
from calibre.library.database2 import LibraryDatabase2
|
|
from calibre.library.sqlite import sqlite, DatabaseException
|
|
|
|
if iswindows:
|
|
winutil = plugins['winutil'][0]
|
|
|
|
def option_parser():
|
|
parser = _option_parser('''\
|
|
%prog [opts] [path_to_ebook]
|
|
|
|
Launch the main calibre Graphical User Interface and optionally add the ebook at
|
|
path_to_ebook to the database.
|
|
''')
|
|
parser.add_option('--with-library', default=None, action='store',
|
|
help=_('Use the library located at the specified path.'))
|
|
parser.add_option('--start-in-tray', default=False, action='store_true',
|
|
help=_('Start minimized to system tray.'))
|
|
parser.add_option('-v', '--verbose', default=0, action='count',
|
|
help=_('Log debugging information to console'))
|
|
parser.add_option('--no-update-check', default=False, action='store_true',
|
|
help=_('Do not check for updates'))
|
|
parser.add_option('--ignore-plugins', default=False, action='store_true',
|
|
help=_('Ignore custom plugins, useful if you installed a plugin'
|
|
' that is preventing calibre from starting'))
|
|
parser.add_option('-s', '--shutdown-running-calibre', default=False,
|
|
action='store_true',
|
|
help=_('Cause a running calibre instance, if any, to be'
|
|
' shutdown. Note that if there are running jobs, they '
|
|
'will be silently aborted, so use with care.'))
|
|
return parser
|
|
|
|
def init_qt(args):
|
|
from calibre.gui2.ui import Main
|
|
parser = option_parser()
|
|
opts, args = parser.parse_args(args)
|
|
if opts.with_library is not None:
|
|
if not os.path.exists(opts.with_library):
|
|
os.makedirs(opts.with_library)
|
|
if os.path.isdir(opts.with_library):
|
|
prefs.set('library_path', os.path.abspath(opts.with_library))
|
|
prints('Using library at', prefs['library_path'])
|
|
QCoreApplication.setOrganizationName(ORG_NAME)
|
|
QCoreApplication.setApplicationName(APP_UID)
|
|
app = Application(args)
|
|
actions = tuple(Main.create_application_menubar())
|
|
app.setWindowIcon(QIcon(I('library.png')))
|
|
return app, opts, args, actions
|
|
|
|
|
|
def get_default_library_path():
|
|
fname = _('Calibre Library')
|
|
if iswindows:
|
|
fname = 'Calibre Library'
|
|
if isinstance(fname, unicode):
|
|
try:
|
|
fname = fname.encode(filesystem_encoding)
|
|
except:
|
|
fname = 'Calibre Library'
|
|
x = os.path.expanduser('~'+os.sep+fname)
|
|
if not os.path.exists(x):
|
|
try:
|
|
os.makedirs(x)
|
|
except:
|
|
x = os.path.expanduser('~')
|
|
return x
|
|
|
|
|
|
def get_library_path(parent=None):
|
|
library_path = prefs['library_path']
|
|
if library_path is None: # Need to migrate to new database layout
|
|
base = os.path.expanduser('~')
|
|
if iswindows:
|
|
base = winutil.special_folder_path(winutil.CSIDL_PERSONAL)
|
|
if not base or not os.path.exists(base):
|
|
from PyQt4.Qt import QDir
|
|
base = unicode(QDir.homePath()).replace('/', os.sep)
|
|
candidate = choose_dir(None, 'choose calibre library',
|
|
_('Choose a location for your calibre e-book library'),
|
|
default_dir=base)
|
|
if not candidate:
|
|
candidate = os.path.join(base, 'Calibre Library')
|
|
library_path = os.path.abspath(candidate)
|
|
if not os.path.exists(library_path):
|
|
try:
|
|
os.makedirs(library_path)
|
|
except:
|
|
error_dialog(parent, _('Failed to create library'),
|
|
_('Failed to create calibre library at: %r.')%library_path,
|
|
det_msg=traceback.format_exc(), show=True)
|
|
library_path = choose_dir(parent, 'choose calibre library',
|
|
_('Choose a location for your new calibre e-book library'),
|
|
default_dir=get_default_library_path())
|
|
return library_path
|
|
|
|
def repair_library(library_path):
|
|
from calibre.gui2.dialogs.restore_library import repair_library_at
|
|
return repair_library_at(library_path)
|
|
|
|
class GuiRunner(QObject):
|
|
'''Make sure an event loop is running before starting the main work of
|
|
initialization'''
|
|
|
|
def __init__(self, opts, args, actions, listener, app, gui_debug=None):
|
|
self.startup_time = time.time()
|
|
self.opts, self.args, self.listener, self.app = opts, args, listener, app
|
|
self.gui_debug = gui_debug
|
|
self.actions = actions
|
|
self.main = None
|
|
QObject.__init__(self)
|
|
self.splash_screen = None
|
|
self.timer = QTimer.singleShot(1, self.initialize)
|
|
if DEBUG:
|
|
prints('Starting up...')
|
|
|
|
def start_gui(self, db):
|
|
from calibre.gui2.ui import Main
|
|
main = Main(self.opts, gui_debug=self.gui_debug)
|
|
if self.splash_screen is not None:
|
|
self.splash_screen.showMessage(_('Initializing user interface...'))
|
|
main.initialize(self.library_path, db, self.listener, self.actions)
|
|
if self.splash_screen is not None:
|
|
self.splash_screen.finish(main)
|
|
if DEBUG:
|
|
prints('Started up in', time.time() - self.startup_time, 'with',
|
|
len(db.data), 'books')
|
|
add_filesystem_book = partial(main.iactions['Add Books'].add_filesystem_book, allow_device=False)
|
|
sys.excepthook = main.unhandled_exception
|
|
if len(self.args) > 1:
|
|
files = [os.path.abspath(p) for p in self.args[1:] if not
|
|
os.path.isdir(p)]
|
|
if len(files) < len(sys.argv[1:]):
|
|
prints('Ignoring directories passed as command line arguments')
|
|
add_filesystem_book(files)
|
|
self.app.file_event_hook = add_filesystem_book
|
|
self.main = main
|
|
|
|
def initialization_failed(self):
|
|
print 'Catastrophic failure initializing GUI, bailing out...'
|
|
QCoreApplication.exit(1)
|
|
raise SystemExit(1)
|
|
|
|
def initialize_db_stage2(self, db, tb):
|
|
|
|
if db is None and tb is not None:
|
|
# DB Repair failed
|
|
error_dialog(self.splash_screen, _('Repairing failed'),
|
|
_('The database repair failed. Starting with '
|
|
'a new empty library.'),
|
|
det_msg=tb, show=True)
|
|
if db is None:
|
|
candidate = choose_dir(self.splash_screen, 'choose calibre library',
|
|
_('Choose a location for your new calibre e-book library'),
|
|
default_dir=get_default_library_path())
|
|
if not candidate:
|
|
self.initialization_failed()
|
|
|
|
try:
|
|
self.library_path = candidate
|
|
db = LibraryDatabase2(candidate)
|
|
except:
|
|
error_dialog(self.splash_screen, _('Bad database location'),
|
|
_('Bad database location %r. calibre will now quit.'
|
|
)%self.library_path,
|
|
det_msg=traceback.format_exc(), show=True)
|
|
self.initialization_failed()
|
|
|
|
self.start_gui(db)
|
|
|
|
def initialize_db(self):
|
|
db = None
|
|
try:
|
|
db = LibraryDatabase2(self.library_path)
|
|
except (sqlite.Error, DatabaseException):
|
|
repair = question_dialog(self.splash_screen, _('Corrupted database'),
|
|
_('The library database at %s appears to be corrupted. Do '
|
|
'you want calibre to try and rebuild it automatically? '
|
|
'The rebuild may not be completely successful. '
|
|
'If you say No, a new empty calibre library will be created.')
|
|
% force_unicode(self.library_path, filesystem_encoding),
|
|
det_msg=traceback.format_exc()
|
|
)
|
|
if repair:
|
|
if repair_library(self.library_path):
|
|
db = LibraryDatabase2(self.library_path)
|
|
except:
|
|
error_dialog(self.splash_screen, _('Bad database location'),
|
|
_('Bad database location %r. Will start with '
|
|
' a new, empty calibre library')%self.library_path,
|
|
det_msg=traceback.format_exc(), show=True)
|
|
|
|
self.initialize_db_stage2(db, None)
|
|
|
|
def show_splash_screen(self):
|
|
self.splash_pixmap = QPixmap()
|
|
self.splash_pixmap.load(I('library.png'))
|
|
self.splash_screen = QSplashScreen(self.splash_pixmap)
|
|
self.splash_screen.showMessage(_('Starting %s: Loading books...') %
|
|
__appname__)
|
|
self.splash_screen.show()
|
|
QApplication.instance().processEvents()
|
|
|
|
def initialize(self, *args):
|
|
if gprefs['show_splash_screen']:
|
|
self.show_splash_screen()
|
|
|
|
self.library_path = get_library_path(parent=self.splash_screen)
|
|
if not self.library_path:
|
|
self.initialization_failed()
|
|
|
|
self.initialize_db()
|
|
|
|
def run_in_debug_mode(logpath=None):
|
|
e = sys.executable if getattr(sys, 'frozen', False) else sys.argv[0]
|
|
import tempfile, subprocess
|
|
fd, logpath = tempfile.mkstemp('.txt')
|
|
os.close(fd)
|
|
|
|
if hasattr(sys, 'frameworks_dir'):
|
|
base = os.path.dirname(sys.frameworks_dir)
|
|
if 'console.app' not in base:
|
|
base = os.path.join(base, 'console.app', 'Contents')
|
|
exe = os.path.basename(e)
|
|
exe = os.path.join(base, 'MacOS', exe+'-debug')
|
|
else:
|
|
base, ext = os.path.splitext(e)
|
|
exe = base + '-debug' + ext
|
|
print 'Starting debug executable:', exe
|
|
creationflags = 0
|
|
if iswindows:
|
|
import win32process
|
|
creationflags = win32process.CREATE_NO_WINDOW
|
|
subprocess.Popen([exe, '--gui-debug', logpath], stdout=open(logpath, 'w'),
|
|
stderr=subprocess.STDOUT, stdin=open(os.devnull, 'r'),
|
|
creationflags=creationflags)
|
|
|
|
def run_gui(opts, args, actions, listener, app, gui_debug=None):
|
|
initialize_file_icon_provider()
|
|
if not dynamic.get('welcome_wizard_was_run', False):
|
|
from calibre.gui2.wizard import wizard
|
|
wizard().exec_()
|
|
dynamic.set('welcome_wizard_was_run', True)
|
|
runner = GuiRunner(opts, args, actions, listener, app, gui_debug=gui_debug)
|
|
ret = app.exec_()
|
|
if getattr(runner.main, 'run_wizard_b4_shutdown', False):
|
|
from calibre.gui2.wizard import wizard
|
|
wizard().exec_()
|
|
if getattr(runner.main, 'restart_after_quit', False):
|
|
e = sys.executable if getattr(sys, 'frozen', False) else sys.argv[0]
|
|
if getattr(runner.main, 'debug_on_restart', False):
|
|
run_in_debug_mode()
|
|
else:
|
|
import subprocess
|
|
print 'Restarting with:', e, sys.argv
|
|
if hasattr(sys, 'frameworks_dir'):
|
|
app = os.path.dirname(os.path.dirname(sys.frameworks_dir))
|
|
subprocess.Popen('sleep 3s; open '+app, shell=True)
|
|
else:
|
|
subprocess.Popen([e] + sys.argv[1:])
|
|
else:
|
|
if iswindows:
|
|
try:
|
|
runner.main.system_tray_icon.hide()
|
|
except:
|
|
pass
|
|
if getattr(runner.main, 'gui_debug', None) is not None:
|
|
e = sys.executable if getattr(sys, 'frozen', False) else sys.argv[0]
|
|
import subprocess
|
|
creationflags = 0
|
|
if iswindows:
|
|
import win32process
|
|
creationflags = win32process.CREATE_NO_WINDOW
|
|
subprocess.Popen([e, '--show-gui-debug', runner.main.gui_debug],
|
|
creationflags=creationflags, stdout=open(os.devnull, 'w'),
|
|
stderr=subprocess.PIPE, stdin=open(os.devnull, 'r'))
|
|
return ret
|
|
|
|
def cant_start(msg=_('If you are sure it is not running')+', ',
|
|
what=None):
|
|
base = '<p>%s</p><p>%s %s'
|
|
where = __appname__ + ' '+_('may be running in the system tray, in the')+' '
|
|
if isosx:
|
|
where += _('upper right region of the screen.')
|
|
else:
|
|
where += _('lower right region of the screen.')
|
|
if what is None:
|
|
if iswindows:
|
|
what = _('try rebooting your computer.')
|
|
else:
|
|
what = _('try deleting the file')+': '+ADDRESS
|
|
|
|
info = base%(where, msg, what)
|
|
error_dialog(None, _('Cannot Start ')+__appname__,
|
|
'<p>'+(_('%s is already running.')%__appname__)+'</p>'+info, show=True)
|
|
|
|
raise SystemExit(1)
|
|
|
|
def communicate(opts, args):
|
|
t = RC()
|
|
t.start()
|
|
time.sleep(3)
|
|
if not t.done:
|
|
f = os.path.expanduser('~/.calibre_calibre GUI.lock')
|
|
cant_start(what=_('try deleting the file')+': '+f)
|
|
raise SystemExit(1)
|
|
|
|
if opts.shutdown_running_calibre:
|
|
t.conn.send('shutdown:')
|
|
else:
|
|
if len(args) > 1:
|
|
args[1] = os.path.abspath(args[1])
|
|
t.conn.send('launched:'+repr(args))
|
|
t.conn.close()
|
|
raise SystemExit(0)
|
|
|
|
|
|
def main(args=sys.argv):
|
|
gui_debug = None
|
|
if args[0] == '__CALIBRE_GUI_DEBUG__':
|
|
gui_debug = args[1]
|
|
args = ['calibre']
|
|
|
|
app, opts, args, actions = init_qt(args)
|
|
from calibre.utils.lock import singleinstance
|
|
from multiprocessing.connection import Listener
|
|
si = singleinstance('calibre GUI')
|
|
if si and opts.shutdown_running_calibre:
|
|
return 0
|
|
if si:
|
|
try:
|
|
listener = Listener(address=ADDRESS)
|
|
except socket.error:
|
|
if iswindows:
|
|
cant_start()
|
|
if os.path.exists(ADDRESS):
|
|
os.remove(ADDRESS)
|
|
try:
|
|
listener = Listener(address=ADDRESS)
|
|
except socket.error:
|
|
cant_start()
|
|
else:
|
|
return run_gui(opts, args, actions, listener, app,
|
|
gui_debug=gui_debug)
|
|
else:
|
|
return run_gui(opts, args, actions, listener, app,
|
|
gui_debug=gui_debug)
|
|
otherinstance = False
|
|
try:
|
|
listener = Listener(address=ADDRESS)
|
|
except socket.error: # Good si is correct (on UNIX)
|
|
otherinstance = True
|
|
else:
|
|
# On windows only singleinstance can be trusted
|
|
otherinstance = True if iswindows else False
|
|
if not otherinstance and not opts.shutdown_running_calibre:
|
|
return run_gui(opts, args, actions, listener, app, gui_debug=gui_debug)
|
|
|
|
communicate(opts, args)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
sys.exit(main())
|
|
except Exception as err:
|
|
if not iswindows: raise
|
|
tb = traceback.format_exc()
|
|
from PyQt4.QtGui import QErrorMessage
|
|
logfile = os.path.join(os.path.expanduser('~'), 'calibre.log')
|
|
if os.path.exists(logfile):
|
|
log = open(logfile).read().decode('utf-8', 'ignore')
|
|
d = QErrorMessage()
|
|
d.showMessage(('<b>Error:</b>%s<br><b>Traceback:</b><br>'
|
|
'%s<b>Log:</b><br>%s')%(unicode(err),
|
|
unicode(tb).replace('\n', '<br>'),
|
|
log.replace('\n', '<br>')))
|
|
|