diff --git a/resources/recipes/wash_post.recipe b/resources/recipes/wash_post.recipe index a917371cec..ae56172674 100644 --- a/resources/recipes/wash_post.recipe +++ b/resources/recipes/wash_post.recipe @@ -21,16 +21,20 @@ class WashingtonPost(BasicNewsRecipe): body{font-family:arial,helvetica,sans-serif} ''' - feeds = [ ('Today\'s Highlights', 'http://www.washingtonpost.com/wp-dyn/rss/linkset/2005/03/24/LI2005032400102.xml'), - ('Politics', 'http://www.washingtonpost.com/wp-dyn/rss/politics/index.xml'), - ('Nation', 'http://www.washingtonpost.com/wp-dyn/rss/nation/index.xml'), - ('World', 'http://www.washingtonpost.com/wp-dyn/rss/world/index.xml'), - ('Business', 'http://www.washingtonpost.com/wp-dyn/rss/business/index.xml'), - ('Technology', 'http://www.washingtonpost.com/wp-dyn/rss/technology/index.xml'), - ('Health', 'http://www.washingtonpost.com/wp-dyn/rss/health/index.xml'), - ('Education', 'http://www.washingtonpost.com/wp-dyn/rss/education/index.xml'), - ('Editorials', 'http://www.washingtonpost.com/wp-dyn/rss/linkset/2005/05/30/LI2005053000331.xml'), - ] + feeds = [ ('Today\'s Highlights', 'http://www.washingtonpost.com/wp-dyn/rss/linkset/2005/03/24/LI2005032400102.xml'), + ('Politics', 'http://www.washingtonpost.com/wp-dyn/rss/politics/index.xml'), + ('Nation', 'http://www.washingtonpost.com/wp-dyn/rss/nation/index.xml'), + ('World', 'http://www.washingtonpost.com/wp-dyn/rss/world/index.xml'), + ('Business', 'http://www.washingtonpost.com/wp-dyn/rss/business/index.xml'), + ('Technology', 'http://www.washingtonpost.com/wp-dyn/rss/technology/index.xml'), + ('Health', 'http://www.washingtonpost.com/wp-dyn/rss/health/index.xml'), + ('Education', 'http://www.washingtonpost.com/wp-dyn/rss/education/index.xml'), + ('Style', + 'http://www.washingtonpost.com/wp-dyn/rss/print/style/index.xml'), + ('Sports', + 'http://feeds.washingtonpost.com/wp-dyn/rss/linkset/2010/08/19/LI2010081904067_xml'), + ('Editorials', 'http://www.washingtonpost.com/wp-dyn/rss/linkset/2005/05/30/LI2005053000331.xml'), + ] remove_tags = [{'id':['pfmnav', 'ArticleCommentsWrapper']}] diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 16aaab73dd..0579c75eea 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -455,6 +455,24 @@ def prepare_string_for_xml(raw, attribute=False): def isbytestring(obj): return isinstance(obj, (str, bytes)) +def force_unicode(obj, enc=preferred_encoding): + if isbytestring(obj): + try: + obj = obj.decode(enc) + except: + try: + obj = obj.decode(filesystem_encoding if enc == + preferred_encoding else preferred_encoding) + except: + try: + obj = obj.decode('utf-8') + except: + obj = repr(obj) + if isbytestring(obj): + obj = obj.decode('utf-8') + return obj + + def human_readable(size): """ Convert a size in bytes into a human readable form """ divisor, suffix = 1, "B" diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 25b6d1aaae..9bab688ba5 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -707,7 +707,7 @@ OptionRecommendation(name='timestamp', if mi.cover.startswith('http:') or mi.cover.startswith('https:'): mi.cover = self.download_cover(mi.cover) ext = mi.cover.rpartition('.')[-1].lower().strip() - if ext not in ('png', 'jpg', 'jpeg'): + if ext not in ('png', 'jpg', 'jpeg', 'gif'): ext = 'jpg' mi.cover_data = (ext, open(mi.cover, 'rb').read()) mi.cover = None diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index b5f61db3ac..e85098e293 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -654,8 +654,6 @@ class Metadata(object): if predicate(x): l.remove(x) - - def __getitem__(self, key): return self.items[key] diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index 559421326c..0f61969373 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -132,17 +132,23 @@ class OEBReader(object): if not mi.language: mi.language = get_lang().replace('_', '-') self.oeb.metadata.add('language', mi.language) - if not mi.title: - mi.title = self.oeb.translate(__('Unknown')) - if not mi.authors: - mi.authors = [self.oeb.translate(__('Unknown'))] if not mi.book_producer: - mi.book_producer = '%(a)s (%(v)s) [http://%(a)s.kovidgoyal.net]'%\ + mi.book_producer = '%(a)s (%(v)s) [http://%(a)s-ebook.com]'%\ dict(a=__appname__, v=__version__) meta_info_to_oeb_metadata(mi, self.oeb.metadata, self.logger) - self.oeb.metadata.add('identifier', str(uuid.uuid4()), id='uuid_id', - scheme='uuid') + m = self.oeb.metadata + m.add('identifier', str(uuid.uuid4()), id='uuid_id', scheme='uuid') self.oeb.uid = self.oeb.metadata.identifier[-1] + if not m.title: + m.add('title', self.oeb.translate(__('Unknown'))) + has_aut = False + for x in m.creator: + if getattr(x, 'role', '').lower() in ('', 'aut'): + has_aut = True + break + if not has_aut: + m.add('creator', self.oeb.translate(__('Unknown')), role='aut') + def _manifest_prune_invalid(self): ''' diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 1b0027dcc2..47bb61a7dc 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -3,6 +3,7 @@ __copyright__ = '2008, Kovid Goyal ' """ The GUI """ import os, sys, Queue, threading from threading import RLock +from urllib import unquote from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \ QByteArray, QTranslator, QCoreApplication, QThread, \ @@ -505,6 +506,11 @@ class FileDialog(QObject): fs = QFileDialog.getOpenFileNames(parent, title, initial_dir, ftext, "") for f in fs: f = unicode(f) + if not f: continue + if not os.path.exists(f): + # QFileDialog for some reason quotes spaces + # on linux if there is more than one space in a row + f = unquote(f) if f and os.path.exists(f): self.selected_files.append(f) else: diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index e0a7b5647e..be1f8f4eaf 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -234,13 +234,14 @@ class AddAction(InterfaceAction): self.gui.set_books_in_library(booklists=[model.db], reset=True) self.gui.refresh_ondevice() - def add_books_from_device(self, view): - rows = view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - d = error_dialog(self.gui, _('Add to library'), _('No book selected')) - d.exec_() - return - paths = [p for p in view._model.paths(rows) if p is not None] + def add_books_from_device(self, view, paths=None): + if paths is None: + rows = view.selectionModel().selectedRows() + if not rows or len(rows) == 0: + d = error_dialog(self.gui, _('Add to library'), _('No book selected')) + d.exec_() + return + paths = [p for p in view.model().paths(rows) if p is not None] ve = self.gui.device_manager.device.VIRTUAL_BOOK_EXTENSIONS def ext(x): ans = os.path.splitext(x)[1] @@ -261,7 +262,7 @@ class AddAction(InterfaceAction): return from calibre.gui2.add import Adder self.__adder_func = partial(self._add_from_device_adder, on_card=None, - model=view._model) + model=view.model()) self._adder = Adder(self.gui, self.gui.library_view.model().db, self.Dispatcher(self.__adder_func), spare_server=self.gui.spare_server) self._adder.add(paths) diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index ea62b61f6a..b206bf68a6 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -21,7 +21,10 @@ from calibre.gui2.convert import Widget def create_opf_file(db, book_id): mi = db.get_metadata(book_id, index_is_id=True) mi.application_id = uuid.uuid4() + old_cover = mi.cover + mi.cover = None raw = metadata_to_opf(mi) + mi.cover = old_cover opf_file = PersistentTemporaryFile('.opf') opf_file.write(raw) opf_file.close() diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index d81fad3da9..c8a2325a0a 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -23,7 +23,7 @@ from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \ warning_dialog, \ question_dialog, info_dialog, choose_dir from calibre.ebooks.metadata import authors_to_string -from calibre import preferred_encoding, prints +from calibre import preferred_encoding, prints, force_unicode from calibre.utils.filenames import ascii_filename from calibre.devices.errors import FreeSpaceError from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \ @@ -964,12 +964,12 @@ class DeviceMixin(object): # {{{ for jobname, exception, tb in results: title = jobname.partition(':')[-1] if exception is not None: - errors.append([title, exception, tb]) + errors.append(list(map(force_unicode, [title, exception, tb]))) else: good.append(title) if errors: - errors = '\n'.join([ - '%s\n\n%s\n%s\n' % + errors = u'\n'.join([ + u'%s\n\n%s\n%s\n' % (title, e, tb) for \ title, e, tb in errors ]) diff --git a/src/calibre/gui2/dialogs/user_profiles.ui b/src/calibre/gui2/dialogs/user_profiles.ui index 4eca4045b1..4ca47539d1 100644 --- a/src/calibre/gui2/dialogs/user_profiles.ui +++ b/src/calibre/gui2/dialogs/user_profiles.ui @@ -375,7 +375,7 @@ p, li { white-space: pre-wrap; } - For help with writing advanced news recipes, please visit <a href="http://__appname__.kovidgoyal.net/user_manual/news.html">User Recipes</a> + For help with writing advanced news recipes, please visit <a href="http://__appname__-ebook.com/user_manual/news.html">User Recipes</a> true diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index 0d2f9bfd92..6c237bd67b 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -56,6 +56,7 @@ class LocationManager(QObject): # {{{ self._mem.append(a) else: ac.setToolTip(tooltip) + ac.calibre_name = name return ac @@ -112,7 +113,6 @@ class LocationManager(QObject): # {{{ ac.setWhatsThis(t) ac.setStatusTip(t) - @property def has_device(self): return max(self.free) > -1 @@ -228,6 +228,7 @@ class ToolBar(QToolBar): # {{{ self.added_actions = [] self.build_bar() self.preferred_width = self.sizeHint().width() + self.setAcceptDrops(True) def apply_settings(self): sz = gprefs['toolbar_icon_size'] @@ -317,6 +318,59 @@ class ToolBar(QToolBar): # {{{ def database_changed(self, db): pass + #support drag&drop from/to library from/to reader/card + def dragEnterEvent(self, event): + md = event.mimeData() + if md.hasFormat("application/calibre+from_library") or \ + md.hasFormat("application/calibre+from_device"): + event.setDropAction(Qt.CopyAction) + event.accept() + else: + event.ignore() + + def dragMoveEvent(self, event): + allowed = False + md = event.mimeData() + #Drop is only allowed in the location manager widget's different from the selected one + for ac in self.location_manager.available_actions: + w = self.widgetForAction(ac) + if w is not None: + if ( md.hasFormat("application/calibre+from_library") or \ + md.hasFormat("application/calibre+from_device") ) and \ + w.geometry().contains(event.pos()) and \ + isinstance(w, QToolButton) and not w.isChecked(): + allowed = True + break + if allowed: + event.acceptProposedAction() + else: + event.ignore() + + def dropEvent(self, event): + data = event.mimeData() + + mime = 'application/calibre+from_library' + if data.hasFormat(mime): + ids = list(map(int, str(data.data(mime)).split())) + tgt = None + for ac in self.location_manager.available_actions: + w = self.widgetForAction(ac) + if w is not None and w.geometry().contains(event.pos()): + tgt = ac.calibre_name + if tgt is not None: + if tgt == 'main': + tgt = None + self.gui.sync_to_device(tgt, False, send_ids=ids) + event.accept() + + mime = 'application/calibre+from_device' + if data.hasFormat(mime): + paths = [unicode(u.toLocalFile()) for u in data.urls()] + if paths: + self.gui.iactions['Add Books'].add_books_from_device( + self.gui.current_view(), paths=paths) + event.accept() + # }}} class MainWindowMixin(object): # {{{ diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index a808fd9c43..19bd38e08f 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -1081,12 +1081,11 @@ class DeviceBooksModel(BooksModel): # {{{ self.db = db self.map = list(range(0, len(db))) - def current_changed(self, current, previous): - data = {} - item = self.db[self.map[current.row()]] + def cover(self, row): + item = self.db[self.map[row]] cdata = item.thumbnail + img = QImage() if cdata is not None: - img = QImage() if hasattr(cdata, 'image_path'): img.load(cdata.image_path) elif cdata: @@ -1094,9 +1093,16 @@ class DeviceBooksModel(BooksModel): # {{{ img.loadFromData(cdata[-1]) else: img.loadFromData(cdata) - if img.isNull(): - img = self.default_image - data['cover'] = img + if img.isNull(): + img = self.default_image + return img + + def current_changed(self, current, previous): + data = {} + item = self.db[self.map[current.row()]] + cover = self.cover(current.row()) + if cover is not self.default_image: + data['cover'] = cover type = _('Unknown') ext = os.path.splitext(item.path)[1] if ext: diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 4b6bda1d2a..4411e1fd9f 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -9,7 +9,8 @@ import os from functools import partial from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \ - QModelIndex, QIcon, QItemSelection + QModelIndex, QIcon, QItemSelection, QMimeData, QDrag, QApplication, \ + QPoint, QPixmap, QUrl, QImage, QPainter, QColor, QRect from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \ TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \ @@ -18,7 +19,8 @@ from calibre.gui2.library.models import BooksModel, DeviceBooksModel from calibre.utils.config import tweaks, prefs from calibre.gui2 import error_dialog, gprefs from calibre.gui2.library import DEFAULT_SORT - +from calibre.constants import filesystem_encoding +from calibre import force_unicode class BooksView(QTableView): # {{{ @@ -31,6 +33,7 @@ class BooksView(QTableView): # {{{ self.setDragEnabled(True) self.setDragDropOverwriteMode(False) self.setDragDropMode(self.DragDrop) + self.drag_start_pos = None self.setAlternatingRowColors(True) self.setSelectionBehavior(self.SelectRows) self.setShowGrid(False) @@ -426,6 +429,69 @@ class BooksView(QTableView): # {{{ urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()] return [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)] + def drag_icon(self, cover, multiple): + cover = cover.scaledToHeight(120, Qt.SmoothTransformation) + if multiple: + base_width = cover.width() + base_height = cover.height() + base = QImage(base_width+21, base_height+21, + QImage.Format_ARGB32_Premultiplied) + base.fill(QColor(255, 255, 255, 0).rgba()) + p = QPainter(base) + rect = QRect(20, 0, base_width, base_height) + p.fillRect(rect, QColor('white')) + p.drawRect(rect) + rect.moveLeft(10) + rect.moveTop(10) + p.fillRect(rect, QColor('white')) + p.drawRect(rect) + rect.moveLeft(0) + rect.moveTop(20) + p.fillRect(rect, QColor('white')) + p.save() + p.setCompositionMode(p.CompositionMode_SourceAtop) + p.drawImage(rect.topLeft(), cover) + p.restore() + p.drawRect(rect) + p.end() + cover = base + return QPixmap.fromImage(cover) + + def drag_data(self): + m = self.model() + db = m.db + rows = self.selectionModel().selectedRows() + selected = map(m.id, rows) + ids = ' '.join(map(str, selected)) + md = QMimeData() + md.setData('application/calibre+from_library', ids) + md.setUrls([QUrl.fromLocalFile(db.abspath(i, index_is_id=True)) + for i in selected]) + drag = QDrag(self) + drag.setMimeData(md) + cover = self.drag_icon(m.cover(self.currentIndex().row()), + len(selected) > 1) + drag.setHotSpot(QPoint(cover.width()//3, cover.height()//3)) + drag.setPixmap(cover) + return drag + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + self.drag_start_pos = event.pos() + return QTableView.mousePressEvent(self, event) + + def mouseMoveEvent(self, event): + if not (event.buttons() & Qt.LeftButton) or self.drag_start_pos is None: + return + if (event.pos() - self.drag_start_pos).manhattanLength() \ + < QApplication.startDragDistance(): + return + index = self.indexAt(event.pos()) + if not index.isValid(): + return + drag = self.drag_data() + drag.exec_(Qt.CopyAction) + def dragEnterEvent(self, event): if int(event.possibleActions() & Qt.CopyAction) + \ int(event.possibleActions() & Qt.MoveAction) == 0: @@ -547,6 +613,21 @@ class DeviceBooksView(BooksView): # {{{ self.setDragDropMode(self.NoDragDrop) self.setAcceptDrops(False) + def drag_data(self): + m = self.model() + rows = self.selectionModel().selectedRows() + paths = [force_unicode(p, enc=filesystem_encoding) for p in m.paths(rows) if p] + md = QMimeData() + md.setData('application/calibre+from_device', 'dummy') + md.setUrls([QUrl.fromLocalFile(p) for p in paths]) + drag = QDrag(self) + drag.setMimeData(md) + cover = self.drag_icon(m.cover(self.currentIndex().row()), len(paths) > + 1) + drag.setHotSpot(QPoint(cover.width()//3, cover.height()//3)) + drag.setPixmap(cover) + return drag + def contextMenuEvent(self, event): edit_collections = callable(getattr(self._model.db, 'supports_collections', None)) and \ self._model.db.supports_collections() and \ diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 46f40f9c31..e3e52b516d 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -302,7 +302,7 @@ def do_add_empty(db, title, authors, isbn): if isbn: mi.isbn = isbn db.import_book(mi, []) - write_dirtied() + write_dirtied(db) send_message() def command_add(args, dbpath): @@ -456,7 +456,7 @@ def do_set_metadata(db, id, stream): db.set_metadata(id, mi) db.clean() do_show_metadata(db, id, False) - write_dirtied() + write_dirtied(db) send_message() def set_metadata_option_parser(): diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index f3c384cb15..639cbb32a6 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -19,7 +19,7 @@ from calibre.ebooks.metadata.meta import set_metadata from calibre.constants import preferred_encoding, filesystem_encoding from calibre.ebooks.metadata import fmt_sidx from calibre.ebooks.metadata import title_sort -from calibre import strftime +from calibre import strftime, prints plugboard_any_device_value = 'any device' plugboard_any_format_value = 'any format' diff --git a/src/calibre/manual/metadata.rst b/src/calibre/manual/metadata.rst index d87becaea2..e6ae581514 100644 --- a/src/calibre/manual/metadata.rst +++ b/src/calibre/manual/metadata.rst @@ -18,7 +18,7 @@ Editing the metadata of one book at a time Click the book you want to edit and then click the :guilabel:`Edit metadata` button or press the ``E`` key. A dialog opens that allows you to edit all aspects of the metadata. It has various features to make editing faster and more efficient. A list of the commonly used tips: - * You can click the button in between title and authors to swap them automatically. Or + * You can click the button in between title and authors to swap them automatically. * You can click the button next to author sort to automatically to have |app| automatically fill it from the author name. * You can click the button next to tags to use the Tag Editor to manage the tags associated with the book. * The ISBN box will have a red background if you enter an invalid ISBN. It will be green for valid ISBNs diff --git a/src/calibre/startup.py b/src/calibre/startup.py index 9d5c4b51fc..1046cd93b3 100644 --- a/src/calibre/startup.py +++ b/src/calibre/startup.py @@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en' Perform various initialization tasks. ''' -import locale, sys, os +import locale, sys, os, re # Default translation is NOOP import __builtin__ @@ -114,6 +114,34 @@ if not _run_once: r, w, a, rb, wb, ab, r+, w+, a+, r+b, w+b, a+b ''' if iswindows: + class fwrapper(object): + def __init__(self, name, fobject): + object.__setattr__(self, 'fobject', fobject) + object.__setattr__(self, 'name', name) + + def __getattribute__(self, attr): + if attr == 'name': + return object.__getattribute__(self, attr) + fobject = object.__getattribute__(self, 'fobject') + return getattr(fobject, attr) + + def __setattr__(self, attr, val): + fobject = object.__getattribute__(self, 'fobject') + return setattr(fobject, attr, val) + + def __repr__(self): + fobject = object.__getattribute__(self, 'fobject') + name = object.__getattribute__(self, 'name') + return re.sub(r'''['"]['"]''', repr(name), + repr(fobject)) + + def __str__(self): + return repr(self) + + def __unicode__(self): + return repr(self).decode('utf-8') + + m = mode[0] random = len(mode) > 1 and mode[1] == '+' binary = mode[-1] == 'b' @@ -139,6 +167,7 @@ if not _run_once: flags |= os.O_NOINHERIT fd = os.open(name, flags) ans = os.fdopen(fd, mode, bufsize) + ans = fwrapper(name, ans) else: import fcntl try: diff --git a/src/calibre/translations/calibre.pot b/src/calibre/translations/calibre.pot index e12e9a84b7..f619b452ca 100644 --- a/src/calibre/translations/calibre.pot +++ b/src/calibre/translations/calibre.pot @@ -5,8 +5,8 @@ msgid "" msgstr "" "Project-Id-Version: calibre 0.7.21\n" -"POT-Creation-Date: 2010-10-01 14:42+MDT\n" -"PO-Revision-Date: 2010-10-01 14:42+MDT\n" +"POT-Creation-Date: 2010-10-02 11:26+MDT\n" +"PO-Revision-Date: 2010-10-02 11:26+MDT\n" "Last-Translator: Automatically generated\n" "Language-Team: LANGUAGE\n" "MIME-Version: 1.0\n" @@ -10057,7 +10057,7 @@ msgid "" " files in each directory of the calibre library. This is\n" " useful if your metadata.db file has been corrupted.\n" "\n" -" WARNING: This completely regenrates your datbase. You will\n" +" WARNING: This completely regenerates your datbase. You will\n" " lose stored per-book conversion settings and custom recipes.\n" " " msgstr ""