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.
""" The GUI for libprs500. """
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 libprs500 import __appname__ as APP_TITLE
from libprs500 import __author__
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):
return os.path.splitext(path)[1][1:].lower()
@ -90,6 +92,25 @@ class FileIconProvider(QFileIconProvider):
for i in ('dir', 'default'):
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):
key = 'default'
icons = self.icons
@ -101,18 +122,8 @@ class FileIconProvider(QFileIconProvider):
key = 'dir'
else:
ext = qstring_to_unicode(fileinfo.completeSuffix()).lower()
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'
candidate = icons[key]
if isinstance(candidate, QIcon):
return candidate
icon = QIcon(candidate)
icons[key] = icon
if icon.isNull():
print 'null icon: ', key
return icon
key = self.key_from_ext(ext)
return self.cached_icon(key)
def icon(self, arg):
if isinstance(arg, QFileInfo):
@ -123,7 +134,15 @@ class FileIconProvider(QFileIconProvider):
return self.icons['default']
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):
def __init__(self, title='Choose Files',
@ -134,11 +153,9 @@ class FileDialog(QFileDialog):
name = '',
mode = QFileDialog.ExistingFiles,
):
global file_icon_provider
if file_icon_provider is None:
file_icon_provider = FileIconProvider()
initialize_file_icon_provider()
QFileDialog.__init__(self, parent)
self.setIconProvider(file_icon_provider)
self.setIconProvider(_file_icon_provider)
self.setModal(modal)
settings = QSettings()
state = settings.value(name, QVariant()).toByteArray()
@ -185,3 +202,24 @@ def choose_files(window, name, title,
if fd.exec_() == QFileDialog.Accepted:
return fd.get_files()
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
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
'''
The dialog used to edit meta information for a book as well as
add/remove formats
"""
'''
import os
from PyQt4.QtCore import Qt, SIGNAL
from PyQt4.QtCore import SIGNAL
from PyQt4.Qt import QObject, QPixmap, QListWidgetItem, QErrorMessage, \
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.metadata_single_ui import Ui_MetadataSingleDialog
@ -31,22 +32,19 @@ class Format(QListWidgetItem):
def __init__(self, parent, ext, path=None):
self.path = path
self.ext = ext
QListWidgetItem.__init__(self, ext.upper(), parent, \
QListWidgetItem.UserType)
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
ext.upper(), parent, QListWidgetItem.UserType)
class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog):
def select_cover(self, checked):
settings = QSettings()
_dir = settings.value("change cover dir", \
QVariant(os.path.expanduser("~"))).toString()
_file = str(QFileDialog.getOpenFileName(self.parent, \
"Choose cover for " + str(self.title.text()), _dir, \
"Images (*.png *.gif *.jpeg *.jpg *.svg);;All files (*)"))
if len(_file):
files = choose_images(self.window, 'change cover dialog',
u'Choose cover for ' + qstring_to_unicode(self.title.text()))
if not files:
return
_file = files[0]
if _file:
_file = os.path.abspath(_file)
settings.setValue("change cover dir", \
QVariant(os.path.dirname(_file)))
if not os.access(_file, os.R_OK):
d = error_dialog(self.window, 'Cannot read',
'You do not have permission to read the file: ' + _file)
@ -64,26 +62,21 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog):
pix = QPixmap()
pix.loadFromData(cover)
if pix.isNull():
QErrorMessage(self.parent).showMessage(_file + \
" is not a valid picture")
d = error_dialog(self.window, _file + " is not a valid picture")
d.exec_()
else:
self.cover_path.setText(_file)
self.cover.setPixmap(pix)
self.cover_changed = True
self.cpixmap = pix
def add_format(self, x):
settings = QSettings()
_dir = settings.value("add formats dialog dir", \
QVariant(os.path.expanduser("~"))).toString()
files = QFileDialog.getOpenFileNames(self.parent, \
"Choose formats for " + str(self.title.text()), _dir, \
"Books (*.lrf *.lrx *.rtf *.txt *.html *.xhtml *.htm *.rar);;"+\
"All files (*)")
if not files.isEmpty():
x = str(files[0])
settings.setValue("add formats dialog dir", \
QVariant(os.path.dirname(x)))
files = str(files.join("|||")).split("|||")
files = choose_files(self.window, 'add formats dialog',
"Choose formats for " + str(self.title.text()),
[('Books', BOOK_EXTENSIONS)])
if not files:
return
for _file in files:
_file = os.path.abspath(_file)
if not os.access(_file, os.R_OK):
@ -112,7 +105,7 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog):
for row in range(self.formats.count()):
fmt = self.formats.item(row)
ext, path = fmt.ext, fmt.path
if "unknown" in ext.lower():
if 'unknown' in ext.lower():
ext = None
if path:
new_extensions.add(ext)
@ -120,25 +113,26 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog):
else:
old_extensions.add(ext)
for ext in new_extensions:
self.db.add_format(self.id, ext, file(paths[ext], "rb"))
db_extensions = self.db.get_extensions(self.id)
self.db.add_format(self.row, ext, open(paths[ext], "rb"))
db_extensions = set(self.db.formats(self.row).split(','))
extensions = new_extensions.union(old_extensions)
for ext in db_extensions:
if ext not in extensions:
self.db.remove_format(self.id, ext)
self.db.update_max_size(self.id)
self.db.remove_format(self.row, ext)
def __init__(self, window, row, db, slot):
def __init__(self, window, row, db):
Ui_MetadataSingleDialog.__init__(self)
ModalDialog.__init__(self, window)
self.setupUi(self.dialog)
self.splitter.setStretchFactor(100, 1)
self.db = db
self.id = db.id(row)
self.row = row
self.cover_data = None
self.formats_changed = False
self.cover_changed = False
self.slot = slot
self.cpixmap = None
self.changed = False
QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \
self.select_cover)
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 '')
rating = self.db.rating(row)
if rating > 0:
self.rating.setValue(rating)
self.rating.setValue(int(rating/2.))
comments = self.db.comments(row)
self.comments.setPlainText(comments if comments else '')
cover = self.db.cover(row)
@ -166,14 +160,40 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog):
pm.loadFromData(cover)
if not pm.isNull():
self.cover.setPixmap(pm)
# exts = self.db.get_extensions(self.id)
# for ext in exts:
# if not ext:
# ext = "Unknown"
# Format(self.formats, ext)
exts = self.db.formats(row)
if exts:
exts = exts.split(',')
for ext in exts:
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_()
def enable_series_index(self, *args):
self.series_index.setEnabled(True)
def sync(self):
if self.formats_changed:
self.sync_formats()
@ -181,7 +201,15 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog):
self.db.set_title(self.id, title)
au = qstring_to_unicode(self.authors.text()).split(',')
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):
self.rejected = True

