Merge from trunk

This commit is contained in:
Charles Haley 2013-01-24 13:53:37 +01:00
commit caede70fb8
29 changed files with 786 additions and 312 deletions

View File

@ -28,6 +28,8 @@ class Barrons(BasicNewsRecipe):
## Don't grab articles more than 7 days old
oldest_article = 7
use_javascript_to_login = True
requires_version = (0, 9, 16)
extra_css = '''
.datestamp{font-family:Verdana,Geneva,Kalimati,sans-serif; font-size:x-small;}
@ -40,7 +42,7 @@ class Barrons(BasicNewsRecipe):
.insettipUnit{font-size: x-small;}
'''
remove_tags = [
dict(name ='div', attrs={'class':['tabContainer artTabbedNav','rssToolBox hidden','articleToolbox']}),
dict(name ='div', attrs={'class':['sTools sTools-t', 'tabContainer artTabbedNav','rssToolBox hidden','articleToolbox']}),
dict(name = 'a', attrs ={'class':'insetClose'})
]
@ -60,21 +62,17 @@ class Barrons(BasicNewsRecipe):
]
]
def get_browser(self):
br = BasicNewsRecipe.get_browser()
if self.username is not None and self.password is not None:
br.open('http://commerce.barrons.com/auth/login')
br.select_form(nr=0)
br['username'] = self.username
br['password'] = self.password
br.submit()
return br
def javascript_login(self, br, username, password):
br.visit('http://commerce.barrons.com/auth/login')
f = br.select_form(nr=0)
f['username'] = username
f['password'] = password
br.submit(timeout=120)
## Use the print version of a page when available.
def print_version(self, url):
main, sep, rest = url.rpartition('?')
return main + '#printmode'
return main + '#text.print'
def postprocess_html(self, soup, first):

View File

@ -7,12 +7,16 @@ class AdvancedUserRecipe1282093204(BasicNewsRecipe):
oldest_article = 1
max_articles_per_feed = 15
use_embedded_content = False
no_stylesheets = True
auto_cleanup = True
masthead_url = 'http://farm5.static.flickr.com/4118/4929686950_0e22e2c88a.jpg'
feeds = [
(u'News-Bill McClellan', u'http://www2.stltoday.com/search/?q=&d1=&d2=&s=start_time&sd=desc&c=news%2Flocal%2fcolumns%2Fbill-mclellan&f=rss&t=article'),
(u'News-Columns', u'http://www2.stltoday.com/search/?q=&d1=&d2=&s=start_time&sd=desc&c=news%2Flocal%2Fcolumns*&l=50&f=rss&t=article'),
(u'News-Crime & Courtshttp://www2.stltoday.com/search/?q=&d1=&d2=&s=start_time&sd=desc&c=news%2Flocal%2Fcrime-and-courts&l=50&f=rss&t=article'),
(u'News-Crime & Courts', 'http://www2.stltoday.com/search/?q=&d1=&d2=&s=start_time&sd=desc&c=news%2Flocal%2Fcrime-and-courts&l=50&f=rss&t=article'),
(u'News-Deb Peterson', u'http://www2.stltoday.com/search/?q=&d1=&d2=&s=start_time&sd=desc&c=news%2Flocal%2fcolumns%2Fdeb-peterson&f=rss&t=article'),
(u'News-Education', u'http://www2.stltoday.com/search/?q=&d1=&d2=&s=start_time&sd=desc&c=news%2Flocal%2feducation&f=rss&t=article'),
(u'News-Government & Politics', u'http://www2.stltoday.com/search/?q=&d1=&d2=&s=start_time&sd=desc&c=news%2Flocal%2fgovt-and-politics&f=rss&t=article'),
@ -62,9 +66,9 @@ class AdvancedUserRecipe1282093204(BasicNewsRecipe):
(u'Entertainment-House-O-Fun', u'http://www2.stltoday.com/search/?q=&d1=&d2=&s=start_time&sd=desc&c=entertainment%2Fhouse-o-fun&l=100&f=rss&t=article'),
(u'Entertainment-Kevin C. Johnson', u'http://www2.stltoday.com/search/?q=&d1=&d2=&s=start_time&sd=desc&c=entertainment%2Fmusic%2Fkevin-johnson&l=100&f=rss&t=article')
]
remove_empty_feeds = True
remove_tags = [dict(name='div', attrs={'id':'blox-logo'}),dict(name='a')]
keep_only_tags = [dict(name='h1'), dict(name='p', attrs={'class':'byline'}), dict(name="div", attrs={'id':'blox-story-text'})]
#remove_empty_feeds = True
#remove_tags = [dict(name='div', attrs={'id':'blox-logo'}),dict(name='a')]
#keep_only_tags = [dict(name='h1'), dict(name='p', attrs={'class':'byline'}), dict(name="div", attrs={'id':'blox-story-text'})]
extra_css = 'p {text-align: left;}'

View File

@ -7,28 +7,15 @@ class AdvancedUserRecipe1289990851(BasicNewsRecipe):
language = 'en_CA'
__author__ = 'Nexus'
no_stylesheets = True
auto_cleanup = True
use_embedded_content = False
INDEX = 'http://tsn.ca/nhl/story/?id=nhl'
keep_only_tags = [dict(name='div', attrs={'id':['tsnColWrap']}),
dict(name='div', attrs={'id':['tsnStory']})]
remove_tags = [dict(name='div', attrs={'id':'tsnRelated'}),
dict(name='div', attrs={'class':'textSize'})]
def parse_index(self):
feeds = []
soup = self.index_to_soup(self.INDEX)
feed_parts = soup.findAll('div', attrs={'class': 'feature'})
for feed_part in feed_parts:
articles = []
if not feed_part.h2:
continue
feed_title = feed_part.h2.string
article_parts = feed_part.findAll('a')
for article_part in article_parts:
article_title = article_part.string
article_date = ''
article_url = 'http://tsn.ca/' + article_part['href']
articles.append({'title': article_title, 'url': article_url, 'description':'', 'date':article_date})
if articles:
feeds.append((feed_title, articles))
return feeds
#keep_only_tags = [dict(name='div', attrs={'id':['tsnColWrap']}),
#dict(name='div', attrs={'id':['tsnStory']})]
#remove_tags = [dict(name='div', attrs={'id':'tsnRelated'}),
#dict(name='div', attrs={'class':'textSize'})]
feeds = [
('News',
'http://www.tsn.ca/datafiles/rss/Stories.xml'),
]

View File

@ -79,6 +79,42 @@ def debug():
global DEBUG
DEBUG = True
_cache_dir = None
def _get_cache_dir():
confcache = os.path.join(config_dir, u'caches')
if isportable:
return confcache
if os.environ.has_key('CALIBRE_CACHE_DIRECTORY'):
return os.path.abspath(os.environ['CALIBRE_CACHE_DIRECTORY'])
if iswindows:
w = plugins['winutil'][0]
candidate = os.path.join(w.special_folder_path(w.CSIDL_LOCAL_APPDATA), u'%s-cache'%__appname__)
elif isosx:
candidate = os.path.join(os.path.expanduser(u'~/Library/Caches'), __appname__)
else:
candidate = os.environ.get('XDG_CACHE_HOME', u'~/.cache')
candidate = os.path.join(os.path.expanduser(candidate),
__appname__)
if isinstance(candidate, bytes):
try:
candidate = candidate.decode(filesystem_encoding)
except ValueError:
candidate = confcache
if not os.path.exists(candidate):
try:
os.makedirs(candidate)
except:
candidate = confcache
return candidate
def cache_dir():
global _cache_dir
if _cache_dir is None:
_cache_dir = _get_cache_dir()
return _cache_dir
# plugins {{{
class Plugins(collections.Mapping):

View File

