metadata editing for single book implemented.

This commit is contained in:
Kovid Goyal 2007-08-03 23:46:14 +00:00
parent 93a1a15533
commit 7cd1973a85
5 changed files with 264 additions and 120 deletions

View File

@ -14,13 +14,15 @@
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
""" The GUI for libprs500. """ """ The GUI for libprs500. """
import sys, os, re, StringIO, traceback import sys, os, re, StringIO, traceback
from PyQt4.QtCore import QVariant, QSettings, QFileInfo, QObject, SIGNAL from PyQt4.QtCore import QVariant, QSettings, QFileInfo, QObject, SIGNAL, QBuffer, \
QByteArray
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, QIcon from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, QIcon
from libprs500 import __appname__ as APP_TITLE from libprs500 import __appname__ as APP_TITLE
from libprs500 import __author__ from libprs500 import __author__
NONE = QVariant() #: Null value to return from the data function of item models NONE = QVariant() #: Null value to return from the data function of item models
error_dialog = None BOOK_EXTENSIONS = ['lrf', 'lrx', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm',
'html', 'xhtml', 'epub',]
def extension(path): def extension(path):
return os.path.splitext(path)[1][1:].lower() return os.path.splitext(path)[1][1:].lower()
@ -90,6 +92,25 @@ class FileIconProvider(QFileIconProvider):
for i in ('dir', 'default'): for i in ('dir', 'default'):
self.icons[i] = QIcon(self.icons[i]) self.icons[i] = QIcon(self.icons[i])
def key_from_ext(self, ext):
key = ext if ext in self.icons.keys() else 'default'
if key == 'default' and ext.count('.') > 0:
ext = ext.rpartition('.')[2]
key = ext if ext in self.icons.keys() else 'default'
return key
def cached_icon(self, key):
candidate = self.icons[key]
if isinstance(candidate, QIcon):
return candidate
icon = QIcon(candidate)
self.icons[key] = icon
return icon
def icon_from_ext(self, ext):
key = self.key_from_ext(ext)
return self.cached_icon(key)
def load_icon(self, fileinfo): def load_icon(self, fileinfo):
key = 'default' key = 'default'
icons = self.icons icons = self.icons
@ -101,18 +122,8 @@ class FileIconProvider(QFileIconProvider):
key = 'dir' key = 'dir'
else: else:
ext = qstring_to_unicode(fileinfo.completeSuffix()).lower() ext = qstring_to_unicode(fileinfo.completeSuffix()).lower()
key = ext if ext in self.icons.keys() else 'default' key = self.key_from_ext(ext)
if key == 'default' and ext.count('.') > 0: return self.cached_icon(key)
ext = ext.rpartition('.')[2]
key = ext if ext in self.icons.keys() else 'default'
candidate = icons[key]
if isinstance(candidate, QIcon):
return candidate
icon = QIcon(candidate)
icons[key] = icon
if icon.isNull():
print 'null icon: ', key
return icon
def icon(self, arg): def icon(self, arg):
if isinstance(arg, QFileInfo): if isinstance(arg, QFileInfo):
@ -123,7 +134,15 @@ class FileIconProvider(QFileIconProvider):
return self.icons['default'] return self.icons['default']
return QFileIconProvider.icon(self, arg) return QFileIconProvider.icon(self, arg)
file_icon_provider = None _file_icon_provider = None
def initialize_file_icon_provider():
global _file_icon_provider
if _file_icon_provider is None:
_file_icon_provider = FileIconProvider()
def file_icon_provider():
global _file_icon_provider
return _file_icon_provider
class FileDialog(QFileDialog): class FileDialog(QFileDialog):
def __init__(self, title='Choose Files', def __init__(self, title='Choose Files',
@ -134,11 +153,9 @@ class FileDialog(QFileDialog):
name = '', name = '',
mode = QFileDialog.ExistingFiles, mode = QFileDialog.ExistingFiles,
): ):
global file_icon_provider initialize_file_icon_provider()
if file_icon_provider is None:
file_icon_provider = FileIconProvider()
QFileDialog.__init__(self, parent) QFileDialog.__init__(self, parent)
self.setIconProvider(file_icon_provider) self.setIconProvider(_file_icon_provider)
self.setModal(modal) self.setModal(modal)
settings = QSettings() settings = QSettings()
state = settings.value(name, QVariant()).toByteArray() state = settings.value(name, QVariant()).toByteArray()
@ -184,4 +201,25 @@ def choose_files(window, name, title,
) )
if fd.exec_() == QFileDialog.Accepted: if fd.exec_() == QFileDialog.Accepted:
return fd.get_files() return fd.get_files()
return None return None
def choose_images(window, name, title, select_only_single_file=True):
mode = QFileDialog.ExistingFile if select_only_single_file else QFileDialog.ExistingFiles
fd = FileDialog(title=title, name=name,
filters=[('Images', ['png', 'gif', 'jpeg', 'jpg', 'svg'])],
parent=window, add_all_files_filter=False, mode=mode,
)
if fd.exec_() == QFileDialog.Accepted:
return fd.get_files()
return None
def pixmap_to_data(pixmap, format='JPEG'):
'''
Return the QPixmap pixmap as a string saved in the specified format.
'''
ba = QByteArray()
buf = QBuffer(ba)
buf.open(QBuffer.WriteOnly)
pixmap.save(buf, format)
return str(ba.data())

