calibre/src/calibre/gui2/dialogs/metadata_single.py
2011-04-19 12:43:09 -06:00

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'