@ -7,10 +7,11 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
import os, traceback
from collections import defaultdict
from functools import wraps, partial
from calibre.db.categories import get_categories
from calibre.db.locking import create_locks, RecordLock
from calibre.db.fields import create_field
from calibre.db.search import Search
@ -18,6 +19,7 @@ from calibre.db.tables import VirtualTable
from calibre.db.lazy import FormatMetadata, FormatsList
from calibre.ebooks.metadata.book.base import Metadata
from calibre.utils.date import now
from calibre.utils.icu import sort_key
def api(f):
f.is_cache_api = True
@ -67,6 +69,36 @@ class Cache(object):
lock = self.read_lock if ira else self.write_lock
setattr(self, name, wrap_simple(lock, func))
self.initialize_dynamic()
def initialize_dynamic(self):
# Reconstruct the user categories, putting them into field_metadata
# Assumption is that someone else will fix them if they change.
self.field_metadata.remove_dynamic_categories()
for user_cat in sorted(self.pref('user_categories', {}).iterkeys(), key=sort_key):
cat_name = '@' + user_cat # add the '@' to avoid name collision
self.field_metadata.add_user_category(label=cat_name, name=user_cat)
# add grouped search term user categories
muc = frozenset(self.pref('grouped_search_make_user_categories', []))
for cat in sorted(self.pref('grouped_search_terms', {}).iterkeys(), key=sort_key):
if cat in muc:
# There is a chance that these can be duplicates of an existing
# user category. Print the exception and continue.
try:
self.field_metadata.add_user_category(label=u'@' + cat, name=cat)
except:
traceback.print_exc()
# TODO: Saved searches
# if len(saved_searches().names()):
# self.field_metadata.add_search_category(label='search', name=_('Searches'))
self.field_metadata.add_grouped_search_terms(
self.pref('grouped_search_terms', {}))
self._search_api.change_locations(self.field_metadata.get_search_terms())
@property
def field_metadata(self):
return self.backend.field_metadata
@ -262,13 +294,13 @@ class Cache(object):
Return all the books associated with the item identified by
``item_id``, where the item belongs to the field ``name``.
Returned value is a tuple of book ids, or the empty tuple if the item
Returned value is a set of book ids, or the empty set if the item
or the field does not exist.
'''
try:
return self.fields[name].books_for(item_id)
except (KeyError, IndexError):
return ()
return set()
@read_api
def all_book_ids(self, type=frozenset):
@ -319,8 +351,8 @@ class Cache(object):
return ans
@read_api
def pref(self, name):
return self.backend.prefs[name]
def pref(self, name, default=None):
return self.backend.prefs.get(name, default)
@api
def get_metadata(self, book_id,
@ -384,9 +416,7 @@ class Cache(object):
all_book_ids = frozenset(self._all_book_ids() if ids_to_sort is None
else ids_to_sort)
get_metadata = partial(self._get_metadata, get_user_categories=False)
def get_lang(book_id):
ans = self._field_for('languages', book_id)
return ans[0] if ans else None
lang_map = self.fields['languages'].book_value_map
fm = {'title':'sort', 'authors':'author_sort'}
@ -395,10 +425,10 @@ class Cache(object):
idx = field + '_index'
is_series = idx in self.fields
ans = self.fields[fm.get(field, field)].sort_keys_for_books(
get_metadata, get_lang, all_book_ids,)
get_metadata, lang_map, all_book_ids,)
if is_series:
idx_ans = self.fields[idx].sort_keys_for_books(
get_metadata, get_lang, all_book_ids)
get_metadata, lang_map, all_book_ids)
ans = {k:(v, idx_ans[k]) for k, v in ans.iteritems()}
return ans
@ -412,8 +442,14 @@ class Cache(object):
return sorted(all_book_ids, key=partial(SortKey, fields, sort_keys))
@read_api
def search(self, query, restriction):
return self._search_api(self, query, restriction)
def search(self, query, restriction, virtual_fields=None):
return self._search_api(self, query, restriction,
virtual_fields=virtual_fields)
@read_api
def get_categories(self, sort='name', book_ids=None, icon_map=None):
return get_categories(self, sort=sort, book_ids=book_ids,
icon_map=icon_map)
# }}}

View File

@ -0,0 +1,118 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from functools import partial
from operator import attrgetter
from calibre.library.field_metadata import TagsIcons
from calibre.utils.config_base import tweaks
from calibre.utils.icu import sort_key
CATEGORY_SORTS = { 'name', 'popularity', 'rating' }
class Tag(object):
def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None,
tooltip=None, icon=None, category=None, id_set=None,
is_editable=True, is_searchable=True, use_sort_as_name=False):
self.name = self.original_name = name
self.id = id
self.count = count
self.state = state
self.is_hierarchical = ''
self.is_editable = is_editable
self.is_searchable = is_searchable
self.id_set = id_set if id_set is not None else set([])
self.avg_rating = avg/2.0 if avg is not None else 0
self.sort = sort
self.use_sort_as_name = use_sort_as_name
if self.avg_rating > 0:
if tooltip:
tooltip = tooltip + ': '
tooltip = _('%(tt)sAverage rating is %(rating)3.1f')%dict(
tt=tooltip, rating=self.avg_rating)
self.tooltip = tooltip
self.icon = icon
self.category = category
def __unicode__(self):
return u'%s:%s:%s:%s:%s:%s'%(self.name, self.count, self.id, self.state,
self.category, self.tooltip)
def __str__(self):
return unicode(self).encode('utf-8')
def __repr__(self):
return str(self)
def find_categories(field_metadata):
for category, cat in field_metadata.iteritems():
if (cat['is_category'] and cat['kind'] not in {'user', 'search'} and
category != 'news'):
yield (category, cat['is_multiple'].get('cache_to_list', None), False)
elif (cat['datatype'] == 'composite' and
cat['display'].get('make_category', False)):
yield (category, cat['is_multiple'].get('cache_to_list', None), True)
def create_tag_class(category, fm, icon_map):
cat = fm[category]
icon = None
tooltip = None if category in {'formats', 'identifiers'} else ('(' + category + ')')
label = fm.key_to_label(category)
if icon_map:
if not fm.is_custom_field(category):
if category in icon_map:
icon = icon_map[label]
else:
icon = icon_map['custom:']
icon_map[category] = icon
is_editable = category not in {'news', 'rating', 'languages', 'formats',
'identifiers'}
if (tweaks['categories_use_field_for_author_name'] == 'author_sort' and
(category == 'authors' or
(cat['display'].get('is_names', False) and
cat['is_custom'] and cat['is_multiple'] and
cat['datatype'] == 'text'))):
use_sort_as_name = True
else:
use_sort_as_name = False
return partial(Tag, use_sort_as_name=use_sort_as_name, icon=icon,
tooltip=tooltip, is_editable=is_editable,
category=category)
def get_categories(dbcache, sort='name', book_ids=None, icon_map=None):
if icon_map is not None and type(icon_map) != TagsIcons:
raise TypeError('icon_map passed to get_categories must be of type TagIcons')
if sort not in CATEGORY_SORTS:
raise ValueError('sort ' + sort + ' not a valid value')
fm = dbcache.field_metadata
book_rating_map = dbcache.fields['rating'].book_value_map
lang_map = dbcache.fields['languages'].book_value_map
categories = {}
book_ids = frozenset(book_ids) if book_ids else book_ids
for category, is_multiple, is_composite in find_categories(fm):
tag_class = create_tag_class(category, fm, icon_map)
cats = dbcache.fields[category].get_categories(
tag_class, book_rating_map, lang_map, book_ids)
if sort == 'popularity':
key=attrgetter('count')
elif sort == 'rating':
key=attrgetter('avg_rating')
else:
key=lambda x:sort_key(x.sort or x.name)
cats.sort(key=key)
categories[category] = cats
return categories

View File

@ -9,7 +9,7 @@ __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from threading import Lock
from collections import defaultdict
from collections import defaultdict, Counter
from calibre.db.tables import ONE_ONE, MANY_ONE, MANY_MANY
from calibre.ebooks.metadata import title_sort
@ -24,22 +24,26 @@ class Field(object):
def __init__(self, name, table):
self.name, self.table = name, table
self.has_text_data = self.metadata['datatype'] in ('text', 'comments',
'series', 'enumeration')
self.table_type = self.table.table_type
dt = self.metadata['datatype']
self.has_text_data = dt in {'text', 'comments', 'series', 'enumeration'}
self.table_type = self.table.table_type
self._sort_key = (sort_key if dt in ('text', 'series', 'enumeration') else lambda x: x)
self._default_sort_key = ''
if self.metadata['datatype'] in ('int', 'float', 'rating'):
if dt in { 'int', 'float', 'rating' }:
self._default_sort_key = 0
elif self.metadata['datatype'] == 'bool':
elif dt == 'bool':
self._default_sort_key = None
elif self.metadata['datatype'] == 'datetime':
elif dt == 'datetime':
self._default_sort_key = UNDEFINED_DATE
if self.name == 'languages':
self._sort_key = lambda x:sort_key(calibre_langcode_to_name(x))
self.is_multiple = (bool(self.metadata['is_multiple']) or self.name ==
'formats')
self.category_formatter = type(u'')
if dt == 'rating':
self.category_formatter = lambda x:'\u2605'*int(x/2)
elif name == 'languages':
self.category_formatter = calibre_langcode_to_name
@property
def metadata(self):
@ -63,7 +67,7 @@ class Field(object):
def books_for(self, item_id):
'''
Return the ids of all books associated with the item identified by
item_id as a tuple. An empty tuple is returned if no books are found.
item_id as a set. An empty set is returned if no books are found.
'''
raise NotImplementedError()
@ -77,7 +81,7 @@ class Field(object):
'''
return iter(())
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
'''
Return a mapping of book_id -> sort_key. The sort key is suitable for
use in sorting the list of all books by this field, via the python cmp
@ -94,6 +98,27 @@ class Field(object):
'''
raise NotImplementedError()
def get_categories(self, tag_class, book_rating_map, lang_map, book_ids=None):
ans = []
if not self.is_many:
return ans
special_sort = hasattr(self, 'category_sort_value')
for item_id, item_book_ids in self.table.col_book_map.iteritems():
if book_ids is not None:
item_book_ids = item_book_ids.intersection(book_ids)
if item_book_ids:
ratings = tuple(r for r in (book_rating_map.get(book_id, 0) for
book_id in item_book_ids) if r > 0)
avg = sum(ratings)/len(ratings) if ratings else 0
name = self.category_formatter(self.table.id_map[item_id])
sval = (self.category_sort_value(item_id, item_book_ids, lang_map)
if special_sort else name)
c = tag_class(name, id=item_id, sort=sval, avg=avg,
id_set=item_book_ids, count=len(item_book_ids))
ans.append(c)
return ans
class OneToOneField(Field):
def for_book(self, book_id, default_value=None):
@ -103,12 +128,12 @@ class OneToOneField(Field):
return (book_id,)
def books_for(self, item_id):
return (item_id,)
return {item_id}
def __iter__(self):
return self.table.book_col_map.iterkeys()
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
return {id_ : self._sort_key(self.table.book_col_map.get(id_,
self._default_sort_key)) for id_ in all_book_ids}
@ -150,7 +175,7 @@ class CompositeField(OneToOneField):
ans = mi.get('#'+self.metadata['label'])
return ans
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
return {id_ : sort_key(self.get_value_with_cache(id_, get_metadata)) for id_ in
all_book_ids}
@ -193,7 +218,7 @@ class OnDeviceField(OneToOneField):
def __iter__(self):
return iter(())
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
return {id_ : self.for_book(id_) for id_ in
all_book_ids}
@ -223,12 +248,12 @@ class ManyToOneField(Field):
return (id_,)
def books_for(self, item_id):
return self.table.col_book_map.get(item_id, ())
return self.table.col_book_map.get(item_id, set())
def __iter__(self):
return self.table.id_map.iterkeys()
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
ans = {id_ : self.table.book_col_map.get(id_, None)
for id_ in all_book_ids}
sk_map = {cid : (self._default_sort_key if cid is None else
@ -238,11 +263,17 @@ class ManyToOneField(Field):
def iter_searchable_values(self, get_metadata, candidates, default_value=None):
cbm = self.table.col_book_map
empty = set()
for item_id, val in self.table.id_map.iteritems():
book_ids = set(cbm.get(item_id, ())).intersection(candidates)
book_ids = cbm.get(item_id, empty).intersection(candidates)
if book_ids:
yield val, book_ids
@property
def book_value_map(self):
return {book_id:self.table.id_map[item_id] for book_id, item_id in
self.table.book_col_map.iteritems()}
class ManyToManyField(Field):
is_many = True
@ -263,12 +294,12 @@ class ManyToManyField(Field):
return self.table.book_col_map.get(book_id, ())
def books_for(self, item_id):
return self.table.col_book_map.get(item_id, ())
return self.table.col_book_map.get(item_id, set())
def __iter__(self):
return self.table.id_map.iterkeys()
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
ans = {id_ : self.table.book_col_map.get(id_, ())
for id_ in all_book_ids}
all_cids = set()
@ -282,8 +313,9 @@ class ManyToManyField(Field):
def iter_searchable_values(self, get_metadata, candidates, default_value=None):
cbm = self.table.col_book_map
empty = set()
for item_id, val in self.table.id_map.iteritems():
book_ids = set(cbm.get(item_id, ())).intersection(candidates)
book_ids = cbm.get(item_id, empty).intersection(candidates)
if book_ids:
yield val, book_ids
@ -295,6 +327,11 @@ class ManyToManyField(Field):
for count, book_ids in val_map.iteritems():
yield count, book_ids
@property
def book_value_map(self):
return {book_id:tuple(self.table.id_map[item_id] for item_id in item_ids)
for book_id, item_ids in self.table.book_col_map.iteritems()}
class IdentifiersField(ManyToManyField):
def for_book(self, book_id, default_value=None):
@ -303,7 +340,7 @@ class IdentifiersField(ManyToManyField):
ids = default_value
return ids
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
'Sort by identifier keys'
ans = {id_ : self.table.book_col_map.get(id_, ())
for id_ in all_book_ids}
@ -318,6 +355,17 @@ class IdentifiersField(ManyToManyField):
if val:
yield val, {book_id}
def get_categories(self, tag_class, book_rating_map, lang_map, book_ids=None):
ans = []
for id_key, item_book_ids in self.table.col_book_map.iteritems():
if book_ids is not None:
item_book_ids = item_book_ids.intersection(book_ids)
if item_book_ids:
c = tag_class(id_key, id_set=item_book_ids, count=len(item_book_ids))
ans.append(c)
return ans
class AuthorsField(ManyToManyField):
def author_data(self, author_id):
@ -327,6 +375,9 @@ class AuthorsField(ManyToManyField):
'link' : self.table.alink_map[author_id],
}
def category_sort_value(self, item_id, book_ids, lang_map):
return self.table.asort_map[item_id]
class FormatsField(ManyToManyField):
def for_book(self, book_id, default_value=None):
@ -346,21 +397,50 @@ class FormatsField(ManyToManyField):
for val, book_ids in val_map.iteritems():
yield val, book_ids
def get_categories(self, tag_class, book_rating_map, lang_map, book_ids=None):
ans = []
for fmt, item_book_ids in self.table.col_book_map.iteritems():
if book_ids is not None:
item_book_ids = item_book_ids.intersection(book_ids)
if item_book_ids:
c = tag_class(fmt, id_set=item_book_ids, count=len(item_book_ids))
ans.append(c)
return ans
class SeriesField(ManyToOneField):
def sort_key_for_series(self, book_id, get_lang, series_sort_order):
def sort_key_for_series(self, book_id, lang_map, series_sort_order):
sid = self.table.book_col_map.get(book_id, None)
if sid is None:
return self._default_sort_key
lang = lang_map.get(book_id, None) or None
if lang:
lang = lang[0]
return self._sort_key(title_sort(self.table.id_map[sid],
order=series_sort_order,
lang=get_lang(book_id)))
order=series_sort_order, lang=lang))
def sort_keys_for_books(self, get_metadata, get_lang, all_book_ids):
def sort_keys_for_books(self, get_metadata, lang_map, all_book_ids):
sso = tweaks['title_series_sorting']
return {book_id:self.sort_key_for_series(book_id, get_lang, sso) for book_id
return {book_id:self.sort_key_for_series(book_id, lang_map, sso) for book_id
in all_book_ids}
def category_sort_value(self, item_id, book_ids, lang_map):
lang = None
tss = tweaks['title_series_sorting']
if tss != 'strictly_alphabetic':
c = Counter()
for book_id in book_ids:
l = lang_map.get(book_id, None)
if l:
c[l[0]] += 1
if c:
lang = c.most_common(1)[0][0]
val = self.table.id_map[item_id]
return title_sort(val, order=tss, lang=lang)
def create_field(name, table):
cls = {
ONE_ONE : OneToOneField,

View File

@ -14,6 +14,7 @@ from datetime import timedelta
from calibre.utils.config_base import prefs
from calibre.utils.date import parse_date, UNDEFINED_DATE, now
from calibre.utils.icu import primary_find
from calibre.utils.localization import lang_map, canonicalize_lang
from calibre.utils.search_query_parser import SearchQueryParser, ParseException
# TODO: Thread safety of saved searches
@ -392,7 +393,7 @@ class Parser(SearchQueryParser):
def __init__(self, dbcache, all_book_ids, gst, date_search, num_search,
bool_search, keypair_search, limit_search_columns, limit_search_columns_to,
locations):
locations, virtual_fields):
self.dbcache, self.all_book_ids = dbcache, all_book_ids
self.all_search_locations = frozenset(locations)
self.grouped_search_terms = gst
@ -400,6 +401,9 @@ class Parser(SearchQueryParser):
self.bool_search, self.keypair_search = bool_search, keypair_search
self.limit_search_columns, self.limit_search_columns_to = (
limit_search_columns, limit_search_columns_to)
self.virtual_fields = virtual_fields or {}
if 'marked' not in self.virtual_fields:
self.virtual_fields['marked'] = self
super(Parser, self).__init__(locations, optimize=True)
@property
@ -411,8 +415,15 @@ class Parser(SearchQueryParser):
def field_iter(self, name, candidates):
get_metadata = partial(self.dbcache._get_metadata, get_user_categories=False)
return self.dbcache.fields[name].iter_searchable_values(get_metadata,
candidates)
try:
field = self.dbcache.fields[name]
except KeyError:
field = self.virtual_fields[name]
return field.iter_searchable_values(get_metadata, candidates)
def iter_searchable_values(self, *args, **kwargs):
for x in []:
yield x, set()
def get_matches(self, location, query, candidates=None,
allow_recursion=True):
@ -480,6 +491,8 @@ class Parser(SearchQueryParser):
pass
return matches
upf = prefs['use_primary_find_in_search']
if location in self.field_metadata:
fm = self.field_metadata[location]
dt = fm['datatype']
@ -519,7 +532,6 @@ class Parser(SearchQueryParser):
# is a special case within the case
if fm.get('is_csp', False):
field_iter = partial(self.field_iter, location, candidates)
upf = prefs['use_primary_find_in_search']
if location == 'identifiers' and original_location == 'isbn':
return self.keypair_search('=isbn:'+query, field_iter,
candidates, upf)
@ -529,6 +541,87 @@ class Parser(SearchQueryParser):
if len(location) >= 2 and location.startswith('@'):
return self.get_user_category_matches(location[1:], icu_lower(query), candidates)
# Everything else (and 'all' matches)
matchkind, query = _matchkind(query)
all_locs = set()
text_fields = set()
field_metadata = {}
for x, fm in self.field_metadata.iteritems():
if x.startswith('@'): continue
if fm['search_terms'] and x != 'series_sort':
all_locs.add(x)
field_metadata[x] = fm
if fm['datatype'] in { 'composite', 'text', 'comments', 'series', 'enumeration' }:
text_fields.add(x)
locations = all_locs if location == 'all' else {location}
current_candidates = set(candidates)
try:
rating_query = int(float(query)) * 2
except:
rating_query = None
try:
int_query = int(float(query))
except:
int_query = None
try:
float_query = float(query)
except:
float_query = None
for location in locations:
current_candidates -= matches
q = query
if location == 'languages':
q = canonicalize_lang(query)
if q is None:
lm = lang_map()
rm = {v.lower():k for k,v in lm.iteritems()}
q = rm.get(query, query)
if matchkind == CONTAINS_MATCH and q in {'true', 'false'}:
found = set()
for val, book_ids in self.field_iter(location, current_candidates):
if val and (not hasattr(val, 'strip') or val.strip()):
found |= book_ids
matches |= (found if q == 'true' else (current_candidates-found))
continue
dt = field_metadata.get(location, {}).get('datatype', None)
if dt == 'rating':
if rating_query is not None:
for val, book_ids in self.field_iter(location, current_candidates):
if val == rating_query:
matches |= book_ids
continue
if dt == 'float':
if float_query is not None:
for val, book_ids in self.field_iter(location, current_candidates):
if val == float_query:
matches |= book_ids
continue
if dt == 'int':
if int_query is not None:
for val, book_ids in self.field_iter(location, current_candidates):
if val == int_query:
matches |= book_ids
continue
if location in text_fields:
for val, book_ids in self.field_iter(location, current_candidates):
if val is not None:
if isinstance(val, basestring):
val = (val,)
if _match(q, val, matchkind, use_primary_find_in_search=upf):
matches |= book_ids
return matches
def get_user_category_matches(self, location, query, candidates):
@ -557,7 +650,7 @@ class Parser(SearchQueryParser):
class Search(object):
def __init__(self, all_search_locations):
def __init__(self, all_search_locations=()):
self.all_search_locations = all_search_locations
self.date_search = DateSearch()
self.num_search = NumericSearch()
@ -567,7 +660,7 @@ class Search(object):
def change_locations(self, newlocs):
self.all_search_locations = newlocs
def __call__(self, dbcache, query, search_restriction):
def __call__(self, dbcache, query, search_restriction, virtual_fields=None):
'''
Return the set of ids of all records that match the specified
query and restriction
@ -596,7 +689,8 @@ class Search(object):
self.date_search, self.num_search, self.bool_search,
self.keypair_search,
prefs[ 'limit_search_columns' ],
prefs[ 'limit_search_columns_to' ], self.all_search_locations)
prefs[ 'limit_search_columns_to' ], self.all_search_locations,
virtual_fields)
try:
ret = sqp.parse(q)

View File

@ -132,13 +132,10 @@ class ManyToOneTable(Table):
'SELECT book, {0} FROM {1}'.format(
self.metadata['link_column'], self.link_table)):
if row[1] not in self.col_book_map:
self.col_book_map[row[1]] = []
self.col_book_map[row[1]].append(row[0])
self.col_book_map[row[1]] = set()
self.col_book_map[row[1]].add(row[0])
self.book_col_map[row[0]] = row[1]
for key in tuple(self.col_book_map.iterkeys()):
self.col_book_map[key] = tuple(self.col_book_map[key])
class ManyToManyTable(ManyToOneTable):
'''
@ -154,15 +151,12 @@ class ManyToManyTable(ManyToOneTable):
for row in db.conn.execute(
self.selectq.format(self.metadata['link_column'], self.link_table)):
if row[1] not in self.col_book_map:
self.col_book_map[row[1]] = []
self.col_book_map[row[1]].append(row[0])
self.col_book_map[row[1]] = set()
self.col_book_map[row[1]].add(row[0])
if row[0] not in self.book_col_map:
self.book_col_map[row[0]] = []
self.book_col_map[row[0]].append(row[1])
for key in tuple(self.col_book_map.iterkeys()):
self.col_book_map[key] = tuple(self.col_book_map[key])
for key in tuple(self.book_col_map.iterkeys()):
self.book_col_map[key] = tuple(self.book_col_map[key])
@ -191,8 +185,8 @@ class FormatsTable(ManyToManyTable):
if row[1] is not None:
fmt = row[1].upper()
if fmt not in self.col_book_map:
self.col_book_map[fmt] = []
self.col_book_map[fmt].append(row[0])
self.col_book_map[fmt] = set()
self.col_book_map[fmt].add(row[0])
if row[0] not in self.book_col_map:
self.book_col_map[row[0]] = []
self.book_col_map[row[0]].append(fmt)
@ -200,9 +194,6 @@ class FormatsTable(ManyToManyTable):
self.fname_map[row[0]] = {}
self.fname_map[row[0]][fmt] = row[2]
for key in tuple(self.col_book_map.iterkeys()):
self.col_book_map[key] = tuple(self.col_book_map[key])
for key in tuple(self.book_col_map.iterkeys()):
self.book_col_map[key] = tuple(sorted(self.book_col_map[key]))
@ -215,15 +206,12 @@ class IdentifiersTable(ManyToManyTable):
for row in db.conn.execute('SELECT book, type, val FROM identifiers'):
if row[1] is not None and row[2] is not None:
if row[1] not in self.col_book_map:
self.col_book_map[row[1]] = []
self.col_book_map[row[1]].append(row[0])
self.col_book_map[row[1]] = set()
self.col_book_map[row[1]].add(row[0])
if row[0] not in self.book_col_map:
self.book_col_map[row[0]] = {}
self.book_col_map[row[0]][row[1]] = row[2]
for key in tuple(self.col_book_map.iterkeys()):
self.col_book_map[key] = tuple(self.col_book_map[key])
class LanguagesTable(ManyToManyTable):
def read_id_maps(self, db):

View File

@ -42,6 +42,7 @@ class BaseTest(unittest.TestCase):
if attr == 'format_metadata': continue # TODO: Not implemented yet
attr1, attr2 = getattr(mi1, attr), getattr(mi2, attr)
if attr == 'formats':
continue # TODO: Not implemented yet
attr1, attr2 = map(lambda x:tuple(x) if x else (), (attr1, attr2))
self.assertEqual(attr1, attr2,
'%s not the same: %r != %r'%(attr, attr1, attr2))

View File

@ -218,9 +218,17 @@ class ReadingTest(BaseTest):
'identifiers:t:n', 'identifiers:=test:=two', 'identifiers:x:y',
'identifiers:z',
# TODO: Tests for searching the size column and
# Text tests
'title:="Title One"', 'title:~title', '#enum:=one', '#enum:tw',
'#enum:false', '#enum:true', 'series:one', 'tags:one', 'tags:true',
'tags:false', '2', 'one', '20.02', '"publisher one"',
'"my comments one"',
# User categories
'@Good Authors:One', '@Good Series.good tags:two',
# TODO: Tests for searching the size and #formats columns and
# cover:true|false
# TODO: Tests for user categories searching
)}
old = None
@ -233,6 +241,18 @@ class ReadingTest(BaseTest):
# }}}
def test_get_categories(self): # {{{
'Check that get_categories() returns the same data for both backends'
from calibre.library.database2 import LibraryDatabase2
old = LibraryDatabase2(self.library_path)
old_categories = old.get_categories()
cache = self.init_cache(self.library_path)
import pprint
pprint.pprint(old_categories)
pprint.pprint(cache.get_categories())
# }}}
def tests():
return unittest.TestLoader().loadTestsFromTestCase(ReadingTest)

