%PDF- %PDF-
| Direktori : /lib/calibre/calibre/db/cli/ |
| Current File : //lib/calibre/calibre/db/cli/cmd_list.py |
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import json
import os
import sys
from textwrap import TextWrapper
from calibre.db.cli.utils import str_width
from calibre.ebooks.metadata import authors_to_string
from calibre.utils.date import isoformat
from polyglot.builtins import as_bytes, iteritems
readonly = True
version = 0 # change this if you change signature of implementation()
FIELDS = {
'title', 'authors', 'author_sort', 'publisher', 'rating', 'timestamp', 'size',
'tags', 'comments', 'series', 'series_index', 'formats', 'isbn', 'uuid',
'pubdate', 'cover', 'last_modified', 'identifiers', 'languages'
}
def formats(db, book_id):
for fmt in db.formats(book_id, verify_formats=False):
path = db.format_abspath(book_id, fmt)
if path:
yield path.replace(os.sep, '/')
def cover(db, book_id):
return db.format_abspath(book_id, '__COVER_INTERNAL__')
def implementation(
db, notify_changes, fields, sort_by, ascending, search_text, limit
):
is_remote = notify_changes is not None
with db.safe_read_lock:
fm = db.field_metadata
afields = set(FIELDS) | {'id'}
for k in fm.custom_field_keys():
afields.add('*' + k[1:])
if 'all' in fields:
fields = sorted(afields)
sort_by = sort_by or 'id'
if sort_by not in afields:
return f'Unknown sort field: {sort_by}'
if not set(fields).issubset(afields):
return 'Unknown fields: {}'.format(', '.join(set(fields) - afields))
if search_text:
book_ids = db.multisort([(sort_by, ascending)],
ids_to_sort=db.search(search_text))
else:
book_ids = db.multisort([(sort_by, ascending)])
if limit > -1:
book_ids = book_ids[:limit]
data = {}
metadata = {}
for field in fields:
if field in 'id':
continue
if field == 'isbn':
x = db.all_field_for('identifiers', book_ids, default_value={})
data[field] = {k: v.get('isbn') or '' for k, v in iteritems(x)}
continue
field = field.replace('*', '#')
metadata[field] = fm[field]
if not is_remote:
if field == 'formats':
data[field] = {k: list(formats(db, k)) for k in book_ids}
continue
if field == 'cover':
data[field] = {k: cover(db, k) for k in book_ids}
continue
data[field] = db.all_field_for(field, book_ids)
return {'book_ids': book_ids, "data": data, 'metadata': metadata, 'fields':fields}
def stringify(data, metadata, for_machine):
for field, m in iteritems(metadata):
if field == 'authors':
data[field] = {
k: authors_to_string(v)
for k, v in iteritems(data[field])
}
else:
dt = m['datatype']
if dt == 'datetime':
data[field] = {
k: isoformat(v, as_utc=for_machine) if v else 'None'
for k, v in iteritems(data[field])
}
elif not for_machine:
ism = m['is_multiple']
if ism:
data[field] = {
k: ism['list_to_ui'].join(v)
for k, v in iteritems(data[field])
}
if field == 'formats':
data[field] = {
k: '[' + v + ']'
for k, v in iteritems(data[field])
}
def as_machine_data(book_ids, data, metadata):
for book_id in book_ids:
ans = {'id': book_id}
for field, val_map in iteritems(data):
val = val_map.get(book_id)
if val is not None:
ans[field.replace('#', '*')] = val
yield ans
def prepare_output_table(fields, book_ids, data, metadata):
ans = []
for book_id in book_ids:
row = []
ans.append(row)
for field in fields:
if field == 'id':
row.append(str(book_id))
continue
val = data.get(field.replace('*', '#'), {}).get(book_id)
row.append(str(val).replace('\n', ' '))
return ans
def do_list(
dbctx,
fields,
afields,
sort_by,
ascending,
search_text,
line_width,
separator,
prefix,
limit,
for_machine=False
):
if sort_by is None:
ascending = True
ans = dbctx.run('list', fields, sort_by, ascending, search_text, limit)
try:
book_ids, data, metadata = ans['book_ids'], ans['data'], ans['metadata']
except TypeError:
raise SystemExit(ans)
fields = list(ans['fields'])
try:
fields.remove('id')
except ValueError:
pass
fields = ['id'] + fields
stringify(data, metadata, for_machine)
if for_machine:
raw = json.dumps(
list(as_machine_data(book_ids, data, metadata)),
indent=2,
sort_keys=True
)
if not isinstance(raw, bytes):
raw = raw.encode('utf-8')
getattr(sys.stdout, 'buffer', sys.stdout).write(raw)
return
from calibre.utils.terminal import ColoredStream, geometry
output_table = prepare_output_table(fields, book_ids, data, metadata)
widths = list(map(lambda x: 0, fields))
for record in output_table:
for j in range(len(fields)):
widths[j] = max(widths[j], str_width(record[j]))
screen_width = geometry()[0] if line_width < 0 else line_width
if not screen_width:
screen_width = 80
field_width = screen_width // len(fields)
base_widths = list(map(lambda x: min(x + 1, field_width), widths))
while sum(base_widths) < screen_width:
adjusted = False
for i in range(len(widths)):
if base_widths[i] < widths[i]:
base_widths[i] += min(
screen_width - sum(base_widths), widths[i] - base_widths[i]
)
adjusted = True
break
if not adjusted:
break
widths = list(base_widths)
titles = map(
lambda x, y: '%-*s%s' % (x - len(separator), y, separator), widths,
fields
)
with ColoredStream(sys.stdout, fg='green'):
print(''.join(titles), flush=True)
stdout = getattr(sys.stdout, 'buffer', sys.stdout)
linesep = as_bytes(os.linesep)
wrappers = [TextWrapper(x - 1).wrap if x > 1 else lambda y: y for x in widths]
for record in output_table:
text = [
wrappers[i](record[i]) for i, field in enumerate(fields)
]
lines = max(map(len, text))
for l in range(lines):
for i, field in enumerate(text):
ft = text[i][l] if l < len(text[i]) else ''
stdout.write(ft.encode('utf-8'))
if i < len(text) - 1:
filler = ('%*s' % (widths[i] - str_width(ft) - 1, ''))
stdout.write((filler + separator).encode('utf-8'))
stdout.write(linesep)
def option_parser(get_parser, args):
parser = get_parser(
_(
'''\
%prog list [options]
List the books available in the calibre database.
'''
)
)
parser.add_option(
'-f',
'--fields',
default='title,authors',
help=_(
'The fields to display when listing books in the'
' database. Should be a comma separated list of'
' fields.\nAvailable fields: %s\nDefault: %%default. The'
' special field "all" can be used to select all fields.'
' In addition to the builtin fields above, custom fields are'
' also available as *field_name, for example, for a custom field'
' #rating, use the name: *rating'
) % ', '.join(sorted(FIELDS))
)
parser.add_option(
'--sort-by',
default=None,
help=_(
'The field by which to sort the results.\nAvailable fields: {0}\nDefault: {1}'
).format(', '.join(sorted(FIELDS)), 'id')
)
parser.add_option(
'--ascending',
default=False,
action='store_true',
help=_('Sort results in ascending order')
)
parser.add_option(
'-s',
'--search',
default=None,
help=_(
'Filter the results by the search query. For the format of the search query,'
' please see the search related documentation in the User Manual. Default is to do no filtering.'
)
)
parser.add_option(
'-w',
'--line-width',
default=-1,
type=int,
help=_(
'The maximum width of a single line in the output. Defaults to detecting screen size.'
)
)
parser.add_option(
'--separator',
default=' ',
help=_('The string used to separate fields. Default is a space.')
)
parser.add_option(
'--prefix',
default=None,
help=_(
'The prefix for all file paths. Default is the absolute path to the library folder.'
)
)
parser.add_option(
'--limit',
default=-1,
type=int,
help=_('The maximum number of results to display. Default: all')
)
parser.add_option(
'--for-machine',
default=False,
action='store_true',
help=_(
'Generate output in JSON format, which is more suitable for machine parsing. Causes the line width and separator options to be ignored.'
)
)
return parser
def main(opts, args, dbctx):
afields = set(FIELDS) | {'id'}
if opts.fields.strip():
fields = [str(f.strip().lower()) for f in opts.fields.split(',')]
else:
fields = []
do_list(
dbctx,
fields,
afields,
opts.sort_by,
opts.ascending,
opts.search,
opts.line_width,
opts.separator,
opts.prefix,
opts.limit,
for_machine=opts.for_machine
)
return 0