Implement getting of covers and add tests for it

This commit is contained in:
Kovid Goyal 2013-02-20 13:08:05 +05:30
parent 277f0ce19c
commit 449410d317
3 changed files with 89 additions and 32 deletions

View File

@ -16,15 +16,14 @@ import apsw
from calibre import isbytestring, force_unicode, prints from calibre import isbytestring, force_unicode, prints
from calibre.constants import (iswindows, filesystem_encoding, from calibre.constants import (iswindows, filesystem_encoding,
preferred_encoding) preferred_encoding)
from calibre.ptempfile import PersistentTemporaryFile, SpooledTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.db import SPOOL_SIZE
from calibre.db.schema_upgrades import SchemaUpgrade from calibre.db.schema_upgrades import SchemaUpgrade
from calibre.library.field_metadata import FieldMetadata from calibre.library.field_metadata import FieldMetadata
from calibre.ebooks.metadata import title_sort, author_to_author_sort from calibre.ebooks.metadata import title_sort, author_to_author_sort
from calibre.utils.icu import strcmp from calibre.utils.icu import strcmp
from calibre.utils.config import to_json, from_json, prefs, tweaks from calibre.utils.config import to_json, from_json, prefs, tweaks
from calibre.utils.date import utcfromtimestamp, parse_date from calibre.utils.date import utcfromtimestamp, parse_date
from calibre.utils.filenames import is_case_sensitive from calibre.utils.filenames import (is_case_sensitive, samefile, hardlink_file)
from calibre.db.tables import (OneToOneTable, ManyToOneTable, ManyToManyTable, from calibre.db.tables import (OneToOneTable, ManyToOneTable, ManyToManyTable,
SizeTable, FormatsTable, AuthorsTable, IdentifiersTable, SizeTable, FormatsTable, AuthorsTable, IdentifiersTable,
CompositeTable, LanguagesTable) CompositeTable, LanguagesTable)
@ -863,10 +862,16 @@ class DB(object):
def has_format(self, book_id, fmt): def has_format(self, book_id, fmt):
return self.format_abspath(book_id, fmt) is not None return self.format_abspath(book_id, fmt) is not None
def cover(self, path, as_file=False, as_image=False, def copy_cover_to(self, path, dest, windows_atomic_move=None, use_hardlink=False):
as_path=False):
path = os.path.join(self.library_path, path, 'cover.jpg') path = os.path.join(self.library_path, path, 'cover.jpg')
ret = None if windows_atomic_move is not None:
if not isinstance(dest, basestring):
raise Exception("Error, you must pass the dest as a path when"
" using windows_atomic_move")
if os.access(path, os.R_OK) and dest and not samefile(dest, path):
windows_atomic_move.copy_path_to(path, dest)
return True
else:
if os.access(path, os.R_OK): if os.access(path, os.R_OK):
try: try:
f = lopen(path, 'rb') f = lopen(path, 'rb')
@ -874,23 +879,22 @@ class DB(object):
time.sleep(0.2) time.sleep(0.2)
f = lopen(path, 'rb') f = lopen(path, 'rb')
with f: with f:
if as_path: if hasattr(dest, 'write'):
pt = PersistentTemporaryFile('_dbcover.jpg') shutil.copyfileobj(f, dest)
with pt: if hasattr(dest, 'flush'):
shutil.copyfileobj(f, pt) dest.flush()
return pt.name return True
if as_file: elif dest and not samefile(dest, path):
ret = SpooledTemporaryFile(SPOOL_SIZE) if use_hardlink:
shutil.copyfileobj(f, ret) try:
ret.seek(0) hardlink_file(path, dest)
else: return True
ret = f.read() except:
if as_image: pass
from PyQt4.Qt import QImage with lopen(dest, 'wb') as d:
i = QImage() shutil.copyfileobj(f, d)
i.loadFromData(ret) return True
ret = i return False
return ret
# }}} # }}}

View File

