diff --git a/README.md b/README.md index 3ea98034..5f774709 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,12 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d - Bootstrap 3 HTML5 interface - full graphical setup -- User management with fine grained per-user permissions +- User management with fine-grained per-user permissions - Admin interface - User Interface in dutch, english, french, german, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish, swedish, ukrainian - OPDS feed for eBook reader apps - Filter and search by titles, authors, tags, series and language -- Create custom book collection (shelves) +- Create a custom book collection (shelves) - Support for editing eBook metadata and deleting eBooks from Calibre library - Support for converting eBooks through Calibre binaries - Restrict eBook download to logged-in users @@ -25,7 +25,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d - Upload new books in many formats - Support for Calibre custom columns - Ability to hide content based on categories for certain users -- Self update capability +- Self-update capability - "Magic Link" login to make it easy to log on eReaders ## Quick start @@ -50,11 +50,11 @@ Python 2.7+, python 3.x+ Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-kindle feature, or during editing of ebooks metadata: -[Download and install](https://calibre-ebook.com/download) the Calibre desktop program for your platform and enter the folder including programm name (normally /opt/calibre/ebook-convert, or C:\Program Files\calibre\ebook-convert.exe) in the field "calibre's converter tool" on the setup page. +[Download and install](https://calibre-ebook.com/download) the Calibre desktop program for your platform and enter the folder including program name (normally /opt/calibre/ebook-convert, or C:\Program Files\calibre\ebook-convert.exe) in the field "calibre's converter tool" on the setup page. \*** DEPRECATED \*** Support will be removed in future releases -[Download](http://www.amazon.com/gp/feature.html?docId=1000765211) Amazon's KindleGen tool for your platform and place the binary named as `kindlegen` in the `vendor` folder. +[Download](http://www.amazon.com/gp/feature.html?docId=1000765211) Amazon's KindleGen tool for your platform and place the binary named `kindlegen` in the `vendor` folder. ## Docker Images @@ -82,4 +82,4 @@ Pre-built Docker images are available in these Docker Hub repositories: # Wiki -For further informations, How To's and FAQ please check the [Wiki](https://github.com/janeczku/calibre-web/wiki) +For further information, How To's and FAQ please check the [Wiki](https://github.com/janeczku/calibre-web/wiki) diff --git a/cps/about.py b/cps/about.py index 8d0a938f..630243f4 100644 --- a/cps/about.py +++ b/cps/about.py @@ -37,6 +37,7 @@ except ImportError: from flask_login.__about__ import __version__ as flask_loginVersion try: import unidecode + # _() necessary to make babel aware of string for translation unidecode_version = _(u'installed') except ImportError: unidecode_version = _(u'not installed') @@ -62,8 +63,8 @@ _VERSIONS = OrderedDict( iso639=isoLanguages.__version__, pytz=pytz.__version__, Unidecode = unidecode_version, - Flask_SimpleLDAP = _(u'installed') if bool(services.ldap) else _(u'not installed'), - Goodreads = _(u'installed') if bool(services.goodreads_support) else _(u'not installed'), + Flask_SimpleLDAP = u'installed' if bool(services.ldap) else u'not installed', + Goodreads = u'installed' if bool(services.goodreads_support) else u'not installed', ) _VERSIONS.update(uploader.get_versions()) diff --git a/cps/admin.py b/cps/admin.py index 0292eee3..e5c7c002 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -272,6 +272,8 @@ def _configuration_update_helper(): gdrive_secrets = {} gdriveError = gdriveutils.get_error_text(gdrive_secrets) if "config_use_google_drive" in to_save and not config.config_use_google_drive and not gdriveError: + with open(gdriveutils.CLIENT_SECRETS, 'r') as settings: + gdrive_secrets = json.load(settings)['web'] if not gdrive_secrets: return _configuration_result('client_secrets.json is not configured for web application') gdriveutils.update_settings( @@ -415,7 +417,8 @@ def _configuration_result(error_flash=None, gdriveError=None): if gdriveError: gdriveError = _(gdriveError) else: - gdrivefolders = gdriveutils.listRootFolders() + if config.config_use_google_drive and not gdrive_authenticate: + gdrivefolders = gdriveutils.listRootFolders() show_back_button = current_user.is_authenticated show_login_button = config.db_configured and not current_user.is_authenticated @@ -614,6 +617,21 @@ def edit_user(user_id): return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check, title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") + if "nickname" in to_save and to_save["nickname"] != content.nickname: + # Query User nickname, if not existing, change + if not ub.session.query(ub.User).filter(ub.User.nickname == to_save["nickname"]).scalar(): + content.nickname = to_save["nickname"] + else: + flash(_(u"This username is already taken"), category="error") + return render_title_template("user_edit.html", + translations=translations, + languages=languages, + new_user=0, content=content, + downloads=downloads, + registered_oauth=oauth_check, + title=_(u"Edit User %(nick)s", + nick=content.nickname), + page="edituser") if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail: content.kindle_mail = to_save["kindle_mail"] diff --git a/cps/converter.py b/cps/converter.py index 32bc273f..d3482e5f 100644 --- a/cps/converter.py +++ b/cps/converter.py @@ -19,6 +19,7 @@ from __future__ import division, print_function, unicode_literals import os import re +from flask_babel import gettext as _ from . import config, logger from .subproc_wrapper import process_wait @@ -26,7 +27,8 @@ from .subproc_wrapper import process_wait log = logger.create() -_NOT_CONFIGURED = 'not configured' +# _() necessary to make babel aware of string for translation +_NOT_CONFIGURED = _('not configured') _NOT_INSTALLED = 'not installed' _EXECUTION_ERROR = 'Execution permissions missing' diff --git a/cps/editbooks.py b/cps/editbooks.py index 2da9991f..f0156f71 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -567,6 +567,12 @@ def upload(): filepath = os.path.join(config.config_calibre_dir, author_dir, title_dir) saved_filename = os.path.join(filepath, title_dir + meta.extension.lower()) + if title != u'Unknown' and authr != u'Unknown': + entry = helper.check_exists_book(authr, title) + if entry: + book_html = flash(_(u"Uploaded book probably exists in the library, consider to change before upload new: ") + + Markup(render_title_template('book_exists_flash.html', entry=entry)), category="warning") + # check if file path exists, otherwise create it, copy file to calibre path and delete temp file if not os.path.exists(filepath): try: diff --git a/cps/epub.py b/cps/epub.py index d9129646..d315abf6 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -64,7 +64,12 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension): for s in ['title', 'description', 'creator', 'language', 'subject']: tmp = p.xpath('dc:%s/text()' % s, namespaces=ns) if len(tmp) > 0: - epub_metadata[s] = p.xpath('dc:%s/text()' % s, namespaces=ns)[0] + if s == 'creator': + epub_metadata[s] = ' & '.join(p.xpath('dc:%s/text()' % s, namespaces=ns)) + elif s == 'subject': + epub_metadata[s] = ', '.join(p.xpath('dc:%s/text()' % s, namespaces=ns)) + else: + epub_metadata[s] = p.xpath('dc:%s/text()' % s, namespaces=ns)[0] else: epub_metadata[s] = "Unknown" @@ -109,18 +114,20 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension): meta_cover = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='cover']/@content", namespaces=ns) if len(meta_cover) > 0: coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='"+meta_cover[0]+"']/@href", namespaces=ns) - if len(coversection) > 0: - filetype = coversection[0].rsplit('.', 1)[-1] - if filetype == "xhtml" or filetype == "html": # if cover is (x)html format - markup = epubZip.read(os.path.join(coverpath, coversection[0])) - markupTree = etree.fromstring(markup) - # no matter xhtml or html with no namespace - imgsrc = markupTree.xpath("//*[local-name() = 'img']/@src") - # imgsrc maybe startwith "../"" so fullpath join then relpath to cwd - filename = os.path.relpath(os.path.join(os.path.dirname(os.path.join(coverpath, coversection[0])), imgsrc[0])) - coverfile = extractCover(epubZip, filename, "", tmp_file_path) - else: - coverfile = extractCover(epubZip, coversection[0], coverpath, tmp_file_path) + else: + coversection = tree.xpath("/pkg:package/pkg:guide/pkg:reference/@href", namespaces=ns) + if len(coversection) > 0: + filetype = coversection[0].rsplit('.', 1)[-1] + if filetype == "xhtml" or filetype == "html": # if cover is (x)html format + markup = epubZip.read(os.path.join(coverpath, coversection[0])) + markupTree = etree.fromstring(markup) + # no matter xhtml or html with no namespace + imgsrc = markupTree.xpath("//*[local-name() = 'img']/@src") + # imgsrc maybe startwith "../"" so fullpath join then relpath to cwd + filename = os.path.relpath(os.path.join(os.path.dirname(os.path.join(coverpath, coversection[0])), imgsrc[0])) + coverfile = extractCover(epubZip, filename, "", tmp_file_path) + else: + coverfile = extractCover(epubZip, coversection[0], coverpath, tmp_file_path) if not epub_metadata['title']: title = original_file_name diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py index 4ebbb0c7..d950f738 100644 --- a/cps/gdriveutils.py +++ b/cps/gdriveutils.py @@ -584,5 +584,7 @@ def get_error_text(client_secrets=None): filedata = json.load(settings) if 'web' not in filedata: return 'client_secrets.json is not configured for web application' + if 'redirect_uris' not in filedata['web']: + return 'Callback url (redirect url) is missing in client_secrets.json' if client_secrets: client_secrets.update(filedata['web']) diff --git a/cps/helper.py b/cps/helper.py index a68aad7b..51e9456e 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -54,7 +54,7 @@ except ImportError: use_unidecode = False try: - from PIL import Image + from PIL import Image as PILImage use_PIL = True except ImportError: use_PIL = False @@ -183,7 +183,7 @@ def check_read_formats(entry): bookformats = list() if len(entry.data): for ele in iter(entry.data): - if ele.format in EXTENSIONS_READER: + if ele.format.upper() in EXTENSIONS_READER: bookformats.append(ele.format.lower()) return bookformats @@ -511,9 +511,9 @@ def save_cover(img, book_path): # convert to jpg because calibre only supports jpg if content_type in ('image/png', 'image/webp'): if hasattr(img,'stream'): - imgc = Image.open(img.stream) + imgc = PILImage.open(img.stream) else: - imgc = Image.open(io.BytesIO(img.content)) + imgc = PILImage.open(io.BytesIO(img.content)) im = imgc.convert('RGB') tmp_bytesio = io.BytesIO() im.save(tmp_bytesio, format='JPEG') @@ -794,9 +794,22 @@ def get_download_link(book_id, book_format): else: abort(404) +def check_exists_book(authr,title): + db.session.connection().connection.connection.create_function("lower", 1, lcase) + q = list() + authorterms = re.split(r'\s*&\s*', authr) + for authorterm in authorterms: + q.append(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + authorterm + "%"))) + return db.session.query(db.Books).filter( + and_(db.Books.authors.any(and_(*q)), + func.lower(db.Books.title).ilike("%" + title + "%") + )).first() ############### Database Helper functions def lcase(s): - return unidecode.unidecode(s.lower()) if use_unidecode else s.lower() + try: + return unidecode.unidecode(s.lower()) + except Exception as e: + log.exception(e) diff --git a/cps/logger.py b/cps/logger.py index 54be25e7..77a721d3 100644 --- a/cps/logger.py +++ b/cps/logger.py @@ -18,6 +18,7 @@ from __future__ import division, print_function, unicode_literals import os +import sys import inspect import logging from logging import Formatter, StreamHandler @@ -34,6 +35,7 @@ DEFAULT_LOG_LEVEL = logging.INFO DEFAULT_LOG_FILE = os.path.join(_CONFIG_DIR, "calibre-web.log") DEFAULT_ACCESS_LOG = os.path.join(_CONFIG_DIR, "access.log") LOG_TO_STDERR = '/dev/stderr' +LOG_TO_STDOUT = '/dev/stdout' logging.addLevelName(logging.WARNING, "WARN") logging.addLevelName(logging.CRITICAL, "CRIT") @@ -112,9 +114,13 @@ def setup(log_file, log_level=None): return logging.debug("logging to %s level %s", log_file, r.level) - if log_file == LOG_TO_STDERR: - file_handler = StreamHandler() - file_handler.baseFilename = LOG_TO_STDERR + if log_file == LOG_TO_STDERR or log_file == LOG_TO_STDOUT: + if log_file == LOG_TO_STDOUT: + file_handler = StreamHandler(sys.stdout) + file_handler.baseFilename = log_file + else: + file_handler = StreamHandler() + file_handler.baseFilename = log_file else: try: file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2) @@ -164,5 +170,5 @@ class StderrLogger(object): self.log.debug("Logging Error") -# default configuration, before application settngs are applied +# default configuration, before application settings are applied setup(LOG_TO_STDERR, logging.DEBUG if os.environ.get('FLASK_DEBUG') else DEFAULT_LOG_LEVEL) diff --git a/cps/opds.py b/cps/opds.py index 44e1341d..9198554b 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -47,7 +47,7 @@ def requires_basic_auth_if_no_ano(f): def decorated(*args, **kwargs): auth = request.authorization if config.config_anonbrowse != 1: - if not auth or not check_auth(auth.username, auth.password): + if not auth or auth.type != 'basic' or not check_auth(auth.username, auth.password): return authenticate() return f(*args, **kwargs) if config.config_login_type == constants.LOGIN_LDAP and services.ldap: @@ -219,7 +219,7 @@ def feed_series(book_id): @requires_basic_auth_if_no_ano def feed_shelfindex(public): off = request.args.get("offset") or 0 - if public is not 0: + if public != 0: shelf = g.public_shelfes number = len(shelf) else: @@ -261,9 +261,10 @@ def opds_download_link(book_id, book_format): return get_download_link(book_id,book_format) +@opds.route("/ajax/book//") @opds.route("/ajax/book/") @requires_basic_auth_if_no_ano -def get_metadata_calibre_companion(uuid): +def get_metadata_calibre_companion(uuid, library): entry = db.session.query(db.Books).filter(db.Books.uuid.like("%" + uuid + "%")).first() if entry is not None: js = render_template('json.txt', entry=entry) diff --git a/cps/server.py b/cps/server.py index e5fe78e4..43792ecd 100644 --- a/cps/server.py +++ b/cps/server.py @@ -146,6 +146,9 @@ class WebServer(object): self.unix_socket_file = None def _start_tornado(self): + if os.name == 'nt': + import asyncio + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) log.info('Starting Tornado server on %s', _readable_listen_address(self.listen_address, self.listen_port)) # Max Buffersize set to 200MB ) diff --git a/cps/static/css/style.css b/cps/static/css/style.css index aaa7503e..da088021 100644 --- a/cps/static/css/style.css +++ b/cps/static/css/style.css @@ -64,6 +64,7 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te .navbar-default .navbar-toggle .icon-bar {background-color: #000;} .navbar-default .navbar-toggle {border-color: #000;} .cover { margin-bottom: 10px;} +.cover-height { max-height: 100px;} .btn-file {position: relative; overflow: hidden;} .btn-file input[type=file] {position: absolute; top: 0; right: 0; min-width: 100%; min-height: 100%; font-size: 100px; text-align: right; filter: alpha(opacity=0); opacity: 0; outline: none; background: white; cursor: inherit; display: block;} diff --git a/cps/templates/book_edit.html b/cps/templates/book_edit.html index 9305e204..60421ea6 100644 --- a/cps/templates/book_edit.html +++ b/cps/templates/book_edit.html @@ -73,7 +73,7 @@
- +
diff --git a/cps/templates/book_exists_flash.html b/cps/templates/book_exists_flash.html new file mode 100644 index 00000000..b0855120 --- /dev/null +++ b/cps/templates/book_exists_flash.html @@ -0,0 +1,3 @@ + + {{entry.title|shortentitle}} + \ No newline at end of file diff --git a/cps/templates/feed.xml b/cps/templates/feed.xml index 473dc2d4..16548a91 100644 --- a/cps/templates/feed.xml +++ b/cps/templates/feed.xml @@ -41,7 +41,7 @@ {% for entry in entries %} {{entry.title}} - {{entry.uuid}} + urn:uuid:{{entry.uuid}} {{entry.atom_timestamp}} {% if entry.authors.__len__() > 0 %} diff --git a/cps/templates/index.xml b/cps/templates/index.xml index f7e8d6f0..f0ca1e2e 100644 --- a/cps/templates/index.xml +++ b/cps/templates/index.xml @@ -15,28 +15,28 @@ {{_('Hot Books')}} - + {{url_for('opds.feed_hot')}} {{ current_time }} {{_('Popular publications from this catalog based on Downloads.')}} {{_('Best rated Books')}} - + {{url_for('opds.feed_best_rated')}} {{ current_time }} {{_('Popular publications from this catalog based on Rating.')}} {{_('New Books')}} - + {{url_for('opds.feed_new')}} {{ current_time }} {{_('The latest Books')}} {{_('Random Books')}} - + {{url_for('opds.feed_discover')}} {{ current_time }} {{_('Show Random Books')}} @@ -44,14 +44,14 @@ {% if not current_user.is_anonymous %} {{_('Read Books')}} - + {{url_for('opds.feed_read_books')}} {{ current_time }} {{_('Read Books')}} {{_('Unread Books')}} - + {{url_for('opds.feed_unread_books')}} {{ current_time }} {{_('Unread Books')}} @@ -59,35 +59,35 @@ {% endif %} {{_('Authors')}} - + {{url_for('opds.feed_authorindex')}} {{ current_time }} {{_('Books ordered by Author')}} {{_('Publishers')}} - + {{url_for('opds.feed_publisherindex')}} {{ current_time }} {{_('Books ordered by publisher')}} {{_('Category list')}} - + {{url_for('opds.feed_categoryindex')}} {{ current_time }} {{_('Books ordered by category')}} {{_('Series list')}} - + {{url_for('opds.feed_seriesindex')}} {{ current_time }} {{_('Books ordered by series')}} {{_('Public Shelves')}} - + {{url_for('opds.feed_shelfindex', public="public")}} {{ current_time }} {{_('Books organized in public shelfs, visible to everyone')}} @@ -95,7 +95,7 @@ {% if not current_user.is_anonymous %} {{_('Your Shelves')}} - + {{url_for('opds.feed_shelfindex')}} {{ current_time }} {{_("User's own shelfs, only visible to the current user himself")}} diff --git a/cps/templates/layout.html b/cps/templates/layout.html index 72eba890..9ffb04f8 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -99,6 +99,11 @@
{{ message[1] }}
{%endif%} + {%if message[0] == "warning" %} +
+
{{ message[1] }}
+
+ {%endif%} {%if message[0] == "success" %}
{{ message[1] }}
diff --git a/cps/templates/listenmp3.html b/cps/templates/listenmp3.html index 0abc289d..8113c759 100644 --- a/cps/templates/listenmp3.html +++ b/cps/templates/listenmp3.html @@ -17,11 +17,6 @@ - - -