%PDF- %PDF-
Direktori : /lib/calibre/calibre/db/cli/ |
Current File : //lib/calibre/calibre/db/cli/main.py |
#!/usr/bin/env python3 # License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net> import json import os import sys from calibre import browser, prints from calibre.constants import __appname__, __version__, iswindows from calibre.db.cli import module_for_cmd from calibre.db.legacy import LibraryDatabase from calibre.utils.config import OptionParser, prefs from calibre.utils.localization import localize_user_manual_link from calibre.utils.lock import singleinstance from calibre.utils.serialize import MSGPACK_MIME from polyglot import http_client from polyglot.urllib import urlencode, urlparse, urlunparse COMMANDS = ( 'list', 'add', 'remove', 'add_format', 'remove_format', 'show_metadata', 'set_metadata', 'export', 'catalog', 'saved_searches', 'add_custom_column', 'custom_columns', 'remove_custom_column', 'set_custom', 'restore_database', 'check_library', 'list_categories', 'backup_metadata', 'clone', 'embed_metadata', 'search' ) def option_parser_for(cmd, args=()): def cmd_option_parser(): return module_for_cmd(cmd).option_parser(get_parser, args) return cmd_option_parser def run_cmd(cmd, opts, args, dbctx): m = module_for_cmd(cmd) if dbctx.is_remote and getattr(m, 'no_remote', False): raise SystemExit(_('The {} command is not supported with remote (server based) libraries').format(cmd)) ret = m.main(opts, args, dbctx) return ret def get_parser(usage): parser = OptionParser(usage) go = parser.add_option_group(_('GLOBAL OPTIONS')) go.is_global_options = True go.add_option( '--library-path', '--with-library', default=None, help=_( 'Path to the calibre library. Default is to use the path stored in the settings.' ' You can also connect to a calibre Content server to perform actions on' ' remote libraries. To do so use a URL of the form: http://hostname:port/#library_id' ' for example, http://localhost:8080/#mylibrary. library_id is the library id' ' of the library you want to connect to on the Content server. You can use' ' the special library_id value of - to get a list of library ids available' ' on the server. For details on how to setup access via a Content server, see' ' {}.' ).format(localize_user_manual_link( 'https://manual.calibre-ebook.com/generated/en/calibredb.html' )) ) go.add_option( '-h', '--help', help=_('show this help message and exit'), action='help' ) go.add_option( '--version', help=_("show program's version number and exit"), action='version' ) go.add_option( '--username', help=_('Username for connecting to a calibre Content server') ) go.add_option( '--password', help=_('Password for connecting to a calibre Content server.' ' To read the password from standard input, use the special value: {0}.' ' To read the password from a file, use: {1} (i.e. <f: followed by the full path to the file and a trailing >).' ' The angle brackets in the above are required, remember to escape them or use quotes' ' for your shell.').format( '<stdin>', '<f:C:/path/to/file>' if iswindows else '<f:/path/to/file>') ) go.add_option( '--timeout', type=float, default=120, help=_('The timeout, in seconds, when connecting to a calibre library over the network. The default is' ' two minutes.') ) return parser def option_parser(): return get_parser( _( '''\ %%prog command [options] [arguments] %%prog is the command line interface to the calibre books database. command is one of: %s For help on an individual command: %%prog command --help ''' ) % '\n '.join(COMMANDS) ) def read_credentials(opts): username = opts.username pw = opts.password if pw: if pw == '<stdin>': from getpass import getpass pw = getpass(_('Enter the password: ')) elif pw.startswith('<f:') and pw.endswith('>'): with lopen(pw[3:-1], 'rb') as f: pw = f.read().decode('utf-8').rstrip() return username, pw class DBCtx: def __init__(self, opts): self.library_path = opts.library_path or prefs['library_path'] self.timeout = opts.timeout self.url = None if self.library_path is None: raise SystemExit( 'No saved library path, either run the GUI or use the' ' --with-library option' ) if self.library_path.partition(':')[0] in ('http', 'https'): parts = urlparse(self.library_path) self.library_id = parts.fragment or None self.url = urlunparse(parts._replace(fragment='')).rstrip('/') self.br = browser(handle_refresh=False, user_agent=f'{__appname__} {__version__}') self.is_remote = True username, password = read_credentials(opts) self.has_credentials = False if username and password: self.br.add_password(self.url, username, password) self.has_credentials = True if self.library_id == '-': self.list_libraries() raise SystemExit() else: self.library_path = os.path.expanduser(self.library_path) if not singleinstance('db'): ext = '.exe' if iswindows else '' raise SystemExit(_( 'Another calibre program such as {} or the main calibre program is running.' ' Having multiple programs that can make changes to a calibre library' ' running at the same time is a bad idea. calibredb can connect directly' ' to a running calibre Content server, to make changes through it, instead.' ' See the documentation of the {} option for details.' ).format('calibre-server' + ext, '--with-library') ) self._db = None self.is_remote = False @property def db(self): if self._db is None: self._db = LibraryDatabase(self.library_path) return self._db def path(self, path): if self.is_remote: with lopen(path, 'rb') as f: return path, f.read() return path def run(self, name, *args): m = module_for_cmd(name) if self.is_remote: return self.remote_run(name, m, *args) return m.implementation(self.db.new_api, None, *args) def interpret_http_error(self, err): if err.code == http_client.UNAUTHORIZED: if self.has_credentials: raise SystemExit('The username/password combination is incorrect') raise SystemExit('A username and password is required to access this server') if err.code == http_client.FORBIDDEN: raise SystemExit(err.reason) if err.code == http_client.NOT_FOUND: raise SystemExit(err.reason) def remote_run(self, name, m, *args): from mechanize import HTTPError, Request from calibre.utils.serialize import msgpack_loads, msgpack_dumps url = self.url + '/cdb/cmd/{}/{}'.format(name, getattr(m, 'version', 0)) if self.library_id: url += '?' + urlencode({'library_id':self.library_id}) rq = Request(url, data=msgpack_dumps(args), headers={'Accept': MSGPACK_MIME, 'Content-Type': MSGPACK_MIME}) try: res = self.br.open_novisit(rq, timeout=self.timeout) ans = msgpack_loads(res.read()) except HTTPError as err: self.interpret_http_error(err) raise if 'err' in ans: if ans['tb']: prints(ans['tb']) raise SystemExit(ans['err']) return ans['result'] def list_libraries(self): from mechanize import HTTPError url = self.url + '/ajax/library-info' try: res = self.br.open_novisit(url, timeout=self.timeout) ans = json.loads(res.read()) except HTTPError as err: self.interpret_http_error(err) raise library_map, default_library = ans['library_map'], ans['default_library'] for lid in sorted(library_map, key=lambda lid: (lid != default_library, lid)): prints(lid) def main(args=sys.argv): parser = option_parser() if len(args) < 2: parser.print_help() return 1 if args[1] in ('-h', '--help'): parser.print_help() return 0 if args[1] == '--version': parser.print_version() return 0 for i, x in enumerate(args): if i > 0 and x in COMMANDS: cmd = x break else: parser.print_help() return 1 del args[i] parser = option_parser_for(cmd, args[1:])() opts, args = parser.parse_args(args) return run_cmd(cmd, opts, args[1:], DBCtx(opts)) if __name__ == '__main__': main()