View File

@ -141,7 +141,7 @@ class ANDROID(USBMS):
# LG
0x1004 : {
0x61c5 : [0x100, 0x226, 0x227, 0x9999],
0x61c5 : [0x100, 0x226, 0x227, 0x229, 0x9999],
0x61cc : [0x226, 0x227, 0x9999, 0x100],
0x61ce : [0x226, 0x227, 0x9999, 0x100],
0x618e : [0x226, 0x227, 0x9999, 0x100],
@ -235,7 +235,7 @@ class ANDROID(USBMS):
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID',
'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E',
'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS',
'ICS']
'ICS', 'E400']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
@ -246,7 +246,7 @@ class ANDROID(USBMS):
'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0', 'XT875',
'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727',
'USB_FLASH_DRIVER', 'ANDROID', 'MID7042', '7035', 'VIEWPAD_7E',
'NOVO7', 'ADVANCED', 'TABLET_PC', 'F']
'NOVO7', 'ADVANCED', 'TABLET_PC', 'F', 'E400_SD_CARD']
OSX_MAIN_MEM = 'Android Device Main Memory'

View File

@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
import cStringIO, ctypes, datetime, os, platform, re, shutil, sys, tempfile, time
from calibre.constants import __appname__, __version__, DEBUG
from calibre.constants import __appname__, __version__, DEBUG, cache_dir
from calibre import fit_image, confirm_config_name, strftime as _strftime
from calibre.constants import isosx, iswindows
from calibre.devices.errors import OpenFeedback, UserFeedback
@ -289,9 +289,7 @@ class ITUNES(DriverBase):
# Properties
cached_books = {}
cache_dir = os.path.join(config_dir, 'caches', 'itunes')
calibre_library_path = prefs['library_path']
archive_path = os.path.join(cache_dir, "thumbs.zip")
description_prefix = "added by calibre"
ejected = False
iTunes = None
@ -309,6 +307,14 @@ class ITUNES(DriverBase):
update_msg = None
update_needed = False
@property
def cache_dir(self):
return os.path.join(cache_dir(), 'itunes')
@property
def archive_path(self):
return os.path.join(self.cache_dir, "thumbs.zip")
# Public methods
def add_books_to_metadata(self, locations, metadata, booklists):
'''

View File

@ -195,7 +195,7 @@ class PRST1(USBMS):
for i, row in enumerate(cursor):
try:
comp_date = int(os.path.getmtime(self.normalize_path(prefix + row[0])) * 1000);
except (OSError, IOError):
except (OSError, IOError, TypeError):
# In case the db has incorrect path info
continue
device_date = int(row[1]);

View File

@ -515,6 +515,7 @@ class HTMLPreProcessor(object):
if not getattr(self.extra_opts, 'keep_ligatures', False):
html = _ligpat.sub(lambda m:LIGATURES[m.group()], html)
user_sr_rules = {}
# Function for processing search and replace
def do_search_replace(search_pattern, replace_txt):
try:
@ -522,6 +523,7 @@ class HTMLPreProcessor(object):
if not replace_txt:
replace_txt = ''
rules.insert(0, (search_re, replace_txt))
user_sr_rules[(search_re, replace_txt)] = search_pattern
except Exception as e:
self.log.error('Failed to parse %r regexp because %s' %
(search, as_unicode(e)))
@ -587,7 +589,16 @@ class HTMLPreProcessor(object):
#dump(html, 'pre-preprocess')
for rule in rules + end_rules:
html = rule[0].sub(rule[1], html)
try:
html = rule[0].sub(rule[1], html)
except re.error as e:
if rule in user_sr_rules:
self.log.error(
'User supplied search & replace rule: %s -> %s '
'failed with error: %s, ignoring.'%(
user_sr_rules[rule], rule[1], e))
else:
raise
if is_pdftohtml and length > -1:
# Dehyphenate

View File

@ -200,7 +200,7 @@ class Source(Plugin):
#: during the identify phase
touched_fields = frozenset()
#: Set this to True if your plugin return HTML formatted comments
#: Set this to True if your plugin returns HTML formatted comments
has_html_comments = False
#: Setting this to True means that the browser object will add

View File

@ -98,6 +98,9 @@ _self_closing_pat = re.compile(
def close_self_closing_tags(raw):
return _self_closing_pat.sub(r'<\g<tag>\g<arg>></\g<tag>>', raw)
def uuid_id():
return 'u'+unicode(uuid.uuid4())
def iterlinks(root, find_links_in_css=True):
'''
Iterate over all links in a OEB Document.
@ -1528,7 +1531,7 @@ class TOC(object):
if parent is None:
parent = etree.Element(NCX('navMap'))
for node in self.nodes:
id = node.id or unicode(uuid.uuid4())
id = node.id or uuid_id()
po = node.play_order
if po == 0:
po = 1
@ -1634,10 +1637,10 @@ class PageList(object):
return self.pages.remove(page)
def to_ncx(self, parent=None):
plist = element(parent, NCX('pageList'), id=str(uuid.uuid4()))
plist = element(parent, NCX('pageList'), id=uuid_id())
values = dict((t, count(1)) for t in ('front', 'normal', 'special'))
for page in self.pages:
id = page.id or unicode(uuid.uuid4())
id = page.id or uuid_id()
type = page.type
value = str(values[type].next())
attrib = {'id': id, 'value': value, 'type': type, 'playOrder': '0'}

