diff --git a/recipes/acrimed.recipe b/recipes/acrimed.recipe
new file mode 100644
index 0000000000..acd98a063a
--- /dev/null
+++ b/recipes/acrimed.recipe
@@ -0,0 +1,30 @@
+# vim:fileencoding=utf-8
+from __future__ import unicode_literals
+
+__license__ = 'GPL v3'
+__copyright__ = '2012'
+'''
+acrimed.org
+'''
+
+import re
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class Acrimed(BasicNewsRecipe):
+ title = u'Acrimed'
+ __author__ = 'Gaƫtan Lehmann'
+ oldest_article = 30
+ max_articles_per_feed = 100
+ auto_cleanup = True
+ auto_cleanup_keep = '//div[@class="crayon article-chapo-4112 chapo"]'
+ language = 'fr'
+ masthead_url = 'http://www.acrimed.org/IMG/siteon0.gif'
+ feeds = [(u'Acrimed', u'http://www.acrimed.org/spip.php?page=backend')]
+
+ preprocess_regexps = [
+ (re.compile(r'
(.*) - Acrimed \| Action Critique M.*dias'), lambda m: '' + m.group(1) + ''),
+ (re.compile(r'(.*) - Acrimed \| Action Critique M.*dias
'), lambda m: '' + m.group(1) + '
')]
+
+ extra_css = """
+ .chapo{font-style:italic; margin: 1em 0 0.5em}
+ """
diff --git a/recipes/icons/acrimed.png b/recipes/icons/acrimed.png
new file mode 100644
index 0000000000..b88f368d3c
Binary files /dev/null and b/recipes/icons/acrimed.png differ
diff --git a/recipes/lamebook.recipe b/recipes/lamebook.recipe
index e449285d84..0f4fbe9dc0 100644
--- a/recipes/lamebook.recipe
+++ b/recipes/lamebook.recipe
@@ -13,6 +13,8 @@ class LamebookRecipe(BasicNewsRecipe):
language = 'en'
use_embedded_content = False
publication_type = 'blog'
+ reverse_article_order = True
+ encoding = 'utf-8'
keep_only_tags = [
dict(name='div', attrs={'class':'entry'})
diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py
index a322c9b6d7..bc8f1024f5 100644
--- a/src/calibre/db/cache.py
+++ b/src/calibre/db/cache.py
@@ -161,7 +161,8 @@ class Cache(object):
def _get_metadata(self, book_id, get_user_categories=True): # {{{
mi = Metadata(None, template_cache=self.formatter_template_cache)
author_ids = self._field_ids_for('authors', book_id)
- aut_list = [self._author_data(i) for i in author_ids]
+ adata = self._author_data(author_ids)
+ aut_list = [adata[i] for i in author_ids]
aum = []
aus = {}
aul = {}
@@ -363,6 +364,9 @@ class Cache(object):
@read_api
def all_field_names(self, field):
''' Frozen set of all fields names (should only be used for many-one and many-many fields) '''
+ if field == 'formats':
+ return frozenset(self.fields[field].table.col_book_map)
+
try:
return frozenset(self.fields[field].table.id_map.itervalues())
except AttributeError:
@@ -385,17 +389,21 @@ class Cache(object):
raise ValueError('%s is not a many-one or many-many field' % field)
@read_api
- def author_data(self, author_id):
+ def get_item_name(self, field, item_id):
+ return self.fields[field].table.id_map[item_id]
+
+ @read_api
+ def author_data(self, author_ids=None):
'''
Return author data as a dictionary with keys: name, sort, link
- If no author with the specified id is found an empty dictionary is
- returned.
+ If no authors with the specified ids are found an empty dictionary is
+ returned. If author_ids is None, data for all authors is returned.
'''
- try:
- return self.fields['authors'].author_data(author_id)
- except (KeyError, IndexError):
- return {}
+ af = self.fields['authors']
+ if author_ids is None:
+ author_ids = tuple(af.table.id_map)
+ return {aid:af.author_data(aid) for aid in author_ids if aid in af.table.id_map}
@read_api
def format_metadata(self, book_id, fmt, allow_cache=True):
diff --git a/src/calibre/db/legacy.py b/src/calibre/db/legacy.py
index 4c825247e9..8c5fa5bd31 100644
--- a/src/calibre/db/legacy.py
+++ b/src/calibre/db/legacy.py
@@ -6,10 +6,11 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal '
-import os, traceback
+import os, traceback, types
from functools import partial
from future_builtins import zip
+from calibre import force_unicode
from calibre.db import _get_next_series_num_for_list, _get_series_values
from calibre.db.adding import (
find_books_in_directory, import_book_directory_multiple,
@@ -62,17 +63,43 @@ class LibraryDatabase(object):
setattr(self, prop, partial(self.get_property,
loc=self.FIELD_MAP[fm]))
+ MT = lambda func: types.MethodType(func, self, LibraryDatabase)
+
for meth in ('get_next_series_num_for', 'has_book', 'author_sort_from_authors'):
setattr(self, meth, getattr(self.new_api, meth))
+ # Legacy API to get information about many-(one, many) fields
for field in ('authors', 'tags', 'publisher', 'series'):
+ def getter(field):
+ def func(self):
+ return self.new_api.all_field_names(field)
+ return func
name = field[:-1] if field in {'authors', 'tags'} else field
- setattr(self, 'all_%s_names' % name, partial(self.new_api.all_field_names, field))
+ setattr(self, 'all_%s_names' % name, MT(getter(field)))
+ self.all_formats = MT(lambda self:self.new_api.all_field_names('formats'))
for func, field in {'all_authors':'authors', 'all_titles':'title', 'all_tags2':'tags', 'all_series':'series', 'all_publishers':'publisher'}.iteritems():
setattr(self, func, partial(self.field_id_map, field))
- self.all_tags = lambda : list(self.all_tag_names())
+ self.all_tags = MT(lambda self: list(self.all_tag_names()))
+ self.get_authors_with_ids = MT(
+ lambda self: [[aid, adata['name'], adata['sort'], adata['link']] for aid, adata in self.new_api.author_data().iteritems()])
+ for field in ('tags', 'series', 'publishers', 'ratings', 'languages'):
+ def getter(field):
+ fname = field[:-1] if field in {'publishers', 'ratings'} else field
+ def func(self):
+ return [[tid, tag] for tid, tag in self.new_api.get_id_map(fname).iteritems()]
+ return func
+ setattr(self, 'get_%s_with_ids' % field,
+ MT(getter(field)))
+ for field in ('author', 'tag', 'series'):
+ def getter(field):
+ field = field if field == 'series' else (field+'s')
+ def func(self, item_id):
+ return self.new_api.get_item_name(field, item_id)
+ return func
+ setattr(self, '%s_name' % field, MT(getter(field)))
+ # Legacy field API
for func in (
'standard_field_keys', 'custom_field_keys', 'all_field_keys',
'searchable_fields', 'sortable_field_keys',
@@ -82,6 +109,12 @@ class LibraryDatabase(object):
self.metadata_for_field = self.field_metadata.get
self.last_update_check = self.last_modified()
+ self.book_on_device_func = None
+ # Cleaning is not required anymore
+ self.clean = self.clean_custom = MT(lambda self:None)
+ self.clean_standard_field = MT(lambda self, field, commit=False:None)
+ # apsw operates in autocommit mode
+ self.commit = MT(lambda self:None)
def close(self):
self.backend.close()
@@ -286,8 +319,71 @@ class LibraryDatabase(object):
mi = self.new_api.get_metadata(book_id, get_cover=key == 'cover')
return mi.get(key, default)
- # Private interface {{{
+ def authors_sort_strings(self, index, index_is_id=False):
+ book_id = index if index_is_id else self.data.index_to_id(index)
+ with self.new_api.read_lock:
+ authors = self.new_api._field_ids_for('authors', book_id)
+ adata = self.new_api._author_data(authors)
+ return [adata[aid]['sort'] for aid in authors]
+ def author_sort_from_book(self, index, index_is_id=False):
+ return ' & '.join(self.authors_sort_strings(index, index_is_id=index_is_id))
+
+ def authors_with_sort_strings(self, index, index_is_id=False):
+ book_id = index if index_is_id else self.data.index_to_id(index)
+ with self.new_api.read_lock:
+ authors = self.new_api._field_ids_for('authors', book_id)
+ adata = self.new_api._author_data(authors)
+ return [(aid, adata[aid]['name'], adata[aid]['sort'], adata[aid]['link']) for aid in authors]
+
+ def book_on_device(self, book_id):
+ if callable(self.book_on_device_func):
+ return self.book_on_device_func(book_id)
+ return None
+
+ def book_on_device_string(self, book_id):
+ loc = []
+ count = 0
+ on = self.book_on_device(book_id)
+ if on is not None:
+ m, a, b, count = on[:4]
+ if m is not None:
+ loc.append(_('Main'))
+ if a is not None:
+ loc.append(_('Card A'))
+ if b is not None:
+ loc.append(_('Card B'))
+ return ', '.join(loc) + ((_(' (%s books)')%count) if count > 1 else '')
+
+ def set_book_on_device_func(self, func):
+ self.book_on_device_func = func
+
+ def books_in_series(self, series_id):
+ with self.new_api.read_lock:
+ book_ids = self.new_api._books_for_field('series', series_id)
+ ff = self.new_api._field_for
+ return sorted(book_ids, key=lambda x:ff('series_index', x))
+
+ def books_in_series_of(self, index, index_is_id=False):
+ book_id = index if index_is_id else self.data.index_to_id(index)
+ series_ids = self.new_api.field_ids_for('series', book_id)
+ if not series_ids:
+ return []
+ return self.books_in_series(series_ids[0])
+
+ def books_with_same_title(self, mi, all_matches=True):
+ title = mi.title
+ ans = set()
+ if title:
+ title = icu_lower(force_unicode(title))
+ for book_id, x in self.new_api.get_id_map('title').iteritems():
+ if icu_lower(x) == title:
+ ans.add(book_id)
+ if not all_matches:
+ break
+ return ans
+
+ # Private interface {{{
def __iter__(self):
for row in self.data.iterall():
yield row
diff --git a/src/calibre/db/tests/legacy.py b/src/calibre/db/tests/legacy.py
index 6413dc0d8f..2f5c879532 100644
--- a/src/calibre/db/tests/legacy.py
+++ b/src/calibre/db/tests/legacy.py
@@ -32,7 +32,11 @@ class ET(object):
return newres
def compare_argspecs(old, new, attr):
- ok = len(old.args) == len(new.args) and len(old.defaults or ()) == len(new.defaults or ()) and old.args[-len(old.defaults or ()):] == new.args[-len(new.defaults or ()):] # noqa
+ # We dont compare the names of the non-keyword arguments as they are often
+ # different and they dont affect the usage of the API.
+ num = len(old.defaults or ())
+
+ ok = len(old.args) == len(new.args) and old.defaults == new.defaults and (num == 0 or old.args[-num:] == new.args[-num:])
if not ok:
raise AssertionError('The argspec for %s does not match. %r != %r' % (attr, old, new))
@@ -143,12 +147,12 @@ class LegacyTest(BaseTest):
'all_tag_names':[()],
'all_series_names':[()],
'all_publisher_names':[()],
- 'all_authors':[()],
- 'all_tags2':[()],
- 'all_tags':[()],
- 'all_publishers':[()],
- 'all_titles':[()],
- 'all_series':[()],
+ '!all_authors':[()],
+ '!all_tags2':[()],
+ '@all_tags':[()],
+ '!all_publishers':[()],
+ '!all_titles':[()],
+ '!all_series':[()],
'standard_field_keys':[()],
'all_field_keys':[()],
'searchable_fields':[()],
@@ -156,15 +160,32 @@ class LegacyTest(BaseTest):
'metadata_for_field':[('title',), ('tags',)],
'sortable_field_keys':[()],
'custom_field_keys':[(True,), (False,)],
- 'get_usage_count_by_id':[('authors',), ('tags',), ('series',), ('publisher',), ('#tags',), ('languages',)],
+ '!get_usage_count_by_id':[('authors',), ('tags',), ('series',), ('publisher',), ('#tags',), ('languages',)],
'get_field':[(1, 'title'), (2, 'tags'), (0, 'rating'), (1, 'authors'), (2, 'series'), (1, '#tags')],
+ 'all_formats':[()],
+ 'get_authors_with_ids':[()],
+ '!get_tags_with_ids':[()],
+ '!get_series_with_ids':[()],
+ '!get_publishers_with_ids':[()],
+ '!get_ratings_with_ids':[()],
+ '!get_languages_with_ids':[()],
+ 'tag_name':[(3,)],
+ 'author_name':[(3,)],
+ 'series_name':[(3,)],
+ 'authors_sort_strings':[(0,), (1,), (2,)],
+ 'author_sort_from_book':[(0,), (1,), (2,)],
+ 'authors_with_sort_strings':[(0,), (1,), (2,)],
+ 'book_on_device_string':[(1,), (2,), (3,)],
+ 'books_in_series_of':[(0,), (1,), (2,)],
+ 'books_with_same_title':[(Metadata(db.title(0)),), (Metadata(db.title(1)),), (Metadata('1234'),)],
}.iteritems():
for a in args:
fmt = lambda x: x
- if meth in {'get_usage_count_by_id', 'all_series', 'all_authors', 'all_tags2', 'all_publishers', 'all_titles'}:
- fmt = dict
- elif meth in {'all_tags'}:
- fmt = frozenset
+ if meth[0] in {'!', '@'}:
+ fmt = {'!':dict, '@':frozenset}[meth[0]]
+ meth = meth[1:]
+ elif meth == 'get_authors_with_ids':
+ fmt = lambda val:{x[0]:tuple(x[1:]) for x in val}
self.assertEqual(fmt(getattr(db, meth)(*a)), fmt(getattr(ndb, meth)(*a)),
'The method: %s() returned different results for argument %s' % (meth, a))
db.close()
@@ -242,9 +263,16 @@ class LegacyTest(BaseTest):
'_set_title', '_set_custom', '_update_author_in_cache',
# Feeds are now stored in the config folder
'get_feeds', 'get_feed', 'update_feed', 'remove_feeds', 'add_feed', 'set_feeds',
+ # Obsolete/broken methods
+ 'author_id', # replaced by get_author_id
+ 'books_for_author', # broken
+ 'books_in_old_database', # unused
+
+ # Internal API
+ 'clean_user_categories', 'cleanup_tags', 'books_list_filter',
}
SKIP_ARGSPEC = {
- '__init__', 'get_next_series_num_for', 'has_book', 'author_sort_from_authors', 'all_tags',
+ '__init__', 'get_next_series_num_for', 'has_book', 'author_sort_from_authors',
}
missing = []
@@ -309,3 +337,4 @@ class LegacyTest(BaseTest):
T(('n', object()))
old.close()
# }}}
+
diff --git a/src/calibre/db/view.py b/src/calibre/db/view.py
index 3f135860d9..ecd5182232 100644
--- a/src/calibre/db/view.py
+++ b/src/calibre/db/view.py
@@ -189,10 +189,8 @@ class View(object):
id_ = idx if index_is_id else self.index_to_id(idx)
with self.cache.read_lock:
ids = self.cache._field_ids_for('authors', id_)
- ans = []
- for id_ in ids:
- data = self.cache._author_data(id_)
- ans.append(':::'.join((data['name'], data['sort'], data['link'])))
+ adata = self.cache._author_data(ids)
+ ans = [':::'.join((adata[aid]['name'], adata[aid]['sort'], adata[aid]['link'])) for aid in ids if aid in adata]
return ':#:'.join(ans) if ans else default_value
def multisort(self, fields=[], subsort=False, only_ids=None):
diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py
index f0e532639a..40fa9d900b 100644
--- a/src/calibre/devices/mtp/driver.py
+++ b/src/calibre/devices/mtp/driver.py
@@ -83,7 +83,7 @@ class MTP_DEVICE(BASE):
return name in {
'alarms', 'android', 'dcim', 'movies', 'music', 'notifications',
'pictures', 'ringtones', 'samsung', 'sony', 'htc', 'bluetooth',
- 'games', 'lost.dir', 'video', 'whatsapp', 'image'}
+ 'games', 'lost.dir', 'video', 'whatsapp', 'image', 'com.zinio.mobile.android.reader'}
def configure_for_kindle_app(self):
proxy = self.prefs
@@ -159,16 +159,17 @@ class MTP_DEVICE(BASE):
def get_driveinfo(self):
if not self.driveinfo:
self.driveinfo = {}
- for sid, location_code in ( (self._main_id, 'main'), (self._carda_id,
+ for sid, location_code in ((self._main_id, 'main'), (self._carda_id,
'A'), (self._cardb_id, 'B')):
- if sid is None: continue
+ if sid is None:
+ continue
self._update_drive_info(self.filesystem_cache.storage(sid), location_code)
return self.driveinfo
def get_device_information(self, end_session=True):
self.report_progress(1.0, _('Get device information...'))
dinfo = self.get_basic_device_information()
- return tuple( list(dinfo) + [self.driveinfo] )
+ return tuple(list(dinfo) + [self.driveinfo])
def card_prefix(self, end_session=True):
return (self._carda_id, self._cardb_id)
@@ -190,7 +191,7 @@ class MTP_DEVICE(BASE):
from calibre.devices.mtp.books import JSONCodec
from calibre.devices.mtp.books import BookList, Book
self.report_progress(0, _('Listing files, this can take a while'))
- self.get_driveinfo() # Ensure driveinfo is loaded
+ self.get_driveinfo() # Ensure driveinfo is loaded
sid = {'carda':self._carda_id, 'cardb':self._cardb_id}.get(oncard,
self._main_id)
if sid is None:
@@ -230,7 +231,7 @@ class MTP_DEVICE(BASE):
cached_metadata.path = mtp_file.mtp_id_path
debug('Using cached metadata for',
'/'.join(mtp_file.full_path))
- continue # No need to update metadata
+ continue # No need to update metadata
book = cached_metadata
else:
book = Book(sid, '/'.join(relpath))
@@ -352,8 +353,8 @@ class MTP_DEVICE(BASE):
def prefix_for_location(self, on_card):
if self.location_paths is None:
self.location_paths = {}
- for sid, loc in ( (self._main_id, None), (self._carda_id, 'carda'),
- (self._cardb_id, 'cardb') ):
+ for sid, loc in ((self._main_id, None), (self._carda_id, 'carda'),
+ (self._cardb_id, 'cardb')):
if sid is not None:
storage = self.filesystem_cache.storage(sid)
prefixes = self.get_pref('send_to')
@@ -470,7 +471,8 @@ class MTP_DEVICE(BASE):
def remove_books_from_metadata(self, paths, booklists):
self.report_progress(0, _('Removing books from metadata'))
- class NextPath(Exception): pass
+ class NextPath(Exception):
+ pass
for i, path in enumerate(paths):
try:
@@ -549,3 +551,4 @@ if __name__ == '__main__':
dev.shutdown()
+