View File

@ -81,6 +81,8 @@ class LibraryDelegate(QItemDelegate):
class BooksModel(QAbstractTableModel):
ROMAN = ['0', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X']
def __init__(self, parent):
QAbstractTableModel.__init__(self, parent)
self.db = None
@ -172,6 +174,11 @@ class BooksModel(QAbstractTableModel):
if not comments:
comments = 'None'
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)
def get_metadata(self, rows):

View File

@ -15,7 +15,7 @@
import os, tempfile, sys
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.QtSvg import QSvgRenderer
@ -23,7 +23,9 @@ from libprs500 import __version__, __appname__
from libprs500.ebooks.metadata.meta import get_metadata
from libprs500.devices.errors import FreeSpaceError
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.device import DeviceDetector, DeviceManager
from libprs500.gui2.status import StatusBar
@ -39,11 +41,7 @@ class Main(QObject, Ui_MainWindow):
p = QPainter(pixmap)
r.render(p)
p.end()
ba = QByteArray()
buf = QBuffer(ba)
buf.open(QBuffer.WriteOnly)
pixmap.save(buf, 'JPEG')
self.default_thumbnail = (pixmap.width(), pixmap.height(), ba.data())
self.default_thumbnail = (pixmap.width(), pixmap.height(), pixmap_to_data(pixmap))
def __init__(self, window):
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.
'''
books = choose_files(self.window, 'add books dialog dir', 'Select books',
filters=[('Books', ['lrf', 'lrx', 'rar', 'zip',
'rtf', 'lit', 'txt', 'htm', 'html', 'xhtml', 'epub',])])
filters=[('Books', BOOK_EXTENSIONS)])
if not books:
return
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:
return
changed = False
def cs():
changed = True
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:
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 \
Device.THUMBNAIL_HEIGHT
p = p.scaledToHeight(ht, Qt.SmoothTransformation)
ba = QByteArray()
buf = QBuffer(ba)
buf.open(QBuffer.WriteOnly)
p.save(buf, 'JPEG')
return (p.width(), p.height(), ba.data())
return (p.width(), p.height(), pixmap_to_data(p))
def sync_to_device(self, on_card):
rows = self.library_view.selectionModel().selectedRows()
@ -438,6 +431,7 @@ def main():
window.setWindowTitle(APP_TITLE)
QCoreApplication.setOrganizationName("KovidsBrain")
QCoreApplication.setApplicationName(APP_TITLE)
initialize_file_icon_provider()
main = Main(window)
def unhandled_exception(type, value, tb):
import traceback

View File

@ -17,7 +17,7 @@ Backend that implements storage of ebooks in an sqlite database.
"""
import sqlite3 as sqlite
import datetime, re
from zlib import compressobj, decompress
from zlib import compress, decompress
class Concatenate(object):
'''String concatenation aggregator for sqlite'''
@ -149,7 +149,7 @@ class LibraryDatabase(object):
sort TEXT COLLATE NOCASE,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
uri TEXT,
series_index INTEGER
series_index INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX books_idx ON books (sort COLLATE NOCASE);
CREATE TRIGGER books_insert_trg
@ -476,7 +476,6 @@ class LibraryDatabase(object):
/**** covers table ****/
CREATE TABLE covers ( id INTEGER PRIMARY KEY,
book INTEGER NON NULL,
type TEXT NON NULL COLLATE NOCASE,
uncompressed_size INTEGER NON NULL,
data BLOB NON NULL,
UNIQUE(book)
@ -644,13 +643,10 @@ class LibraryDatabase(object):
def cover(self, index):
'''Cover as a data string or None'''
id = self.id(index)
matches = self.conn.execute('SELECT data from covers where id=?', (id,)).fetchall()
if not matches:
return None
raw = matches[0][0]
if raw:
return decompress(raw)
data = self.conn.execute('SELECT data FROM covers WHERE book=?', (id,)).fetchone()
if not data:
return None
return(decompress(data[0]))
def tags(self, index):
'''tags as a comman separated list or None'''
@ -660,6 +656,22 @@ class LibraryDatabase(object):
return None
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):
'''Comments as string or None'''
id = self.id(index)
@ -680,6 +692,36 @@ class LibraryDatabase(object):
id = self.id(index)
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):
'''
@ -728,9 +770,8 @@ class LibraryDatabase(object):
self.conn.commit()
def set_publisher(self, id, publisher):
if not publisher:
return
self.conn.execute('DELETE FROM books_publishers_link WHERE book=?',(id,))
if publisher:
pub = self.conn.execute('SELECT id from publishers WHERE name=?', (publisher,)).fetchone()
if pub:
aid = pub[0]
@ -739,6 +780,28 @@ class LibraryDatabase(object):
self.conn.execute('INSERT INTO books_publishers_link(book, publisher) VALUES (?,?)', (id, aid))
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):
self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,))
if series:
@ -750,6 +813,11 @@ class LibraryDatabase(object):
self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid))
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):
rating = int(rating)
self.conn.execute('DELETE FROM books_ratings_link WHERE book=?',(id,))
@ -758,6 +826,15 @@ class LibraryDatabase(object):
self.conn.execute('INSERT INTO books_ratings_link(book, rating) VALUES (?,?)', (id, rat))
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=[]):
'''
Add a book to the database. self.data and self.cache are not updated.
@ -792,7 +869,7 @@ class LibraryDatabase(object):
stream.seek(0)
format = formats.next()
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()
self.conn.commit()