View File

@ -18,7 +18,7 @@ from calibre import guess_type
from calibre.ebooks.oeb.base import (XHTML, XHTML_NS, CSS_MIME, OEB_STYLES,
namespace, barename, XPath)
from calibre.ebooks.oeb.stylizer import Stylizer
from calibre.utils.filenames import ascii_filename
from calibre.utils.filenames import ascii_filename, ascii_text
COLLAPSE = re.compile(r'[ \t\r\n\v]+')
STRIPNUM = re.compile(r'[-0-9]+$')
@ -437,7 +437,7 @@ class CSSFlattener(object):
items.sort()
css = u';\n'.join(u'%s: %s' % (key, val) for key, val in items)
classes = node.get('class', '').strip() or 'calibre'
klass = STRIPNUM.sub('', classes.split()[0].replace('_', ''))
klass = ascii_text(STRIPNUM.sub('', classes.split()[0].replace('_', '')))
if css in styles:
match = styles[css]
else:

View File

@ -413,6 +413,7 @@ class RulesModel(QAbstractListModel): # {{{
rules = list(prefs['column_color_rules'])
self.rules = []
for col, template in rules:
if col not in self.fm: continue
try:
rule = rule_from_template(self.fm, template)
except:

View File

@ -6,10 +6,12 @@ __license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import os
from threading import Lock
from PyQt4.Qt import (QUrl, QCoreApplication)
from calibre.constants import cache_dir
from calibre.gui2 import open_url
from calibre.gui2.store import StorePlugin
from calibre.gui2.store.basic_config import BasicStoreConfig
@ -26,6 +28,16 @@ class MobileReadStore(BasicStoreConfig, StorePlugin):
StorePlugin.__init__(self, *args, **kwargs)
self.lock = Lock()
@property
def cache(self):
if not hasattr(self, '_mr_cache'):
from calibre.utils.config import JSONConfig
self._mr_cache = JSONConfig('mobileread_get_books')
self._mr_cache.file_path = os.path.join(cache_dir(),
'mobileread_get_books.json')
self._mr_cache.refresh()
return self._mr_cache
def open(self, parent=None, detail_item=None, external=False):
url = 'http://www.mobileread.com/'
@ -61,7 +73,7 @@ class MobileReadStore(BasicStoreConfig, StorePlugin):
suppress_progress=False):
if self.lock.acquire(False):
try:
update_thread = CacheUpdateThread(self.config, self.seralize_books, timeout)
update_thread = CacheUpdateThread(self.cache, self.seralize_books, timeout)
if not suppress_progress:
progress = CacheProgressDialog(parent)
progress.set_message(_('Updating MobileRead book cache...'))
@ -85,7 +97,7 @@ class MobileReadStore(BasicStoreConfig, StorePlugin):
self.lock.release()
def get_book_list(self):
return self.deseralize_books(self.config.get('book_list', []))
return self.deseralize_books(self.cache.get('book_list', []))
def seralize_books(self, books):
sbooks = []

