mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Implement a metadata comparison/merging tool
This commit is contained in:
parent
7755ff72c3
commit
890b5e4c57
@ -66,6 +66,7 @@ class EditorWidget(QWebView): # {{{
|
|||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QWebView.__init__(self, parent)
|
QWebView.__init__(self, parent)
|
||||||
|
self.readonly = False
|
||||||
|
|
||||||
self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL)
|
self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL)
|
||||||
|
|
||||||
@ -163,7 +164,11 @@ class EditorWidget(QWebView): # {{{
|
|||||||
self.page().linkClicked.connect(self.link_clicked)
|
self.page().linkClicked.connect(self.link_clicked)
|
||||||
|
|
||||||
self.setHtml('')
|
self.setHtml('')
|
||||||
self.page().setContentEditable(True)
|
self.set_readonly(False)
|
||||||
|
|
||||||
|
def set_readonly(self, what):
|
||||||
|
self.readonly = what
|
||||||
|
self.page().setContentEditable(not self.readonly)
|
||||||
|
|
||||||
def clear_text(self, *args):
|
def clear_text(self, *args):
|
||||||
us = self.page().undoStack()
|
us = self.page().undoStack()
|
||||||
@ -313,7 +318,7 @@ class EditorWidget(QWebView): # {{{
|
|||||||
# toList() is needed because PyQt on Debian is old/broken
|
# toList() is needed because PyQt on Debian is old/broken
|
||||||
for body in self.page().mainFrame().documentElement().findAll('body').toList():
|
for body in self.page().mainFrame().documentElement().findAll('body').toList():
|
||||||
body.setAttribute('style', style)
|
body.setAttribute('style', style)
|
||||||
self.page().setContentEditable(True)
|
self.page().setContentEditable(not self.readonly)
|
||||||
|
|
||||||
def keyPressEvent(self, ev):
|
def keyPressEvent(self, ev):
|
||||||
if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab):
|
if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab):
|
||||||
@ -585,6 +590,7 @@ class Editor(QWidget): # {{{
|
|||||||
self.tabs.addTab(self.code_edit, _('HTML Source'))
|
self.tabs.addTab(self.code_edit, _('HTML Source'))
|
||||||
self.tabs.currentChanged[int].connect(self.change_tab)
|
self.tabs.currentChanged[int].connect(self.change_tab)
|
||||||
self.highlighter = Highlighter(self.code_edit.document())
|
self.highlighter = Highlighter(self.code_edit.document())
|
||||||
|
self.layout().setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
# toolbar1 {{{
|
# toolbar1 {{{
|
||||||
self.toolbar1.addAction(self.editor.action_undo)
|
self.toolbar1.addAction(self.editor.action_undo)
|
||||||
@ -666,6 +672,12 @@ class Editor(QWidget): # {{{
|
|||||||
self.toolbar2.setVisible(False)
|
self.toolbar2.setVisible(False)
|
||||||
self.toolbar3.setVisible(False)
|
self.toolbar3.setVisible(False)
|
||||||
|
|
||||||
|
def set_readonly(self, what):
|
||||||
|
self.editor.set_readonly(what)
|
||||||
|
|
||||||
|
def hide_tabs(self):
|
||||||
|
self.tabs.tabBar().setVisible(False)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -101,7 +101,7 @@ class TitleEdit(EnLineEdit):
|
|||||||
getattr(db, 'set_'+ self.TITLE_ATTR)(id_, title, notify=False,
|
getattr(db, 'set_'+ self.TITLE_ATTR)(id_, title, notify=False,
|
||||||
commit=False)
|
commit=False)
|
||||||
except (IOError, OSError) as err:
|
except (IOError, OSError) as err:
|
||||||
if getattr(err, 'errno', None) == errno.EACCES: # Permission denied
|
if getattr(err, 'errno', None) == errno.EACCES: # Permission denied
|
||||||
import traceback
|
import traceback
|
||||||
fname = getattr(err, 'filename', None)
|
fname = getattr(err, 'filename', None)
|
||||||
p = 'Locked file: %s\n\n'%fname if fname else ''
|
p = 'Locked file: %s\n\n'%fname if fname else ''
|
||||||
@ -273,7 +273,7 @@ class AuthorsEdit(EditWithComplete):
|
|||||||
self.books_to_refresh |= db.set_authors(id_, authors, notify=False,
|
self.books_to_refresh |= db.set_authors(id_, authors, notify=False,
|
||||||
allow_case_change=True)
|
allow_case_change=True)
|
||||||
except (IOError, OSError) as err:
|
except (IOError, OSError) as err:
|
||||||
if getattr(err, 'errno', None) == errno.EACCES: # Permission denied
|
if getattr(err, 'errno', None) == errno.EACCES: # Permission denied
|
||||||
import traceback
|
import traceback
|
||||||
fname = getattr(err, 'filename', None)
|
fname = getattr(err, 'filename', None)
|
||||||
p = 'Locked file: %s\n\n'%fname if fname else ''
|
p = 'Locked file: %s\n\n'%fname if fname else ''
|
||||||
@ -485,7 +485,7 @@ class SeriesEdit(EditWithComplete):
|
|||||||
def initialize(self, db, id_):
|
def initialize(self, db, id_):
|
||||||
self.books_to_refresh = set([])
|
self.books_to_refresh = set([])
|
||||||
all_series = db.all_series()
|
all_series = db.all_series()
|
||||||
all_series.sort(key=lambda x : sort_key(x[1]))
|
all_series.sort(key=lambda x: sort_key(x[1]))
|
||||||
self.update_items_cache([x[1] for x in all_series])
|
self.update_items_cache([x[1] for x in all_series])
|
||||||
series_id = db.series_id(id_, index_is_id=True)
|
series_id = db.series_id(id_, index_is_id=True)
|
||||||
inval = ''
|
inval = ''
|
||||||
@ -586,7 +586,7 @@ class SeriesIndexEdit(QDoubleSpinBox):
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class BuddyLabel(QLabel): # {{{
|
class BuddyLabel(QLabel): # {{{
|
||||||
|
|
||||||
def __init__(self, buddy):
|
def __init__(self, buddy):
|
||||||
QLabel.__init__(self, buddy.LABEL)
|
QLabel.__init__(self, buddy.LABEL)
|
||||||
@ -698,11 +698,11 @@ class FormatsManager(QWidget):
|
|||||||
self.formats.setIconSize(QSize(32, 32))
|
self.formats.setIconSize(QSize(32, 32))
|
||||||
self.formats.setMaximumWidth(200)
|
self.formats.setMaximumWidth(200)
|
||||||
|
|
||||||
l.addWidget(self.cover_from_format_button, 0, 0, 1, 1)
|
l.addWidget(self.cover_from_format_button, 0, 0, 1, 1)
|
||||||
l.addWidget(self.metadata_from_format_button, 2, 0, 1, 1)
|
l.addWidget(self.metadata_from_format_button, 2, 0, 1, 1)
|
||||||
l.addWidget(self.add_format_button, 0, 2, 1, 1)
|
l.addWidget(self.add_format_button, 0, 2, 1, 1)
|
||||||
l.addWidget(self.remove_format_button, 2, 2, 1, 1)
|
l.addWidget(self.remove_format_button, 2, 2, 1, 1)
|
||||||
l.addWidget(self.formats, 0, 1, 3, 1)
|
l.addWidget(self.formats, 0, 1, 3, 1)
|
||||||
|
|
||||||
self.temp_files = []
|
self.temp_files = []
|
||||||
|
|
||||||
@ -882,7 +882,7 @@ class FormatsManager(QWidget):
|
|||||||
self.temp_files = []
|
self.temp_files = []
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class Cover(ImageView): # {{{
|
class Cover(ImageView): # {{{
|
||||||
|
|
||||||
download_cover = pyqtSignal()
|
download_cover = pyqtSignal()
|
||||||
|
|
||||||
@ -1052,7 +1052,7 @@ class Cover(ImageView): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class CommentsEdit(Editor): # {{{
|
class CommentsEdit(Editor): # {{{
|
||||||
|
|
||||||
@dynamic_property
|
@dynamic_property
|
||||||
def current_val(self):
|
def current_val(self):
|
||||||
@ -1076,7 +1076,7 @@ class CommentsEdit(Editor): # {{{
|
|||||||
return True
|
return True
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class RatingEdit(QSpinBox): # {{{
|
class RatingEdit(QSpinBox): # {{{
|
||||||
LABEL = _('&Rating:')
|
LABEL = _('&Rating:')
|
||||||
TOOLTIP = _('Rating of this book. 0-5 stars')
|
TOOLTIP = _('Rating of this book. 0-5 stars')
|
||||||
|
|
||||||
@ -1120,7 +1120,7 @@ class RatingEdit(QSpinBox): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class TagsEdit(EditWithComplete): # {{{
|
class TagsEdit(EditWithComplete): # {{{
|
||||||
LABEL = _('Ta&gs:')
|
LABEL = _('Ta&gs:')
|
||||||
TOOLTIP = '<p>'+_('Tags categorize the book. This is particularly '
|
TOOLTIP = '<p>'+_('Tags categorize the book. This is particularly '
|
||||||
'useful while searching. <br><br>They can be any words '
|
'useful while searching. <br><br>They can be any words '
|
||||||
@ -1174,7 +1174,6 @@ class TagsEdit(EditWithComplete): # {{{
|
|||||||
self.current_val = d.tags
|
self.current_val = d.tags
|
||||||
self.all_items = db.all_tags()
|
self.all_items = db.all_tags()
|
||||||
|
|
||||||
|
|
||||||
def commit(self, db, id_):
|
def commit(self, db, id_):
|
||||||
self.books_to_refresh |= db.set_tags(
|
self.books_to_refresh |= db.set_tags(
|
||||||
id_, self.current_val, notify=False, commit=False,
|
id_, self.current_val, notify=False, commit=False,
|
||||||
@ -1183,7 +1182,7 @@ class TagsEdit(EditWithComplete): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class LanguagesEdit(LE): # {{{
|
class LanguagesEdit(LE): # {{{
|
||||||
|
|
||||||
LABEL = _('&Languages:')
|
LABEL = _('&Languages:')
|
||||||
TOOLTIP = _('A comma separated list of languages for this book')
|
TOOLTIP = _('A comma separated list of languages for this book')
|
||||||
@ -1194,8 +1193,10 @@ class LanguagesEdit(LE): # {{{
|
|||||||
|
|
||||||
@dynamic_property
|
@dynamic_property
|
||||||
def current_val(self):
|
def current_val(self):
|
||||||
def fget(self): return self.lang_codes
|
def fget(self):
|
||||||
def fset(self, val): self.lang_codes = val
|
return self.lang_codes
|
||||||
|
def fset(self, val):
|
||||||
|
self.lang_codes = val
|
||||||
return property(fget=fget, fset=fset)
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
def initialize(self, db, id_):
|
||||||
@ -1221,7 +1222,7 @@ class LanguagesEdit(LE): # {{{
|
|||||||
return True
|
return True
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class IdentifiersEdit(QLineEdit): # {{{
|
class IdentifiersEdit(QLineEdit): # {{{
|
||||||
LABEL = _('I&ds:')
|
LABEL = _('I&ds:')
|
||||||
BASE_TT = _('Edit the identifiers for this book. '
|
BASE_TT = _('Edit the identifiers for this book. '
|
||||||
'For example: \n\n%s')%(
|
'For example: \n\n%s')%(
|
||||||
@ -1309,7 +1310,7 @@ class IdentifiersEdit(QLineEdit): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class ISBNDialog(QDialog) : # {{{
|
class ISBNDialog(QDialog): # {{{
|
||||||
|
|
||||||
def __init__(self, parent, txt):
|
def __init__(self, parent, txt):
|
||||||
QDialog.__init__(self, parent)
|
QDialog.__init__(self, parent)
|
||||||
@ -1320,7 +1321,7 @@ class ISBNDialog(QDialog) : # {{{
|
|||||||
l.addWidget(w, 0, 0, 1, 2)
|
l.addWidget(w, 0, 0, 1, 2)
|
||||||
w = QLabel(_('ISBN:'))
|
w = QLabel(_('ISBN:'))
|
||||||
l.addWidget(w, 1, 0, 1, 1)
|
l.addWidget(w, 1, 0, 1, 1)
|
||||||
self.line_edit = w = QLineEdit();
|
self.line_edit = w = QLineEdit()
|
||||||
w.setText(txt)
|
w.setText(txt)
|
||||||
w.selectAll()
|
w.selectAll()
|
||||||
w.textChanged.connect(self.checkText)
|
w.textChanged.connect(self.checkText)
|
||||||
@ -1361,7 +1362,7 @@ class ISBNDialog(QDialog) : # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class PublisherEdit(EditWithComplete): # {{{
|
class PublisherEdit(EditWithComplete): # {{{
|
||||||
LABEL = _('&Publisher:')
|
LABEL = _('&Publisher:')
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
@ -1388,7 +1389,7 @@ class PublisherEdit(EditWithComplete): # {{{
|
|||||||
def initialize(self, db, id_):
|
def initialize(self, db, id_):
|
||||||
self.books_to_refresh = set([])
|
self.books_to_refresh = set([])
|
||||||
all_publishers = db.all_publishers()
|
all_publishers = db.all_publishers()
|
||||||
all_publishers.sort(key=lambda x : sort_key(x[1]))
|
all_publishers.sort(key=lambda x: sort_key(x[1]))
|
||||||
self.update_items_cache([x[1] for x in all_publishers])
|
self.update_items_cache([x[1] for x in all_publishers])
|
||||||
publisher_id = db.publisher_id(id_, index_is_id=True)
|
publisher_id = db.publisher_id(id_, index_is_id=True)
|
||||||
inval = ''
|
inval = ''
|
||||||
@ -1421,7 +1422,7 @@ class DateEdit(QDateTimeEdit):
|
|||||||
ATTR = 'timestamp'
|
ATTR = 'timestamp'
|
||||||
TWEAK = 'gui_timestamp_display_format'
|
TWEAK = 'gui_timestamp_display_format'
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, create_clear_button=True):
|
||||||
QDateTimeEdit.__init__(self, parent)
|
QDateTimeEdit.__init__(self, parent)
|
||||||
self.setToolTip(self.TOOLTIP)
|
self.setToolTip(self.TOOLTIP)
|
||||||
self.setWhatsThis(self.TOOLTIP)
|
self.setWhatsThis(self.TOOLTIP)
|
||||||
@ -1435,10 +1436,11 @@ class DateEdit(QDateTimeEdit):
|
|||||||
self.setCalendarWidget(self.cw)
|
self.setCalendarWidget(self.cw)
|
||||||
self.setMinimumDateTime(UNDEFINED_QDATETIME)
|
self.setMinimumDateTime(UNDEFINED_QDATETIME)
|
||||||
self.setSpecialValueText(_('Undefined'))
|
self.setSpecialValueText(_('Undefined'))
|
||||||
self.clear_button = QToolButton(parent)
|
if create_clear_button:
|
||||||
self.clear_button.setIcon(QIcon(I('trash.png')))
|
self.clear_button = QToolButton(parent)
|
||||||
self.clear_button.setToolTip(_('Clear date'))
|
self.clear_button.setIcon(QIcon(I('trash.png')))
|
||||||
self.clear_button.clicked.connect(self.reset_date)
|
self.clear_button.setToolTip(_('Clear date'))
|
||||||
|
self.clear_button.clicked.connect(self.reset_date)
|
||||||
|
|
||||||
def reset_date(self, *args):
|
def reset_date(self, *args):
|
||||||
self.current_val = None
|
self.current_val = None
|
||||||
|
@ -6,5 +6,477 @@ from __future__ import (unicode_literals, division, absolute_import,
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
import os
|
||||||
|
from collections import OrderedDict, namedtuple
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from PyQt4.Qt import (
|
||||||
|
QDialog, QWidget, QGridLayout, QLineEdit, QLabel, QToolButton, QIcon,
|
||||||
|
QVBoxLayout, QDialogButtonBox, QApplication, pyqtSignal, QFont, QPixmap,
|
||||||
|
QSize, QPainter, Qt, QColor, QPen, QSizePolicy, QScrollArea, QFrame)
|
||||||
|
|
||||||
|
from calibre import fit_image
|
||||||
|
from calibre.ebooks.metadata import title_sort, authors_to_sort_string
|
||||||
|
from calibre.gui2 import pixmap_to_data, gprefs
|
||||||
|
from calibre.gui2.comments_editor import Editor
|
||||||
|
from calibre.gui2.metadata.basic_widgets import PubdateEdit, RatingEdit
|
||||||
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
from calibre.utils.date import UNDEFINED_DATE
|
||||||
|
|
||||||
|
Widgets = namedtuple('Widgets', 'new old label button')
|
||||||
|
|
||||||
|
# Widgets {{{
|
||||||
|
|
||||||
|
class LineEdit(QLineEdit):
|
||||||
|
|
||||||
|
changed = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, field, is_new, parent, metadata, extra):
|
||||||
|
QLineEdit.__init__(self, parent)
|
||||||
|
self.is_new = is_new
|
||||||
|
self.field = field
|
||||||
|
self.metadata = metadata
|
||||||
|
if not is_new:
|
||||||
|
self.setReadOnly(True)
|
||||||
|
self.textChanged.connect(self.changed)
|
||||||
|
|
||||||
|
def from_mi(self, mi):
|
||||||
|
val = mi.get(self.field, default='') or ''
|
||||||
|
ism = self.metadata['is_multiple']
|
||||||
|
if ism:
|
||||||
|
if not val:
|
||||||
|
val = ''
|
||||||
|
else:
|
||||||
|
val = ism['list_to_ui'].join(val)
|
||||||
|
self.setText(val)
|
||||||
|
self.setCursorPosition(0)
|
||||||
|
|
||||||
|
def to_mi(self, mi):
|
||||||
|
val = unicode(self.text()).strip()
|
||||||
|
ism = self.metadata['is_multiple']
|
||||||
|
if ism:
|
||||||
|
if not val:
|
||||||
|
val = []
|
||||||
|
else:
|
||||||
|
val = [x.strip() for x in val.split(ism['list_to_ui']) if x.strip()]
|
||||||
|
mi.set(self.field, val)
|
||||||
|
if self.field == 'title':
|
||||||
|
mi.set('title_sort', title_sort(val, lang=mi.language))
|
||||||
|
elif self.field == 'authors':
|
||||||
|
mi.set('author_sort', authors_to_sort_string(val))
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
def fget(self):
|
||||||
|
return unicode(self.text())
|
||||||
|
def fset(self, val):
|
||||||
|
self.setText(val)
|
||||||
|
self.setCursorPosition(0)
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_blank(self):
|
||||||
|
return not self.current_val.strip()
|
||||||
|
|
||||||
|
class RatingsEdit(RatingEdit):
|
||||||
|
|
||||||
|
changed = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, field, is_new, parent, metadata, extra):
|
||||||
|
RatingEdit.__init__(self, parent)
|
||||||
|
self.is_new = is_new
|
||||||
|
self.field = field
|
||||||
|
self.metadata = metadata
|
||||||
|
self.valueChanged.connect(self.changed)
|
||||||
|
if not is_new:
|
||||||
|
self.setReadOnly(True)
|
||||||
|
|
||||||
|
def from_mi(self, mi):
|
||||||
|
val = (mi.get(self.field, default=0) or 0)/2
|
||||||
|
self.setValue(val)
|
||||||
|
|
||||||
|
def to_mi(self, mi):
|
||||||
|
mi.set(self.field, self.value() * 2)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_blank(self):
|
||||||
|
return self.value() == 0
|
||||||
|
|
||||||
|
class DateEdit(PubdateEdit):
|
||||||
|
|
||||||
|
changed = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, field, is_new, parent, metadata, extra):
|
||||||
|
PubdateEdit.__init__(self, parent, create_clear_button=False)
|
||||||
|
self.is_new = is_new
|
||||||
|
self.field = field
|
||||||
|
self.metadata = metadata
|
||||||
|
self.setDisplayFormat(extra)
|
||||||
|
self.dateTimeChanged.connect(self.changed)
|
||||||
|
if not is_new:
|
||||||
|
self.setReadOnly(True)
|
||||||
|
|
||||||
|
def from_mi(self, mi):
|
||||||
|
self.current_val = mi.get(self.field, default=None)
|
||||||
|
|
||||||
|
def to_mi(self, mi):
|
||||||
|
mi.set(self.field, self.current_val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_blank(self):
|
||||||
|
return self.current_val == UNDEFINED_DATE
|
||||||
|
|
||||||
|
class SeriesEdit(LineEdit):
|
||||||
|
|
||||||
|
def from_mi(self, mi):
|
||||||
|
series = mi.get(self.field, default='')
|
||||||
|
series_index = mi.get(self.field + '_index', default=1.0)
|
||||||
|
val = ''
|
||||||
|
if series:
|
||||||
|
val = '%s [%s]' % (series, mi.format_series_index(series_index))
|
||||||
|
self.setText(val)
|
||||||
|
self.setCursorPosition(0)
|
||||||
|
|
||||||
|
def to_mi(self, mi):
|
||||||
|
val = unicode(self.text()).strip()
|
||||||
|
try:
|
||||||
|
series_index = float(val.rpartition('[')[-1].rstrip(']').strip())
|
||||||
|
except:
|
||||||
|
series_index = 1.0
|
||||||
|
series = val.rpartition('[')[0].strip() or None
|
||||||
|
mi.set(self.field, series)
|
||||||
|
mi.set(self.field + '_index', series_index)
|
||||||
|
|
||||||
|
class IdentifiersEdit(LineEdit):
|
||||||
|
|
||||||
|
def from_mi(self, mi):
|
||||||
|
val = ('%s:%s' % (k, v) for k, v in mi.identifiers.iteritems())
|
||||||
|
self.setText(', '.join(val))
|
||||||
|
|
||||||
|
def to_mi(self, mi):
|
||||||
|
parts = (x.strip() for x in self.current_val.split(',') if x.strip())
|
||||||
|
val = {x.partition(':')[0].strip():x.partition(':')[-1].strip() for x in parts}
|
||||||
|
mi.set_identifiers({k:v for k, v in val.iteritems() if k and v})
|
||||||
|
|
||||||
|
class CommentsEdit(Editor):
|
||||||
|
|
||||||
|
changed = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, field, is_new, parent, metadata, extra):
|
||||||
|
Editor.__init__(self, parent, one_line_toolbar=False)
|
||||||
|
self.is_new = is_new
|
||||||
|
self.field = field
|
||||||
|
self.metadata = metadata
|
||||||
|
self.hide_tabs()
|
||||||
|
if not is_new:
|
||||||
|
self.hide_toolbars()
|
||||||
|
self.set_readonly(True)
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
def fget(self):
|
||||||
|
return self.html
|
||||||
|
def fset(self, val):
|
||||||
|
self.html = val or ''
|
||||||
|
self.changed.emit()
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def from_mi(self, mi):
|
||||||
|
val = mi.get(self.field, default='')
|
||||||
|
self.current_val = val
|
||||||
|
|
||||||
|
def to_mi(self, mi):
|
||||||
|
mi.set(self.field, self.current_val)
|
||||||
|
|
||||||
|
def sizeHint(self):
|
||||||
|
return QSize(450, 200)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_blank(self):
|
||||||
|
return not self.current_val.strip()
|
||||||
|
|
||||||
|
class CoverView(QWidget):
|
||||||
|
|
||||||
|
changed = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, field, is_new, parent, metadata, extra):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self.is_new = is_new
|
||||||
|
self.field = field
|
||||||
|
self.metadata = metadata
|
||||||
|
self.pixmap = None
|
||||||
|
self.blank = QPixmap(I('blank.png'))
|
||||||
|
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.GrowFlag|QSizePolicy.ExpandFlag)
|
||||||
|
self.sizePolicy().setHeightForWidth(True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_blank(self):
|
||||||
|
return self.pixmap is None
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
|
def current_val(self):
|
||||||
|
def fget(self):
|
||||||
|
return self.pixmap
|
||||||
|
def fset(self, val):
|
||||||
|
self.pixmap = val
|
||||||
|
self.changed.emit()
|
||||||
|
self.update()
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
|
def from_mi(self, mi):
|
||||||
|
p = getattr(mi, 'cover', None)
|
||||||
|
if p and os.path.exists(p):
|
||||||
|
pmap = QPixmap()
|
||||||
|
with open(p, 'rb') as f:
|
||||||
|
pmap.loadFromData(f.read())
|
||||||
|
if not pmap.isNull():
|
||||||
|
self.pixmap = pmap
|
||||||
|
self.update()
|
||||||
|
self.changed.emit()
|
||||||
|
return
|
||||||
|
cd = getattr(mi, 'cover_data', (None, None))
|
||||||
|
if cd and cd[1]:
|
||||||
|
pmap = QPixmap()
|
||||||
|
pmap.loadFromData(cd[1])
|
||||||
|
if not pmap.isNull():
|
||||||
|
self.pixmap = pmap
|
||||||
|
self.update()
|
||||||
|
self.changed.emit()
|
||||||
|
return
|
||||||
|
self.pixmap = None
|
||||||
|
self.update()
|
||||||
|
self.changed.emit()
|
||||||
|
|
||||||
|
def to_mi(self, mi):
|
||||||
|
mi.cover, mi.cover_data = None, (None, None)
|
||||||
|
if self.pixmap is not None and not self.pixmap.isNull():
|
||||||
|
with PersistentTemporaryFile('.jpg') as pt:
|
||||||
|
pt.write(pixmap_to_data(self.pixmap))
|
||||||
|
mi.cover = pt.name
|
||||||
|
|
||||||
|
def sizeHint(self):
|
||||||
|
return QSize(225, 300)
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
pmap = self.blank if self.pixmap is None or self.pixmap.isNull() else self.pixmap
|
||||||
|
target = self.rect()
|
||||||
|
scaled, width, height = fit_image(pmap.width(), pmap.height(), target.width(), target.height())
|
||||||
|
target.setRect(target.x(), target.y(), width, height)
|
||||||
|
p = QPainter(self)
|
||||||
|
p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
|
||||||
|
p.drawPixmap(target, pmap)
|
||||||
|
|
||||||
|
if self.pixmap is not None and not self.pixmap.isNull():
|
||||||
|
sztgt = target.adjusted(0, 0, 0, -4)
|
||||||
|
f = p.font()
|
||||||
|
f.setBold(True)
|
||||||
|
p.setFont(f)
|
||||||
|
sz = u'\u00a0%d x %d\u00a0'%(self.pixmap.width(), self.pixmap.height())
|
||||||
|
flags = Qt.AlignBottom|Qt.AlignRight|Qt.TextSingleLine
|
||||||
|
szrect = p.boundingRect(sztgt, flags, sz)
|
||||||
|
p.fillRect(szrect.adjusted(0, 0, 0, 4), QColor(0, 0, 0, 200))
|
||||||
|
p.setPen(QPen(QColor(255,255,255)))
|
||||||
|
p.drawText(sztgt, flags, sz)
|
||||||
|
p.end()
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class CompareSingle(QWidget):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, field_metadata, parent=None, revert_tooltip=None,
|
||||||
|
datetime_fmt='MMMM yyyy', blank_as_equal=True,
|
||||||
|
fields=('title', 'authors', 'series', 'tags', 'rating', 'publisher', 'pubdate', 'identifiers', 'comments', 'cover')):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self.l = l = QGridLayout()
|
||||||
|
l.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.setLayout(l)
|
||||||
|
revert_tooltip = revert_tooltip or _('Revert %s')
|
||||||
|
self.current_mi = None
|
||||||
|
self.changed_font = QFont(QApplication.font())
|
||||||
|
self.changed_font.setBold(True)
|
||||||
|
self.changed_font.setItalic(True)
|
||||||
|
self.blank_as_equal = blank_as_equal
|
||||||
|
|
||||||
|
self.widgets = OrderedDict()
|
||||||
|
row = 0
|
||||||
|
|
||||||
|
for field in fields:
|
||||||
|
m = field_metadata[field]
|
||||||
|
dt = m['datatype']
|
||||||
|
extra = None
|
||||||
|
if 'series' in {field, dt}:
|
||||||
|
cls = SeriesEdit
|
||||||
|
elif field == 'identifiers':
|
||||||
|
cls = IdentifiersEdit
|
||||||
|
elif 'comments' in {field, dt}:
|
||||||
|
cls = CommentsEdit
|
||||||
|
elif 'rating' in {field, dt}:
|
||||||
|
cls = RatingsEdit
|
||||||
|
elif dt == 'datetime':
|
||||||
|
extra = datetime_fmt
|
||||||
|
cls = DateEdit
|
||||||
|
elif field == 'cover':
|
||||||
|
cls = CoverView
|
||||||
|
elif dt in {'text', 'enum'}:
|
||||||
|
cls = LineEdit
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
neww = cls(field, True, self, m, extra)
|
||||||
|
neww.changed.connect(partial(self.changed, field))
|
||||||
|
oldw = cls(field, False, self, m, extra)
|
||||||
|
newl = QLabel('&%s:' % m['name'])
|
||||||
|
newl.setBuddy(neww)
|
||||||
|
button = QToolButton(self)
|
||||||
|
button.setIcon(QIcon(I('back.png')))
|
||||||
|
button.clicked.connect(partial(self.revert, field))
|
||||||
|
button.setToolTip(revert_tooltip % m['name'])
|
||||||
|
self.widgets[field] = Widgets(neww, oldw, newl, button)
|
||||||
|
for i, w in enumerate((newl, neww, button, oldw)):
|
||||||
|
c = i if i < 2 else i + 1
|
||||||
|
if w is oldw:
|
||||||
|
c += 1
|
||||||
|
l.addWidget(w, row, c)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
self.sep = f = QFrame(self)
|
||||||
|
f.setFrameShape(f.VLine)
|
||||||
|
l.addWidget(f, 0, 2, row, 1)
|
||||||
|
self.sep2 = f = QFrame(self)
|
||||||
|
f.setFrameShape(f.VLine)
|
||||||
|
l.addWidget(f, 0, 4, row, 1)
|
||||||
|
|
||||||
|
def changed(self, field):
|
||||||
|
w = self.widgets[field]
|
||||||
|
if w.new.current_val != w.old.current_val and (not self.blank_as_equal or not w.new.is_blank):
|
||||||
|
w.label.setFont(self.changed_font)
|
||||||
|
else:
|
||||||
|
w.label.setFont(QApplication.font())
|
||||||
|
|
||||||
|
def revert(self, field):
|
||||||
|
widgets = self.widgets[field]
|
||||||
|
neww, oldw = widgets[:2]
|
||||||
|
neww.current_val = oldw.current_val
|
||||||
|
|
||||||
|
def __call__(self, oldmi, newmi):
|
||||||
|
self.current_mi = newmi
|
||||||
|
self.initial_vals = {}
|
||||||
|
for field, widgets in self.widgets.iteritems():
|
||||||
|
widgets.old.from_mi(oldmi)
|
||||||
|
widgets.new.from_mi(newmi)
|
||||||
|
self.initial_vals[field] = widgets.new.current_val
|
||||||
|
|
||||||
|
def apply_changes(self):
|
||||||
|
changed = False
|
||||||
|
for field, widgets in self.widgets.iteritems():
|
||||||
|
val = widgets.new.current_val
|
||||||
|
if val != self.initial_vals[field]:
|
||||||
|
widgets.new.to_mi(self.current_mi)
|
||||||
|
changed = True
|
||||||
|
return changed
|
||||||
|
|
||||||
|
class CompareMany(QDialog):
|
||||||
|
|
||||||
|
def __init__(self, ids, get_metadata, field_metadata, parent=None, window_title=None, skip_button_tooltip=None,
|
||||||
|
accept_all_tooltip=None, reject_all_tooltip=None, **kwargs):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
self.l = l = QVBoxLayout()
|
||||||
|
self.setLayout(l)
|
||||||
|
self.setWindowIcon(QIcon(I('auto_author_sort.png')))
|
||||||
|
self.get_metadata = get_metadata
|
||||||
|
self.ids = list(ids)
|
||||||
|
self.total = len(self.ids)
|
||||||
|
self.accepted = OrderedDict()
|
||||||
|
self.window_title = window_title or _('Compare metadata')
|
||||||
|
|
||||||
|
self.compare_widget = CompareSingle(field_metadata, parent=parent, **kwargs)
|
||||||
|
self.sa = sa = QScrollArea()
|
||||||
|
l.addWidget(sa)
|
||||||
|
sa.setWidget(self.compare_widget)
|
||||||
|
sa.setWidgetResizable(True)
|
||||||
|
|
||||||
|
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Cancel)
|
||||||
|
bb.rejected.connect(self.reject)
|
||||||
|
self.aarb = b = bb.addButton(_('&Accept all remaining'), bb.YesRole)
|
||||||
|
if accept_all_tooltip:
|
||||||
|
b.setToolTip(accept_all_tooltip)
|
||||||
|
b.clicked.connect(self.accept_all_remaining)
|
||||||
|
self.rarb = b = bb.addButton(_('Re&ject all remaining'), bb.NoRole)
|
||||||
|
if reject_all_tooltip:
|
||||||
|
b.setToolTip(reject_all_tooltip)
|
||||||
|
b.clicked.connect(self.reject_all_remaining)
|
||||||
|
self.sb = b = bb.addButton(_('&Reject'), bb.ActionRole)
|
||||||
|
b.clicked.connect(partial(self.next_item, False))
|
||||||
|
if skip_button_tooltip:
|
||||||
|
b.setToolTip(skip_button_tooltip)
|
||||||
|
self.nb = b = bb.addButton(_('&Next'), bb.ActionRole)
|
||||||
|
b.setIcon(QIcon(I('forward.png')))
|
||||||
|
b.clicked.connect(partial(self.next_item, True))
|
||||||
|
b.setDefault(True)
|
||||||
|
l.addWidget(bb)
|
||||||
|
|
||||||
|
self.next_item(True)
|
||||||
|
|
||||||
|
desktop = QApplication.instance().desktop()
|
||||||
|
geom = desktop.availableGeometry(parent or self)
|
||||||
|
width = max(700, min(950, geom.width()-50))
|
||||||
|
height = max(650, min(1000, geom.height()-100))
|
||||||
|
self.resize(QSize(width, height))
|
||||||
|
geom = gprefs.get('diff_dialog_geom', None)
|
||||||
|
if geom is not None:
|
||||||
|
self.restoreGeometry(geom)
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
gprefs.set('diff_dialog_geom', bytearray(self.saveGeometry()))
|
||||||
|
super(CompareMany, self).accept()
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
gprefs.set('diff_dialog_geom', bytearray(self.saveGeometry()))
|
||||||
|
super(CompareMany, self).reject()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_mi(self):
|
||||||
|
return self.compare_widget.current_mi
|
||||||
|
|
||||||
|
def next_item(self, accept):
|
||||||
|
if not self.ids:
|
||||||
|
return self.accept()
|
||||||
|
if self.current_mi is not None:
|
||||||
|
changed = self.compare_widget.apply_changes()
|
||||||
|
self.setWindowTitle(self.window_title + _(' [%(num)d of %(tot)d]') % dict(
|
||||||
|
num=(self.total - len(self.ids) + 1), tot=self.total))
|
||||||
|
oldmi, newmi = self.get_metadata(self.ids[0])
|
||||||
|
old_id = self.ids.pop(0)
|
||||||
|
if self.current_mi is not None:
|
||||||
|
self.accepted[old_id] = (changed, self.current_mi) if accept else (False, None)
|
||||||
|
self.compare_widget(oldmi, newmi)
|
||||||
|
|
||||||
|
def accept_all_remaining(self):
|
||||||
|
self.next_item(True)
|
||||||
|
for id_ in self.ids:
|
||||||
|
oldmi, newmi = self.get_metadata(id_)
|
||||||
|
self.accepted[id_] = (False, newmi)
|
||||||
|
self.ids = []
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def reject_all_remaining(self):
|
||||||
|
self.next_item(False)
|
||||||
|
for id_ in self.ids:
|
||||||
|
oldmi, newmi = self.get_metadata(id_)
|
||||||
|
self.accepted[id_] = (False, None)
|
||||||
|
self.ids = []
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app = QApplication([])
|
||||||
|
from calibre.library import db
|
||||||
|
db = db()
|
||||||
|
ids = sorted(db.all_ids(), reverse=True)
|
||||||
|
ids = [(x, ids[i+1]) for i, x in enumerate(ids[0::2])]
|
||||||
|
gm = partial(db.get_metadata, index_is_id=True, get_cover=True, cover_as_data=True)
|
||||||
|
get_metadata = lambda x:map(gm, ids[x])
|
||||||
|
d = CompareMany(list(xrange(len(ids))), get_metadata, db.field_metadata)
|
||||||
|
if d.exec_() == d.Accepted:
|
||||||
|
for changed, mi in d.accepted.itervalues():
|
||||||
|
if changed and mi is not None:
|
||||||
|
print (mi)
|
||||||
|
|
||||||
|
@ -252,7 +252,7 @@ class FieldMetadata(dict):
|
|||||||
'datatype':'int',
|
'datatype':'int',
|
||||||
'is_multiple':{},
|
'is_multiple':{},
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
'name':None,
|
'name':_('Cover'),
|
||||||
'search_terms':['cover'],
|
'search_terms':['cover'],
|
||||||
'is_custom':False,
|
'is_custom':False,
|
||||||
'is_category':False,
|
'is_category':False,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user