formats API

This commit is contained in:
Kovid Goyal 2013-07-14 17:07:58 +05:30
parent 9ec8aac3f4
commit 3e47e065ee
4 changed files with 101 additions and 11 deletions

View File

@ -8,7 +8,7 @@ __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
# Imports {{{ # Imports {{{
import os, shutil, uuid, json, glob, time, cPickle import os, shutil, uuid, json, glob, time, cPickle, hashlib
from functools import partial from functools import partial
import apsw import apsw
@ -17,7 +17,9 @@ 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 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.db.errors import NoSuchFormat
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 sort_key from calibre.utils.icu import sort_key
@ -926,6 +928,19 @@ class DB(object):
shutil.copyfile(candidates[0], fmt_path) shutil.copyfile(candidates[0], fmt_path)
return fmt_path return fmt_path
def format_hash(self, book_id, fmt, fname, path):
path = self.format_abspath(book_id, fmt, fname, path)
if path is None:
raise NoSuchFormat('Record %d has no fmt: %s'%(book_id, fmt))
sha = hashlib.sha256()
with lopen(path, 'rb') as f:
while True:
raw = f.read(SPOOL_SIZE)
sha.update(raw)
if len(raw) < SPOOL_SIZE:
break
return sha.hexdigest()
def format_metadata(self, book_id, fmt, fname, path): def format_metadata(self, book_id, fmt, fname, path):
path = self.format_abspath(book_id, fmt, fname, path) path = self.format_abspath(book_id, fmt, fname, path)
ans = {} ans = {}

View File

@ -408,7 +408,16 @@ class Cache(object):
return {aid:af.author_data(aid) for aid in author_ids if aid in af.table.id_map} return {aid:af.author_data(aid) for aid in author_ids if aid in af.table.id_map}
@read_api @read_api
def format_metadata(self, book_id, fmt, allow_cache=True): def format_hash(self, book_id, fmt):
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._field_for('path', book_id).replace('/', os.sep)
except:
raise NoSuchFormat('Record %d has no fmt: %s'%(book_id, fmt))
return self.backend.format_hash(book_id, fmt, name, path)
@api
def format_metadata(self, book_id, fmt, allow_cache=True, update_db=False):
if not fmt: if not fmt:
return {} return {}
fmt = fmt.upper() fmt = fmt.upper()
@ -416,18 +425,30 @@ class Cache(object):
x = self.format_metadata_cache[book_id].get(fmt, None) x = self.format_metadata_cache[book_id].get(fmt, None)
if x is not None: if x is not None:
return x return x
try: with self.read_lock:
name = self.fields['formats'].format_fname(book_id, fmt) try:
path = self._field_for('path', book_id).replace('/', os.sep) name = self.fields['formats'].format_fname(book_id, fmt)
except: path = self._field_for('path', book_id).replace('/', os.sep)
return {} except:
return {}
ans = {}
if path and name:
ans = self.backend.format_metadata(book_id, fmt, name, path)
self.format_metadata_cache[book_id][fmt] = ans
if update_db and 'size' in ans:
with self.write_lock:
max_size = self.fields['formats'].table.update_fmt(book_id, fmt, name, ans['size'], self.backend)
self.fields['size'].table.update_sizes({book_id: max_size})
ans = {}
if path and name:
ans = self.backend.format_metadata(book_id, fmt, name, path)
self.format_metadata_cache[book_id][fmt] = ans
return ans return ans
@read_api
def format_files(self, book_id):
field = self.fields['formats']
fmts = field.table.book_col_map.get(book_id, ())
return {fmt:field.format_fname(book_id, fmt) for fmt in fmts}
@read_api @read_api
def pref(self, name, default=None): def pref(self, name, default=None):
return self.backend.prefs.get(name, default) return self.backend.prefs.get(name, default)
@ -524,6 +545,7 @@ class Cache(object):
the path is different from the current path (taking case sensitivity the path is different from the current path (taking case sensitivity
into account). into account).
''' '''
fmt = (fmt or '').upper()
try: try:
name = self.fields['formats'].format_fname(book_id, fmt) name = self.fields['formats'].format_fname(book_id, fmt)
path = self._field_for('path', book_id).replace('/', os.sep) path = self._field_for('path', book_id).replace('/', os.sep)
@ -544,6 +566,7 @@ class Cache(object):
Apart from the viewer, I don't believe any of the others do any file Apart from the viewer, I don't believe any of the others do any file
I/O with the results of this call. I/O with the results of this call.
''' '''
fmt = (fmt or '').upper()
try: try:
name = self.fields['formats'].format_fname(book_id, fmt) name = self.fields['formats'].format_fname(book_id, fmt)
path = self._field_for('path', book_id).replace('/', os.sep) path = self._field_for('path', book_id).replace('/', os.sep)
@ -555,6 +578,7 @@ class Cache(object):
@read_api @read_api
def has_format(self, book_id, fmt): def has_format(self, book_id, fmt):
'Return True iff the format exists on disk' 'Return True iff the format exists on disk'
fmt = (fmt or '').upper()
try: try:
name = self.fields['formats'].format_fname(book_id, fmt) name = self.fields['formats'].format_fname(book_id, fmt)
path = self._field_for('path', book_id).replace('/', os.sep) path = self._field_for('path', book_id).replace('/', os.sep)
@ -601,6 +625,7 @@ class Cache(object):
this means that repeated calls yield the same this means that repeated calls yield the same
temp file (which is re-created each time) temp file (which is re-created each time)
''' '''
fmt = (fmt or '').upper()
ext = ('.'+fmt.lower()) if fmt else '' ext = ('.'+fmt.lower()) if fmt else ''
if as_path: if as_path:
if preserve_filename: if preserve_filename:

View File

@ -17,6 +17,7 @@ from calibre.db.adding import (
import_book_directory, recursive_import, add_catalog, add_news) import_book_directory, recursive_import, add_catalog, add_news)
from calibre.db.backend import DB from calibre.db.backend import DB
from calibre.db.cache import Cache from calibre.db.cache import Cache
from calibre.db.errors import NoSuchFormat
from calibre.db.categories import CATEGORY_SORTS from calibre.db.categories import CATEGORY_SORTS
from calibre.db.view import View from calibre.db.view import View
from calibre.db.write import clean_identifier from calibre.db.write import clean_identifier
@ -436,6 +437,43 @@ class LibraryDatabase(object):
def has_id(self, book_id): def has_id(self, book_id):
return book_id in self.new_api.all_book_ids() return book_id in self.new_api.all_book_ids()
def format(self, index, fmt, index_is_id=False, as_file=False, mode='r+b', as_path=False, preserve_filename=False):
book_id = index if index_is_id else self.id(index)
return self.new_api.format(book_id, fmt, as_file=as_file, as_path=as_path, preserve_filename=preserve_filename)
def format_abspath(self, index, fmt, index_is_id=False):
book_id = index if index_is_id else self.id(index)
return self.new_api.format_abspath(book_id, fmt)
def format_path(self, index, fmt, index_is_id=False):
book_id = index if index_is_id else self.id(index)
ans = self.new_api.format_abspath(book_id, fmt)
if ans is None:
raise NoSuchFormat('Record %d has no format: %s'%(book_id, fmt))
return ans
def format_files(self, index, index_is_id=False):
book_id = index if index_is_id else self.id(index)
return [(v, k) for k, v in self.new_api.format_files(book_id).iteritems()]
def format_metadata(self, book_id, fmt, allow_cache=True, update_db=False, commit=False):
return self.new_api.format_metadata(book_id, fmt, allow_cache=allow_cache, update_db=update_db)
def format_last_modified(self, book_id, fmt):
m = self.format_metadata(book_id, fmt)
if m:
return m['mtime']
def formats(self, index, index_is_id=False, verify_formats=True):
book_id = index if index_is_id else self.id(index)
ans = self.new_api.formats(book_id, verify_formats=verify_formats)
if ans:
return ','.join(ans)
def has_format(self, index, fmt, index_is_id=False):
book_id = index if index_is_id else self.id(index)
return self.new_api.has_format(book_id, fmt)
# Private interface {{{ # Private interface {{{
def __iter__(self): def __iter__(self):
for row in self.data.iterall(): for row in self.data.iterall():
@ -463,6 +501,7 @@ for prop in ('author_sort', 'authors', 'comment', 'comments', 'publisher',
return func return func
setattr(LibraryDatabase, prop, MT(getter(prop))) setattr(LibraryDatabase, prop, MT(getter(prop)))
LibraryDatabase.format_hash = MT(lambda self, book_id, fmt:self.new_api.format_hash(book_id, fmt))
LibraryDatabase.index = MT(lambda self, book_id, cache=False:self.data.id_to_index(book_id)) LibraryDatabase.index = MT(lambda self, book_id, cache=False:self.data.id_to_index(book_id))
LibraryDatabase.has_cover = MT(lambda self, book_id:self.new_api.field_for('cover', book_id)) LibraryDatabase.has_cover = MT(lambda self, book_id:self.new_api.field_for('cover', book_id))
LibraryDatabase.get_tags = MT(lambda self, book_id:set(self.new_api.field_for('tags', book_id))) LibraryDatabase.get_tags = MT(lambda self, book_id:set(self.new_api.field_for('tags', book_id)))

View File

@ -11,6 +11,7 @@ from io import BytesIO
from repr import repr from repr import repr
from functools import partial from functools import partial
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from operator import itemgetter
from calibre.db.tests.base import BaseTest from calibre.db.tests.base import BaseTest
@ -159,6 +160,11 @@ class LegacyTest(BaseTest):
for meth, args in { for meth, args in {
'get_next_series_num_for': [('A Series One',)], 'get_next_series_num_for': [('A Series One',)],
'format':[(1, 'FMT1', True), (2, 'FMT1', True), (0, 'xxxxxx')],
'has_format':[(1, 'FMT1', True), (2, 'FMT1', True), (0, 'xxxxxx')],
'@format_files':[(0,),(1,),(2,)],
'formats':[(0,),(1,),(2,)],
'format_hash':[(1, 'FMT1'),(1, 'FMT2'), (2, 'FMT1')],
'author_sort_from_authors': [(['Author One', 'Author Two', 'Unknown'],)], 'author_sort_from_authors': [(['Author One', 'Author Two', 'Unknown'],)],
'has_book':[(Metadata('title one'),), (Metadata('xxxx1111'),)], 'has_book':[(Metadata('title one'),), (Metadata('xxxx1111'),)],
'has_id':[(1,), (2,), (3,), (9999,)], 'has_id':[(1,), (2,), (3,), (9999,)],
@ -330,6 +336,7 @@ class LegacyTest(BaseTest):
'author_id', # replaced by get_author_id 'author_id', # replaced by get_author_id
'books_for_author', # broken 'books_for_author', # broken
'books_in_old_database', # unused 'books_in_old_database', # unused
'migrate_old', # no longer supported
# Internal API # Internal API
'clean_user_categories', 'cleanup_tags', 'books_list_filter', 'conn', 'connect', 'construct_file_name', 'clean_user_categories', 'cleanup_tags', 'books_list_filter', 'conn', 'connect', 'construct_file_name',
@ -337,6 +344,7 @@ class LegacyTest(BaseTest):
'run_import_plugins', 'vacuum', 'set_path', 'row', 'row_factory', 'rows', 'rmtree', 'series_index_pat', 'run_import_plugins', 'vacuum', 'set_path', 'row', 'row_factory', 'rows', 'rmtree', 'series_index_pat',
'import_old_database', 'dirtied_lock', 'dirtied_cache', 'dirty_queue_length', 'dirty_books_referencing', 'import_old_database', 'dirtied_lock', 'dirtied_cache', 'dirty_queue_length', 'dirty_books_referencing',
'windows_check_if_files_in_use', 'get_metadata_for_dump', 'get_a_dirtied_book', 'dirtied_sequence', 'windows_check_if_files_in_use', 'get_metadata_for_dump', 'get_a_dirtied_book', 'dirtied_sequence',
'format_filename_cache', 'format_metadata_cache', 'filter', 'create_version1',
} }
SKIP_ARGSPEC = { SKIP_ARGSPEC = {
'__init__', '__init__',
@ -411,6 +419,9 @@ class LegacyTest(BaseTest):
ndb = self.init_legacy(self.cloned_library) ndb = self.init_legacy(self.cloned_library)
db = self.init_old(self.cloned_library) db = self.init_old(self.cloned_library)
run_funcs(self, db, ndb, ( run_funcs(self, db, ndb, (
('+format_metadata', 1, 'FMT1', itemgetter('size')),
('+format_metadata', 1, 'FMT2', itemgetter('size')),
('+format_metadata', 2, 'FMT1', itemgetter('size')),
('get_tags', 0), ('get_tags', 1), ('get_tags', 2), ('get_tags', 0), ('get_tags', 1), ('get_tags', 2),
('is_tag_used', 'News'), ('is_tag_used', 'xchkjgfh'), ('is_tag_used', 'News'), ('is_tag_used', 'xchkjgfh'),
('bulk_modify_tags', (1,), ['t1'], ['News']), ('bulk_modify_tags', (1,), ['t1'], ['News']),