View File

@ -9,7 +9,7 @@ from xml.sax.saxutils import escape
from calibre import (prepare_string_for_xml, strftime, force_unicode,
isbytestring)
from calibre.constants import isosx
from calibre.constants import isosx, cache_dir
from calibre.customize.conversion import DummyReporter
from calibre.customize.ui import output_profiles
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
@ -18,7 +18,6 @@ from calibre.ebooks.metadata import author_to_author_sort
from calibre.library.catalogs import AuthorSortMismatchException, EmptyCatalogException, \
InvalidGenresSourceFieldException
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.config import config_dir
from calibre.utils.date import format_date, is_date_undefined, now as nowf
from calibre.utils.filenames import ascii_text, shorten_components_to
from calibre.utils.icu import capitalize, collation_order, sort_key
@ -109,7 +108,7 @@ class CatalogBuilder(object):
self.plugin = plugin
self.reporter = report_progress
self.stylesheet = stylesheet
self.cache_dir = os.path.join(config_dir, 'caches', 'catalog')
self.cache_dir = os.path.join(cache_dir(), 'catalog')
self.catalog_path = PersistentTemporaryDirectory("_epub_mobi_catalog", prefix='')
self.content_dir = os.path.join(self.catalog_path, "content")
self.excluded_tags = self.get_excluded_tags()

