mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
metadata editing for single book implemented.
This commit is contained in:
parent
93a1a15533
commit
7cd1973a85
@ -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())
|
||||||
|
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user