From b3cbbd3ea8e05e1cd75b12e70d28ca1decc505d4 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Thu, 26 Aug 2010 14:32:57 +0100
Subject: [PATCH 001/289] Initial attempt, including full cached metadata, json
serialization, and custom fields in save paths. Custom fields in collections
probably work, but they haven't been tested.
---
src/calibre/devices/apple/driver.py | 7 +-
src/calibre/devices/interface.py | 4 +-
src/calibre/devices/kobo/books.py | 21 +-
src/calibre/devices/usbms/books.py | 43 +--
src/calibre/devices/usbms/driver.py | 21 +-
src/calibre/ebooks/metadata/__init__.py | 216 +------------
src/calibre/ebooks/metadata/book/__init__.py | 41 ++-
src/calibre/ebooks/metadata/book/base.py | 296 +++++++++++++++---
.../ebooks/metadata/book/json_codec.py | 125 ++++++++
src/calibre/ebooks/metadata/isbndb.py | 6 +-
src/calibre/ebooks/metadata/opf2.py | 9 +-
.../gui2/dialogs/config/save_template.py | 29 +-
src/calibre/gui2/library/models.py | 3 +-
src/calibre/library/database2.py | 31 +-
src/calibre/library/save_to_disk.py | 19 +-
15 files changed, 514 insertions(+), 357 deletions(-)
create mode 100644 src/calibre/ebooks/metadata/book/json_codec.py
diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py
index 916c88f203..75517e9df7 100644
--- a/src/calibre/devices/apple/driver.py
+++ b/src/calibre/devices/apple/driver.py
@@ -13,7 +13,8 @@ from calibre.devices.errors import UserFeedback
from calibre.devices.usbms.deviceconfig import DeviceConfig
from calibre.devices.interface import DevicePlugin
from calibre.ebooks.BeautifulSoup import BeautifulSoup
-from calibre.ebooks.metadata import MetaInformation, authors_to_string
+from calibre.ebooks.metadata import authors_to_string
+from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.epub import set_metadata
from calibre.library.server.utils import strftime
from calibre.utils.config import config_dir
@@ -2998,14 +2999,14 @@ class BookList(list):
'''
return {}
-class Book(MetaInformation):
+class Book(Metadata):
'''
A simple class describing a book in the iTunes Books Library.
- See ebooks.metadata.__init__ for all fields
'''
def __init__(self,title,author):
- MetaInformation.__init__(self, title, authors=[author])
+ Metadata.__init__(self, title, authors=[author])
@dynamic_property
def title_sorter(self):
diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py
index 1384fa03d9..7783173c9c 100644
--- a/src/calibre/devices/interface.py
+++ b/src/calibre/devices/interface.py
@@ -316,7 +316,7 @@ class DevicePlugin(Plugin):
being uploaded to the device.
:param names: A list of file names that the books should have
once uploaded to the device. len(names) == len(files)
- :param metadata: If not None, it is a list of :class:`MetaInformation` objects.
+ :param metadata: If not None, it is a list of :class:`Metadata` objects.
The idea is to use the metadata to determine where on the device to
put the book. len(metadata) == len(files). Apart from the regular
cover (path to cover), there may also be a thumbnail attribute, which should
@@ -335,7 +335,7 @@ class DevicePlugin(Plugin):
the device.
:param locations: Result of a call to L{upload_books}
- :param metadata: List of :class:`MetaInformation` objects, same as for
+ :param metadata: List of :class:`Metadata` objects, same as for
:meth:`upload_books`.
:param booklists: A tuple containing the result of calls to
(:meth:`books(oncard=None)`,
diff --git a/src/calibre/devices/kobo/books.py b/src/calibre/devices/kobo/books.py
index a5b2e98d2f..11ab42c29f 100644
--- a/src/calibre/devices/kobo/books.py
+++ b/src/calibre/devices/kobo/books.py
@@ -7,11 +7,11 @@ import os
import re
import time
-from calibre.ebooks.metadata import MetaInformation
+from calibre.ebooks.metadata.book.base import Metadata
from calibre.constants import filesystem_encoding, preferred_encoding
from calibre import isbytestring
-class Book(MetaInformation):
+class Book(Metadata):
BOOK_ATTRS = ['lpath', 'size', 'mime', 'device_collections', '_new_book']
@@ -23,9 +23,9 @@ class Book(MetaInformation):
'uuid',
]
- def __init__(self, prefix, lpath, title, authors, mime, date, ContentType, thumbnail_name, other=None):
-
- MetaInformation.__init__(self, '')
+ def __init__(self, prefix, lpath, title, authors, mime, date, ContentType,
+ thumbnail_name, other=None):
+ Metadata.__init__(self, '')
self.device_collections = []
self._new_book = False
@@ -34,7 +34,7 @@ class Book(MetaInformation):
self.path = self.path.replace('/', '\\')
self.lpath = lpath.replace('\\', '/')
else:
- self.lpath = lpath
+ self.lpath = lpath
self.title = title
if not authors:
@@ -52,10 +52,9 @@ class Book(MetaInformation):
else:
self.datetime = time.gmtime(os.path.getctime(self.path))
except:
- self.datetime = time.gmtime()
-
- if thumbnail_name is not None:
- self.thumbnail = ImageWrapper(thumbnail_name)
+ self.datetime = time.gmtime()
+ if thumbnail_name is not None:
+ self.thumbnail = ImageWrapper(thumbnail_name)
self.tags = []
if other:
self.smart_update(other)
@@ -90,7 +89,7 @@ class Book(MetaInformation):
in C{other} takes precedence, unless the information in C{other} is NULL.
'''
- MetaInformation.smart_update(self, other)
+ Metadata.smart_update(self, other)
for attr in self.BOOK_ATTRS:
if hasattr(other, attr):
diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index 959f26199c..e3c405ee4e 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -6,29 +6,18 @@ __docformat__ = 'restructuredtext en'
import os, re, time, sys
-from calibre.ebooks.metadata import MetaInformation
+from calibre.ebooks.metadata.book.base import Metadata
from calibre.devices.mime import mime_type_ext
from calibre.devices.interface import BookList as _BookList
from calibre.constants import filesystem_encoding, preferred_encoding
from calibre import isbytestring
from calibre.utils.config import prefs
-class Book(MetaInformation):
-
- BOOK_ATTRS = ['lpath', 'size', 'mime', 'device_collections', '_new_book']
-
- JSON_ATTRS = [
- 'lpath', 'title', 'authors', 'mime', 'size', 'tags', 'author_sort',
- 'title_sort', 'comments', 'category', 'publisher', 'series',
- 'series_index', 'rating', 'isbn', 'language', 'application_id',
- 'book_producer', 'lccn', 'lcc', 'ddc', 'rights', 'publication_type',
- 'uuid',
- ]
-
+class Book(Metadata):
def __init__(self, prefix, lpath, size=None, other=None):
from calibre.ebooks.metadata.meta import path_to_ext
- MetaInformation.__init__(self, '')
+ Metadata.__init__(self, '')
self._new_book = False
self.device_collections = []
@@ -72,32 +61,6 @@ class Book(MetaInformation):
def thumbnail(self):
return None
- def smart_update(self, other, replace_metadata=False):
- '''
- Merge the information in C{other} into self. In case of conflicts, the information
- in C{other} takes precedence, unless the information in C{other} is NULL.
- '''
-
- MetaInformation.smart_update(self, other, replace_metadata)
-
- for attr in self.BOOK_ATTRS:
- if hasattr(other, attr):
- val = getattr(other, attr, None)
- setattr(self, attr, val)
-
- def to_json(self):
- json = {}
- for attr in self.JSON_ATTRS:
- val = getattr(self, attr)
- if isbytestring(val):
- enc = filesystem_encoding if attr == 'lpath' else preferred_encoding
- val = val.decode(enc, 'replace')
- elif isinstance(val, (list, tuple)):
- val = [x.decode(preferred_encoding, 'replace') if
- isbytestring(x) else x for x in val]
- json[attr] = val
- return json
-
class BookList(_BookList):
def __init__(self, oncard, prefix, settings):
diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py
index 0d28f06f49..a0d1d9dbf8 100644
--- a/src/calibre/devices/usbms/driver.py
+++ b/src/calibre/devices/usbms/driver.py
@@ -13,7 +13,6 @@ for a particular device.
import os
import re
import time
-import json
from itertools import cycle
from calibre import prints, isbytestring
@@ -21,6 +20,7 @@ from calibre.constants import filesystem_encoding, DEBUG
from calibre.devices.usbms.cli import CLI
from calibre.devices.usbms.device import Device
from calibre.devices.usbms.books import BookList, Book
+from calibre.ebooks.metadata.book.json_codec import JsonCodec
BASE_TIME = None
def debug_print(*args):
@@ -288,6 +288,7 @@ class USBMS(CLI, Device):
# at the end just before the return
def sync_booklists(self, booklists, end_session=True):
debug_print('USBMS: starting sync_booklists')
+ json_codec = JsonCodec()
if not os.path.exists(self.normalize_path(self._main_prefix)):
os.makedirs(self.normalize_path(self._main_prefix))
@@ -296,10 +297,8 @@ class USBMS(CLI, Device):
if prefix is not None and isinstance(booklists[listid], self.booklist_class):
if not os.path.exists(prefix):
os.makedirs(self.normalize_path(prefix))
- js = [item.to_json() for item in booklists[listid] if
- hasattr(item, 'to_json')]
with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f:
- f.write(json.dumps(js, indent=2, encoding='utf-8'))
+ json_codec.encode_to_file(f, booklists[listid])
write_prefix(self._main_prefix, 0)
write_prefix(self._card_a_prefix, 1)
write_prefix(self._card_b_prefix, 2)
@@ -345,19 +344,13 @@ class USBMS(CLI, Device):
@classmethod
def parse_metadata_cache(cls, bl, prefix, name):
- # bl = cls.booklist_class()
- js = []
+ json_codec = JsonCodec()
need_sync = False
cache_file = cls.normalize_path(os.path.join(prefix, name))
if os.access(cache_file, os.R_OK):
try:
with open(cache_file, 'rb') as f:
- js = json.load(f, encoding='utf-8')
- for item in js:
- book = cls.book_class(prefix, item.get('lpath', None))
- for key in item.keys():
- setattr(book, key, item[key])
- bl.append(book)
+ json_codec.decode_from_file(f, bl, cls.book_class, prefix)
except:
import traceback
traceback.print_exc()
@@ -392,7 +385,7 @@ class USBMS(CLI, Device):
@classmethod
def book_from_path(cls, prefix, lpath):
- from calibre.ebooks.metadata import MetaInformation
+ from calibre.ebooks.metadata.book.base import Metadata
if cls.settings().read_metadata or cls.MUST_READ_METADATA:
mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath)))
@@ -401,7 +394,7 @@ class USBMS(CLI, Device):
mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)),
cls.build_template_regexp())
if mi is None:
- mi = MetaInformation(os.path.splitext(os.path.basename(lpath))[0],
+ mi = Metadata(os.path.splitext(os.path.basename(lpath))[0],
[_('Unknown')])
size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size
book = cls.book_class(prefix, lpath, other=mi, size=size)
diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py
index d4a21e2c8c..fb894d3bbd 100644
--- a/src/calibre/ebooks/metadata/__init__.py
+++ b/src/calibre/ebooks/metadata/__init__.py
@@ -221,214 +221,18 @@ class ResourceCollection(object):
-class MetaInformation(object):
- '''Convenient encapsulation of book metadata'''
-
- @staticmethod
- def copy(mi):
- ans = MetaInformation(mi.title, mi.authors)
- for attr in ('author_sort', 'title_sort', 'comments', 'category',
- 'publisher', 'series', 'series_index', 'rating',
- 'isbn', 'tags', 'cover_data', 'application_id', 'guide',
- 'manifest', 'spine', 'toc', 'cover', 'language',
- 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
- 'author_sort_map',
- 'pubdate', 'rights', 'publication_type', 'uuid'):
- if hasattr(mi, attr):
- setattr(ans, attr, getattr(mi, attr))
-
- def __init__(self, title, authors=(_('Unknown'),)):
- '''
+def MetaInformation(title, authors=(_('Unknown'),)):
+ ''' Convenient encapsulation of book metadata, needed for compatibility
@param title: title or ``_('Unknown')`` or a MetaInformation object
@param authors: List of strings or []
- '''
- mi = None
- if hasattr(title, 'title') and hasattr(title, 'authors'):
- mi = title
- title = mi.title
- authors = mi.authors
- self.title = title
- self.author = list(authors) if authors else []# Needed for backward compatibility
- #: List of strings or []
- self.authors = list(authors) if authors else []
- self.tags = getattr(mi, 'tags', [])
- #: mi.cover_data = (ext, data)
- self.cover_data = getattr(mi, 'cover_data', (None, None))
- self.author_sort_map = getattr(mi, 'author_sort_map', {})
-
- for x in ('author_sort', 'title_sort', 'comments', 'category', 'publisher',
- 'series', 'series_index', 'rating', 'isbn', 'language',
- 'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
- 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
- 'rights', 'publication_type', 'uuid',
- ):
- setattr(self, x, getattr(mi, x, None))
-
- def print_all_attributes(self):
- for x in ('title','author', 'author_sort', 'title_sort', 'comments', 'category', 'publisher',
- 'series', 'series_index', 'tags', 'rating', 'isbn', 'language',
- 'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
- 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
- 'rights', 'publication_type', 'uuid', 'author_sort_map'
- ):
- prints(x, getattr(self, x, 'None'))
-
- def smart_update(self, mi, replace_metadata=False):
- '''
- Merge the information in C{mi} into self. In case of conflicts, the
- information in C{mi} takes precedence, unless the information in mi is
- NULL. If replace_metadata is True, then the information in mi always
- takes precedence.
- '''
- if mi.title and mi.title != _('Unknown'):
- self.title = mi.title
-
- if mi.authors and mi.authors[0] != _('Unknown'):
- self.authors = mi.authors
-
- for attr in ('author_sort', 'title_sort', 'category',
- 'publisher', 'series', 'series_index', 'rating',
- 'isbn', 'application_id', 'manifest', 'spine', 'toc',
- 'cover', 'guide', 'book_producer',
- 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', 'rights',
- 'publication_type', 'uuid'):
- if replace_metadata:
- setattr(self, attr, getattr(mi, attr, 1.0 if \
- attr == 'series_index' else None))
- elif hasattr(mi, attr):
- val = getattr(mi, attr)
- if val is not None:
- setattr(self, attr, val)
-
- if replace_metadata:
- self.tags = mi.tags
- elif mi.tags:
- self.tags += mi.tags
- self.tags = list(set(self.tags))
-
- if mi.author_sort_map:
- self.author_sort_map.update(mi.author_sort_map)
-
- if getattr(mi, 'cover_data', False):
- other_cover = mi.cover_data[-1]
- self_cover = self.cover_data[-1] if self.cover_data else ''
- if not self_cover: self_cover = ''
- if not other_cover: other_cover = ''
- if len(other_cover) > len(self_cover):
- self.cover_data = mi.cover_data
-
- if replace_metadata:
- self.comments = getattr(mi, 'comments', '')
- else:
- my_comments = getattr(self, 'comments', '')
- other_comments = getattr(mi, 'comments', '')
- if not my_comments:
- my_comments = ''
- if not other_comments:
- other_comments = ''
- if len(other_comments.strip()) > len(my_comments.strip()):
- self.comments = other_comments
-
- other_lang = getattr(mi, 'language', None)
- if other_lang and other_lang.lower() != 'und':
- self.language = other_lang
-
-
- def format_series_index(self):
- try:
- x = float(self.series_index)
- except ValueError:
- x = 1
- return fmt_sidx(x)
-
- def authors_from_string(self, raw):
- self.authors = string_to_authors(raw)
-
- def format_authors(self):
- return authors_to_string(self.authors)
-
- def format_tags(self):
- return u', '.join([unicode(t) for t in self.tags])
-
- def format_rating(self):
- return unicode(self.rating)
-
- def __unicode__(self):
- ans = []
- def fmt(x, y):
- ans.append(u'%-20s: %s'%(unicode(x), unicode(y)))
-
- fmt('Title', self.title)
- if self.title_sort:
- fmt('Title sort', self.title_sort)
- if self.authors:
- fmt('Author(s)', authors_to_string(self.authors) + \
- ((' [' + self.author_sort + ']') if self.author_sort else ''))
- if self.publisher:
- fmt('Publisher', self.publisher)
- if getattr(self, 'book_producer', False):
- fmt('Book Producer', self.book_producer)
- if self.category:
- fmt('Category', self.category)
- if self.comments:
- fmt('Comments', self.comments)
- if self.isbn:
- fmt('ISBN', self.isbn)
- if self.tags:
- fmt('Tags', u', '.join([unicode(t) for t in self.tags]))
- if self.series:
- fmt('Series', self.series + ' #%s'%self.format_series_index())
- if self.language:
- fmt('Language', self.language)
- if self.rating is not None:
- fmt('Rating', self.rating)
- if self.timestamp is not None:
- fmt('Timestamp', isoformat(self.timestamp))
- if self.pubdate is not None:
- fmt('Published', isoformat(self.pubdate))
- if self.rights is not None:
- fmt('Rights', unicode(self.rights))
- if self.lccn:
- fmt('LCCN', unicode(self.lccn))
- if self.lcc:
- fmt('LCC', unicode(self.lcc))
- if self.ddc:
- fmt('DDC', unicode(self.ddc))
-
- return u'\n'.join(ans)
-
- def to_html(self):
- ans = [(_('Title'), unicode(self.title))]
- ans += [(_('Author(s)'), (authors_to_string(self.authors) if self.authors else _('Unknown')))]
- ans += [(_('Publisher'), unicode(self.publisher))]
- ans += [(_('Producer'), unicode(self.book_producer))]
- ans += [(_('Comments'), unicode(self.comments))]
- ans += [('ISBN', unicode(self.isbn))]
- if self.lccn:
- ans += [('LCCN', unicode(self.lccn))]
- if self.lcc:
- ans += [('LCC', unicode(self.lcc))]
- if self.ddc:
- ans += [('DDC', unicode(self.ddc))]
- ans += [(_('Tags'), u', '.join([unicode(t) for t in self.tags]))]
- if self.series:
- ans += [(_('Series'), unicode(self.series)+ ' #%s'%self.format_series_index())]
- ans += [(_('Language'), unicode(self.language))]
- if self.timestamp is not None:
- ans += [(_('Timestamp'), unicode(self.timestamp.isoformat(' ')))]
- if self.pubdate is not None:
- ans += [(_('Published'), unicode(self.pubdate.isoformat(' ')))]
- if self.rights is not None:
- ans += [(_('Rights'), unicode(self.rights))]
- for i, x in enumerate(ans):
- ans[i] = u'
'
__docformat__ = 'restructuredtext en'
import copy
+import traceback
+
+from calibre import prints
+from calibre.ebooks.metadata.book import COPYABLE_METADATA_FIELDS
+from calibre.ebooks.metadata.book import STANDARD_METADATA_FIELDS
+from calibre.utils.date import isoformat
-from calibre.ebooks.metadata.book import RESERVED_METADATA_FIELDS
NULL_VALUES = {
'user_metadata': {},
@@ -24,98 +29,313 @@ NULL_VALUES = {
class Metadata(object):
'''
- This class must expose a superset of the API of MetaInformation in terms
- of attribute access and methods. Only the __init__ method is different.
- MetaInformation will simply become a function that creates and fills in
- the attributes of this class.
+ A class representing all the metadata for a book.
Please keep the method based API of this class to a minimum. Every method
becomes a reserved field name.
'''
- def __init__(self):
+ def __init__(self, title, authors=(_('Unknown'),), other=None):
+ '''
+ @param title: title or ``_('Unknown')``
+ @param authors: List of strings or []
+ @param other: None or a metadata object
+ '''
object.__setattr__(self, '_data', copy.deepcopy(NULL_VALUES))
+ if other is not None:
+ self.smart_update(other)
+ else:
+ if title:
+ self.title = title
+ if authors:
+ #: List of strings or []
+ self.author = list(authors) if authors else []# Needed for backward compatibility
+ self.authors = list(authors) if authors else []
def __getattribute__(self, field):
_data = object.__getattribute__(self, '_data')
- if field in RESERVED_METADATA_FIELDS:
+ if field in STANDARD_METADATA_FIELDS:
return _data.get(field, None)
try:
return object.__getattribute__(self, field)
except AttributeError:
pass
if field in _data['user_metadata'].iterkeys():
- # TODO: getting user metadata values
- pass
+ return _data['user_metadata'][field]['#value#']
raise AttributeError(
'Metadata object has no attribute named: '+ repr(field))
-
def __setattr__(self, field, val):
_data = object.__getattribute__(self, '_data')
- if field in RESERVED_METADATA_FIELDS:
- if field != 'user_metadata':
- if not val:
- val = NULL_VALUES[field]
- _data[field] = val
- else:
- raise AttributeError('You cannot set user_metadata directly.')
+ if field in STANDARD_METADATA_FIELDS:
+ if not val:
+ val = NULL_VALUES.get(field, None)
+ _data[field] = val
elif field in _data['user_metadata'].iterkeys():
- # TODO: Setting custom column values
- pass
+ _data['user_metadata'][field]['#value#'] = val
else:
# You are allowed to stick arbitrary attributes onto this object as
# long as they dont conflict with global or user metadata names
# Don't abuse this privilege
self.__dict__[field] = val
+ def get(self, field):
+ return self.__getattribute__(field)
+
+ def set(self, field, val):
+ self.__setattr__(field, val)
+
@property
- def user_metadata_names(self):
+ def user_metadata_keys(self):
'The set of user metadata names this object knows about'
_data = object.__getattribute__(self, '_data')
return frozenset(_data['user_metadata'].iterkeys())
- # Old MetaInformation API {{{
- def copy(self):
- pass
+ @property
+ def all_user_metadata(self):
+ '''
+ return a dict containing all the custom field metadata associated with
+ the book. Return a deep copy, just in case the user wants to change
+ values in the dict (json does).
+ '''
+ _data = object.__getattribute__(self, '_data')
+ _data = _data['user_metadata']
+ res = {}
+ for k in _data:
+ res[k] = copy.deepcopy(_data[k])
+ return res
+
+ def get_user_metadata(self, field):
+ '''
+ return field metadata from the object if it is there. Otherwise return
+ None. field is the key name, not the label. Return a shallow copy,
+ just in case the user wants to change values in the dict (json does).
+ '''
+ _data = object.__getattribute__(self, '_data')
+ _data = _data['user_metadata']
+ if field in _data:
+ return copy.deepcopy(_data[field])
+ return None
+
+ def set_all_user_metadata(self, metadata):
+ '''
+ store custom field metadata into the object. Field is the key name
+ not the label
+ '''
+ if metadata is None:
+ traceback.print_stack()
+ else:
+ for key in metadata:
+ self.set_user_metadata(key, metadata[key])
+
+ def set_user_metadata(self, field, metadata):
+ '''
+ store custom field metadata for one column into the object. Field is
+ the key name not the label
+ '''
+ if field is not None:
+ if metadata is None:
+ traceback.print_stack()
+ metadata = copy.deepcopy(metadata)
+ if '#value#' not in metadata:
+ metadata['#value#'] = None
+ _data = object.__getattribute__(self, '_data')
+ _data['user_metadata'][field] = metadata
+
+ @property
+ def all_attributes(self):
+ result = {}
+ _data = object.__getattribute__(self, '_data')
+ for attr in STANDARD_METADATA_FIELDS:
+ v = _data.get(attr, None)
+ if v is not None:
+ result[attr] = v
+ for attr in self.user_metadata_keys:
+ if self.get(attr) is not None:
+ result[attr] = self.get(attr)
+ return result
+
+ # Old Metadata API {{{
+ @staticmethod
+ def copy(mi):
+ ans = Metadata(mi.title, mi.authors)
+ for attr in STANDARD_METADATA_FIELDS:
+ if hasattr(mi, attr):
+ setattr(ans, attr, copy.deepcopy(getattr(mi, attr)))
+ for x in mi.user_metadata_keys:
+ meta = mi.get_user_metadata(x)
+ if meta is not None:
+ ans.set_user_metadata(x, meta) # get... did the deep copy
def print_all_attributes(self):
- pass
+ for x in STANDARD_METADATA_FIELDS:
+ prints('%s:'%x, getattr(self, x, 'None'))
+ for x in self.user_metadata_keys:
+ meta = self.get_user_metadata(x)
+ if meta is not None:
+ prints(x, meta)
+ prints('--------------')
def smart_update(self, other, replace_metadata=False):
- pass
+ '''
+ Merge the information in C{other} into self. In case of conflicts, the information
+ in C{other} takes precedence, unless the information in other is NULL.
+ '''
+ if other.title and other.title != _('Unknown'):
+ self.title = other.title
+
+ if other.authors and other.authors[0] != _('Unknown'):
+ self.authors = other.authors
+
+ for attr in COPYABLE_METADATA_FIELDS:
+ if replace_metadata:
+ setattr(self, attr, getattr(other, attr, 1.0 if \
+ attr == 'series_index' else None))
+ elif hasattr(other, attr):
+ val = getattr(other, attr)
+ if val is not None:
+ setattr(self, attr, copy.deepcopy(val))
+
+ if replace_metadata:
+ self.tags = other.tags
+ elif other.tags:
+ self.tags += other.tags
+ self.tags = list(set(self.tags))
+
+ if getattr(other, 'author_sort_map', None):
+ self.author_sort_map.update(other.author_sort_map)
+
+ if getattr(other, 'cover_data', False):
+ other_cover = other.cover_data[-1]
+ self_cover = self.cover_data[-1] if self.cover_data else ''
+ if not self_cover: self_cover = ''
+ if not other_cover: other_cover = ''
+ if len(other_cover) > len(self_cover):
+ self.cover_data = other.cover_data
+
+ if getattr(other, 'user_metadata_keys', None):
+ for x in other.user_metadata_keys:
+ meta = other.get_user_metadata(x)
+ if meta is not None or replace_metadata:
+ self.set_user_metadata(x, meta) # get... did the deepcopy
+
+ if replace_metadata:
+ self.comments = getattr(other, 'comments', '')
+ else:
+ my_comments = getattr(self, 'comments', '')
+ other_comments = getattr(other, 'comments', '')
+ if not my_comments:
+ my_comments = ''
+ if not other_comments:
+ other_comments = ''
+ if len(other_comments.strip()) > len(my_comments.strip()):
+ self.comments = other_comments
+
+ other_lang = getattr(other, 'language', None)
+ if other_lang and other_lang.lower() != 'und':
+ self.language = other_lang
+
def format_series_index(self):
- pass
+ from calibre.ebooks.metadata import fmt_sidx
+ try:
+ x = float(self.series_index)
+ except ValueError:
+ x = 1
+ return fmt_sidx(x)
def authors_from_string(self, raw):
- pass
+ from calibre.ebooks.metadata import string_to_authors
+ self.authors = string_to_authors(raw)
def format_authors(self):
- pass
+ from calibre.ebooks.metadata import authors_to_string
+ return authors_to_string(self.authors)
def format_tags(self):
- pass
+ return u', '.join([unicode(t) for t in self.tags])
def format_rating(self):
return unicode(self.rating)
def __unicode__(self):
- pass
+ from calibre.ebooks.metadata import authors_to_string
+ ans = []
+ def fmt(x, y):
+ ans.append(u'%-20s: %s'%(unicode(x), unicode(y)))
+
+ fmt('Title', self.title)
+ if self.title_sort:
+ fmt('Title sort', self.title_sort)
+ if self.authors:
+ fmt('Author(s)', authors_to_string(self.authors) + \
+ ((' [' + self.author_sort + ']') if self.author_sort else ''))
+ if self.publisher:
+ fmt('Publisher', self.publisher)
+ if getattr(self, 'book_producer', False):
+ fmt('Book Producer', self.book_producer)
+ if self.category:
+ fmt('Category', self.category)
+ if self.comments:
+ fmt('Comments', self.comments)
+ if self.isbn:
+ fmt('ISBN', self.isbn)
+ if self.tags:
+ fmt('Tags', u', '.join([unicode(t) for t in self.tags]))
+ if self.series:
+ fmt('Series', self.series + ' #%s'%self.format_series_index())
+ if self.language:
+ fmt('Language', self.language)
+ if self.rating is not None:
+ fmt('Rating', self.rating)
+ if self.timestamp is not None:
+ fmt('Timestamp', isoformat(self.timestamp))
+ if self.pubdate is not None:
+ fmt('Published', isoformat(self.pubdate))
+ if self.rights is not None:
+ fmt('Rights', unicode(self.rights))
+ if self.lccn:
+ fmt('LCCN', unicode(self.lccn))
+ if self.lcc:
+ fmt('LCC', unicode(self.lcc))
+ if self.ddc:
+ fmt('DDC', unicode(self.ddc))
+ # CUSTFIELD: What to do about custom fields?
+ return u'\n'.join(ans)
def to_html(self):
- pass
+ from calibre.ebooks.metadata import authors_to_string
+ ans = [(_('Title'), unicode(self.title))]
+ ans += [(_('Author(s)'), (authors_to_string(self.authors) if self.authors else _('Unknown')))]
+ ans += [(_('Publisher'), unicode(self.publisher))]
+ ans += [(_('Producer'), unicode(self.book_producer))]
+ ans += [(_('Comments'), unicode(self.comments))]
+ ans += [('ISBN', unicode(self.isbn))]
+ if self.lccn:
+ ans += [('LCCN', unicode(self.lccn))]
+ if self.lcc:
+ ans += [('LCC', unicode(self.lcc))]
+ if self.ddc:
+ ans += [('DDC', unicode(self.ddc))]
+ ans += [(_('Tags'), u', '.join([unicode(t) for t in self.tags]))]
+ if self.series:
+ ans += [(_('Series'), unicode(self.series)+ ' #%s'%self.format_series_index())]
+ ans += [(_('Language'), unicode(self.language))]
+ if self.timestamp is not None:
+ ans += [(_('Timestamp'), unicode(self.timestamp.isoformat(' ')))]
+ if self.pubdate is not None:
+ ans += [(_('Published'), unicode(self.pubdate.isoformat(' ')))]
+ if self.rights is not None:
+ ans += [(_('Rights'), unicode(self.rights))]
+ for i, x in enumerate(ans):
+ ans[i] = u'%s | %s |
'%x
+ # CUSTFIELD: What to do about custom fields
+ return u''%u'\n'.join(ans)
def __str__(self):
return self.__unicode__().encode('utf-8')
def __nonzero__(self):
- return True
+ return bool(self.title or self.author or self.comments or self.tags)
# }}}
-
-# We don't need reserved field names for this object any more. Lets just use a
-# protocol like the last char of a user field label should be _ when using this
-# object
-# So mi.tags returns the builtin tags and mi.tags_ returns the user tags
-
diff --git a/src/calibre/ebooks/metadata/book/json_codec.py b/src/calibre/ebooks/metadata/book/json_codec.py
new file mode 100644
index 0000000000..5e13650f0e
--- /dev/null
+++ b/src/calibre/ebooks/metadata/book/json_codec.py
@@ -0,0 +1,125 @@
+'''
+Created on 4 Jun 2010
+
+@author: charles
+'''
+
+from base64 import b64encode, b64decode
+import json
+import traceback
+from PIL import Image
+
+from . import SERIALIZABLE_FIELDS
+from calibre.constants import filesystem_encoding, preferred_encoding
+from calibre.library.field_metadata import FieldMetadata
+from calibre.utils.date import parse_date, isoformat, UNDEFINED_DATE
+
+# Translate datetimes to and from strings. The string form is the datetime in
+# UTC. The returned date is also UTC
+def string_to_datetime(src):
+ if src == "None":
+ return None
+# dt = strptime(src, '%d %m %Y %H:%M:%S', assume_utc=True, as_utc=True)
+# if dt == UNDEFINED_DATE:
+# return None
+ return parse_date(src)
+
+def datetime_to_string(dateval):
+ if dateval is None or dateval == UNDEFINED_DATE:
+ return "None"
+# tt = date_to_utc(dateval).timetuple()
+# res = "%02d %02d %04d %02d:%02d:%02d"%(tt.tm_mday, tt.tm_mon, tt.tm_year,
+# tt.tm_hour, tt.tm_min, tt.tm_sec)
+ return isoformat(dateval)
+
+def encode_thumbnail(thumbnail):
+ '''
+ Encode the image part of a thumbnail, then return the 3 part tuple
+ '''
+ if thumbnail is None:
+ return None
+ return (thumbnail[0], thumbnail[1], b64encode(str(thumbnail[2])))
+
+def decode_thumbnail(tup):
+ '''
+ Decode an encoded thumbnail into its 3 component parts
+ '''
+ if tup is None:
+ return None
+ return (tup[0], tup[1], b64decode(tup[2]))
+
+class JsonCodec(object):
+
+ def __init__(self):
+ self.field_metadata = FieldMetadata()
+
+ def encode_to_file(self, file, booklist):
+ json.dump(self.encode_booklist_metadata(booklist), file, indent=2, encoding='utf-8')
+
+ def encode_booklist_metadata(self, booklist):
+ result = []
+ for book in booklist:
+ result.append(self.encode_book_metadata(book))
+ return result
+
+ def encode_book_metadata(self, book):
+ result = {}
+ for key in SERIALIZABLE_FIELDS:
+ result[key] = self.encode_metadata_attr(book, key)
+ return result
+
+ def encode_metadata_attr(self, book, key):
+ if key == 'user_metadata':
+ meta = book.all_user_metadata
+ for k in meta:
+ if meta[k]['datatype'] == 'datetime':
+ meta[k]['#value#'] = datetime_to_string(meta[k]['#value#'])
+ return meta
+ if key in self.field_metadata:
+ datatype = self.field_metadata[key]['datatype']
+ else:
+ datatype = None
+ value = book.get(key)
+ if key == 'thumbnail':
+ return encode_thumbnail(value)
+ elif isinstance(value, str): # str includes bytes
+ enc = filesystem_encoding if key == 'lpath' else preferred_encoding
+ return value.decode(enc, 'replace')
+ elif isinstance(value, (list, tuple)):
+ return [x.decode(preferred_encoding, 'replace') if
+ isinstance(x, str) else x for x in value]
+ elif datatype == 'datetime':
+ return datetime_to_string(value)
+ else:
+ return value
+
+ def decode_from_file(self, file, booklist, book_class, prefix):
+ js = []
+ try:
+ js = json.load(file, encoding='utf-8')
+ for item in js:
+ book = book_class(prefix, item.get('lpath', None))
+ for key in item.keys():
+ meta = self.decode_metadata(key, item[key])
+ if key == 'user_metadata':
+ book.set_all_user_metadata(meta)
+ else:
+ setattr(book, key, meta)
+ booklist.append(book)
+ except:
+ print 'exception during JSON decoding'
+ traceback.print_exc()
+ booklist = []
+
+ def decode_metadata(self, key, value):
+ if key == 'user_metadata':
+ for k in value:
+ if value[k]['datatype'] == 'datetime':
+ value[k]['#value#'] = string_to_datetime(value[k]['#value#'])
+ return value
+ elif key in self.field_metadata:
+ if self.field_metadata[key]['datatype'] == 'datetime':
+ return string_to_datetime(value)
+ if key == 'thumbnail':
+ return decode_thumbnail(value)
+ return value
diff --git a/src/calibre/ebooks/metadata/isbndb.py b/src/calibre/ebooks/metadata/isbndb.py
index 356cc3f1b1..b5fc5830c8 100644
--- a/src/calibre/ebooks/metadata/isbndb.py
+++ b/src/calibre/ebooks/metadata/isbndb.py
@@ -8,7 +8,7 @@ import sys, re
from urllib import quote
from calibre.utils.config import OptionParser
-from calibre.ebooks.metadata import MetaInformation
+from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
from calibre import browser
@@ -42,10 +42,10 @@ def fetch_metadata(url, max=100, timeout=5.):
return books
-class ISBNDBMetadata(MetaInformation):
+class ISBNDBMetadata(Metadata):
def __init__(self, book):
- MetaInformation.__init__(self, None, [])
+ Metadata.__init__(self, None, [])
self.isbn = book.get('isbn13', book.get('isbn'))
self.title = book.find('titlelong').string
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index f93b614ef2..0ab6d3bbc0 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -16,7 +16,8 @@ from lxml import etree
from calibre.ebooks.chardet import xml_to_unicode
from calibre.constants import __appname__, __version__, filesystem_encoding
from calibre.ebooks.metadata.toc import TOC
-from calibre.ebooks.metadata import MetaInformation, string_to_authors
+from calibre.ebooks.metadata import string_to_authors
+from calibre.ebooks.metadata.book.base import Metadata
from calibre.utils.date import parse_date, isoformat
from calibre.utils.localization import get_lang
@@ -926,16 +927,16 @@ class OPF(object):
setattr(self, attr, val)
-class OPFCreator(MetaInformation):
+class OPFCreator(Metadata):
- def __init__(self, base_path, *args, **kwargs):
+ def __init__(self, base_path, other):
'''
Initialize.
@param base_path: An absolute path to the directory in which this OPF file
will eventually be. This is used by the L{create_manifest} method
to convert paths to files into relative paths.
'''
- MetaInformation.__init__(self, *args, **kwargs)
+ Metadata.__init__(self, title='', other=other)
self.base_path = os.path.abspath(base_path)
if self.application_id is None:
self.application_id = str(uuid.uuid4())
diff --git a/src/calibre/gui2/dialogs/config/save_template.py b/src/calibre/gui2/dialogs/config/save_template.py
index 71eb15f4aa..2157d5b3bf 100644
--- a/src/calibre/gui2/dialogs/config/save_template.py
+++ b/src/calibre/gui2/dialogs/config/save_template.py
@@ -34,25 +34,24 @@ class SaveTemplate(QWidget, Ui_Form):
self.option_name = name
def validate(self):
- tmpl = preprocess_template(self.opt_template.text())
- fa = {}
- for x in FORMAT_ARG_DESCS.keys():
- fa[x]='random long string'
- try:
- tmpl.format(**fa)
- except Exception, err:
- error_dialog(self, _('Invalid template'),
- ''+_('The template %s is invalid:')%tmpl + \
- '
'+str(err), show=True)
- return False
+ # TODO: I haven't figured out how to get the custom columns into here,
+ # so for the moment make all templates valid.
return True
+# tmpl = preprocess_template(self.opt_template.text())
+# fa = {}
+# for x in FORMAT_ARG_DESCS.keys():
+# fa[x]='random long string'
+# try:
+# tmpl.format(**fa)
+# except Exception, err:
+# error_dialog(self, _('Invalid template'),
+# '
'+_('The template %s is invalid:')%tmpl + \
+# '
'+str(err), show=True)
+# return False
+# return True
def save_settings(self, config, name):
val = unicode(self.opt_template.text())
config.set(name, val)
self.opt_template.save_history(self.option_name+'_template_history')
-
-
-
-
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index 89008735fe..fdf21ecc23 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -372,7 +372,6 @@ class BooksModel(QAbstractTableModel): # {{{
return ans
def get_metadata(self, rows, rows_are_ids=False, full_metadata=False):
- # Should this add the custom columns? It doesn't at the moment
metadata, _full_metadata = [], []
if not rows_are_ids:
rows = [self.db.id(row.row()) for row in rows]
@@ -1053,7 +1052,7 @@ class DeviceBooksModel(BooksModel): # {{{
if hasattr(cdata, 'image_path'):
img.load(cdata.image_path)
else:
- img.loadFromData(cdata)
+ img.loadFromData(cdata[2])
if img.isNull():
img = self.default_image
data['cover'] = img
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index ef74188bdf..9f21fe0eda 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -509,15 +509,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
'''
Convenience method to return metadata as a L{MetaInformation} object.
'''
- aum = self.authors(idx, index_is_id=index_is_id)
- if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')]
+ aut_list = self.authors_with_sort_strings(idx, index_is_id=index_is_id)
+ aum = []
+ aus = {}
+ for (author, author_sort) in aut_list:
+ aum.append(author)
+ aus[author] = author_sort
mi = MetaInformation(self.title(idx, index_is_id=index_is_id), aum)
mi.author_sort = self.author_sort(idx, index_is_id=index_is_id)
- if mi.authors:
- mi.author_sort_map = {}
- for name, sort in zip(mi.authors, self.authors_sort_strings(idx,
- index_is_id)):
- mi.author_sort_map[name] = sort
+ mi.author_sort_map = aus
mi.comments = self.comments(idx, index_is_id=index_is_id)
mi.publisher = self.publisher(idx, index_is_id=index_is_id)
mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
@@ -534,6 +534,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
mi.isbn = self.isbn(idx, index_is_id=index_is_id)
id = idx if index_is_id else self.id(idx)
mi.application_id = id
+ for key,meta in self.field_metadata.iteritems():
+ if meta['is_custom']:
+ mi.set_user_metadata(key, meta)
+ mi.set(key, self.get_custom(idx, label=meta['label'], index_is_id=index_is_id))
if get_cover:
mi.cover = self.cover(id, index_is_id=True, as_path=True)
return mi
@@ -1049,6 +1053,19 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
result.append(sort)
return result
+ # Given a book, return the map of author sort strings for the book's authors
+ def authors_with_sort_strings(self, id, index_is_id=False):
+ id = id if index_is_id else self.id(id)
+ aut_strings = self.conn.get('''
+ SELECT authors.name, authors.sort
+ FROM authors, books_authors_link as bl
+ WHERE bl.book=? and authors.id=bl.author
+ ORDER BY bl.id''', (id,))
+ result = []
+ for (author, sort,) in aut_strings:
+ result.append((author.replace('|', ','), sort))
+ return result
+
# Given a book, return the author_sort string for authors of the book
def author_sort_from_book(self, id, index_is_id=False):
auts = self.authors_sort_strings(id, index_is_id)
diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py
index 15020855f7..258ea7ba9e 100644
--- a/src/calibre/library/save_to_disk.py
+++ b/src/calibre/library/save_to_disk.py
@@ -105,6 +105,8 @@ def safe_format(x, format_args):
pass
except AttributeError: # Thrown if user used a non existing attribute
pass
+ except KeyError: # Thrown if user used custom field w/value None
+ pass
return ''
def get_components(template, mi, id, timefmt='%b %Y', length=250,
@@ -113,13 +115,12 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
library_order = tweaks['save_template_title_series_sorting'] == 'library_order'
tsfmt = title_sort if library_order else lambda x: x
format_args = dict(**FORMAT_ARGS)
+ format_args.update(mi.all_attributes)
if mi.title:
format_args['title'] = tsfmt(mi.title)
if mi.authors:
format_args['authors'] = mi.format_authors()
format_args['author'] = format_args['authors']
- if mi.author_sort:
- format_args['author_sort'] = mi.author_sort
if mi.tags:
format_args['tags'] = mi.format_tags()
if format_args['tags'].startswith('/'):
@@ -132,15 +133,21 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
template = re.sub(r'\{series_index[^}]*?\}', '', template)
if mi.rating is not None:
format_args['rating'] = mi.format_rating()
- if mi.isbn:
- format_args['isbn'] = mi.isbn
- if mi.publisher:
- format_args['publisher'] = mi.publisher
if hasattr(mi.timestamp, 'timetuple'):
format_args['timestamp'] = strftime(timefmt, mi.timestamp.timetuple())
if hasattr(mi.pubdate, 'timetuple'):
format_args['pubdate'] = strftime(timefmt, mi.pubdate.timetuple())
format_args['id'] = str(id)
+
+ # These are not necessary any more. The values are set by
+ # 'format_args.update' above, and there is no special formatting
+# if mi.author_sort:
+# format_args['author_sort'] = mi.author_sort
+# if mi.isbn:
+# format_args['isbn'] = mi.isbn
+# if mi.publisher:
+# format_args['publisher'] = mi.publisher
+
components = [x.strip() for x in template.split('/') if x.strip()]
components = [safe_format(x, format_args) for x in components]
components = [sanitize_func(x) for x in components if x]
From b04faf70c2378c3569a4d1cf010da3d57a707c42 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Fri, 27 Aug 2010 08:22:08 +0100
Subject: [PATCH 002/289] Make Kobo driver use new metadata framework
---
src/calibre/devices/kobo/books.py | 80 ++----------------------------
src/calibre/devices/kobo/driver.py | 2 +-
2 files changed, 5 insertions(+), 77 deletions(-)
diff --git a/src/calibre/devices/kobo/books.py b/src/calibre/devices/kobo/books.py
index f0cf7c3763..1c3d05ea12 100644
--- a/src/calibre/devices/kobo/books.py
+++ b/src/calibre/devices/kobo/books.py
@@ -4,37 +4,15 @@ __copyright__ = '2010, Timothy Legge '
'''
import os
-import re
import time
-from calibre.ebooks.metadata.book.base import Metadata
-from calibre.constants import filesystem_encoding, preferred_encoding
-from calibre import isbytestring
+from calibre.devices.usbms.books import Book as Book_
-class Book(Metadata):
-
- BOOK_ATTRS = ['lpath', 'size', 'mime', 'device_collections', '_new_book']
-
- JSON_ATTRS = [
- 'lpath', 'title', 'authors', 'mime', 'size', 'tags', 'author_sort',
- 'title_sort', 'comments', 'category', 'publisher', 'series',
- 'series_index', 'rating', 'isbn', 'language', 'application_id',
- 'book_producer', 'lccn', 'lcc', 'ddc', 'rights', 'publication_type',
- 'uuid', 'device_collections',
- ]
+class Book(Book_):
def __init__(self, prefix, lpath, title, authors, mime, date, ContentType,
thumbnail_name, other=None):
- Metadata.__init__(self, '')
- self.device_collections = []
- self._new_book = False
-
- self.path = os.path.join(prefix, lpath)
- if os.sep == '\\':
- self.path = self.path.replace('/', '\\')
- self.lpath = lpath.replace('\\', '/')
- else:
- self.lpath = lpath
+ Book_.__init__(self, prefix, lpath)
self.title = title
if not authors:
@@ -59,57 +37,7 @@ class Book(Metadata):
if other:
self.smart_update(other)
- def __eq__(self, other):
- return self.path == getattr(other, 'path', None)
-
- @dynamic_property
- def db_id(self):
- doc = '''The database id in the application database that this file corresponds to'''
- def fget(self):
- match = re.search(r'_(\d+)$', self.lpath.rpartition('.')[0])
- if match:
- return int(match.group(1))
- return None
- return property(fget=fget, doc=doc)
-
- @dynamic_property
- def title_sorter(self):
- doc = '''String to sort the title. If absent, title is returned'''
- def fget(self):
- return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', self.title).rstrip()
- return property(doc=doc, fget=fget)
-
- @dynamic_property
- def thumbnail(self):
- return None
-
- def smart_update(self, other, replace_metadata=False):
- '''
- Merge the information in C{other} into self. In case of conflicts, the information
- in C{other} takes precedence, unless the information in C{other} is NULL.
- '''
-
- Metadata.smart_update(self, other)
-
- for attr in self.BOOK_ATTRS:
- if hasattr(other, attr):
- val = getattr(other, attr, None)
- setattr(self, attr, val)
-
- def to_json(self):
- json = {}
- for attr in self.JSON_ATTRS:
- val = getattr(self, attr)
- if isbytestring(val):
- enc = filesystem_encoding if attr == 'lpath' else preferred_encoding
- val = val.decode(enc, 'replace')
- elif isinstance(val, (list, tuple)):
- val = [x.decode(preferred_encoding, 'replace') if
- isbytestring(x) else x for x in val]
- json[attr] = val
- return json
-
class ImageWrapper(object):
def __init__(self, image_path):
- self.image_path = image_path
+ self.image_path = image_path
diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py
index 35fceb80f7..5f939a4498 100644
--- a/src/calibre/devices/kobo/driver.py
+++ b/src/calibre/devices/kobo/driver.py
@@ -132,7 +132,7 @@ class KOBO(USBMS):
changed = False
for i, row in enumerate(cursor):
- # self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
+ # self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
path = self.path_from_contentid(row[3], row[5], oncard)
mime = mime_type_ext(path_to_ext(row[3]))
From 47fedcee36db9d7f9f5ab39460a6eafcbe21df20 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Fri, 27 Aug 2010 10:26:36 +0100
Subject: [PATCH 003/289] Bug fixes: 1) Only reset to initial values when None
is assigned. Using 'if not var' is true for empty lists 2) Take unused values
out of the to_html and unicode functions 3) add 'language' as a valid
metadata field
---
src/calibre/ebooks/metadata/book/__init__.py | 68 +++++++-------------
src/calibre/ebooks/metadata/book/base.py | 31 +++++----
2 files changed, 42 insertions(+), 57 deletions(-)
diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py
index 9de7ca1c6b..b1a322b143 100644
--- a/src/calibre/ebooks/metadata/book/__init__.py
+++ b/src/calibre/ebooks/metadata/book/__init__.py
@@ -11,50 +11,39 @@ an empty list/dictionary for complex types and (None, None) for cover_data
'''
SOCIAL_METADATA_FIELDS = frozenset([
- 'tags', # Ordered list
- # A floating point number between 0 and 10
- 'rating',
- # A simple HTML enabled string
- 'comments',
- # A simple string
- 'series',
- # A floating point number
- 'series_index',
+ 'tags', # Ordered list
+ 'rating', # A floating point number between 0 and 10
+ 'comments', # A simple HTML enabled string
+ 'series', # A simple string
+ 'series_index', # A floating point number
# Of the form { scheme1:value1, scheme2:value2}
# For example: {'isbn':'123456789', 'doi':'xxxx', ... }
'classifiers',
- 'isbn', # Pseudo field for convenience, should get/set isbn classifier
- # TODO: not sure what this is, but it is used by OPF
- 'category',
-
+ 'isbn', # Pseudo field for convenience, should get/set isbn classifier
+ 'category', # TODO: not sure what this is, but it is used by OPF
])
PUBLICATION_METADATA_FIELDS = frozenset([
- # title must never be None. Should be _('Unknown')
- 'title',
+ 'title', # title must never be None. Should be _('Unknown')
# Pseudo field that can be set, but if not set is auto generated
# from title and languages
'title_sort',
- # Ordered list of authors. Must never be None, can be [_('Unknown')]
- 'authors',
- # Map of sort strings for each author
- 'author_sort_map',
+ 'authors', # Ordered list. Must never be None, can be [_('Unknown')]
+ 'author_sort_map', # Map of sort strings for each author
# Pseudo field that can be set, but if not set is auto generated
# from authors and languages
'author_sort',
'book_producer',
- # Dates and times must be timezone aware
- 'timestamp',
+ 'timestamp', # Dates and times must be timezone aware
'pubdate',
'rights',
# So far only known publication type is periodical:calibre
# If None, means book
'publication_type',
- # A UUID usually of type 4
- 'uuid',
- 'languages', # ordered list
- # Simple string, no special semantics
- 'publisher',
+ 'uuid', # A UUID usually of type 4
+ 'language', # the primary language of this book
+ 'languages', # ordered list
+ 'publisher', # Simple string, no special semantics
# Absolute path to image file encoded in filesystem_encoding
'cover',
# Of the form (format, data) where format is, for e.g. 'jpeg', 'png', 'gif'...
@@ -77,22 +66,18 @@ USER_METADATA_FIELDS = frozenset([
])
DEVICE_METADATA_FIELDS = frozenset([
- # Ordered list of strings
- 'device_collections',
- 'lpath', # Unicode, / separated
- # In bytes
- 'size',
- # Mimetype of the book file being represented
- 'mime',
+ 'device_collections', # Ordered list of strings
+ 'lpath', # Unicode, / separated
+ 'size', # In bytes
+ 'mime', # Mimetype of the book file being represented
+
])
CALIBRE_METADATA_FIELDS = frozenset([
- # An application id
- # Semantics to be defined. Is it a db key? a db name + key? A uuid?
- # (It is currently set to the db_id.)
- 'application_id',
- # the calibre primary key of the item. May want to remove this once Sony's no longer use it
- 'db_id',
+ 'application_id', # An application id, currently set to the db_id.
+ # the calibre primary key of the item.
+ 'db_id', # the calibre primary key of the item.
+ # TODO: May want to remove once Sony's no longer use it
]
)
@@ -124,7 +109,4 @@ SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
CALIBRE_METADATA_FIELDS).union(
DEVICE_METADATA_FIELDS) - \
frozenset(['device_collections'])
- # I don't think we need device_collections
-
-# Serialization of covers/thumbnails will have to be handled carefully, maybe
-# as an option to the serializer class
+ # device_collections is rebuilt when needed
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 697de8d890..e352aecbf8 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -24,6 +24,7 @@ NULL_VALUES = {
'author_sort_map': {},
'authors' : [_('Unknown')],
'title' : _('Unknown'),
+ 'language' : 'und'
}
class Metadata(object):
@@ -68,14 +69,14 @@ class Metadata(object):
def __setattr__(self, field, val):
_data = object.__getattribute__(self, '_data')
if field in STANDARD_METADATA_FIELDS:
- if not val:
+ if val is None:
val = NULL_VALUES.get(field, None)
_data[field] = val
elif field in _data['user_metadata'].iterkeys():
_data['user_metadata'][field]['#value#'] = val
else:
# You are allowed to stick arbitrary attributes onto this object as
- # long as they dont conflict with global or user metadata names
+ # long as they don't conflict with global or user metadata names
# Don't abuse this privilege
self.__dict__[field] = val
@@ -294,12 +295,13 @@ class Metadata(object):
fmt('Published', isoformat(self.pubdate))
if self.rights is not None:
fmt('Rights', unicode(self.rights))
- if self.lccn:
- fmt('LCCN', unicode(self.lccn))
- if self.lcc:
- fmt('LCC', unicode(self.lcc))
- if self.ddc:
- fmt('DDC', unicode(self.ddc))
+# TODO: These are not in metadata. Should they be?
+# if self.lccn:
+# fmt('LCCN', unicode(self.lccn))
+# if self.lcc:
+# fmt('LCC', unicode(self.lcc))
+# if self.ddc:
+# fmt('DDC', unicode(self.ddc))
# CUSTFIELD: What to do about custom fields?
return u'\n'.join(ans)
@@ -311,12 +313,13 @@ class Metadata(object):
ans += [(_('Producer'), unicode(self.book_producer))]
ans += [(_('Comments'), unicode(self.comments))]
ans += [('ISBN', unicode(self.isbn))]
- if self.lccn:
- ans += [('LCCN', unicode(self.lccn))]
- if self.lcc:
- ans += [('LCC', unicode(self.lcc))]
- if self.ddc:
- ans += [('DDC', unicode(self.ddc))]
+# TODO: These are not in metadata. Should they be?
+# if self.lccn:
+# ans += [('LCCN', unicode(self.lccn))]
+# if self.lcc:
+# ans += [('LCC', unicode(self.lcc))]
+# if self.ddc:
+# ans += [('DDC', unicode(self.ddc))]
ans += [(_('Tags'), u', '.join([unicode(t) for t in self.tags]))]
if self.series:
ans += [(_('Series'), unicode(self.series)+ ' #%s'%self.format_series_index())]
From b4b0cb483df4a647de50145abf88a7b26ecd79c9 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sat, 28 Aug 2010 13:34:49 +0100
Subject: [PATCH 004/289] Changes to respond to Kovid's mail, and some
cleanups.
---
src/calibre/ebooks/metadata/book/__init__.py | 4 +-
src/calibre/ebooks/metadata/book/base.py | 132 +++++++++++-------
.../ebooks/metadata/book/json_codec.py | 2 +-
src/calibre/library/database2.py | 15 +-
src/calibre/library/save_to_disk.py | 13 +-
5 files changed, 100 insertions(+), 66 deletions(-)
diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py
index b1a322b143..fbcca79aba 100644
--- a/src/calibre/ebooks/metadata/book/__init__.py
+++ b/src/calibre/ebooks/metadata/book/__init__.py
@@ -101,7 +101,9 @@ COPYABLE_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
BOOK_STRUCTURE_FIELDS).union(
DEVICE_METADATA_FIELDS).union(
CALIBRE_METADATA_FIELDS) - \
- frozenset(['title', 'authors', 'comments', 'cover_data'])
+ frozenset(['title', 'title_sort', 'authors',
+ 'author_sort', 'author_sort_map' 'comments',
+ 'cover_data', 'tags', 'language'])
SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
USER_METADATA_FIELDS).union(
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index e352aecbf8..a81ce46c34 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -66,7 +66,7 @@ class Metadata(object):
raise AttributeError(
'Metadata object has no attribute named: '+ repr(field))
- def __setattr__(self, field, val):
+ def __setattr__(self, field, val, extra=None):
_data = object.__getattribute__(self, '_data')
if field in STANDARD_METADATA_FIELDS:
if val is None:
@@ -74,17 +74,23 @@ class Metadata(object):
_data[field] = val
elif field in _data['user_metadata'].iterkeys():
_data['user_metadata'][field]['#value#'] = val
+ _data['user_metadata'][field]['#extra#'] = extra
else:
# You are allowed to stick arbitrary attributes onto this object as
# long as they don't conflict with global or user metadata names
# Don't abuse this privilege
self.__dict__[field] = val
- def get(self, field):
+ def get(self, field, default=None):
+ if default is not None:
+ try:
+ return self.__getattribute__(field)
+ except AttributeError:
+ return default
return self.__getattribute__(field)
- def set(self, field, val):
- self.__setattr__(field, val)
+ def set(self, field, val, extra=None):
+ self.__setattr__(field, val, extra)
@property
def user_metadata_keys(self):
@@ -92,25 +98,25 @@ class Metadata(object):
_data = object.__getattribute__(self, '_data')
return frozenset(_data['user_metadata'].iterkeys())
- @property
- def all_user_metadata(self):
+ def get_all_user_metadata(self, make_copy):
'''
return a dict containing all the custom field metadata associated with
- the book. Return a deep copy, just in case the user wants to change
- values in the dict (json does).
+ the book.
'''
_data = object.__getattribute__(self, '_data')
- _data = _data['user_metadata']
+ user_metadata = _data['user_metadata']
+ if not make_copy:
+ return user_metadata
res = {}
- for k in _data:
- res[k] = copy.deepcopy(_data[k])
+ for k in user_metadata:
+ res[k] = copy.deepcopy(user_metadata[k])
return res
def get_user_metadata(self, field):
'''
return field metadata from the object if it is there. Otherwise return
- None. field is the key name, not the label. Return a shallow copy,
- just in case the user wants to change values in the dict (json does).
+ None. field is the key name, not the label. Return a copy, just in case
+ the user wants to change values in the dict (json does).
'''
_data = object.__getattribute__(self, '_data')
_data = _data['user_metadata']
@@ -118,6 +124,14 @@ class Metadata(object):
return copy.deepcopy(_data[field])
return None
+ @classmethod
+ def get_user_metadata_value(user_mi):
+ return user_mi['#value#']
+
+ @classmethod
+ def get_user_metadata_extra(user_mi):
+ return user_mi['#extra#']
+
def set_all_user_metadata(self, metadata):
'''
store custom field metadata into the object. Field is the key name
@@ -139,21 +153,30 @@ class Metadata(object):
traceback.print_stack()
metadata = copy.deepcopy(metadata)
if '#value#' not in metadata:
- metadata['#value#'] = None
+ if metadata['datatype'] == 'text' and metadata['is_multiple']:
+ metadata['#value#'] = []
+ else:
+ metadata['#value#'] = None
_data = object.__getattribute__(self, '_data')
_data['user_metadata'][field] = metadata
- @property
- def all_attributes(self):
+ def get_all_non_none_attributes(self):
+ '''
+ Return a dictionary containing all non-None metadata fields, including
+ the custom ones.
+ '''
result = {}
_data = object.__getattribute__(self, '_data')
for attr in STANDARD_METADATA_FIELDS:
v = _data.get(attr, None)
if v is not None:
result[attr] = v
- for attr in self.user_metadata_keys:
- if self.get(attr) is not None:
- result[attr] = self.get(attr)
+ for attr in _data['user_metadata'].iterkeys():
+ v = _data['user_metadata'][attr]['#value#']
+ if v is not None:
+ result[attr] = v
+ if _data['user_metadata'][attr]['datatype'] == 'series':
+ result[attr+'_index'] = _data['user_metadata'][attr]['#extra#']
return result
# Old Metadata API {{{
@@ -184,45 +207,49 @@ class Metadata(object):
'''
if other.title and other.title != _('Unknown'):
self.title = other.title
+ if hasattr(other, 'title_sort'):
+ self.title_sort = other.title_sort
if other.authors and other.authors[0] != _('Unknown'):
self.authors = other.authors
+ if hasattr(other, 'author_sort_map'):
+ self.author_sort_map = other.author_sort_map
+ if hasattr(other, 'author_sort'):
+ self.author_sort = other.author_sort
- for attr in COPYABLE_METADATA_FIELDS:
- if replace_metadata:
+ if replace_metadata:
+ for attr in COPYABLE_METADATA_FIELDS:
setattr(self, attr, getattr(other, attr, 1.0 if \
attr == 'series_index' else None))
- elif hasattr(other, attr):
- val = getattr(other, attr)
- if val is not None:
- setattr(self, attr, copy.deepcopy(val))
-
- if replace_metadata:
self.tags = other.tags
- elif other.tags:
- self.tags += other.tags
- self.tags = list(set(self.tags))
-
- if getattr(other, 'author_sort_map', None):
- self.author_sort_map.update(other.author_sort_map)
-
- if getattr(other, 'cover_data', False):
- other_cover = other.cover_data[-1]
- self_cover = self.cover_data[-1] if self.cover_data else ''
- if not self_cover: self_cover = ''
- if not other_cover: other_cover = ''
- if len(other_cover) > len(self_cover):
- self.cover_data = other.cover_data
-
- if getattr(other, 'user_metadata_keys', None):
- for x in other.user_metadata_keys:
- meta = other.get_user_metadata(x)
- if meta is not None or replace_metadata:
- self.set_user_metadata(x, meta) # get... did the deepcopy
-
- if replace_metadata:
+ self.cover_data = getattr(other, 'cover_data', '')
+ self.set_all_user_metadata(other.get_all_user_metadata(make_copy=True))
self.comments = getattr(other, 'comments', '')
+ self.language = getattr(other, 'language', None)
else:
+ for attr in COPYABLE_METADATA_FIELDS:
+ if hasattr(other, attr):
+ val = getattr(other, attr)
+ if val is not None:
+ setattr(self, attr, copy.deepcopy(val))
+
+ if other.tags:
+ self.tags += list(set(self.tags + other.tags))
+
+ if getattr(other, 'cover_data', False):
+ other_cover = other.cover_data[-1]
+ self_cover = self.cover_data[-1] if self.cover_data else ''
+ if not self_cover: self_cover = ''
+ if not other_cover: other_cover = ''
+ if len(other_cover) > len(self_cover):
+ self.cover_data = other.cover_data
+
+ if getattr(other, 'user_metadata_keys', None):
+ for x in other.user_metadata_keys:
+ meta = other.get_user_metadata(x)
+ if meta is not None:
+ self.set_user_metadata(x, meta) # get... did the deepcopy
+
my_comments = getattr(self, 'comments', '')
other_comments = getattr(other, 'comments', '')
if not my_comments:
@@ -232,10 +259,9 @@ class Metadata(object):
if len(other_comments.strip()) > len(my_comments.strip()):
self.comments = other_comments
- other_lang = getattr(other, 'language', None)
- if other_lang and other_lang.lower() != 'und':
- self.language = other_lang
-
+ other_lang = getattr(other, 'language', None)
+ if other_lang and other_lang.lower() != 'und':
+ self.language = other_lang
def format_series_index(self):
from calibre.ebooks.metadata import fmt_sidx
diff --git a/src/calibre/ebooks/metadata/book/json_codec.py b/src/calibre/ebooks/metadata/book/json_codec.py
index 5e13650f0e..7a80e16854 100644
--- a/src/calibre/ebooks/metadata/book/json_codec.py
+++ b/src/calibre/ebooks/metadata/book/json_codec.py
@@ -70,7 +70,7 @@ class JsonCodec(object):
def encode_metadata_attr(self, book, key):
if key == 'user_metadata':
- meta = book.all_user_metadata
+ meta = book.get_all_user_metadata(make_copy=True)
for k in meta:
if meta[k]['datatype'] == 'datetime':
meta[k]['#value#'] = datetime_to_string(meta[k]['#value#'])
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 9f21fe0eda..935776f838 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -22,6 +22,7 @@ from calibre.library.sqlite import connect, IntegrityError, DBThread
from calibre.library.prefs import DBPrefs
from calibre.ebooks.metadata import string_to_authors, authors_to_string, \
MetaInformation
+from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
from calibre.ptempfile import PersistentTemporaryFile
@@ -537,7 +538,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
for key,meta in self.field_metadata.iteritems():
if meta['is_custom']:
mi.set_user_metadata(key, meta)
- mi.set(key, self.get_custom(idx, label=meta['label'], index_is_id=index_is_id))
+ mi.set(key, val=self.get_custom(idx, label=meta['label'],
+ index_is_id=index_is_id),
+ extra=self.get_custom_extra(idx, label=meta['label'],
+ index_is_id=index_is_id))
if get_cover:
mi.cover = self.cover(id, index_is_id=True, as_path=True)
return mi
@@ -1038,6 +1042,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if getattr(mi, 'timestamp', None) is not None:
doit(self.set_timestamp, id, mi.timestamp, notify=False)
self.set_path(id, True)
+
+ user_mi = mi.get_all_user_metadata(make_copy=False)
+ for key in user_mi.iterkeys():
+ if key in self.field_metadata and \
+ user_mi[key]['datatype'] == self.field_metadata[key]['datatype']:
+ doit(self.set_custom, id,
+ val=Metadata.get_user_metadata_value(user_mi[key]),
+ extra=Metadata.get_user_metadata_extra(user_mi[key]),
+ label=user_mi[key]['label'])
self.notify('metadata', [id])
# Given a book, return the list of author sort strings for the book's authors
diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py
index 258ea7ba9e..7a3515305b 100644
--- a/src/calibre/library/save_to_disk.py
+++ b/src/calibre/library/save_to_disk.py
@@ -115,7 +115,7 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
library_order = tweaks['save_template_title_series_sorting'] == 'library_order'
tsfmt = title_sort if library_order else lambda x: x
format_args = dict(**FORMAT_ARGS)
- format_args.update(mi.all_attributes)
+ format_args.update(mi.get_all_non_none_attributes())
if mi.title:
format_args['title'] = tsfmt(mi.title)
if mi.authors:
@@ -131,6 +131,8 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
format_args['series_index'] = mi.format_series_index()
else:
template = re.sub(r'\{series_index[^}]*?\}', '', template)
+ ## TODO: format custom values. Check all the datatypes.
+
if mi.rating is not None:
format_args['rating'] = mi.format_rating()
if hasattr(mi.timestamp, 'timetuple'):
@@ -139,15 +141,6 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
format_args['pubdate'] = strftime(timefmt, mi.pubdate.timetuple())
format_args['id'] = str(id)
- # These are not necessary any more. The values are set by
- # 'format_args.update' above, and there is no special formatting
-# if mi.author_sort:
-# format_args['author_sort'] = mi.author_sort
-# if mi.isbn:
-# format_args['isbn'] = mi.isbn
-# if mi.publisher:
-# format_args['publisher'] = mi.publisher
-
components = [x.strip() for x in template.split('/') if x.strip()]
components = [safe_format(x, format_args) for x in components]
components = [sanitize_func(x) for x in components if x]
From 65f8767057afa440c143353c52e038a092da6783 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 29 Aug 2010 11:18:09 +0100
Subject: [PATCH 005/289] New metadata: 1) remove 'category' from standard
metadata fields 2) make isbn a classifier. Add ability to add more
classifiers 3) fixup TODO: to make them easier to find.
---
src/calibre/devices/usbms/books.py | 1 +
src/calibre/ebooks/metadata/book/__init__.py | 12 ++++++---
src/calibre/ebooks/metadata/book/base.py | 25 ++++++-------------
src/calibre/ebooks/metadata/opf2.py | 2 +-
.../gui2/dialogs/config/save_template.py | 4 +--
src/calibre/library/save_to_disk.py | 2 +-
6 files changed, 21 insertions(+), 25 deletions(-)
diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index e3c405ee4e..0efa507e09 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -135,6 +135,7 @@ class CollectionsBookList(BookList):
elif isinstance(val, unicode):
val = [val]
for category in val:
+ # TODO: NEWMETA: format the custom fields
if attr == 'tags' and len(category) > 1 and \
category[0] == '[' and category[-1] == ']':
continue
diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py
index fbcca79aba..47eb616394 100644
--- a/src/calibre/ebooks/metadata/book/__init__.py
+++ b/src/calibre/ebooks/metadata/book/__init__.py
@@ -19,8 +19,14 @@ SOCIAL_METADATA_FIELDS = frozenset([
# Of the form { scheme1:value1, scheme2:value2}
# For example: {'isbn':'123456789', 'doi':'xxxx', ... }
'classifiers',
- 'isbn', # Pseudo field for convenience, should get/set isbn classifier
- 'category', # TODO: not sure what this is, but it is used by OPF
+])
+
+'''
+The list of names that convert to classifiers when in get and set.
+'''
+
+TOP_LEVEL_CLASSIFIERS = frozenset([
+ 'isbn',
])
PUBLICATION_METADATA_FIELDS = frozenset([
@@ -77,7 +83,7 @@ CALIBRE_METADATA_FIELDS = frozenset([
'application_id', # An application id, currently set to the db_id.
# the calibre primary key of the item.
'db_id', # the calibre primary key of the item.
- # TODO: May want to remove once Sony's no longer use it
+ # TODO: NEWMETA: May want to remove once Sony's no longer use it
]
)
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index a81ce46c34..6d89049bfb 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -11,6 +11,7 @@ import traceback
from calibre import prints
from calibre.ebooks.metadata.book import COPYABLE_METADATA_FIELDS
from calibre.ebooks.metadata.book import STANDARD_METADATA_FIELDS
+from calibre.ebooks.metadata.book import TOP_LEVEL_CLASSIFIERS
from calibre.utils.date import isoformat
@@ -55,6 +56,8 @@ class Metadata(object):
def __getattribute__(self, field):
_data = object.__getattribute__(self, '_data')
+ if field in TOP_LEVEL_CLASSIFIERS:
+ return _data.get('classifiers').get(field, None)
if field in STANDARD_METADATA_FIELDS:
return _data.get(field, None)
try:
@@ -68,7 +71,9 @@ class Metadata(object):
def __setattr__(self, field, val, extra=None):
_data = object.__getattribute__(self, '_data')
- if field in STANDARD_METADATA_FIELDS:
+ if field in TOP_LEVEL_CLASSIFIERS:
+ _data['classifiers'].update({field: val})
+ elif field in STANDARD_METADATA_FIELDS:
if val is None:
val = NULL_VALUES.get(field, None)
_data[field] = val
@@ -301,8 +306,6 @@ class Metadata(object):
fmt('Publisher', self.publisher)
if getattr(self, 'book_producer', False):
fmt('Book Producer', self.book_producer)
- if self.category:
- fmt('Category', self.category)
if self.comments:
fmt('Comments', self.comments)
if self.isbn:
@@ -321,14 +324,7 @@ class Metadata(object):
fmt('Published', isoformat(self.pubdate))
if self.rights is not None:
fmt('Rights', unicode(self.rights))
-# TODO: These are not in metadata. Should they be?
-# if self.lccn:
-# fmt('LCCN', unicode(self.lccn))
-# if self.lcc:
-# fmt('LCC', unicode(self.lcc))
-# if self.ddc:
-# fmt('DDC', unicode(self.ddc))
- # CUSTFIELD: What to do about custom fields?
+ # TODO: NEWMETA: What to do about custom fields?
return u'\n'.join(ans)
def to_html(self):
@@ -339,13 +335,6 @@ class Metadata(object):
ans += [(_('Producer'), unicode(self.book_producer))]
ans += [(_('Comments'), unicode(self.comments))]
ans += [('ISBN', unicode(self.isbn))]
-# TODO: These are not in metadata. Should they be?
-# if self.lccn:
-# ans += [('LCCN', unicode(self.lccn))]
-# if self.lcc:
-# ans += [('LCC', unicode(self.lcc))]
-# if self.ddc:
-# ans += [('DDC', unicode(self.ddc))]
ans += [(_('Tags'), u', '.join([unicode(t) for t in self.tags]))]
if self.series:
ans += [(_('Series'), unicode(self.series)+ ' #%s'%self.format_series_index())]
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index 0ab6d3bbc0..54d97fc157 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -1188,7 +1188,7 @@ def metadata_to_opf(mi, as_string=True):
factory(DC('contributor'), mi.book_producer, __appname__, 'bkp')
if hasattr(mi.pubdate, 'isoformat'):
factory(DC('date'), isoformat(mi.pubdate))
- if mi.category:
+ if hasattr(mi, 'category') and mi.category:
factory(DC('type'), mi.category)
if mi.comments:
factory(DC('description'), mi.comments)
diff --git a/src/calibre/gui2/dialogs/config/save_template.py b/src/calibre/gui2/dialogs/config/save_template.py
index 2157d5b3bf..7e49e86d29 100644
--- a/src/calibre/gui2/dialogs/config/save_template.py
+++ b/src/calibre/gui2/dialogs/config/save_template.py
@@ -34,8 +34,8 @@ class SaveTemplate(QWidget, Ui_Form):
self.option_name = name
def validate(self):
- # TODO: I haven't figured out how to get the custom columns into here,
- # so for the moment make all templates valid.
+ # TODO: NEWMETA: I haven't figured out how to get the custom columns
+ # into here, so for the moment make all templates valid.
return True
# tmpl = preprocess_template(self.opt_template.text())
# fa = {}
diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py
index 7a3515305b..963afd4085 100644
--- a/src/calibre/library/save_to_disk.py
+++ b/src/calibre/library/save_to_disk.py
@@ -131,7 +131,7 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
format_args['series_index'] = mi.format_series_index()
else:
template = re.sub(r'\{series_index[^}]*?\}', '', template)
- ## TODO: format custom values. Check all the datatypes.
+ ## TODO: NEWMETA: format custom values. Check all the datatypes.
if mi.rating is not None:
format_args['rating'] = mi.format_rating()
From 6eb7383e82b564a146cd9e719d6424ec8f79c355 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 30 Aug 2010 12:16:37 +0100
Subject: [PATCH 006/289] Remove unused (and unworking) copy method
---
src/calibre/ebooks/metadata/book/base.py | 13 +------------
1 file changed, 1 insertion(+), 12 deletions(-)
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 6d89049bfb..dcb31c3ecc 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -185,17 +185,6 @@ class Metadata(object):
return result
# Old Metadata API {{{
- @staticmethod
- def copy(mi):
- ans = Metadata(mi.title, mi.authors)
- for attr in STANDARD_METADATA_FIELDS:
- if hasattr(mi, attr):
- setattr(ans, attr, copy.deepcopy(getattr(mi, attr)))
- for x in mi.user_metadata_keys:
- meta = mi.get_user_metadata(x)
- if meta is not None:
- ans.set_user_metadata(x, meta) # get... did the deep copy
-
def print_all_attributes(self):
for x in STANDARD_METADATA_FIELDS:
prints('%s:'%x, getattr(self, x, 'None'))
@@ -347,7 +336,7 @@ class Metadata(object):
ans += [(_('Rights'), unicode(self.rights))]
for i, x in enumerate(ans):
ans[i] = u'%s | %s |
'%x
- # CUSTFIELD: What to do about custom fields
+ # TODO: NEWMETA: What to do about custom fields
return u''%u'\n'.join(ans)
def __str__(self):
From 11f7bd06a82a0b1d03afffd374344593dfb0c5dc Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 30 Aug 2010 17:47:25 +0100
Subject: [PATCH 007/289] Format custom fields in save_to_disk.
---
src/calibre/library/save_to_disk.py | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py
index 963afd4085..2bc71cde9c 100644
--- a/src/calibre/library/save_to_disk.py
+++ b/src/calibre/library/save_to_disk.py
@@ -14,6 +14,7 @@ from calibre.utils.filenames import shorten_components_to, supports_long_names,
from calibre.ebooks.metadata.opf2 import metadata_to_opf
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
@@ -131,8 +132,6 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
format_args['series_index'] = mi.format_series_index()
else:
template = re.sub(r'\{series_index[^}]*?\}', '', template)
- ## TODO: NEWMETA: format custom values. Check all the datatypes.
-
if mi.rating is not None:
format_args['rating'] = mi.format_rating()
if hasattr(mi.timestamp, 'timetuple'):
@@ -140,6 +139,19 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
if hasattr(mi.pubdate, 'timetuple'):
format_args['pubdate'] = strftime(timefmt, mi.pubdate.timetuple())
format_args['id'] = str(id)
+ # Now format the custom fields
+ custom_metadata = mi.get_all_user_metadata(make_copy=False)
+ for key in custom_metadata:
+ if key in format_args:
+ ## TODO: NEWMETA: should ratings be divided by 2? The standard rating isn't...
+ if custom_metadata[key]['datatype'] == 'series':
+ format_args[key] = tsfmt(format_args[key])
+ if key+'_index' in format_args:
+ format_args[key+'_index'] = fmt_sidx(format_args[key+'_index'])
+ elif custom_metadata[key]['datatype'] == 'datetime':
+ format_args[key] = strftime(timefmt, format_args[key].timetuple())
+ elif custom_metadata[key]['datatype'] == 'bool':
+ format_args[key] = _('yes') if format_args[key] else _('no')
components = [x.strip() for x in template.split('/') if x.strip()]
components = [safe_format(x, format_args) for x in components]
From 16e5b2f3b0204efefe1c73531c4ce98376342fc7 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 1 Sep 2010 16:44:12 +0100
Subject: [PATCH 008/289] Add code for Sony collections
---
resources/default_tweaks.py | 32 ++++++-
src/calibre/devices/usbms/books.py | 95 ++++++++++++++------
src/calibre/ebooks/metadata/book/__init__.py | 2 +-
src/calibre/ebooks/metadata/book/base.py | 17 ++--
4 files changed, 111 insertions(+), 35 deletions(-)
diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py
index e03b0680be..e68096ecd5 100644
--- a/resources/default_tweaks.py
+++ b/resources/default_tweaks.py
@@ -90,4 +90,34 @@ save_template_title_series_sorting = 'library_order'
# Examples:
# auto_connect_to_folder = 'C:\\Users\\someone\\Desktop\\testlib'
# auto_connect_to_folder = '/home/dropbox/My Dropbox/someone/library'
-auto_connect_to_folder = ''
\ No newline at end of file
+auto_connect_to_folder = ''
+
+# Specify renaming rules for sony collections. Collections on Sonys are named
+# depending upon whether the field is standard or custom. A collection derived
+# from a standard field is named for the value in that field. For example, if
+# the standard 'series' column contains the name 'Darkover', then the series
+# will be named 'Darkover'. A collection derived from a custom field will have
+# the name of the field added to the value. For example, if a custom series
+# column named 'My Series' contains the name 'Darkover', then the collection
+# will be named 'Darkover (My Series)'. If two books have fields that generate
+# the same collection name, then both books will be in that collection. This
+# tweak lets you specify for a standard or custom field the value to be put
+# inside the parentheses. You can use it to add a parenthetical description to a
+# standard field, for example 'Foo (Tag)' instead of the 'Foo'. You can also use
+# it to force multiple fields to end up in the same collection. For example, you
+# could force the values in 'series', '#my_series_1', and '#my_series_2' to
+# appear in collections named 'some_value (Series)', thereby merging all of the
+# fields into one set of collections. The syntax of this tweak is
+# {'field_lookup_name':'name_to_use', 'lookup_name':'name', ...}
+# Example 1: I want three series columns to be merged into one set of
+# collections. If the column lookup names are 'series', '#series_1' and
+# '#series_2', and if I want nothing in the parenthesis, then the value to use
+# in the tweak value would be:
+# sony_collection_renaming_rules={'series':'', '#series_1':'', '#series_2':''}
+# Example 2: I want the word '(Series)' to appear on collections made from
+# series, and the word '(Tag)' to appear on collections made from tags. Use:
+# sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'}
+# Example 3: I want 'series' and '#myseries' to be merged, and for the
+# collection name to have '(Series)' appended. The renaming rule is:
+# sony_collection_renaming_rules={'series':'Series', '#myseries':'Series'}
+sony_collection_renaming_rules={}
\ No newline at end of file
diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index 0efa507e09..3e13527bd0 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -9,9 +9,10 @@ import os, re, time, sys
from calibre.ebooks.metadata.book.base import Metadata
from calibre.devices.mime import mime_type_ext
from calibre.devices.interface import BookList as _BookList
-from calibre.constants import filesystem_encoding, preferred_encoding
+from calibre.constants import preferred_encoding
from calibre import isbytestring
-from calibre.utils.config import prefs
+from calibre.utils.config import prefs, tweaks
+from calibre.utils.date import format_date
class Book(Metadata):
def __init__(self, prefix, lpath, size=None, other=None):
@@ -94,11 +95,38 @@ class CollectionsBookList(BookList):
def supports_collections(self):
return True
+ def compute_category_name(self, attr, category, cust_field_meta):
+ renames = tweaks['sony_collection_renaming_rules']
+ attr_name = renames.get(attr, None)
+ if attr_name is None:
+ if attr in cust_field_meta:
+ attr_name = '(%s)'%cust_field_meta[attr]['name']
+ else:
+ attr_name = ''
+ elif attr_name != '':
+ attr_name = '(%s)'%attr_name
+
+ if attr not in cust_field_meta:
+ cat_name = '%s %s'%(category, attr_name)
+ else:
+ fm = cust_field_meta[attr]
+ if fm['datatype'] == 'bool':
+ if category:
+ cat_name = '%s %s'%(_('Yes'), attr_name)
+ else:
+ cat_name = '%s %s'%(_('No'), attr_name)
+ elif fm['datatype'] == 'datetime':
+ cat_name = '%s %s'%(format_date(category,
+ fm['display'].get('date_format','dd MMM yyyy')), attr_name)
+ else:
+ cat_name = '%s %s'%(category, attr_name)
+ return cat_name.strip()
+
def get_collections(self, collection_attributes):
from calibre.devices.usbms.driver import debug_print
debug_print('Starting get_collections:', prefs['manage_device_metadata'])
+ debug_print('Renaming rules:', tweaks['sony_collection_renaming_rules'])
collections = {}
- series_categories = set([])
# This map of sets is used to avoid linear searches when testing for
# book equality
collections_lpaths = {}
@@ -124,42 +152,55 @@ class CollectionsBookList(BookList):
# For existing books, modify the collections only if the user
# specified 'on_connect'
attrs = collection_attributes
+ meta_vals = book.get_all_non_none_attributes()
+ cust_field_meta = book.get_all_user_metadata(make_copy=False)
for attr in attrs:
attr = attr.strip()
- val = getattr(book, attr, None)
+ val = meta_vals.get(attr, None)
if not val: continue
if isbytestring(val):
val = val.decode(preferred_encoding, 'replace')
if isinstance(val, (list, tuple)):
val = list(val)
- elif isinstance(val, unicode):
+ else:
val = [val]
for category in val:
- # TODO: NEWMETA: format the custom fields
- if attr == 'tags' and len(category) > 1 and \
- category[0] == '[' and category[-1] == ']':
+ is_series = False
+ if attr in cust_field_meta: # is a custom field
+ fm = cust_field_meta[attr]
+ if fm['datatype'] == 'text' and len(category) > 1 and \
+ category[0] == '[' and category[-1] == ']':
+ continue
+ if fm['datatype'] == 'series':
+ is_series = True
+ else: # is a standard field
+ if attr == 'tags' and len(category) > 1 and \
+ category[0] == '[' and category[-1] == ']':
+ continue
+ if attr == 'series' or \
+ ('series' in collection_attributes and
+ meta_vals.get('series', None) == category):
+ is_series = True
+ cat_name = self.compute_category_name(attr, category,
+ cust_field_meta)
+ if cat_name not in collections:
+ collections[cat_name] = []
+ collections_lpaths[cat_name] = set()
+ if lpath in collections_lpaths[cat_name]:
continue
- if category not in collections:
- collections[category] = []
- collections_lpaths[category] = set()
- if lpath not in collections_lpaths[category]:
- collections_lpaths[category].add(lpath)
- collections[category].append(book)
- if attr == 'series' or \
- ('series' in collection_attributes and
- getattr(book, 'series', None) == category):
- series_categories.add(category)
+ collections_lpaths[cat_name].add(lpath)
+ if is_series:
+ collections[cat_name].append(
+ (book, meta_vals.get(attr+'_index', sys.maxint)))
+ else:
+ collections[cat_name].append(
+ (book, meta_vals.get('title_sort', 'zzzz')))
# Sort collections
+ result = {}
for category, books in collections.items():
- def tgetter(x):
- return getattr(x, 'title_sort', 'zzzz')
- books.sort(cmp=lambda x,y:cmp(tgetter(x), tgetter(y)))
- if category in series_categories:
- # Ensures books are sub sorted by title
- def getter(x):
- return getattr(x, 'series_index', sys.maxint)
- books.sort(cmp=lambda x,y:cmp(getter(x), getter(y)))
- return collections
+ books.sort(cmp=lambda x,y:cmp(x[1], y[1]))
+ result[category] = [x[0] for x in books]
+ return result
def rebuild_collections(self, booklist, oncard):
'''
diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py
index 47eb616394..ca7f4f7074 100644
--- a/src/calibre/ebooks/metadata/book/__init__.py
+++ b/src/calibre/ebooks/metadata/book/__init__.py
@@ -109,7 +109,7 @@ COPYABLE_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
CALIBRE_METADATA_FIELDS) - \
frozenset(['title', 'title_sort', 'authors',
'author_sort', 'author_sort_map' 'comments',
- 'cover_data', 'tags', 'language'])
+ 'cover_data', 'tags', 'language', 'lpath'])
SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
USER_METADATA_FIELDS).union(
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index dcb31c3ecc..3f5507d676 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -117,16 +117,18 @@ class Metadata(object):
res[k] = copy.deepcopy(user_metadata[k])
return res
- def get_user_metadata(self, field):
+ def get_user_metadata(self, field, make_copy):
'''
return field metadata from the object if it is there. Otherwise return
- None. field is the key name, not the label. Return a copy, just in case
- the user wants to change values in the dict (json does).
+ None. field is the key name, not the label. Return a copy if requested,
+ just in case the user wants to change values in the dict.
'''
_data = object.__getattribute__(self, '_data')
_data = _data['user_metadata']
if field in _data:
- return copy.deepcopy(_data[field])
+ if make_copy:
+ return copy.deepcopy(_data[field])
+ return _data[field]
return None
@classmethod
@@ -189,7 +191,7 @@ class Metadata(object):
for x in STANDARD_METADATA_FIELDS:
prints('%s:'%x, getattr(self, x, 'None'))
for x in self.user_metadata_keys:
- meta = self.get_user_metadata(x)
+ meta = self.get_user_metadata(x, make_copy=False)
if meta is not None:
prints(x, meta)
prints('--------------')
@@ -220,6 +222,9 @@ class Metadata(object):
self.set_all_user_metadata(other.get_all_user_metadata(make_copy=True))
self.comments = getattr(other, 'comments', '')
self.language = getattr(other, 'language', None)
+ lpath = getattr(other, 'lpath', None)
+ if lpath is not None:
+ self.lpath = lpath
else:
for attr in COPYABLE_METADATA_FIELDS:
if hasattr(other, attr):
@@ -240,7 +245,7 @@ class Metadata(object):
if getattr(other, 'user_metadata_keys', None):
for x in other.user_metadata_keys:
- meta = other.get_user_metadata(x)
+ meta = other.get_user_metadata(x, make_copy=True)
if meta is not None:
self.set_user_metadata(x, meta) # get... did the deepcopy
From 0606afc8ff6edb0dfb6042bbc6cde3f891a068e9 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Thu, 2 Sep 2010 13:19:01 +0100
Subject: [PATCH 009/289] 1) make to_html support custom fields 2) clean up the
_extra code 3) add a format_custom_field method to avoid duplicating code 4)
pass custom metadata to book_details
---
src/calibre/ebooks/metadata/book/base.py | 43 ++++++++++++++++++------
src/calibre/gui2/book_details.py | 2 ++
src/calibre/gui2/library/models.py | 6 +++-
src/calibre/library/database2.py | 4 +--
4 files changed, 42 insertions(+), 13 deletions(-)
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 3f5507d676..caaaccb3d0 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -12,7 +12,8 @@ from calibre import prints
from calibre.ebooks.metadata.book import COPYABLE_METADATA_FIELDS
from calibre.ebooks.metadata.book import STANDARD_METADATA_FIELDS
from calibre.ebooks.metadata.book import TOP_LEVEL_CLASSIFIERS
-from calibre.utils.date import isoformat
+from calibre.utils.date import isoformat, format_date
+
NULL_VALUES = {
@@ -94,6 +95,13 @@ class Metadata(object):
return default
return self.__getattribute__(field)
+ def get_extra(self, field):
+ _data = object.__getattribute__(self, '_data')
+ if field in _data['user_metadata'].iterkeys():
+ return _data['user_metadata'][field]['#extra#']
+ raise AttributeError(
+ 'Metadata object has no attribute named: '+ repr(field))
+
def set(self, field, val, extra=None):
self.__setattr__(field, val, extra)
@@ -131,14 +139,6 @@ class Metadata(object):
return _data[field]
return None
- @classmethod
- def get_user_metadata_value(user_mi):
- return user_mi['#value#']
-
- @classmethod
- def get_user_metadata_extra(user_mi):
- return user_mi['#extra#']
-
def set_all_user_metadata(self, metadata):
'''
store custom field metadata into the object. Field is the key name
@@ -284,6 +284,25 @@ class Metadata(object):
def format_rating(self):
return unicode(self.rating)
+ def format_custom_field(self, key):
+ '''
+ returns the tuple (field_name, formatted_value)
+ '''
+ cmeta = self.get_user_metadata(key, make_copy=False)
+ name = unicode(cmeta['name'])
+ res = self.get(key, None)
+ if res is not None:
+ datatype = cmeta['datatype']
+ if datatype == 'text' and cmeta['is_multiple']:
+ res = u', '.join(res)
+ elif datatype == 'series':
+ res = res + ' [%s]'%self.format_series_index(self.get_extra(key))
+ elif datatype == 'datetime':
+ res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
+ elif datatype == 'bool':
+ res = _('Yes') if res else _('No')
+ return (name, unicode(res))
+
def __unicode__(self):
from calibre.ebooks.metadata import authors_to_string
ans = []
@@ -339,9 +358,13 @@ class Metadata(object):
ans += [(_('Published'), unicode(self.pubdate.isoformat(' ')))]
if self.rights is not None:
ans += [(_('Rights'), unicode(self.rights))]
+ for key in self.user_metadata_keys:
+ val = self.get(key, None)
+ if val is not None:
+ (name, val) = self.format_custom_field(key)
+ ans += [(name, val)]
for i, x in enumerate(ans):
ans[i] = u'%s | %s |
'%x
- # TODO: NEWMETA: What to do about custom fields
return u''%u'\n'.join(ans)
def __str__(self):
diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py
index f08dd09429..4e11e0c84f 100644
--- a/src/calibre/gui2/book_details.py
+++ b/src/calibre/gui2/book_details.py
@@ -28,6 +28,8 @@ WEIGHTS[_('Tags')] = 4
def render_rows(data):
keys = data.keys()
+ # First sort by name. The WEIGHTS sort will preserve this sub-order
+ keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower()))
keys.sort(cmp=lambda x, y: cmp(WEIGHTS[x], WEIGHTS[y]))
rows = []
for key in keys:
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index fdf21ecc23..2711756856 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -323,7 +323,11 @@ class BooksModel(QAbstractTableModel): # {{{
data[_('Series')] = \
_('Book %s of %s.')%\
(sidx, prepare_string_for_xml(series))
-
+ mi = self.db.get_metadata(idx)
+ for key in mi.user_metadata_keys:
+ name, val = mi.format_custom_field(key)
+ if val is not None:
+ data[name] = val
return data
def set_cache(self, idx):
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 6457e12905..bb6d72bcff 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -1052,8 +1052,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if key in self.field_metadata and \
user_mi[key]['datatype'] == self.field_metadata[key]['datatype']:
doit(self.set_custom, id,
- val=Metadata.get_user_metadata_value(user_mi[key]),
- extra=Metadata.get_user_metadata_extra(user_mi[key]),
+ val=mi.get(key),
+ extra=mi.get_extra(key),
label=user_mi[key]['label'])
self.notify('metadata', [id])
From 7ff7da0fbb2485301142a6ba2e6c04923a6d4a59 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Fri, 3 Sep 2010 18:43:17 +0100
Subject: [PATCH 010/289] Remove relative import
---
src/calibre/ebooks/metadata/book/json_codec.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/ebooks/metadata/book/json_codec.py b/src/calibre/ebooks/metadata/book/json_codec.py
index 7a80e16854..96178c4a63 100644
--- a/src/calibre/ebooks/metadata/book/json_codec.py
+++ b/src/calibre/ebooks/metadata/book/json_codec.py
@@ -9,7 +9,7 @@ import json
import traceback
from PIL import Image
-from . import SERIALIZABLE_FIELDS
+from calibre.ebooks.metadata.book import SERIALIZABLE_FIELDS
from calibre.constants import filesystem_encoding, preferred_encoding
from calibre.library.field_metadata import FieldMetadata
from calibre.utils.date import parse_date, isoformat, UNDEFINED_DATE
From 0ba513e2879b5e476d4787b333fb82b8dbe2e914 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 5 Sep 2010 11:01:55 +0100
Subject: [PATCH 011/289] Add a comment about not using the Metadata class, and
why
---
src/calibre/library/server/mobile.py | 4 ++++
src/calibre/library/server/xml.py | 7 ++++---
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/src/calibre/library/server/mobile.py b/src/calibre/library/server/mobile.py
index 229e0c21c4..6e08581aed 100644
--- a/src/calibre/library/server/mobile.py
+++ b/src/calibre/library/server/mobile.py
@@ -199,6 +199,10 @@ class MobileServer(object):
CKEYS = [key for key in sorted(CFM.get_custom_fields(),
cmp=lambda x,y: cmp(CFM[x]['name'].lower(),
CFM[y]['name'].lower()))]
+ # This method uses its own book dict, not the Metadata dict. The loop
+ # below could be changed to use db.get_metadata instead of reading
+ # info directly from the record made by the view, but it doesn't seem
+ # worth it at the moment.
books = []
for record in items[(start-1):(start-1)+num]:
book = {'formats':record[FM['formats']], 'size':record[FM['size']]}
diff --git a/src/calibre/library/server/xml.py b/src/calibre/library/server/xml.py
index ed8479980e..5bf2783f96 100644
--- a/src/calibre/library/server/xml.py
+++ b/src/calibre/library/server/xml.py
@@ -66,6 +66,10 @@ class XMLServer(object):
return x.decode(preferred_encoding, 'replace')
return unicode(x)
+ # This method uses its own book dict, not the Metadata dict. The loop
+ # below could be changed to use db.get_metadata instead of reading
+ # info directly from the record made by the view, but it doesn't seem
+ # worth it at the moment.
for record in items[start:start+num]:
kwargs = {}
aus = record[FM['authors']] if record[FM['authors']] else __builtin__._('Unknown')
@@ -138,6 +142,3 @@ class XMLServer(object):
return etree.tostring(ans, encoding='utf-8', pretty_print=True,
xml_declaration=True)
-
-
-
From 38c6199c7b54b243d039aba25cad86125326a582 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 5 Sep 2010 11:06:54 +0100
Subject: [PATCH 012/289] Change some comments from 'class:MetaInformation' to
'class:Metadata'
---
src/calibre/customize/__init__.py | 4 ++--
src/calibre/devices/apple/driver.py | 2 +-
src/calibre/ebooks/metadata/epub.py | 2 +-
src/calibre/ebooks/metadata/fetch.py | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py
index 27e319de14..8ddc791b2f 100644
--- a/src/calibre/customize/__init__.py
+++ b/src/calibre/customize/__init__.py
@@ -218,7 +218,7 @@ class MetadataReaderPlugin(Plugin): # {{{
with the input data.
:param type: The type of file. Guaranteed to be one of the entries
in :attr:`file_types`.
- :return: A :class:`calibre.ebooks.metadata.MetaInformation` object
+ :return: A :class:`calibre.ebooks.metadata.book.Metadata` object
'''
return None
# }}}
@@ -248,7 +248,7 @@ class MetadataWriterPlugin(Plugin): # {{{
with the input data.
:param type: The type of file. Guaranteed to be one of the entries
in :attr:`file_types`.
- :param mi: A :class:`calibre.ebooks.metadata.MetaInformation` object
+ :param mi: A :class:`calibre.ebooks.metadata.book.Metadata` object
'''
pass
diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py
index 75517e9df7..94aea1e79d 100644
--- a/src/calibre/devices/apple/driver.py
+++ b/src/calibre/devices/apple/driver.py
@@ -872,7 +872,7 @@ class ITUNES(DriverBase):
once uploaded to the device. len(names) == len(files)
:return: A list of 3-element tuples. The list is meant to be passed
to L{add_books_to_metadata}.
- :metadata: If not None, it is a list of :class:`MetaInformation` objects.
+ :metadata: If not None, it is a list of :class:`Metadata` objects.
The idea is to use the metadata to determine where on the device to
put the book. len(metadata) == len(files). Apart from the regular
cover (path to cover), there may also be a thumbnail attribute, which should
diff --git a/src/calibre/ebooks/metadata/epub.py b/src/calibre/ebooks/metadata/epub.py
index 041a1ee603..ac6b5feebe 100644
--- a/src/calibre/ebooks/metadata/epub.py
+++ b/src/calibre/ebooks/metadata/epub.py
@@ -164,7 +164,7 @@ def get_cover(opf, opf_path, stream, reader=None):
return render_html_svg_workaround(cpage, default_log)
def get_metadata(stream, extract_cover=True):
- """ Return metadata as a :class:`MetaInformation` object """
+ """ Return metadata as a :class:`Metadata` object """
stream.seek(0)
reader = OCFZipReader(stream)
mi = MetaInformation(reader.opf)
diff --git a/src/calibre/ebooks/metadata/fetch.py b/src/calibre/ebooks/metadata/fetch.py
index 96807c06ae..9b8a42e482 100644
--- a/src/calibre/ebooks/metadata/fetch.py
+++ b/src/calibre/ebooks/metadata/fetch.py
@@ -29,7 +29,7 @@ class MetadataSource(Plugin): # {{{
future use.
The fetch method must store the results in `self.results` as a list of
- :class:`MetaInformation` objects. If there is an error, it should be stored
+ :class:`Metadata` objects. If there is an error, it should be stored
in `self.exception` and `self.tb` (for the traceback).
'''
From 06b266840c8785aa93aa265c0c20308c1c7286f7 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 5 Sep 2010 12:35:35 +0100
Subject: [PATCH 013/289] Make gui version of content server able to show both
abbreviated and full lists of tags.
---
resources/content_server/gui.js | 34 ++++++++++++++++++++++++
src/calibre/ebooks/metadata/book/base.py | 7 ++---
src/calibre/library/server/utils.py | 9 ++++---
src/calibre/library/server/xml.py | 6 +++--
4 files changed, 47 insertions(+), 9 deletions(-)
diff --git a/resources/content_server/gui.js b/resources/content_server/gui.js
index 8368866a5e..abbd409dc8 100644
--- a/resources/content_server/gui.js
+++ b/resources/content_server/gui.js
@@ -59,14 +59,44 @@ function render_book(book) {
title = title.slice(0, title.length-2);
title += ' ({0} MB) '.format(size);
}
+ title += ''
+ if (tags) {
+ t = tags.split(':&:', 2);
+ m = parseInt(t[0]);
+ t = t[1].split(',', m);
+ if (t.length == m) t[m] = '...'
+ title += 'Tags=[{0}] '.format(t.join(','));
+ }
+ custcols = book.attr("custcols").split(',')
+ for ( i = 0; i < custcols.length; i++) {
+ if (custcols[i].length > 0) {
+ vals = book.attr(custcols[i]).split(':#:', 2);
+ if (vals[0].indexOf('#T#') == 0) { //startswith
+ vals[0] = vals[0].substr(3, vals[0].length)
+ t = vals[1].split(':&:', 2);
+ m = parseInt(t[0]);
+ t = t[1].split(',', m);
+ if (t.length == m) t[m] = '...';
+ vals[1] = t.join(',');
+ }
+ title += '{0}=[{1}] '.format(vals[0], vals[1]);
+ }
+ }
+ title += ''
+ title += ''
if (tags) title += 'Tags=[{0}] '.format(tags);
custcols = book.attr("custcols").split(',')
for ( i = 0; i < custcols.length; i++) {
if (custcols[i].length > 0) {
vals = book.attr(custcols[i]).split(':#:', 2);
+ if (vals[0].indexOf('#T#') == 0) { //startswith
+ vals[0] = vals[0].substr(3, vals[0].length)
+ vals[1] = (vals[1].split(':&:', 2))[1];
+ }
title += '{0}=[{1}] '.format(vals[0], vals[1]);
}
}
+ title += ''
title += '
'.format(id);
title += ''.format(comments)
// Render authors cell
@@ -170,11 +200,15 @@ function fetch_library_books(start, num, timeout, sort, order, search) {
var cover = row.find('img').attr('src');
var collapsed = row.find('.comments').css('display') == 'none';
$("#book_list tbody tr * .comments").css('display', 'none');
+ $("#book_list tbody tr * .tagdata_short").css('display', 'inherit');
+ $("#book_list tbody tr * .tagdata_long").css('display', 'none');
$('#cover_pane').css('visibility', 'hidden');
if (collapsed) {
row.find('.comments').css('display', 'inherit');
$('#cover_pane img').attr('src', cover);
$('#cover_pane').css('visibility', 'visible');
+ row.find(".tagdata_short").css('display', 'none');
+ row.find(".tagdata_long").css('display', 'inherit');
}
});
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index caaaccb3d0..2ff24b0ddc 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -262,10 +262,11 @@ class Metadata(object):
if other_lang and other_lang.lower() != 'und':
self.language = other_lang
- def format_series_index(self):
+ def format_series_index(self, val=None):
from calibre.ebooks.metadata import fmt_sidx
+ v = self.series_index if val is None else val
try:
- x = float(self.series_index)
+ x = float(v)
except ValueError:
x = 1
return fmt_sidx(x)
@@ -296,7 +297,7 @@ class Metadata(object):
if datatype == 'text' and cmeta['is_multiple']:
res = u', '.join(res)
elif datatype == 'series':
- res = res + ' [%s]'%self.format_series_index(self.get_extra(key))
+ res = res + ' [%s]'%self.format_series_index(val=self.get_extra(key))
elif datatype == 'datetime':
res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
elif datatype == 'bool':
diff --git a/src/calibre/library/server/utils.py b/src/calibre/library/server/utils.py
index 23916aa75c..373653c15f 100644
--- a/src/calibre/library/server/utils.py
+++ b/src/calibre/library/server/utils.py
@@ -5,7 +5,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-import time
+import time, sys
import cherrypy
@@ -44,8 +44,8 @@ def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None):
except:
return _strftime(fmt, nowf().timetuple())
-def format_tag_string(tags, sep):
- MAX = tweaks['max_content_server_tags_shown']
+def format_tag_string(tags, sep, ignore_max=False):
+ MAX = sys.maxint if ignore_max else tweaks['max_content_server_tags_shown']
if tags:
tlist = [t.strip() for t in tags.split(sep)]
else:
@@ -53,5 +53,6 @@ def format_tag_string(tags, sep):
tlist.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
if len(tlist) > MAX:
tlist = tlist[:MAX]+['...']
- return u'%s'%(', '.join(tlist)) if tlist else ''
+ return u'%s:&:%s'%(tweaks['max_content_server_tags_shown'],
+ ', '.join(tlist)) if tlist else ''
diff --git a/src/calibre/library/server/xml.py b/src/calibre/library/server/xml.py
index 5bf2783f96..8715dda7d0 100644
--- a/src/calibre/library/server/xml.py
+++ b/src/calibre/library/server/xml.py
@@ -89,7 +89,7 @@ class XMLServer(object):
'comments'):
y = record[FM[x]]
if x == 'tags':
- y = format_tag_string(y, ',')
+ y = format_tag_string(y, ',', ignore_max=True)
kwargs[x] = serialize(y) if y else ''
c = kwargs.pop('comments')
@@ -111,7 +111,9 @@ class XMLServer(object):
name = CFM[key]['name']
custcols.append(k)
if datatype == 'text' and CFM[key]['is_multiple']:
- kwargs[k] = concat(name, format_tag_string(val,'|'))
+ kwargs[k] = concat('#T#'+name,
+ format_tag_string(val,'|',
+ ignore_max=True))
elif datatype == 'series':
kwargs[k] = concat(name, '%s [%s]'%(val,
fmt_sidx(record[CFM.cc_series_index_column_for(key)])))
From 6bad998546cc2c9891bdf48a5ff89ebe8a86b0cd Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 8 Sep 2010 16:52:40 +0100
Subject: [PATCH 014/289] Add custom fields to the 'unicode' function
---
src/calibre/ebooks/metadata/book/base.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 2ff24b0ddc..648beb7b5c 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -338,7 +338,11 @@ class Metadata(object):
fmt('Published', isoformat(self.pubdate))
if self.rights is not None:
fmt('Rights', unicode(self.rights))
- # TODO: NEWMETA: What to do about custom fields?
+ for key in self.user_metadata_keys:
+ val = self.get(key, None)
+ if val is not None:
+ (name, val) = self.format_custom_field(key)
+ fmt(name, unicode(val))
return u'\n'.join(ans)
def to_html(self):
From 9155f838f1f43d878c083bd1a9b9ce955d1c32c8 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 8 Sep 2010 19:39:57 +0100
Subject: [PATCH 015/289] Fix some bugs found after or introduced by trunk
merges
---
src/calibre/ebooks/metadata/book/__init__.py | 3 ++-
src/calibre/ebooks/metadata/book/base.py | 26 ++++++++++----------
src/calibre/library/save_to_disk.py | 25 ++++++++++---------
3 files changed, 29 insertions(+), 25 deletions(-)
diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py
index ca7f4f7074..e7f58ce858 100644
--- a/src/calibre/ebooks/metadata/book/__init__.py
+++ b/src/calibre/ebooks/metadata/book/__init__.py
@@ -109,7 +109,8 @@ COPYABLE_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
CALIBRE_METADATA_FIELDS) - \
frozenset(['title', 'title_sort', 'authors',
'author_sort', 'author_sort_map' 'comments',
- 'cover_data', 'tags', 'language', 'lpath'])
+ 'cover_data', 'tags', 'language', 'lpath',
+ 'size'])
SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
USER_METADATA_FIELDS).union(
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 648beb7b5c..69a3c42f4d 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -201,6 +201,11 @@ class Metadata(object):
Merge the information in C{other} into self. In case of conflicts, the information
in C{other} takes precedence, unless the information in other is NULL.
'''
+ def copy_not_none(dest, src, attr):
+ v = getattr(src, attr, None)
+ if v is not None:
+ setattr(dest, attr, copy.deepcopy(v))
+
if other.title and other.title != _('Unknown'):
self.title = other.title
if hasattr(other, 'title_sort'):
@@ -220,21 +225,19 @@ class Metadata(object):
self.tags = other.tags
self.cover_data = getattr(other, 'cover_data', '')
self.set_all_user_metadata(other.get_all_user_metadata(make_copy=True))
- self.comments = getattr(other, 'comments', '')
- self.language = getattr(other, 'language', None)
- lpath = getattr(other, 'lpath', None)
- if lpath is not None:
- self.lpath = lpath
+ copy_not_none(self, other, 'lpath')
+ copy_not_none(self, other, 'size')
+ copy_not_none(self, other, 'comments')
+ # language is handled below
else:
for attr in COPYABLE_METADATA_FIELDS:
if hasattr(other, attr):
+ copy_not_none(self, other, attr)
val = getattr(other, attr)
if val is not None:
setattr(self, attr, copy.deepcopy(val))
-
if other.tags:
self.tags += list(set(self.tags + other.tags))
-
if getattr(other, 'cover_data', False):
other_cover = other.cover_data[-1]
self_cover = self.cover_data[-1] if self.cover_data else ''
@@ -242,13 +245,11 @@ class Metadata(object):
if not other_cover: other_cover = ''
if len(other_cover) > len(self_cover):
self.cover_data = other.cover_data
-
if getattr(other, 'user_metadata_keys', None):
for x in other.user_metadata_keys:
meta = other.get_user_metadata(x, make_copy=True)
if meta is not None:
self.set_user_metadata(x, meta) # get... did the deepcopy
-
my_comments = getattr(self, 'comments', '')
other_comments = getattr(other, 'comments', '')
if not my_comments:
@@ -257,10 +258,9 @@ class Metadata(object):
other_comments = ''
if len(other_comments.strip()) > len(my_comments.strip()):
self.comments = other_comments
-
- other_lang = getattr(other, 'language', None)
- if other_lang and other_lang.lower() != 'und':
- self.language = other_lang
+ other_lang = getattr(other, 'language', None)
+ if other_lang and other_lang.lower() != 'und':
+ self.language = other_lang
def format_series_index(self, val=None):
from calibre.ebooks.metadata import fmt_sidx
diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py
index c940cc006b..d33cbb04b5 100644
--- a/src/calibre/library/save_to_disk.py
+++ b/src/calibre/library/save_to_disk.py
@@ -6,7 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-import os, traceback, cStringIO, re
+import os, traceback, cStringIO, re, string
from calibre.utils.config import Config, StringConfig, tweaks
from calibre.utils.filenames import shorten_components_to, supports_long_names, \
@@ -98,17 +98,20 @@ def preprocess_template(template):
template = template.decode(preferred_encoding, 'replace')
return template
+class SafeFormat(string.Formatter):
+ '''
+ Provides a format function that substitutes '' for any missing value
+ '''
+ def get_value(self, key, args, kwargs):
+ try:
+ return kwargs[key]
+ except:
+ return ''
+safe_formatter = SafeFormat()
+
def safe_format(x, format_args):
- try:
- ans = x.format(**format_args).strip()
- return re.sub(r'\s+', ' ', ans)
- except IndexError: # Thrown if user used [] and index is out of bounds
- pass
- except AttributeError: # Thrown if user used a non existing attribute
- pass
- except KeyError: # Thrown if user used custom field w/value None
- pass
- return ''
+ ans = safe_formatter.vformat(x, [], format_args).strip()
+ return re.sub(r'\s+', ' ', ans)
def get_components(template, mi, id, timefmt='%b %Y', length=250,
sanitize_func=ascii_filename, replace_whitespace=False,
From 7603f208b39032cbcdc42939c0753ed8ddbbe987 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 8 Sep 2010 20:29:42 +0100
Subject: [PATCH 016/289] SLight cleanup
---
src/calibre/library/save_to_disk.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py
index d33cbb04b5..3fa40c68b2 100644
--- a/src/calibre/library/save_to_disk.py
+++ b/src/calibre/library/save_to_disk.py
@@ -118,7 +118,7 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
to_lowercase=False):
library_order = tweaks['save_template_title_series_sorting'] == 'library_order'
tsfmt = title_sort if library_order else lambda x: x
- format_args = dict(**FORMAT_ARGS)
+ format_args = FORMAT_ARGS.copy()
format_args.update(mi.get_all_non_none_attributes())
if mi.title:
format_args['title'] = tsfmt(mi.title)
From f175b8bf6d75ac8615227c2557dcccc341f493d4 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Fri, 10 Sep 2010 20:12:12 -0600
Subject: [PATCH 017/289] ...
---
src/calibre/ebooks/metadata/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py
index f64a269fd1..429ba06c6e 100644
--- a/src/calibre/ebooks/metadata/__init__.py
+++ b/src/calibre/ebooks/metadata/__init__.py
@@ -231,7 +231,7 @@ def MetaInformation(title, authors=(_('Unknown'),)):
mi = title
title = mi.title
authors = mi.authors
- return Metadata(title, authors, mi)
+ return Metadata(title, authors, other=mi)
def check_isbn10(isbn):
try:
From 4f01b09ded8a0aa21f273596051ca8f32426bef2 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sat, 11 Sep 2010 09:21:29 +0100
Subject: [PATCH 018/289] Check in the metadata class that custom field names
begin with '#'
---
src/calibre/ebooks/metadata/book/base.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 69a3c42f4d..d0b428bf96 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -156,6 +156,9 @@ class Metadata(object):
the key name not the label
'''
if field is not None:
+ if not field.startswith('#'):
+ raise AttributeError(
+ 'Custom field name %s must begin with \'#\''%repr(field))
if metadata is None:
traceback.print_stack()
metadata = copy.deepcopy(metadata)
From afe5546a15aa34fec849c875cb2b9b139c36ebd6 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sat, 11 Sep 2010 09:23:03 +0100
Subject: [PATCH 019/289] Avoid spurious exceptions when adding None custom
metadata
---
src/calibre/ebooks/metadata/book/base.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index d0b428bf96..be9a4675c0 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -161,6 +161,7 @@ class Metadata(object):
'Custom field name %s must begin with \'#\''%repr(field))
if metadata is None:
traceback.print_stack()
+ return
metadata = copy.deepcopy(metadata)
if '#value#' not in metadata:
if metadata['datatype'] == 'text' and metadata['is_multiple']:
From db54abd2b659290ba8a8d598f99fc16c853bf2f7 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Sun, 12 Sep 2010 12:08:18 -0600
Subject: [PATCH 020/289] Various tweaks to the way smart_update works
---
src/calibre/ebooks/metadata/book/base.py | 34 +++++++++++++++---------
1 file changed, 21 insertions(+), 13 deletions(-)
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index be9a4675c0..f52c41e4c5 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -202,12 +202,12 @@ class Metadata(object):
def smart_update(self, other, replace_metadata=False):
'''
- Merge the information in C{other} into self. In case of conflicts, the information
- in C{other} takes precedence, unless the information in other is NULL.
+ Merge the information in `other` into self. In case of conflicts, the information
+ in `other` takes precedence, unless the information in `other` is NULL.
'''
def copy_not_none(dest, src, attr):
v = getattr(src, attr, None)
- if v is not None:
+ if v not in (None, NULL_VALUES.get(attr, None)):
setattr(dest, attr, copy.deepcopy(v))
if other.title and other.title != _('Unknown'):
@@ -216,32 +216,38 @@ class Metadata(object):
self.title_sort = other.title_sort
if other.authors and other.authors[0] != _('Unknown'):
- self.authors = other.authors
+ self.authors = list(other.authors)
if hasattr(other, 'author_sort_map'):
- self.author_sort_map = other.author_sort_map
+ self.author_sort_map = dict(other.author_sort_map)
if hasattr(other, 'author_sort'):
self.author_sort = other.author_sort
if replace_metadata:
+ SPECIAL_FIELDS = frozenset(['lpath', 'size', 'comments'])
for attr in COPYABLE_METADATA_FIELDS:
setattr(self, attr, getattr(other, attr, 1.0 if \
attr == 'series_index' else None))
self.tags = other.tags
- self.cover_data = getattr(other, 'cover_data', '')
+ self.cover_data = getattr(other, 'cover_data',
+ NULL_VALUES['cover_data'])
self.set_all_user_metadata(other.get_all_user_metadata(make_copy=True))
- copy_not_none(self, other, 'lpath')
- copy_not_none(self, other, 'size')
- copy_not_none(self, other, 'comments')
+ for x in SPECIAL_FIELDS:
+ copy_not_none(self, other, x)
# language is handled below
else:
for attr in COPYABLE_METADATA_FIELDS:
if hasattr(other, attr):
copy_not_none(self, other, attr)
- val = getattr(other, attr)
- if val is not None:
- setattr(self, attr, copy.deepcopy(val))
if other.tags:
- self.tags += list(set(self.tags + other.tags))
+ # Case-insensitive but case preserving merging
+ lotags = [t.lower() for t in other.tags]
+ lstags = [t.lower() for t in self.tags]
+ ot, st = map(frozenset, (lotags, lstags))
+ for t in st.interection(ot):
+ sidx = lstags.index(t)
+ oidx = lotags.index(t)
+ self.tags[sidx] = other.tags[oidx]
+ self.tags += [t for t in other.tags if t.lower() in ot-st]
if getattr(other, 'cover_data', False):
other_cover = other.cover_data[-1]
self_cover = self.cover_data[-1] if self.cover_data else ''
@@ -262,6 +268,7 @@ class Metadata(object):
other_comments = ''
if len(other_comments.strip()) > len(my_comments.strip()):
self.comments = other_comments
+
other_lang = getattr(other, 'language', None)
if other_lang and other_lang.lower() != 'und':
self.language = other_lang
@@ -383,3 +390,4 @@ class Metadata(object):
return bool(self.title or self.author or self.comments or self.tags)
# }}}
+
From b01b603358c0332d481a3c572fcd44d63d5cccdf Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Sun, 12 Sep 2010 13:28:22 -0600
Subject: [PATCH 021/289] json_codec: Handle dictionaries with bytsestring
keys/vals as well
---
.../ebooks/metadata/book/json_codec.py | 35 ++++++++++++-------
1 file changed, 22 insertions(+), 13 deletions(-)
diff --git a/src/calibre/ebooks/metadata/book/json_codec.py b/src/calibre/ebooks/metadata/book/json_codec.py
index 0e205c52b0..ea0de07342 100644
--- a/src/calibre/ebooks/metadata/book/json_codec.py
+++ b/src/calibre/ebooks/metadata/book/json_codec.py
@@ -12,23 +12,18 @@ from calibre.ebooks.metadata.book import SERIALIZABLE_FIELDS
from calibre.constants import filesystem_encoding, preferred_encoding
from calibre.library.field_metadata import FieldMetadata
from calibre.utils.date import parse_date, isoformat, UNDEFINED_DATE
+from calibre import isbytestring
# Translate datetimes to and from strings. The string form is the datetime in
# UTC. The returned date is also UTC
def string_to_datetime(src):
if src == "None":
return None
-# dt = strptime(src, '%d %m %Y %H:%M:%S', assume_utc=True, as_utc=True)
-# if dt == UNDEFINED_DATE:
-# return None
return parse_date(src)
def datetime_to_string(dateval):
if dateval is None or dateval == UNDEFINED_DATE:
return "None"
-# tt = date_to_utc(dateval).timetuple()
-# res = "%02d %02d %04d %02d:%02d:%02d"%(tt.tm_mday, tt.tm_mon, tt.tm_year,
-# tt.tm_hour, tt.tm_min, tt.tm_sec)
return isoformat(dateval)
def encode_thumbnail(thumbnail):
@@ -47,6 +42,24 @@ def decode_thumbnail(tup):
return None
return (tup[0], tup[1], b64decode(tup[2]))
+def object_to_unicode(obj, enc=preferred_encoding):
+
+ def dec(x):
+ return x.decode(enc, 'replace')
+
+ if isbytestring(obj):
+ return dec(obj)
+ if isinstance(obj, (list, tuple)):
+ return [dec(x) if isbytestring(x) else x for x in obj]
+ if isinstance(obj, dict):
+ ans = {}
+ for k, v in obj.items():
+ k = object_to_unicode(k)
+ v = object_to_unicode(v)
+ ans[k] = v
+ return ans
+ return obj
+
class JsonCodec(object):
def __init__(self):
@@ -81,16 +94,13 @@ class JsonCodec(object):
value = book.get(key)
if key == 'thumbnail':
return encode_thumbnail(value)
- elif isinstance(value, str): # str includes bytes
+ elif isbytestring(value): # str includes bytes
enc = filesystem_encoding if key == 'lpath' else preferred_encoding
- return value.decode(enc, 'replace')
- elif isinstance(value, (list, tuple)):
- return [x.decode(preferred_encoding, 'replace') if
- isinstance(x, str) else x for x in value]
+ return object_to_unicode(value, enc=enc)
elif datatype == 'datetime':
return datetime_to_string(value)
else:
- return value
+ return object_to_unicode(value)
def decode_from_file(self, file, booklist, book_class, prefix):
js = []
@@ -108,7 +118,6 @@ class JsonCodec(object):
except:
print 'exception during JSON decoding'
traceback.print_exc()
- booklist = []
def decode_metadata(self, key, value):
if key == 'user_metadata':
From ea3719c7737b4c4df60840ed0b86bed872e07a6c Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 12 Sep 2010 20:52:26 +0100
Subject: [PATCH 022/289] Content server fixes
---
src/calibre/library/server/mobile.py | 8 ++++++--
src/calibre/library/server/opds.py | 10 +++++++---
src/calibre/library/server/utils.py | 7 +++++--
3 files changed, 18 insertions(+), 7 deletions(-)
diff --git a/src/calibre/library/server/mobile.py b/src/calibre/library/server/mobile.py
index 6e08581aed..ab5b39eed8 100644
--- a/src/calibre/library/server/mobile.py
+++ b/src/calibre/library/server/mobile.py
@@ -124,6 +124,7 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
series = u'[%s - %s]'%(book['series'], book['series_index']) \
if book['series'] else ''
tags = u'Tags=[%s]'%book['tags'] if book['tags'] else ''
+ print tags
ctext = ''
for key in CKEYS:
@@ -217,7 +218,8 @@ class MobileServer(object):
book['authors'] = authors
book['series_index'] = fmt_sidx(float(record[FM['series_index']]))
book['series'] = record[FM['series']]
- book['tags'] = format_tag_string(record[FM['tags']], ',')
+ book['tags'] = format_tag_string(record[FM['tags']], ',',
+ no_tag_count=True)
book['title'] = record[FM['title']]
for x in ('timestamp', 'pubdate'):
book[x] = strftime('%Y/%m/%d %H:%M:%S', record[FM[x]])
@@ -233,7 +235,9 @@ class MobileServer(object):
continue
name = CFM[key]['name']
if datatype == 'text' and CFM[key]['is_multiple']:
- book[key] = concat(name, format_tag_string(val, '|'))
+ book[key] = concat(name,
+ format_tag_string(val, '|',
+ no_tag_count=True))
elif datatype == 'series':
book[key] = concat(name, '%s [%s]'%(val,
fmt_sidx(record[CFM.cc_series_index_column_for(key)])))
diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py
index c3a1d68749..e495598a2f 100644
--- a/src/calibre/library/server/opds.py
+++ b/src/calibre/library/server/opds.py
@@ -17,6 +17,7 @@ import routes
from calibre.constants import __appname__
from calibre.ebooks.metadata import fmt_sidx
from calibre.library.comments import comments_to_html
+from calibre.library.server.utils import format_tag_string
from calibre import guess_type
from calibre.utils.ordered_dict import OrderedDict
from calibre.utils.date import format_date
@@ -147,8 +148,9 @@ def ACQUISITION_ENTRY(item, version, FM, updated, CFM, CKEYS):
extra.append(_('RATING: %s
')%rating)
tags = item[FM['tags']]
if tags:
- extra.append(_('TAGS: %s
')%\
- ', '.join(tags.split(',')))
+ extra.append(_('TAGS: %s
')%format_tag_string(tags, ',',
+ ignore_max=True,
+ no_tag_count=True))
series = item[FM['series']]
if series:
extra.append(_('SERIES: %s [%s]
')%\
@@ -160,7 +162,9 @@ def ACQUISITION_ENTRY(item, version, FM, updated, CFM, CKEYS):
name = CFM[key]['name']
datatype = CFM[key]['datatype']
if datatype == 'text' and CFM[key]['is_multiple']:
- extra.append('%s: %s
'%(name, ', '.join(val.split('|'))))
+ extra.append('%s: %s
'%(name, format_tag_string(val, '|',
+ ignore_max=True,
+ no_tag_count=True)))
elif datatype == 'series':
extra.append('%s: %s [%s]
'%(name, val,
fmt_sidx(item[CFM.cc_series_index_column_for(key)])))
diff --git a/src/calibre/library/server/utils.py b/src/calibre/library/server/utils.py
index 373653c15f..9a64948a3d 100644
--- a/src/calibre/library/server/utils.py
+++ b/src/calibre/library/server/utils.py
@@ -44,7 +44,7 @@ def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None):
except:
return _strftime(fmt, nowf().timetuple())
-def format_tag_string(tags, sep, ignore_max=False):
+def format_tag_string(tags, sep, ignore_max=False, no_tag_count=False):
MAX = sys.maxint if ignore_max else tweaks['max_content_server_tags_shown']
if tags:
tlist = [t.strip() for t in tags.split(sep)]
@@ -53,6 +53,9 @@ def format_tag_string(tags, sep, ignore_max=False):
tlist.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
if len(tlist) > MAX:
tlist = tlist[:MAX]+['...']
- return u'%s:&:%s'%(tweaks['max_content_server_tags_shown'],
+ if no_tag_count:
+ return ', '.join(tlist) if tlist else ''
+ else:
+ return u'%s:&:%s'%(tweaks['max_content_server_tags_shown'],
', '.join(tlist)) if tlist else ''
From 240c9428f5da2c5452dd7f0ab6cc8a6c8cd67afe Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Sun, 12 Sep 2010 13:57:42 -0600
Subject: [PATCH 023/289] Revert change to how Metadata.thumbnail is
interpreted in library.models
---
src/calibre/gui2/library/models.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index b0aec7446a..b81628cd27 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -1055,8 +1055,8 @@ class DeviceBooksModel(BooksModel): # {{{
img = QImage()
if hasattr(cdata, 'image_path'):
img.load(cdata.image_path)
- else:
- img.loadFromData(cdata[2])
+ elif cdata:
+ img.loadFromData(cdata)
if img.isNull():
img = self.default_image
data['cover'] = img
From 0a2f80fdbff1b5fb541e567a8735ba5b8ca20ffa Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 12 Sep 2010 21:13:35 +0100
Subject: [PATCH 024/289] Merge from trunk
---
src/calibre/devices/usbms/books.py | 2 +-
src/calibre/ebooks/metadata/book/__init__.py | 2 +-
src/calibre/ebooks/metadata/book/base.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index 3e13527bd0..4d5110b049 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -59,7 +59,7 @@ class Book(Metadata):
return property(doc=doc, fget=fget)
@dynamic_property
- def thumbnail(self):
+ def thumbnail(self):'
return None
class BookList(_BookList):
diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py
index e7f58ce858..84a88606f2 100644
--- a/src/calibre/ebooks/metadata/book/__init__.py
+++ b/src/calibre/ebooks/metadata/book/__init__.py
@@ -110,7 +110,7 @@ COPYABLE_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
frozenset(['title', 'title_sort', 'authors',
'author_sort', 'author_sort_map' 'comments',
'cover_data', 'tags', 'language', 'lpath',
- 'size'])
+ 'size', 'thumbnail'])
SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
USER_METADATA_FIELDS).union(
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index f52c41e4c5..647a9f467e 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -243,7 +243,7 @@ class Metadata(object):
lotags = [t.lower() for t in other.tags]
lstags = [t.lower() for t in self.tags]
ot, st = map(frozenset, (lotags, lstags))
- for t in st.interection(ot):
+ for t in st.intersection(ot):
sidx = lstags.index(t)
oidx = lotags.index(t)
self.tags[sidx] = other.tags[oidx]
From db4b8d8216bd2299d471be43b114e3e05edf1f26 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 12 Sep 2010 21:15:31 +0100
Subject: [PATCH 025/289] Fix some inadvertent changes
---
src/calibre/devices/usbms/books.py | 2 +-
src/calibre/ebooks/metadata/book/__init__.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index 4d5110b049..3e13527bd0 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -59,7 +59,7 @@ class Book(Metadata):
return property(doc=doc, fget=fget)
@dynamic_property
- def thumbnail(self):'
+ def thumbnail(self):
return None
class BookList(_BookList):
diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py
index 84a88606f2..e7f58ce858 100644
--- a/src/calibre/ebooks/metadata/book/__init__.py
+++ b/src/calibre/ebooks/metadata/book/__init__.py
@@ -110,7 +110,7 @@ COPYABLE_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
frozenset(['title', 'title_sort', 'authors',
'author_sort', 'author_sort_map' 'comments',
'cover_data', 'tags', 'language', 'lpath',
- 'size', 'thumbnail'])
+ 'size'])
SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
USER_METADATA_FIELDS).union(
From 6ca5263d005a10218008ba74a1a8942ca5c6f74f Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 12 Sep 2010 21:36:23 +0100
Subject: [PATCH 026/289] Fix typo, add merge is_multiple
---
src/calibre/ebooks/metadata/book/base.py | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 647a9f467e..f2031afd0e 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -259,7 +259,20 @@ class Metadata(object):
for x in other.user_metadata_keys:
meta = other.get_user_metadata(x, make_copy=True)
if meta is not None:
+ self_tags = self.get(x, [])
self.set_user_metadata(x, meta) # get... did the deepcopy
+ other_tags = other.get(x, [])
+ if meta['is_multiple']:
+ # Case-insensitive but case preserving merging
+ lotags = [t.lower() for t in other_tags]
+ lstags = [t.lower() for t in self_tags]
+ ot, st = map(frozenset, (lotags, lstags))
+ for t in st.intersection(ot):
+ sidx = lstags.index(t)
+ oidx = lotags.index(t)
+ self_tags[sidx] = other.tags[oidx]
+ self_tags += [t for t in other.tags if t.lower() in ot-st]
+ setattr(self, x, self_tags)
my_comments = getattr(self, 'comments', '')
other_comments = getattr(other, 'comments', '')
if not my_comments:
From 8b554ee0cda99dd11c50ba6def1634ca2416396b Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 12 Sep 2010 22:04:10 +0100
Subject: [PATCH 027/289] Fixes for thumbnails.
---
src/calibre/ebooks/metadata/book/__init__.py | 2 +-
src/calibre/ebooks/metadata/book/base.py | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py
index e7f58ce858..84a88606f2 100644
--- a/src/calibre/ebooks/metadata/book/__init__.py
+++ b/src/calibre/ebooks/metadata/book/__init__.py
@@ -110,7 +110,7 @@ COPYABLE_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
frozenset(['title', 'title_sort', 'authors',
'author_sort', 'author_sort_map' 'comments',
'cover_data', 'tags', 'language', 'lpath',
- 'size'])
+ 'size', 'thumbnail'])
SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
USER_METADATA_FIELDS).union(
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index f2031afd0e..7812f81180 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -223,7 +223,7 @@ class Metadata(object):
self.author_sort = other.author_sort
if replace_metadata:
- SPECIAL_FIELDS = frozenset(['lpath', 'size', 'comments'])
+ SPECIAL_FIELDS = frozenset(['lpath', 'size', 'comments', 'thumbnail'])
for attr in COPYABLE_METADATA_FIELDS:
setattr(self, attr, getattr(other, attr, 1.0 if \
attr == 'series_index' else None))
@@ -238,6 +238,7 @@ class Metadata(object):
for attr in COPYABLE_METADATA_FIELDS:
if hasattr(other, attr):
copy_not_none(self, other, attr)
+ copy_not_none(self, other, 'thumbnail')
if other.tags:
# Case-insensitive but case preserving merging
lotags = [t.lower() for t in other.tags]
From 171ac9488aeadfe1b6fd0605c17bbc71662726ed Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 12 Sep 2010 22:32:20 +0100
Subject: [PATCH 028/289] An attempt to make covers work.
---
src/calibre/gui2/library/models.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index b81628cd27..4e8e9a10bd 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -1056,7 +1056,10 @@ class DeviceBooksModel(BooksModel): # {{{
if hasattr(cdata, 'image_path'):
img.load(cdata.image_path)
elif cdata:
- img.loadFromData(cdata)
+ if isinstance(cdata, tuple):
+ img.loadFromData(cdata[2])
+ else:
+ img.loadFromData(cdata)
if img.isNull():
img = self.default_image
data['cover'] = img
From e73b688ca8ee61b39a65dbfb40d36863b74800f2 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 12 Sep 2010 22:44:36 +0100
Subject: [PATCH 029/289] Deal with the two thumbnail formats
---
src/calibre/ebooks/metadata/book/json_codec.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/calibre/ebooks/metadata/book/json_codec.py b/src/calibre/ebooks/metadata/book/json_codec.py
index ea0de07342..a6235e64d5 100644
--- a/src/calibre/ebooks/metadata/book/json_codec.py
+++ b/src/calibre/ebooks/metadata/book/json_codec.py
@@ -12,6 +12,7 @@ from calibre.ebooks.metadata.book import SERIALIZABLE_FIELDS
from calibre.constants import filesystem_encoding, preferred_encoding
from calibre.library.field_metadata import FieldMetadata
from calibre.utils.date import parse_date, isoformat, UNDEFINED_DATE
+from calibre.utils.magick.draw import identify_data
from calibre import isbytestring
# Translate datetimes to and from strings. The string form is the datetime in
@@ -32,7 +33,12 @@ def encode_thumbnail(thumbnail):
'''
if thumbnail is None:
return None
- return (thumbnail[0], thumbnail[1], b64encode(str(thumbnail[2])))
+ if isinstance(thumbnail, tuple):
+ try:
+ thumbnail = identify_data(thumbnail)
+ except:
+ return None
+ return (0, 0, b64encode(str(thumbnail)))
def decode_thumbnail(tup):
'''
From e0261c2ba1e53300275849818df0b78729f99b98 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 12 Sep 2010 22:50:34 +0100
Subject: [PATCH 030/289] This time, do json thumbnails right.
---
src/calibre/ebooks/metadata/book/json_codec.py | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/src/calibre/ebooks/metadata/book/json_codec.py b/src/calibre/ebooks/metadata/book/json_codec.py
index a6235e64d5..51b9722803 100644
--- a/src/calibre/ebooks/metadata/book/json_codec.py
+++ b/src/calibre/ebooks/metadata/book/json_codec.py
@@ -12,7 +12,7 @@ from calibre.ebooks.metadata.book import SERIALIZABLE_FIELDS
from calibre.constants import filesystem_encoding, preferred_encoding
from calibre.library.field_metadata import FieldMetadata
from calibre.utils.date import parse_date, isoformat, UNDEFINED_DATE
-from calibre.utils.magick.draw import identify_data
+from calibre.utils.magick import Image
from calibre import isbytestring
# Translate datetimes to and from strings. The string form is the datetime in
@@ -33,12 +33,15 @@ def encode_thumbnail(thumbnail):
'''
if thumbnail is None:
return None
- if isinstance(thumbnail, tuple):
+ if not isinstance(thumbnail, tuple):
try:
- thumbnail = identify_data(thumbnail)
+ img = Image()
+ img.load(thumbnail)
+ width, height = img.size
+ thumbnail = (width, height, thumbnail)
except:
return None
- return (0, 0, b64encode(str(thumbnail)))
+ return (thumbnail[0], thumbnail[1], b64encode(str(thumbnail[2])))
def decode_thumbnail(tup):
'''
From be95815b6ce234602f1fe6f76a51a3571db06535 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Sun, 12 Sep 2010 18:10:52 -0600
Subject: [PATCH 031/289] ...
---
src/calibre/gui2/library/models.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index 4e8e9a10bd..09a28fb04e 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -1056,8 +1056,8 @@ class DeviceBooksModel(BooksModel): # {{{
if hasattr(cdata, 'image_path'):
img.load(cdata.image_path)
elif cdata:
- if isinstance(cdata, tuple):
- img.loadFromData(cdata[2])
+ if isinstance(cdata, (tuple, list)):
+ img.loadFromData(cdata[-1])
else:
img.loadFromData(cdata)
if img.isNull():
From 43adf4226a6d381cf121ad5871b9237af57c58af Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 13 Sep 2010 08:18:09 +0100
Subject: [PATCH 032/289] Rationalize how smart_update knows what to do.
Introduced 2 more metadata groups. One is the list of attributes that
smart_update is to process specially. The other is the list of attributes
that are to be copied if not none (what you called SPECIAL_FIELDS). These two
help keep the replace and merge branches in sync.
---
src/calibre/ebooks/metadata/book/__init__.py | 17 ++++++++++-----
src/calibre/ebooks/metadata/book/base.py | 23 ++++++++++++--------
2 files changed, 26 insertions(+), 14 deletions(-)
diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py
index 84a88606f2..e087f8072d 100644
--- a/src/calibre/ebooks/metadata/book/__init__.py
+++ b/src/calibre/ebooks/metadata/book/__init__.py
@@ -101,16 +101,23 @@ STANDARD_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
DEVICE_METADATA_FIELDS).union(
CALIBRE_METADATA_FIELDS)
+# Metadata fields that smart update must do special processing to copy.
+
+SC_FIELDS_NOT_COPIED = frozenset(['title', 'title_sort', 'authors',
+ 'author_sort', 'author_sort_map',
+ 'cover_data', 'tags', 'language'])
+
+# Metadata fields that smart update should copy only if the source is not None
+SC_FIELDS_COPY_NOT_NULL = frozenset(['lpath', 'size', 'comments', 'thumbnail'])
+
# Metadata fields that smart update should copy without special handling
-COPYABLE_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
+SC_COPYABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
PUBLICATION_METADATA_FIELDS).union(
BOOK_STRUCTURE_FIELDS).union(
DEVICE_METADATA_FIELDS).union(
CALIBRE_METADATA_FIELDS) - \
- frozenset(['title', 'title_sort', 'authors',
- 'author_sort', 'author_sort_map' 'comments',
- 'cover_data', 'tags', 'language', 'lpath',
- 'size', 'thumbnail'])
+ SC_FIELDS_NOT_COPIED.union(
+ SC_FIELDS_COPY_NOT_NULL)
SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
USER_METADATA_FIELDS).union(
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 7812f81180..8538ed886c 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -9,7 +9,8 @@ import copy
import traceback
from calibre import prints
-from calibre.ebooks.metadata.book import COPYABLE_METADATA_FIELDS
+from calibre.ebooks.metadata.book import SC_COPYABLE_FIELDS
+from calibre.ebooks.metadata.book import SC_FIELDS_COPY_NOT_NULL
from calibre.ebooks.metadata.book import STANDARD_METADATA_FIELDS
from calibre.ebooks.metadata.book import TOP_LEVEL_CLASSIFIERS
from calibre.utils.date import isoformat, format_date
@@ -223,22 +224,23 @@ class Metadata(object):
self.author_sort = other.author_sort
if replace_metadata:
- SPECIAL_FIELDS = frozenset(['lpath', 'size', 'comments', 'thumbnail'])
- for attr in COPYABLE_METADATA_FIELDS:
+ # SPECIAL_FIELDS = frozenset(['lpath', 'size', 'comments', 'thumbnail'])
+ for attr in SC_COPYABLE_FIELDS:
setattr(self, attr, getattr(other, attr, 1.0 if \
attr == 'series_index' else None))
self.tags = other.tags
self.cover_data = getattr(other, 'cover_data',
- NULL_VALUES['cover_data'])
+ NULL_VALUES['cover_data'])
self.set_all_user_metadata(other.get_all_user_metadata(make_copy=True))
- for x in SPECIAL_FIELDS:
+ for x in SC_FIELDS_COPY_NOT_NULL:
copy_not_none(self, other, x)
# language is handled below
else:
- for attr in COPYABLE_METADATA_FIELDS:
- if hasattr(other, attr):
- copy_not_none(self, other, attr)
- copy_not_none(self, other, 'thumbnail')
+ for attr in SC_COPYABLE_FIELDS:
+ copy_not_none(self, other, attr)
+ for x in SC_FIELDS_COPY_NOT_NULL:
+ copy_not_none(self, other, x)
+
if other.tags:
# Case-insensitive but case preserving merging
lotags = [t.lower() for t in other.tags]
@@ -249,6 +251,7 @@ class Metadata(object):
oidx = lotags.index(t)
self.tags[sidx] = other.tags[oidx]
self.tags += [t for t in other.tags if t.lower() in ot-st]
+
if getattr(other, 'cover_data', False):
other_cover = other.cover_data[-1]
self_cover = self.cover_data[-1] if self.cover_data else ''
@@ -256,6 +259,7 @@ class Metadata(object):
if not other_cover: other_cover = ''
if len(other_cover) > len(self_cover):
self.cover_data = other.cover_data
+
if getattr(other, 'user_metadata_keys', None):
for x in other.user_metadata_keys:
meta = other.get_user_metadata(x, make_copy=True)
@@ -274,6 +278,7 @@ class Metadata(object):
self_tags[sidx] = other.tags[oidx]
self_tags += [t for t in other.tags if t.lower() in ot-st]
setattr(self, x, self_tags)
+
my_comments = getattr(self, 'comments', '')
other_comments = getattr(other, 'comments', '')
if not my_comments:
From a85be2ba3229f2e27221fb7f64d25a7e48977ab7 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 13 Sep 2010 11:59:45 +0100
Subject: [PATCH 033/289] Several changes: 1) allow use of unusual standard
fields in get_collections. Format them appropriately 2) change
metadata.book.base.format_field to handle standard fields. 3) add standard
metadata access methods to metadata.book.base.
---
src/calibre/devices/usbms/books.py | 18 +-----
src/calibre/ebooks/metadata/book/base.py | 71 +++++++++++++++++++++---
src/calibre/gui2/library/models.py | 2 +-
src/calibre/library/field_metadata.py | 17 ++++--
4 files changed, 76 insertions(+), 32 deletions(-)
diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index 3e13527bd0..2b19027df4 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -105,21 +105,7 @@ class CollectionsBookList(BookList):
attr_name = ''
elif attr_name != '':
attr_name = '(%s)'%attr_name
-
- if attr not in cust_field_meta:
- cat_name = '%s %s'%(category, attr_name)
- else:
- fm = cust_field_meta[attr]
- if fm['datatype'] == 'bool':
- if category:
- cat_name = '%s %s'%(_('Yes'), attr_name)
- else:
- cat_name = '%s %s'%(_('No'), attr_name)
- elif fm['datatype'] == 'datetime':
- cat_name = '%s %s'%(format_date(category,
- fm['display'].get('date_format','dd MMM yyyy')), attr_name)
- else:
- cat_name = '%s %s'%(category, attr_name)
+ cat_name = '%s %s'%(category, attr_name)
return cat_name.strip()
def get_collections(self, collection_attributes):
@@ -156,7 +142,7 @@ class CollectionsBookList(BookList):
cust_field_meta = book.get_all_user_metadata(make_copy=False)
for attr in attrs:
attr = attr.strip()
- val = meta_vals.get(attr, None)
+ ign, val = book.format_field(attr, ignore_series_index=True)
if not val: continue
if isbytestring(val):
val = val.decode(preferred_encoding, 'replace')
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 8538ed886c..6e0351353f 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -13,6 +13,7 @@ from calibre.ebooks.metadata.book import SC_COPYABLE_FIELDS
from calibre.ebooks.metadata.book import SC_FIELDS_COPY_NOT_NULL
from calibre.ebooks.metadata.book import STANDARD_METADATA_FIELDS
from calibre.ebooks.metadata.book import TOP_LEVEL_CLASSIFIERS
+from calibre.library.field_metadata import FieldMetadata
from calibre.utils.date import isoformat, format_date
@@ -30,6 +31,8 @@ NULL_VALUES = {
'language' : 'und'
}
+field_metadata = FieldMetadata()
+
class Metadata(object):
'''
@@ -112,6 +115,31 @@ class Metadata(object):
_data = object.__getattribute__(self, '_data')
return frozenset(_data['user_metadata'].iterkeys())
+ def get_standard_metadata(self, field, make_copy):
+ '''
+ return field metadata from the field if it is there. Otherwise return
+ None. field is the key name, not the label. Return a copy if requested,
+ just in case the user wants to change values in the dict.
+ '''
+ if field in field_metadata and field_metadata[field]['kind'] == 'field':
+ if make_copy:
+ return copy.deepcopy(field_metadata[field])
+ return field_metadata[field]
+ return None
+
+ def get_all_standard_metadata(self, make_copy):
+ '''
+ return a dict containing all the standard field metadata associated with
+ the book.
+ '''
+ if not make_copy:
+ return field_metadata
+ res = {}
+ for k in field_metadata:
+ if field_metadata[k]['kind'] == 'field':
+ res[k] = copy.deepcopy(field_metadata[k])
+ return res
+
def get_all_user_metadata(self, make_copy):
'''
return a dict containing all the custom field metadata associated with
@@ -315,24 +343,49 @@ class Metadata(object):
def format_rating(self):
return unicode(self.rating)
- def format_custom_field(self, key):
+ def format_field(self, key, ignore_series_index=False):
+ from calibre.ebooks.metadata import authors_to_string
'''
returns the tuple (field_name, formatted_value)
'''
- cmeta = self.get_user_metadata(key, make_copy=False)
- name = unicode(cmeta['name'])
- res = self.get(key, None)
- if res is not None:
+ if key in self.user_metadata_keys:
+ res = self.get(key, None)
+ if res is None or res == '':
+ return (None, None)
+ cmeta = self.get_user_metadata(key, make_copy=False)
+ name = unicode(cmeta['name'])
datatype = cmeta['datatype']
if datatype == 'text' and cmeta['is_multiple']:
res = u', '.join(res)
elif datatype == 'series':
- res = res + ' [%s]'%self.format_series_index(val=self.get_extra(key))
+ if not ignore_series_index:
+ res = res + \
+ ' [%s]'%self.format_series_index(val=self.get_extra(key))
elif datatype == 'datetime':
res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
elif datatype == 'bool':
res = _('Yes') if res else _('No')
- return (name, unicode(res))
+ return (name, unicode(res))
+
+ if key in field_metadata and field_metadata[key]['kind'] == 'field':
+ res = self.get(key, None)
+ if res is None or res == '':
+ return (None, None)
+ fmeta = field_metadata[key]
+ name = unicode(fmeta['name'])
+ datatype = fmeta['datatype']
+ if key == 'authors':
+ res = authors_to_string(res)
+ elif datatype == 'text' and fmeta['is_multiple']:
+ res = u', '.join(res)
+ elif datatype == 'series':
+ if not ignore_series_index:
+ res = res + ' [%s]'%self.format_series_index()
+ elif datatype == 'datetime':
+ res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy'))
+ return (name, unicode(res))
+
+ return (None, None)
def __unicode__(self):
from calibre.ebooks.metadata import authors_to_string
@@ -371,7 +424,7 @@ class Metadata(object):
for key in self.user_metadata_keys:
val = self.get(key, None)
if val is not None:
- (name, val) = self.format_custom_field(key)
+ (name, val) = self.format_field(key)
fmt(name, unicode(val))
return u'\n'.join(ans)
@@ -396,7 +449,7 @@ class Metadata(object):
for key in self.user_metadata_keys:
val = self.get(key, None)
if val is not None:
- (name, val) = self.format_custom_field(key)
+ (name, val) = self.format_field(key)
ans += [(name, val)]
for i, x in enumerate(ans):
ans[i] = u'%s | %s |
'%x
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index 09a28fb04e..5fa514ae8a 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -320,7 +320,7 @@ class BooksModel(QAbstractTableModel): # {{{
(sidx, prepare_string_for_xml(series))
mi = self.db.get_metadata(idx)
for key in mi.user_metadata_keys:
- name, val = mi.format_custom_field(key)
+ name, val = mi.format_field(key)
if val is not None:
data[name] = val
return data
diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py
index 276a6ba971..2773f573b2 100644
--- a/src/calibre/library/field_metadata.py
+++ b/src/calibre/library/field_metadata.py
@@ -5,6 +5,7 @@ Created on 25 May 2010
'''
from calibre.utils.ordered_dict import OrderedDict
+from calibre.utils.config import tweaks
class TagsIcons(dict):
'''
@@ -213,7 +214,7 @@ class FieldMetadata(dict):
'datatype':'text',
'is_multiple':None,
'kind':'field',
- 'name':None,
+ 'name':_('On Device'),
'search_terms':['ondevice'],
'is_custom':False,
'is_category':False}),
@@ -231,7 +232,7 @@ class FieldMetadata(dict):
'datatype':'datetime',
'is_multiple':None,
'kind':'field',
- 'name':None,
+ 'name':_('Published'),
'search_terms':['pubdate'],
'is_custom':False,
'is_category':False}),
@@ -258,7 +259,7 @@ class FieldMetadata(dict):
'datatype':'float',
'is_multiple':None,
'kind':'field',
- 'name':None,
+ 'name':_('Size (MB)'),
'search_terms':['size'],
'is_custom':False,
'is_category':False}),
@@ -267,7 +268,7 @@ class FieldMetadata(dict):
'datatype':'datetime',
'is_multiple':None,
'kind':'field',
- 'name':None,
+ 'name':_('Date'),
'search_terms':['date'],
'is_custom':False,
'is_category':False}),
@@ -276,7 +277,7 @@ class FieldMetadata(dict):
'datatype':'text',
'is_multiple':None,
'kind':'field',
- 'name':None,
+ 'name':_('Title'),
'search_terms':['title'],
'is_custom':False,
'is_category':False}),
@@ -310,6 +311,10 @@ class FieldMetadata(dict):
self._tb_cats[k]['display'] = {}
self._tb_cats[k]['is_editable'] = True
self._add_search_terms_to_map(k, v['search_terms'])
+ self._tb_cats['timestamp']['display'] = {
+ 'date_format': tweaks['gui_timestamp_display_format']}
+ self._tb_cats['pubdate']['display'] = {
+ 'date_format': tweaks['gui_pubdate_display_format']}
self.custom_field_prefix = '#'
self.get = self._tb_cats.get
@@ -410,7 +415,7 @@ class FieldMetadata(dict):
if datatype == 'series':
key += '_index'
self._tb_cats[key] = {'table':None, 'column':None,
- 'datatype':'float', 'is_multiple':False,
+ 'datatype':'float', 'is_multiple':None,
'kind':'field', 'name':'',
'search_terms':[key], 'label':label+'_index',
'colnum':None, 'display':{},
From 2a654f3062401e864cdd357850033ad05937f34e Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Tue, 14 Sep 2010 07:41:06 +0100
Subject: [PATCH 034/289] Fix stupidity in collectiions_management where I
broke tag splitting
---
src/calibre/devices/usbms/books.py | 4 +++-
src/calibre/ebooks/metadata/book/base.py | 13 ++++++++-----
2 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index cf60f1311c..d25787fc89 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -141,7 +141,9 @@ class CollectionsBookList(BookList):
cust_field_meta = book.get_all_user_metadata(make_copy=False)
for attr in attrs:
attr = attr.strip()
- ign, val = book.format_field(attr, ignore_series_index=True)
+ ign, val = book.format_field(attr,
+ ignore_series_index=True,
+ return_multiples_as_list=True)
if not val: continue
if isbytestring(val):
val = val.decode(preferred_encoding, 'replace')
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 6e0351353f..7405f20a7c 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -343,7 +343,8 @@ class Metadata(object):
def format_rating(self):
return unicode(self.rating)
- def format_field(self, key, ignore_series_index=False):
+ def format_field(self, key, ignore_series_index=False,
+ return_multiples_as_list=False):
from calibre.ebooks.metadata import authors_to_string
'''
returns the tuple (field_name, formatted_value)
@@ -356,7 +357,8 @@ class Metadata(object):
name = unicode(cmeta['name'])
datatype = cmeta['datatype']
if datatype == 'text' and cmeta['is_multiple']:
- res = u', '.join(res)
+ if not return_multiples_as_list:
+ res = u', '.join(res)
elif datatype == 'series':
if not ignore_series_index:
res = res + \
@@ -365,7 +367,7 @@ class Metadata(object):
res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
elif datatype == 'bool':
res = _('Yes') if res else _('No')
- return (name, unicode(res))
+ return (name, res)
if key in field_metadata and field_metadata[key]['kind'] == 'field':
res = self.get(key, None)
@@ -377,13 +379,14 @@ class Metadata(object):
if key == 'authors':
res = authors_to_string(res)
elif datatype == 'text' and fmeta['is_multiple']:
- res = u', '.join(res)
+ if not return_multiples_as_list:
+ res = u', '.join(res)
elif datatype == 'series':
if not ignore_series_index:
res = res + ' [%s]'%self.format_series_index()
elif datatype == 'datetime':
res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy'))
- return (name, unicode(res))
+ return (name, res)
return (None, None)
From 6653fff4cd3228629c879e0e534024a9cc203fd2 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 15 Sep 2010 21:20:55 -0600
Subject: [PATCH 035/289] OPF serialization of user metadata
---
.../ebooks/metadata/book/json_codec.py | 2 +-
src/calibre/ebooks/metadata/opf2.py | 108 ++++++++++++++++--
2 files changed, 98 insertions(+), 12 deletions(-)
diff --git a/src/calibre/ebooks/metadata/book/json_codec.py b/src/calibre/ebooks/metadata/book/json_codec.py
index 51b9722803..2550089473 100644
--- a/src/calibre/ebooks/metadata/book/json_codec.py
+++ b/src/calibre/ebooks/metadata/book/json_codec.py
@@ -33,7 +33,7 @@ def encode_thumbnail(thumbnail):
'''
if thumbnail is None:
return None
- if not isinstance(thumbnail, tuple):
+ if not isinstance(thumbnail, (tuple, list)):
try:
img = Image()
img.load(thumbnail)
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index be8507f478..236b2fa18f 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
lxml based OPF parser.
'''
-import re, sys, unittest, functools, os, mimetypes, uuid, glob, cStringIO
+import re, sys, unittest, functools, os, mimetypes, uuid, glob, cStringIO, json
from urllib import unquote
from urlparse import urlparse
@@ -20,8 +20,9 @@ from calibre.ebooks.metadata import string_to_authors, MetaInformation
from calibre.ebooks.metadata.book.base import Metadata
from calibre.utils.date import parse_date, isoformat
from calibre.utils.localization import get_lang
+from calibre import prints
-class Resource(object):
+class Resource(object): # {{{
'''
Represents a resource (usually a file on the filesystem or a URL pointing
to the web. Such resources are commonly referred to in OPF files.
@@ -102,8 +103,9 @@ class Resource(object):
def __repr__(self):
return 'Resource(%s, %s)'%(repr(self.path), repr(self.href()))
+# }}}
-class ResourceCollection(object):
+class ResourceCollection(object): # {{{
def __init__(self):
self._resources = []
@@ -154,10 +156,9 @@ class ResourceCollection(object):
for res in self:
res.set_basedir(path)
+# }}}
-
-
-class ManifestItem(Resource):
+class ManifestItem(Resource): # {{{
@staticmethod
def from_opf_manifest_item(item, basedir):
@@ -195,8 +196,9 @@ class ManifestItem(Resource):
return self.media_type
raise IndexError('%d out of bounds.'%index)
+# }}}
-class Manifest(ResourceCollection):
+class Manifest(ResourceCollection): # {{{
@staticmethod
def from_opf_manifest_element(items, dir):
@@ -263,7 +265,9 @@ class Manifest(ResourceCollection):
if i.id == id:
return i.mime_type
-class Spine(ResourceCollection):
+# }}}
+
+class Spine(ResourceCollection): # {{{
class Item(Resource):
@@ -335,7 +339,9 @@ class Spine(ResourceCollection):
for i in self:
yield i.path
-class Guide(ResourceCollection):
+# }}}
+
+class Guide(ResourceCollection): # {{{
class Reference(Resource):
@@ -372,6 +378,7 @@ class Guide(ResourceCollection):
self[-1].type = type
self[-1].title = ''
+# }}}
class MetadataField(object):
@@ -413,7 +420,29 @@ class MetadataField(object):
elem = obj.create_metadata_element(self.name, is_dc=self.is_dc)
obj.set_text(elem, unicode(val))
-class OPF(object):
+
+def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8)):
+ from calibre.utils.config import to_json
+ from calibre.ebooks.metadata.book.json_codec import object_to_unicode
+
+ for name, fm in all_user_metadata.items():
+ try:
+ fm = object_to_unicode(fm)
+ fm = json.dumps(fm, default=to_json, ensure_ascii=False)
+ except:
+ prints('Failed to write user metadata:', name)
+ import traceback
+ traceback.print_exc()
+ continue
+ meta = metadata_elem.makeelement('meta')
+ meta.set('name', 'calibre:user_metadata:'+name)
+ meta.set('content', fm)
+ meta.tail = tail
+ metadata_elem.append(meta)
+
+
+class OPF(object): # {{{
+
MIMETYPE = 'application/oebps-package+xml'
PARSER = etree.XMLParser(recover=True)
NAMESPACES = {
@@ -498,6 +527,34 @@ class OPF(object):
self.guide = Guide.from_opf_guide(guide, basedir) if guide else None
self.cover_data = (None, None)
self.find_toc()
+ self.read_user_metadata()
+
+ def read_user_metadata(self):
+ self.user_metadata = {}
+ from calibre.utils.config import from_json
+ elems = self.root.xpath('//*[name() = "meta" and starts-with(@name,'
+ '"calibre:user_metadata:") and @content]')
+ for elem in elems:
+ name = elem.get('name')
+ name = ':'.join(name.split(':')[2:])
+ if not name or not name.startswith('#'):
+ continue
+ fm = elem.get('content')
+ try:
+ fm = json.loads(fm, object_hook=from_json)
+ except:
+ prints('Failed to read user metadata:', name)
+ import traceback
+ traceback.print_exc()
+ continue
+ self.user_metadata[name] = fm
+
+
+ def write_user_metadata(self):
+ for elem in self.user_metadata_path(self.root):
+ elem.getparent().remove(elem)
+ serialize_user_metadata(self.metadata,
+ self.user_metadata)
def find_toc(self):
self.toc = None
@@ -912,6 +969,7 @@ class OPF(object):
return elem
def render(self, encoding='utf-8'):
+ self.write_user_metadata()
raw = etree.tostring(self.root, encoding=encoding, pretty_print=True)
if not raw.lstrip().startswith('\n'%encoding.upper()+raw
@@ -926,6 +984,7 @@ class OPF(object):
if val is not None and val != [] and val != (None, None):
setattr(self, attr, val)
+# }}}
class OPFCreator(Metadata):
@@ -1116,6 +1175,8 @@ class OPFCreator(Metadata):
item.set('title', ref.title)
guide.append(item)
+ serialize_user_metadata(metadata, self.get_all_user_metadata(False))
+
root = E.package(
metadata,
manifest,
@@ -1218,6 +1279,8 @@ def metadata_to_opf(mi, as_string=True):
if mi.title_sort:
meta('title_sort', mi.title_sort)
+ serialize_user_metadata(metadata, mi.get_all_user_metadata(False))
+
metadata[-1].tail = '\n' +(' '*4)
if mi.cover:
@@ -1335,5 +1398,28 @@ def suite():
def test():
unittest.TextTestRunner(verbosity=2).run(suite())
+def test_user_metadata():
+ from cStringIO import StringIO
+ mi = Metadata('Test title', ['test author1', 'test author2'])
+ um = {
+ '#myseries': { '#value#': u'test series\xe4', 'datatype':'text',
+ 'is_multiple': False, 'name': u'My Series'},
+ '#myseries_index': { '#value#': 2.45, 'datatype': 'float',
+ 'is_multiple': False}
+ }
+ mi.set_all_user_metadata(um)
+ raw = metadata_to_opf(mi)
+ opfc = OPFCreator(os.getcwd(), other=mi)
+ out = StringIO()
+ opfc.render(out)
+ raw2 = out.getvalue()
+ f = StringIO(raw)
+ opf = OPF(f)
+ f2 = StringIO(raw2)
+ opf2 = OPF(f2)
+ assert um == opf.user_metadata
+ assert um == opf2.user_metadata
+ print raw
+
if __name__ == '__main__':
- test()
+ test_user_metadata()
From 420db7851b8650ae7e61f2b441b9a7822dddbd8b Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 15 Sep 2010 21:50:06 -0600
Subject: [PATCH 036/289] Fix use of OPF class to generate a Metadata object
and have OPF.smart_update also update user metadata
---
src/calibre/customize/builtins.py | 3 +--
src/calibre/ebooks/conversion/plumber.py | 2 +-
src/calibre/ebooks/metadata/cli.py | 2 +-
src/calibre/ebooks/metadata/epub.py | 2 +-
src/calibre/ebooks/metadata/lit.py | 3 +--
src/calibre/ebooks/metadata/meta.py | 2 +-
src/calibre/ebooks/metadata/opf2.py | 24 +++++++++++++++++-------
src/calibre/ebooks/mobi/reader.py | 2 +-
src/calibre/ebooks/oeb/reader.py | 3 +--
src/calibre/gui2/add.py | 2 +-
src/calibre/library/cli.py | 2 +-
11 files changed, 27 insertions(+), 20 deletions(-)
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index 68df832048..1ddb2843a1 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -226,8 +226,7 @@ class OPFMetadataReader(MetadataReaderPlugin):
def get_metadata(self, stream, ftype):
from calibre.ebooks.metadata.opf2 import OPF
- from calibre.ebooks.metadata import MetaInformation
- return MetaInformation(OPF(stream, os.getcwd()))
+ return OPF(stream, os.getcwd()).to_book_metadata()
class PDBMetadataReader(MetadataReaderPlugin):
diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py
index 16282dd28d..38e47f6bf7 100644
--- a/src/calibre/ebooks/conversion/plumber.py
+++ b/src/calibre/ebooks/conversion/plumber.py
@@ -692,7 +692,7 @@ OptionRecommendation(name='timestamp',
self.opts.read_metadata_from_opf)
opf = OPF(open(self.opts.read_metadata_from_opf, 'rb'),
os.path.dirname(self.opts.read_metadata_from_opf))
- mi = MetaInformation(opf)
+ mi = opf.to_book_metadata()
self.opts_to_mi(mi)
if mi.cover:
if mi.cover.startswith('http:') or mi.cover.startswith('https:'):
diff --git a/src/calibre/ebooks/metadata/cli.py b/src/calibre/ebooks/metadata/cli.py
index 780d3febcf..a0be187512 100644
--- a/src/calibre/ebooks/metadata/cli.py
+++ b/src/calibre/ebooks/metadata/cli.py
@@ -109,7 +109,7 @@ def do_set_metadata(opts, mi, stream, stream_type):
from_opf = getattr(opts, 'from_opf', None)
if from_opf is not None:
from calibre.ebooks.metadata.opf2 import OPF
- opf_mi = MetaInformation(OPF(open(from_opf, 'rb')))
+ opf_mi = OPF(open(from_opf, 'rb')).to_book_metadata()
mi.smart_update(opf_mi)
for pref in config().option_set.preferences:
diff --git a/src/calibre/ebooks/metadata/epub.py b/src/calibre/ebooks/metadata/epub.py
index ac6b5feebe..8984a252a3 100644
--- a/src/calibre/ebooks/metadata/epub.py
+++ b/src/calibre/ebooks/metadata/epub.py
@@ -167,7 +167,7 @@ def get_metadata(stream, extract_cover=True):
""" Return metadata as a :class:`Metadata` object """
stream.seek(0)
reader = OCFZipReader(stream)
- mi = MetaInformation(reader.opf)
+ mi = reader.opf.to_book_metadata()
if extract_cover:
try:
cdata = get_cover(reader.opf, reader.opf_path, stream, reader=reader)
diff --git a/src/calibre/ebooks/metadata/lit.py b/src/calibre/ebooks/metadata/lit.py
index 1a267b6858..3be1f22632 100644
--- a/src/calibre/ebooks/metadata/lit.py
+++ b/src/calibre/ebooks/metadata/lit.py
@@ -6,7 +6,6 @@ Support for reading the metadata from a LIT file.
import cStringIO, os
-from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf2 import OPF
def get_metadata(stream):
@@ -16,7 +15,7 @@ def get_metadata(stream):
src = litfile.get_metadata().encode('utf-8')
litfile = litfile._litfile
opf = OPF(cStringIO.StringIO(src), os.getcwd())
- mi = MetaInformation(opf)
+ mi = opf.to_book_metadata()
covers = []
for item in opf.iterguide():
if 'cover' not in item.get('type', '').lower():
diff --git a/src/calibre/ebooks/metadata/meta.py b/src/calibre/ebooks/metadata/meta.py
index eae8171362..68deca5e10 100644
--- a/src/calibre/ebooks/metadata/meta.py
+++ b/src/calibre/ebooks/metadata/meta.py
@@ -194,7 +194,7 @@ def opf_metadata(opfpath):
try:
opf = OPF(f, os.path.dirname(opfpath))
if opf.application_id is not None:
- mi = MetaInformation(opf)
+ mi = opf.to_book_metadata()
if hasattr(opf, 'cover') and opf.cover:
cpath = os.path.join(os.path.dirname(opfpath), opf.cover)
if os.access(cpath, os.R_OK):
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index 236b2fa18f..96f1fa4832 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -530,7 +530,7 @@ class OPF(object): # {{{
self.read_user_metadata()
def read_user_metadata(self):
- self.user_metadata = {}
+ self._user_metadata_ = {}
from calibre.utils.config import from_json
elems = self.root.xpath('//*[name() = "meta" and starts-with(@name,'
'"calibre:user_metadata:") and @content]')
@@ -547,14 +547,21 @@ class OPF(object): # {{{
import traceback
traceback.print_exc()
continue
- self.user_metadata[name] = fm
+ self._user_metadata_[name] = fm
+ def to_book_metadata(self):
+ ans = MetaInformation(self)
+ for n, v in self._user_metadata_.items():
+ ans.set_user_metadata(n, v)
+ return ans
def write_user_metadata(self):
- for elem in self.user_metadata_path(self.root):
+ elems = self.root.xpath('//*[name() = "meta" and starts-with(@name,'
+ '"calibre:user_metadata:") and @content]')
+ for elem in elems:
elem.getparent().remove(elem)
serialize_user_metadata(self.metadata,
- self.user_metadata)
+ self._user_metadata_)
def find_toc(self):
self.toc = None
@@ -983,6 +990,9 @@ class OPF(object): # {{{
val = getattr(mi, attr, None)
if val is not None and val != [] and val != (None, None):
setattr(self, attr, val)
+ temp = self.to_book_metadata()
+ temp.smart_update(mi, replace_metadata=replace_metadata)
+ self._user_metadata_ = temp.get_all_user_metadata(True)
# }}}
@@ -1417,9 +1427,9 @@ def test_user_metadata():
opf = OPF(f)
f2 = StringIO(raw2)
opf2 = OPF(f2)
- assert um == opf.user_metadata
- assert um == opf2.user_metadata
- print raw
+ assert um == opf._user_metadata_
+ assert um == opf2._user_metadata_
+ print opf.render()
if __name__ == '__main__':
test_user_metadata()
diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py
index 2a35c7cb45..6a44c2aa77 100644
--- a/src/calibre/ebooks/mobi/reader.py
+++ b/src/calibre/ebooks/mobi/reader.py
@@ -441,7 +441,7 @@ class MobiReader(object):
html.tostring(elem, encoding='utf-8') + ''
stream = cStringIO.StringIO(raw)
opf = OPF(stream)
- self.embedded_mi = MetaInformation(opf)
+ self.embedded_mi = opf.to_book_metadata()
if guide is not None:
for ref in guide.xpath('descendant::reference'):
if 'cover' in ref.get('type', '').lower():
diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py
index d7d7bbf725..559421326c 100644
--- a/src/calibre/ebooks/oeb/reader.py
+++ b/src/calibre/ebooks/oeb/reader.py
@@ -126,10 +126,9 @@ class OEBReader(object):
def _metadata_from_opf(self, opf):
from calibre.ebooks.metadata.opf2 import OPF
- from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.oeb.transforms.metadata import meta_info_to_oeb_metadata
stream = cStringIO.StringIO(etree.tostring(opf))
- mi = MetaInformation(OPF(stream))
+ mi = OPF(stream).to_book_metadata()
if not mi.language:
mi.language = get_lang().replace('_', '-')
self.oeb.metadata.add('language', mi.language)
diff --git a/src/calibre/gui2/add.py b/src/calibre/gui2/add.py
index 5b9fb35be3..9f246aeb93 100644
--- a/src/calibre/gui2/add.py
+++ b/src/calibre/gui2/add.py
@@ -138,7 +138,7 @@ class DBAdder(Thread): # {{{
self.critical[name] = open(opf, 'rb').read().decode('utf-8', 'replace')
else:
try:
- mi = MetaInformation(OPF(opf))
+ mi = OPF(opf).to_book_metadata()
except:
import traceback
mi = MetaInformation('', [_('Unknown')])
diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py
index 9a2d0b0a62..cd4e472807 100644
--- a/src/calibre/library/cli.py
+++ b/src/calibre/library/cli.py
@@ -448,7 +448,7 @@ def command_show_metadata(args, dbpath):
return 0
def do_set_metadata(db, id, stream):
- mi = OPF(stream)
+ mi = OPF(stream).to_book_metadata()
db.set_metadata(id, mi)
db.clean()
do_show_metadata(db, id, False)
From 4bc7aa1b710e00bdefdd82bed915c8a977b2523e Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 15 Sep 2010 21:56:02 -0600
Subject: [PATCH 037/289] More robust reading of user metadata from OPF
---
src/calibre/ebooks/metadata/opf2.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index 96f1fa4832..ecbef3194d 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -531,6 +531,7 @@ class OPF(object): # {{{
def read_user_metadata(self):
self._user_metadata_ = {}
+ temp = Metadata('x', ['x'])
from calibre.utils.config import from_json
elems = self.root.xpath('//*[name() = "meta" and starts-with(@name,'
'"calibre:user_metadata:") and @content]')
@@ -542,12 +543,13 @@ class OPF(object): # {{{
fm = elem.get('content')
try:
fm = json.loads(fm, object_hook=from_json)
+ temp.set_user_metadata(name, fm)
except:
prints('Failed to read user metadata:', name)
import traceback
traceback.print_exc()
continue
- self._user_metadata_[name] = fm
+ self._user_metadata_ = temp.get_all_user_metadata(True)
def to_book_metadata(self):
ans = MetaInformation(self)
From 56023722709d35816424cd3c77f5afe972103d0c Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Thu, 16 Sep 2010 12:24:56 +0100
Subject: [PATCH 038/289] Minor changes to OPF testing
---
src/calibre/ebooks/metadata/opf2.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index ecbef3194d..8a4ff6a5bd 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -1415,9 +1415,11 @@ def test_user_metadata():
mi = Metadata('Test title', ['test author1', 'test author2'])
um = {
'#myseries': { '#value#': u'test series\xe4', 'datatype':'text',
- 'is_multiple': False, 'name': u'My Series'},
+ 'is_multiple': None, 'name': u'My Series'},
'#myseries_index': { '#value#': 2.45, 'datatype': 'float',
- 'is_multiple': False}
+ 'is_multiple': None},
+ '#mytags': {'#value#':['t1','t2','t3'], 'datatype':'text',
+ 'is_multiple': '|', 'name': u'My Tags'}
}
mi.set_all_user_metadata(um)
raw = metadata_to_opf(mi)
From e1dd08acef1b14c73a3da542f974acd9a10a1f79 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Thu, 16 Sep 2010 14:02:49 +0100
Subject: [PATCH 039/289] Several changes: 1) Add an option to specify the time
format when sending to device. This is the analog of the same option that
already exists for save to disk. 2) refactor the format_field code. Remove
the special parameters on format_field. Add format_field_extended that
returns a 4-element tuple (name, formatted val, original val, field
metadata). 3) change (simplify) usbms collections management to use new
format_field_extended method. 4) change device.py to not call sync_booklists
twice. 5) add the fix for gui-not-updating, in hopes that we can avoid merge
conflicts
---
src/calibre/devices/usbms/books.py | 21 +++++++--------
src/calibre/devices/usbms/device.py | 4 ++-
src/calibre/ebooks/metadata/book/base.py | 33 ++++++++++++------------
src/calibre/gui2/actions/add.py | 2 +-
src/calibre/gui2/device.py | 28 ++++++++++++--------
src/calibre/gui2/preferences/sending.py | 3 +++
src/calibre/gui2/preferences/sending.ui | 15 ++++++++++-
src/calibre/library/save_to_disk.py | 3 +++
8 files changed, 68 insertions(+), 41 deletions(-)
diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index d25787fc89..eab625f7be 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -94,12 +94,12 @@ class CollectionsBookList(BookList):
def supports_collections(self):
return True
- def compute_category_name(self, attr, category, cust_field_meta):
+ def compute_category_name(self, attr, category, field_meta):
renames = tweaks['sony_collection_renaming_rules']
attr_name = renames.get(attr, None)
if attr_name is None:
- if attr in cust_field_meta:
- attr_name = '(%s)'%cust_field_meta[attr]['name']
+ if field_meta['is_custom']:
+ attr_name = '(%s)'%field_meta['name']
else:
attr_name = ''
elif attr_name != '':
@@ -138,23 +138,23 @@ class CollectionsBookList(BookList):
# specified 'on_connect'
attrs = collection_attributes
meta_vals = book.get_all_non_none_attributes()
- cust_field_meta = book.get_all_user_metadata(make_copy=False)
for attr in attrs:
attr = attr.strip()
- ign, val = book.format_field(attr,
- ignore_series_index=True,
- return_multiples_as_list=True)
+ ign, val, orig_val, fm = book.format_field_extended(attr)
if not val: continue
if isbytestring(val):
val = val.decode(preferred_encoding, 'replace')
if isinstance(val, (list, tuple)):
val = list(val)
+ elif fm['datatype'] == 'series':
+ val = [orig_val]
+ elif fm['datatype'] == 'text' and fm['is_multiple']:
+ val = orig_val
else:
val = [val]
for category in val:
is_series = False
- if attr in cust_field_meta: # is a custom field
- fm = cust_field_meta[attr]
+ if fm['is_custom']: # is a custom field
if fm['datatype'] == 'text' and len(category) > 1 and \
category[0] == '[' and category[-1] == ']':
continue
@@ -168,8 +168,7 @@ class CollectionsBookList(BookList):
('series' in collection_attributes and
meta_vals.get('series', None) == category):
is_series = True
- cat_name = self.compute_category_name(attr, category,
- cust_field_meta)
+ cat_name = self.compute_category_name(attr, category, fm)
if cat_name not in collections:
collections[cat_name] = []
collections_lpaths[cat_name] = set()
diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py
index b954911242..928d00ad4a 100644
--- a/src/calibre/devices/usbms/device.py
+++ b/src/calibre/devices/usbms/device.py
@@ -829,12 +829,14 @@ class Device(DeviceConfig, DevicePlugin):
ext = os.path.splitext(fname)[1]
from calibre.library.save_to_disk import get_components
+ from calibre.library.save_to_disk import config
+ opts = config().parse()
if not isinstance(template, unicode):
template = template.decode('utf-8')
app_id = str(getattr(mdata, 'application_id', ''))
# The db id will be in the created filename
extra_components = get_components(template, mdata, fname,
- length=250-len(app_id)-1)
+ timefmt=opts.send_timefmt, length=250-len(app_id)-1)
if not extra_components:
extra_components.append(sanitize(self.filename_callback(fname,
mdata)))
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 7405f20a7c..b252f518da 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -343,8 +343,11 @@ class Metadata(object):
def format_rating(self):
return unicode(self.rating)
- def format_field(self, key, ignore_series_index=False,
- return_multiples_as_list=False):
+ def format_field(self, key):
+ name, val, ign, ign = self.format_field_extended(key)
+ return (name, val)
+
+ def format_field_extended(self, key):
from calibre.ebooks.metadata import authors_to_string
'''
returns the tuple (field_name, formatted_value)
@@ -352,43 +355,41 @@ class Metadata(object):
if key in self.user_metadata_keys:
res = self.get(key, None)
if res is None or res == '':
- return (None, None)
+ return (None, None, None, None)
+ orig_res = res
cmeta = self.get_user_metadata(key, make_copy=False)
name = unicode(cmeta['name'])
datatype = cmeta['datatype']
if datatype == 'text' and cmeta['is_multiple']:
- if not return_multiples_as_list:
- res = u', '.join(res)
+ res = u', '.join(res)
elif datatype == 'series':
- if not ignore_series_index:
- res = res + \
- ' [%s]'%self.format_series_index(val=self.get_extra(key))
+ res = res + \
+ ' [%s]'%self.format_series_index(val=self.get_extra(key))
elif datatype == 'datetime':
res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
elif datatype == 'bool':
res = _('Yes') if res else _('No')
- return (name, res)
+ return (name, res, orig_res, cmeta)
if key in field_metadata and field_metadata[key]['kind'] == 'field':
res = self.get(key, None)
if res is None or res == '':
- return (None, None)
+ return (None, None, None, None)
+ orig_res = res
fmeta = field_metadata[key]
name = unicode(fmeta['name'])
datatype = fmeta['datatype']
if key == 'authors':
res = authors_to_string(res)
elif datatype == 'text' and fmeta['is_multiple']:
- if not return_multiples_as_list:
- res = u', '.join(res)
+ res = u', '.join(res)
elif datatype == 'series':
- if not ignore_series_index:
- res = res + ' [%s]'%self.format_series_index()
+ res = res + ' [%s]'%self.format_series_index()
elif datatype == 'datetime':
res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy'))
- return (name, res)
+ return (name, res, orig_res, fmeta)
- return (None, None)
+ return (None, None, None, None)
def __unicode__(self):
from calibre.ebooks.metadata import authors_to_string
diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py
index add7bf1d5b..aa20b8bc16 100644
--- a/src/calibre/gui2/actions/add.py
+++ b/src/calibre/gui2/actions/add.py
@@ -230,7 +230,7 @@ class AddAction(InterfaceAction):
self._files_added(paths, names, infos, on_card=on_card)
# set the in-library flags, and as a consequence send the library's
# metadata for this book to the device. This sets the uuid to the
- # correct value.
+ # correct value. Note that set_books_in_library might sync_booklists
self.gui.set_books_in_library(booklists=[model.db], reset=True)
model.reset()
diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py
index f839e1d519..196e97f2a3 100644
--- a/src/calibre/gui2/device.py
+++ b/src/calibre/gui2/device.py
@@ -745,6 +745,7 @@ class DeviceMixin(object): # {{{
if job.failed:
self.device_job_exception(job)
return
+ # set_books_in_library might schedule a sync_booklists job
self.set_books_in_library(job.result, reset=True)
mainlist, cardalist, cardblist = job.result
self.memory_view.set_database(mainlist)
@@ -789,11 +790,12 @@ class DeviceMixin(object): # {{{
self.device_manager.remove_books_from_metadata(paths,
self.booklists())
model.paths_deleted(paths)
- self.upload_booklists()
# Force recomputation the library's ondevice info. We need to call
# set_books_in_library even though books were not added because
- # the deleted book might have been an exact match.
- self.set_books_in_library(self.booklists(), reset=True)
+ # the deleted book might have been an exact match. Upload the booklists
+ # if set_books_in_library did not.
+ if not self.set_books_in_library(self.booklists(), reset=True):
+ self.upload_booklists()
self.book_on_device(None, None, reset=True)
# We need to reset the ondevice flags in the library. Use a big hammer,
# so we don't need to worry about whether some succeeded or not.
@@ -1280,8 +1282,6 @@ class DeviceMixin(object): # {{{
self.device_manager.add_books_to_metadata(job.result,
metadata, self.booklists())
- self.upload_booklists()
-
books_to_be_deleted = []
if memory and memory[1]:
books_to_be_deleted = memory[1]
@@ -1291,12 +1291,15 @@ class DeviceMixin(object): # {{{
# book already there with a different book. This happens frequently in
# news. When this happens, the book match indication will be wrong
# because the UUID changed. Force both the device and the library view
- # to refresh the flags.
- self.set_books_in_library(self.booklists(), reset=True)
+ # to refresh the flags. Set_books_in_library could upload the booklists.
+ # If it does not, then do it here.
+ if not self.set_books_in_library(self.booklists(), reset=True):
+ self.upload_booklists()
self.book_on_device(None, reset=True)
self.refresh_ondevice_info(device_connected = True)
- view = self.card_a_view if on_card == 'carda' else self.card_b_view if on_card == 'cardb' else self.memory_view
+ view = self.card_a_view if on_card == 'carda' else \
+ self.card_b_view if on_card == 'cardb' else self.memory_view
view.model().resort(reset=False)
view.model().research()
for f in files:
@@ -1371,7 +1374,7 @@ class DeviceMixin(object): # {{{
try:
db = self.library_view.model().db
except:
- return
+ return False
# Build a cache (map) of the library, so the search isn't On**2
self.db_book_title_cache = {}
self.db_book_uuid_cache = {}
@@ -1466,10 +1469,13 @@ class DeviceMixin(object): # {{{
# Set author_sort if it isn't already
asort = getattr(book, 'author_sort', None)
if not asort and book.authors:
- book.author_sort = self.library_view.model().db.author_sort_from_authors(book.authors)
+ book.author_sort = self.library_view.model().db.\
+ author_sort_from_authors(book.authors)
if update_metadata:
if self.device_manager.is_device_connected:
- self.device_manager.sync_booklists(None, booklists)
+ self.device_manager.sync_booklists(
+ Dispatcher(self.metadata_synced), booklists)
+ return update_metadata
# }}}
diff --git a/src/calibre/gui2/preferences/sending.py b/src/calibre/gui2/preferences/sending.py
index 748c6b2a2d..ac4abbcf41 100644
--- a/src/calibre/gui2/preferences/sending.py
+++ b/src/calibre/gui2/preferences/sending.py
@@ -22,6 +22,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r = self.register
+ for x in ('send_timefmt',):
+ r(x, self.proxy)
+
choices = [(_('Manual management'), 'manual'),
(_('Only on send'), 'on_send'),
(_('Automatic management'), 'on_connect')]
diff --git a/src/calibre/gui2/preferences/sending.ui b/src/calibre/gui2/preferences/sending.ui
index e064646afd..b9d1d1e1d2 100644
--- a/src/calibre/gui2/preferences/sending.ui
+++ b/src/calibre/gui2/preferences/sending.ui
@@ -80,7 +80,20 @@
- -
+
-
+
+
+ Format &dates as:
+
+
+ opt_send_timefmt
+
+
+
+ -
+
+
+ -
Here you can control how calibre will save your books when you click the Send to Device button. This setting can be overriden for individual devices by customizing the device interface plugins in Preferences->Advanced->Plugins
diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py
index 3fa40c68b2..71850abcd5 100644
--- a/src/calibre/library/save_to_disk.py
+++ b/src/calibre/library/save_to_disk.py
@@ -84,6 +84,9 @@ def config(defaults=None):
x('timefmt', default='%b, %Y',
help=_('The format in which to display dates. %d - day, %b - month, '
'%Y - year. Default is: %b, %Y'))
+ x('send_timefmt', default='%b, %Y',
+ help=_('The format in which to display dates. %d - day, %b - month, '
+ '%Y - year. Default is: %b, %Y'))
x('to_lowercase', default=False,
help=_('Convert paths to lowercase.'))
x('replace_whitespace', default=False,
From 4645138a67193537ba3e91fc3e4942017d72e1de Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Thu, 16 Sep 2010 16:03:07 +0100
Subject: [PATCH 040/289] 1) Re-enable syntactic validation of save templates.
2) fix row numbering on send_to_device preferences ui template.
---
src/calibre/gui2/preferences/save_template.py | 37 +++++++++++--------
src/calibre/gui2/preferences/sending.ui | 2 +-
2 files changed, 22 insertions(+), 17 deletions(-)
diff --git a/src/calibre/gui2/preferences/save_template.py b/src/calibre/gui2/preferences/save_template.py
index 26dc02f259..0dbee5bf21 100644
--- a/src/calibre/gui2/preferences/save_template.py
+++ b/src/calibre/gui2/preferences/save_template.py
@@ -8,8 +8,10 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QWidget, pyqtSignal
+from calibre.gui2 import error_dialog
from calibre.gui2.preferences.save_template_ui import Ui_Form
-from calibre.library.save_to_disk import FORMAT_ARG_DESCS
+from calibre.library.save_to_disk import FORMAT_ARG_DESCS, preprocess_template,\
+ safe_format
class SaveTemplate(QWidget, Ui_Form):
@@ -24,8 +26,11 @@ class SaveTemplate(QWidget, Ui_Form):
variables = sorted(FORMAT_ARG_DESCS.keys())
rows = []
for var in variables:
- rows.append(u'
%s | %s |
'%
+ rows.append(u'%s | | %s |
'%
(var, FORMAT_ARG_DESCS[var]))
+ rows.append(u'%s | | %s |
'%(
+ _('Any custom field'),
+ _('The lookup name of any custom field. These names begin with "#")')))
table = u''%(u'\n'.join(rows))
self.template_variables.setText(table)
@@ -39,21 +44,21 @@ class SaveTemplate(QWidget, Ui_Form):
self.changed_signal.emit()
def validate(self):
- # TODO: NEWMETA: I haven't figured out how to get the custom columns
- # into here, so for the moment make all templates valid.
+ '''
+ Do a syntax check on the format string. Doing a semantic check
+ (verifying that the fields exist) is not useful in the presence of
+ custom fields, because they may or may not exist.
+ '''
+ tmpl = preprocess_template(self.opt_template.text())
+ fa = {}
+ try:
+ safe_format(tmpl, fa)
+ except Exception, err:
+ error_dialog(self, _('Invalid template'),
+ ''+_('The template %s is invalid:')%tmpl + \
+ '
'+str(err), show=True)
+ return False
return True
-# tmpl = preprocess_template(self.opt_template.text())
-# fa = {}
-# for x in FORMAT_ARG_DESCS.keys():
-# fa[x]='random long string'
-# try:
-# tmpl.format(**fa)
-# except Exception, err:
-# error_dialog(self, _('Invalid template'),
-# '
'+_('The template %s is invalid:')%tmpl + \
-# '
'+str(err), show=True)
-# return False
-# return True
def set_value(self, val):
self.opt_template.set_value(val)
diff --git a/src/calibre/gui2/preferences/sending.ui b/src/calibre/gui2/preferences/sending.ui
index b9d1d1e1d2..75b1899a3a 100644
--- a/src/calibre/gui2/preferences/sending.ui
+++ b/src/calibre/gui2/preferences/sending.ui
@@ -103,7 +103,7 @@
- -
+
-
From d147775dc0fbbcc581f89a28921f6d856a46347f Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Sat, 18 Sep 2010 04:50:54 +0000
Subject: [PATCH 041/289] Launchpad automatic translations update.
---
src/calibre/translations/ar.po | 1560 +++++++++++++------------
src/calibre/translations/ca.po | 1632 ++++++++++++++------------
src/calibre/translations/cs.po | 1546 +++++++++++++------------
src/calibre/translations/da.po | 1608 ++++++++++++++------------
src/calibre/translations/de.po | 1616 ++++++++++++++------------
src/calibre/translations/es.po | 1612 ++++++++++++++------------
src/calibre/translations/eu.po | 1612 ++++++++++++++------------
src/calibre/translations/fr.po | 1618 ++++++++++++++------------
src/calibre/translations/it.po | 1612 ++++++++++++++------------
src/calibre/translations/ja.po | 1547 +++++++++++++------------
src/calibre/translations/ko.po | 1586 ++++++++++++++------------
src/calibre/translations/nb.po | 1608 ++++++++++++++------------
src/calibre/translations/pt_BR.po | 1768 ++++++++++++++++-------------
src/calibre/translations/ro.po | 1546 +++++++++++++------------
src/calibre/translations/sk.po | 1559 +++++++++++++------------
src/calibre/translations/sr.po | 1651 +++++++++++++++------------
src/calibre/translations/sv.po | 1618 ++++++++++++++------------
src/calibre/translations/vi.po | 1594 ++++++++++++++------------
18 files changed, 15830 insertions(+), 13063 deletions(-)
diff --git a/src/calibre/translations/ar.po b/src/calibre/translations/ar.po
index e6ff4dfc3e..d0b5eb08f9 100644
--- a/src/calibre/translations/ar.po
+++ b/src/calibre/translations/ar.po
@@ -7,14 +7,14 @@ msgid ""
msgstr ""
"Project-Id-Version: calibre\n"
"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2010-09-10 19:51+0000\n"
-"PO-Revision-Date: 2010-09-16 13:31+0000\n"
+"POT-Creation-Date: 2010-09-17 21:00+0000\n"
+"PO-Revision-Date: 2010-09-17 22:37+0000\n"
"Last-Translator: Hsn \n"
"Language-Team: Arabic \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2010-09-17 04:48+0000\n"
+"X-Launchpad-Export-Date: 2010-09-18 04:47+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#: /home/kovid/work/calibre/src/calibre/customize/__init__.py:43
@@ -24,7 +24,7 @@ msgstr "لا يفعل شيءً"
#: /home/kovid/work/calibre/src/calibre/customize/__init__.py:46
#: /home/kovid/work/calibre/src/calibre/devices/jetbook/driver.py:74
#: /home/kovid/work/calibre/src/calibre/devices/kindle/driver.py:76
-#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:395
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:410
#: /home/kovid/work/calibre/src/calibre/devices/nook/driver.py:70
#: /home/kovid/work/calibre/src/calibre/devices/nook/driver.py:71
#: /home/kovid/work/calibre/src/calibre/devices/prs500/books.py:267
@@ -78,7 +78,9 @@ msgstr "لا يفعل شيءً"
#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:982
#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/reader.py:137
#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/reader.py:139
-#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/jacket.py:108
+#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/jacket.py:64
+#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/jacket.py:112
+#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/jacket.py:118
#: /home/kovid/work/calibre/src/calibre/ebooks/pdb/ereader/writer.py:173
#: /home/kovid/work/calibre/src/calibre/ebooks/pdb/ereader/writer.py:174
#: /home/kovid/work/calibre/src/calibre/ebooks/pdb/input.py:39
@@ -101,43 +103,43 @@ msgstr "لا يفعل شيءً"
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/split.py:82
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/writer.py:97
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/writer.py:98
-#: /home/kovid/work/calibre/src/calibre/ebooks/rtf/input.py:247
-#: /home/kovid/work/calibre/src/calibre/ebooks/rtf/input.py:249
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:323
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:330
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:290
+#: /home/kovid/work/calibre/src/calibre/ebooks/rtf/input.py:239
+#: /home/kovid/work/calibre/src/calibre/ebooks/rtf/input.py:241
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:324
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:331
#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:293
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:296
#: /home/kovid/work/calibre/src/calibre/gui2/add.py:137
#: /home/kovid/work/calibre/src/calibre/gui2/add.py:144
#: /home/kovid/work/calibre/src/calibre/gui2/convert/__init__.py:42
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:111
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:136
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:138
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:864
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:873
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1157
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1160
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:865
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:874
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1158
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1161
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf.py:47
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:120
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:155
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:521
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:552
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:173
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:363
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:383
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:883
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1061
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:357
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:377
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:877
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1062
#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:91
#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:96
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:187
#: /home/kovid/work/calibre/src/calibre/library/cli.py:213
#: /home/kovid/work/calibre/src/calibre/library/database.py:913
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:374
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:386
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:1057
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:1126
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:1824
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:1826
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:1953
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:375
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:387
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:1065
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:1137
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:1837
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:1839
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:1966
#: /home/kovid/work/calibre/src/calibre/library/server/mobile.py:211
#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:137
#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:140
@@ -258,162 +260,162 @@ msgstr "ضبط دليل المعلومات في الملفات %s"
msgid "Set metadata from %s files"
msgstr "ضبط دليل المعلومات من ملفات %s"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:683
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:684
msgid "Look and Feel"
msgstr "المظهر"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:685
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:697
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:708
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:719
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:686
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:698
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:709
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:720
msgid "Interface"
msgstr "الواجهة"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:689
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:690
msgid "Adjust the look and feel of the calibre interface to suit your tastes"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:695
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:696
msgid "Behavior"
msgstr "سلوك"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:701
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:702
msgid "Change the way calibre behaves"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:706
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:707
#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:176
msgid "Add your own columns"
msgstr "اضف عامودك الخاص"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:712
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:713
msgid "Add/remove your own columns to the calibre book list"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:717
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:718
msgid "Customize the toolbar"
msgstr "خصِّص شريط الأدوات"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:723
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:724
msgid ""
"Customize the toolbars and context menus, changing which actions are "
"available in each"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:729
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:730
msgid "Input Options"
-msgstr ""
+msgstr "خيارات الإدخال"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:731
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:742
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:753
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:732
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:743
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:754
msgid "Conversion"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:735
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:736
msgid "Set conversion options specific to each input format"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:740
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:741
msgid "Common Options"
msgstr "خيارات متداولة"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:746
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:747
msgid "Set conversion options common to all formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:751
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:752
msgid "Output Options"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:757
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:758
msgid "Set conversion options specific to each output format"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:762
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:763
msgid "Adding books"
msgstr "إضافة كتب"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:764
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:776
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:788
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:765
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:777
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:789
msgid "Import/Export"
msgstr "إستيراد/تصدير"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:768
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:769
msgid "Control how calibre reads metadata from files when adding books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:774
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:775
msgid "Saving books to disk"
msgstr "حفظ الكتب على القرص"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:780
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:781
msgid ""
"Control how calibre exports files from its database to disk when using Save "
"to disk"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:786
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:787
msgid "Sending books to devices"
msgstr "ارسال الكتب الى الاجهزة"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:792
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:793
msgid "Control how calibre transfers files to your ebook reader"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:798
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:799
msgid "Sharing books by email"
msgstr "مشاركة الكتب عبر البريد الالكتروني"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:800
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:812
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:801
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:813
msgid "Sharing"
msgstr "مشاركة"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:804
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:805
msgid ""
"Setup sharing of books via email. Can be used for automatic sending of "
"downloaded news to your devices"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:810
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:811
msgid "Sharing over the net"
msgstr "المشاركة على الشبكة العنكبوتية"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:816
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:817
msgid ""
"Setup the calibre Content Server which will give you access to your calibre "
"library from anywhere, on any device, over the internet"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:823
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:824
msgid "Plugins"
msgstr "الملحقات"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:825
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:837
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:848
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:826
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:838
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:849
msgid "Advanced"
msgstr "متقدّم"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:829
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:830
msgid "Add/remove/customize various bits of calibre functionality"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:835
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:836
msgid "Tweaks"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:841
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:842
msgid "Fine tune how calibre behaves in various contexts"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:846
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:847
msgid "Miscellaneous"
msgstr "متفرقات"
-#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:852
+#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:853
msgid "Miscellaneous advanced configuration"
msgstr ""
@@ -457,7 +459,7 @@ msgstr ""
"وثيقة الإدخال."
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:57
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:414
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:418
msgid ""
"This profile is intended for the SONY PRS line. The 500/505/600/700 etc."
msgstr ""
@@ -469,62 +471,62 @@ msgid "This profile is intended for the SONY PRS 300."
msgstr "ملف التعريف هذا هو المقصود لجهاز سوني PRS 300."
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:78
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:449
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:453
msgid "This profile is intended for the SONY PRS-900."
msgstr "ملف التعريف هذا هو المقصود لجهاز سوني PRS 900."
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:86
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:479
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:483
msgid "This profile is intended for the Microsoft Reader."
msgstr "هذا الطور يستخدم مع Microsoft Reader"
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:97
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:490
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:494
msgid "This profile is intended for the Mobipocket books."
msgstr "ملف التعريف هذا يستخدم مع كتب Mobipocket ."
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:110
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:503
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:507
msgid "This profile is intended for the Hanlin V3 and its clones."
msgstr "ملف التعريف هذا يستخدم مع Hanlin V3 وأمثاله."
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:122
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:515
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:519
msgid "This profile is intended for the Hanlin V5 and its clones."
msgstr "ملف التعريف هذا يستخدم مع Hanlin V5 وأمثاله."
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:132
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:523
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:527
msgid "This profile is intended for the Cybook G3."
msgstr "ملف التعريف هذا يستخدم مع Cybook G3"
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:145
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:536
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:540
msgid "This profile is intended for the Cybook Opus."
msgstr "ملف التعريف هذا يستخدم مع Cybook Opus ."
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:157
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:547
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:551
msgid "This profile is intended for the Amazon Kindle."
msgstr "ملف التعريف هذا يستخدم مع Amazon Kindle ."
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:169
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:584
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:589
msgid "This profile is intended for the Irex Illiad."
msgstr "ملف التعريف هذا يستخدم مع Irex Illiad ."
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:181
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:597
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:602
msgid "This profile is intended for the IRex Digital Reader 1000."
msgstr "ملف التعريف هذا يستخدم مع IRex Digital Reader 1000 ."
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:194
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:611
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:616
msgid "This profile is intended for the IRex Digital Reader 800."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:206
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:625
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:630
msgid "This profile is intended for the B&N Nook."
msgstr "ملف التعريف هذا يستخدم مع B&N Nook ."
@@ -541,24 +543,24 @@ msgstr ""
"ملف التعريف هذا يحاول تقديم افتراضات عاقلة و مفيدة إذا كنت ترغب في إصدار "
"وثيقة للقراءة في جهاز الكمبيوتر أو على مجموعة من الأجهزة."
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:259
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:262
msgid ""
"Intended for the iPad and similar devices with a resolution of 768x1024"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:427
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:431
msgid "This profile is intended for the Kobo Reader."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:440
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:444
msgid "This profile is intended for the SONY PRS-300."
msgstr "ملف التعريف هذا يستخدم مع سوني PRS-300 ."
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:458
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:462
msgid "This profile is intended for the 5-inch JetBook."
msgstr "ملف التعريف هذا يستخدم مع الخمسة بوصة JetBook ."
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:467
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:471
msgid ""
"This profile is intended for the SONY PRS line. The 500/505/700 etc, in "
"landscape mode. Mainly useful for comics."
@@ -566,7 +568,7 @@ msgstr ""
"ملف التعريف هذا يستخدم مع سوني خط إنتاج PRS . الـ500/505/700 الخ ، في وضع "
"أفقي.غالباً مفيد للكاريكاتيرات."
-#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:566
+#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:571
msgid "This profile is intended for the Amazon Kindle DX."
msgstr "ملف التعريف هذا يستخدم مع Amazon Kindle DX"
@@ -588,7 +590,7 @@ msgstr "ملحقات معطلة"
#: /home/kovid/work/calibre/src/calibre/customize/ui.py:38
msgid "Enabled plugins"
-msgstr ""
+msgstr "تفعيل الاضافات"
#: /home/kovid/work/calibre/src/calibre/customize/ui.py:86
msgid "No valid plugin found in "
@@ -648,7 +650,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/android/driver.py:92
msgid "Communicate with S60 phones."
-msgstr ""
+msgstr "تواصل معا هواتف S60."
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:85
msgid "Apple device"
@@ -660,7 +662,7 @@ msgstr "التواصل عن طريق iTunes/iBooks."
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:93
msgid "Apple device detected, launching iTunes, please wait ..."
-msgstr ""
+msgstr "تم الكشف عن جهاز ابل, يتم تشغيل iTunes, الرجاء الانتظار..."
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:246
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:249
@@ -669,16 +671,16 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:323
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:362
-#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:921
-#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:957
-#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2823
-#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2862
+#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:922
+#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:962
+#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2831
+#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2871
msgid "%d of %d"
msgstr "%d من %d"
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:369
-#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:962
-#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2868
+#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:967
+#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2877
msgid "finished"
msgstr "تم"
@@ -703,27 +705,27 @@ msgid ""
"Click 'Show Details' for a list."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2491
+#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2499
#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:817
#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:823
#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:851
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:244
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:193
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:206
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:1693
-#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:132
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:198
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:211
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:1706
+#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:134
msgid "News"
msgstr "الأخبار"
-#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2492
+#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2500
#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi.py:20
#: /home/kovid/work/calibre/src/calibre/library/catalog.py:556
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:1656
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:1674
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:1669
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:1687
msgid "Catalog"
-msgstr ""
+msgstr "الفهرس"
-#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2730
+#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2738
msgid "Communicate with iTunes."
msgstr "تواصل معا iTunes"
@@ -863,14 +865,20 @@ msgstr ""
msgid "Communicate with the Kindle DX eBook reader."
msgstr "التواصل مع القارئ الكتاب الاليكترونى Kindle DX ."
-#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:22
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:23
msgid "Communicate with the Kobo Reader"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:53
-#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:56
-#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:59
-#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:170
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:47
+msgid ""
+"The Kobo supports only one collection currently: the \"Im_Reading\" list. "
+"Create a tag called \"Im_Reading\" "
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:63
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:66
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:69
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:186
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:68
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:71
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:74
@@ -880,33 +888,33 @@ msgstr ""
msgid "Getting list of books on device..."
msgstr "يجري إحصاء قائمة كتب من الجهاز..."
-#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:230
-#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:274
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:246
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:278
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:253
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:271
msgid "Removing books from device..."
msgstr "يجري حذف الكتب من الجهاز..."
-#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:278
-#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:285
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:282
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:289
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:278
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:283
msgid "Removing books from device metadata listing..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:290
-#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:324
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:294
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:328
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:217
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:247
msgid "Adding books to device metadata listing..."
msgstr "إضافة كتب لقائمة البيانات الوصفية للجهاز ..."
-#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:375
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:251
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:390
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:252
msgid "Not Implemented"
msgstr "غير مطبق"
-#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:376
+#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:391
msgid ""
"\".kobo\" files do not exist on the device as books instead, they are rows "
"in the sqlite database. Currently they cannot be exported or viewed."
@@ -997,6 +1005,10 @@ msgstr ""
msgid "Communicate with the iPapyrus reader."
msgstr ""
+#: /home/kovid/work/calibre/src/calibre/devices/teclast/driver.py:59
+msgid "Communicate with the Sovos reader."
+msgstr ""
+
#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:255
msgid "Unable to detect the %s disk drive. Try rebooting."
msgstr "لم يتمكن من كشف القرص %s. حاول إعادة التشغيل."
@@ -1573,28 +1585,43 @@ msgid ""
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:367
+msgid ""
+"Scale used to determine the length at which a line should be unwrapped if "
+"preprocess is enabled. Valid values are a decimal between 0 and 1. The "
+"default is 0.40, just below the median line length. This will unwrap typical "
+"books with hard line breaks, but should be reduced if the line length is "
+"variable."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:376
+msgid ""
+"Convert plain quotes, dashes and ellipsis to their typographically correct "
+"equivalents. For details, see http://daringfireball.net/projects/smartypants"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:384
msgid "Use a regular expression to try and remove the header."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:374
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:391
msgid "The regular expression to use to remove the header."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:380
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:397
msgid "Use a regular expression to try and remove the footer."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:387
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:404
msgid "The regular expression to use to remove the footer."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:394
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:411
msgid ""
"Read metadata from the specified OPF file. Metadata read from this file will "
"override any metadata in the source file."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:401
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:418
msgid ""
"Transliterate unicode characters to an ASCII representation. Use with care "
"because this will replace unicode characters with ASCII. For instance it "
@@ -1604,7 +1631,7 @@ msgid ""
"number of people will be used (Chinese in the previous example)."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:416
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:433
msgid ""
"Preserve ligatures present in the input document. A ligature is a special "
"rendering of a pair of characters like ff, fi, fl et cetera. Most readers do "
@@ -1614,101 +1641,101 @@ msgid ""
"instead."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:428
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:445
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:38
msgid "Set the title."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:432
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:449
msgid "Set the authors. Multiple authors should be separated by ampersands."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:437
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:454
msgid "The version of the title to be used for sorting. "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:441
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:458
msgid "String to be used when sorting by author. "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:445
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:462
msgid "Set the cover to the specified file or URL"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:449
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:466
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:54
msgid "Set the ebook description."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:453
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:470
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:56
msgid "Set the ebook publisher."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:457
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:474
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:60
msgid "Set the series this ebook belongs to."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:461
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:478
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:62
msgid "Set the index of the book in this series."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:465
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:482
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:64
msgid "Set the rating. Should be a number between 1 and 5."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:469
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:486
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:66
msgid "Set the ISBN of the book."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:473
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:490
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:68
msgid "Set the tags for the book. Should be a comma separated list."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:477
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:494
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:70
msgid "Set the book producer."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:481
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:498
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:72
msgid "Set the language."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:485
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:502
msgid "Set the publication date."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:489
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:506
msgid "Set the book timestamp (used by the date column in calibre)."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:589
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:606
msgid "Could not find an ebook inside the archive"
msgstr "لم يتمكّن من الحصول على كتاب داخل الأرشيف"
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:647
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:664
msgid "Values of series index and rating must be numbers. Ignoring"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:654
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:671
msgid "Failed to parse date/time"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:809
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:826
msgid "Converting input to HTML..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:836
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:853
msgid "Running transforms on ebook..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:923
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:940
msgid "Creating"
msgstr ""
@@ -2090,8 +2117,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:75
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:58
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:65
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:360
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:888
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:354
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:882
#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:589
msgid "Title"
msgstr "العنوان"
@@ -2099,8 +2126,8 @@ msgstr "العنوان"
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:402
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:59
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:67
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:365
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:889
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:359
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:883
msgid "Author(s)"
msgstr "المؤلف أو المؤلفون"
@@ -2122,28 +2149,30 @@ msgstr "المنتج"
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:189
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:99
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:72
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:319
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1080
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:313
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1081
msgid "Comments"
msgstr "التعليقات"
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:413
+#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/jacket.py:154
#: /home/kovid/work/calibre/src/calibre/gui2/book_details.py:27
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories.py:50
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:73
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:307
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1076
-#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:143
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:301
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1077
+#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:145
msgid "Tags"
msgstr "الوسوم"
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:415
+#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/jacket.py:152
#: /home/kovid/work/calibre/src/calibre/gui2/book_details.py:26
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories.py:50
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:74
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:324
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1085
-#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:91
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:318
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1086
+#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:93
msgid "Series"
msgstr "السلسلة"
@@ -2152,11 +2181,12 @@ msgid "Language"
msgstr "اللغة"
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:418
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1068
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1069
msgid "Timestamp"
msgstr "ختم التوقيت"
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:420
+#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/jacket.py:151
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:63
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:70
msgid "Published"
@@ -2504,7 +2534,7 @@ msgid "%s format books are not supported"
msgstr "الكتب بتهيئة %s ليست مدعومة"
#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/cover.py:101
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:159
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:156
msgid "Book %s of %s"
msgstr ""
@@ -2512,8 +2542,9 @@ msgstr ""
msgid "HTML TOC generation options."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/jacket.py:113
-msgid "Book Jacket"
+#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/jacket.py:153
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:71
+msgid "Rating"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/split.py:34
@@ -2577,8 +2608,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/input.py:26
msgid ""
"Scale used to determine the length at which a line should be unwrapped. "
-"Valid values are a decimal between 0 and 1. The default is 0.5, this is the "
-"median line length."
+"Valid values are a decimal between 0 and 1. The default is 0.45, just below "
+"the median line length."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/input.py:30
@@ -2859,130 +2890,130 @@ msgid ""
"allows max-line-length to be below the minimum"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:65
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:66
msgid "Send file to storage card instead of main memory by default"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:67
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:68
msgid "Confirm before deleting"
msgstr "تأكيد قبل الحذف"
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:69
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:70
msgid "Main window geometry"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:71
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:72
msgid "Notify when a new version is available"
msgstr "Notify when a new version is available"
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:73
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:74
msgid "Use Roman numerals for series number"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:75
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:76
msgid "Sort tags list by name, popularity, or rating"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:77
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:78
msgid "Number of covers to show in the cover browsing mode"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:79
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:80
msgid "Defaults for conversion to LRF"
msgstr "الإفتراضي للتحويل إلى LRF"
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:81
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:82
msgid "Options for the LRF ebook viewer"
msgstr "الخيارات لمستعرض كتب LRF"
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:84
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:85
msgid "Formats that are viewed using the internal viewer"
msgstr "تهيئات التي تعرض عن طريق المستعرض الداخلي"
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:86
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:87
msgid "Columns to be displayed in the book list"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:87
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:88
msgid "Automatically launch content server on application startup"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:88
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:89
msgid "Oldest news kept in database"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:89
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:90
msgid "Show system tray icon"
msgstr "إظهار أيقونة صينية النظام"
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:91
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:92
msgid "Upload downloaded news to device"
msgstr "رفع أخبار تم تنزيلها إلى الجهاز"
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:93
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:94
msgid "Delete books from library after uploading to device"
msgstr "حذف كتب من المكتبة بعد رفعها إلى الجهاز"
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:95
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:96
msgid ""
"Show the cover flow in a separate window instead of in the main calibre "
"window"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:97
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:98
msgid "Disable notifications from the system tray icon"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:99
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:100
msgid "Default action to perform when send to device button is clicked"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:119
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:120
msgid "Maximum number of waiting worker processes"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:121
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:122
msgid "Download social metadata (tags/rating/etc.)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:123
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:124
msgid "Overwrite author and title with new metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:125
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:126
msgid "Limit max simultaneous jobs to number of CPUs"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:127
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:128
msgid "tag browser categories not to display"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:129
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:130
msgid "The layout of the user interface"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:131
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:132
msgid "Show the average rating per item indication in the tag browser"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:133
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:134
msgid "Disable UI animations"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:181
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:182
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:479
msgid "Copied"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:215
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:216
msgid "Copy"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:215
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:216
msgid "Copy to Clipboard"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:433
+#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:434
msgid "Choose Files"
msgstr ""
@@ -2998,127 +3029,127 @@ msgstr ""
msgid "A"
msgstr "A"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:32
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:33
msgid "Add books from a single directory"
msgstr "إضافة كتب من دليل واحد"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:34
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:35
msgid ""
"Add books from directories, including sub-directories (One book per "
"directory, assumes every ebook file is the same book in a different format)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:38
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:39
msgid ""
"Add books from directories, including sub directories (Multiple books per "
"directory, assumes every ebook file is a different book)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:42
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:43
msgid "Add Empty book. (Book entry with no formats)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:44
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:45
msgid "Add from ISBN"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:83
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:84
msgid "How many empty books?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:84
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:85
msgid "How many empty books should be added?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:142
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:200
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:143
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:201
msgid "Uploading books to device."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:159
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:173
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:160
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:170
msgid "Books"
msgstr "كتب"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:160
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:161
msgid "EPUB Books"
msgstr "كتب EPUB"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:161
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:162
msgid "LRF Books"
msgstr "كتب LRF"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:162
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:163
msgid "HTML Books"
msgstr "كتب HTML"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:163
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:164
msgid "LIT Books"
msgstr "كتب LIT"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:164
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:165
msgid "MOBI Books"
msgstr "كتب MOBI"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:165
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:166
msgid "Topaz books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:166
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:167
msgid "Text books"
msgstr "كتب نصّية"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:167
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:168
msgid "PDF Books"
msgstr "كتب PDF"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:168
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:169
msgid "Comics"
msgstr "الرسومات"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:169
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:170
msgid "Archives"
msgstr "أرشيفات"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:173
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:174
msgid "Supported books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:209
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:210
msgid "Merged some books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:210
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:211
msgid ""
"Some duplicates were found and merged into the following existing books:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:219
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:220
msgid "Failed to read metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:220
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:221
msgid "Failed to read metadata from the following"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:239
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:258
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:240
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:259
msgid "Add to library"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:239
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:55
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:94
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:119
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:240
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:56
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:95
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:120
msgid "No book selected"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:252
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:253
msgid ""
"The following books are virtual and cannot be added to the calibre library:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:258
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:259
msgid "No book files found"
msgstr ""
@@ -3135,58 +3166,58 @@ msgstr ""
msgid "Fetch annotations (experimental)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:55
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:235
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:56
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:236
msgid "Use library only"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:56
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:236
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:57
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:237
msgid "User annotations generated from main library only"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:63
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:64
#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:30
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:86
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:115
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:75
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:141
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:177
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:204
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:91
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:87
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:116
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:76
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:142
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:178
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:205
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:92
msgid "No books selected"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:64
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:65
msgid "No books selected to fetch annotations from"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:89
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:90
msgid "Merging user annotations into database"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:117
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:118
msgid "%s
Last Page Read: %d (%d%%)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:123
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:124
msgid "%s
Last Page Read: Location %d (%d%%)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:142
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:143
msgid "Location %d • %s
%s
"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:151
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:152
msgid "Page %d • %s
"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:156
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:157
msgid "Location %d • %s
"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:20
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:33
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:34
msgid "Create catalog of books in your calibre library"
msgstr ""
@@ -3300,7 +3331,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:249
#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:254
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:100
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:101
#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:554
msgid "Not allowed"
msgstr ""
@@ -3321,19 +3352,19 @@ msgstr "C"
msgid "Convert books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:27
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:28
msgid "Convert individually"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:29
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:30
msgid "Bulk convert"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:85
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:86
msgid "Cannot convert"
msgstr "لا يمكن تحويله"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:114
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:115
msgid "Starting conversion of %d book(s)"
msgstr ""
@@ -3345,34 +3376,34 @@ msgstr ""
msgid "Copy selected books to the specified library"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:114
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:115
msgid "Cannot copy"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:119
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:120
msgid "No library"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:120
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:121
msgid "No library found at %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:123
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:127
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:124
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:128
msgid "Copying"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:137
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:138
msgid "Could not copy books: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:137
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:671
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:234
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:138
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:670
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:424
msgid "Failed"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:140
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:141
msgid "Copied %d books to %s"
msgstr ""
@@ -3384,82 +3415,82 @@ msgstr "Del"
msgid "Remove books"
msgstr "حذف كتب"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:23
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:24
msgid "Remove selected books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:25
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:26
msgid "Remove files of a specific format from selected books.."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:28
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:29
msgid "Remove all formats from selected books, except..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:31
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:32
msgid "Remove covers from selected books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:34
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:35
msgid "Remove matching books from device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:52
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:53
msgid "Cannot delete"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:65
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:66
msgid "Choose formats to be deleted"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:83
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:84
msgid "Choose formats not to be deleted"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:103
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:104
msgid "Cannot delete books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:104
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:105
msgid "No device is connected"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:114
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:115
msgid "Main memory"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:115
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:116
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:435
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:444
msgid "Storage Card A"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:116
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:117
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:437
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:446
msgid "Storage Card B"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:121
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:122
msgid "No books to delete"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:122
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:123
msgid "None of the selected books are on the device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:139
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:194
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:140
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:195
msgid "Deleting books from device."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:160
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:161
msgid ""
"The selected books will be permanently deleted and the files removed "
"from your calibre library. Are you sure?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:179
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:180
msgid ""
"The selected books will be permanently deleted from your device. Are "
"you sure?"
@@ -3524,79 +3555,79 @@ msgstr "E"
msgid "Edit metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:27
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:28
msgid "Merge book records"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:28
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:29
msgid "M"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:30
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:31
msgid "Edit metadata individually"
msgstr "تحرير الميتاداتا فردياً"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:33
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:34
msgid "Edit metadata in bulk"
msgstr "تحرير الميتاداتا جملةً"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:36
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:37
msgid "Download metadata and covers"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:39
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:40
msgid "Download only metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:41
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:42
msgid "Download only covers"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:44
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:45
msgid "Download only social metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:50
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:51
msgid "Merge into first selected book - delete others"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:53
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:54
msgid "Merge into first selected book - keep others"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:74
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:75
msgid "Cannot download metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:97
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:98
msgid "social metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:99
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:100
msgid "covers"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:99
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:100
msgid "metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:104
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:105
msgid "Downloading %s for %d book(s)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:125
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:126
msgid "Failed to download some metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:126
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:127
msgid "Failed to download metadata for the following:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:129
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:130
msgid "Failed to download metadata:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:130
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:131
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:607
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/misc.py:65
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/misc.py:112
@@ -3604,39 +3635,40 @@ msgstr ""
msgid "Error"
msgstr "خطأ"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:140
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:176
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:141
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:177
msgid "Cannot edit metadata"
msgstr "لا يمكن تحرير الميتاداتا"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:203
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:206
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:204
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:207
msgid "Cannot merge books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:207
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:208
msgid "At least two books must be selected for merging"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:211
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:212
msgid ""
-"All book formats and metadata from the selected books will be added to the "
-"first selected book.
The second and subsequently selected "
-"books will not be deleted or changed.
Please confirm you want to "
-"proceed."
+"Book formats and metadata from the selected books will be added to the "
+"first selected book. ISBN will not be merged.
The "
+"second and subsequently selected books will not be deleted or "
+"changed.
Please confirm you want to proceed."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:222
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:224
msgid ""
-"All book formats and metadata from the selected books will be merged into "
-"the first selected book.
After merger the second and "
-"subsequently selected books will be deleted.
All book formats "
-"of the first selected book will be kept and any duplicate formats in the "
-"second and subsequently selected books will be permanently deleted "
-"from your computer.
Are you sure you want to proceed?"
+"Book formats and metadata from the selected books will be merged into the "
+"first selected book. ISBN will not be merged.
After "
+"merger the second and subsequently selected books will be deleted. "
+"
All book formats of the first selected book will be kept and any "
+"duplicate formats in the second and subsequently selected books will be "
+"permanently deleted from your computer.
Are you sure "
+"you want to proceed?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:234
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:237
msgid ""
"You are about to merge more than 5 books. Are you sure you want to "
"proceed?"
@@ -3716,53 +3748,53 @@ msgid "S"
msgstr "S"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:40
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:45
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:46
msgid "Save to disk"
msgstr "حفظ إلى القرص"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:47
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:48
msgid "Save to disk in a single directory"
msgstr "حفظ إلى القرص في دليل واحد"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:49
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:68
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:50
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:69
msgid "Save only %s format to disk"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:53
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:71
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:54
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:72
msgid "Save only %s format to disk in a single directory"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:90
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:91
msgid "Cannot save to disk"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:93
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:94
msgid "Choose destination directory"
msgstr "إختيار دليل الوجهة"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:101
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:102
msgid ""
"You are trying to save files into the calibre library. This can cause "
"corruption of your library. Save to disk is meant to export files from your "
"calibre library elsewhere."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:135
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:136
msgid "Error while saving"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:136
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:137
msgid "There was an error while saving."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:143
#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:144
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:145
msgid "Could not save some books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:145
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:146
msgid "Click the show details button to see which ones."
msgstr ""
@@ -3774,11 +3806,11 @@ msgstr ""
msgid "I"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/show_book_details.py:25
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/show_book_details.py:26
msgid "No detailed info available"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/show_book_details.py:26
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/show_book_details.py:27
msgid "No detailed information is available for books on the device."
msgstr ""
@@ -3786,35 +3818,35 @@ msgstr ""
msgid "Similar books..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:23
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:24
msgid "Alt+A"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:23
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:24
msgid "Books by same author"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:24
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:25
msgid "Books in this series"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:25
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:26
msgid "Alt+Shift+S"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:26
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:27
msgid "Alt+P"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:26
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:27
msgid "Books by this publisher"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:27
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:28
msgid "Alt+T"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:27
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/similar_books.py:28
msgid "Books with the same tags"
msgstr "كتب بنفس الوسوم"
@@ -3823,29 +3855,29 @@ msgid "V"
msgstr "V"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:24
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:31
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:32
msgid "View"
msgstr "عرض"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:32
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:33
msgid "View specific format"
msgstr "عرض تهيئة معينة"
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:94
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:155
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:95
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:156
msgid "Cannot view"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:100
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:101
#: /home/kovid/work/calibre/src/calibre/gui2/convert/regex_builder.py:77
msgid "Choose the format to view"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:108
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:109
msgid "Multiple Books Selected"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:109
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:110
msgid ""
"You are attempting to open %d books. Opening too many books at once can be "
"slow and have a negative effect on the responsiveness of your computer. Once "
@@ -3853,11 +3885,11 @@ msgid ""
"continue?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:118
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:119
msgid "Cannot open folder"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:156
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:157
msgid "%s has no available formats."
msgstr ""
@@ -3882,7 +3914,7 @@ msgid "The specified directory could not be processed."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/add.py:228
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:806
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:807
msgid "No books"
msgstr ""
@@ -4001,19 +4033,19 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/add_wizard/welcome_ui.py:71
#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:57
#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:58
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:130
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:128
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:176
#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:58
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:84
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:85
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:77
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:369
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:374
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:388
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:399
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:376
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:390
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:401
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:403
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:409
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:405
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:411
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:92
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:95
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:161
@@ -4076,8 +4108,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:116
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:126
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:76
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:314
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1066
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:308
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1067
msgid "Path"
msgstr "المسار"
@@ -4087,15 +4119,15 @@ msgstr "المسار"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:118
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:119
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:122
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:313
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:307
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/emailp.py:24
-#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:100
+#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:102
msgid "Formats"
msgstr "التهيئات"
#: /home/kovid/work/calibre/src/calibre/gui2/book_details.py:25
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:892
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1069
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:886
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1070
msgid "Collections"
msgstr ""
@@ -4105,11 +4137,11 @@ msgid "Click to open"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/book_details.py:48
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:300
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:306
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:312
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:318
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1075
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1079
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1076
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1080
#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts.py:47
#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts_ui.py:78
#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts_ui.py:83
@@ -4160,14 +4192,14 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_bibtex_ui.py:81
#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_csv_xml_ui.py:37
-#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:68
+#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:76
#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_tab_template_ui.py:27
#: /home/kovid/work/calibre/src/calibre/gui2/convert/comic_input_ui.py:88
#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:54
#: /home/kovid/work/calibre/src/calibre/gui2/convert/epub_output_ui.py:48
#: /home/kovid/work/calibre/src/calibre/gui2/convert/fb2_input_ui.py:28
#: /home/kovid/work/calibre/src/calibre/gui2/convert/fb2_output_ui.py:31
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:124
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:122
#: /home/kovid/work/calibre/src/calibre/gui2/convert/lrf_output_ui.py:115
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:171
#: /home/kovid/work/calibre/src/calibre/gui2/convert/mobi_output_ui.py:66
@@ -4177,7 +4209,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_input_ui.py:38
#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:42
#: /home/kovid/work/calibre/src/calibre/gui2/convert/rb_output_ui.py:28
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:60
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:80
#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc_ui.py:62
#: /home/kovid/work/calibre/src/calibre/gui2/convert/txt_input_ui.py:46
#: /home/kovid/work/calibre/src/calibre/gui2/convert/txt_output_ui.py:45
@@ -4185,14 +4217,14 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/xpath_wizard_ui.py:67
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:82
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_item_ui.py:35
-#: /home/kovid/work/calibre/src/calibre/gui2/filename_pattern_ui.py:106
+#: /home/kovid/work/calibre/src/calibre/gui2/filename_pattern_ui.py:111
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/adding_ui.py:48
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior_ui.py:136
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/columns_ui.py:81
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/conversion_ui.py:54
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/custom_columns_ui.py:81
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/email_ui.py:65
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:97
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel_ui.py:105
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/misc_ui.py:63
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins_ui.py:81
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/save_template_ui.py:46
@@ -4281,23 +4313,23 @@ msgstr ""
msgid "E-book options"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:69
+#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:77
msgid "'Don't include this book' tag:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:70
+#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:78
msgid "'Mark this book as read' tag:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:71
+#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:79
msgid "Additional note tag prefix:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:72
+#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:80
msgid "Regex pattern describing tags to exclude as genres:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:73
+#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:81
msgid ""
"Regex tips:\n"
"- The default regex - \\[.+\\] - excludes genre tags of the form [tag], "
@@ -4306,18 +4338,22 @@ msgid ""
"Genre Section"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:76
+#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:84
msgid "Include 'Titles' Section"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:77
+#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:85
msgid "Include 'Recently Added' Section"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:78
+#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:86
msgid "Sort numbers as text"
msgstr ""
+#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi_ui.py:87
+msgid "Include 'Series' Section"
+msgstr ""
+
#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_tab_template_ui.py:28
msgid "Tab template for catalog.ui"
msgstr ""
@@ -4530,15 +4566,15 @@ msgid "&Base font size:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:110
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:128
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:126
msgid "Font size &key:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:111
#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:115
#: /home/kovid/work/calibre/src/calibre/gui2/convert/font_key_ui.py:117
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:127
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:132
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:125
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:130
#: /home/kovid/work/calibre/src/calibre/gui2/convert/lrf_output_ui.py:118
#: /home/kovid/work/calibre/src/calibre/gui2/convert/lrf_output_ui.py:120
#: /home/kovid/work/calibre/src/calibre/gui2/convert/lrf_output_ui.py:125
@@ -4589,69 +4625,73 @@ msgstr ""
msgid "Justify text"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:125
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:123
msgid "&Disable font size rescaling"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:126
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:124
msgid "Base &font size:"
msgstr "حجم الخط& الأساسي:"
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:129
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:127
msgid "Wizard to help you choose an appropriate font size key"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:131
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:129
msgid "Line &height:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:133
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:131
msgid "Input character &encoding:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:134
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:132
msgid "Remove &spacing between paragraphs"
msgstr "حذف الفراغات& بين الفقرات"
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:135
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:133
msgid "Indent size:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:136
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:134
msgid ""
"When calibre removes inter paragraph spacing, it automatically sets a "
"paragraph indent, to ensure that paragraphs can be easily distinguished. "
"This option controls the width of that indent."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:137
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:135
msgid " em"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:138
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:136
msgid "Text justification:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:139
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:137
msgid "&Linearize tables"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:140
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:138
msgid "Extra &CSS"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:141
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:139
msgid "&Transliterate unicode characters to ASCII"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:142
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:140
msgid "Insert &blank line"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:143
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:141
msgid "Keep &ligatures"
msgstr ""
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:142
+msgid "Smarten &punctuation"
+msgstr ""
+
#: /home/kovid/work/calibre/src/calibre/gui2/convert/lrf_output.py:19
msgid "LRF Output"
msgstr ""
@@ -4717,38 +4757,38 @@ msgid ""
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:165
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:112
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:109
msgid "Choose cover for "
msgstr "إختار الغلاف لـ "
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:172
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:119
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:116
msgid "Cannot read"
msgstr "لا يمكن القراءة"
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:173
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:120
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:117
msgid "You do not have permission to read the file: "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:181
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:188
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:128
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:125
msgid "Error reading file"
msgstr "خطأ في قراءة الملف"
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:182
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:129
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:126
msgid "
There was an error reading from file:
"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:189
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:137
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:134
msgid " is not a valid picture"
msgstr " ليست صورة صالحة"
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:172
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:405
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:407
msgid "Book Cover"
msgstr "غلاف الكتاب"
@@ -4757,7 +4797,7 @@ msgid "Use cover from &source file"
msgstr "استخدم غلاف من المصدر&"
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:174
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:406
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:408
msgid "Change &cover image:"
msgstr "تغيير صورة الغلاف&:"
@@ -4776,7 +4816,7 @@ msgid "Change the title of this book"
msgstr "تغيير عنوان هذا الكتاب"
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:179
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:166
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:229
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:370
msgid "&Author(s): "
msgstr "ال&مؤلف: "
@@ -4792,19 +4832,19 @@ msgid ""
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:182
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:175
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:379
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:238
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:381
msgid "&Publisher: "
msgstr "&الناشر: "
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:183
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:380
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:382
msgid "Ta&gs: "
msgstr "الو&سوم: "
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:184
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:177
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:381
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:240
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:383
msgid ""
"Tags categorize the book. This is particularly useful while searching. "
"
They can be any words or phrases, separated by commas."
@@ -4813,22 +4853,22 @@ msgstr ""
"مجموعة كلمات، مفرقة بفاصلة."
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:185
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:184
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:384
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:247
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:386
msgid "&Series:"
msgstr "&سلسلات:"
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:186
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:187
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:185
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:186
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:385
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:386
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:248
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:249
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:387
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:388
msgid "List of known series. You can add new series."
msgstr "قائمة السلسلات المعروفة. بإمكانك إضافة سلسلات جديدة."
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:188
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:391
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:393
msgid "Book "
msgstr "الكتاب "
@@ -4987,7 +5027,7 @@ msgid "Regex:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/regex_builder_ui.py:55
-#: /home/kovid/work/calibre/src/calibre/gui2/filename_pattern_ui.py:117
+#: /home/kovid/work/calibre/src/calibre/gui2/filename_pattern_ui.py:122
msgid "Test"
msgstr "تجربة"
@@ -5025,66 +5065,70 @@ msgid ""
"Fine tune the detection of chapter headings and other document structure."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:35
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:37
msgid "Detect chapters at (XPath expression):"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:36
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:38
msgid "Insert page breaks before (XPath expression):"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:38
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:40
msgid "Header regular expression:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:41
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:43
msgid "Footer regular expression:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:57
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:59
#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:87
msgid "Invalid regular expression"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:58
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:60
#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:88
msgid "Invalid regular expression: %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:63
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:65
#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:39
msgid "Invalid XPath"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:64
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:66
#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:40
msgid "The XPath expression %s is invalid."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:61
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:81
msgid "Chapter &mark:"
msgstr "ع&لامة الفصل:"
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:62
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:82
msgid "Remove first &image"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:63
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:83
msgid "Insert &metadata as page at start of book"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:64
-msgid "&Preprocess input file to possibly improve structure detection"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:65
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:84
msgid "Remove F&ooter"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:66
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:85
msgid "Remove H&eader"
msgstr ""
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:86
+msgid "Line &un-wrap factor during preprocess:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:87
+msgid "&Preprocess input file to possibly improve structure detection"
+msgstr ""
+
#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:16
msgid ""
"Table of\n"
@@ -5306,7 +5350,7 @@ msgid " index:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:451
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:193
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:256
msgid "Automatically number books in this series"
msgstr ""
@@ -5408,142 +5452,142 @@ msgstr ""
msgid "Select folder to open as device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:677
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:676
msgid "Error talking to device"
msgstr "خطأ في الاتصال بالجهاز"
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:678
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:677
msgid ""
"There was a temporary error talking to the device. Please unplug and "
"reconnect the device and or reboot."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:717
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:716
msgid "Device: "
msgstr "الجهاز: "
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:719
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:718
msgid " detected."
msgstr " تم كشفه."
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:807
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:808
msgid "selected to send"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:812
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:813
msgid "Choose format to send to device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:821
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:822
msgid "No device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:822
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:823
msgid "Cannot send: No device is connected"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:825
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:829
-msgid "No card"
-msgstr ""
-
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:826
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:830
+msgid "No card"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:827
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:831
msgid "Cannot send: Device has no storage card"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:871
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:872
msgid "E-book:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:874
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:875
msgid "Attached, you will find the e-book"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:875
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:876
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:107
msgid "by"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:876
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:877
msgid "in the %s format."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:889
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:890
msgid "Sending email to"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:919
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:927
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1020
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1082
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1201
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1209
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:920
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:928
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1021
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1083
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1202
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1210
msgid "No suitable formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:920
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:921
msgid "Auto convert the following books before sending via email?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:928
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:929
msgid ""
"Could not email the following books as no suitable formats were found:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:946
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:947
msgid "Failed to email books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:947
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:948
msgid "Failed to email the following books:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:951
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:952
msgid "Sent by email:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:979
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:980
msgid "News:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:980
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:981
msgid "Attached is the"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:991
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:992
msgid "Sent news to"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1021
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1083
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1202
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1022
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1084
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1203
msgid "Auto convert the following books before uploading to the device?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1051
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1052
msgid "Sending catalogs to device."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1115
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1116
msgid "Sending news to device."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1168
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1169
msgid "Sending books to device."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1210
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1211
msgid ""
"Could not upload the following books to the device, as no suitable formats "
"were found. Convert the book(s) to a format supported by your device first."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1272
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1273
msgid "No space on device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1273
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1274
msgid ""
"
Cannot upload books to device there is no more free space available "
msgstr ""
@@ -5738,14 +5782,14 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:76
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:69
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:890
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:884
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/create_custom_column.py:31
#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:588
msgid "Date"
msgstr "تاريخ"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:76
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1065
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1066
msgid "Format"
msgstr "التهيئة"
@@ -5874,90 +5918,133 @@ msgstr ""
msgid "Stop &all non device jobs"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:111
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:107
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:385
+msgid "Lower Case"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:108
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:384
+msgid "Upper Case"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:109
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:387
+msgid "Title Case"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:119
msgid "Editing meta information for %d books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:225
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:166
+msgid "Book %d:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:182
+msgid ""
+"Search and replace in text fields using regular expressions. The search text "
+"is an arbitrary python-compatible regular expression. The replacement text "
+"can contain backreferences to parenthesized expressions in the pattern. The "
+"search is not anchored, and can match and replace multiple times on the same "
+"string. See this "
+"reference for more information, and in particular the 'sub' function."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:192
+msgid ""
+"Note: you can destroy your library using this feature. Changes are "
+"permanent. There is no undo function. You are strongly encouraged to back up "
+"your library before proceeding."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:382
+msgid "Search/replace invalid"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:383
+msgid "Search pattern is invalid: %s"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:415
msgid "Applying changes to %d books. This may take a while."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:165
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:228
msgid "Edit Meta information"
msgstr "تحرير معلومات الميتا"
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:167
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:230
msgid "A&utomatically set author sort"
msgstr "ضبط& ترتيب المؤلف آلياً"
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:168
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:231
msgid "Author s&ort: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:169
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:372
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:232
msgid ""
"Specify how the author(s) of this book should be sorted. For example Charles "
"Dickens should be sorted as Dickens, Charles."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:170
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:375
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:233
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:377
msgid "&Rating:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:171
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:172
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:376
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:377
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:234
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:235
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:378
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:379
msgid "Rating of this book. 0-5 stars"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:173
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:236
msgid "No change"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:174
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:378
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:237
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:380
msgid " stars"
msgstr " نجمة"
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:176
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:239
msgid "Add ta&gs: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:178
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:179
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:382
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:383
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:241
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:242
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:384
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:385
msgid "Open Tag Editor"
msgstr "فتح محرر الوسوم"
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:180
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:243
msgid "&Remove tags:"
msgstr "حذف& الوسوم:"
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:181
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:244
msgid "Comma separated list of tags to remove from the books. "
msgstr "قائمة من الوسوم مفرقة بالفاصلة لحذفها من الكتب. "
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:182
-msgid "Remove all"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:183
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:245
msgid "Check this box to remove all tags from the books."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:187
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:246
+msgid "Remove all"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:250
msgid "Remove &format:"
msgstr "حذف الت&هيئة:"
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:188
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:251
msgid "&Swap title and author"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:189
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:252
msgid ""
"Selected books will be automatically numbered,\n"
"in the order you selected them.\n"
@@ -5965,161 +6052,205 @@ msgid ""
"Book A will have series number 1 and Book B series number 2."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:194
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:257
msgid ""
"Remove stored conversion settings for the selected books.\n"
"\n"
"Future conversion of these books will use the default settings."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:197
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:260
msgid "Remove &stored conversion settings for the selected books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:198
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:413
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:261
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:415
msgid "&Basic metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:199
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:414
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:262
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:416
msgid "&Custom metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:94
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:263
+msgid "Search &field:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:264
+msgid "&Search for:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:265
+msgid "&Replace with:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:266
+msgid "Apply function &after replace:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:267
+msgid "Test &text"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:268
+msgid "Test re&sult"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:269
+msgid "Your test:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:270
+msgid "&Search and replace (experimental)"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:91
msgid "Last modified: %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:136
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:133
msgid "Not a valid picture"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:153
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:150
msgid "Specify title and author"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:154
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:151
msgid "You must specify a title and author before generating a cover"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:172
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:169
msgid "Choose formats for "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:203
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:200
msgid "No permission"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:204
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:201
msgid "You do not have permission to read the following files:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:231
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:232
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:228
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:229
msgid "No format selected"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:243
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:240
msgid "Could not read metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:244
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:241
msgid "Could not read metadata from %s format"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:292
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:298
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:289
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:295
msgid "Could not read cover"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:293
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:290
msgid "Could not read cover from %s format"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:299
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:296
msgid "The cover in the %s format is invalid"
msgstr ""
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:333
+msgid ""
+" The green color indicates that the current author sort matches the current "
+"author"
+msgstr ""
+
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:336
+msgid ""
+" The red color indicates that the current author sort does not match the "
+"current author"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:341
msgid "Abort the editing of all remaining books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:474
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:479
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:505
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:510
msgid "This ISBN number is valid"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:482
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:513
msgid "This ISBN number is invalid"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:561
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:592
msgid "Cannot use tag editor"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:562
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:593
msgid "The tags editor cannot be used if you have modified the tags"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:582
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:613
msgid "Downloading cover..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:594
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:599
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:605
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:610
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:625
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:630
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:636
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:641
msgid "Cannot fetch cover"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:595
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:606
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:611
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:626
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:637
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:642
msgid "Could not fetch cover.
"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:596
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:627
msgid "The download timed out."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:600
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:631
msgid "Could not find cover for this book. Try specifying the ISBN first."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:612
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:643
msgid ""
"For the error message from each cover source, click Show details below."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:619
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:650
msgid "Bad cover"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:620
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:651
msgid "The cover is not a valid picture"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:653
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:684
msgid "There were errors"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:654
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:685
msgid "There were errors downloading social metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:683
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:714
msgid "Cannot fetch metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:684
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:715
msgid "You must specify at least one of ISBN, Title, Authors or Publisher"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:767
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:798
msgid "Permission denied"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:768
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:799
msgid "Could not open %s. Is it being used by another program?"
msgstr ""
@@ -6139,76 +6270,87 @@ msgstr ""
msgid "Author S&ort: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:373
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:372
msgid ""
-"Automatically create the author sort entry based on the current author entry"
-msgstr "ينشئ مدخل ترتيب المؤلف حسب مدخل المؤلف الحالي"
+"Specify how the author(s) of this book should be sorted. For example Charles "
+"Dickens should be sorted as Dickens, Charles.\n"
+"If the box is colored green, then text matches the individual author's sort "
+"strings. If it is colored red, then the authors and this text do not match."
+msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:387
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:374
+msgid ""
+"Automatically create the author sort entry based on the current author "
+"entry.\n"
+"Using this button to create author sort will change author sort from red to "
+"green."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:389
msgid "Remove unused series (Series that have no books)"
msgstr "حذف سلسلات غير مستخدمة (سلسلات التي لا تحتوي على كتب)"
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:389
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:391
msgid "IS&BN:"
msgstr "IS&BN:"
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:390
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:392
msgid "Publishe&d:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:393
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:395
msgid "dd MMM yyyy"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:394
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:396
msgid "&Date:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:395
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:397
msgid "&Comments"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:396
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:398
msgid "&Fetch metadata from server"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:397
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:399
msgid "Available Formats"
msgstr "التهيئات المتوفرة"
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:398
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:400
msgid "Add a new format for this book to the database"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:400
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:402
msgid "Remove the selected formats for this book from the database."
msgstr "حذف التهيئات المختارة لهذا الكتاب من قاعدة البيانات."
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:402
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:404
msgid "Set the cover for the book from the selected format"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:404
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:406
msgid "Update metadata from the metadata in the selected format"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:407
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:409
msgid "&Browse"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:408
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:410
msgid "Reset cover to default"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:410
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:412
msgid "Download co&ver"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:411
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:413
msgid "Generate a default cover based on the title and author"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:412
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:414
msgid "&Generate cover"
msgstr ""
@@ -6513,12 +6655,12 @@ msgid "Choose formats"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories.py:50
-#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:80
+#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:82
msgid "Authors"
msgstr "المؤلفون"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories.py:50
-#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:111
+#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:113
msgid "Publishers"
msgstr "الناشرون"
@@ -6700,7 +6842,7 @@ msgid "Send test mail from %s to:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/test_email_ui.py:58
-#: /home/kovid/work/calibre/src/calibre/gui2/filename_pattern_ui.py:115
+#: /home/kovid/work/calibre/src/calibre/gui2/filename_pattern_ui.py:120
msgid "&Test"
msgstr "&تجربة"
@@ -6880,7 +7022,7 @@ msgstr ""
msgid "Recipe source code (python)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/filename_pattern_ui.py:107
+#: /home/kovid/work/calibre/src/calibre/gui2/filename_pattern_ui.py:112
msgid ""
"\n"
@@ -6904,27 +7046,27 @@ msgid ""
"metadata entries are documented in tooltips.