View File

@ -44,47 +44,13 @@ from calibre.utils.recycle_bin import delete_file, delete_tree
from calibre.utils.formatter_functions import load_user_template_functions
from calibre.db.errors import NoSuchFormat
from calibre.db.lazy import FormatMetadata, FormatsList
from calibre.db.categories import Tag
from calibre.utils.localization import (canonicalize_lang,
calibre_langcode_to_name)
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
SPOOL_SIZE = 30*1024*1024
class Tag(object):
def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None,
tooltip=None, icon=None, category=None, id_set=None,
is_editable = True, is_searchable=True, use_sort_as_name=False):
self.name = self.original_name = name
self.id = id
self.count = count
self.state = state
self.is_hierarchical = ''
self.is_editable = is_editable
self.is_searchable = is_searchable
self.id_set = id_set if id_set is not None else set([])
self.avg_rating = avg/2.0 if avg is not None else 0
self.sort = sort
self.use_sort_as_name = use_sort_as_name
if self.avg_rating > 0:
if tooltip:
tooltip = tooltip + ': '
tooltip = _('%(tt)sAverage rating is %(rating)3.1f')%dict(
tt=tooltip, rating=self.avg_rating)
self.tooltip = tooltip
self.icon = icon
self.category = category
def __unicode__(self):
return u'%s:%s:%s:%s:%s:%s'%(self.name, self.count, self.id, self.state,
self.category, self.tooltip)
def __str__(self):
return unicode(self).encode('utf-8')
def __repr__(self):
return str(self)
class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
'''
An ebook metadata database that stores references to ebook files on disk.
@ -1220,7 +1186,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
loc.append(_('Card A'))
if b is not None:
loc.append(_('Card B'))
return ', '.join(loc) + ((' (%s books)'%count) if count > 1 else '')
return ', '.join(loc) + ((_(' (%s books)')%count) if count > 1 else '')
def set_book_on_device_func(self, func):
self.book_on_device_func = func

View File

@ -5,8 +5,8 @@
msgid ""
msgstr ""
"Project-Id-Version: calibre 0.9.15\n"
"POT-Creation-Date: 2013-01-18 09:12+IST\n"
"PO-Revision-Date: 2013-01-18 09:12+IST\n"
"POT-Creation-Date: 2013-01-22 10:10+IST\n"
"PO-Revision-Date: 2013-01-22 10:10+IST\n"
"Last-Translator: Automatically generated\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
@ -21,9 +21,9 @@ msgid "Does absolutely nothing"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/__init__.py:59
#: /home/kovid/work/calibre/src/calibre/db/cache.py:106
#: /home/kovid/work/calibre/src/calibre/db/cache.py:109
#: /home/kovid/work/calibre/src/calibre/db/cache.py:120
#: /home/kovid/work/calibre/src/calibre/db/cache.py:139
#: /home/kovid/work/calibre/src/calibre/db/cache.py:142
#: /home/kovid/work/calibre/src/calibre/db/cache.py:153
#: /home/kovid/work/calibre/src/calibre/devices/android/driver.py:379
#: /home/kovid/work/calibre/src/calibre/devices/android/driver.py:380
#: /home/kovid/work/calibre/src/calibre/devices/hanvon/driver.py:114
@ -42,8 +42,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/prst1/driver.py:469
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:480
#: /home/kovid/work/calibre/src/calibre/ebooks/chm/metadata.py:57
#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plugins/chm_input.py:109
#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plugins/chm_input.py:112
#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plugins/chm_input.py:183
#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plugins/comic_input.py:189
#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plugins/fb2_input.py:99
#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plugins/fb2_input.py:101
@ -106,10 +105,10 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/sources/ozon.py:130
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/sources/worker.py:26
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/txt.py:18
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:27
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:95
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:154
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:193
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:28
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:98
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:156
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:195
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/mobi6.py:618
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/utils.py:316
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/writer2/indexer.py:463
@ -155,11 +154,11 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/email.py:193
#: /home/kovid/work/calibre/src/calibre/gui2/email.py:208
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:439
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1103
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1319
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1322
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1325
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1413
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1104
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1320
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1323
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1326
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1414
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:85
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:250
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:261
@ -884,7 +883,7 @@ msgstr ""
msgid "Path to library too long. Must be less than %d characters."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/cache.py:134
#: /home/kovid/work/calibre/src/calibre/db/cache.py:167
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/book/base.py:666
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:67
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:678
@ -894,23 +893,88 @@ msgstr ""
msgid "Yes"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/fields.py:163
#: /home/kovid/work/calibre/src/calibre/db/fields.py:186
#: /home/kovid/work/calibre/src/calibre/library/database2.py:1218
msgid "Main"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/fields.py:165
#: /home/kovid/work/calibre/src/calibre/db/fields.py:188
#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:77
#: /home/kovid/work/calibre/src/calibre/library/database2.py:1220
msgid "Card A"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/fields.py:167
#: /home/kovid/work/calibre/src/calibre/db/fields.py:190
#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:79
#: /home/kovid/work/calibre/src/calibre/library/database2.py:1222
msgid "Card B"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/search.py:33
#: /home/kovid/work/calibre/src/calibre/db/search.py:313
#: /home/kovid/work/calibre/src/calibre/library/caches.py:135
#: /home/kovid/work/calibre/src/calibre/library/caches.py:577
msgid "checked"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/search.py:33
#: /home/kovid/work/calibre/src/calibre/db/search.py:311
#: /home/kovid/work/calibre/src/calibre/library/caches.py:135
#: /home/kovid/work/calibre/src/calibre/library/caches.py:575
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:229
msgid "yes"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/search.py:35
#: /home/kovid/work/calibre/src/calibre/db/search.py:310
#: /home/kovid/work/calibre/src/calibre/library/caches.py:137
#: /home/kovid/work/calibre/src/calibre/library/caches.py:574
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:229
msgid "no"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/search.py:35
#: /home/kovid/work/calibre/src/calibre/db/search.py:312
#: /home/kovid/work/calibre/src/calibre/library/caches.py:137
#: /home/kovid/work/calibre/src/calibre/library/caches.py:576
msgid "unchecked"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/search.py:110
#: /home/kovid/work/calibre/src/calibre/library/caches.py:313
msgid "today"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/search.py:111
#: /home/kovid/work/calibre/src/calibre/library/caches.py:314
msgid "yesterday"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/search.py:112
#: /home/kovid/work/calibre/src/calibre/library/caches.py:315
msgid "thismonth"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/search.py:113
#: /home/kovid/work/calibre/src/calibre/library/caches.py:316
msgid "daysago"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/search.py:314
#: /home/kovid/work/calibre/src/calibre/library/caches.py:578
msgid "empty"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/search.py:315
#: /home/kovid/work/calibre/src/calibre/library/caches.py:579
msgid "blank"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/search.py:324
#: /home/kovid/work/calibre/src/calibre/library/caches.py:591
msgid "Invalid boolean query \"{0}\""
msgstr ""
#: /home/kovid/work/calibre/src/calibre/debug.py:70
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:47
msgid "Cause a running calibre instance, if any, to be shutdown. Note that if there are running jobs, they will be silently aborted, so use with care."
@ -1123,8 +1187,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/bambook/driver.py:268
#: /home/kovid/work/calibre/src/calibre/devices/bambook/driver.py:324
#: /home/kovid/work/calibre/src/calibre/devices/mtp/driver.py:391
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1134
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1136
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1128
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1130
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:277
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:279
msgid "Transferring books to device..."
@ -1135,8 +1199,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:491
#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:525
#: /home/kovid/work/calibre/src/calibre/devices/mtp/driver.py:430
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1147
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1158
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1141
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1152
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:301
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:332
msgid "Adding books to device metadata listing..."
@ -1158,8 +1222,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/bambook/driver.py:374
#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:479
#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:486
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1190
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1196
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1202
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:366
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:371
msgid "Removing books from device metadata listing..."
@ -1668,7 +1732,7 @@ msgid "Communicate with MTP devices"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/mtp/driver.py:167
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:950
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:952
#: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:95
msgid "Get device information..."
msgstr ""
@ -1967,17 +2031,17 @@ msgstr ""
msgid "Too many connection attempts from %s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1312
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1306
#, python-format
msgid "Invalid port in options: %s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1320
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1314
#, python-format
msgid "Failed to connect to port %d. Try a different value."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1332
#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1326
msgid "Failed to allocate a random port"
msgstr ""
@ -3443,7 +3507,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/quickview.py:85
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/template_dialog.py:222
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:83
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1108
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1109
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:150
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/metadata_sources.py:162
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/models.py:39
@ -3456,7 +3520,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/book/base.py:770
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:85
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1109
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1110
#: /home/kovid/work/calibre/src/calibre/gui2/store/stores/mobileread/models.py:23
msgid "Author(s)"
msgstr ""
@ -3501,7 +3565,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/catalogs/epub_mobi_builder.py:982
#: /home/kovid/work/calibre/src/calibre/library/catalogs/epub_mobi_builder.py:1228
#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:201
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:802
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:804
msgid "Tags"
msgstr ""
@ -3742,7 +3806,7 @@ msgstr ""
msgid "Downloads metadata and covers from OZON.ru"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:58
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:61
msgid "Sample Book"
msgstr ""
@ -3778,7 +3842,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:1281
#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/htmltoc.py:15
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:221
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/toc.py:217
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/toc.py:219
msgid "Table of Contents"
msgstr ""
@ -3863,7 +3927,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/create_custom_column.py:71
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/metadata_sources.py:160
#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:176
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:800
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:802
msgid "Rating"
msgstr ""
@ -4985,8 +5049,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:101
#: /home/kovid/work/calibre/src/calibre/gui2/dnd.py:84
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:518
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:830
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:527
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:839
msgid "Download failed"
msgstr ""
@ -5018,7 +5082,7 @@ msgid "Download complete"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:123
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:892
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:901
msgid "Download log"
msgstr ""
@ -5249,7 +5313,7 @@ msgid "Click the show details button to see which ones."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/show_book_details.py:16
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:807
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:809
msgid "Show book details"
msgstr ""
@ -5799,7 +5863,7 @@ msgid "Click to open"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/book_details.py:180
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:856
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:858
msgid "Ids"
msgstr ""
@ -5809,7 +5873,7 @@ msgid "Book %(sidx)s of <span class=\"series_name\">%(series)s</span>"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/book_details.py:233
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1112
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1113
msgid "Collections"
msgstr ""
@ -8315,7 +8379,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/mtp_config.py:421
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/message_box.py:141
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:885
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:894
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/tweaks.py:344
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:227
msgid "Copy to clipboard"
@ -8815,7 +8879,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:77
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:87
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1110
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1111
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/create_custom_column.py:35
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/create_custom_column.py:76
#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:365
@ -8931,7 +8995,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/edit_authors_dialog.py:122
#: /home/kovid/work/calibre/src/calibre/gui2/lrf_renderer/main.py:160
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:527
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:536
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:729
msgid "No matches found"
msgstr ""
@ -9110,8 +9174,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/message_box.py:196
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/message_box.py:251
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:950
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:1059
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:959
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:1074
#: /home/kovid/work/calibre/src/calibre/gui2/proceed.py:48
msgid "View log"
msgstr ""
@ -11539,13 +11603,13 @@ msgid "Modified"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:819
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1455
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1456
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:335
msgid "The lookup/search name is \"{0}\""
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:825
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1457
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1458
msgid "This book's UUID is \"{0}\""
msgstr ""
@ -11574,20 +11638,20 @@ msgstr ""
msgid "Could not set data, click Show Details to see why."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1107
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1108
msgid "In Library"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1111
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1112
#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:355
msgid "Size"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1437
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1438
msgid "Marked for deletion"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1440
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1441
msgid "Double click to <b>edit</b> me<br><br>"
msgstr ""
@ -11690,7 +11754,7 @@ msgid "Previous Page"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/lrf_renderer/main_ui.py:133
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:947
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:956
#: /home/kovid/work/calibre/src/calibre/gui2/store/web_store_dialog_ui.py:62
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:215
msgid "Back"
@ -12131,7 +12195,7 @@ msgid "Edit Metadata"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:63
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:940
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:949
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:108
#: /home/kovid/work/calibre/src/calibre/web/feeds/templates.py:219
#: /home/kovid/work/calibre/src/calibre/web/feeds/templates.py:410
@ -12288,62 +12352,62 @@ msgid ""
"cover stage, and vice versa."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:292
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:301
msgid "See at"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:446
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:455
msgid "calibre is downloading metadata from: "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:468
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:477
msgid "Please wait"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:500
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:509
msgid "Query: "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:519
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:528
msgid "Failed to download metadata. Click Show Details to see details"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:528
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:537
msgid "Failed to find any books that match your search. Try making the search <b>less specific</b>. For example, use only the author's last name and a single distinctive word from the title.<p>To see the full log, click Show Details."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:636
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:645
msgid "Current cover"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:639
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:648
msgid "Searching..."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:800
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:809
#, python-format
msgid "Downloading covers for <b>%s</b>, please wait..."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:831
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:840
msgid "Failed to download any covers, click \"Show details\" for details."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:837
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:846
#, python-format
msgid "Could not find any covers for <b>%s</b>"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:839
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:848
#, python-format
msgid "Found <b>%(num)d</b> covers of %(title)s. Pick the one you like best."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:928
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:937
msgid "Downloading metadata..."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:1043
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:1058
msgid "Downloading cover..."
msgstr ""
@ -16693,56 +16757,6 @@ msgid ""
"<p>Stanza should see your calibre collection automatically. If not, try adding the URL http://myhostname:8080 as a new catalog in the Stanza reader on your iPhone. Here myhostname should be the fully qualified hostname or the IP address of the computer calibre is running on."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/caches.py:177
#: /home/kovid/work/calibre/src/calibre/library/caches.py:617
msgid "checked"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/caches.py:177
#: /home/kovid/work/calibre/src/calibre/library/caches.py:615
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:229
msgid "yes"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/caches.py:179
#: /home/kovid/work/calibre/src/calibre/library/caches.py:614
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:229
msgid "no"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/caches.py:179
#: /home/kovid/work/calibre/src/calibre/library/caches.py:616
msgid "unchecked"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/caches.py:355
msgid "today"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/caches.py:356
msgid "yesterday"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/caches.py:357
msgid "thismonth"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/caches.py:358
msgid "daysago"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/caches.py:618
msgid "empty"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/caches.py:619
msgid "blank"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/caches.py:631
msgid "Invalid boolean query \"{0}\""
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/catalogs/bibtex.py:36
#, python-format
msgid ""
@ -17766,6 +17780,11 @@ msgstr ""
msgid "creating custom column "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/database2.py:1223
#, python-format
msgid " (%s books)"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/database2.py:3698
#, python-format
msgid "<p>Migrating old database to ebook library in %s<br><center>"
@ -17985,19 +18004,19 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/ajax.py:317
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:355
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:649
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:651
msgid "All books"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/ajax.py:318
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:354
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:648
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:650
#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:584
msgid "Newest"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:65
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:518
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:520
msgid "Loading, please wait"
msgstr ""
@ -18050,65 +18069,65 @@ msgstr ""
msgid "Random book"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:403
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:472
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:405
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:474
msgid "Browse books by"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:408
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:410
msgid "Choose a category to browse by:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:543
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:545
msgid "Browsing by"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:544
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:546
msgid "Up"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:684
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:686
msgid "in"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:687
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:689
msgid "Books in"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:781
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:783
msgid "Other formats"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:788
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:790
#, python-format
msgid "Read %(title)s in the %(fmt)s format"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:793
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:795
msgid "Get"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:806
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:808
msgid "Details"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:808
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:810
msgid "Permalink"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:809
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:811
msgid "A permanent link to this book"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:821
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:823
msgid "This book has been deleted"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:927
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:929
msgid "in search"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:929
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:931
msgid "Matching books"
msgstr ""

View File

@ -32,6 +32,10 @@ class Browser(B):
B.set_cookiejar(self, *args, **kwargs)
self._clone_actions['set_cookiejar'] = ('set_cookiejar', args, kwargs)
def copy_cookies_from_jsbrowser(self, jsbrowser):
for cookie in jsbrowser.cookies:
self.cookiejar.set_cookie(cookie)
@property
def cookiejar(self):
return self._clone_actions['set_cookiejar'][1][0]

View File

@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en'
from future_builtins import map
from calibre.utils.fonts.utils import get_all_font_names
from calibre.utils.fonts.sfnt.container import UnsupportedFont
class FontMetrics(object):
@ -19,6 +20,10 @@ class FontMetrics(object):
'''
def __init__(self, sfnt):
for table in (b'head', b'hhea', b'hmtx', b'cmap', b'OS/2', b'post',
b'name'):
if table not in sfnt:
raise UnsupportedFont('This font has no %s table'%table)
self.sfnt = sfnt
self.head = self.sfnt[b'head']