View File

@ -12,18 +12,19 @@
## You should have received a copy of the GNU General Public License along ## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
""" '''
The dialog used to edit meta information for a book as well as The dialog used to edit meta information for a book as well as
add/remove formats add/remove formats
""" '''
import os import os
from PyQt4.QtCore import Qt, SIGNAL from PyQt4.QtCore import SIGNAL
from PyQt4.Qt import QObject, QPixmap, QListWidgetItem, QErrorMessage, \ from PyQt4.Qt import QObject, QPixmap, QListWidgetItem, QErrorMessage, \
QVariant, QSettings, QFileDialog QVariant, QSettings, QFileDialog
from libprs500.gui2 import qstring_to_unicode, error_dialog from libprs500.gui2 import qstring_to_unicode, error_dialog, file_icon_provider, \
choose_files, pixmap_to_data, BOOK_EXTENSIONS, choose_images
from libprs500.gui2.dialogs import ModalDialog from libprs500.gui2.dialogs import ModalDialog
from libprs500.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog from libprs500.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog
@ -31,22 +32,19 @@ class Format(QListWidgetItem):
def __init__(self, parent, ext, path=None): def __init__(self, parent, ext, path=None):
self.path = path self.path = path
self.ext = ext self.ext = ext
QListWidgetItem.__init__(self, ext.upper(), parent, \ QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
QListWidgetItem.UserType) ext.upper(), parent, QListWidgetItem.UserType)
class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog): class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog):
def select_cover(self, checked): def select_cover(self, checked):
settings = QSettings() files = choose_images(self.window, 'change cover dialog',
_dir = settings.value("change cover dir", \ u'Choose cover for ' + qstring_to_unicode(self.title.text()))
QVariant(os.path.expanduser("~"))).toString() if not files:
_file = str(QFileDialog.getOpenFileName(self.parent, \ return
"Choose cover for " + str(self.title.text()), _dir, \ _file = files[0]
"Images (*.png *.gif *.jpeg *.jpg *.svg);;All files (*)")) if _file:
if len(_file):
_file = os.path.abspath(_file) _file = os.path.abspath(_file)
settings.setValue("change cover dir", \
QVariant(os.path.dirname(_file)))
if not os.access(_file, os.R_OK): if not os.access(_file, os.R_OK):
d = error_dialog(self.window, 'Cannot read', d = error_dialog(self.window, 'Cannot read',
'You do not have permission to read the file: ' + _file) 'You do not have permission to read the file: ' + _file)
@ -63,43 +61,38 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog):
if cover: if cover:
pix = QPixmap() pix = QPixmap()
pix.loadFromData(cover) pix.loadFromData(cover)
if pix.isNull(): if pix.isNull():
QErrorMessage(self.parent).showMessage(_file + \ d = error_dialog(self.window, _file + " is not a valid picture")
" is not a valid picture") d.exec_()
else: else:
self.cover_path.setText(_file) self.cover_path.setText(_file)
self.cover.setPixmap(pix) self.cover.setPixmap(pix)
self.cover_changed = True
self.cpixmap = pix
def add_format(self, x): def add_format(self, x):
settings = QSettings() files = choose_files(self.window, 'add formats dialog',
_dir = settings.value("add formats dialog dir", \ "Choose formats for " + str(self.title.text()),
QVariant(os.path.expanduser("~"))).toString() [('Books', BOOK_EXTENSIONS)])
files = QFileDialog.getOpenFileNames(self.parent, \ if not files:
"Choose formats for " + str(self.title.text()), _dir, \ return
"Books (*.lrf *.lrx *.rtf *.txt *.html *.xhtml *.htm *.rar);;"+\ for _file in files:
"All files (*)") _file = os.path.abspath(_file)
if not files.isEmpty(): if not os.access(_file, os.R_OK):
x = str(files[0]) QErrorMessage(self.window).showMessage("You do not have "+\
settings.setValue("add formats dialog dir", \ "permission to read the file: " + _file)
QVariant(os.path.dirname(x))) continue
files = str(files.join("|||")).split("|||") ext = os.path.splitext(_file)[1].lower()
for _file in files: if '.' in ext:
_file = os.path.abspath(_file) ext = ext.replace('.', '')
if not os.access(_file, os.R_OK): for row in range(self.formats.count()):
QErrorMessage(self.window).showMessage("You do not have "+\ fmt = self.formats.item(row)
"permission to read the file: " + _file) if fmt.ext == ext:
continue self.formats.takeItem(row)
ext = os.path.splitext(_file)[1].lower() break
if '.' in ext: Format(self.formats, ext, path=_file)
ext = ext.replace('.', '') self.formats_changed = True
for row in range(self.formats.count()):
fmt = self.formats.item(row)
if fmt.ext == ext:
self.formats.takeItem(row)
break
Format(self.formats, ext, path=_file)
self.formats_changed = True
def remove_format(self, x): def remove_format(self, x):
rows = self.formats.selectionModel().selectedRows(0) rows = self.formats.selectionModel().selectedRows(0)
@ -112,7 +105,7 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog):
for row in range(self.formats.count()): for row in range(self.formats.count()):
fmt = self.formats.item(row) fmt = self.formats.item(row)
ext, path = fmt.ext, fmt.path ext, path = fmt.ext, fmt.path
if "unknown" in ext.lower(): if 'unknown' in ext.lower():
ext = None ext = None
if path: if path:
new_extensions.add(ext) new_extensions.add(ext)
@ -120,25 +113,26 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog):
else: else:
old_extensions.add(ext) old_extensions.add(ext)
for ext in new_extensions: for ext in new_extensions:
self.db.add_format(self.id, ext, file(paths[ext], "rb")) self.db.add_format(self.row, ext, open(paths[ext], "rb"))
db_extensions = self.db.get_extensions(self.id) db_extensions = set(self.db.formats(self.row).split(','))
extensions = new_extensions.union(old_extensions) extensions = new_extensions.union(old_extensions)
for ext in db_extensions: for ext in db_extensions:
if ext not in extensions: if ext not in extensions:
self.db.remove_format(self.id, ext) self.db.remove_format(self.row, ext)
self.db.update_max_size(self.id)
def __init__(self, window, row, db, slot): def __init__(self, window, row, db):
Ui_MetadataSingleDialog.__init__(self) Ui_MetadataSingleDialog.__init__(self)
ModalDialog.__init__(self, window) ModalDialog.__init__(self, window)
self.setupUi(self.dialog) self.setupUi(self.dialog)
self.splitter.setStretchFactor(100, 1) self.splitter.setStretchFactor(100, 1)
self.db = db self.db = db
self.id = db.id(row) self.id = db.id(row)
self.row = row
self.cover_data = None self.cover_data = None
self.formats_changed = False self.formats_changed = False
self.cover_changed = False self.cover_changed = False
self.slot = slot self.cpixmap = None
self.changed = False
QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \ QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \
self.select_cover) self.select_cover)
QObject.connect(self.add_format_button, SIGNAL("clicked(bool)"), \ QObject.connect(self.add_format_button, SIGNAL("clicked(bool)"), \
@ -157,7 +151,7 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog):
self.tags.setText(tags if tags else '') self.tags.setText(tags if tags else '')
rating = self.db.rating(row) rating = self.db.rating(row)
if rating > 0: if rating > 0:
self.rating.setValue(rating) self.rating.setValue(int(rating/2.))
comments = self.db.comments(row) comments = self.db.comments(row)
self.comments.setPlainText(comments if comments else '') self.comments.setPlainText(comments if comments else '')
cover = self.db.cover(row) cover = self.db.cover(row)
@ -166,14 +160,40 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog):
pm.loadFromData(cover) pm.loadFromData(cover)
if not pm.isNull(): if not pm.isNull():
self.cover.setPixmap(pm) self.cover.setPixmap(pm)
# exts = self.db.get_extensions(self.id) exts = self.db.formats(row)
# for ext in exts: if exts:
# if not ext: exts = exts.split(',')
# ext = "Unknown" for ext in exts:
# Format(self.formats, ext) if not ext:
ext = ''
Format(self.formats, ext)
if qstring_to_unicode(self.series.currentText()):
self.enable_series_index()
QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.enable_series_index)
QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.enable_series_index)
all_series = self.db.all_series()
series_id = self.db.series_id(row)
idx, c = None, 0
for i in all_series:
id, name = i
if id == series_id:
idx = c
self.series.addItem(name)
c += 1
if idx is not None:
self.series.setCurrentIndex(idx)
self.series_index.setValue(self.db.series_index(row))
self.dialog.exec_() self.dialog.exec_()
def enable_series_index(self, *args):
self.series_index.setEnabled(True)
def sync(self): def sync(self):
if self.formats_changed: if self.formats_changed:
self.sync_formats() self.sync_formats()
@ -181,7 +201,15 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog):
self.db.set_title(self.id, title) self.db.set_title(self.id, title)
au = qstring_to_unicode(self.authors.text()).split(',') au = qstring_to_unicode(self.authors.text()).split(',')
self.db.set_authors(self.id, au) self.db.set_authors(self.id, au)
self.slot() self.db.set_rating(self.id, 2*self.rating.value())
self.db.set_publisher(self.id, qstring_to_unicode(self.publisher.text()))
self.db.set_tags(self.id, qstring_to_unicode(self.tags.text()).split(','))
self.db.set_series(self.id, qstring_to_unicode(self.series.currentText()))
self.db.set_series_index(self.id, self.series_index.value())
self.db.set_comment(self.id, qstring_to_unicode(self.comments.toPlainText()))
if self.cover_changed:
self.db.set_cover(self.id, pixmap_to_data(self.cover.pixmap()))
self.changed = True
def reject(self): def reject(self):
self.rejected = True self.rejected = True

View File

@ -81,6 +81,8 @@ class LibraryDelegate(QItemDelegate):
class BooksModel(QAbstractTableModel): class BooksModel(QAbstractTableModel):
ROMAN = ['0', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X']
def __init__(self, parent): def __init__(self, parent):
QAbstractTableModel.__init__(self, parent) QAbstractTableModel.__init__(self, parent)
self.db = None self.db = None
@ -172,6 +174,11 @@ class BooksModel(QAbstractTableModel):
if not comments: if not comments:
comments = 'None' comments = 'None'
data['Comments'] = comments data['Comments'] = comments
series = self.db.series(idx)
if series:
sidx = self.db.series_index(idx)
sidx = self.__class__.ROMAN[sidx] if sidx < len(self.__class__.ROMAN) else str(sidx)
data['Series'] = 'Book <font face="serif">%s</font> of %s.'%(sidx, series)
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data) self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
def get_metadata(self, rows): def get_metadata(self, rows):

View File

@ -15,7 +15,7 @@
import os, tempfile, sys import os, tempfile, sys
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \ from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \
QSettings, QVariant, QSize, QThread, QBuffer, QByteArray QSettings, QVariant, QSize, QThread
from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon
from PyQt4.QtSvg import QSvgRenderer from PyQt4.QtSvg import QSvgRenderer
@ -23,7 +23,9 @@ from libprs500 import __version__, __appname__
from libprs500.ebooks.metadata.meta import get_metadata from libprs500.ebooks.metadata.meta import get_metadata
from libprs500.devices.errors import FreeSpaceError from libprs500.devices.errors import FreeSpaceError
from libprs500.devices.interface import Device from libprs500.devices.interface import Device
from libprs500.gui2 import APP_TITLE, warning_dialog, choose_files, error_dialog from libprs500.gui2 import APP_TITLE, warning_dialog, choose_files, error_dialog, \
initialize_file_icon_provider, BOOK_EXTENSIONS, \
pixmap_to_data
from libprs500.gui2.main_ui import Ui_MainWindow from libprs500.gui2.main_ui import Ui_MainWindow
from libprs500.gui2.device import DeviceDetector, DeviceManager from libprs500.gui2.device import DeviceDetector, DeviceManager
from libprs500.gui2.status import StatusBar from libprs500.gui2.status import StatusBar
@ -39,11 +41,7 @@ class Main(QObject, Ui_MainWindow):
p = QPainter(pixmap) p = QPainter(pixmap)
r.render(p) r.render(p)
p.end() p.end()
ba = QByteArray() self.default_thumbnail = (pixmap.width(), pixmap.height(), pixmap_to_data(pixmap))
buf = QBuffer(ba)
buf.open(QBuffer.WriteOnly)
pixmap.save(buf, 'JPEG')
self.default_thumbnail = (pixmap.width(), pixmap.height(), ba.data())
def __init__(self, window): def __init__(self, window):
QObject.__init__(self) QObject.__init__(self)
@ -196,8 +194,7 @@ class Main(QObject, Ui_MainWindow):
Add books from the local filesystem to either the library or the device. Add books from the local filesystem to either the library or the device.
''' '''
books = choose_files(self.window, 'add books dialog dir', 'Select books', books = choose_files(self.window, 'add books dialog dir', 'Select books',
filters=[('Books', ['lrf', 'lrx', 'rar', 'zip', filters=[('Books', BOOK_EXTENSIONS)])
'rtf', 'lit', 'txt', 'htm', 'html', 'xhtml', 'epub',])])
if not books: if not books:
return return
on_card = False if self.stack.currentIndex() != 2 else True on_card = False if self.stack.currentIndex() != 2 else True
@ -317,10 +314,10 @@ class Main(QObject, Ui_MainWindow):
if not rows or len(rows) == 0: if not rows or len(rows) == 0:
return return
changed = False changed = False
def cs():
changed = True
for row in rows: for row in rows:
MetadataSingleDialog(self.window, row.row(), self.library_view.model().db, cs) if MetadataSingleDialog(self.window, row.row(),
self.library_view.model().db).changed:
changed = True
if changed: if changed:
self.library_view.model().resort() self.library_view.model().resort()
@ -342,11 +339,7 @@ class Main(QObject, Ui_MainWindow):
ht = self.device_manager.device_class.THUMBNAIL_HEIGHT if self.device_manager else \ ht = self.device_manager.device_class.THUMBNAIL_HEIGHT if self.device_manager else \
Device.THUMBNAIL_HEIGHT Device.THUMBNAIL_HEIGHT
p = p.scaledToHeight(ht, Qt.SmoothTransformation) p = p.scaledToHeight(ht, Qt.SmoothTransformation)
ba = QByteArray() return (p.width(), p.height(), pixmap_to_data(p))
buf = QBuffer(ba)
buf.open(QBuffer.WriteOnly)
p.save(buf, 'JPEG')
return (p.width(), p.height(), ba.data())
def sync_to_device(self, on_card): def sync_to_device(self, on_card):
rows = self.library_view.selectionModel().selectedRows() rows = self.library_view.selectionModel().selectedRows()
@ -438,6 +431,7 @@ def main():
window.setWindowTitle(APP_TITLE) window.setWindowTitle(APP_TITLE)
QCoreApplication.setOrganizationName("KovidsBrain") QCoreApplication.setOrganizationName("KovidsBrain")
QCoreApplication.setApplicationName(APP_TITLE) QCoreApplication.setApplicationName(APP_TITLE)
initialize_file_icon_provider()
main = Main(window) main = Main(window)
def unhandled_exception(type, value, tb): def unhandled_exception(type, value, tb):
import traceback import traceback

View File

@ -17,7 +17,7 @@ Backend that implements storage of ebooks in an sqlite database.
""" """
import sqlite3 as sqlite import sqlite3 as sqlite
import datetime, re import datetime, re
from zlib import compressobj, decompress from zlib import compress, decompress
class Concatenate(object): class Concatenate(object):
'''String concatenation aggregator for sqlite''' '''String concatenation aggregator for sqlite'''
@ -149,7 +149,7 @@ class LibraryDatabase(object):
sort TEXT COLLATE NOCASE, sort TEXT COLLATE NOCASE,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
uri TEXT, uri TEXT,
series_index INTEGER series_index INTEGER NOT NULL DEFAULT 1
); );
CREATE INDEX books_idx ON books (sort COLLATE NOCASE); CREATE INDEX books_idx ON books (sort COLLATE NOCASE);
CREATE TRIGGER books_insert_trg CREATE TRIGGER books_insert_trg
@ -476,7 +476,6 @@ class LibraryDatabase(object):
/**** covers table ****/ /**** covers table ****/
CREATE TABLE covers ( id INTEGER PRIMARY KEY, CREATE TABLE covers ( id INTEGER PRIMARY KEY,
book INTEGER NON NULL, book INTEGER NON NULL,
type TEXT NON NULL COLLATE NOCASE,
uncompressed_size INTEGER NON NULL, uncompressed_size INTEGER NON NULL,
data BLOB NON NULL, data BLOB NON NULL,
UNIQUE(book) UNIQUE(book)
@ -644,13 +643,10 @@ class LibraryDatabase(object):
def cover(self, index): def cover(self, index):
'''Cover as a data string or None''' '''Cover as a data string or None'''
id = self.id(index) id = self.id(index)
matches = self.conn.execute('SELECT data from covers where id=?', (id,)).fetchall() data = self.conn.execute('SELECT data FROM covers WHERE book=?', (id,)).fetchone()
if not matches: if not data:
return None return None
raw = matches[0][0] return(decompress(data[0]))
if raw:
return decompress(raw)
return None
def tags(self, index): def tags(self, index):
'''tags as a comman separated list or None''' '''tags as a comman separated list or None'''
@ -660,6 +656,22 @@ class LibraryDatabase(object):
return None return None
return matches[0][0] return matches[0][0]
def series_id(self, index):
id = self.id(index)
ans= self.conn.execute('SELECT series from books_series_link WHERE book=?', (id,)).fetchone()
if ans:
return ans[0]
def series(self, index):
id = self.series_id(index)
ans = self.conn.execute('SELECT name from series WHERE id=?', (id,)).fetchone()
if ans:
return ans[0]
def series_index(self, index):
id = self.id(index)
return self.conn.execute('SELECT series_index FROM books WHERE id=?', (id,)).fetchone()[0]
def comments(self, index): def comments(self, index):
'''Comments as string or None''' '''Comments as string or None'''
id = self.id(index) id = self.id(index)
@ -679,8 +691,38 @@ class LibraryDatabase(object):
def format(self, index, format): def format(self, index, format):
id = self.id(index) id = self.id(index)
return decompress(self.conn.execute('SELECT data FROM data WHERE book=? AND format=?', (id, format)).fetchone()[0]) return decompress(self.conn.execute('SELECT data FROM data WHERE book=? AND format=?', (id, format)).fetchone()[0])
def all_series(self):
return [ (i[0], i[1]) for i in \
self.conn.execute('SELECT id, name FROM series').fetchall()]
def add_format(self, index, ext, stream):
'''
Add the format specified by ext. If it already exists it is replaced.
'''
id = self.id(index)
stream.seek(0, 2)
usize = stream.tell()
stream.seek(0)
data = sqlite.Binary(compress(stream.read()))
exts = self.formats(index)
if not exts:
exts = []
if ext in exts:
self.conn.execute('UPDATE data SET data=? WHERE format=? AND book=?',
(data, ext, id))
self.conn.execute('UPDATE data SET uncompressed_size=? WHERE format=? AND book=?',
(usize, ext, id))
else:
self.conn.execute('INSERT INTO data(book, format, uncompressed_size, data) VALUES (?, ?, ?, ?)',
(id, ext, usize, data))
self.conn.commit()
def remove_format(self, index, ext):
id = self.id(index)
self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, ext.lower()))
self.conn.commit()
def set(self, row, column, val): def set(self, row, column, val):
''' '''
Convenience method for setting the title, authors, publisher or rating Convenience method for setting the title, authors, publisher or rating
@ -728,17 +770,38 @@ class LibraryDatabase(object):
self.conn.commit() self.conn.commit()
def set_publisher(self, id, publisher): def set_publisher(self, id, publisher):
if not publisher:
return
self.conn.execute('DELETE FROM books_publishers_link WHERE book=?',(id,)) self.conn.execute('DELETE FROM books_publishers_link WHERE book=?',(id,))
pub = self.conn.execute('SELECT id from publishers WHERE name=?', (publisher,)).fetchone() if publisher:
if pub: pub = self.conn.execute('SELECT id from publishers WHERE name=?', (publisher,)).fetchone()
aid = pub[0] if pub:
else: aid = pub[0]
aid = self.conn.execute('INSERT INTO publishers(name) VALUES (?)', (publisher,)).lastrowid else:
self.conn.execute('INSERT INTO books_publishers_link(book, publisher) VALUES (?,?)', (id, aid)) aid = self.conn.execute('INSERT INTO publishers(name) VALUES (?)', (publisher,)).lastrowid
self.conn.execute('INSERT INTO books_publishers_link(book, publisher) VALUES (?,?)', (id, aid))
self.conn.commit() self.conn.commit()
def set_comment(self, id, text):
self.conn.execute('DELETE FROM comments WHERE book=?', (id,))
self.conn.execute('INSERT INTO comments(book,text) VALUES (?,?)', (id, text))
self.conn.commit()
def set_tags(self, id, tags):
'''
@param tags: list of strings
'''
self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,))
tag = set(tags)
for tag in tags:
t = self.conn.execute('SELECT id from tags WHERE name=?', (tag,)).fetchone()
if t:
tid = t[0]
else:
tid = self.conn.execute('INSERT INTO tags(name) VALUES(?)', (tag,)).lastrowid
self.conn.execute('INSERT INTO books_tags_link(book, tag) VALUES (?,?)',
(id, tid))
self.conn.commit()
def set_series(self, id, series): def set_series(self, id, series):
self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,)) self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,))
if series: if series:
@ -748,7 +811,12 @@ class LibraryDatabase(object):
else: else:
aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid
self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid)) self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid))
self.conn.commit() self.conn.commit()
def set_series_index(self, id, idx):
print
self.conn.execute('UPDATE books SET series_index=? WHERE id=?', (idx, id))
self.conn.commit()
def set_rating(self, id, rating): def set_rating(self, id, rating):
rating = int(rating) rating = int(rating)
@ -757,7 +825,16 @@ class LibraryDatabase(object):
rat = rat[0] if rat else self.conn.execute('INSERT INTO ratings(rating) VALUES (?)', (rating,)).lastrowid rat = rat[0] if rat else self.conn.execute('INSERT INTO ratings(rating) VALUES (?)', (rating,)).lastrowid
self.conn.execute('INSERT INTO books_ratings_link(book, rating) VALUES (?,?)', (id, rat)) self.conn.execute('INSERT INTO books_ratings_link(book, rating) VALUES (?,?)', (id, rat))
self.conn.commit() self.conn.commit()
def set_cover(self, id, data):
self.conn.execute('DELETE FROM covers where book=?', (id,))
if data:
usize = len(data)
data = compress(data)
self.conn.execute('INSERT INTO covers(book, uncompressed_size, data) VALUES (?,?,?)',
(id, usize, sqlite.Binary(data)))
self.conn.commit()
def add_books(self, paths, formats, metadata, uris=[]): def add_books(self, paths, formats, metadata, uris=[]):
''' '''
Add a book to the database. self.data and self.cache are not updated. Add a book to the database. self.data and self.cache are not updated.
@ -792,7 +869,7 @@ class LibraryDatabase(object):
stream.seek(0) stream.seek(0)
format = formats.next() format = formats.next()
self.conn.execute('INSERT INTO data(book, format, uncompressed_size, data) VALUES (?,?,?,?)', self.conn.execute('INSERT INTO data(book, format, uncompressed_size, data) VALUES (?,?,?,?)',
(id, format, usize, compressobj().compress(stream.read()))) (id, format, usize, sqlite.Binary(compress(stream.read()))))
stream.close() stream.close()
self.conn.commit() self.conn.commit()