mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Clean up GUI initialization and add support for restoring corrupted databases automatically
This commit is contained in:
parent
68a3256258
commit
14adce229b
@ -40,7 +40,7 @@ Run an embedded python interpreter.
|
||||
|
||||
return parser
|
||||
|
||||
def reinit_db(dbpath):
|
||||
def reinit_db(dbpath, callback=None):
|
||||
if not os.path.exists(dbpath):
|
||||
raise ValueError(dbpath + ' does not exist')
|
||||
from calibre.library.sqlite import connect
|
||||
@ -50,15 +50,26 @@ def reinit_db(dbpath):
|
||||
uv = conn.get('PRAGMA user_version;', all=False)
|
||||
conn.execute('PRAGMA writable_schema=ON')
|
||||
conn.commit()
|
||||
sql = conn.dump()
|
||||
sql_lines = conn.dump()
|
||||
conn.close()
|
||||
dest = dbpath + '.tmp'
|
||||
try:
|
||||
with closing(connect(dest, False)) as nconn:
|
||||
nconn.execute('create temporary table temp_sequence(id INTEGER PRIMARY KEY AUTOINCREMENT)')
|
||||
nconn.commit()
|
||||
nconn.executescript(sql)
|
||||
nconn.commit()
|
||||
if callable(callback):
|
||||
callback(len(sql_lines), True)
|
||||
for i, line in enumerate(sql_lines):
|
||||
try:
|
||||
nconn.execute(line)
|
||||
except:
|
||||
import traceback
|
||||
prints('SQL line %r failed with error:'%line)
|
||||
prints(traceback.format_exc())
|
||||
continue
|
||||
finally:
|
||||
if callable(callback):
|
||||
callback(i, False)
|
||||
nconn.execute('pragma user_version=%d'%int(uv))
|
||||
nconn.commit()
|
||||
os.remove(dbpath)
|
||||
|
@ -410,6 +410,7 @@ class FileDialog(QObject):
|
||||
modal = True,
|
||||
name = '',
|
||||
mode = QFileDialog.ExistingFiles,
|
||||
default_dir='~'
|
||||
):
|
||||
QObject.__init__(self)
|
||||
ftext = ''
|
||||
@ -428,9 +429,10 @@ class FileDialog(QObject):
|
||||
self.selected_files = None
|
||||
self.fd = None
|
||||
|
||||
initial_dir = dynamic.get(self.dialog_name, os.path.expanduser('~'))
|
||||
initial_dir = dynamic.get(self.dialog_name,
|
||||
os.path.expanduser(default_dir))
|
||||
if not isinstance(initial_dir, basestring):
|
||||
initial_dir = os.path.expanduser('~')
|
||||
initial_dir = os.path.expanduser(default_dir)
|
||||
self.selected_files = []
|
||||
if mode == QFileDialog.AnyFile:
|
||||
f = unicode(QFileDialog.getSaveFileName(parent, title, initial_dir, ftext, ""))
|
||||
@ -465,9 +467,10 @@ class FileDialog(QObject):
|
||||
return tuple(self.selected_files)
|
||||
|
||||
|
||||
def choose_dir(window, name, title):
|
||||
fd = FileDialog(title, [], False, window, name=name,
|
||||
mode=QFileDialog.DirectoryOnly)
|
||||
def choose_dir(window, name, title, default_dir='~'):
|
||||
fd = FileDialog(title=title, filters=[], add_all_files_filter=False,
|
||||
parent=window, name=name, mode=QFileDialog.DirectoryOnly,
|
||||
default_dir=default_dir)
|
||||
dir = fd.get_files()
|
||||
if dir:
|
||||
return dir[0]
|
||||
|
@ -4,15 +4,18 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
import sys, os, time, socket, traceback
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import QCoreApplication, QIcon, QMessageBox
|
||||
from PyQt4.Qt import QCoreApplication, QIcon, QMessageBox, QObject, QTimer, \
|
||||
QThread, pyqtSignal, Qt, QProgressDialog, QString
|
||||
|
||||
from calibre import prints
|
||||
from calibre.constants import iswindows, __appname__, isosx
|
||||
from calibre import prints, plugins
|
||||
from calibre.constants import iswindows, __appname__, isosx, filesystem_encoding
|
||||
from calibre.utils.ipc import ADDRESS, RC
|
||||
from calibre.gui2 import ORG_NAME, APP_UID, initialize_file_icon_provider, \
|
||||
Application
|
||||
Application, choose_dir, error_dialog, question_dialog
|
||||
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
|
||||
|
||||
def option_parser():
|
||||
parser = _option_parser('''\
|
||||
@ -48,25 +51,186 @@ def init_qt(args):
|
||||
app.setWindowIcon(QIcon(I('library.png')))
|
||||
return app, opts, args, actions
|
||||
|
||||
def get_library_path():
|
||||
library_path = prefs['library_path']
|
||||
if library_path is None: # Need to migrate to new database layout
|
||||
base = os.path.expanduser('~')
|
||||
if iswindows:
|
||||
base = plugins['winutil'][0].special_folder_path(
|
||||
plugins['winutil'][0].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(None, _('Failed to create library'),
|
||||
_('Failed to create calibre library at: %r. Aborting.')%library_path,
|
||||
det_msg = traceback.print_exc(), show=True)
|
||||
library_path = None
|
||||
return library_path
|
||||
|
||||
class DBRepair(QThread):
|
||||
|
||||
repair_done = pyqtSignal(object, object)
|
||||
progress = pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, library_path, parent, pd):
|
||||
QThread.__init__(self, parent)
|
||||
self.library_path = library_path
|
||||
self.pd = pd
|
||||
self.progress.connect(self._callback, type=Qt.QueuedConnection)
|
||||
|
||||
def _callback(self, num, is_length):
|
||||
if is_length:
|
||||
self.pd.setRange(0, num-1)
|
||||
num = 0
|
||||
self.pd.setValue(num)
|
||||
|
||||
def callback(self, num, is_length):
|
||||
self.progress.emit(num, is_length)
|
||||
|
||||
def run(self):
|
||||
from calibre.debug import reinit_db
|
||||
try:
|
||||
reinit_db(os.path.join(self.library_path, 'metadata.db'),
|
||||
self.callback)
|
||||
db = LibraryDatabase2(self.library_path)
|
||||
tb = None
|
||||
except:
|
||||
db, tb = None, traceback.format_exc()
|
||||
self.repair_done.emit(db, tb)
|
||||
|
||||
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):
|
||||
self.opts, self.args, self.listener, self.app = opts, args, listener, app
|
||||
self.actions = actions
|
||||
self.main = None
|
||||
QObject.__init__(self)
|
||||
self.timer = QTimer.singleShot(1, self.initialize)
|
||||
|
||||
def start_gui(self):
|
||||
from calibre.gui2.ui import Main
|
||||
main = Main(self.library_path, self.db, self.listener, self.opts, self.actions)
|
||||
add_filesystem_book = partial(main.add_filesystem_book, allow_device=False)
|
||||
sys.excepthook = main.unhandled_exception
|
||||
if len(self.args) > 1:
|
||||
p = os.path.abspath(self.args[1])
|
||||
add_filesystem_book(p)
|
||||
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):
|
||||
repair_pd = getattr(self, 'repair_pd', None)
|
||||
if repair_pd is not None:
|
||||
repair_pd.cancel()
|
||||
|
||||
if db is None and tb is not None:
|
||||
# DB Repair failed
|
||||
error_dialog(None, _('Repairing failed'),
|
||||
_('The database repair failed. Starting with '
|
||||
'a new empty library.'),
|
||||
det_msg=tb, show=True)
|
||||
if db is None:
|
||||
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('~')
|
||||
candidate = choose_dir(None, 'choose calibre library',
|
||||
_('Choose a location for your new calibre e-book library'),
|
||||
default_dir=x)
|
||||
|
||||
if not candidate:
|
||||
self.initialization_failed()
|
||||
|
||||
try:
|
||||
self.library_path = candidate
|
||||
db = LibraryDatabase2(candidate)
|
||||
except:
|
||||
error_dialog(None, _('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.db = db
|
||||
self.start_gui()
|
||||
|
||||
def initialize_db(self):
|
||||
db = None
|
||||
try:
|
||||
db = LibraryDatabase2(self.library_path)
|
||||
except (sqlite.Error, DatabaseException):
|
||||
repair = question_dialog(None, _('Corrupted database'),
|
||||
_('Your calibre database appears to be corrupted. Do '
|
||||
'you want calibre to try and repair it automatically? '
|
||||
'If you say No, a new empty calibre library will be created.'),
|
||||
det_msg=traceback.format_exc()
|
||||
)
|
||||
if repair:
|
||||
self.repair_pd = QProgressDialog(_('Repairing database. This '
|
||||
'can take a very long time for a large collection'), QString(),
|
||||
0, 0)
|
||||
self.repair_pd.setWindowModality(Qt.WindowModal)
|
||||
self.repair_pd.show()
|
||||
|
||||
self.repair = DBRepair(self.library_path, self, self.repair_pd)
|
||||
self.repair.repair_done.connect(self.initialize_db_stage2,
|
||||
type=Qt.QueuedConnection)
|
||||
self.repair.start()
|
||||
return
|
||||
except:
|
||||
error_dialog(None, _('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 initialize(self, *args):
|
||||
self.library_path = get_library_path()
|
||||
if self.library_path is None:
|
||||
self.initialization_failed()
|
||||
|
||||
self.initialize_db()
|
||||
|
||||
|
||||
|
||||
def run_gui(opts, args, actions, listener, app):
|
||||
from calibre.gui2.ui import Main
|
||||
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)
|
||||
main = Main(listener, opts, actions)
|
||||
add_filesystem_book = partial(main.add_filesystem_book, allow_device=False)
|
||||
sys.excepthook = main.unhandled_exception
|
||||
if len(args) > 1:
|
||||
args[1] = os.path.abspath(args[1])
|
||||
add_filesystem_book(args[1])
|
||||
app.file_event_hook = add_filesystem_book
|
||||
runner = GuiRunner(opts, args, actions, listener, app)
|
||||
ret = app.exec_()
|
||||
if getattr(main, 'run_wizard_b4_shutdown', False):
|
||||
if getattr(runner.main, 'run_wizard_b4_shutdown', False):
|
||||
from calibre.gui2.wizard import wizard
|
||||
wizard().exec_()
|
||||
if getattr(main, 'restart_after_quit', False):
|
||||
if getattr(runner.main, 'restart_after_quit', False):
|
||||
e = sys.executable if getattr(sys, 'frozen', False) else sys.argv[0]
|
||||
print 'Restarting with:', e, sys.argv
|
||||
if hasattr(sys, 'frameworks_dir'):
|
||||
@ -78,7 +242,7 @@ def run_gui(opts, args, actions, listener, app):
|
||||
else:
|
||||
if iswindows:
|
||||
try:
|
||||
main.system_tray_icon.hide()
|
||||
runner.main.system_tray_icon.hide()
|
||||
except:
|
||||
pass
|
||||
return ret
|
||||
|
@ -14,9 +14,9 @@ from xml.parsers.expat import ExpatError
|
||||
from Queue import Queue, Empty
|
||||
from threading import Thread
|
||||
from functools import partial
|
||||
from PyQt4.Qt import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer, \
|
||||
from PyQt4.Qt import Qt, SIGNAL, QObject, QUrl, QTimer, \
|
||||
QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
|
||||
QToolButton, QDialog, QDesktopServices, QFileDialog, \
|
||||
QToolButton, QDialog, QDesktopServices, \
|
||||
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
||||
QMessageBox, QStackedLayout, QHelpEvent, QInputDialog,\
|
||||
QThread, pyqtSignal
|
||||
@ -125,8 +125,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
self.default_thumbnail = (pixmap.width(), pixmap.height(),
|
||||
pixmap_to_data(pixmap))
|
||||
|
||||
def __init__(self, listener, opts, actions, parent=None):
|
||||
def __init__(self, library_path, db, listener, opts, actions, parent=None):
|
||||
self.preferences_action, self.quit_action = actions
|
||||
self.library_path = library_path
|
||||
self.spare_servers = []
|
||||
MainWindow.__init__(self, opts, parent)
|
||||
# Initialize fontconfig in a separate thread as this can be a lengthy
|
||||
@ -513,31 +514,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
if self.system_tray_icon.isVisible() and opts.start_in_tray:
|
||||
self.hide_windows()
|
||||
self.stack.setCurrentIndex(0)
|
||||
try:
|
||||
db = LibraryDatabase2(self.library_path)
|
||||
except Exception:
|
||||
import traceback
|
||||
error_dialog(self, _('Bad database location'),
|
||||
_('Bad database location')+':'+self.library_path,
|
||||
det_msg=traceback.format_exc()).exec_()
|
||||
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):
|
||||
os.makedirs(x)
|
||||
dir = unicode(QFileDialog.getExistingDirectory(self,
|
||||
_('Choose a location for your ebook library.'),
|
||||
x))
|
||||
if not dir:
|
||||
QCoreApplication.exit(1)
|
||||
raise SystemExit(1)
|
||||
else:
|
||||
self.library_path = dir
|
||||
db = LibraryDatabase2(self.library_path)
|
||||
self.library_view.set_database(db)
|
||||
prefs['library_path'] = self.library_path
|
||||
self.library_view.sortByColumn(*dynamic.get('sort_column',
|
||||
@ -2330,38 +2306,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
d.show()
|
||||
self._modeless_dialogs.append(d)
|
||||
|
||||
|
||||
def initialize_database(self):
|
||||
self.library_path = prefs['library_path']
|
||||
if self.library_path is None: # Need to migrate to new database layout
|
||||
base = os.path.expanduser('~')
|
||||
if iswindows:
|
||||
from calibre import plugins
|
||||
from PyQt4.Qt import QDir
|
||||
base = plugins['winutil'][0].special_folder_path(
|
||||
plugins['winutil'][0].CSIDL_PERSONAL)
|
||||
if not base or not os.path.exists(base):
|
||||
base = unicode(QDir.homePath()).replace('/', os.sep)
|
||||
dir = unicode(QFileDialog.getExistingDirectory(self,
|
||||
_('Choose a location for your ebook library.'), base))
|
||||
if not dir:
|
||||
dir = os.path.expanduser('~/Library')
|
||||
self.library_path = os.path.abspath(dir)
|
||||
if not os.path.exists(self.library_path):
|
||||
try:
|
||||
os.makedirs(self.library_path)
|
||||
except:
|
||||
self.library_path = os.path.expanduser('~/CalibreLibrary')
|
||||
error_dialog(self, _('Invalid library location'),
|
||||
_('Could not access %s. Using %s as the library.')%
|
||||
(repr(self.library_path), repr(self.library_path))
|
||||
).exec_()
|
||||
if not os.path.exists(self.library_path):
|
||||
os.makedirs(self.library_path)
|
||||
|
||||
|
||||
def read_settings(self):
|
||||
self.initialize_database()
|
||||
geometry = config['main_window_geometry']
|
||||
if geometry is not None:
|
||||
self.restoreGeometry(geometry)
|
||||
|
@ -1399,7 +1399,7 @@ books_series_link feeds
|
||||
def check_integrity(self, callback):
|
||||
callback(0., _('Checking SQL integrity...'))
|
||||
user_version = self.user_version
|
||||
sql = self.conn.dump()
|
||||
sql = '\n'.join(self.conn.dump())
|
||||
self.conn.close()
|
||||
dest = self.dbpath+'.tmp'
|
||||
if os.path.exists(dest):
|
||||
|
@ -116,7 +116,7 @@ class DBThread(Thread):
|
||||
break
|
||||
if func == 'dump':
|
||||
try:
|
||||
ok, res = True, '\n'.join(self.conn.iterdump())
|
||||
ok, res = True, tuple(self.conn.iterdump())
|
||||
except Exception, err:
|
||||
ok, res = False, (err, traceback.format_exc())
|
||||
else:
|
||||
|
Loading…
x
Reference in New Issue
Block a user