View File

@ -332,6 +332,12 @@ class BasicNewsRecipe(Recipe):
#: ignore_duplicate_articles = {'title', 'url'}
ignore_duplicate_articles = None
#: If you set this True, then calibre will use javascript to login to the
#: website. This is needed for some websites that require the use of
#: javascript to login. If you set this to True you must implement the
#: :meth:`javascript_login` method, to do the actual logging in.
use_javascript_to_login = False
# See the built-in profiles for examples of these settings.
def short_title(self):
@ -404,8 +410,7 @@ class BasicNewsRecipe(Recipe):
'''
return url
@classmethod
def get_browser(cls, *args, **kwargs):
def get_browser(self, *args, **kwargs):
'''
Return a browser instance used to fetch documents from the web. By default
it returns a `mechanize <http://wwwsearch.sourceforge.net/mechanize/>`_
@ -427,9 +432,47 @@ class BasicNewsRecipe(Recipe):
return br
'''
br = browser(*args, **kwargs)
br.addheaders += [('Accept', '*/*')]
return br
if self.use_javascript_to_login:
if getattr(self, 'browser', None) is not None:
return self.clone_browser(self.browser)
from calibre.web.jsbrowser.browser import Browser
br = Browser()
with br:
self.javascript_login(br, self.username, self.password)
kwargs['user_agent'] = br.user_agent
ans = browser(*args, **kwargs)
ans.copy_cookies_from_jsbrowser(br)
return ans
else:
br = browser(*args, **kwargs)
br.addheaders += [('Accept', '*/*')]
return br
def javascript_login(self, browser, username, password):
'''
This method is used to login to a website that uses javascript for its
login form. After the login is complete, the cookies returned from the
website are copied to a normal (non-javascript) browser and the
download proceeds using those cookies.
An example implementation::
def javascript_login(self, browser, username, password):
browser.visit('http://some-page-that-has-a-login')
form = browser.select_form(nr=0) # Select the first form on the page
form['username'] = username
form['password'] = password
browser.submit(timeout=120) # Submit the form and wait at most two minutes for loading to complete
Note that you can also select forms with CSS2 selectors, like this::
browser.select_form('form#login_form')
browser.select_from('form[name="someform"]')
'''
raise NotImplementedError('You must implement the javascript_login()'
' method if you set use_javascript_to_login'
' to True')
def clone_browser(self, br):
'''

