Commit 0b709f7d authored by Michael Shavit's avatar Michael Shavit

Merge branch 'master' into kobo

parents b5da2c41 fda0ab1e
......@@ -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)
......@@ -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())
......
......@@ -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"]
......
......@@ -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'
......
......@@ -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:
......
......@@ -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
......
......@@ -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'])
......@@ -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)
......@@ -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)
......@@ -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/<string:uuid>/<library>")
@opds.route("/ajax/book/<string:uuid>")
@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)
......
......@@ -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 )
......
......@@ -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;}
......
......@@ -73,7 +73,7 @@
</div>
<div class="form-group">
<label for="series_index">{{_('Series id')}}</label>
<input type="number" step="0.1" min="0" class="form-control" name="series_index" id="series_index" value="{{book.series_index}}">
<input type="number" step="0.01" min="0" class="form-control" name="series_index" id="series_index" value="{{book.series_index}}">
</div>
<div class="form-group">
<label for="rating">{{_('Rating')}}</label>
......
<a href="{{ url_for('web.show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<span class="title">{{entry.title|shortentitle}}</span>
</a>
\ No newline at end of file
......@@ -41,7 +41,7 @@
{% for entry in entries %}
<entry>
<title>{{entry.title}}</title>
<id>{{entry.uuid}}</id>
<id>urn:uuid:{{entry.uuid}}</id>
<updated>{{entry.atom_timestamp}}</updated>
{% if entry.authors.__len__() > 0 %}
<author>
......
......@@ -15,28 +15,28 @@
</author>
<entry>
<title>{{_('Hot Books')}}</title>
<link rel="http://opds-spec.org/sort/popular" href="{{url_for('opds.feed_hot')}}" type="application/atom+xml;profile=opds-catalog"/>
<link href="{{url_for('opds.feed_hot')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('opds.feed_hot')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Popular publications from this catalog based on Downloads.')}}</content>
</entry>
<entry>
<title>{{_('Best rated Books')}}</title>
<link rel="http://opds-spec.org/recommended" href="{{url_for('opds.feed_best_rated')}}" type="application/atom+xml;profile=opds-catalog"/>
<link href="{{url_for('opds.feed_best_rated')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('opds.feed_best_rated')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Popular publications from this catalog based on Rating.')}}</content>
</entry>
<entry>
<title>{{_('New Books')}}</title>
<link rel="http://opds-spec.org/sort/new" href="{{url_for('opds.feed_new')}}" type="application/atom+xml;profile=opds-catalog"/>
<link href="{{url_for('opds.feed_new')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('opds.feed_new')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('The latest Books')}}</content>
</entry>
<entry>
<title>{{_('Random Books')}}</title>
<link rel="http://opds-spec.org/featured" href="{{url_for('opds.feed_discover')}}" type="application/atom+xml;profile=opds-catalog"/>
<link href="{{url_for('opds.feed_discover')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('opds.feed_discover')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Show Random Books')}}</content>
......@@ -44,14 +44,14 @@
{% if not current_user.is_anonymous %}
<entry>
<title>{{_('Read Books')}}</title>
<link rel="subsection" href="{{url_for('opds.feed_read_books')}}" type="application/atom+xml;profile=opds-catalog"/>
<link href="{{url_for('opds.feed_read_books')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('opds.feed_read_books')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Read Books')}}</content>
</entry>
<entry>
<title>{{_('Unread Books')}}</title>
<link rel="subsection" href="{{url_for('opds.feed_unread_books')}}" type="application/atom+xml;profile=opds-catalog"/>
<link href="{{url_for('opds.feed_unread_books')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('opds.feed_unread_books')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Unread Books')}}</content>
......@@ -59,35 +59,35 @@
{% endif %}
<entry>
<title>{{_('Authors')}}</title>
<link rel="subsection" href="{{url_for('opds.feed_authorindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<link href="{{url_for('opds.feed_authorindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('opds.feed_authorindex')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by Author')}}</content>
</entry>
<entry>
<title>{{_('Publishers')}}</title>
<link rel="subsection" href="{{url_for('opds.feed_publisherindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<link href="{{url_for('opds.feed_publisherindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('opds.feed_publisherindex')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by publisher')}}</content>
</entry>
<entry>
<title>{{_('Category list')}}</title>
<link rel="subsection" href="{{url_for('opds.feed_categoryindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<link href="{{url_for('opds.feed_categoryindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('opds.feed_categoryindex')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by category')}}</content>
</entry>
<entry>
<title>{{_('Series list')}}</title>
<link rel="subsection" href="{{url_for('opds.feed_seriesindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<link href="{{url_for('opds.feed_seriesindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('opds.feed_seriesindex')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by series')}}</content>
</entry>
<entry>
<title>{{_('Public Shelves')}}</title>
<link rel="subsection" href="{{url_for('opds.feed_shelfindex', public="public")}}" type="application/atom+xml;profile=opds-catalog"/>
<link href="{{url_for('opds.feed_shelfindex', public='public')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('opds.feed_shelfindex', public="public")}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books organized in public shelfs, visible to everyone')}}</content>
......@@ -95,7 +95,7 @@
{% if not current_user.is_anonymous %}
<entry>
<title>{{_('Your Shelves')}}</title>
<link rel="subsection" href="{{url_for('opds.feed_shelfindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<link href="{{url_for('opds.feed_shelfindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('opds.feed_shelfindex')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_("User's own shelfs, only visible to the current user himself")}}</content>
......
......@@ -99,6 +99,11 @@
<div id="flash_info" class="alert alert-info">{{ message[1] }}</div>
</div>
{%endif%}
{%if message[0] == "warning" %}
<div class="row-fluid text-center" style="margin-top: -20px;">
<div id="flash_warning" class="alert alert-warning">{{ message[1] }}</div>
</div>
{%endif%}
{%if message[0] == "success" %}
<div class="row-fluid text-center" style="margin-top: -20px;">
<div id="flash_success" class="alert alert-success">{{ message[1] }}</div>
......
......@@ -17,11 +17,6 @@
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link href="{{ url_for('static', filename='css/libs/bootstrap.min.css') }}" rel="stylesheet" media="screen">
<link rel="stylesheet" href="{{ url_for('static', filename='css/libs/bar-ui.css') }}" />
<!--link rel="stylesheet" href="{{ url_for('static', filename='css/listen.css') }}" /-->
<!-- <link rel="stylesheet" href="{{ url_for('static', filename='css/libs/normalize.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/popup.css') }}"> -->
<script>
"use strict";
......@@ -30,95 +25,6 @@
</head>
<body>
<!--div id="main">
<div class="content">
<h2>{{ entry.title }}</h2>
<div class="cover">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
</div>
{% if entry.ratings.__len__() > 0 %}
<div class="rating">
<p>
{% for number in range((entry.ratings[0].rating/2)|int(2)) %}
<span class="glyphicon glyphicon-star good"></span>
{% if loop.last and loop.index
< 5 %} {% for numer in range(5 - loop.index) %} <span class="glyphicon glyphicon-star">
</span>
{% endfor %} {% endif %} {% endfor %}
</p>
</div>
{% endif %}
<h3>{{_('Description:')}}</h3>
{{entry.comments[0].text|safe}}
</div-->
<!--div class="sm2-bar-ui compact full-width">
<div class="bd sm2-main-controls">
<div class="sm2-inline-texture"></div>
<div class="sm2-inline-gradient"></div>
<div class="sm2-inline-element sm2-button-element">
<div class="sm2-button-bd">
<a href="#play" class="sm2-inline-button sm2-icon-play-pause">{{_('Play / pause')}}</a>
</div>
</div>
<div class="sm2-inline-element sm2-inline-status">
<div class="sm2-playlist">
<div class="sm2-playlist-target">
<!-- playlist <ul> + <li> markup will be injected here -->
<!-- if you want default / non-JS content, you can put that here. -->
<!--noscript><p>JavaScript is required.</p></noscript>
</div>
</div>
<div class="sm2-progress">
<div class="sm2-row">
<div class="sm2-inline-time">0:00</div>
<div class="sm2-progress-bd">
<div class="sm2-progress-track">
<div class="sm2-progress-bar"></div>
<div class="sm2-progress-ball"><div class="icon-overlay"></div></div>
</div>
</div>
<div class="sm2-inline-duration">0:00</div>
</div>
</div>
</div>
<div class="sm2-inline-element sm2-button-element sm2-volume">
<div class="sm2-button-bd">
<span class="sm2-inline-button sm2-volume-control volume-shade"></span>
<a href="#volume" class="sm2-inline-button sm2-volume-control">{{_('volume')}}</a>
</div>
</div>
</div>
<div class="bd sm2-playlist-drawer sm2-element">
<div class="sm2-inline-texture">
<div class="sm2-box-shadow"></div>
</div>
<!-- playlist content is mirrored here -->
<!--div class="sm2-playlist-wrapper">
<ul class="sm2-playlist-bd">
<li><a href="{{ url_for('web.serve_book', book_id=mp3file,book_format=audioformat)}}"><b>{% for author in entry.authors %}{{author.name.replace('|',',')}}
{% if not loop.last %} & {% endif %} {% endfor %}</b> - {{entry.title}}</a></li>
</ul>
</div>
</div>
</div-->
<div class="sm2-bar-ui full-width fixed">
......
......@@ -86,7 +86,7 @@
filePath: "{{ url_for('static', filename='js/libs/') }}",
cssPath: "{{ url_for('static', filename='css/') }}",
bookmarkUrl: "{{ url_for('web.bookmark', book_id=bookid, book_format='EPUB') }}",
bookUrl: "{{ url_for('web.download_link', book_id=bookid, book_format='epub', anyname='file.epub') }}",
bookUrl: "{{ url_for('web.serve_book', book_id=bookid, book_format='epub', anyname='file.epub') }}",
bookmark: "{{ bookmark.bookmark_key if bookmark != None }}",
useBookmarks: "{{ g.user.is_authenticated | tojson }}"
};
......
{% extends "layout.html" %}
{% block body %}
<div class="col-sm-6 col-lg-6 col-xs-6">
<div class="col-sm-8 col-lg-8 col-xs-12">
<h2>{{title}}</h2>
<div>{{_('Drag \'n drop to rearrange order')}}</div>
<div id="sortTrue" class="list-group">
{% for entry in entries %}
<div id="{{entry.id}}" class="list-group-item">{{entry.title}}</div>
<div id="{{entry.id}}" class="list-group-item">
<div class="row">
<div class="col-lg-2 col-sm-4 hidden-xs">
<img class="cover-height" src="{{ url_for('web.get_cover', book_id=entry.id) }}">
</div>
<div class="col-lg-10 col-sm-8 col-xs-12">
{{entry.title}}
{% if entry.series|length > 0 %}
<br>
{{entry.series_index}} - {{entry.series[0].name}}
{% endif %}
<br>
{% for author in entry.authors %}
{{author.name.replace('|',',')}}
{% if not loop.last %}
&amp;
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
<button onclick="sendData('{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}')" class="btn btn-default" id="ChangeOrder">{{_('Change order')}}</button>
......
{% extends "layout.html" %}
{% block body %}
<h3>{{_('About')}}</h3>
<p>{{instance}} powered by
<p>{{instance}} powered by
<a href="https://github.com/janeczku/calibre-web" title="Calibre-Web">Calibre-Web</a>.
</p>
</p>
<h3>{{_('Calibre library statistics')}}</h3>
<table id="stats" class="table">
<tbody>
......@@ -37,7 +37,7 @@
{% for library,version in versions.items() %}
<tr>
<th>{{library}}</th>
<td>{{version}}</td>
<td>{{_(version)}}</td>
</tr>
{% endfor %}
</tbody>
......
......@@ -3,7 +3,7 @@
<div class="discover">
<h1>{{title}}</h1>
<form role="form" method="POST" autocomplete="off">
{% if g.user and g.user.role_admin() and new_user %}
{% if new_user or ( g.user and content.nickname != "Guest" and g.user.role_admin() ) %}
<div class="form-group required">
<label for="nickname">{{_('Username')}}</label>
<input type="text" class="form-control" name="nickname" id="nickname" value="{{ content.nickname if content.nickname != None }}" autocomplete="off">
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from subproc_wrapper import process_open, cmdlineCall
import os
import sys
import re
import time
def main():
quotes = [1, 2]
format_new_ext = '.mobi'
format_old_ext = '.epub'
file_path = '/home/matthias/Dokumente/bücher/Bettina Szramah/Die Giftmischerin TCP_IP (10)/Die Giftmischerin TCP_IP - Bettina, Szrama'
command = ['/opt/calibre/ebook-convert', (file_path + format_old_ext),
(file_path + format_new_ext)]
#print(command)
#p1 = cmdlineCall(command[0],command[1:])
#time.sleep(10)
#print(p1)
p = process_open(command, quotes)
while p.poll() is None:
nextline = p.stdout.readline()
if os.name == 'nt' and sys.version_info < (3, 0):
nextline = nextline.decode('windows-1252')
elif os.name == 'posix' and sys.version_info < (3, 0):
nextline = nextline.decode('utf-8')
# log.debug(nextline.strip('\r\n'))
# parse progress string from calibre-converter
progress = re.search(r"(\d+)%\s.*", nextline)
if progress:
print('Progress:' + str(progress))
# self.UIqueue[index]['progress'] = progress.group(1) + ' %
# process returncode
check = p.returncode
calibre_traceback = p.stderr.readlines()
for ele in calibre_traceback:
if sys.version_info < (3, 0):
ele = ele.decode('utf-8')
print(ele.strip('\n'))
if not ele.startswith('Traceback') and not ele.startswith(' File'):
print( "Calibre failed with error: %s" % ele.strip('\n'))
print(str(check))
if __name__ == '__main__':
main()
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -21,9 +21,6 @@ from __future__ import division, print_function, unicode_literals
import os
import hashlib
from tempfile import gettempdir
from flask_babel import gettext as _
from . import logger, comic
from .constants import BookMeta
......@@ -68,16 +65,13 @@ except ImportError as e:
use_fb2_meta = False
try:
from PIL import Image
from PIL import Image as PILImage
from PIL import __version__ as PILversion
use_PIL = True
except ImportError as e:
log.debug('cannot import Pillow, using png and webp images as cover will not work: %s', e)
use_generic_pdf_cover = True
use_PIL = False
__author__ = 'lemmsh'
......@@ -149,51 +143,11 @@ def pdf_preview(tmp_file_path, tmp_dir):
if use_generic_pdf_cover:
return None
else:
if use_PIL:
try:
input1 = PdfFileReader(open(tmp_file_path, 'rb'), strict=False)
page0 = input1.getPage(0)
xObject = page0['/Resources']['/XObject'].getObject()
for obj in xObject:
if xObject[obj]['/Subtype'] == '/Image':
size = (xObject[obj]['/Width'], xObject[obj]['/Height'])
data = xObject[obj]._data # xObject[obj].getData()
if xObject[obj]['/ColorSpace'] == '/DeviceRGB':
mode = "RGB"
else:
mode = "P"
if '/Filter' in xObject[obj]:
if xObject[obj]['/Filter'] == '/FlateDecode':
img = Image.frombytes(mode, size, data)
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.png"
img.save(filename=os.path.join(tmp_dir, cover_file_name))
return cover_file_name
# img.save(obj[1:] + ".png")
elif xObject[obj]['/Filter'] == '/DCTDecode':
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg"
img = open(cover_file_name, "wb")
img.write(data)
img.close()
return cover_file_name
elif xObject[obj]['/Filter'] == '/JPXDecode':
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jp2"
img = open(cover_file_name, "wb")
img.write(data)
img.close()
return cover_file_name
else:
img = Image.frombytes(mode, size, data)
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.png"
img.save(filename=os.path.join(tmp_dir, cover_file_name))
return cover_file_name
# img.save(obj[1:] + ".png")
except Exception as ex:
print(ex)
try:
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg"
with Image(filename=tmp_file_path + "[0]", resolution=150) as img:
with Image() as img:
img.options["pdf:use-cropbox"] = "true"
img.read(filename=tmp_file_path + '[0]', resolution = 150)
img.compression_quality = 88
img.save(filename=os.path.join(tmp_dir, cover_file_name))
return cover_file_name
......@@ -210,24 +164,24 @@ def get_versions():
IVersion = ImageVersion.MAGICK_VERSION
WVersion = ImageVersion.VERSION
else:
IVersion = _(u'not installed')
WVersion = _(u'not installed')
IVersion = u'not installed'
WVersion = u'not installed'
if use_pdf_meta:
PVersion='v'+PyPdfVersion
else:
PVersion=_(u'not installed')
PVersion=u'not installed'
if lxmlversion:
XVersion = 'v'+'.'.join(map(str, lxmlversion))
else:
XVersion = _(u'not installed')
XVersion = u'not installed'
if use_PIL:
PILVersion = 'v' + PILversion
else:
PILVersion = _(u'not installed')
PILVersion = u'not installed'
if comic.use_comic_meta:
ComicVersion = _(u'installed')
ComicVersion = u'installed'
else:
ComicVersion = _(u'not installed')
ComicVersion = u'not installed'
return {'Image Magick': IVersion,
'PyPdf': PVersion,
'lxml':XVersion,
......
......@@ -38,7 +38,8 @@ from flask import render_template, request, redirect, send_from_directory, make_
from flask_babel import gettext as _
from flask_login import login_user, logout_user, login_required, current_user
from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql.expression import text, func, true, false, not_, and_
from sqlalchemy.sql.expression import text, func, true, false, not_, and_, \
exists
from werkzeug.exceptions import default_exceptions
from werkzeug.datastructures import Headers
from werkzeug.security import generate_password_hash, check_password_hash
......@@ -781,6 +782,12 @@ def get_tasks_status():
# ################################### Search functions ################################################################
@app.route("/reconnect")
def reconnect():
db.session.close()
db.engine.dispose()
db.setup_db()
return json.dumps({})
@web.route("/search", methods=["GET"])
@login_required_if_no_ano
......@@ -792,9 +799,9 @@ def search():
for element in entries:
ids.append(element.id)
searched_ids[current_user.id] = ids
return render_title_template('search.html', searchterm=term, entries=entries, page="search")
return render_title_template('search.html', searchterm=term, entries=entries, title=_(u"Search"), page="search")
else:
return render_title_template('search.html', searchterm="", page="search")
return render_title_template('search.html', searchterm="", title=_(u"Search"), page="search")
@web.route("/advanced_search", methods=['GET'])
......@@ -985,10 +992,11 @@ def get_cover(book_id):
return get_book_cover(book_id)
@web.route("/show/<book_id>/<book_format>")
@web.route("/show/<int:book_id>/<book_format>", defaults={'anyname': 'None'})
@web.route("/show/<int:book_id>/<book_format>/<anyname>")
@login_required_if_no_ano
@viewer_required
def serve_book(book_id, book_format):
def serve_book(book_id, book_format, anyname):
book_format = book_format.split(".")[0]
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format.upper())\
......@@ -1003,11 +1011,11 @@ def serve_book(book_id, book_format):
return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format)
@web.route("/download/<int:book_id>/<book_format>", defaults={'anyname': 'None'})
@web.route("/download/<int:book_id>/<book_format>/<anyname>")
# @web.route("/download/<int:book_id>/<book_format>", defaults={'anyname': 'None'})
@web.route("/download/<int:book_id>/<book_format>")
@login_required_if_no_ano
@download_required
def download_link(book_id, book_format, anyname):
def download_link(book_id, book_format):
return get_download_link(book_id, book_format)
......@@ -1254,6 +1262,21 @@ def profile():
return render_title_template("user_edit.html", content=current_user, downloads=downloads,
title=_(u"%(name)s's profile", name=current_user.nickname), page="me",
registered_oauth=oauth_check, oauth_status=oauth_status)
if "nickname" in to_save and to_save["nickname"] != current_user.nickname:
# Query User nickname, if not existing, change
if not ub.session.query(ub.User).filter(ub.User.nickname == to_save["nickname"]).scalar():
current_user.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=current_user,
downloads=downloads,
registered_oauth=oauth_check,
title=_(u"Edit User %(nick)s",
nick=current_user.nickname),
page="edituser")
current_user.email = to_save["email"]
if "show_random" in to_save and to_save["show_random"] == "on":
current_user.random_books = 1
......
This diff is collapsed.
This diff is collapsed.
# GDrive Integration
google-api-python-client==1.7.11
gevent==1.2.1
greenlet==0.4.12
httplib2==0.9.2
oauth2client==4.0.0
uritemplate==3.0.0
pyasn1-modules==0.0.8
pyasn1==0.1.9
PyDrive==1.3.1
PyYAML==3.12
gevent>=1.2.1
greenlet>=0.4.12
httplib2>=0.9.2
oauth2client>=4.0.0
uritemplate>=3.0.0
pyasn1-modules>=0.0.8
pyasn1>=0.1.9
PyDrive>=1.3.1
PyYAML>=3.12
rsa==3.4.2
six==1.10.0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment