mirror of
https://github.com/kovidgoyal/calibre.git
synced 2026-02-19 17:50:10 -05:00
1032 lines
42 KiB
Python
1032 lines
42 KiB
Python
__license__ = 'GPL v3'
|
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|
|
|
'''
|
|
The dialog used to edit meta information for a book as well as
|
|
add/remove formats
|
|
'''
|
|
|
|
import os, re, time, traceback, textwrap
|
|
from functools import partial
|
|
from threading import Thread
|
|
|
|
from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QDate, \
|
|
QPixmap, QListWidgetItem, QDialog, pyqtSignal, QIcon, \
|
|
QPushButton, QKeySequence
|
|
|
|
from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \
|
|
choose_files, choose_images, ResizableDialog, \
|
|
warning_dialog, question_dialog, UNDEFINED_QDATE
|
|
from calibre.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog
|
|
from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
|
|
from calibre.gui2.dialogs.tag_editor import TagEditor
|
|
from calibre.gui2.widgets import ProgressIndicator
|
|
from calibre.ebooks import BOOK_EXTENSIONS
|
|
from calibre.ebooks.metadata import string_to_authors, \
|
|
authors_to_string, check_isbn, title_sort
|
|
from calibre.ebooks.metadata.covers import download_cover
|
|
from calibre.ebooks.metadata import MetaInformation
|
|
from calibre.utils.config import prefs, tweaks
|
|
from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp
|
|
from calibre.utils.icu import sort_key
|
|
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
|
|
from calibre.gui2.preferences.social import SocialMetadata
|
|
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
|
from calibre import strftime
|
|
from calibre.library.comments import comments_to_html
|
|
|
|
class CoverFetcher(Thread): # {{{
|
|
|
|
def __init__(self, username, password, isbn, timeout, title, author):
|
|
Thread.__init__(self)
|
|
self.daemon = True
|
|
|
|
self.username = username.strip() if username else username
|
|
self.password = password.strip() if password else password
|
|
self.timeout = timeout
|
|
self.isbn = isbn
|
|
self.title = title
|
|
self.needs_isbn = False
|
|
self.author = author
|
|
self.exception = self.traceback = self.cover_data = self.errors = None
|
|
|
|
def run(self):
|
|
try:
|
|
au = self.author if self.author else None
|
|
mi = MetaInformation(self.title, [au])
|
|
if not self.isbn:
|
|
from calibre.ebooks.metadata.fetch import search
|
|
if not self.title:
|
|
self.needs_isbn = True
|
|
return
|
|
key = get_isbndb_key()
|
|
if not key:
|
|
key = None
|
|
results = search(title=self.title, author=au,
|
|
isbndb_key=key)[0]
|
|
results = sorted([x.isbn for x in results if x.isbn],
|
|
cmp=lambda x,y:cmp(len(x),len(y)), reverse=True)
|
|
if not results:
|
|
self.needs_isbn = True
|
|
return
|
|
self.isbn = results[0]
|
|
|
|
mi.isbn = self.isbn
|
|
|
|
self.cover_data, self.errors = download_cover(mi,
|
|
timeout=self.timeout)
|
|
except Exception as e:
|
|
self.exception = e
|
|
self.traceback = traceback.format_exc()
|
|
print self.traceback
|
|
|
|
# }}}
|
|
|
|
class Format(QListWidgetItem): # {{{
|
|
|
|
def __init__(self, parent, ext, size, path=None, timestamp=None):
|
|
self.path = path
|
|
self.ext = ext
|
|
self.size = float(size)/(1024*1024)
|
|
text = '%s (%.2f MB)'%(self.ext.upper(), self.size)
|
|
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
|
|
text, parent, QListWidgetItem.UserType)
|
|
if timestamp is not None:
|
|
ts = timestamp.astimezone(local_tz)
|
|
t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple())
|
|
text = _('Last modified: %s')%t
|
|
self.setToolTip(text)
|
|
self.setStatusTip(text)
|
|
|
|
# }}}
|
|
|
|
|
|
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|
|
|
COVER_FETCH_TIMEOUT = 240 # seconds
|
|
view_format = pyqtSignal(object)
|
|
|
|
# Cover processing {{{
|
|
|
|
def set_cover(self):
|
|
mi, ext = self.get_selected_format_metadata()
|
|
if mi is None:
|
|
return
|
|
cdata = None
|
|
if mi.cover and os.access(mi.cover, os.R_OK):
|
|
cdata = open(mi.cover).read()
|
|
elif mi.cover_data[1] is not None:
|
|
cdata = mi.cover_data[1]
|
|
if cdata is None:
|
|
error_dialog(self, _('Could not read cover'),
|
|
_('Could not read cover from %s format')%ext).exec_()
|
|
return
|
|
pix = QPixmap()
|
|
pix.loadFromData(cdata)
|
|
if pix.isNull():
|
|
error_dialog(self, _('Could not read cover'),
|
|
_('The cover in the %s format is invalid')%ext).exec_()
|
|
return
|
|
self.cover.setPixmap(pix)
|
|
self.update_cover_tooltip()
|
|
self.cover_changed = True
|
|
self.cpixmap = pix
|
|
self.cover_data = cdata
|
|
|
|
def trim_cover(self, *args):
|
|
from calibre.utils.magick import Image
|
|
cdata = self.cover_data
|
|
if not cdata:
|
|
return
|
|
im = Image()
|
|
im.load(cdata)
|
|
im.trim(10)
|
|
cdata = im.export('png')
|
|
pix = QPixmap()
|
|
pix.loadFromData(cdata)
|
|
self.cover.setPixmap(pix)
|
|
self.update_cover_tooltip()
|
|
self.cover_changed = True
|
|
self.cpixmap = pix
|
|
self.cover_data = cdata
|
|
|
|
|
|
|
|
def update_cover_tooltip(self):
|
|
p = self.cover.pixmap()
|
|
self.cover.setToolTip(_('Cover size: %dx%d pixels') %
|
|
(p.width(), p.height()))
|
|
|
|
|
|
def do_reset_cover(self, *args):
|
|
pix = QPixmap(I('default_cover.png'))
|
|
self.cover.setPixmap(pix)
|
|
self.update_cover_tooltip()
|
|
self.cover_changed = True
|
|
self.cover_data = None
|
|
|
|
def select_cover(self, checked):
|
|
files = choose_images(self, 'change cover dialog',
|
|
_('Choose cover for ') + unicode(self.title.text()))
|
|
if not files:
|
|
return
|
|
_file = files[0]
|
|
if _file:
|
|
_file = os.path.abspath(_file)
|
|
if not os.access(_file, os.R_OK):
|
|
d = error_dialog(self, _('Cannot read'),
|
|
_('You do not have permission to read the file: ') + _file)
|
|
d.exec_()
|
|
return
|
|
cf, cover = None, None
|
|
try:
|
|
cf = open(_file, "rb")
|
|
cover = cf.read()
|
|
except IOError as e:
|
|
d = error_dialog(self, _('Error reading file'),
|
|
_("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e))
|
|
d.exec_()
|
|
if cover:
|
|
pix = QPixmap()
|
|
pix.loadFromData(cover)
|
|
if pix.isNull():
|
|
d = error_dialog(self,
|
|
_("Not a valid picture"),
|
|
_file + _(" is not a valid picture"))
|
|
d.exec_()
|
|
else:
|
|
self.cover.setPixmap(pix)
|
|
self.update_cover_tooltip()
|
|
self.cover_changed = True
|
|
self.cpixmap = pix
|
|
self.cover_data = cover
|
|
|
|
def generate_cover(self, *args):
|
|
from calibre.ebooks import calibre_cover
|
|
from calibre.ebooks.metadata import fmt_sidx
|
|
from calibre.gui2 import config
|
|
title = unicode(self.title.text()).strip()
|
|
author = unicode(self.authors.text()).strip()
|
|
if author.endswith('&'):
|
|
author = author[:-1].strip()
|
|
if not title or not author:
|
|
return error_dialog(self, _('Specify title and author'),
|
|
_('You must specify a title and author before generating '
|
|
'a cover'), show=True)
|
|
series = unicode(self.series.text()).strip()
|
|
series_string = None
|
|
if series:
|
|
series_string = _('Book %s of %s')%(
|
|
fmt_sidx(self.series_index.value(),
|
|
use_roman=config['use_roman_numerals_for_series_number']), series)
|
|
self.cover_data = calibre_cover(title, author,
|
|
series_string=series_string)
|
|
pix = QPixmap()
|
|
pix.loadFromData(self.cover_data)
|
|
self.cover.setPixmap(pix)
|
|
self.update_cover_tooltip()
|
|
self.cover_changed = True
|
|
self.cpixmap = pix
|
|
|
|
def cover_dropped(self, cover_data):
|
|
self.cover_changed = True
|
|
self.cover_data = cover_data
|
|
self.update_cover_tooltip()
|
|
|
|
def fetch_cover(self):
|
|
isbn = re.sub(r'[^0-9a-zA-Z]', '', unicode(self.isbn.text())).strip()
|
|
self.fetch_cover_button.setEnabled(False)
|
|
self.setCursor(Qt.WaitCursor)
|
|
title, author = map(unicode, (self.title.text(), self.authors.text()))
|
|
self.cover_fetcher = CoverFetcher(None, None, isbn,
|
|
self.timeout, title, author)
|
|
self.cover_fetcher.start()
|
|
self.cf_start_time = time.time()
|
|
self.pi.start(_('Downloading cover...'))
|
|
QTimer.singleShot(100, self.hangcheck)
|
|
|
|
def hangcheck(self):
|
|
cf = self.cover_fetcher
|
|
if cf is None:
|
|
# Called after dialog closed
|
|
return
|
|
|
|
if cf.is_alive() and \
|
|
time.time()-self.cf_start_time < self.COVER_FETCH_TIMEOUT:
|
|
QTimer.singleShot(100, self.hangcheck)
|
|
return
|
|
|
|
try:
|
|
if cf.is_alive():
|
|
error_dialog(self, _('Cannot fetch cover'),
|
|
_('<b>Could not fetch cover.</b><br/>')+
|
|
_('The download timed out.')).exec_()
|
|
return
|
|
if cf.needs_isbn:
|
|
error_dialog(self, _('Cannot fetch cover'),
|
|
_('Could not find cover for this book. Try '
|
|
'specifying the ISBN first.')).exec_()
|
|
return
|
|
if cf.exception is not None:
|
|
err = cf.exception
|
|
error_dialog(self, _('Cannot fetch cover'),
|
|
_('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_()
|
|
return
|
|
if cf.errors and cf.cover_data is None:
|
|
details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in cf.errors])
|
|
error_dialog(self, _('Cannot fetch cover'),
|
|
_('<b>Could not fetch cover.</b><br/>') +
|
|
_('For the error message from each cover source, '
|
|
'click Show details below.'), det_msg=details, show=True)
|
|
return
|
|
|
|
pix = QPixmap()
|
|
pix.loadFromData(cf.cover_data)
|
|
if pix.isNull():
|
|
error_dialog(self, _('Bad cover'),
|
|
_('The cover is not a valid picture')).exec_()
|
|
else:
|
|
self.cover.setPixmap(pix)
|
|
self.update_cover_tooltip()
|
|
self.cover_changed = True
|
|
self.cpixmap = pix
|
|
self.cover_data = cf.cover_data
|
|
finally:
|
|
self.fetch_cover_button.setEnabled(True)
|
|
self.unsetCursor()
|
|
if self.pi is not None:
|
|
self.pi.stop()
|
|
|
|
|
|
# }}}
|
|
|
|
# Formats processing {{{
|
|
def add_format(self, x):
|
|
files = choose_files(self, 'add formats dialog',
|
|
_("Choose formats for ") + unicode((self.title.text())),
|
|
[(_('Books'), BOOK_EXTENSIONS)])
|
|
self._add_formats(files)
|
|
|
|
def _add_formats(self, paths):
|
|
added = False
|
|
if not paths:
|
|
return added
|
|
bad_perms = []
|
|
for _file in paths:
|
|
_file = os.path.abspath(_file)
|
|
if not os.access(_file, os.R_OK):
|
|
bad_perms.append(_file)
|
|
continue
|
|
|
|
nfile = run_plugins_on_import(_file)
|
|
if nfile is not None:
|
|
_file = nfile
|
|
stat = os.stat(_file)
|
|
size = stat.st_size
|
|
ext = os.path.splitext(_file)[1].lower().replace('.', '')
|
|
timestamp = utcfromtimestamp(stat.st_mtime)
|
|
for row in range(self.formats.count()):
|
|
fmt = self.formats.item(row)
|
|
if fmt.ext.lower() == ext:
|
|
self.formats.takeItem(row)
|
|
break
|
|
Format(self.formats, ext, size, path=_file, timestamp=timestamp)
|
|
self.formats_changed = True
|
|
added = True
|
|
if bad_perms:
|
|
error_dialog(self, _('No permission'),
|
|
_('You do not have '
|
|
'permission to read the following files:'),
|
|
det_msg='\n'.join(bad_perms), show=True)
|
|
|
|
return added
|
|
|
|
def formats_dropped(self, event, paths):
|
|
if self._add_formats(paths):
|
|
event.accept()
|
|
|
|
def remove_format(self, *args):
|
|
rows = self.formats.selectionModel().selectedRows(0)
|
|
for row in rows:
|
|
self.formats.takeItem(row.row())
|
|
self.formats_changed = True
|
|
|
|
def get_selected_format_metadata(self):
|
|
from calibre.ebooks.metadata.meta import get_metadata
|
|
old = prefs['read_file_metadata']
|
|
if not old:
|
|
prefs['read_file_metadata'] = True
|
|
try:
|
|
row = self.formats.currentRow()
|
|
fmt = self.formats.item(row)
|
|
if fmt is None:
|
|
if self.formats.count() == 1:
|
|
fmt = self.formats.item(0)
|
|
if fmt is None:
|
|
error_dialog(self, _('No format selected'),
|
|
_('No format selected')).exec_()
|
|
return None, None
|
|
ext = fmt.ext.lower()
|
|
if fmt.path is None:
|
|
stream = self.db.format(self.row, ext, as_file=True)
|
|
else:
|
|
stream = open(fmt.path, 'r+b')
|
|
try:
|
|
mi = get_metadata(stream, ext)
|
|
return mi, ext
|
|
except:
|
|
error_dialog(self, _('Could not read metadata'),
|
|
_('Could not read metadata from %s format')%ext).exec_()
|
|
return None, None
|
|
finally:
|
|
if old != prefs['read_file_metadata']:
|
|
prefs['read_file_metadata'] = old
|
|
|
|
def set_metadata_from_format(self):
|
|
mi, ext = self.get_selected_format_metadata()
|
|
if mi is None:
|
|
return
|
|
if mi.title:
|
|
self.title.setText(mi.title)
|
|
if mi.authors:
|
|
self.authors.setEditText(authors_to_string(mi.authors))
|
|
if mi.author_sort:
|
|
self.author_sort.setText(mi.author_sort)
|
|
if mi.rating is not None:
|
|
try:
|
|
self.rating.setValue(mi.rating)
|
|
except:
|
|
pass
|
|
if mi.publisher:
|
|
self.publisher.setEditText(mi.publisher)
|
|
if mi.tags:
|
|
self.tags.setText(', '.join(mi.tags))
|
|
if mi.isbn:
|
|
self.isbn.setText(mi.isbn)
|
|
if mi.pubdate:
|
|
self.pubdate.setDate(QDate(mi.pubdate.year, mi.pubdate.month,
|
|
mi.pubdate.day))
|
|
if mi.series and mi.series.strip():
|
|
self.series.setEditText(mi.series)
|
|
if mi.series_index is not None:
|
|
self.series_index.setValue(float(mi.series_index))
|
|
if mi.comments and mi.comments.strip():
|
|
comments = comments_to_html(mi.comments)
|
|
self.comments.html = comments
|
|
|
|
|
|
def sync_formats(self):
|
|
old_extensions, new_extensions, paths = set(), set(), {}
|
|
for row in range(self.formats.count()):
|
|
fmt = self.formats.item(row)
|
|
ext, path = fmt.ext.lower(), fmt.path
|
|
if 'unknown' in ext.lower():
|
|
ext = None
|
|
if path:
|
|
new_extensions.add(ext)
|
|
paths[ext] = path
|
|
else:
|
|
old_extensions.add(ext)
|
|
for ext in new_extensions:
|
|
self.db.add_format(self.row, ext, open(paths[ext], 'rb'), notify=False)
|
|
dbfmts = self.db.formats(self.row)
|
|
db_extensions = set([f.lower() for f in (dbfmts.split(',') if dbfmts
|
|
else [])])
|
|
extensions = new_extensions.union(old_extensions)
|
|
for ext in db_extensions:
|
|
if ext not in extensions and ext in self.original_formats:
|
|
self.db.remove_format(self.row, ext, notify=False)
|
|
|
|
def show_format(self, item, *args):
|
|
fmt = item.ext
|
|
self.view_format.emit(fmt)
|
|
|
|
# }}}
|
|
|
|
def __init__(self, window, row, db, prev=None,
|
|
next_=None):
|
|
ResizableDialog.__init__(self, window)
|
|
self.cover_fetcher = None
|
|
self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
|
|
base = unicode(self.author_sort.toolTip())
|
|
ok_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+
|
|
_(' The green color indicates that the current '
|
|
'author sort matches the current author'))
|
|
bad_tooltip = '<p>'+textwrap.fill(base + '<br><br>'+
|
|
_(' The red color indicates that the current '
|
|
'author sort does not match the current author. '
|
|
'No action is required if this is what you want.'))
|
|
self.aus_tooltips = (ok_tooltip, bad_tooltip)
|
|
|
|
base = unicode(self.title_sort.toolTip())
|
|
ok_tooltip = '<p>' + textwrap.fill(base+'<br><br>'+
|
|
_(' The green color indicates that the current '
|
|
'title sort matches the current title'))
|
|
bad_tooltip = '<p>'+textwrap.fill(base + '<br><br>'+
|
|
_(' The red color warns that the current '
|
|
'title sort does not match the current title. '
|
|
'No action is required if this is what you want.'))
|
|
self.ts_tooltips = (ok_tooltip, bad_tooltip)
|
|
self.row_delta = 0
|
|
if prev:
|
|
self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'),
|
|
self)
|
|
self.button_box.addButton(self.prev_button, self.button_box.ActionRole)
|
|
tip = (_('Save changes and edit the metadata of %s')+' [Alt+Left]')%prev
|
|
self.prev_button.setToolTip(tip)
|
|
self.prev_button.clicked.connect(partial(self.next_triggered,
|
|
-1))
|
|
self.prev_button.setShortcut(QKeySequence('Alt+Left'))
|
|
if next_:
|
|
self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'),
|
|
self)
|
|
self.button_box.addButton(self.next_button, self.button_box.ActionRole)
|
|
tip = (_('Save changes and edit the metadata of %s')+' [Alt+Right]')%next_
|
|
self.next_button.setToolTip(tip)
|
|
self.next_button.clicked.connect(partial(self.next_triggered, 1))
|
|
self.next_button.setShortcut(QKeySequence('Alt+Right'))
|
|
|
|
self.splitter.setStretchFactor(100, 1)
|
|
self.read_state()
|
|
self.db = db
|
|
self.pi = ProgressIndicator(self)
|
|
self.id = db.id(row)
|
|
self.row = row
|
|
self.cover_data = None
|
|
self.formats_changed = False
|
|
self.formats.setAcceptDrops(True)
|
|
self.cover_changed = False
|
|
self.cpixmap = None
|
|
self.pubdate.setMinimumDate(UNDEFINED_QDATE)
|
|
pubdate_format = tweaks['gui_pubdate_display_format']
|
|
if pubdate_format is not None:
|
|
self.pubdate.setDisplayFormat(pubdate_format)
|
|
self.date.setMinimumDate(UNDEFINED_QDATE)
|
|
self.pubdate.setSpecialValueText(_('Undefined'))
|
|
self.date.setSpecialValueText(_('Undefined'))
|
|
self.clear_pubdate_button.clicked.connect(self.clear_pubdate)
|
|
|
|
|
|
self.connect(self.cover, SIGNAL('cover_changed(PyQt_PyObject)'), self.cover_dropped)
|
|
QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \
|
|
self.select_cover)
|
|
QObject.connect(self.add_format_button, SIGNAL("clicked(bool)"), \
|
|
self.add_format)
|
|
self.connect(self.formats,
|
|
SIGNAL('formats_dropped(PyQt_PyObject,PyQt_PyObject)'),
|
|
self.formats_dropped)
|
|
QObject.connect(self.remove_format_button, SIGNAL("clicked(bool)"), \
|
|
self.remove_format)
|
|
QObject.connect(self.fetch_metadata_button, SIGNAL('clicked()'),
|
|
self.fetch_metadata)
|
|
|
|
QObject.connect(self.fetch_cover_button, SIGNAL('clicked()'),
|
|
self.fetch_cover)
|
|
QObject.connect(self.tag_editor_button, SIGNAL('clicked()'),
|
|
self.edit_tags)
|
|
QObject.connect(self.remove_series_button, SIGNAL('clicked()'),
|
|
self.remove_unused_series)
|
|
QObject.connect(self.auto_author_sort, SIGNAL('clicked()'),
|
|
self.deduce_author_sort)
|
|
QObject.connect(self.auto_title_sort, SIGNAL('clicked()'),
|
|
self.deduce_title_sort)
|
|
self.trim_cover_button.clicked.connect(self.trim_cover)
|
|
self.connect(self.title_sort, SIGNAL('textChanged(const QString&)'),
|
|
self.title_sort_box_changed)
|
|
self.connect(self.title, SIGNAL('textChanged(const QString&)'),
|
|
self.title_box_changed)
|
|
self.connect(self.author_sort, SIGNAL('textChanged(const QString&)'),
|
|
self.author_sort_box_changed)
|
|
self.connect(self.authors, SIGNAL('editTextChanged(const QString&)'),
|
|
self.authors_box_changed)
|
|
self.connect(self.formats, SIGNAL('itemDoubleClicked(QListWidgetItem*)'),
|
|
self.show_format)
|
|
self.connect(self.formats, SIGNAL('delete_format()'), self.remove_format)
|
|
self.connect(self.button_set_cover, SIGNAL('clicked()'), self.set_cover)
|
|
self.connect(self.button_set_metadata, SIGNAL('clicked()'),
|
|
self.set_metadata_from_format)
|
|
self.connect(self.reset_cover, SIGNAL('clicked()'), self.do_reset_cover)
|
|
self.connect(self.swap_button, SIGNAL('clicked()'), self.swap_title_author)
|
|
self.timeout = float(prefs['network_timeout'])
|
|
|
|
|
|
self.title.setText(db.title(row))
|
|
self.title_sort.setText(db.title_sort(row))
|
|
isbn = db.isbn(self.id, index_is_id=True)
|
|
if not isbn:
|
|
isbn = ''
|
|
self.isbn.textChanged.connect(self.validate_isbn)
|
|
self.isbn.setText(isbn)
|
|
aus = self.db.author_sort(row)
|
|
self.author_sort.setText(aus if aus else '')
|
|
tags = self.db.tags(row)
|
|
self.original_tags = ', '.join(tags.split(',')) if tags else ''
|
|
self.tags.setText(self.original_tags)
|
|
self.tags.update_items_cache(self.db.all_tags())
|
|
rating = self.db.rating(row)
|
|
if rating > 0:
|
|
self.rating.setValue(int(rating/2.))
|
|
comments = self.db.comments(row)
|
|
if comments and comments.strip():
|
|
comments = comments_to_html(comments)
|
|
self.comments.html = comments
|
|
cover = self.db.cover(row)
|
|
pubdate = db.pubdate(self.id, index_is_id=True)
|
|
self.pubdate.setDate(QDate(pubdate.year, pubdate.month,
|
|
pubdate.day))
|
|
timestamp = db.timestamp(self.id, index_is_id=True)
|
|
self.date.setDate(QDate(timestamp.year, timestamp.month,
|
|
timestamp.day))
|
|
self.orig_date = qt_to_dt(self.date.date())
|
|
|
|
exts = self.db.formats(row)
|
|
self.original_formats = []
|
|
if exts:
|
|
exts = exts.split(',')
|
|
for ext in exts:
|
|
if not ext:
|
|
ext = ''
|
|
size = self.db.sizeof_format(row, ext)
|
|
timestamp = self.db.format_last_modified(self.id, ext)
|
|
if size is None:
|
|
continue
|
|
Format(self.formats, ext, size, timestamp=timestamp)
|
|
self.original_formats.append(ext.lower())
|
|
|
|
|
|
self.initialize_combos()
|
|
si = self.db.series_index(row)
|
|
if si is None:
|
|
si = 1.0
|
|
try:
|
|
self.series_index.setValue(float(si))
|
|
except:
|
|
self.series_index.setValue(1.0)
|
|
QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.enable_series_index)
|
|
QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.enable_series_index)
|
|
self.series.lineEdit().editingFinished.connect(self.increment_series_index)
|
|
|
|
pm = QPixmap()
|
|
if cover:
|
|
pm.loadFromData(cover)
|
|
if pm.isNull():
|
|
pm = QPixmap(I('default_cover.png'))
|
|
else:
|
|
self.cover_data = cover
|
|
self.cover.setPixmap(pm)
|
|
self.update_cover_tooltip()
|
|
self.original_series_name = unicode(self.series.text()).strip()
|
|
if len(db.custom_column_label_map) == 0:
|
|
self.central_widget.tabBar().setVisible(False)
|
|
self.central_widget.setTabEnabled(1, False)
|
|
else:
|
|
self.create_custom_column_editors()
|
|
self.generate_cover_button.clicked.connect(self.generate_cover)
|
|
|
|
self.original_author = unicode(self.authors.text()).strip()
|
|
self.original_title = unicode(self.title.text()).strip()
|
|
self.books_to_refresh = set()
|
|
|
|
self.show()
|
|
|
|
def clear_pubdate(self, *args):
|
|
self.pubdate.setDate(UNDEFINED_QDATE)
|
|
|
|
def create_custom_column_editors(self):
|
|
w = self.central_widget.widget(1)
|
|
layout = w.layout()
|
|
self.custom_column_widgets, self.__cc_spacers = \
|
|
populate_metadata_page(layout, self.db, self.id, parent=w, bulk=False,
|
|
two_column=tweaks['metadata_single_use_2_cols_for_custom_fields'])
|
|
self.__custom_col_layouts = [layout]
|
|
ans = self.custom_column_widgets
|
|
for i in range(len(ans)-1):
|
|
if len(ans[i+1].widgets) == 2:
|
|
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[1])
|
|
else:
|
|
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[0])
|
|
for c in range(2, len(ans[i].widgets), 2):
|
|
w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1])
|
|
|
|
def title_box_changed(self, txt):
|
|
ts = unicode(txt)
|
|
ts = title_sort(ts)
|
|
self.mark_box_as_ok(control = self.title_sort, tt=self.ts_tooltips,
|
|
normal=(unicode(self.title_sort.text()) == ts))
|
|
|
|
def title_sort_box_changed(self, txt):
|
|
ts = unicode(txt)
|
|
self.mark_box_as_ok(control = self.title_sort, tt=self.ts_tooltips,
|
|
normal=(title_sort(unicode(self.title.text())) == ts))
|
|
|
|
def authors_box_changed(self, txt):
|
|
aus = unicode(txt)
|
|
aus = re.sub(r'\s+et al\.$', '', aus)
|
|
aus = self.db.author_sort_from_authors(string_to_authors(aus))
|
|
self.mark_box_as_ok(control = self.author_sort, tt=self.aus_tooltips,
|
|
normal=(unicode(self.author_sort.text()) == aus))
|
|
|
|
def author_sort_box_changed(self, txt):
|
|
au = unicode(self.authors.text())
|
|
au = re.sub(r'\s+et al\.$', '', au)
|
|
au = self.db.author_sort_from_authors(string_to_authors(au))
|
|
self.mark_box_as_ok(control = self.author_sort, tt=self.aus_tooltips,
|
|
normal=(au == txt))
|
|
|
|
def mark_box_as_ok(self, control, tt, normal=True):
|
|
if normal:
|
|
col = 'rgb(0, 255, 0, 20%)'
|
|
else:
|
|
col = 'rgb(255, 0, 0, 20%)'
|
|
control.setStyleSheet('QLineEdit { color: black; '
|
|
'background-color: %s; }'%col)
|
|
tt = tt[0] if normal else tt[1]
|
|
control.setToolTip(tt)
|
|
|
|
def validate_isbn(self, isbn):
|
|
isbn = unicode(isbn).strip()
|
|
if not isbn:
|
|
self.isbn.setStyleSheet('QLineEdit { background-color: rgba(0,255,0,0%) }')
|
|
self.isbn.setToolTip(_('This ISBN number is valid'))
|
|
return
|
|
|
|
if check_isbn(isbn):
|
|
self.isbn.setStyleSheet('QLineEdit { background-color: rgba(0,255,0,20%) }')
|
|
self.isbn.setToolTip(_('This ISBN number is valid'))
|
|
else:
|
|
self.isbn.setStyleSheet('QLineEdit { background-color: rgba(255,0,0,20%) }')
|
|
self.isbn.setToolTip(_('This ISBN number is invalid'))
|
|
|
|
def deduce_author_sort(self):
|
|
au = unicode(self.authors.text())
|
|
au = re.sub(r'\s+et al\.$', '', au)
|
|
authors = string_to_authors(au)
|
|
self.author_sort.setText(self.db.author_sort_from_authors(authors))
|
|
|
|
def deduce_title_sort(self):
|
|
ts = unicode(self.title.text())
|
|
self.title_sort.setText(title_sort(ts))
|
|
|
|
def swap_title_author(self):
|
|
title = self.title.text()
|
|
self.title.setText(self.authors.text())
|
|
self.authors.setText(title)
|
|
self.deduce_author_sort()
|
|
self.deduce_title_sort()
|
|
|
|
def initialize_combos(self):
|
|
self.initalize_authors()
|
|
self.initialize_series()
|
|
self.initialize_publisher()
|
|
|
|
self.layout().activate()
|
|
|
|
def initalize_authors(self):
|
|
all_authors = self.db.all_authors()
|
|
all_authors.sort(key=lambda x : sort_key(x[1]))
|
|
for i in all_authors:
|
|
id, name = i
|
|
name = [name.strip().replace('|', ',') for n in name.split(',')]
|
|
self.authors.addItem(authors_to_string(name))
|
|
|
|
au = self.db.authors(self.row)
|
|
if not au:
|
|
au = _('Unknown')
|
|
au = ' & '.join([a.strip().replace('|', ',') for a in au.split(',')])
|
|
self.authors.setEditText(au)
|
|
|
|
self.authors.set_separator('&')
|
|
self.authors.set_space_before_sep(True)
|
|
self.authors.set_add_separator(tweaks['authors_completer_append_separator'])
|
|
self.authors.update_items_cache(self.db.all_author_names())
|
|
|
|
def initialize_series(self):
|
|
self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow)
|
|
all_series = self.db.all_series()
|
|
all_series.sort(key=lambda x : sort_key(x[1]))
|
|
self.series.set_separator(None)
|
|
self.series.update_items_cache([x[1] for x in all_series])
|
|
series_id = self.db.series_id(self.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
|
|
|
|
self.series.lineEdit().setText('')
|
|
if idx is not None:
|
|
self.series.setCurrentIndex(idx)
|
|
self.enable_series_index()
|
|
|
|
def initialize_publisher(self):
|
|
all_publishers = self.db.all_publishers()
|
|
all_publishers.sort(key=lambda x : sort_key(x[1]))
|
|
self.publisher.set_separator(None)
|
|
self.publisher.update_items_cache([x[1] for x in all_publishers])
|
|
publisher_id = self.db.publisher_id(self.row)
|
|
idx, c = None, 0
|
|
for i in all_publishers:
|
|
id, name = i
|
|
if id == publisher_id:
|
|
idx = c
|
|
self.publisher.addItem(name)
|
|
c += 1
|
|
|
|
self.publisher.setEditText('')
|
|
if idx is not None:
|
|
self.publisher.setCurrentIndex(idx)
|
|
|
|
def edit_tags(self):
|
|
if self.tags.text() != self.original_tags:
|
|
if question_dialog(self, _('Tags changed'),
|
|
_('You have changed the tags. In order to use the tags'
|
|
' editor, you must either discard or apply these '
|
|
'changes. Apply changes?'), show_copy_button=False):
|
|
self.books_to_refresh |= self.apply_tags(commit=True,
|
|
notify=True)
|
|
self.original_tags = unicode(self.tags.text())
|
|
else:
|
|
self.tags.setText(self.original_tags)
|
|
d = TagEditor(self, self.db, self.id)
|
|
d.exec_()
|
|
if d.result() == QDialog.Accepted:
|
|
tag_string = ', '.join(d.tags)
|
|
self.tags.setText(tag_string)
|
|
self.tags.update_items_cache(self.db.all_tags())
|
|
|
|
|
|
def fetch_metadata(self):
|
|
isbn = re.sub(r'[^0-9a-zA-Z]', '', unicode(self.isbn.text()))
|
|
title = unicode(self.title.text())
|
|
try:
|
|
author = string_to_authors(unicode(self.authors.text()))[0]
|
|
except:
|
|
author = ''
|
|
publisher = unicode(self.publisher.currentText())
|
|
if isbn or title or author or publisher:
|
|
d = FetchMetadata(self, isbn, title, author, publisher, self.timeout)
|
|
self._fetch_metadata_scope = d
|
|
with d:
|
|
if d.exec_() == QDialog.Accepted:
|
|
book = d.selected_book()
|
|
if book:
|
|
if d.opt_get_social_metadata.isChecked():
|
|
d2 = SocialMetadata(book, self)
|
|
d2.exec_()
|
|
if d2.timed_out:
|
|
warning_dialog(self, _('Timed out'),
|
|
_('The download of social'
|
|
' metadata timed out, the servers are'
|
|
' probably busy. Try again later.'),
|
|
show=True)
|
|
elif d2.exceptions:
|
|
det = '\n'.join([x[0]+'\n\n'+x[-1]+'\n\n\n' for
|
|
x in d2.exceptions])
|
|
warning_dialog(self, _('There were errors'),
|
|
_('There were errors downloading social metadata'),
|
|
det_msg=det, show=True)
|
|
else:
|
|
book.tags = []
|
|
if d.opt_overwrite_author_title_metadata.isChecked():
|
|
self.title.setText(book.title)
|
|
self.authors.setText(authors_to_string(book.authors))
|
|
if book.author_sort: self.author_sort.setText(book.author_sort)
|
|
if book.publisher: self.publisher.setEditText(book.publisher)
|
|
if book.isbn: self.isbn.setText(book.isbn)
|
|
if book.pubdate:
|
|
dt = book.pubdate
|
|
self.pubdate.setDate(QDate(dt.year, dt.month, dt.day))
|
|
summ = book.comments
|
|
if summ:
|
|
prefix = self.comments.html
|
|
if prefix:
|
|
prefix += '\n'
|
|
self.comments.html = prefix + comments_to_html(summ)
|
|
if book.rating is not None:
|
|
self.rating.setValue(int(book.rating))
|
|
if book.tags:
|
|
self.tags.setText(', '.join(book.tags))
|
|
if book.series is not None:
|
|
if self.series.text() is None or self.series.text() == '':
|
|
self.series.setText(book.series)
|
|
if book.series_index is not None:
|
|
self.series_index.setValue(book.series_index)
|
|
if book.has_cover:
|
|
if d.opt_auto_download_cover.isChecked():
|
|
self.fetch_cover()
|
|
else:
|
|
self.fetch_cover_button.setFocus(Qt.OtherFocusReason)
|
|
else:
|
|
error_dialog(self, _('Cannot fetch metadata'),
|
|
_('You must specify at least one of ISBN, Title, '
|
|
'Authors or Publisher'), show=True)
|
|
self.title.setFocus(Qt.OtherFocusReason)
|
|
|
|
def enable_series_index(self, *args):
|
|
self.series_index.setEnabled(True)
|
|
|
|
def increment_series_index(self):
|
|
if self.db is not None:
|
|
try:
|
|
series = unicode(self.series.text()).strip()
|
|
if series and series != self.original_series_name:
|
|
ns = 1
|
|
if tweaks['series_index_auto_increment'] != 'const':
|
|
ns = self.db.get_next_series_num_for(series)
|
|
self.series_index.setValue(ns)
|
|
self.original_series_name = series
|
|
except:
|
|
traceback.print_exc()
|
|
|
|
def remove_unused_series(self):
|
|
self.db.remove_unused_series()
|
|
idx = unicode(self.series.currentText())
|
|
self.series.clear()
|
|
self.initialize_series()
|
|
if idx:
|
|
for i in range(self.series.count()):
|
|
if unicode(self.series.itemText(i)) == idx:
|
|
self.series.setCurrentIndex(i)
|
|
break
|
|
|
|
def apply_tags(self, commit=False, notify=False):
|
|
return self.db.set_tags(self.id, [x.strip() for x in
|
|
unicode(self.tags.text()).split(',')],
|
|
notify=notify, commit=commit, allow_case_change=True)
|
|
|
|
def next_triggered(self, row_delta, *args):
|
|
self.row_delta = row_delta
|
|
self.accept()
|
|
|
|
def accept(self):
|
|
try:
|
|
if self.formats_changed:
|
|
self.sync_formats()
|
|
title = unicode(self.title.text()).strip()
|
|
if title != self.original_title:
|
|
self.db.set_title(self.id, title, notify=False)
|
|
# This must be after setting the title because of the DB update trigger
|
|
ts = unicode(self.title_sort.text()).strip()
|
|
if ts:
|
|
self.db.set_title_sort(self.id, ts, notify=False, commit=False)
|
|
au = unicode(self.authors.text()).strip()
|
|
if au and au != self.original_author:
|
|
self.books_to_refresh |= self.db.set_authors(self.id,
|
|
string_to_authors(au),
|
|
notify=False,
|
|
allow_case_change=True)
|
|
aus = unicode(self.author_sort.text()).strip()
|
|
if aus:
|
|
self.db.set_author_sort(self.id, aus, notify=False, commit=False)
|
|
self.db.set_isbn(self.id,
|
|
re.sub(r'[^0-9a-zA-Z]', '',
|
|
unicode(self.isbn.text()).strip()),
|
|
notify=False, commit=False)
|
|
self.db.set_rating(self.id, 2*self.rating.value(), notify=False,
|
|
commit=False)
|
|
self.books_to_refresh |= self.apply_tags()
|
|
self.books_to_refresh |= self.db.set_publisher(self.id,
|
|
unicode(self.publisher.currentText()).strip(),
|
|
notify=False, commit=False, allow_case_change=True)
|
|
self.books_to_refresh |= self.db.set_series(self.id,
|
|
unicode(self.series.currentText()).strip(), notify=False,
|
|
commit=False, allow_case_change=True)
|
|
self.db.set_series_index(self.id, self.series_index.value(),
|
|
notify=False, commit=False)
|
|
self.db.set_comment(self.id,
|
|
self.comments.html,
|
|
notify=False, commit=False)
|
|
d = self.pubdate.date()
|
|
d = qt_to_dt(d)
|
|
self.db.set_pubdate(self.id, d, notify=False, commit=False)
|
|
d = self.date.date()
|
|
d = qt_to_dt(d)
|
|
if d != self.orig_date:
|
|
self.db.set_timestamp(self.id, d, notify=False, commit=False)
|
|
self.db.commit()
|
|
|
|
if self.cover_changed:
|
|
if self.cover_data is not None:
|
|
self.db.set_cover(self.id, self.cover_data)
|
|
else:
|
|
self.db.remove_cover(self.id)
|
|
for w in getattr(self, 'custom_column_widgets', []):
|
|
self.books_to_refresh |= w.commit(self.id)
|
|
self.db.commit()
|
|
except (IOError, OSError) as err:
|
|
if getattr(err, 'errno', -1) == 13: # Permission denied
|
|
fname = err.filename if err.filename else 'file'
|
|
return error_dialog(self, _('Permission denied'),
|
|
_('Could not open %s. Is it being used by another'
|
|
' program?')%fname, det_msg=traceback.format_exc(),
|
|
show=True)
|
|
raise
|
|
self.save_state()
|
|
self.cover_fetcher = None
|
|
QDialog.accept(self)
|
|
|
|
def reject(self, *args):
|
|
self.save_state()
|
|
self.cover_fetcher = None
|
|
QDialog.reject(self, *args)
|
|
|
|
def read_state(self):
|
|
wg = dynamic.get('metasingle_window_geometry2', None)
|
|
ss = dynamic.get('metasingle_splitter_state2', None)
|
|
if wg is not None:
|
|
self.restoreGeometry(wg)
|
|
if ss is not None:
|
|
self.splitter.restoreState(ss)
|
|
|
|
def save_state(self):
|
|
dynamic.set('metasingle_window_geometry2', bytes(self.saveGeometry()))
|
|
dynamic.set('metasingle_splitter_state2',
|
|
bytes(self.splitter.saveState()))
|
|
|
|
def break_cycles(self):
|
|
# Break any reference cycles that could prevent python
|
|
# from garbage collecting this dialog
|
|
def disconnect(signal):
|
|
try:
|
|
signal.disconnect()
|
|
except:
|
|
pass # Fails if view format was never connected
|
|
disconnect(self.view_format)
|
|
for b in ('next_button', 'prev_button'):
|
|
x = getattr(self, b, None)
|
|
if x is not None:
|
|
disconnect(x.clicked)
|
|
|
|
if __name__ == '__main__':
|
|
from calibre.library import db
|
|
from PyQt4.Qt import QApplication
|
|
from calibre.utils.mem import memory
|
|
import gc
|
|
|
|
|
|
app = QApplication([])
|
|
db = db()
|
|
|
|
# Initialize all Qt Objects once
|
|
d = MetadataSingleDialog(None, 4, db)
|
|
d.break_cycles()
|
|
d.reject()
|
|
del d
|
|
|
|
for i in range(5):
|
|
gc.collect()
|
|
before = memory()
|
|
|
|
d = MetadataSingleDialog(None, 4, db)
|
|
d.reject()
|
|
d.break_cycles()
|
|
del d
|
|
|
|
for i in range(5):
|
|
gc.collect()
|
|
print 'Used memory:', memory(before)/1024.**2, 'MB'
|
|
|
|
|