View File

@ -10,8 +10,6 @@ UTF-8 encoding with any charset declarations removed.
import sys, socket, os, urlparse, re, time, copy, urllib2, threading, traceback, imghdr
from urllib import url2pathname, quote
from httplib import responses
from PIL import Image
from cStringIO import StringIO
from base64 import b64decode
from calibre import browser, relpath, unicode_path
@ -21,6 +19,8 @@ from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag
from calibre.ebooks.chardet import xml_to_unicode
from calibre.utils.config import OptionParser
from calibre.utils.logging import Log
from calibre.utils.magick import Image
from calibre.utils.magick.draw import identify_data
class FetchError(Exception):
pass
@ -374,8 +374,8 @@ class RecursiveFetcher(object):
fname = ascii_filename('img'+str(c))
if isinstance(fname, unicode):
fname = fname.encode('ascii', 'replace')
imgpath = os.path.join(diskpath, fname+'.jpg')
if (imghdr.what(None, data) is None and b'<svg' in data[:1024]):
itype = imghdr.what(None, data)
if itype is None and b'<svg' in data[:1024]:
# SVG image
imgpath = os.path.join(diskpath, fname+'.svg')
with self.imagemap_lock:
@ -385,11 +385,18 @@ class RecursiveFetcher(object):
tag['src'] = imgpath
else:
try:
im = Image.open(StringIO(data)).convert('RGBA')
if itype not in {'png', 'jpg', 'jpeg'}:
itype == 'png' if itype == 'gif' else 'jpg'
im = Image()
im.load(data)
data = im.export(itype)
else:
identify_data(data)
imgpath = os.path.join(diskpath, fname+'.'+itype)
with self.imagemap_lock:
self.imagemap[iurl] = imgpath
with open(imgpath, 'wb') as x:
im.save(x, 'JPEG')
x.write(data)
tag['src'] = imgpath
except:
traceback.print_exc()

View File

@ -16,7 +16,7 @@ from PyQt4.Qt import (QObject, QNetworkAccessManager, QNetworkDiskCache,
from PyQt4.QtWebKit import QWebPage, QWebSettings, QWebView, QWebElement
from calibre import USER_AGENT, prints, get_proxies, get_proxy_info
from calibre.constants import ispy3, config_dir
from calibre.constants import ispy3, cache_dir
from calibre.utils.logging import ThreadSafeLog
from calibre.gui2 import must_use_qt
from calibre.web.jsbrowser.forms import FormsMixin
@ -44,7 +44,7 @@ class WebPage(QWebPage): # {{{
settings = self.settings()
if enable_developer_tools:
settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True)
QWebSettings.enablePersistentStorage(os.path.join(config_dir, 'caches',
QWebSettings.enablePersistentStorage(os.path.join(cache_dir(),
'webkit-persistence'))
QWebSettings.setMaximumPagesInCache(0)
@ -135,8 +135,7 @@ class NetworkAccessManager(QNetworkAccessManager): # {{{
self.log = log
if use_disk_cache:
self.cache = QNetworkDiskCache(self)
self.cache.setCacheDirectory(os.path.join(config_dir, 'caches',
'jsbrowser'))
self.cache.setCacheDirectory(os.path.join(cache_dir(), 'jsbrowser'))
self.setCache(self.cache)
self.sslErrors.connect(self.on_ssl_errors)
self.pf = ProxyFactory(log)
@ -303,6 +302,10 @@ class Browser(QObject, FormsMixin):
self.nam = NetworkAccessManager(log, use_disk_cache=use_disk_cache, parent=self)
self.page.setNetworkAccessManager(self.nam)
@property
def user_agent(self):
return self.page.user_agent
def _wait_for_load(self, timeout, url=None):
loop = QEventLoop(self)
start_time = time.time()
@ -422,3 +425,9 @@ class Browser(QObject, FormsMixin):
pass
self.nam = self.page = None
def __enter__(self):
pass
def __exit__(self, *args):
self.close()

View File

@ -11,6 +11,7 @@ import unittest, pprint, threading, time
import cherrypy
from calibre import browser
from calibre.web.jsbrowser.browser import Browser
from calibre.library.server.utils import (cookie_max_age_to_expires,
cookie_time_fmt)
@ -105,6 +106,12 @@ class Server(object):
import traceback
traceback.print_exc()
@cherrypy.expose
def receive_cookies(self):
self.received_cookies = {n:(c.value, dict(c)) for n, c in
dict(cherrypy.request.cookie).iteritems()}
return pprint.pformat(self.received_cookies)
class Test(unittest.TestCase):
@classmethod
@ -202,6 +209,26 @@ class Test(unittest.TestCase):
if fexp:
self.assertEqual(fexp, cexp)
def test_cookie_copy(self):
'Test copying of cookies from jsbrowser to mechanize'
self.assertEqual(self.browser.visit('http://127.0.0.1:%d/cookies'%self.port),
True)
sent_cookies = self.server.sent_cookies.copy()
self.browser.visit('http://127.0.0.1:%d/receive_cookies'%self.port)
orig_rc = self.server.received_cookies.copy()
br = browser(user_agent=self.browser.user_agent)
br.copy_cookies_from_jsbrowser(self.browser)
br.open('http://127.0.0.1:%d/receive_cookies'%self.port)
for name, vals in sent_cookies.iteritems():
val = vals[0]
try:
rval = self.server.received_cookies[name][0]
except:
self.fail('The cookie: %s was not received by the server')
self.assertEqual(val, rval,
'The received value for the cookie: %s, %s != %s'%(
name, rval, val))
self.assertEqual(orig_rc, self.server.received_cookies)
def tests():
return unittest.TestLoader().loadTestsFromTestCase(Test)