Template language: Add a couple of new functions to get the path to individual book formats and the calibre library as a whole

This commit is contained in:
Kovid Goyal 2012-09-18 22:56:45 +05:30
commit 5821ac063a
6 changed files with 296 additions and 85 deletions

View File

@ -10,9 +10,12 @@ Created on 29 Jun 2012
import socket, select, json, inspect, os, traceback, time, sys, random import socket, select, json, inspect, os, traceback, time, sys, random
import posixpath import posixpath
import hashlib, threading import hashlib, threading
import Queue
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from functools import wraps from functools import wraps
from errno import EAGAIN, EINTR from errno import EAGAIN, EINTR
from threading import Thread
from calibre import prints from calibre import prints
from calibre.constants import numeric_version, DEBUG from calibre.constants import numeric_version, DEBUG
@ -48,6 +51,110 @@ def synchronous(tlockname):
return _synched return _synched
class ConnectionListener (Thread):
def __init__(self, driver):
Thread.__init__(self)
self.daemon = True
self.driver = driver
self.keep_running = True
def stop(self):
self.keep_running = False
def run(self):
queue_not_serviced_count = 0
device_socket = None
while self.keep_running:
try:
time.sleep(1)
except:
# Happens during interpreter shutdown
break
if not self.keep_running:
break
if not self.driver.connection_queue.empty():
queue_not_serviced_count += 1
if queue_not_serviced_count >= 3:
self.driver._debug('queue not serviced')
try:
sock = self.driver.connection_queue.get_nowait()
s = self.driver._json_encode(
self.driver.opcodes['CALIBRE_BUSY'], {})
self.driver._send_byte_string(device_socket, (b'%d' % len(s)) + s)
sock.close()
except Queue.Empty:
pass
queue_not_serviced_count = 0
else:
queue_not_serviced_count = 0
if getattr(self.driver, 'broadcast_socket', None) is not None:
while True:
ans = select.select((self.driver.broadcast_socket,), (), (), 0)
if len(ans[0]) > 0:
try:
packet = self.driver.broadcast_socket.recvfrom(100)
remote = packet[1]
message = str(self.driver.ZEROCONF_CLIENT_STRING + b' (on ' +
str(socket.gethostname().partition('.')[0]) +
b'),' + str(self.driver.port))
self.driver._debug('received broadcast', packet, message)
self.driver.broadcast_socket.sendto(message, remote)
except:
pass
else:
break
if self.driver.connection_queue.empty() and \
getattr(self.driver, 'listen_socket', None) is not None:
ans = select.select((self.driver.listen_socket,), (), (), 0)
if len(ans[0]) > 0:
# timeout in 10 ms to detect rare case where the socket went
# way between the select and the accept
try:
self.driver._debug('attempt to open device socket')
device_socket = None
self.driver.listen_socket.settimeout(0.010)
device_socket, ign = eintr_retry_call(
self.driver.listen_socket.accept)
self.driver.listen_socket.settimeout(None)
device_socket.settimeout(None)
try:
peer = self.driver.device_socket.getpeername()[0]
attempts = self.drjver.connection_attempts.get(peer, 0)
if attempts >= self.MAX_UNSUCCESSFUL_CONNECTS:
self.driver._debug('too many connection attempts from', peer)
device_socket.close()
device_socket = None
# raise InitialConnectionError(_('Too many connection attempts from %s') % peer)
else:
self.driver.connection_attempts[peer] = attempts + 1
except InitialConnectionError:
raise
except:
pass
try:
self.driver.connection_queue.put_nowait(device_socket)
except Queue.Full:
device_socket.close()
device_socket = None
self.driver._debug('driver is not answering')
except socket.timeout:
pass
except socket.error:
x = sys.exc_info()[1]
self.driver._debug('unexpected socket exception', x.args[0])
device_socket.close()
device_socket = None
# raise
class SDBook(Book): class SDBook(Book):
def __init__(self, prefix, lpath, size=None, other=None): def __init__(self, prefix, lpath, size=None, other=None):
Book.__init__(self, prefix, lpath, size=size, other=other) Book.__init__(self, prefix, lpath, size=size, other=other)
@ -80,8 +187,20 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
NEWS_IN_FOLDER = True NEWS_IN_FOLDER = True
SUPPORTS_USE_AUTHOR_SORT = False SUPPORTS_USE_AUTHOR_SORT = False
WANTS_UPDATED_THUMBNAILS = True WANTS_UPDATED_THUMBNAILS = True
# Guess about the max length on windows. This number will be reduced by
# the length of the path on the client, and by the fudge factor below. We
# use this on all platforms because the device might be connected to windows
# in the future.
MAX_PATH_LEN = 250 MAX_PATH_LEN = 250
# guess of length of MTP name. The length of the full path to the folder
# on the device is added to this. That path includes the device's mount point
# making this number effectively around 10 to 15 larger.
PATH_FUDGE_FACTOR = 40
THUMBNAIL_HEIGHT = 160 THUMBNAIL_HEIGHT = 160
DEFAULT_THUMBNAIL_HEIGHT = 160
PREFIX = '' PREFIX = ''
BACKLOADING_ERROR_MESSAGE = None BACKLOADING_ERROR_MESSAGE = None
@ -112,6 +231,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
'OK' : 0, 'OK' : 0,
'BOOK_DATA' : 10, 'BOOK_DATA' : 10,
'BOOK_DONE' : 11, 'BOOK_DONE' : 11,
'CALIBRE_BUSY' : 18,
'DELETE_BOOK' : 13, 'DELETE_BOOK' : 13,
'DISPLAY_MESSAGE' : 17, 'DISPLAY_MESSAGE' : 17,
'FREE_SPACE' : 5, 'FREE_SPACE' : 5,
@ -248,7 +368,11 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
# remove the 'path' argument and all its uses. Also removed the calls to # remove the 'path' argument and all its uses. Also removed the calls to
# filename_callback and sanitize_path_components # filename_callback and sanitize_path_components
def _create_upload_path(self, mdata, fname, create_dirs=True): def _create_upload_path(self, mdata, fname, create_dirs=True):
maxlen = self.MAX_PATH_LEN fname = sanitize(fname)
ext = os.path.splitext(fname)[1]
maxlen = (self.MAX_PATH_LEN - (self.PATH_FUDGE_FACTOR +
self.exts_path_lengths.get(ext, self.PATH_FUDGE_FACTOR)))
special_tag = None special_tag = None
if mdata.tags: if mdata.tags:
@ -269,9 +393,6 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
template = "{title}_%d-%d-%d" % date template = "{title}_%d-%d-%d" % date
use_subdirs = self.SUPPORTS_SUB_DIRS and settings.use_subdirs use_subdirs = self.SUPPORTS_SUB_DIRS and settings.use_subdirs
fname = sanitize(fname)
ext = os.path.splitext(fname)[1]
from calibre.library.save_to_disk import get_components from calibre.library.save_to_disk import get_components
from calibre.library.save_to_disk import config from calibre.library.save_to_disk import config
opts = config().parse() opts = config().parse()
@ -346,6 +467,13 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
return json.dumps([op, res], encoding='utf-8') return json.dumps([op, res], encoding='utf-8')
# Network functions # Network functions
def _read_binary_from_net(self, length):
self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT)
v = self.device_socket.recv(length)
self.device_socket.settimeout(None)
return v
def _read_string_from_net(self): def _read_string_from_net(self):
data = bytes(0) data = bytes(0)
while True: while True:
@ -354,9 +482,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
break break
# recv seems to return a pointer into some internal buffer. # recv seems to return a pointer into some internal buffer.
# Things get trashed if we don't make a copy of the data. # Things get trashed if we don't make a copy of the data.
self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT) v = self._read_binary_from_net(2)
v = self.device_socket.recv(2)
self.device_socket.settimeout(None)
if len(v) == 0: if len(v) == 0:
return '' # documentation says the socket is broken permanently. return '' # documentation says the socket is broken permanently.
data += v data += v
@ -364,16 +490,14 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
data = data[dex:] data = data[dex:]
pos = len(data) pos = len(data)
while pos < total_len: while pos < total_len:
self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT) v = self._read_binary_from_net(total_len - pos)
v = self.device_socket.recv(total_len - pos)
self.device_socket.settimeout(None)
if len(v) == 0: if len(v) == 0:
return '' # documentation says the socket is broken permanently. return '' # documentation says the socket is broken permanently.
data += v data += v
pos += len(v) pos += len(v)
return data return data
def _send_byte_string(self, s): def _send_byte_string(self, sock, s):
if not isinstance(s, bytes): if not isinstance(s, bytes):
self._debug('given a non-byte string!') self._debug('given a non-byte string!')
raise PacketError("Internal error: found a string that isn't bytes") raise PacketError("Internal error: found a string that isn't bytes")
@ -382,11 +506,11 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
while sent_len < total_len: while sent_len < total_len:
try: try:
if sent_len == 0: if sent_len == 0:
amt_sent = self.device_socket.send(s) amt_sent = sock.send(s)
else: else:
amt_sent = self.device_socket.send(s[sent_len:]) amt_sent = sock.send(s[sent_len:])
if amt_sent <= 0: if amt_sent <= 0:
raise IOError('Bad write on device socket') raise IOError('Bad write on socket')
sent_len += amt_sent sent_len += amt_sent
except socket.error as e: except socket.error as e:
self._debug('socket error', e, e.errno) self._debug('socket error', e, e.errno)
@ -410,7 +534,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
if print_debug_info and extra_debug: if print_debug_info and extra_debug:
self._debug('send string', s) self._debug('send string', s)
self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT) self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT)
self._send_byte_string((b'%d' % len(s)) + s) self._send_byte_string(self.device_socket, (b'%d' % len(s)) + s)
if not wait_for_response: if not wait_for_response:
return None, None return None, None
return self._receive_from_client(print_debug_info=print_debug_info) return self._receive_from_client(print_debug_info=print_debug_info)
@ -466,11 +590,12 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
length = infile.tell() length = infile.tell()
book_metadata.size = length book_metadata.size = length
infile.seek(0) infile.seek(0)
self._debug(lpath, length)
opcode, result = self._call_client('SEND_BOOK', {'lpath': lpath, 'length': length, opcode, result = self._call_client('SEND_BOOK', {'lpath': lpath, 'length': length,
'metadata': book_metadata, 'thisBook': this_book, 'metadata': book_metadata, 'thisBook': this_book,
'totalBooks': total_books, 'totalBooks': total_books,
'willStreamBooks': self.client_can_stream_books}, 'willStreamBooks': self.client_can_stream_books,
'willStreamBinary' : self.client_can_receive_book_binary},
print_debug_info=False, print_debug_info=False,
wait_for_response=(not self.client_can_stream_books)) wait_for_response=(not self.client_can_stream_books))
@ -483,6 +608,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
blen = len(b) blen = len(b)
if not b: if not b:
break break
if self.client_can_stream_books and self.client_can_receive_book_binary:
self._send_byte_string(self.device_socket, b)
else:
b = b64encode(b) b = b64encode(b)
opcode, result = self._call_client('BOOK_DATA', opcode, result = self._call_client('BOOK_DATA',
{'lpath': lpath, 'position': pos, 'data': b}, {'lpath': lpath, 'position': pos, 'data': b},
@ -493,6 +621,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self._debug('protocol error', opcode) self._debug('protocol error', opcode)
failed = True failed = True
break break
if not (self.client_can_stream_books and self.client_can_receive_book_binary):
self._call_client('BOOK_DONE', {'lpath': lpath}) self._call_client('BOOK_DONE', {'lpath': lpath})
self.time = None self.time = None
if close_: if close_:
@ -517,7 +646,8 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
v = self.known_metadata.get(book.lpath, None) v = self.known_metadata.get(book.lpath, None)
if v is not None: if v is not None:
return (v.get('uuid', None) == book.get('uuid', None) and return (v.get('uuid', None) == book.get('uuid', None) and
v.get('last_modified', None) == book.get('last_modified', None)) v.get('last_modified', None) == book.get('last_modified', None) and
v.get('thumbnail', None) == book.get('thumbnail', None))
return False return False
def _set_known_metadata(self, book, remove=False): def _set_known_metadata(self, book, remove=False):
@ -620,17 +750,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
break break
if getattr(self, 'listen_socket', None) is not None: if getattr(self, 'listen_socket', None) is not None:
ans = select.select((self.listen_socket,), (), (), 0)
if len(ans[0]) > 0:
# timeout in 10 ms to detect rare case where the socket went
# way between the select and the accept
try: try:
self.device_socket = None ans = self.connection_queue.get_nowait()
self.listen_socket.settimeout(0.010) self.device_socket = ans
self.device_socket, ign = eintr_retry_call(
self.listen_socket.accept)
self.listen_socket.settimeout(None)
self.device_socket.settimeout(None)
self.is_connected = True self.is_connected = True
try: try:
peer = self.device_socket.getpeername()[0] peer = self.device_socket.getpeername()[0]
@ -645,13 +767,8 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
raise raise
except: except:
pass pass
except socket.timeout: except Queue.Empty:
self._close_device_socket() self.is_connected = False
except socket.error:
x = sys.exc_info()[1]
self._debug('unexpected socket exception', x.args[0])
self._close_device_socket()
raise
return (self.is_connected, self) return (self.is_connected, self)
return (False, None) return (False, None)
@ -705,17 +822,34 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self._debug('Device can stream books', self.client_can_stream_books) self._debug('Device can stream books', self.client_can_stream_books)
self.client_can_stream_metadata = result.get('canStreamMetadata', False) self.client_can_stream_metadata = result.get('canStreamMetadata', False)
self._debug('Device can stream metadata', self.client_can_stream_metadata) self._debug('Device can stream metadata', self.client_can_stream_metadata)
self.client_can_receive_book_binary = result.get('canReceiveBookBinary', False)
self._debug('Device can receive book binary', self.client_can_stream_metadata)
self.max_book_packet_len = result.get('maxBookContentPacketLen', self.max_book_packet_len = result.get('maxBookContentPacketLen',
self.BASE_PACKET_LEN) self.BASE_PACKET_LEN)
self._debug('max_book_packet_len', self.max_book_packet_len)
exts = result.get('acceptedExtensions', None) exts = result.get('acceptedExtensions', None)
if exts is None or not isinstance(exts, list) or len(exts) == 0: if exts is None or not isinstance(exts, list) or len(exts) == 0:
self._debug('Protocol error - bogus accepted extensions') self._debug('Protocol error - bogus accepted extensions')
self._close_device_socket() self._close_device_socket()
return False return False
config = self._configProxy() config = self._configProxy()
config['format_map'] = exts config['format_map'] = exts
self._debug('selected formats', config['format_map']) self._debug('selected formats', config['format_map'])
self.exts_path_lengths = result.get('extensionPathLengths', {})
self._debug('extension path lengths', self.exts_path_lengths)
self.THUMBNAIL_HEIGHT = result.get('coverHeight', self.DEFAULT_THUMBNAIL_HEIGHT)
if 'coverWidth' in result:
# Setting this field forces the aspect ratio
self.THUMBNAIL_WIDTH = result.get('coverWidth',
(self.DEFAULT_THUMBNAIL_HEIGHT/3) * 4)
elif hasattr(self, 'THUMBNAIL_WIDTH'):
delattr(self, 'THUMBNAIL_WIDTH')
if password: if password:
returned_hash = result.get('passwordHash', None) returned_hash = result.get('passwordHash', None)
if result.get('passwordHash', None) is None: if result.get('passwordHash', None) is None:
@ -909,7 +1043,10 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
@synchronous('sync_lock') @synchronous('sync_lock')
def upload_books(self, files, names, on_card=None, end_session=True, def upload_books(self, files, names, on_card=None, end_session=True,
metadata=None): metadata=None):
if self.settings().extra_customization[self.OPT_EXTRA_DEBUG]:
self._debug(names) self._debug(names)
else:
self._debug()
paths = [] paths = []
names = iter(names) names = iter(names)
@ -956,7 +1093,11 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
@synchronous('sync_lock') @synchronous('sync_lock')
def delete_books(self, paths, end_session=True): def delete_books(self, paths, end_session=True):
if self.settings().extra_customization[self.OPT_EXTRA_DEBUG]:
self._debug(paths) self._debug(paths)
else:
self._debug()
for path in paths: for path in paths:
# the path has the prefix on it (I think) # the path has the prefix on it (I think)
path = self._strip_prefix(path) path = self._strip_prefix(path)
@ -968,7 +1109,11 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
@synchronous('sync_lock') @synchronous('sync_lock')
def remove_books_from_metadata(self, paths, booklists): def remove_books_from_metadata(self, paths, booklists):
if self.settings().extra_customization[self.OPT_EXTRA_DEBUG]:
self._debug(paths) self._debug(paths)
else:
self._debug()
for i, path in enumerate(paths): for i, path in enumerate(paths):
path = self._strip_prefix(path) path = self._strip_prefix(path)
self.report_progress((i + 1) / float(len(paths)), _('Removing books from device metadata listing...')) self.report_progress((i + 1) / float(len(paths)), _('Removing books from device metadata listing...'))
@ -983,17 +1128,33 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
@synchronous('sync_lock') @synchronous('sync_lock')
def get_file(self, path, outfile, end_session=True, this_book=None, total_books=None): def get_file(self, path, outfile, end_session=True, this_book=None, total_books=None):
if self.settings().extra_customization[self.OPT_EXTRA_DEBUG]:
self._debug(path) self._debug(path)
else:
self._debug()
eof = False eof = False
position = 0 position = 0
while not eof: while not eof:
opcode, result = self._call_client('GET_BOOK_FILE_SEGMENT', opcode, result = self._call_client('GET_BOOK_FILE_SEGMENT',
{'lpath' : path, 'position': position, {'lpath' : path, 'position': position,
'thisBook': this_book, 'totalBooks': total_books, 'thisBook': this_book, 'totalBooks': total_books,
'canStream':True}, 'canStream':True, 'canStreamBinary': True},
print_debug_info=False) print_debug_info=False)
if opcode == 'OK': if opcode == 'OK':
client_will_stream = 'willStream' in result client_will_stream = 'willStream' in result
client_will_stream_binary = 'willStreamBinary' in result
if (client_will_stream_binary):
length = result.get('fileLength')
remaining = length
while remaining > 0:
v = self._read_binary_from_net(min(remaining, self.max_book_packet_len))
outfile.write(v)
remaining -= len(v)
eof = True
else:
while not eof: while not eof:
if not result['eof']: if not result['eof']:
data = b64decode(result['data']) data = b64decode(result['data'])
@ -1127,17 +1288,22 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self._debug('broadcast socket listening on port', port) self._debug('broadcast socket listening on port', port)
break break
message = None
if port == 0: if port == 0:
self.broadcast_socket.close() self.broadcast_socket.close()
self.broadcast_socket = None self.broadcast_socket = None
message = 'attaching port to broadcast socket failed. This is not fatal.' message = 'attaching port to broadcast socket failed. This is not fatal.'
self._debug(message) self._debug(message)
return message
self.connection_queue = Queue.Queue(1)
self.connection_listener = ConnectionListener(self)
self.connection_listener.start()
return message
@synchronous('sync_lock') @synchronous('sync_lock')
def shutdown(self): def shutdown(self):
if getattr(self, 'listen_socket', None) is not None: if getattr(self, 'listen_socket', None) is not None:
self.connection_listener.stop()
unpublish_zeroconf('calibre smart device client', unpublish_zeroconf('calibre smart device client',
'_calibresmartdeviceapp._tcp', self.port, {}) '_calibresmartdeviceapp._tcp', self.port, {})
self._close_listen_socket() self._close_listen_socket()

View File

@ -1673,7 +1673,6 @@ class DeviceMixin(object): # {{{
if update_metadata: if update_metadata:
mi = db.get_metadata(id_, index_is_id=True, mi = db.get_metadata(id_, index_is_id=True,
get_cover=get_covers) get_cover=get_covers)
if book.get('last_modified', None) != mi.last_modified:
book.smart_update(db.get_metadata(id_, book.smart_update(db.get_metadata(id_,
index_is_id=True, index_is_id=True,
get_cover=get_covers), get_cover=get_covers),

View File

@ -61,13 +61,18 @@ def generate_test_db(library_path, # {{{
print 'Time per record:', t/float(num_of_records) print 'Time per record:', t/float(num_of_records)
# }}} # }}}
def current_library_name(): def current_library_path():
from calibre.utils.config import prefs from calibre.utils.config import prefs
import posixpath
path = prefs['library_path'] path = prefs['library_path']
if path: if path:
path = path.replace('\\', '/') path = path.replace('\\', '/')
while path.endswith('/'): while path.endswith('/'):
path = path[:-1] path = path[:-1]
return path
def current_library_name():
import posixpath
path = current_library_path()
if path:
return posixpath.basename(path) return posixpath.basename(path)

View File

@ -1262,6 +1262,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
ans = {} ans = {}
if path is not None: if path is not None:
stat = os.stat(path) stat = os.stat(path)
ans['path'] = path
ans['size'] = stat.st_size ans['size'] = stat.st_size
ans['mtime'] = utcfromtimestamp(stat.st_mtime) ans['mtime'] = utcfromtimestamp(stat.st_mtime)
self.format_metadata_cache[id_][fmt] = ans self.format_metadata_cache[id_][fmt] = ans
@ -2563,6 +2564,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if notify: if notify:
self.notify('metadata', [id]) self.notify('metadata', [id])
def get_id_from_uuid(self, uuid):
if uuid:
return self.conn.get('SELECT id FROM books WHERE uuid=?', (uuid,),
all=False)
# Convenience methods for tags_list_editor # Convenience methods for tags_list_editor
# Note: we generally do not need to refresh_ids because library_view will # Note: we generally do not need to refresh_ids because library_view will
# refresh everything. # refresh everything.

View File

@ -181,7 +181,7 @@ class AjaxServer(object):
return data, mi.last_modified return data, mi.last_modified
@Endpoint(set_last_modified=False) @Endpoint(set_last_modified=False)
def ajax_book(self, book_id, category_urls='true'): def ajax_book(self, book_id, category_urls='true', id_is_uuid='false'):
''' '''
Return the metadata of the book as a JSON dictionary. Return the metadata of the book as a JSON dictionary.
@ -192,6 +192,9 @@ class AjaxServer(object):
cherrypy.response.timeout = 3600 cherrypy.response.timeout = 3600
try: try:
if id_is_uuid == 'true':
book_id = self.db.get_id_from_uuid(book_id)
else:
book_id = int(book_id) book_id = int(book_id)
data, last_modified = self.ajax_book_to_json(book_id, data, last_modified = self.ajax_book_to_json(book_id,
get_category_urls=category_urls.lower()=='true') get_category_urls=category_urls.lower()=='true')
@ -204,7 +207,7 @@ class AjaxServer(object):
return data return data
@Endpoint(set_last_modified=False) @Endpoint(set_last_modified=False)
def ajax_books(self, ids=None, category_urls='true'): def ajax_books(self, ids=None, category_urls='true', id_is_uuid='false'):
''' '''
Return the metadata for a list of books specified as a comma separated Return the metadata for a list of books specified as a comma separated
list of ids. The metadata is returned as a dictionary mapping ids to list of ids. The metadata is returned as a dictionary mapping ids to
@ -218,6 +221,9 @@ class AjaxServer(object):
if ids is None: if ids is None:
raise cherrypy.HTTPError(404, 'Must specify some ids') raise cherrypy.HTTPError(404, 'Must specify some ids')
try: try:
if id_is_uuid == 'true':
ids = set(self.db.get_id_from_uuid(x) for x in ids.split(','))
else:
ids = set(int(x.strip()) for x in ids.split(',')) ids = set(int(x.strip()) for x in ids.split(','))
except: except:
raise cherrypy.HTTPError(404, 'ids must be a comma separated list' raise cherrypy.HTTPError(404, 'ids must be a comma separated list'

View File

@ -629,6 +629,22 @@ class BuiltinFormatsSizes(BuiltinFormatterFunction):
fmt_data = mi.get('format_metadata', {}) fmt_data = mi.get('format_metadata', {})
return ','.join(k.upper()+':'+str(v['size']) for k,v in fmt_data.iteritems()) return ','.join(k.upper()+':'+str(v['size']) for k,v in fmt_data.iteritems())
class BuiltinFormatsPaths(BuiltinFormatterFunction):
name = 'formats_paths'
arg_count = 0
category = 'Get values from metadata'
__doc__ = doc = _('formats_paths() -- return a comma-separated list of '
'colon_separated items representing full path to '
'the formats of a book. You can use the select '
'function to get the path for a specific '
'format. Note that format names are always uppercase, '
'as in EPUB.'
)
def evaluate(self, formatter, kwargs, mi, locals):
fmt_data = mi.get('format_metadata', {})
return ','.join(k.upper()+':'+str(v['path']) for k,v in fmt_data.iteritems())
class BuiltinHumanReadable(BuiltinFormatterFunction): class BuiltinHumanReadable(BuiltinFormatterFunction):
name = 'human_readable' name = 'human_readable'
arg_count = 1 arg_count = 1
@ -1146,6 +1162,18 @@ class BuiltinCurrentLibraryName(BuiltinFormatterFunction):
from calibre.library import current_library_name from calibre.library import current_library_name
return current_library_name() return current_library_name()
class BuiltinCurrentLibraryPath(BuiltinFormatterFunction):
name = 'current_library_path'
arg_count = 0
category = 'Get values from metadata'
__doc__ = doc = _('current_library_path() -- '
'return the path to the current calibre library. This function can '
'be called in template program mode using the template '
'"{:\'current_library_path()\'}".')
def evaluate(self, formatter, kwargs, mi, locals):
from calibre.library import current_library_path
return current_library_path()
class BuiltinFinishFormatting(BuiltinFormatterFunction): class BuiltinFinishFormatting(BuiltinFormatterFunction):
name = 'finish_formatting' name = 'finish_formatting'
arg_count = 4 arg_count = 4
@ -1168,7 +1196,8 @@ _formatter_builtins = [
BuiltinCurrentLibraryName(), BuiltinCurrentLibraryName(),
BuiltinDaysBetween(), BuiltinDivide(), BuiltinEval(), BuiltinFirstNonEmpty(), BuiltinDaysBetween(), BuiltinDivide(), BuiltinEval(), BuiltinFirstNonEmpty(),
BuiltinField(), BuiltinFinishFormatting(), BuiltinFormatDate(), BuiltinField(), BuiltinFinishFormatting(), BuiltinFormatDate(),
BuiltinFormatNumber(), BuiltinFormatsModtimes(), BuiltinFormatsSizes(), BuiltinFormatNumber(), BuiltinFormatsModtimes(), BuiltinFormatsPaths(),
BuiltinFormatsSizes(),
BuiltinHasCover(), BuiltinHumanReadable(), BuiltinIdentifierInList(), BuiltinHasCover(), BuiltinHumanReadable(), BuiltinIdentifierInList(),
BuiltinIfempty(), BuiltinLanguageCodes(), BuiltinLanguageStrings(), BuiltinIfempty(), BuiltinLanguageCodes(), BuiltinLanguageStrings(),
BuiltinInList(), BuiltinListDifference(), BuiltinListEquals(), BuiltinInList(), BuiltinListDifference(), BuiltinListEquals(),