mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Initial implementation of file system based ebook database. Tested on linux.
This commit is contained in:
parent
48930e7847
commit
3ad9cc9ef2
@ -13,6 +13,7 @@ RESOURCES = dict(
|
|||||||
opf_template = '%p/ebooks/metadata/opf.xml',
|
opf_template = '%p/ebooks/metadata/opf.xml',
|
||||||
ncx_template = '%p/ebooks/metadata/ncx.xml',
|
ncx_template = '%p/ebooks/metadata/ncx.xml',
|
||||||
fb2_xsl = '%p/ebooks/lrf/fb2/fb2.xsl',
|
fb2_xsl = '%p/ebooks/lrf/fb2/fb2.xsl',
|
||||||
|
metadata_sqlite = '%p/library/metadata_sqlite.sql',
|
||||||
)
|
)
|
||||||
|
|
||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
|
@ -15,7 +15,7 @@ from optparse import OptionParser as _OptionParser
|
|||||||
from optparse import IndentedHelpFormatter
|
from optparse import IndentedHelpFormatter
|
||||||
from logging import Formatter
|
from logging import Formatter
|
||||||
|
|
||||||
from PyQt4.QtCore import QSettings, QVariant, QUrl
|
from PyQt4.QtCore import QSettings, QVariant, QUrl, QByteArray
|
||||||
from PyQt4.QtGui import QDesktopServices
|
from PyQt4.QtGui import QDesktopServices
|
||||||
|
|
||||||
from calibre.translations.msgfmt import make
|
from calibre.translations.msgfmt import make
|
||||||
@ -448,7 +448,7 @@ class Settings(QSettings):
|
|||||||
|
|
||||||
def set(self, key, val):
|
def set(self, key, val):
|
||||||
val = cPickle.dumps(val, -1)
|
val = cPickle.dumps(val, -1)
|
||||||
self.setValue(str(key), QVariant(val))
|
self.setValue(str(key), QVariant(QByteArray(val)))
|
||||||
|
|
||||||
_settings = Settings()
|
_settings = Settings()
|
||||||
if not _settings.get('migrated from QSettings'):
|
if not _settings.get('migrated from QSettings'):
|
||||||
|
@ -24,11 +24,8 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
self.db = db
|
self.db = db
|
||||||
self.current_cols = columns
|
self.current_cols = columns
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
path = qstring_to_unicode(\
|
path = settings.get('library path')
|
||||||
settings.value("database path",
|
self.location.setText(path)
|
||||||
QVariant(os.path.join(os.path.expanduser('~'),'library1.db'))).toString())
|
|
||||||
|
|
||||||
self.location.setText(os.path.dirname(path))
|
|
||||||
self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse)
|
self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse)
|
||||||
self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact)
|
self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact)
|
||||||
|
|
||||||
@ -94,10 +91,12 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
settings.setValue('filename pattern', QVariant(pattern))
|
settings.setValue('filename pattern', QVariant(pattern))
|
||||||
|
|
||||||
if not path or not os.path.exists(path) or not os.path.isdir(path):
|
if not path or not os.path.exists(path) or not os.path.isdir(path):
|
||||||
d = error_dialog(self, _('Invalid database location'), _('Invalid database location ')+path+_('<br>Must be a directory.'))
|
d = error_dialog(self, _('Invalid database location'),
|
||||||
|
_('Invalid database location ')+path+_('<br>Must be a directory.'))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
elif not os.access(path, os.W_OK):
|
elif not os.access(path, os.W_OK):
|
||||||
d = error_dialog(self, _('Invalid database location'), _('Invalid database location.<br>Cannot write to ')+path)
|
d = error_dialog(self, _('Invalid database location'),
|
||||||
|
_('Invalid database location.<br>Cannot write to ')+path)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
else:
|
else:
|
||||||
self.database_location = os.path.abspath(path)
|
self.database_location = os.path.abspath(path)
|
||||||
|
@ -81,8 +81,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>583</width>
|
<width>595</width>
|
||||||
<height>625</height>
|
<height>638</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" >
|
<layout class="QGridLayout" >
|
||||||
@ -91,7 +91,10 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label" >
|
<widget class="QLabel" name="label" >
|
||||||
<property name="text" >
|
<property name="text" >
|
||||||
<string>&Location of books database (library1.db)</string>
|
<string>&Location of ebooks (The ebooks are stored in folders sorted by author and metadata is stored in the file metadata.db)</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap" >
|
||||||
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="buddy" >
|
<property name="buddy" >
|
||||||
<cstring>location</cstring>
|
<cstring>location</cstring>
|
||||||
|
@ -3,15 +3,14 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
import os, textwrap, traceback, time
|
import os, textwrap, traceback, time
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from collections import deque
|
|
||||||
from math import cos, sin, pi
|
from math import cos, sin, pi
|
||||||
from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \
|
from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \
|
||||||
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
|
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
|
||||||
QPen, QStyle, QPainter, QLineEdit, QApplication, \
|
QPen, QStyle, QPainter, QLineEdit, QApplication, \
|
||||||
QPalette, QImage
|
QPalette, QImage
|
||||||
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
|
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
|
||||||
QCoreApplication, SIGNAL, QObject, QSize, QModelIndex, \
|
QCoreApplication, SIGNAL, QObject, QSize, QModelIndex
|
||||||
QTimer
|
|
||||||
|
|
||||||
from calibre import Settings, preferred_encoding
|
from calibre import Settings, preferred_encoding
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
@ -94,7 +93,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
num -= d
|
num -= d
|
||||||
return ''.join(result)
|
return ''.join(result)
|
||||||
|
|
||||||
def __init__(self, parent=None, buffer=20):
|
def __init__(self, parent=None, buffer=40):
|
||||||
QAbstractTableModel.__init__(self, parent)
|
QAbstractTableModel.__init__(self, parent)
|
||||||
self.db = None
|
self.db = None
|
||||||
self.cols = ['title', 'authors', 'size', 'date', 'rating', 'publisher', 'tags', 'series']
|
self.cols = ['title', 'authors', 'size', 'date', 'rating', 'publisher', 'tags', 'series']
|
||||||
@ -104,14 +103,11 @@ class BooksModel(QAbstractTableModel):
|
|||||||
self.last_search = '' # The last search performed on this model
|
self.last_search = '' # The last search performed on this model
|
||||||
self.read_config()
|
self.read_config()
|
||||||
self.buffer_size = buffer
|
self.buffer_size = buffer
|
||||||
self.clear_caches()
|
self.cover_cache = None
|
||||||
self.load_timer = QTimer()
|
|
||||||
self.connect(self.load_timer, SIGNAL('timeout()'), self.load)
|
|
||||||
self.load_timer.start(50)
|
|
||||||
|
|
||||||
def clear_caches(self):
|
def clear_caches(self):
|
||||||
self.buffer = {}
|
if self.cover_cache:
|
||||||
self.load_queue = deque()
|
self.cover_cache.clear_cache()
|
||||||
|
|
||||||
def read_config(self):
|
def read_config(self):
|
||||||
self.use_roman_numbers = bool(Settings().value('use roman numerals for series number',
|
self.use_roman_numbers = bool(Settings().value('use roman numerals for series number',
|
||||||
@ -204,18 +200,6 @@ class BooksModel(QAbstractTableModel):
|
|||||||
def count(self):
|
def count(self):
|
||||||
return self.rowCount(None)
|
return self.rowCount(None)
|
||||||
|
|
||||||
def load(self):
|
|
||||||
if self.load_queue:
|
|
||||||
index = self.load_queue.popleft()
|
|
||||||
if self.buffer.has_key(index):
|
|
||||||
return
|
|
||||||
data = self.db.cover(index)
|
|
||||||
img = QImage()
|
|
||||||
img.loadFromData(data)
|
|
||||||
if img.isNull():
|
|
||||||
img = self.default_image
|
|
||||||
self.buffer[index] = img
|
|
||||||
|
|
||||||
def get_book_display_info(self, idx):
|
def get_book_display_info(self, idx):
|
||||||
data = {}
|
data = {}
|
||||||
cdata = self.cover(idx)
|
cdata = self.cover(idx)
|
||||||
@ -245,17 +229,22 @@ class BooksModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def set_cache(self, idx):
|
||||||
|
l, r = 0, self.count()-1
|
||||||
|
if self.cover_cache:
|
||||||
|
l = max(l, idx-self.buffer_size)
|
||||||
|
r = min(r, idx+self.buffer_size)
|
||||||
|
k = min(r-idx, idx-l)
|
||||||
|
ids = [idx]
|
||||||
|
for i in range(1, k):
|
||||||
|
ids.extend([idx-i, idx+i])
|
||||||
|
ids = ids + [i for i in range(l, r, 1) if i not in ids]
|
||||||
|
ids = [self.db.id(i) for i in ids]
|
||||||
|
self.cover_cache.set_cache(ids)
|
||||||
|
|
||||||
def current_changed(self, current, previous, emit_signal=True):
|
def current_changed(self, current, previous, emit_signal=True):
|
||||||
|
|
||||||
idx = current.row()
|
idx = current.row()
|
||||||
|
self.set_cache(idx)
|
||||||
for key in self.buffer.keys():
|
|
||||||
if abs(key - idx) > self.buffer_size:
|
|
||||||
self.buffer.pop(key)
|
|
||||||
for i in range(max(0, idx-self.buffer_size), min(self.count(), idx+self.buffer_size)):
|
|
||||||
if not self.buffer.has_key(i):
|
|
||||||
self.load_queue.append(i)
|
|
||||||
|
|
||||||
data = self.get_book_display_info(idx)
|
data = self.get_book_display_info(idx)
|
||||||
if emit_signal:
|
if emit_signal:
|
||||||
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
|
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
|
||||||
@ -333,14 +322,22 @@ class BooksModel(QAbstractTableModel):
|
|||||||
return self.db.title(row_number)
|
return self.db.title(row_number)
|
||||||
|
|
||||||
def cover(self, row_number):
|
def cover(self, row_number):
|
||||||
img = self.buffer.get(row_number, -1)
|
id = self.db.id(row_number)
|
||||||
if img == -1:
|
data = None
|
||||||
|
if self.cover_cache:
|
||||||
|
img = self.cover_cache.cover(id)
|
||||||
|
if img:
|
||||||
|
if img.isNull():
|
||||||
|
img = self.default_image
|
||||||
|
return img
|
||||||
|
if not data:
|
||||||
data = self.db.cover(row_number)
|
data = self.db.cover(row_number)
|
||||||
|
if not data:
|
||||||
|
return self.default_image
|
||||||
img = QImage()
|
img = QImage()
|
||||||
img.loadFromData(data)
|
img.loadFromData(data)
|
||||||
if img.isNull():
|
if img.isNull():
|
||||||
img = self.default_image
|
img = self.default_image
|
||||||
self.buffer[row_number] = img
|
|
||||||
return img
|
return img
|
||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
import os, sys, textwrap, collections, traceback, shutil, time
|
import os, sys, textwrap, collections, traceback, time
|
||||||
from xml.parsers.expat import ExpatError
|
from xml.parsers.expat import ExpatError
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \
|
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \
|
||||||
@ -44,7 +44,7 @@ from calibre.ebooks.metadata.meta import set_metadata
|
|||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.ebooks.lrf import preferred_source_formats as LRF_PREFERRED_SOURCE_FORMATS
|
from calibre.ebooks.lrf import preferred_source_formats as LRF_PREFERRED_SOURCE_FORMATS
|
||||||
|
from calibre.library.database2 import LibraryDatabase2, CoverCache
|
||||||
|
|
||||||
|
|
||||||
class Main(MainWindow, Ui_MainWindow):
|
class Main(MainWindow, Ui_MainWindow):
|
||||||
@ -178,7 +178,6 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'), self.do_advanced_search)
|
QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'), self.do_advanced_search)
|
||||||
|
|
||||||
####################### Library view ########################
|
####################### Library view ########################
|
||||||
self.library_view.set_database(self.database_path)
|
|
||||||
QObject.connect(self.library_view, SIGNAL('files_dropped(PyQt_PyObject)'),
|
QObject.connect(self.library_view, SIGNAL('files_dropped(PyQt_PyObject)'),
|
||||||
self.files_dropped)
|
self.files_dropped)
|
||||||
for func, target in [
|
for func, target in [
|
||||||
@ -193,12 +192,26 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
self.show()
|
self.show()
|
||||||
self.stack.setCurrentIndex(0)
|
self.stack.setCurrentIndex(0)
|
||||||
self.library_view.migrate_database()
|
db = LibraryDatabase2(self.library_path)
|
||||||
|
self.library_view.set_database(db)
|
||||||
|
if self.olddb is not None:
|
||||||
|
from PyQt4.QtGui import QProgressDialog
|
||||||
|
pd = QProgressDialog('', '', 0, 100, self)
|
||||||
|
pd.setWindowModality(Qt.ApplicationModal)
|
||||||
|
pd.setCancelButton(None)
|
||||||
|
pd.setWindowTitle(_('Migrating database'))
|
||||||
|
pd.show()
|
||||||
|
db.migrate_old(self.olddb, pd)
|
||||||
|
self.olddb = None
|
||||||
|
Settings().set('library path', self.library_path)
|
||||||
self.library_view.sortByColumn(3, Qt.DescendingOrder)
|
self.library_view.sortByColumn(3, Qt.DescendingOrder)
|
||||||
if not self.library_view.restore_column_widths():
|
if not self.library_view.restore_column_widths():
|
||||||
self.library_view.resizeColumnsToContents()
|
self.library_view.resizeColumnsToContents()
|
||||||
self.library_view.resizeRowsToContents()
|
self.library_view.resizeRowsToContents()
|
||||||
self.search.setFocus(Qt.OtherFocusReason)
|
self.search.setFocus(Qt.OtherFocusReason)
|
||||||
|
self.cover_cache = CoverCache(self.library_path)
|
||||||
|
self.cover_cache.start()
|
||||||
|
self.library_view.model().cover_cache = self.cover_cache
|
||||||
########################### Cover Flow ################################
|
########################### Cover Flow ################################
|
||||||
self.cover_flow = None
|
self.cover_flow = None
|
||||||
if CoverFlow is not None:
|
if CoverFlow is not None:
|
||||||
@ -949,37 +962,26 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
self.tool_bar.setIconSize(settings.value('toolbar icon size', QVariant(QSize(48, 48))).toSize())
|
self.tool_bar.setIconSize(settings.value('toolbar icon size', QVariant(QSize(48, 48))).toSize())
|
||||||
self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if settings.get('show text in toolbar', True) else Qt.ToolButtonIconOnly)
|
self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if settings.get('show text in toolbar', True) else Qt.ToolButtonIconOnly)
|
||||||
|
|
||||||
if os.path.dirname(self.database_path) != d.database_location:
|
if self.library_path != d.database_location:
|
||||||
try:
|
try:
|
||||||
newloc = os.path.join(d.database_location, os.path.basename(self.database_path))
|
newloc = d.database_location
|
||||||
if not os.path.exists(newloc):
|
if not os.path.exists(os.path.join(newloc, 'metadata.db')):
|
||||||
dirname = os.path.dirname(newloc)
|
if os.access(self.library_path, os.R_OK):
|
||||||
if not os.path.isdir(dirname):
|
self.status_bar.showMessage(_('Copying library to ')+newloc)
|
||||||
os.makedirs(dirname)
|
|
||||||
dest = open(newloc, 'wb')
|
|
||||||
if os.access(self.database_path, os.R_OK):
|
|
||||||
self.status_bar.showMessage(_('Copying database to ')+newloc)
|
|
||||||
self.setCursor(Qt.BusyCursor)
|
self.setCursor(Qt.BusyCursor)
|
||||||
self.library_view.setEnabled(False)
|
self.library_view.setEnabled(False)
|
||||||
self.library_view.close()
|
self.library_view.model().db.move_library_to(newloc)
|
||||||
src = open(self.database_path, 'rb')
|
|
||||||
shutil.copyfileobj(src, dest)
|
|
||||||
src.close()
|
|
||||||
dest.close()
|
|
||||||
os.unlink(self.database_path)
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
db = LibraryDatabase(newloc)
|
db = LibraryDatabase2(newloc)
|
||||||
db.close()
|
self.library_view.set_database(db)
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
d = error_dialog(self, _('Invalid database'),
|
d = error_dialog(self, _('Invalid database'),
|
||||||
_('<p>An invalid database already exists at %s, delete it before trying to move the existing database.<br>Error: %s')%(newloc, str(err)))
|
_('<p>An invalid database already exists at %s, delete it before trying to move the existing database.<br>Error: %s')%(newloc, str(err)))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
newloc = self.database_path
|
self.library_path = self.library_view.model().db.library_path
|
||||||
self.database_path = newloc
|
Settings().set('library path', self.library_path)
|
||||||
settings = Settings()
|
|
||||||
settings.setValue("database path", QVariant(self.database_path))
|
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
d = error_dialog(self, _('Could not move database'), unicode(err))
|
d = error_dialog(self, _('Could not move database'), unicode(err))
|
||||||
@ -990,7 +992,6 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
self.status_bar.clearMessage()
|
self.status_bar.clearMessage()
|
||||||
self.search.clear_to_help()
|
self.search.clear_to_help()
|
||||||
self.status_bar.reset_info()
|
self.status_bar.reset_info()
|
||||||
self.library_view.set_database(self.database_path)
|
|
||||||
self.library_view.sortByColumn(3, Qt.DescendingOrder)
|
self.library_view.sortByColumn(3, Qt.DescendingOrder)
|
||||||
self.library_view.resizeRowsToContents()
|
self.library_view.resizeRowsToContents()
|
||||||
if hasattr(d, 'directories'):
|
if hasattr(d, 'directories'):
|
||||||
@ -1085,12 +1086,10 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
ConversionErrorDialog(self, 'Conversion Error', msg, show=True)
|
ConversionErrorDialog(self, 'Conversion Error', msg, show=True)
|
||||||
|
|
||||||
|
|
||||||
def read_settings(self):
|
def initialize_database(self, settings):
|
||||||
settings = Settings()
|
self.library_path = settings.get('library path', None)
|
||||||
settings.beginGroup("Main Window")
|
self.olddb = None
|
||||||
geometry = settings.value('main window geometry', QVariant()).toByteArray()
|
if self.library_path is None: # Need to migrate to new database layout
|
||||||
self.restoreGeometry(geometry)
|
|
||||||
settings.endGroup()
|
|
||||||
dbpath = os.path.join(os.path.expanduser('~'), 'library1.db').decode(sys.getfilesystemencoding())
|
dbpath = os.path.join(os.path.expanduser('~'), 'library1.db').decode(sys.getfilesystemencoding())
|
||||||
self.database_path = qstring_to_unicode(settings.value("database path",
|
self.database_path = qstring_to_unicode(settings.value("database path",
|
||||||
QVariant(QString.fromUtf8(dbpath.encode('utf-8')))).toString())
|
QVariant(QString.fromUtf8(dbpath.encode('utf-8')))).toString())
|
||||||
@ -1103,6 +1102,25 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
os.makedirs(self.database_path)
|
os.makedirs(self.database_path)
|
||||||
self.database_path = os.path.join(self.database_path, 'library1.db')
|
self.database_path = os.path.join(self.database_path, 'library1.db')
|
||||||
settings.setValue('database path', QVariant(QString.fromUtf8(self.database_path.encode('utf-8'))))
|
settings.setValue('database path', QVariant(QString.fromUtf8(self.database_path.encode('utf-8'))))
|
||||||
|
home = os.path.dirname(self.database_path)
|
||||||
|
if not os.path.exists(home):
|
||||||
|
home = os.getcwd()
|
||||||
|
from PyQt4.QtGui import QFileDialog
|
||||||
|
dir = qstring_to_unicode(QFileDialog.getExistingDirectory(self, _('Choose a location for your ebook library.'), home))
|
||||||
|
if not dir:
|
||||||
|
dir = os.path.dirname(self.database_path)
|
||||||
|
self.library_path = os.path.abspath(dir)
|
||||||
|
self.olddb = LibraryDatabase(self.database_path)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def read_settings(self):
|
||||||
|
settings = Settings()
|
||||||
|
settings.beginGroup("Main Window")
|
||||||
|
geometry = settings.value('main window geometry', QVariant()).toByteArray()
|
||||||
|
self.restoreGeometry(geometry)
|
||||||
|
settings.endGroup()
|
||||||
|
self.initialize_database(settings)
|
||||||
set_sidebar_directories(None)
|
set_sidebar_directories(None)
|
||||||
set_filename_pat(qstring_to_unicode(settings.value('filename pattern', QVariant(get_filename_pat())).toString()))
|
set_filename_pat(qstring_to_unicode(settings.value('filename pattern', QVariant(get_filename_pat())).toString()))
|
||||||
self.tool_bar.setIconSize(settings.value('toolbar icon size', QVariant(QSize(48, 48))).toSize())
|
self.tool_bar.setIconSize(settings.value('toolbar icon size', QVariant(QSize(48, 48))).toSize())
|
||||||
@ -1138,9 +1156,11 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
self.job_manager.terminate_all_jobs()
|
self.job_manager.terminate_all_jobs()
|
||||||
self.write_settings()
|
self.write_settings()
|
||||||
self.detector.keep_going = False
|
self.detector.keep_going = False
|
||||||
|
self.cover_cache.stop()
|
||||||
self.hide()
|
self.hide()
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
self.detector.terminate()
|
self.detector.terminate()
|
||||||
|
self.cover_cache.terminate()
|
||||||
e.accept()
|
e.accept()
|
||||||
|
|
||||||
def update_found(self, version):
|
def update_found(self, version):
|
||||||
|
@ -10,26 +10,26 @@ Command line interface to the calibre database.
|
|||||||
import sys, os
|
import sys, os
|
||||||
from textwrap import TextWrapper
|
from textwrap import TextWrapper
|
||||||
|
|
||||||
from PyQt4.QtCore import QVariant
|
|
||||||
|
|
||||||
from calibre import OptionParser, Settings, terminal_controller, preferred_encoding
|
from calibre import OptionParser, Settings, terminal_controller, preferred_encoding
|
||||||
from calibre.gui2 import SingleApplication
|
from calibre.gui2 import SingleApplication
|
||||||
from calibre.ebooks.metadata.meta import get_metadata
|
from calibre.ebooks.metadata.meta import get_metadata
|
||||||
from calibre.library.database import LibraryDatabase, text_to_tokens
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
|
from calibre.library.database import text_to_tokens
|
||||||
|
|
||||||
FIELDS = set(['title', 'authors', 'publisher', 'rating', 'timestamp', 'size', 'tags', 'comments', 'series', 'series_index', 'formats'])
|
FIELDS = set(['title', 'authors', 'publisher', 'rating', 'timestamp', 'size', 'tags', 'comments', 'series', 'series_index', 'formats'])
|
||||||
|
|
||||||
def get_parser(usage):
|
def get_parser(usage):
|
||||||
parser = OptionParser(usage)
|
parser = OptionParser(usage)
|
||||||
go = parser.add_option_group('GLOBAL OPTIONS')
|
go = parser.add_option_group('GLOBAL OPTIONS')
|
||||||
go.add_option('--database', default=None, help=_('Path to the calibre database. Default is to use the path stored in the settings.'))
|
go.add_option('--library-path', default=None, help=_('Path to the calibre library. Default is to use the path stored in the settings.'))
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def get_db(dbpath, options):
|
def get_db(dbpath, options):
|
||||||
if options.database is not None:
|
if options.library_path is not None:
|
||||||
dbpath = options.database
|
dbpath = options.library_path
|
||||||
dbpath = os.path.abspath(dbpath)
|
dbpath = os.path.abspath(dbpath)
|
||||||
return LibraryDatabase(dbpath, row_factory=True)
|
print _('Using library at'), dbpath
|
||||||
|
return LibraryDatabase2(dbpath, row_factory=True)
|
||||||
|
|
||||||
def do_list(db, fields, sort_by, ascending, search_text):
|
def do_list(db, fields, sort_by, ascending, search_text):
|
||||||
db.refresh(sort_by, ascending)
|
db.refresh(sort_by, ascending)
|
||||||
@ -330,7 +330,7 @@ For help on an individual command: %%prog command --help
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
command = eval('command_'+args[1])
|
command = eval('command_'+args[1])
|
||||||
dbpath = unicode(Settings().value('database path', QVariant(os.path.expanduser('~/library1.db'))).toString())
|
dbpath = Settings().get('library path', os.path.expanduser('~'))
|
||||||
|
|
||||||
return command(args[2:], dbpath)
|
return command(args[2:], dbpath)
|
||||||
|
|
||||||
|
@ -794,14 +794,6 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
global _lock_file
|
|
||||||
import os
|
|
||||||
if _lock_file is not None:
|
|
||||||
_lock_file.close()
|
|
||||||
if os.path.exists(_lock_file.name):
|
|
||||||
os.unlink(_lock_file.name)
|
|
||||||
|
|
||||||
def __init__(self, dbpath, row_factory=False):
|
def __init__(self, dbpath, row_factory=False):
|
||||||
self.dbpath = dbpath
|
self.dbpath = dbpath
|
||||||
self.conn = _connect(dbpath)
|
self.conn = _connect(dbpath)
|
||||||
|
523
src/calibre/library/database2.py
Normal file
523
src/calibre/library/database2.py
Normal file
@ -0,0 +1,523 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
'''
|
||||||
|
The database used to store ebook metadata
|
||||||
|
'''
|
||||||
|
import os, re, sys, shutil, cStringIO, glob, collections
|
||||||
|
import sqlite3 as sqlite
|
||||||
|
from itertools import repeat
|
||||||
|
|
||||||
|
from PyQt4.QtCore import QCoreApplication, QThread, QReadWriteLock
|
||||||
|
from PyQt4.QtGui import QApplication, QPixmap, QImage
|
||||||
|
__app = None
|
||||||
|
|
||||||
|
from calibre.library.database import LibraryDatabase
|
||||||
|
|
||||||
|
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
||||||
|
filesystem_encoding = sys.getfilesystemencoding()
|
||||||
|
if filesystem_encoding is None: filesystem_encoding = 'utf-8'
|
||||||
|
|
||||||
|
_filename_sanitize = re.compile(r'[\0\\|\?\*<":>\+\[\]/]')
|
||||||
|
def sanitize_file_name(name, substitute='_'):
|
||||||
|
'''
|
||||||
|
Sanitize the filename `name`. All invalid characters are replaced by `substitute`.
|
||||||
|
The set of invalid characters is the union of the invalid characters in Windows,
|
||||||
|
OS X and Linux.
|
||||||
|
**WARNING:** This function also replaces path separators, so only pass file names
|
||||||
|
and not full paths to it.
|
||||||
|
*NOTE:* This function always returns byte strings, not unicode objects. The byte strings
|
||||||
|
are encoded in the filesystem encoding of the platform, or UTF-8.
|
||||||
|
'''
|
||||||
|
if isinstance(name, unicode):
|
||||||
|
name = name.encode(filesystem_encoding, 'ignore')
|
||||||
|
return _filename_sanitize.sub(substitute, name)
|
||||||
|
|
||||||
|
class CoverCache(QThread):
|
||||||
|
|
||||||
|
def __init__(self, library_path, parent=None):
|
||||||
|
QThread.__init__(self, parent)
|
||||||
|
self.library_path = library_path
|
||||||
|
self.id_map = None
|
||||||
|
self.id_map_lock = QReadWriteLock()
|
||||||
|
self.load_queue = collections.deque()
|
||||||
|
self.load_queue_lock = QReadWriteLock(QReadWriteLock.Recursive)
|
||||||
|
self.cache = {}
|
||||||
|
self.cache_lock = QReadWriteLock()
|
||||||
|
self.keep_running = True
|
||||||
|
|
||||||
|
def build_id_map(self):
|
||||||
|
self.id_map_lock.lockForWrite()
|
||||||
|
self.id_map = {}
|
||||||
|
for f in glob.glob(os.path.join(self.library_path, '*', '* (*)', 'cover.jpg')):
|
||||||
|
c = os.path.basename(os.path.dirname(f))
|
||||||
|
try:
|
||||||
|
id = int(re.search(r'\((\d+)\)', c[c.rindex('('):]).group(1))
|
||||||
|
self.id_map[id] = f
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
self.id_map_lock.unlock()
|
||||||
|
|
||||||
|
|
||||||
|
def set_cache(self, ids):
|
||||||
|
self.cache_lock.lockForWrite()
|
||||||
|
already_loaded = set([])
|
||||||
|
for id in self.cache.keys():
|
||||||
|
if id in ids:
|
||||||
|
already_loaded.add(id)
|
||||||
|
else:
|
||||||
|
self.cache.pop(id)
|
||||||
|
self.cache_lock.unlock()
|
||||||
|
ids = [i for i in ids if i not in already_loaded]
|
||||||
|
self.load_queue_lock.lockForWrite()
|
||||||
|
self.load_queue = collections.deque(ids)
|
||||||
|
self.load_queue_lock.unlock()
|
||||||
|
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while self.keep_running:
|
||||||
|
if self.id_map is None:
|
||||||
|
self.build_id_map()
|
||||||
|
while True:
|
||||||
|
self.load_queue_lock.lockForWrite()
|
||||||
|
try:
|
||||||
|
id = self.load_queue.popleft()
|
||||||
|
except IndexError:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
self.load_queue_lock.unlock()
|
||||||
|
|
||||||
|
self.cache_lock.lockForRead()
|
||||||
|
need = True
|
||||||
|
if id in self.cache.keys():
|
||||||
|
need = False
|
||||||
|
self.cache_lock.unlock()
|
||||||
|
if not need:
|
||||||
|
continue
|
||||||
|
path = None
|
||||||
|
self.id_map_lock.lockForRead()
|
||||||
|
if id in self.id_map.keys():
|
||||||
|
path = self.id_map[id]
|
||||||
|
self.id_map_lock.unlock()
|
||||||
|
if path and os.access(path, os.R_OK):
|
||||||
|
try:
|
||||||
|
img = QImage()
|
||||||
|
data = open(path, 'rb').read()
|
||||||
|
img.loadFromData(data)
|
||||||
|
if img.isNull():
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
self.cache_lock.lockForWrite()
|
||||||
|
self.cache[id] = img
|
||||||
|
self.cache_lock.unlock()
|
||||||
|
|
||||||
|
self.sleep(1)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.keep_running = False
|
||||||
|
|
||||||
|
def cover(self, id):
|
||||||
|
val = None
|
||||||
|
if self.cache_lock.tryLockForRead(50):
|
||||||
|
val = self.cache.get(id, None)
|
||||||
|
self.cache_lock.unlock()
|
||||||
|
return val
|
||||||
|
|
||||||
|
def clear_cache(self):
|
||||||
|
self.cache_lock.lockForWrite()
|
||||||
|
self.cache = {}
|
||||||
|
self.cache_lock.unlock()
|
||||||
|
|
||||||
|
class Concatenate(object):
|
||||||
|
'''String concatenation aggregator for sqlite'''
|
||||||
|
def __init__(self, sep=','):
|
||||||
|
self.sep = sep
|
||||||
|
self.ans = ''
|
||||||
|
|
||||||
|
def step(self, value):
|
||||||
|
if value is not None:
|
||||||
|
self.ans += value + self.sep
|
||||||
|
|
||||||
|
def finalize(self):
|
||||||
|
if not self.ans:
|
||||||
|
return None
|
||||||
|
if self.sep:
|
||||||
|
return self.ans[:-len(self.sep)]
|
||||||
|
return self.ans
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryDatabase2(LibraryDatabase):
|
||||||
|
'''
|
||||||
|
An ebook metadata database that stores references to ebook files on disk.
|
||||||
|
'''
|
||||||
|
PATH_LIMIT = 40 if 'win32' in sys.platform else 100
|
||||||
|
@apply
|
||||||
|
def user_version():
|
||||||
|
doc = 'The user version of this database'
|
||||||
|
def fget(self):
|
||||||
|
return self.conn.execute('pragma user_version;').next()[0]
|
||||||
|
def fset(self, val):
|
||||||
|
self.conn.execute('pragma user_version=%d'%int(val))
|
||||||
|
self.conn.commit()
|
||||||
|
return property(doc=doc, fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
if 'win32' in sys.platform and len(self.library_path) + 4*self.PATH_LIMIT + 10 > 259:
|
||||||
|
raise ValueError('Path to library too long. Must be less than %d characters.'%(259-4*self.PATH_LIMIT-10))
|
||||||
|
exists = os.path.exists(self.dbpath)
|
||||||
|
self.conn = sqlite.connect(self.dbpath,
|
||||||
|
detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES)
|
||||||
|
if exists and self.user_version == 0:
|
||||||
|
self.conn.close()
|
||||||
|
os.remove(self.dbpath)
|
||||||
|
self.conn = sqlite.connect(self.dbpath,
|
||||||
|
detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES)
|
||||||
|
self.conn.row_factory = sqlite.Row if self.row_factory else lambda cursor, row : list(row)
|
||||||
|
self.conn.create_aggregate('concat', 1, Concatenate)
|
||||||
|
title_pat = re.compile('^(A|The|An)\s+', re.IGNORECASE)
|
||||||
|
|
||||||
|
def title_sort(title):
|
||||||
|
match = title_pat.search(title)
|
||||||
|
if match:
|
||||||
|
prep = match.group(1)
|
||||||
|
title = title.replace(prep, '') + ', ' + prep
|
||||||
|
return title.strip()
|
||||||
|
|
||||||
|
self.conn.create_function('title_sort', 1, title_sort)
|
||||||
|
if self.user_version == 0:
|
||||||
|
self.initialize_database()
|
||||||
|
|
||||||
|
def __init__(self, library_path, row_factory=False):
|
||||||
|
if not os.path.exists(library_path):
|
||||||
|
os.makedirs(library_path)
|
||||||
|
self.library_path = os.path.abspath(library_path)
|
||||||
|
self.row_factory = row_factory
|
||||||
|
self.dbpath = os.path.join(library_path, 'metadata.db')
|
||||||
|
if isinstance(self.dbpath, unicode):
|
||||||
|
self.dbpath = self.dbpath.encode(filesystem_encoding)
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_database(self):
|
||||||
|
from calibre.resources import metadata_sqlite
|
||||||
|
self.conn.executescript(metadata_sqlite)
|
||||||
|
self.user_version = 1
|
||||||
|
|
||||||
|
def path(self, index, index_is_id=False):
|
||||||
|
'Return the relative path to the directory containing this books files as a unicode string.'
|
||||||
|
id = index if index_is_id else self.id()
|
||||||
|
path = self.conn.execute('SELECT path FROM books WHERE id=?', (id,)).fetchone()[0].replace('/', os.sep)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def set_path(self, index, index_is_id=False):
|
||||||
|
'''
|
||||||
|
Set the path to the directory containing this books files based on its
|
||||||
|
current title and author. If there was a previous directory, its contents
|
||||||
|
are copied and it is deleted.
|
||||||
|
'''
|
||||||
|
id = index if index_is_id else self.id(index)
|
||||||
|
authors = self.authors(id, index_is_id=True)
|
||||||
|
if not authors:
|
||||||
|
authors = _('Unknown')
|
||||||
|
author = sanitize_file_name(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding)
|
||||||
|
title = sanitize_file_name(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding)
|
||||||
|
path = author + '/' + title + ' (%d)'%id
|
||||||
|
current_path = self.path(id, index_is_id=True).replace(os.sep, '/')
|
||||||
|
if path == current_path:
|
||||||
|
return
|
||||||
|
tpath = os.path.join(self.library_path, *path.split('/'))
|
||||||
|
if not os.path.exists(tpath):
|
||||||
|
os.makedirs(tpath)
|
||||||
|
spath = os.path.join(self.library_path, *current_path.split('/'))
|
||||||
|
if current_path and os.path.exists(spath):
|
||||||
|
for f in os.listdir(spath):
|
||||||
|
copyfile(os.path.join(spath, f), os.path.join(tpath, f))
|
||||||
|
self.conn.execute('UPDATE books SET path=? WHERE id=?', (path, id))
|
||||||
|
self.conn.commit()
|
||||||
|
if current_path and os.path.exists(spath):
|
||||||
|
shutil.rmtree(spath)
|
||||||
|
|
||||||
|
def cover(self, index, index_is_id=False, as_file=False, as_image=False):
|
||||||
|
'''
|
||||||
|
Return the cover image as a bytestring (in JPEG format) or None.
|
||||||
|
|
||||||
|
`as_file` : If True return the image as an open file object
|
||||||
|
`as_image`: If True return the image as a QImage object
|
||||||
|
'''
|
||||||
|
id = index if index_is_id else self.id(index)
|
||||||
|
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
||||||
|
if os.access(path, os.R_OK):
|
||||||
|
f = open(path, 'rb')
|
||||||
|
if as_image:
|
||||||
|
img = QImage()
|
||||||
|
img.loadFromData(f.read())
|
||||||
|
return img
|
||||||
|
return f if as_file else f.read()
|
||||||
|
|
||||||
|
def set_cover(self, id, data):
|
||||||
|
'''
|
||||||
|
Set the cover for this book.
|
||||||
|
|
||||||
|
`data`: Can be either a QImage, QPixmap, file object or bytestring
|
||||||
|
'''
|
||||||
|
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
||||||
|
if callable(getattr(data, 'save', None)):
|
||||||
|
data.save(path)
|
||||||
|
else:
|
||||||
|
if not QCoreApplication.instance():
|
||||||
|
global __app
|
||||||
|
__app = QApplication([])
|
||||||
|
p = QPixmap()
|
||||||
|
if callable(getattr(data, 'read', None)):
|
||||||
|
data = data.read()
|
||||||
|
p.loadFromData(data)
|
||||||
|
p.save(path)
|
||||||
|
|
||||||
|
def format(self, index, format, index_is_id=False, as_file=False, mode='r+b'):
|
||||||
|
'''
|
||||||
|
Return the ebook format as a bytestring or `None` if the format doesn't exist,
|
||||||
|
or we don't have permission to write to the ebook file.
|
||||||
|
|
||||||
|
`as_file`: If True the ebook format is returned as a file object opened in `mode`
|
||||||
|
'''
|
||||||
|
id = index if index_is_id else self.id(index)
|
||||||
|
path = os.path.join(self.library_path, self.path(id, index_is_id=True))
|
||||||
|
name = self.conn.execute('SELECT name FROM data WHERE book=? AND format=?', (id, format)).fetchone()[0]
|
||||||
|
if name:
|
||||||
|
format = ('.' + format.lower()) if format else ''
|
||||||
|
path = os.path.join(path, name+format)
|
||||||
|
if os.access(path, os.R_OK|os.W_OK):
|
||||||
|
f = open(path, mode)
|
||||||
|
return f if as_file else f.read()
|
||||||
|
|
||||||
|
def add_format(self, index, format, stream, index_is_id=False):
|
||||||
|
id = index if index_is_id else self.id(index)
|
||||||
|
authors = self.authors(id, index_is_id=True)
|
||||||
|
if not authors:
|
||||||
|
authors = _('Unknown')
|
||||||
|
path = os.path.join(self.library_path, self.path(id, index_is_id=True))
|
||||||
|
author = sanitize_file_name(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding)
|
||||||
|
title = sanitize_file_name(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding)
|
||||||
|
name = self.conn.execute('SELECT name FROM data WHERE book=? AND format=?', (id, format)).fetchone()
|
||||||
|
if name:
|
||||||
|
self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, format))
|
||||||
|
name = title + ' - ' + author
|
||||||
|
ext = ('.' + format.lower()) if format else ''
|
||||||
|
shutil.copyfileobj(stream, open(os.path.join(path, name+ext), 'wb'))
|
||||||
|
stream.seek(0, 2)
|
||||||
|
size=stream.tell()
|
||||||
|
self.conn.execute('INSERT INTO data (book,format,uncompressed_size,name) VALUES (?,?,?,?)',
|
||||||
|
(id, format.upper(), size, name))
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def delete_book(self, id):
|
||||||
|
'''
|
||||||
|
Removes book from self.cache, self.data and underlying database.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
self.cache.pop(self.index(id, cache=True))
|
||||||
|
self.data.pop(self.index(id, cache=False))
|
||||||
|
except TypeError: #If data and cache are the same object
|
||||||
|
pass
|
||||||
|
path = os.path.join(self.library_path, self.path(id, True))
|
||||||
|
if os.path.exists(path):
|
||||||
|
shutil.rmtree(path)
|
||||||
|
self.conn.execute('DELETE FROM books WHERE id=?', (id,))
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def remove_format(self, index, format, index_is_id=False):
|
||||||
|
id = index if index_is_id else self.id(index)
|
||||||
|
path = os.path.join(self.library_path, self.path(id, index_is_id=True))
|
||||||
|
name = self.conn.execute('SELECT name FROM data WHERE book=? AND format=?', (id, format)).fetchone()[0]
|
||||||
|
if name:
|
||||||
|
ext = ('.' + format.lower()) if format else ''
|
||||||
|
path = os.path.join(path, name+ext)
|
||||||
|
if os.access(path, os.W_OK):
|
||||||
|
os.unlink(path)
|
||||||
|
self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, format.upper()))
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def set_metadata(self, id, mi):
|
||||||
|
'''
|
||||||
|
Set metadata for the book `id` from the `MetaInformation` object `mi`
|
||||||
|
'''
|
||||||
|
if not mi.authors:
|
||||||
|
mi.authors = ['Unknown']
|
||||||
|
authors = []
|
||||||
|
for a in mi.authors:
|
||||||
|
authors += a.split('&')
|
||||||
|
self.set_authors(id, authors)
|
||||||
|
if mi.author_sort:
|
||||||
|
self.set_author_sort(id, mi.author_sort)
|
||||||
|
if mi.publisher:
|
||||||
|
self.set_publisher(id, mi.publisher)
|
||||||
|
if mi.rating:
|
||||||
|
self.set_rating(id, mi.rating)
|
||||||
|
if mi.series:
|
||||||
|
self.set_series(id, mi.series)
|
||||||
|
if mi.cover_data[1] is not None:
|
||||||
|
self.set_cover(id, mi.cover_data[1])
|
||||||
|
self.set_path(id, True)
|
||||||
|
|
||||||
|
def set_authors(self, id, authors):
|
||||||
|
'''
|
||||||
|
`authors`: A list of authors.
|
||||||
|
'''
|
||||||
|
self.conn.execute('DELETE FROM books_authors_link WHERE book=?',(id,))
|
||||||
|
for a in authors:
|
||||||
|
if not a:
|
||||||
|
continue
|
||||||
|
a = a.strip()
|
||||||
|
author = self.conn.execute('SELECT id from authors WHERE name=?', (a,)).fetchone()
|
||||||
|
if author:
|
||||||
|
aid = author[0]
|
||||||
|
# Handle change of case
|
||||||
|
self.conn.execute('UPDATE authors SET name=? WHERE id=?', (a, aid))
|
||||||
|
else:
|
||||||
|
aid = self.conn.execute('INSERT INTO authors(name) VALUES (?)', (a,)).lastrowid
|
||||||
|
try:
|
||||||
|
self.conn.execute('INSERT INTO books_authors_link(book, author) VALUES (?,?)', (id, aid))
|
||||||
|
except sqlite.IntegrityError: # Sometimes books specify the same author twice in their metadata
|
||||||
|
pass
|
||||||
|
self.set_path(id, True)
|
||||||
|
|
||||||
|
def set_title(self, id, title):
|
||||||
|
if not title:
|
||||||
|
return
|
||||||
|
self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id))
|
||||||
|
self.set_path(id, True)
|
||||||
|
|
||||||
|
def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True):
|
||||||
|
'''
|
||||||
|
Add a book to the database. self.data and self.cache are not updated.
|
||||||
|
@param paths: List of paths to book files of file-like objects
|
||||||
|
'''
|
||||||
|
formats, metadata, uris = iter(formats), iter(metadata), iter(uris)
|
||||||
|
duplicates = []
|
||||||
|
for path in paths:
|
||||||
|
mi = metadata.next()
|
||||||
|
format = formats.next()
|
||||||
|
try:
|
||||||
|
uri = uris.next()
|
||||||
|
except StopIteration:
|
||||||
|
uri = None
|
||||||
|
if not add_duplicates and self.has_book(mi):
|
||||||
|
duplicates.append((path, format, mi, uri))
|
||||||
|
continue
|
||||||
|
series_index = 1 if mi.series_index is None else mi.series_index
|
||||||
|
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
|
||||||
|
obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)',
|
||||||
|
(mi.title, uri, series_index, aus))
|
||||||
|
id = obj.lastrowid
|
||||||
|
self.set_path(id, True)
|
||||||
|
self.conn.commit()
|
||||||
|
self.set_metadata(id, mi)
|
||||||
|
stream = path if hasattr(path, 'read') else open(path, 'rb')
|
||||||
|
stream.seek(0)
|
||||||
|
|
||||||
|
self.add_format(id, format, stream, index_is_id=True)
|
||||||
|
if not hasattr(path, 'read'):
|
||||||
|
stream.close()
|
||||||
|
self.conn.commit()
|
||||||
|
if duplicates:
|
||||||
|
paths = tuple(duplicate[0] for duplicate in duplicates)
|
||||||
|
formats = tuple(duplicate[1] for duplicate in duplicates)
|
||||||
|
metadata = tuple(duplicate[2] for duplicate in duplicates)
|
||||||
|
uris = tuple(duplicate[3] for duplicate in duplicates)
|
||||||
|
return (paths, formats, metadata, uris)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def import_book(self, mi, formats):
|
||||||
|
series_index = 1 if mi.series_index is None else mi.series_index
|
||||||
|
if not mi.authors:
|
||||||
|
mi.authors = ['Unknown']
|
||||||
|
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
|
||||||
|
obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)',
|
||||||
|
(mi.title, None, series_index, aus))
|
||||||
|
id = obj.lastrowid
|
||||||
|
self.set_path(id, True)
|
||||||
|
self.set_metadata(id, mi)
|
||||||
|
for path in formats:
|
||||||
|
ext = os.path.splitext(path)[1][1:].lower()
|
||||||
|
stream = open(path, 'rb')
|
||||||
|
self.add_format(id, ext, stream, index_is_id=True)
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def move_library_to(self, newloc):
|
||||||
|
if not os.path.exists(newloc):
|
||||||
|
os.makedirs(newloc)
|
||||||
|
old_dirs = set([])
|
||||||
|
for book in self.conn.execute('SELECT id, path FROM books').fetchall():
|
||||||
|
path = book[1]
|
||||||
|
if not path:
|
||||||
|
continue
|
||||||
|
dir = path.split('/')[0]
|
||||||
|
srcdir = os.path.join(self.library_path, dir)
|
||||||
|
tdir = os.path.join(newloc, dir)
|
||||||
|
if os.path.exists(tdir):
|
||||||
|
shutil.rmtree(tdir)
|
||||||
|
shutil.copytree(srcdir, tdir)
|
||||||
|
old_dirs.add(srcdir)
|
||||||
|
|
||||||
|
dbpath = os.path.join(newloc, os.path.basename(self.dbpath))
|
||||||
|
shutil.copyfile(self.dbpath, dbpath)
|
||||||
|
opath = self.dbpath
|
||||||
|
self.conn.close()
|
||||||
|
self.library_path, self.dbpath = newloc, dbpath
|
||||||
|
self.connect()
|
||||||
|
try:
|
||||||
|
os.unlink(opath)
|
||||||
|
for dir in old_dirs:
|
||||||
|
shutil.rmtree(dir)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_old(self, db, progress):
|
||||||
|
header = _(u'<p>Migrating old database to ebook library in %s<br><center>')%self.library_path
|
||||||
|
db.conn.row_factory = lambda cursor, row : tuple(row)
|
||||||
|
books = db.conn.execute('SELECT id, title, sort, timestamp, uri, series_index, author_sort, isbn FROM books ORDER BY id ASC').fetchall()
|
||||||
|
progress.setRange(0, len(books))
|
||||||
|
progress.setValue(0)
|
||||||
|
progress.setLabelText(header)
|
||||||
|
for book in books:
|
||||||
|
self.conn.execute('INSERT INTO books(id, title, sort, timestamp, uri, series_index, author_sort, isbn) VALUES(?, ?, ?, ?, ?, ?, ?, ?);', book)
|
||||||
|
|
||||||
|
tables = '''
|
||||||
|
authors ratings tags series books_tags_link
|
||||||
|
comments publishers
|
||||||
|
books_authors_link conversion_options
|
||||||
|
books_publishers_link
|
||||||
|
books_ratings_link
|
||||||
|
books_series_link feeds
|
||||||
|
'''.split()
|
||||||
|
for table in tables:
|
||||||
|
rows = db.conn.execute('SELECT * FROM %s ORDER BY id ASC'%table).fetchall()
|
||||||
|
for row in rows:
|
||||||
|
self.conn.execute('INSERT INTO %s VALUES(%s)'%(table, ','.join(repeat('?', len(row)))), row)
|
||||||
|
|
||||||
|
for i, book in enumerate(books):
|
||||||
|
progress.setLabelText(header+_(u'Copying <b>%s</b>')%book[1])
|
||||||
|
id = book[0]
|
||||||
|
self.set_path(id, True)
|
||||||
|
formats = db.formats(id, index_is_id=True)
|
||||||
|
if not formats:
|
||||||
|
formats = []
|
||||||
|
else:
|
||||||
|
formats = formats.split(',')
|
||||||
|
for format in formats:
|
||||||
|
data = db.format(id, format, index_is_id=True)
|
||||||
|
if data:
|
||||||
|
self.add_format(id, format, cStringIO.StringIO(data), index_is_id=True)
|
||||||
|
cover = db.cover(id, index_is_id=True)
|
||||||
|
if cover:
|
||||||
|
self.set_cover(id, cover)
|
||||||
|
progress.setValue(i+1)
|
||||||
|
self.conn.commit()
|
||||||
|
progress.setLabelText(_('Compacting database'))
|
||||||
|
self.vacuum()
|
||||||
|
|
342
src/calibre/library/metadata_sqlite.sql
Normal file
342
src/calibre/library/metadata_sqlite.sql
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
CREATE TABLE authors ( id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
sort TEXT COLLATE NOCASE,
|
||||||
|
UNIQUE(name)
|
||||||
|
);
|
||||||
|
CREATE TABLE books ( id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL DEFAULT 'Unknown' COLLATE NOCASE,
|
||||||
|
sort TEXT COLLATE NOCASE,
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
uri TEXT,
|
||||||
|
series_index INTEGER NOT NULL DEFAULT 1,
|
||||||
|
author_sort TEXT COLLATE NOCASE,
|
||||||
|
isbn TEXT DEFAULT "" COLLATE NOCASE,
|
||||||
|
path TEXT NOT NULL DEFAULT ""
|
||||||
|
);
|
||||||
|
CREATE TABLE books_authors_link ( id INTEGER PRIMARY KEY,
|
||||||
|
book INTEGER NOT NULL,
|
||||||
|
author INTEGER NOT NULL,
|
||||||
|
UNIQUE(book, author)
|
||||||
|
);
|
||||||
|
CREATE TABLE books_publishers_link ( id INTEGER PRIMARY KEY,
|
||||||
|
book INTEGER NOT NULL,
|
||||||
|
publisher INTEGER NOT NULL,
|
||||||
|
UNIQUE(book)
|
||||||
|
);
|
||||||
|
CREATE TABLE books_ratings_link ( id INTEGER PRIMARY KEY,
|
||||||
|
book INTEGER NOT NULL,
|
||||||
|
rating INTEGER NOT NULL,
|
||||||
|
UNIQUE(book, rating)
|
||||||
|
);
|
||||||
|
CREATE TABLE books_series_link ( id INTEGER PRIMARY KEY,
|
||||||
|
book INTEGER NOT NULL,
|
||||||
|
series INTEGER NOT NULL,
|
||||||
|
UNIQUE(book)
|
||||||
|
);
|
||||||
|
CREATE TABLE books_tags_link ( id INTEGER PRIMARY KEY,
|
||||||
|
book INTEGER NOT NULL,
|
||||||
|
tag INTEGER NOT NULL,
|
||||||
|
UNIQUE(book, tag)
|
||||||
|
);
|
||||||
|
CREATE TABLE comments ( id INTEGER PRIMARY KEY,
|
||||||
|
book INTEGER NON NULL,
|
||||||
|
text TEXT NON NULL COLLATE NOCASE,
|
||||||
|
UNIQUE(book)
|
||||||
|
);
|
||||||
|
CREATE TABLE conversion_options ( id INTEGER PRIMARY KEY,
|
||||||
|
format TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
book INTEGER,
|
||||||
|
data BLOB NOT NULL,
|
||||||
|
UNIQUE(format,book)
|
||||||
|
);
|
||||||
|
CREATE TABLE feeds ( id INTEGER PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
script TEXT NOT NULL,
|
||||||
|
UNIQUE(title)
|
||||||
|
);
|
||||||
|
CREATE TABLE publishers ( id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
sort TEXT COLLATE NOCASE,
|
||||||
|
UNIQUE(name)
|
||||||
|
);
|
||||||
|
CREATE TABLE ratings ( id INTEGER PRIMARY KEY,
|
||||||
|
rating INTEGER CHECK(rating > -1 AND rating < 11),
|
||||||
|
UNIQUE (rating)
|
||||||
|
);
|
||||||
|
CREATE TABLE series ( id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
sort TEXT COLLATE NOCASE,
|
||||||
|
UNIQUE (name)
|
||||||
|
);
|
||||||
|
CREATE TABLE tags ( id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL COLLATE NOCASE,
|
||||||
|
UNIQUE (name)
|
||||||
|
);
|
||||||
|
CREATE TABLE data ( id INTEGER PRIMARY KEY,
|
||||||
|
book INTEGER NON NULL,
|
||||||
|
format TEXT NON NULL COLLATE NOCASE,
|
||||||
|
uncompressed_size INTEGER NON NULL,
|
||||||
|
name TEXT NON NULL,
|
||||||
|
UNIQUE(book, format)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE VIEW meta AS
|
||||||
|
SELECT id, title,
|
||||||
|
(SELECT concat(name) FROM authors WHERE authors.id IN (SELECT author from books_authors_link WHERE book=books.id)) authors,
|
||||||
|
(SELECT name FROM publishers WHERE publishers.id IN (SELECT publisher from books_publishers_link WHERE book=books.id)) publisher,
|
||||||
|
(SELECT rating FROM ratings WHERE ratings.id IN (SELECT rating from books_ratings_link WHERE book=books.id)) rating,
|
||||||
|
timestamp,
|
||||||
|
(SELECT MAX(uncompressed_size) FROM data WHERE book=books.id) size,
|
||||||
|
(SELECT concat(name) FROM tags WHERE tags.id IN (SELECT tag from books_tags_link WHERE book=books.id)) tags,
|
||||||
|
(SELECT text FROM comments WHERE book=books.id) comments,
|
||||||
|
(SELECT name FROM series WHERE series.id IN (SELECT series FROM books_series_link WHERE book=books.id)) series,
|
||||||
|
series_index,
|
||||||
|
sort,
|
||||||
|
author_sort,
|
||||||
|
(SELECT concat(format) FROM data WHERE data.book=books.id) formats
|
||||||
|
FROM books;
|
||||||
|
CREATE INDEX authors_idx ON books (author_sort COLLATE NOCASE);
|
||||||
|
CREATE INDEX books_authors_link_aidx ON books_authors_link (author);
|
||||||
|
CREATE INDEX books_authors_link_bidx ON books_authors_link (book);
|
||||||
|
CREATE INDEX books_idx ON books (sort COLLATE NOCASE);
|
||||||
|
CREATE INDEX books_publishers_link_aidx ON books_publishers_link (publisher);
|
||||||
|
CREATE INDEX books_publishers_link_bidx ON books_publishers_link (book);
|
||||||
|
CREATE INDEX books_ratings_link_aidx ON books_ratings_link (rating);
|
||||||
|
CREATE INDEX books_ratings_link_bidx ON books_ratings_link (book);
|
||||||
|
CREATE INDEX books_series_link_aidx ON books_series_link (series);
|
||||||
|
CREATE INDEX books_series_link_bidx ON books_series_link (book);
|
||||||
|
CREATE INDEX books_tags_link_aidx ON books_tags_link (tag);
|
||||||
|
CREATE INDEX books_tags_link_bidx ON books_tags_link (book);
|
||||||
|
CREATE INDEX comments_idx ON comments (book);
|
||||||
|
CREATE INDEX conversion_options_idx_a ON conversion_options (format COLLATE NOCASE);
|
||||||
|
CREATE INDEX conversion_options_idx_b ON conversion_options (book);
|
||||||
|
CREATE INDEX data_idx ON data (book);
|
||||||
|
CREATE INDEX publishers_idx ON publishers (name COLLATE NOCASE);
|
||||||
|
CREATE INDEX series_idx ON series (sort COLLATE NOCASE);
|
||||||
|
CREATE INDEX tags_idx ON tags (name COLLATE NOCASE);
|
||||||
|
CREATE TRIGGER books_delete_trg
|
||||||
|
AFTER DELETE ON books
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM books_authors_link WHERE book=OLD.id;
|
||||||
|
DELETE FROM books_publishers_link WHERE book=OLD.id;
|
||||||
|
DELETE FROM books_ratings_link WHERE book=OLD.id;
|
||||||
|
DELETE FROM books_series_link WHERE book=OLD.id;
|
||||||
|
DELETE FROM books_tags_link WHERE book=OLD.id;
|
||||||
|
DELETE FROM data WHERE book=OLD.id;
|
||||||
|
DELETE FROM comments WHERE book=OLD.id;
|
||||||
|
DELETE FROM conversion_options WHERE book=OLD.id;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER books_insert_trg
|
||||||
|
AFTER INSERT ON books
|
||||||
|
BEGIN
|
||||||
|
UPDATE books SET sort=title_sort(NEW.title) WHERE id=NEW.id;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER books_update_trg
|
||||||
|
AFTER UPDATE ON books
|
||||||
|
BEGIN
|
||||||
|
UPDATE books SET sort=title_sort(NEW.title) WHERE id=NEW.id;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_comments_insert
|
||||||
|
BEFORE INSERT ON comments
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_comments_update
|
||||||
|
BEFORE UPDATE OF book ON comments
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_data_insert
|
||||||
|
BEFORE INSERT ON data
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_data_update
|
||||||
|
BEFORE UPDATE OF book ON data
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_delete_books_authors_link
|
||||||
|
BEFORE DELETE ON authors
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT COUNT(id) FROM books_authors_link WHERE book=OLD.book) > 0
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: author is still referenced')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_delete_books_publishers_link
|
||||||
|
BEFORE DELETE ON publishers
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT COUNT(id) FROM books_publishers_link WHERE book=OLD.book) > 0
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: publisher is still referenced')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_delete_books_series_link
|
||||||
|
BEFORE DELETE ON series
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT COUNT(id) FROM books_series_link WHERE series=OLD.id) > 0
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: series is still referenced')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_delete_books_tags_link
|
||||||
|
BEFORE DELETE ON tags
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT COUNT(id) FROM books_tags_link WHERE tag=OLD.id) > 0
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: tag is still referenced')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_insert_books_authors_link
|
||||||
|
BEFORE INSERT ON books_authors_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
WHEN (SELECT id from authors WHERE id=NEW.author) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: author not in authors')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_insert_books_publishers_link
|
||||||
|
BEFORE INSERT ON books_publishers_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
WHEN (SELECT id from publishers WHERE id=NEW.publisher) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: publisher not in publishers')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_insert_books_ratings_link
|
||||||
|
BEFORE INSERT ON books_ratings_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
WHEN (SELECT id from ratings WHERE id=NEW.rating) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: rating not in ratings')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_insert_books_series_link
|
||||||
|
BEFORE INSERT ON books_series_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
WHEN (SELECT id from series WHERE id=NEW.series) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: series not in series')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_insert_books_tags_link
|
||||||
|
BEFORE INSERT ON books_tags_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
WHEN (SELECT id from tags WHERE id=NEW.tag) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: tag not in tags')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_update_books_authors_link_a
|
||||||
|
BEFORE UPDATE OF book ON books_authors_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_update_books_authors_link_b
|
||||||
|
BEFORE UPDATE OF author ON books_authors_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from authors WHERE id=NEW.author) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: author not in authors')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_update_books_publishers_link_a
|
||||||
|
BEFORE UPDATE OF book ON books_publishers_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_update_books_publishers_link_b
|
||||||
|
BEFORE UPDATE OF publisher ON books_publishers_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from publishers WHERE id=NEW.publisher) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: publisher not in publishers')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_update_books_ratings_link_a
|
||||||
|
BEFORE UPDATE OF book ON books_ratings_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_update_books_ratings_link_b
|
||||||
|
BEFORE UPDATE OF rating ON books_ratings_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from ratings WHERE id=NEW.rating) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: rating not in ratings')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_update_books_series_link_a
|
||||||
|
BEFORE UPDATE OF book ON books_series_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_update_books_series_link_b
|
||||||
|
BEFORE UPDATE OF series ON books_series_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from series WHERE id=NEW.series) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: series not in series')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_update_books_tags_link_a
|
||||||
|
BEFORE UPDATE OF book ON books_tags_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_update_books_tags_link_b
|
||||||
|
BEFORE UPDATE OF tag ON books_tags_link
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from tags WHERE id=NEW.tag) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: tag not in tags')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER series_insert_trg
|
||||||
|
AFTER INSERT ON series
|
||||||
|
BEGIN
|
||||||
|
UPDATE series SET sort=NEW.name WHERE id=NEW.id;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER series_update_trg
|
||||||
|
AFTER UPDATE ON series
|
||||||
|
BEGIN
|
||||||
|
UPDATE series SET sort=NEW.name WHERE id=NEW.id;
|
||||||
|
END;
|
Loading…
x
Reference in New Issue
Block a user