@ -8,9 +8,11 @@ __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, traceback import os, traceback
from io import BytesIO
from collections import defaultdict from collections import defaultdict
from functools import wraps, partial from functools import wraps, partial
from calibre.db import SPOOL_SIZE
from calibre.db.categories import get_categories from calibre.db.categories import get_categories
from calibre.db.locking import create_locks, RecordLock from calibre.db.locking import create_locks, RecordLock
from calibre.db.fields import create_field from calibre.db.fields import create_field
@ -18,6 +20,7 @@ from calibre.db.search import Search
from calibre.db.tables import VirtualTable from calibre.db.tables import VirtualTable
from calibre.db.lazy import FormatMetadata, FormatsList from calibre.db.lazy import FormatMetadata, FormatsList
from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.base import Metadata
from calibre.ptempfile import PersistentTemporaryFile, SpooledTemporaryFile
from calibre.utils.date import now from calibre.utils.date import now
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
@ -397,15 +400,43 @@ class Cache(object):
:param as_path: If True return the image as a path pointing to a :param as_path: If True return the image as a path pointing to a
temporary file temporary file
''' '''
if as_file:
ret = SpooledTemporaryFile(SPOOL_SIZE)
if not self.copy_cover_to(book_id, ret): return
ret.seek(0)
elif as_path:
pt = PersistentTemporaryFile('_dbcover.jpg')
with pt:
if not self.copy_cover_to(book_id, pt): return
ret = pt.name
else:
buf = BytesIO()
if not self.copy_cover_to(book_id, buf): return
ret = buf.getvalue()
if as_image:
from PyQt4.Qt import QImage
i = QImage()
i.loadFromData(ret)
ret = i
return ret
@api
def copy_cover_to(self, book_id, dest):
'''
Copy the cover to the file like object ``dest``. Returns False
if no cover exists or dest is the same file as the current cover.
dest can also be a path in which case the cover is
copied to it iff the path is different from the current path (taking
case sensitivity into account).
'''
with self.read_lock: with self.read_lock:
try: try:
path = self._field_for('path', book_id).replace('/', os.sep) path = self._field_for('path', book_id).replace('/', os.sep)
except: except:
return None return False
with self.record_lock.lock(book_id): with self.record_lock.lock(book_id):
return self.backend.cover(path, as_file=as_file, as_image=as_image, return self.backend.copy_cover_to(path, dest)
as_path=as_path)
@read_api @read_api
def multisort(self, fields, ids_to_sort=None): def multisort(self, fields, ids_to_sort=None):

View File

@ -176,6 +176,10 @@ class ReadingTest(BaseTest):
old_metadata = {i:old.get_metadata( old_metadata = {i:old.get_metadata(
i, index_is_id=True, get_cover=True, cover_as_data=True) for i in i, index_is_id=True, get_cover=True, cover_as_data=True) for i in
xrange(1, 4)} xrange(1, 4)}
for mi in old_metadata.itervalues():
mi.format_metadata = dict(mi.format_metadata)
if mi.formats:
mi.formats = tuple(mi.formats)
old = None old = None
cache = self.init_cache(self.library_path) cache = self.init_cache(self.library_path)
@ -186,6 +190,24 @@ class ReadingTest(BaseTest):
for mi2, mi1 in zip(new_metadata.values(), old_metadata.values()): for mi2, mi1 in zip(new_metadata.values(), old_metadata.values()):
self.compare_metadata(mi1, mi2) self.compare_metadata(mi1, mi2)
def test_get_cover(self): # {{{
'Test cover() returns the same data for both backends'
from calibre.library.database2 import LibraryDatabase2
old = LibraryDatabase2(self.library_path)
covers = {i: old.cover(i, index_is_id=True) for i in (1, 2, 3)}
old = None
cache = self.init_cache(self.library_path)
for book_id, cdata in covers.iteritems():
self.assertEqual(cdata, cache.cover(book_id), 'Reading of cover failed')
f = cache.cover(book_id, as_file=True)
self.assertEqual(cdata, f.read() if f else f, 'Reading of cover as file failed')
if cdata:
with open(cache.cover(book_id, as_path=True), 'rb') as f:
self.assertEqual(cdata, f.read(), 'Reading of cover as path failed')
else:
self.assertEqual(cdata, cache.cover(book_id, as_path=True),
'Reading of null cover as path failed')
# }}} # }}}
def test_searching(self): # {{{ def test_searching(self): # {{{